diff --git a/modules/brother/README.md b/modules/brother/README.md index b4a3902a..2b2e661c 100644 --- a/modules/brother/README.md +++ b/modules/brother/README.md @@ -1,4 +1,11 @@ -# brother — scan a document, crop it, file it, say so +# brother — the Brother multifunction device: scan, printer state, admin pages + +One module for one device (novox/hq ADR 0467, issue 1453): the scanner and the printer are one machine on the +network, so one module serves both. It claims two seats, each held once for the mesh: `scanner` (verbs `scan`, +`status`, `list`) and `printer` (verbs `status`, `settings`; `restart` follows in a later round). It replaces the +`scan` module of issue 1440 (ADR 0464), which ran on no node and is gone from the catalogue. + +## The scanner seat Built for issue 1440 as novox/hq ADR 0464 decides. It scans the glass of a network scanner (any scanner that speaks eSCL, also called AirScan or Mopria), cuts the document out of the lid's white, files it as a one-page PDF @@ -26,28 +33,54 @@ A scanner that refuses or gives no page files nothing and says nothing on the bu |---|---|---| | `scanner` | the scanner's eSCL address, such as `http://192.0.2.10/eSCL` (required, trusted) | none | | `scan-folder` | the absolute path the PDFs are filed in (required, trusted) | none | +| `printer` | the printer's host, such as `192.0.2.10`, for its admin page and IPP (required, trusted) | none | +| `printer-certificate` | the SHA-256 fingerprint of the one certificate the admin page may serve, `AA:BB:...` (required, trusted) | none | | `resolution` | dpi asked: 100, 200, 300 or 600 | 300 | | `color-mode` | `color` or `gray` | `color` | | `crop` | cut the document out of the lid's white | true | The values reach the code in one file, `settings.json` in the module's state directory, which the mesh writes -from the config; the code reads it at every call. With `scanner` or `scan-folder` unset, every tool says which and -writes and asks nothing. +from the config; the code reads it at every call. With `scanner` or `scan-folder` unset, every scanner verb says which and +writes and asks nothing; `printer` and `printer-certificate` are the printer verbs' to say. The scan folder is not a manifest resource: a directory's path cannot take a config value (issue 1441), so the code makes it (mode 0755) when it files the first scan. ## Tools -| tool | does | +| verb (tool) | does | |---|---| | `scanner.scan` | scans, crops, files and emits; answers the event's body | | `scanner.status` | whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is `answers: false` with why. This is how its health is read: the module has no long-running resource to declare a health check on | | `scanner.list` | the newest filed scans, newest first (`limit`, default 20). Read-only | +## The printer seat + +| verb (tool) | does | +|---|---| +| `printer.status` | without a login: over IPP (`Get-Printer-Attributes`, encoded by hand, on port 631) the state (idle, processing, stopped), the state reasons (such as `toner-low-warning`) and the markers' names, levels (percent; below 0 is unknown) and colours; over eSCL the scanner's state and its feeder's. A part that does not answer is `answers: false` with why. Needs `printer` and `scanner` | +| `printer.settings` | logs in, reads one admin page, logs out. Argument `page`: a path on the admin site, starting with `/` and ending in `.html` (default `/general/status.html`). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else | + +### Trust and the password + +The admin page is HTTPS with a self-signed certificate. The module verifies no chain: it accepts the one certificate +whose SHA-256 fingerprint is `printer-certificate` and refuses any other, in the handshake, so no password is sent +to it; the error names both fingerprints. Get the fingerprint from the device's certificate page, and give it as 32 +upper-case hex pairs joined by colons. + +The admin password is the module's own secret `admin-password` (ADR 0467 clause 4; ADR 0283): the operator gives it +at their desk, the mesh never generates or rotates it, and it reaches the code as the file `BROTHER_ADMIN_PASSWORD` +names (`${dir:state}/admin-password.secret`). While it is not given, `printer.settings` says that the secret is not +given and contacts nothing. + +The login reads the form each time: the device generates the password field's name, so the code takes the name from +the form, posts it with the form's hidden fields, and keeps the session cookie. A login is failed when the answer is +the login page again. The logout follows a logout link or form on the page; a page with none is said in the answer +(`logout`), and the session is left to expire on the device. + ## The event -`scan.added`, on the wire `mesh.mod.brother.event.scan.added`; a consumer declares `brother.scan.added`. Its body: +`scan.added`, said by this module, on the wire `mesh.mod.brother.event.scan.added`; a consumer declares `brother.scan.added`. Its body: `path`, `node`, `bytes`, `sha256`, `width-mm`, `height-mm`, `resolution` (dpi, as the scanner gave it), `color-mode`, `cropped`, `scanned-at` (RFC 3339, UTC). @@ -63,4 +96,7 @@ Each directory or archive this module declares says how it treats a file it did ## Tests The unit tests use the captured answers of a real scanner (`cmd/brother/testdata/escl`, captured 2026-10-20) served -by a test HTTP server; no test reaches a scanner. +by a test HTTP server, the captured IPP exchange (`testdata/ipp`) replayed, and the captured login page +(`testdata/admin/login-page.html`) served by a test TLS server with its own certificate. The pages behind the login +are synthetic (`testdata/admin/synthetic-*.html`) until round 2 captures the real ones. No test reaches the real +device.