# Print Platform Pro Browser (Linux)

The Linux build of Print Platform Pro's browser-print agent. Runs
on Linux (tested on Fedora 40+ and Ubuntu 24.04+) and exposes the
**same HTTP/HTTPS protocol on the same ports** (`localhost:9100`
and `localhost:9101`) as the Mac and Windows browser-print agents,
so any web app already wired for those agents works unchanged —
including the `/available`, `/default`, `/write`, and `/read`
endpoints that the JavaScript / TypeScript SDKs call.

> Status: **2.0.0 — Print Platform Pro Browser branding, AppStream
> metainfo + branded hicolor icons, proprietary EULA.** Renamed
> from `zebra-browser-print-linux` (v1.0.x); the old package is
> Obsoleted/Replaced so `dnf/apt upgrade` swaps it in cleanly.
>
> Project website: https://printplatformpro.com

## What it does

- Auto-discovers Zebra USB printers via `/dev/usb/lpN` + sysfs.
- Listens on `127.0.0.1:9100` (HTTP) and `127.0.0.1:9101` (HTTPS).
- Generates and persists a self-signed cert on first run
  (under `~/.config/pppro-browser-linux/`).
- Implements the four Browser Print routes the SDKs depend on:
  - `GET /available` — `{ "printer": [Device, ...] }`
  - `GET /default` — single `Device`
  - `POST /write` — `{ "device": Device, "data": "<ZPL>" }`
  - `POST /read` — `{ "device": Device }` → `text/plain` body
- **First-run origin approval flow** (see below) — same UX as the
  official Mac/Windows agent. New origins get a 403 with an
  `X-ZBPL-Approve-URL` header pointing at an in-browser approval
  prompt; once the user clicks Approve, the origin is persisted to
  `~/.config/pppro-browser-linux/allowed_origins.json` and
  future requests pass without prompting.
- Auto-starts as a **systemd user service** on login (no root
  needed at runtime; root only for the one-time udev rule install).
- **System tray icon** (libayatana-appindicator) with a small menu —
  Settings, Approved origins, Quit. Picking Settings or Origins
  opens a **native WebKit window** (not a browser tab) — the agent
  re-execs itself with `--ui-url <local URL>` so the webview gets
  its own main thread without fighting the tray's GTK loop.
- **Auto-start toggle** in the Settings window — flips the systemd
  user unit's enabled state via `systemctl --user enable/disable`.
  Default ON.
- CSRF + clickjacking hardened: `X-Frame-Options: DENY` on the
  prompt + settings, same-origin form-POST check, single-use
  5-minute approval tokens, no token = no prompt.

## Install

### From source (Fedora 38+ / Ubuntu 24.04+)

```bash
# Fedora
sudo dnf install -y golang libayatana-appindicator-gtk3-devel \
                    webkit2gtk4.1-devel libsoup3-devel

# Ubuntu 24.04+
sudo apt install -y golang libayatana-appindicator3-dev \
                    libwebkit2gtk-4.1-dev libsoup-3.0-dev

git clone https://github.com/UnifiedRetailGroup/zebra-print-platform-pro
cd zebra-print-platform-pro/pppro-browser-linux

make build         # produces ./pppro-browser-linux (uses bundled pkg-config shim)
make install       # binary + systemd user unit, enables and starts it
make udev-install  # one-time, sudo: USB read/write permission for the user
```

#### About the webkit2gtk-4.0 shim

`webview_go` hardcodes `pkg-config webkit2gtk-4.0`, but every
current distro ships only `webkit2gtk-4.1` (binary-compatible).
The repo includes [packaging/pkgconfig/webkit2gtk-4.0.pc](packaging/pkgconfig/webkit2gtk-4.0.pc),
a tiny `Requires: webkit2gtk-4.1` shim that the Makefile prepends
to `PKG_CONFIG_PATH`. This avoids forking webview_go.

### From RPM / DEB

```bash
# Fedora / RHEL
sudo dnf install ~/rpmbuild/RPMS/x86_64/pppro-browser-linux-*.rpm

# Debian / Ubuntu
sudo apt install ./pppro-browser-linux_*_amd64.deb
```

Postinst auto-enables the systemd user unit (`systemctl --global enable`)
and starts it immediately for any logged-in session, so the tray icon
appears within seconds — no manual `systemctl --user enable` needed.

### After install

1. Plug in a Zebra printer (re-plug if it was already connected when
   you ran `make udev-install`).
2. Open `https://localhost:9101/available` in your browser. **You
   will see a self-signed-certificate warning the first time** —
   click "Advanced" → "Proceed". Chrome remembers the trust decision
   for the lifetime of the cert.
3. The page should return `{"printer":[{"deviceType":"printer", ...}]}`.
   If the array is empty, run `~/.local/bin/pppro-browser-linux --list`
   to debug discovery.

## Verify

```bash
# Service status
systemctl --user status pppro-browser-linux.service

# Live logs
journalctl --user -u pppro-browser-linux.service -f

# Direct HTTP probe
curl http://localhost:9100/available
curl -k https://localhost:9101/available

# Send a test print (any plugged-in Zebra)
curl -X POST http://localhost:9100/write \
     -H 'Content-Type: application/json' \
     -d '{"device":{"uid":""},"data":"^XA^FO50,50^A0N,40,40^FDtest^FS^XZ"}'
```

## First-run approval flow

When a new origin (e.g. `https://bc2.app`) calls the agent for the
first time, the request returns:

```http
HTTP/1.1 403 Forbidden
X-ZBPL-Approve-URL: https://localhost:9101/__approve?token=<uuid>&return=https://bc2.app
Access-Control-Allow-Origin: https://bc2.app
Access-Control-Expose-Headers: X-ZBPL-Approve-URL
Content-Type: application/json

{"error":"origin not approved","origin":"https://bc2.app",
 "approveUrl":"https://localhost:9101/__approve?token=<uuid>&return=https://bc2.app"}
```

The frontend SDK reads `approveUrl` (either from the JSON body or
the header) and either:

- **Auto-opens** the URL in a new tab (works without popup-blocker
  trouble *if* the original `print()` call was triggered by a user
  gesture — same constraint as the official agent).
- **Surfaces** it as a "Click to authorise printing" link.

The user lands on the approval prompt, clicks **Approve**, and the
origin is persisted. The page redirects back to the `return=` URL
after 2 seconds. Subsequent requests from that origin pass the gate
silently.

### Approval security

Token is a 32-byte random hex string, single-use, 5-minute TTL,
in-memory only (forgotten on agent restart — by design: a stale
URL in someone's browser history shouldn't authorise a new origin
ten minutes later).

Cross-origin auto-submit attacks are blocked by:

1. **`X-Frame-Options: DENY`** + **`Content-Security-Policy: frame-ancestors 'none'`**
   on the prompt page — can't be iframed.
2. **Same-origin Origin-header check** on the form POST — must
   equal `https://localhost:9101`.
3. **Single-use token consume** — once approved, the token is
   gone; replays fail.

### Operator escape hatches

```bash
# Pre-seed via systemd unit (kiosk-style):
ZBPL_ALLOWED_ORIGINS=https://bc2.app,https://stage.bc2.app

# Or via CLI (persists to allowed_origins.json):
pppro-browser-linux --allow https://bc2.app
pppro-browser-linux --revoke https://bc2.app
pppro-browser-linux --list-origins

# In-browser audit page:
https://localhost:9101/__origins
```

`--list-origins` shows source per row (`prompt` / `cli` / `env`)
so you can audit how each origin got onto the list.

## Why this exists

Web apps that print Zebra labels typically embed Zebra's
[`BrowserPrint-3.0.216.min.js`](https://www.zebra.com/us/en/support-downloads/printer-software/printer-software-developer-tools.html)
SDK, which calls `https://localhost:9101/write` to send raw ZPL to
the local printer via the official agent. On Linux there's no
official agent, so the SDK can't reach a printer — even though
Chrome on Linux is otherwise fully capable.

This agent fills that gap. The web app stays untouched; the user
installs this package once and printing Just Works.

## Architecture

```
Browser tab (Chrome on Linux)
       │  fetch('https://localhost:9101/write', ...)
       ▼
pppro-browser-linux  ─── self-signed TLS cert (per-user)
       │  open(/dev/usb/lp0, O_WRONLY).Write(ZPL)
       ▼
usblp kernel module
       │  USB bulk-out
       ▼
Zebra ZD230 / ZD421 / ...
```

No CGo, no libusb dependency — the kernel's `usblp` driver does the
hard part. Single static Go binary; ships as a ~10 MB RPM.

## File layout

```
pppro-browser-linux/
├── cmd/pppro-browser-linux/main.go    entrypoint
├── internal/
│   ├── api/                                  Browser Print wire types
│   ├── server/                               HTTP/HTTPS listeners + routes
│   ├── usb/                                  /dev/usb/lpN discovery + I/O
│   ├── tlscert/                              self-signed cert generation
│   └── config/                               XDG paths + env overrides
├── packaging/
│   ├── systemd/                              user .service unit
│   ├── udev/                                 USB permission rule
│   └── rpm/                                  RPM spec
├── scripts/build-rpm.sh                      stage + rpmbuild
├── Makefile                                  build / install / uninstall / rpm
└── README.md
```

## System tray + native windows

Once installed, the agent shows a small **Z** icon in your system
tray. Right-click for the menu:

- **Open settings…** — opens the settings page in a **native
  WebKit window** (not a browser tab). The window surfaces the
  current status (listeners, allowlist size, cert path, autostart
  state) and lets you toggle "Start automatically on login"
  (default on).
- **Approved origins…** — same shape, native window for the
  allowlist audit page.
- **Quit** — graceful shutdown of the agent.

### Why WebKit windows, not browser tabs

The native window comes from [webview_go](https://github.com/webview/webview_go)
wrapping WebKitGTK. Each window runs in a short-lived **child
process**: the tray re-execs the agent's own binary with
`--ui-url <local URL>` so that child gets its own main thread for
the GTK loop. Both GTK loops would deadlock if forced into the
same process. The child exits when the user closes the window.

Bonus from this design: a webview crash can't take the agent down.

The auto-start toggle controls the `systemctl --user is-enabled`
state of the unit. When OFF, the agent stops at logout and won't
come back at the next login until you re-enable. The currently-
running process keeps running until you Quit.

If you're running the agent without a graphical session
(SSH session, headless kiosk), or pass `--no-tray`, the tray code
short-circuits and the agent runs as a pure HTTP/HTTPS daemon.
Use the CLI flags (`--list-origins`, `--allow`, `--revoke`,
`--add-printer`, `--remove-printer`) to manage state, or hit
`/__origins` JSON and `/__license` JSON over HTTP directly.

## Configuration

All knobs are environment variables (set them in the systemd unit's
`Environment=` lines). Defaults work for the URG warehouse
deployment.

| Variable | Default | Purpose |
|---|---|---|
| `ZBPL_HTTP_ADDR` | `127.0.0.1:9100` | HTTP listener |
| `ZBPL_HTTPS_ADDR` | `127.0.0.1:9101` | HTTPS listener |
| `ZBPL_CERT_PATH` | `$XDG_CONFIG_HOME/pppro-browser-linux/tls.crt` | TLS cert |
| `ZBPL_KEY_PATH` | `$XDG_CONFIG_HOME/pppro-browser-linux/tls.key` | TLS key |
| `ZBPL_LOG_PATH` | (stderr/journald) | Optional file path for log mirror |
| `ZBPL_ALLOWLIST_PATH` | `$XDG_CONFIG_HOME/pppro-browser-linux/allowed_origins.json` | Persistent approved-origins file |
| `ZBPL_ALLOWED_ORIGINS` | (none) | Pre-approved origins, comma- or whitespace-separated; treated as if approved via the prompt but NOT persisted to the JSON file (managed-kiosk pattern) |
| `ZBPL_PRINTER_INFO_PATH` | `$XDG_CONFIG_HOME/pppro-browser-linux/printer-info.json` | Per-printer dims + quality cache (alongside `offsets.json`) — populated on Calibrate, on Quality SET, or via `?probe=true`; lets the UI read dims/quality without hitting USB on every view |

## Compatibility

The wire protocol mirrors the public Browser Print API documented
in Zebra's *Browser Print User Guide v1.3*. The four endpoints
(`/available`, `/default`, `/write`, `/read`), their JSON shapes,
and the localhost dual-port (HTTP + HTTPS) layout match. Web SDKs
that work against the official agent on Mac/Windows work here too.

Tested with:

- Zebra ZD230 (USB) on Fedora 43 with `usblp` module.
- Chromium 130 + Chrome 134 (cross-origin / CORS path).

## Licence

Proprietary. See [LICENSE](LICENSE) for the EULA. A valid Print
Platform Pro subscription is required for production use; trial
licences are available at https://printplatformpro.com.

## Roadmap

- [x] First-run origin approval prompt (v0.2.0)
- [x] Persistent allowlist with CLI mutate (`--allow` / `--revoke` / `--list-origins`)
- [x] Pre-seed via env var (`ZBPL_ALLOWED_ORIGINS`)
- [x] **System tray icon + settings page + auto-start toggle (v0.3.0)**
- [ ] Connected-printer status surfaced in the tray menu (live count + last-error).
- [ ] Network-attached printer support (TCP 9100 forward to the
  device's print port; same wire shape).
- [ ] Multi-printer routing (the official agent picks based on
  `device.uid` already; we mirror that for USB but haven't tested
  with > 1 connected).
- [ ] Debian / Ubuntu `.deb` packaging.
- [ ] CI build matrix (Fedora / RHEL / Debian / Arch).
