Skip to content

Put /upload and /delete behind a per-session token - #74

Open
chongfun wants to merge 1 commit into
mainfrom
opt/upload-session-token
Open

Put /upload and /delete behind a per-session token#74
chongfun wants to merge 1 commit into
mainfrom
opt/upload-session-token

Conversation

@chongfun

@chongfun chongfun commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Pull Request

Verification

Before requesting review, please confirm you have run the appropriate checks:

  • tools/check.sh fmt passed
  • tools/check.sh fast passed (host Clippy and tests)
  • X4 and X3 visual verification passed (if applicable)
  • tools/check.sh emulator passed (emulator tests and goldens)
  • tools/check.sh firmware passed (firmware Clippy and release builds)
  • tools/check.sh all passed (required before the pull request is considered ready)

Skipped checks

If you skipped any checks, please list them below and explain why:

Description

/upload, /delete and /list answered anything on the LAN. While a wireless session was up, any device on the same network could add or delete books on the card, with no step in between. This closes that.

A token is minted per serving session from the hardware RNG — the same rejection-sampled draw and the same unambiguous alphabet as the portal PSK — and every endpoint moves behind it as the first path segment: /<token>, /<token>/list, /<token>/upload?name=.... It is never persisted; the reset at session end retires it.

The obvious cheap version — embed the token in the page served at / — protects nothing, because that page is exactly what an unauthorized client fetches first. The secret has to be something the user has and a stray LAN client does not, so it goes on the device: the serving screen now renders a QR of http://<ip>/<token>, with the same address printed under it for phones that cannot scan. That is why the alphabet excludes look-alike glyphs — someone is going to read six characters off an e-ink panel.

The page itself stays a build-time constant. Its fetches derive the token from location.pathname, which the browser already has, so the HTML never carries the secret and templating it per request never has to exist.

In app_core::UploadToken::strip_path_prefix, not in the server loop. It is parsing, it decides who may write to the card, and fw has no host tests to hold it — the same argument that moved the reader cache into its own crate. The server calls it and re-slices past the returned prefix, so the dispatch below sees exactly the paths it always did, query strings and offsets included.

A refused request gets 404 Not Found, the same answer a nonexistent path gets, before any upload machinery is touched — a wrong token must not open a storage session, and must not tell the caller it was close. Comparison is over the full length rather than short-circuiting.

the_gate_admits_exactly_the_urls_the_screen_shows pins both directions against the concrete URLs: the four the page produces, and eleven a LAN client would reach for unaided — bare /list, /upload?name=..., wrong case, truncated and overlong segments, and the token appearing as a later segment (/upload/k7mfqx), which a check that scanned instead of anchoring would wrongly admit. It also pins that stripping leaves the query string intact, which is what keeps the delete and upload handlers' offsets valid.

Mutation-checked: dropping the leading-/ anchor and loosening the length comparison to > are both caught.

Token minting is pinned separately — every alphabet byte must be mintable, and nothing a URL or QR payload would need escaped may be.

Flash +2,140 B text; .data and .bss unchanged. sync-flow goldens re-blessed on both boards, deliberately: the serving screen gained the QR and the address line. Reviewed the rendered frames rather than blessing blind.

Device verification is still owed: phone scans the QR and reaches the shelf; upload and delete work through it; the same endpoints without the token return 404 from another machine on the LAN; and the printed address is legible enough to type off the panel, which is the one thing no host test can answer.

Summary by CodeRabbit

  • New Features

    • Added secure, per-session tokens for LAN uploads, downloads, file listing, and deletion.
    • Invalid or missing tokens are rejected before requests are processed.
    • Wireless sharing QR codes and displayed URLs now include the session token.
    • Emulator sharing supports a standard demonstration token.
  • Bug Fixes

    • Preserved upload authorization details across synchronization and serving status updates.
    • Improved protection against unauthorized access to shared files.

`/upload`, `/delete` and `/list` answered anything on the LAN. While a
wireless session was up, any device on the same network could add or delete
books on the card, with no step in between. This closes that.

A token is minted per serving session from the hardware RNG — the same
rejection-sampled draw and the same unambiguous alphabet as the portal PSK —
and every endpoint moves behind it as the first path segment: `/<token>`,
`/<token>/list`, `/<token>/upload?name=...`. It is never persisted; the
reset at session end retires it.

The obvious cheap version — embed the token in the page served at `/` —
protects nothing, because that page is exactly what an unauthorized client
fetches first. The secret has to be something the user has and a stray LAN
client does not, so it goes on the device: the serving screen now renders a
QR of `http://<ip>/<token>`, with the same address printed under it for
phones that cannot scan. That is why the alphabet excludes look-alike glyphs
— someone is going to read six characters off an e-ink panel.

The page itself stays a build-time constant. Its fetches derive the token
from `location.pathname`, which the browser already has, so the HTML never
carries the secret and templating it per request never has to exist.

In `app_core::UploadToken::strip_path_prefix`, not in the server loop. It is
parsing, it decides who may write to the card, and `fw` has no host tests to
hold it — the same argument that moved the reader cache into its own crate.
The server calls it and re-slices past the returned prefix, so the dispatch
below sees exactly the paths it always did, query strings and offsets
included.

A refused request gets `404 Not Found`, the same answer a nonexistent path
gets, before any upload machinery is touched — a wrong token must not open a
storage session, and must not tell the caller it was close. Comparison is
over the full length rather than short-circuiting.

`the_gate_admits_exactly_the_urls_the_screen_shows` pins both directions
against the concrete URLs: the four the page produces, and eleven a LAN
client would reach for unaided — bare `/list`, `/upload?name=...`, wrong
case, truncated and overlong segments, and the token appearing as a *later*
segment (`/upload/k7mfqx`), which a check that scanned instead of anchoring
would wrongly admit. It also pins that stripping leaves the query string
intact, which is what keeps the delete and upload handlers' offsets valid.

Mutation-checked: dropping the leading-`/` anchor and loosening the length
comparison to `>` are both caught.

Token minting is pinned separately — every alphabet byte must be mintable,
and nothing a URL or QR payload would need escaped may be.

Flash +2,140 B text; `.data` and `.bss` unchanged. `sync-flow` goldens
re-blessed on both boards, deliberately: the serving screen gained the QR
and the address line. Reviewed the rendered frames rather than blessing
blind.

**Device verification is still owed:** phone scans the QR and reaches the
shelf; upload and delete work through it; the same endpoints without the
token return 404 from another machine on the LAN; and the printed address
is legible enough to type off the panel, which is the one thing no host
test can answer.
@chongfun chongfun self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds six-character per-session upload tokens. Firmware validates token-prefixed upload, delete, and list routes. Sync state, emulator flows, and UI rendering now carry and display the token through authenticated URLs and QR codes.

Changes

Upload Token Authentication

Layer / File(s) Summary
Token contract and serving state
app-core/src/lib.rs
UploadToken validates URL-safe tokens, supports constant-time matching and path-prefix extraction, and propagates through SyncEvent::Serving and SyncStatus::Serving.
Firmware token generation and request authorization
fw/src/tasks/wifi.rs
The Wi-Fi task generates a session token. The upload server rejects missing or invalid token prefixes with 404 Not Found and dispatches valid requests to existing endpoints.
Emulator token wiring
tools/emulator/src/scenario.rs, tools/web-emulator/src/lib.rs
Emulator serving events use UploadToken::EMULATOR_DEMO. Status matching accepts the expanded serving payload.
Authenticated URL encoding and serving display
ui/src/lib.rs, ui/src/app_render.rs, ui/src/join_qr.rs, ui/src/render.rs
The UI preserves the token, encodes http://<ip>/<token> in QR codes, and displays the authenticated upload URL with instructions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WiFiTask
  participant SyncState
  participant UI
  participant Browser
  participant UploadServer
  WiFiTask->>WiFiTask: Generate session UploadToken
  WiFiTask->>SyncState: Emit Serving(IP, UploadToken)
  SyncState->>UI: Preserve serving token
  UI->>Browser: Present token-prefixed upload URL
  Browser->>UploadServer: Request authenticated route
  UploadServer->>UploadServer: Validate and strip token prefix
  UploadServer-->>Browser: Serve valid endpoint or return 404
Loading

Possibly related PRs

Suggested reviewers: jon-vii

Poem

A rabbit hops with token bright,
Six safe marks unlock the site.
The QR moon guides every ear,
Invalid paths vanish near.
Sync and server share the key,
Uploads flow securely.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: protecting upload and delete endpoints with a per-session token.
Description check ✅ Passed The description is detailed, follows the template, documents verification results, and clearly identifies outstanding device verification.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opt/upload-session-token

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant