Repository navigation
send WebSocketConfig headers, subprotocols and user agent in the upgrade request (#165) - #189
Conversation
…ade request (LabOverWire#165) WebSocketTransport::connect built the upgrade request from a fixed set of headers and always offered Sec-WebSocket-Protocol: mqtt, so with_header, with_subprotocol(s) and with_user_agent never reached the server. Request construction moves to WebSocketConfig::build_handshake_request, which starts from tungstenite's into_client_request (fixing a Host header that dropped non-default ports) and adds the configured subprotocols, user agent and custom headers. Invalid, reserved or case-duplicated headers and non-token subprotocols are rejected before dialing. WebSocketConfig's Debug no longer prints header values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LabOverWire#165) Content-Length, Transfer-Encoding and Sec-WebSocket-Accept are now rejected as custom handshake headers alongside the existing reserved names. Custom headers are validated and sent in name order, so the request and the reported error do not depend on HashMap iteration. The integration test checks Host against the exact bound address, and the CHANGELOG calls out the changes that can affect existing configurations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fabracht
left a comment
There was a problem hiding this comment.
Thanks, this looks good. One change I'd like before merging:
With with_subprotocol("mqttv5.0") the request now carries only mqttv5.0, and the broker in this repo answers without a subprotocol, so the client fails with SubProtocol error: Server sent no subprotocol. On main the same config connects because mqtt was always sent. MQTT-6.0.0-3 also says the client must offer mqtt. Can you always include it, appended if the caller didn't list it? The server still picks one value, so a broker that wants mqttv5.0 can still select it. The CHANGELOG note about adding mqtt to the list can go then.
Two minor ones:
with_subprotocols(&["mqtt", "mqtt"])sendsmqtt, mqtt. RFC 6455 §4.1 requires unique values.TE,Keep-Alive,Proxy-Connection,TrailerandExpectget through as custom headers. They should go in the same list asConnectionandTransfer-Encoding.
Returning http::Request from build_handshake_request is fine, the request modifier will need http types anyway. Leave the [Unreleased] heading as is, I'll move it when I release 0.46.0.
We found these with throwaway tests, so please double check them on your side before changing anything:
- Subprotocol: start an
MqttBrokerwith a websocket listener (BrokerConfig::with_websocket, as intests/broker_websocket_integration.rs) and connect aWebSocketTransportwithWebSocketConfig::new(url)?.with_subprotocol("mqttv5.0"). It connects on main and fails on this branch.with_subprotocols(&["mqttv5.0", "mqtt"])connects on both. - The other two only need
build_handshake_request():with_subprotocols(&["mqtt", "mqtt"])givesSec-WebSocket-Protocol: mqtt, mqtt, andwith_header("TE", "trailers")builds without an error.
…headers (LabOverWire#165) Review follow-up. The configured subprotocols are now followed by "mqtt" when the list does not include it, as MQTT-6.0.0-3 requires: the broker in this crate only selects a subprotocol the client offers, so a config preferring mqttv5.0 failed with "Server sent no subprotocol". A subprotocol listed twice is rejected (RFC 6455 §4.1), and TE, Trailer, Keep-Alive, Proxy-Connection and Expect join the reserved headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
|
Thanks for the review. I reproduced all three points against
I merged Per your answer on #165, |
# Conflicts: # CHANGELOG.md
fabracht
left a comment
There was a problem hiding this comment.
Thanks, Bryan, all three are fixed. I checked them here too: with_subprotocol("mqttv5.0") now connects to our broker and fails again if I take out the mqtt append, with_subprotocols(&["mqtt", "mqtt"]) is rejected, and TE, Trailer, Keep-Alive, Proxy-Connection and Expect all fail as custom headers.
Main moved this week (0.47.1 to 0.47.3), so the CHANGELOG conflicted again. I merged main into your branch and kept your entry under [Unreleased], so there's nothing left for you to do here. Merging now.
Fixes #165. (I filed the issue as @NuSkooler; this is my work account.)
WebSocketTransport::connectbuilt the upgrade request from a fixed set of headers, sowith_header,with_subprotocol(s)andwith_user_agentnever reached the server. This PR sends them, and adds tests that check the request a server actually receives.Changes
WebSocketConfig::build_handshake_request(public, so callers can check a configuration without connecting). It starts from tungstenite'sinto_client_request, then adds:Sec-WebSocket-Protocol: the configured subprotocols in order, followed bymqttwhen the list doesn't include it (MQTT-6.0.0-3);User-Agent, when set. The default is nowmqtt5/<crate version>, replacing the stalemqtt-v5/0.4.0, since it now actually goes on the wire;Hostnow includes a non-default port. The old code usedhost_str(), sows://broker:8080sentHost: broker. Starting frominto_client_requestalso strips userinfo and keeps IPv6 brackets.with_headerkeeps its infallible signature. Errors areMqttError::Configurationand name the header. A request is rejected when:Host,Connection,Upgrade,Sec-WebSocket-Version,Sec-WebSocket-Key). tungstenite would already refuse a duplicate of these, but only at connect time, with a less clear error;Sec-WebSocket-Extensions(the transport negotiates no extensions and could not speak one a server accepted) orSec-WebSocket-Accept(it belongs in the server's response);Content-Length,Transfer-EncodingorTrailer. The upgrade request has no body, and a server told otherwise, e.g.Transfer-Encoding: chunked, would wait for one;TE,Keep-Alive,Proxy-ConnectionorExpect;Sec-WebSocket-ProtocolorUser-Agentis set as a custom header. The error points to the dedicated setter, so each value has one source;headersis aHashMap<String, String>, so which copy won would otherwise depend on iteration order;"a, b"would otherwise silently become two subprotocols.HashMapiteration order.WebSocketConfighas a hand-writtenDebugthat lists header names but not values, since values often carry credentials.subprotocol()'s doc comment said "negotiated", but it returns the first configured subprotocol. I've corrected the comment and left the behaviour alone (see below). The subprotocol setters now cite MQTT-6.0.0-3.Behavior changes for existing users
Because the configuration now reaches the wire, some existing setups will behave differently. The CHANGELOG has a "may affect existing configurations" entry covering both:
connectfail.MqttClient's, now sends aUser-Agentheader.Subprotocols and MQTT-6.0.0-3
mqttis always offered: it's appended after the configured subprotocols when the list doesn't include it. The server still selects one value, so a broker that wantsmqttv5.0can still choose it frommqttv5.0, mqtt, while one that only selectsmqtt, like this crate's, still connects. A subprotocol listed twice is rejected.Tests
build_handshake_request: defaults, applied configuration, the empty-list fallback, unset user agent,Host(default port, userinfo, IPv6),mqttappended after other subprotocols, duplicate subprotocols rejected, every rejection case, deterministic header order (checked across freshly seeded maps; it fails without the sort), andDebugredaction.prop_websocket_config_with_optionsnow also checks the built request. It expects an error exactly when the generated headers include a restricted or case-duplicated name, or a subprotocol repeats, and otherwise expects the configured subprotocols followed bymqttwhen absent. A newprop_handshake_request_rejects_restricted_headerscovers those names in both cases.tests/websocket_handshake_integration.rsconnects the transport to a loopbackaccept_hdr_asyncserver and checks the upgrade request the server received. All three tests fail againstmain:Authorizationarrives asNone;User-Agentarrives asNone. This test also checksHostagainst the exact address the server bound;connectdials and is refused instead of rejecting the config.preferring_another_subprotocol_still_connects_to_the_brokerconnectswith_subprotocol("mqttv5.0")to this crate'sMqttBrokerWebSocket listener (bound to port 0). Without themqttfallback it fails withServer sent no subprotocol.Locally, fmt, clippy (workspace and wasm),
cargo doc -D warningsand the full test suite pass on the pinned 1.98.0 toolchain, apart from tests that fail on this machine only, identically onmain: another service holds 127.0.0.1:20000, the first portTestBrokerhands out, and a local broker holds 127.0.0.1:1883, which the external-broker tests expect to be empty or theirs. For a clean run, upstream's Rust CI and Conformance Suite workflows run on the fork and pass: bryan-ashby#1.Not in this PR
MqttClientsupport.ClientInner::connect_websocket{,_secure}buildsWebSocketConfig::new(url)from the URL alone, so headers set this way only help callers that driveWebSocketTransportdirectly.ConnectOptions::with_websocket_config, as agreed on WebSocket transport ignoresWebSocketConfigheaders, subprotocols, and user_agent #165, is in add ConnectOptions::with_websocket_config for MqttClient (#165) #210; the request modifier will follow separately.Debugand logs. Aws://user:pass@hostURL still shows its password inWebSocketConfig'sDebugand inconnect's tracing span, exactly as before; only header values are newly redacted.subprotocol()could return the server's choice once connected. That changes what two existing tests rely on, so I've left it out; I can add it here or separately.WebSocketConfig::tls_config(custom CA, client certificate, ALPN) #190, fixed in use WebSocketConfig's TLS configuration for wss:// connections (#190) #191.The CHANGELOG entry stays under
[Unreleased], as you asked.🤖 Generated with Claude Code