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:
@@ -265,7 +265,6 @@ fn a_hello_without_a_stream_base_url_omits_it() {
|
||||
let json = serde_json::to_string(&ClientMessage::Hello {
|
||||
gateway_id: "gw-1".into(),
|
||||
farm_id: "farm-1".into(),
|
||||
token: "t".into(),
|
||||
stream_base_url: None,
|
||||
})
|
||||
.expect("should serialize");
|
||||
@@ -279,9 +278,24 @@ fn a_hello_carries_the_stream_base_url_when_one_is_configured() {
|
||||
let json = serde_json::to_string(&ClientMessage::Hello {
|
||||
gateway_id: "gw-1".into(),
|
||||
farm_id: "farm-1".into(),
|
||||
token: "t".into(),
|
||||
stream_base_url: Some("http://gateway.local:1984".into()),
|
||||
})
|
||||
.expect("should serialize");
|
||||
assert!(json.contains(r#""stream_base_url":"http://gateway.local:1984""#), "got:\n {json}");
|
||||
}
|
||||
|
||||
/// The credential must not be in the handshake message. It travels in an
|
||||
/// `Authorization` header on the upgrade instead, and a `token` field here
|
||||
/// would mean the backend parsing a message from a peer that has proved
|
||||
/// nothing - which is what this used to do.
|
||||
#[test]
|
||||
fn a_hello_carries_no_credential() {
|
||||
let json = serde_json::to_string(&ClientMessage::Hello {
|
||||
gateway_id: "gw-1".into(),
|
||||
farm_id: "farm-1".into(),
|
||||
stream_base_url: None,
|
||||
})
|
||||
.expect("should serialize");
|
||||
assert!(!json.contains("token"), "the hello must carry no token, got:\n {json}");
|
||||
assert_eq!(json, r#"{"type":"hello","gateway_id":"gw-1","farm_id":"farm-1"}"#);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user