Send the edge token on the upgrade, not in the hello

The uplink put its edge JWT in a `token` field on the `hello` message.
It now goes in an `Authorization: Bearer` header on the WebSocket upgrade
request, which the backend verifies before the socket exists - so a bad
token is an HTTP 401 here rather than a connection that opens and is then
closed, and the credential stays out of the URL, where it would land in
every access and proxy log on the way. `Authorization` is the header
those tools already know to redact.

`farm_id` stays on the hello even though the token carries the same
claim: the backend checks the two against each other rather than
trusting the message, so sending it is how a configuration mismatch gets
caught instead of silently working against the wrong farm.

The cost is that tokio-tungstenite no longer derives the handshake from
a URL for us - a request we build ourselves needs its own `Host`,
`Connection`, `Upgrade`, `Sec-WebSocket-Version` and
`Sec-WebSocket-Key`, and a handshake missing any of them is rejected
rather than ignored. `host_header` errors rather than guessing if the URL
has no authority, because a wrong `Host` fails the handshake far less
legibly than this does.

Verified against a real backend rather than assumed: a valid token
handshakes to HTTP 101 and the server reads the farm from the token, and
a forged one is refused with 401 before any socket exists.

64 tests, clippy unchanged at the two deliberate variant-size warnings,
`cargo deny check licenses` ok.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 14:00:05 +00:00
co-authored by Claude Opus 5
parent 9d046abf19
commit da6260fa9c
4 changed files with 73 additions and 5 deletions
+11 -1
View File
@@ -6,10 +6,20 @@ use crate::printer::PrintControl;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
/// The handshake, carrying no credential.
///
/// The edge token goes in an `Authorization: Bearer` header on the
/// upgrade request instead - see `connect_and_serve`. It used to be a
/// `token` field right here, which worked but meant the backend had an
/// open socket and a parsed message from a peer that had proved nothing.
///
/// `farm_id` stays, even though the token already carries the farm: the
/// backend checks the two against each other rather than trusting this
/// one, so sending it is how a configuration mismatch gets caught
/// instead of silently working against the wrong farm.
Hello {
gateway_id: String,
farm_id: String,
token: String,
/// Where a browser on this LAN can reach the gateway's camera
/// streams, if the operator has configured one. See
/// `Config::stream_base_url`.