feat(ws): RFC 6455 opening handshake — Conn::upgrade, 101 short-circuit (v0.4 chunk 1)
Design (as agreed in the v0.4 pass): - Dep #3 spent: sha1_smol 1.0.1 (zero transitive deps) instead of a vendored SHA-1 — user call: no hand-rolled crypto. Consequence noted in ROADMAP: the v0.6 wire-format JSON question now costs dep #4. - Base64 stays in-tree but ENCODE-ONLY (RFC 4648 vectors tested): it is an encoding, not crypto, and the decode direction (where parsing bugs live) is deliberately not implemented. - src/ws/{mod,handshake}.rs: validate() implements §4.2.1 (GET, 1.1, Host, upgrade/connection token lists case-insensitively, key shape = base64 of exactly 16 bytes, version 13). Version mismatch -> 426 + sec-websocket-version: 13 (§4.2.2); everything else -> 400. Rejected upgrades stay plain HTTP with keep-alive intact (tested: 400 then 101 on the same connection). - Conn grows pub upgrade: Option<WsUpgrade> (opaque marker; chunk 3 turns it into the duplex/handler handoff payload — shape deliberately uncommitted while the handler API is open). Conn::upgrade(self) is the commit point: 101 + accept key + marker + halt, or the rejection. - conn_actor: upgrade marker + status 101 -> write head, leave the HTTP loop. Until chunk 3 that drops the fd (clients see 101 then EOF); status check is defensive against a post-handler plug clobbering 101. - serialise_response: 1xx no longer get content-length injected (RFC 7230 §3.3.2). 204 left alone on purpose — separate conversation. Accept-key path pinned to the §1.3 worked example end-to-end (unit + wire). Suite: 34 unit + 33 integration + 2 doc.
This commit is contained in:
+47
@@ -308,6 +308,11 @@ pub struct Conn {
|
||||
pub params: Params,
|
||||
pub assigns: Assigns,
|
||||
pub halted: bool,
|
||||
|
||||
/// Set by [`Conn::upgrade`] on an accepted WebSocket handshake; the
|
||||
/// connection actor sees it (with status 101) and leaves the HTTP
|
||||
/// loop after writing the response. `None` for plain HTTP.
|
||||
pub upgrade: Option<crate::ws::WsUpgrade>,
|
||||
}
|
||||
|
||||
impl Conn {
|
||||
@@ -329,6 +334,8 @@ impl Conn {
|
||||
params: Params::new(),
|
||||
assigns: Assigns::new(),
|
||||
halted: false,
|
||||
|
||||
upgrade: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +365,46 @@ impl Conn {
|
||||
self.halted = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Accept a WebSocket upgrade on this request (RFC 6455 §4.2).
|
||||
///
|
||||
/// On a valid handshake: status `101`, the `upgrade`/`connection`/
|
||||
/// `sec-websocket-accept` response headers, the [`WsUpgrade`] marker
|
||||
/// the connection actor acts on, and the pipeline is halted.
|
||||
///
|
||||
/// On an invalid handshake: the appropriate rejection (`400`, or
|
||||
/// `426` + `sec-websocket-version: 13` for a version mismatch),
|
||||
/// empty body, pipeline halted; the connection stays plain HTTP with
|
||||
/// normal keep-alive semantics. Pre-check with
|
||||
/// [`ws::handshake::is_upgrade_request`](crate::ws::handshake::is_upgrade_request)
|
||||
/// to branch before committing.
|
||||
///
|
||||
/// [`WsUpgrade`]: crate::ws::WsUpgrade
|
||||
pub fn upgrade(mut self) -> Self {
|
||||
use crate::ws::{handshake, Rejection};
|
||||
match handshake::validate(&self) {
|
||||
Ok(accept) => {
|
||||
self.upgrade = Some(crate::ws::WsUpgrade { _priv: () });
|
||||
self.put_status(101)
|
||||
.put_header("upgrade", "websocket")
|
||||
.put_header("connection", "Upgrade")
|
||||
.put_header("sec-websocket-accept", accept)
|
||||
.put_body(RespBody::Empty)
|
||||
.halt()
|
||||
}
|
||||
Err(rej) => {
|
||||
let c = self
|
||||
.put_status(rej.status())
|
||||
.put_body(RespBody::Empty)
|
||||
.halt();
|
||||
if rej == Rejection::UnsupportedVersion {
|
||||
c.put_header("sec-websocket-version", "13")
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Conn {
|
||||
|
||||
Reference in New Issue
Block a user