Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / h1 / chunked / trailer-bare-lf.txt
Author[]Andy Green <andy@warmcat.com> 2026-10-05 20:22 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-06 03:36 UTC
Tree9e11e9af0a1a545c23befa4cb1404339d47067c2   Raw Patch
 
npro-ws: permessage-deflate
npro-ws: permessage-deflate

Phase 1f: RFC 7692, behind the pmd feature, with miniz_oxide where C has
zlib.

 - npro_ws::pmd negotiates.  A server takes the first permessage-deflate
   offer it can keep to (server_accept), with server_no_context_takeover
   and client_no_context_takeover as C takes them, and says what it took
   in the 101, which response_101 now carries extension lines for.  A
   client offers permessage-deflate (request_lines takes extensions to
   offer) and takes the server's answer if its parameters are RFC 7692's,
   each once, in C's ranges (client_accept); ClientKey::check returns
   what the server said of the extensions offered, with the subprotocol,
   and still refuses extensions none were offered for.  Params' fields
   are private, so nothing else makes them.

 - Ws::with_pmd inflates a message whose first frame has RSV1; RSV1 is
   refused anywhere else, as C's lws_ws_rsv_valid() refuses it.  The
   payload is unmasked into a hold of at most 1KiB, C's
   buf_rx_holding, and inflated into at most 1KiB a call, the drain
   budget: Ws::rx_pending says when there is more to give with no more
   input, as C's rx_draining_ext.  The trailer is put back at the
   message's end.  Data after a BFINAL that is not padding, data that
   does not inflate, and a message past its limit (C's 256MiB, or
   Params::with_max_message) drop the connection with no close, as C
   marks the socket unusable; what inflated before the failure is given
   first, so the application, and the check of text, see the stream in
   order however it was split.

 - What the application sends is deflated as it is pulled, into frames
   of at most 1KiB, RSV1 on the first, the flush's trailer removed (the
   last four bytes are kept back from each frame until the message ends,
   so it can be), an empty message after a flush C's one octet.  A client
   masks each frame with a mask drawn when it begins.  Each side drops
   its context per message as agreed.

Differences from C, each where C does something RFC 7692 does not have:
miniz_oxide's smaller windows refer further back than they say (zlib
finds distances too far back from any window under 14 bits), so npro's
deflater always has 32KiB, a server declines an offer asking for less and
tries the next (RFC 7692 7.1.2.1), where C takes it and leaves the
parameter out, and a client refuses a client_max_window_bits under 15,
which it did not offer, where C takes it; a client refuses the lws-private
options C takes from a server, and a parameter given twice.

The tests: RFC 7692 7.2.3's example frames inflate, whole and a byte at a
time; messages of every size about the 1KiB bounds, compressible and not,
go client to server and back, written four bytes at a time and read a
byte at a time, with and without context takeover; the zip bomb limit, to
the byte; data after BFINAL and data that does not inflate; RSV1 where it
may not be; and the negotiation both ways.  What npro deflates was
inflated by zlib, as C's peers would, by hand.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
diff --git a/crates/npro-test/tests/ws_client_replay.rs b/crates/npro-test/tests/ws_client_replay.rs index fa92cfe..e00d207 100644 --- a/crates/npro-test/tests/ws_client_replay.rs +++ b/crates/npro-test/tests/ws_client_replay.rs @@ -105,10 +105,10 @@ mod ws_client_replay { progress |= rx.consumed > 0; if rx.event == Some(H1Event::Response) { assert!(client.is_upgraded()); - let chosen = key - .check(client.status(), client.response(), Some(OFFERED)) + let checked = key + .check(client.status(), client.response(), Some(OFFERED), None) .unwrap(); - assert_eq!(chosen, Some(OFFERED)); + assert_eq!(checked.protocol, Some(OFFERED)); let mut ws = Ws::client(random.take().unwrap()); app.established(&mut ws); *conn = Conn::Ws(Box::new(ws)); @@ -145,7 +145,7 @@ mod ws_client_replay { let mut random = SeededRandom::new(seed.get()); let key = ClientKey::new(&mut random).unwrap(); let mut lines = [0u8; MAX_REQUEST_LINES + 32]; - let lines_len = key.request_lines(Some(OFFERED), &mut lines).unwrap(); + let lines_len = key.request_lines(Some(OFFERED), None, &mut lines).unwrap(); let mut client = Client::new( vec![0u8; DEFAULT_CAPACITY], Request { diff --git a/crates/npro-test/tests/ws_server_replay.rs b/crates/npro-test/tests/ws_server_replay.rs index b81999c..3ea0d95 100644 --- a/crates/npro-test/tests/ws_server_replay.rs +++ b/crates/npro-test/tests/ws_server_replay.rs @@ -124,7 +124,8 @@ mod ws_server_replay { match handshake::server(t, &PROTOCOLS, Some(0)) { Ok(a) => { let mut first = [0u8; MAX_101]; - let n = handshake::response_101(&a, PROTOCOLS[a.protocol], &mut first).unwrap(); + let n = + handshake::response_101(&a, PROTOCOLS[a.protocol], b"", &mut first).unwrap(); Answer::Upgraded(Box::new(Ws::server(&first[..n]))) } Err(r) => { diff --git a/crates/npro-ws/src/conn.rs b/crates/npro-ws/src/conn.rs index 455614e..b04f185 100644 --- a/crates/npro-ws/src/conn.rs +++ b/crates/npro-ws/src/conn.rs @@ -33,6 +33,11 @@ //! begun), then our own close, then the pong, then the answer to the peer's //! close, then the application's next frame, whose payload it pulls. A //! pong still owed when we begin a close is forgotten, as C forgets it. +//! +//! With the `pmd` feature and `Ws::with_pmd`, messages are deflated as +//! `crate::pmd` describes: a little input may then inflate to more than one +//! call of [`Ws::rx`] gives, and [`Ws::rx_pending`] says when to call it +//! again with no more. use npro_core::random::{Random, Unavailable}; use npro_core::utf8::Utf8Validator; @@ -192,6 +197,13 @@ enum Op { } impl Op { + const fn of(kind: Kind) -> Self { + match kind { + Kind::Text => Self::Text, + Kind::Binary => Self::Binary, + } + } + const fn control(self) -> bool { matches!(self, Self::Close | Self::Ping | Self::Pong) } @@ -246,7 +258,30 @@ enum Msg { /// Between messages. Idle, /// A message is under way, and its first piece has been given or not. - Open { kind: Kind, given: Given }, + Open { + kind: Kind, + given: Given, + coding: Coding, + }, +} + +/// How a message's payload is coded. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Coding { + /// As it is. + Plain, + /// Deflated: its first frame had RSV1, with permessage-deflate. + #[cfg(feature = "pmd")] + Deflated, +} + +/// The extension in use, if any. +#[derive(Clone, Debug)] +enum Ext { + None, + /// permessage-deflate. + #[cfg(feature = "pmd")] + Pmd(alloc::boxed::Box<crate::pmd::Codec>), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -377,10 +412,55 @@ enum App { mask: Option<[u8; 4]>, at: u64, }, + /// A message being deflated into frames, `owed` of it still to be + /// pulled, and a frame going, its payload `sent` so far. + #[cfg(feature = "pmd")] + Deflating { + kind: Kind, + owed: u64, + frame: Option<Going>, + }, +} + +/// What a step of a deflated message's payload came to. +#[cfg(feature = "pmd")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Deflated { + /// The frame is done with and gave nothing: parsing goes on, from + /// here. + Again(usize), + /// Nothing to give, having taken this much. + Nothing(usize), + /// A piece of the message. + Piece(Inflating), + /// Text that is not UTF-8, with C's reason. + Refused(&'static [u8]), + /// It does not inflate, or is a zip bomb: the connection is dropped. + Dropped, +} + +/// A piece of a deflated message, in the codec's output. +#[cfg(feature = "pmd")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Inflating { + consumed: usize, + kind: Kind, + produced: usize, + first: bool, + last: bool, +} + +/// A deflated frame's payload going out. +#[cfg(feature = "pmd")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Going { + sent: usize, + mask: Option<[u8; 4]>, + fin: bool, } /// Masks `b`, the bytes of a payload from `at` on, with `mask`. -fn apply_mask(b: &mut [u8], mask: [u8; 4], at: u64) { +pub(crate) fn apply_mask(b: &mut [u8], mask: [u8; 4], at: u64) { // the mask's index where `b` starts: `at` mod 4 let start = at.to_le_bytes().first().map_or(0, |l| usize::from(l & 3)); for (x, m) in b.iter_mut().zip(mask.iter().cycle().skip(start)) { @@ -388,8 +468,9 @@ fn apply_mask(b: &mut [u8], mask: [u8; 4], at: u64) { } } -/// A final frame's header, with a client's mask after it. -fn frame_header(op: Op, len: u64, mask: Option<[u8; 4]>, out: &mut Out) { +/// A frame's header, its first byte `first` (FIN, RSV and opcode), with a +/// client's mask after it. +fn frame_header(first: u8, len: u64, mask: Option<[u8; 4]>, out: &mut Out) { // the length is 7 bits, or 126 and 16 bits, or 127 and 64 bits: the // last of its big endian bytes, after the marker let be = len.to_be_bytes(); @@ -401,7 +482,7 @@ fn frame_header(op: Op, len: u64, mask: Option<[u8; 4]>, out: &mut Out) { let masked = if mask.is_some() { 0x80 } else { 0 }; let mut header = [0u8; 14]; if let Some(h) = header.get_mut(..2) { - h.copy_from_slice(&[0x80 | op.code(), masked | marker]); + h.copy_from_slice(&[first, masked | marker]); } let mut used = 2usize.saturating_add(extra); if let (Some(d), Some(s)) = ( @@ -422,7 +503,7 @@ fn frame_header(op: Op, len: u64, mask: Option<[u8; 4]>, out: &mut Out) { /// A control frame, header and payload, masked with a client's mask. fn control_frame(op: Op, c: &Ctl, mask: Option<[u8; 4]>, out: &mut Out) { - frame_header(op, u64::from(c.len), mask, out); + frame_header(0x80 | op.code(), u64::from(c.len), mask, out); out.push(c.payload()); if let Some(m) = mask { out.mask_tail(c.payload().len(), m); @@ -457,6 +538,7 @@ pub struct Ws<P = AsServer> { closing: Closing, out: Out, app: App, + ext: Ext, } impl Ws<AsServer> { @@ -495,6 +577,28 @@ impl<P: Role> Ws<P> { closing: Closing::None, out, app: App::Idle, + ext: Ext::None, + } + } + + /// The connection with permessage-deflate, as negotiated + /// ([`crate::pmd`]): a message whose first frame has RSV1 is inflated, + /// and what the application sends is deflated. + #[cfg(feature = "pmd")] + #[must_use] + pub fn with_pmd(mut self, params: crate::pmd::Params) -> Self { + let codec = crate::pmd::Codec::new(self.role.side(), params); + self.ext = Ext::Pmd(alloc::boxed::Box::new(codec)); + self + } + + /// Whether RSV1 may mark a data message's first frame: with + /// permessage-deflate, as C's `lws_ws_rsv_valid()` has it. + const fn rsv1_marks_deflate(&self) -> bool { + match self.ext { + Ext::None => false, + #[cfg(feature = "pmd")] + Ext::Pmd(_) => true, } } @@ -517,6 +621,35 @@ impl<P: Role> Ws<P> { } } + /// Whether [`Ws::rx`] has more to give without more input: with + /// permessage-deflate, a little input may inflate to more than one + /// call gives, as C's `rx_draining_ext`. While it is true, call + /// [`Ws::rx`] again, with no input if there is none. Without + /// permessage-deflate, never. + #[must_use] + #[cfg_attr( + not(feature = "pmd"), + expect( + clippy::missing_const_for_fn, + reason = "with pmd it asks the codec, which is not const at the MSRV; the API is one" + ) + )] + pub fn rx_pending(&self) -> bool { + #[cfg(feature = "pmd")] + if let ( + Ext::Pmd(codec), + Parse::Payload(f, left), + Msg::Open { + coding: Coding::Deflated, + .. + }, + ) = (&self.ext, self.parse, self.msg) + { + return !f.op.control() && (left == 0 || codec.owes()); + } + false + } + /// Whether the connection has something of its own to write. #[must_use] pub const fn wants_write(&self) -> bool { @@ -564,7 +697,46 @@ impl<P: Role> Ws<P> { event: None, }; } - Parse::Payload(f, left) => return self.payload(f, left, input, used), + Parse::Payload(f, left) => { + #[cfg(feature = "pmd")] + if !f.op.control() + && matches!( + self.msg, + Msg::Open { + coding: Coding::Deflated, + .. + } + ) + { + match self.deflated(f, left, input, used) { + // the frame is done with, and gave nothing: + // on to the next + Deflated::Again(consumed) => { + used = consumed; + continue; + } + Deflated::Nothing(consumed) => { + return Rx { + consumed, + event: None, + }; + } + Deflated::Refused(reason) => { + let mut r = self.refuse(1007, reason); + r.consumed = input.len(); + return r; + } + Deflated::Dropped => { + return Rx { + consumed: input.len(), + event: None, + }; + } + Deflated::Piece(p) => return self.deflated_piece(p), + } + } + return self.payload(f, left, input, used); + } Parse::First | Parse::Len(_) | Parse::LenMore(..) | Parse::Mask(..) => {} } let Some(&c) = input.get(used) else { @@ -583,55 +755,72 @@ impl<P: Role> Ws<P> { } } + /// A frame's first byte: FIN, RSV and the opcode. `Some` refuses + /// the frame. + fn first_byte(&mut self, c: u8) -> Option<(u16, &'static [u8])> { + let fin = c & 0x80 != 0; + let side = self.role.side(); + let op = match c & 0x0f { + 0 => Op::Continuation, + 1 => Op::Text, + 2 => Op::Binary, + 8 => Op::Close, + 9 => Op::Ping, + 10 => Op::Pong, + // C's server calls a reserved control opcode without + // FIN fragmented; its client, a bad opcode + _ if side == Side::Server && c & 0x08 != 0 && !fin => { + return Some((1002, b"frag ctl")); + } + _ => return Some((1002, b"bad opc")), + }; + // RSV1 alone, on a data message's first frame, says it is + // deflated, if that was agreed + let deflated = + self.rsv1_marks_deflate() && c & 0x70 == 0x40 && matches!(op, Op::Text | Op::Binary); + let rsv = c & 0x70 != 0 && !deflated; + if let Some(refused) = Self::first_byte_order(side, op, fin, rsv, self.msg) { + return Some(refused); + } + let coding = Self::coding(deflated); + match (op, self.msg) { + (Op::Text, Msg::Idle) => { + self.utf8 = Utf8Validator::new(); + self.msg = Msg::Open { + kind: Kind::Text, + given: Given::Nothing, + coding, + }; + } + (Op::Binary, Msg::Idle) => { + self.msg = Msg::Open { + kind: Kind::Binary, + given: Given::Nothing, + coding, + }; + } + // refused above, or nothing to do + (Op::Text | Op::Binary, Msg::Open { .. }) + | (Op::Continuation, Msg::Idle | Msg::Open { .. }) + | (Op::Close | Op::Ping | Op::Pong, Msg::Idle | Msg::Open { .. }) => {} + } + self.parse = Parse::Len(Frame { + op, + fin, + masked: false, + len: 0, + mask: [0; 4], + }); + None + } + /// One byte of a frame's header; `Some` refuses the frame. fn header(&mut self, c: u8) -> Option<(u16, &'static [u8])> { match self.parse { Parse::First => { - let fin = c & 0x80 != 0; - let side = self.role.side(); - let op = match c & 0x0f { - 0 => Op::Continuation, - 1 => Op::Text, - 2 => Op::Binary, - 8 => Op::Close, - 9 => Op::Ping, - 10 => Op::Pong, - // C's server calls a reserved control opcode without - // FIN fragmented; its client, a bad opcode - _ if side == Side::Server && c & 0x08 != 0 && !fin => { - return Some((1002, b"frag ctl")); - } - _ => return Some((1002, b"bad opc")), - }; - if let Some(refused) = Self::first_byte_order(side, op, fin, c, self.msg) { + if let Some(refused) = self.first_byte(c) { return Some(refused); } - match (op, self.msg) { - (Op::Text, Msg::Idle) => { - self.utf8 = Utf8Validator::new(); - self.msg = Msg::Open { - kind: Kind::Text, - given: Given::Nothing, - }; - } - (Op::Binary, Msg::Idle) => { - self.msg = Msg::Open { - kind: Kind::Binary, - given: Given::Nothing, - }; - } - // refused above, or nothing to do - (Op::Text | Op::Binary, Msg::Open { .. }) - | (Op::Continuation, Msg::Idle | Msg::Open { .. }) - | (Op::Close | Op::Ping | Op::Pong, Msg::Idle | Msg::Open { .. }) => {} - } - self.parse = Parse::Len(Frame { - op, - fin, - masked: false, - len: 0, - mask: [0; 4], - }); } Parse::Len(mut f) => { f.masked = c & 0x80 != 0; @@ -684,6 +873,23 @@ impl<P: Role> Ws<P> { None } + /// How a message beginning is coded. + #[cfg(feature = "pmd")] + const fn coding(deflated: bool) -> Coding { + if deflated { + Coding::Deflated + } else { + Coding::Plain + } + } + + /// How a message beginning is coded: without permessage-deflate, as + /// it is. + #[cfg(not(feature = "pmd"))] + const fn coding(_deflated: bool) -> Coding { + Coding::Plain + } + /// After the length: the mask, if the frame has one, else the payload. const fn after_len(f: Frame) -> Parse { if f.masked { @@ -702,11 +908,10 @@ impl<P: Role> Ws<P> { side: Side, op: Op, fin: bool, - c: u8, + rsv: bool, msg: Msg, ) -> Option<(u16, &'static [u8])> { let frag_ctl = op.control() && !fin; - let rsv = c & 0x70 != 0; let open = matches!(msg, Msg::Open { .. }); let stray_cont = matches!(op, Op::Continuation) && !open; let new_in_open = matches!(op, Op::Text | Op::Binary) && open; @@ -780,7 +985,12 @@ impl<P: Role> Ws<P> { event: None, }; } - let Msg::Open { kind, given } = self.msg else { + let Msg::Open { + kind, + given, + coding, + } = self.msg + else { return Rx { consumed, event: None, @@ -805,6 +1015,7 @@ impl<P: Role> Ws<P> { Msg::Open { kind, given: Given::Some, + coding, } }; // nothing for the app once a close is under way @@ -825,6 +1036,114 @@ impl<P: Role> Ws<P> { } } + /// The payload of `f`, `left` of it still to come, in a deflated + /// message: what fits is unmasked into the codec's hold, and inflated + /// from there, at most [`crate::pmd::RX_CHUNK`] bytes a call, so it is + /// taken from `input` only as it is held, and given the application + /// as it inflates. + #[cfg(feature = "pmd")] + fn deflated(&mut self, f: Frame, left: u64, input: &[u8], used: usize) -> Deflated { + let Msg::Open { + kind, + given, + coding, + } = self.msg + else { + return Deflated::Nothing(used); + }; + let Ext::Pmd(codec) = &mut self.ext else { + return Deflated::Nothing(used); + }; + let mut left = left; + let mut consumed = used; + let room = codec.room(); + if room > 0 && left > 0 { + let rest = input.get(used..).unwrap_or_default(); + let n = usize::try_from(left) + .unwrap_or(usize::MAX) + .min(rest.len()) + .min(room); + let at = f.len.saturating_sub(left); + codec.hold( + rest.get(..n).unwrap_or_default(), + f.masked.then_some(f.mask), + at, + ); + left = left.saturating_sub(u64::try_from(n).unwrap_or(left)); + consumed = consumed.saturating_add(n); + } + let end = f.fin && left == 0; + let Ok(inflated) = codec.inflate(end) else { + // C marks the socket unusable: no close goes + self.fail(); + return Deflated::Dropped; + }; + let more = left > 0 || codec.owes() || (end && !inflated.done); + let piece = codec.rx_out(inflated.produced); + let bad_text: Option<&'static [u8]> = if kind != Kind::Text { + None + } else if self.utf8.feed(piece).is_err() { + Some(b"bad utf8") + } else if inflated.done && !self.utf8.at_boundary() { + Some(b"partial utf8") + } else { + None + }; + if let Some(reason) = bad_text { + return Deflated::Refused(reason); + } + self.parse = if more { + Parse::Payload(f, left) + } else { + Parse::First + }; + if inflated.produced == 0 && !inflated.done { + return if more { + Deflated::Nothing(consumed) + } else { + Deflated::Again(consumed) + }; + } + self.msg = if inflated.done { + Msg::Idle + } else { + Msg::Open { + kind, + given: Given::Some, + coding, + } + }; + // nothing for the app once a close is under way + if !matches!(self.closing, Closing::None) { + return Deflated::Nothing(consumed); + } + Deflated::Piece(Inflating { + consumed, + kind, + produced: inflated.produced, + first: given == Given::Nothing, + last: inflated.done, + }) + } + + /// A piece of a deflated message, as the application is given it. + #[cfg(feature = "pmd")] + fn deflated_piece(&self, p: Inflating) -> Rx<'_> { + let data = match &self.ext { + Ext::Pmd(codec) => codec.rx_out(p.produced), + Ext::None => &[], + }; + Rx { + consumed: p.consumed, + event: Some(Event::Message { + kind: p.kind, + data, + first: p.first, + last: p.last, + }), + } + } + /// A whole control frame has come. fn control(&mut self, op: Op) -> Option<Event<'_>> { match op { @@ -883,7 +1202,9 @@ impl<P: Role> Ws<P> { /// C's `lws_write()` of a final frame. /// /// A client's frame is masked with a mask drawn now, as C draws it in - /// `lws_write()`. + /// `lws_write()`. With permessage-deflate, the message is deflated as + /// it is pulled, into frames of at most `pmd::TX_CHUNK` bytes, + /// each masked with a mask drawn as it is begun. /// /// # Errors /// @@ -894,15 +1215,21 @@ impl<P: Role> Ws<P> { if self.app != App::Idle || self.out.pending() || !matches!(self.closing, Closing::None) { return Err(SendError::Busy); } - let op = match kind { - Kind::Text => Op::Text, - Kind::Binary => Op::Binary, - }; + #[cfg(feature = "pmd")] + if let Ext::Pmd(codec) = &mut self.ext { + codec.begin_message(); + self.app = App::Deflating { + kind, + owed: len, + frame: None, + }; + return Ok(()); + } let Ok(mask) = self.role.next_mask() else { self.fail(); return Err(SendError::NoMask); }; - frame_header(op, len, mask, &mut self.out); + frame_header(0x80 | Op::of(kind).code(), len, mask, &mut self.out); self.app = App::Sending { owed: len, mask, @@ -911,8 +1238,9 @@ impl<P: Role> Ws<P> { Ok(()) } - /// A client's random source failed it: nothing more is read or - /// written, and it asks to be released. + /// The connection cannot go on: a client's random source failed it, + /// or permessage-deflate did. Nothing more is read or written, and it + /// asks to be released. const fn fail(&mut self) { self.parse = Parse::Stopped; self.pong = None; @@ -931,6 +1259,91 @@ impl<P: Role> Ws<P> { true } + /// The deflated message's next step: a frame's payload, or its + /// header, put in flight. How much went into `room`, or `None` if + /// the application must give more first, or the connection failed. + #[cfg(feature = "pmd")] + fn tx_deflated(&mut self, room: &mut [u8], src: &mut dyn TxSource) -> Option<usize> { + let App::Deflating { + kind, + mut owed, + frame, + } = self.app + else { + return Some(0); + }; + let Ext::Pmd(codec) = &mut self.ext else { + self.app = App::Idle; + return Some(0); + }; + if let Some(mut g) = frame { + let payload = codec.tx_frame(); + let rest = payload.get(g.sent..).unwrap_or_default(); + let n = rest.len().min(room.len()); + if let (Some(d), Some(s)) = (room.get_mut(..n), rest.get(..n)) { + d.copy_from_slice(s); + if let Some(m) = g.mask { + apply_mask(d, m, u64::try_from(g.sent).unwrap_or(0)); + } + } + g.sent = g.sent.saturating_add(n); + self.app = match (g.sent >= payload.len(), g.fin) { + (true, true) => App::Idle, + (true, false) => App::Deflating { + kind, + owed, + frame: None, + }, + (false, _) => App::Deflating { + kind, + owed, + frame: Some(g), + }, + }; + return Some(n); + } + let next = codec.next_frame(&mut owed, &mut |b| src.fill(b)); + let made = match next { + Ok(Some(made)) => made, + Ok(None) => { + self.app = App::Deflating { + kind, + owed, + frame: None, + }; + return None; + } + Err(_) => { + self.fail(); + return None; + } + }; + let Ok(mask) = self.role.next_mask() else { + self.fail(); + return None; + }; + // FIN on the last; RSV1 and the opcode on the first, the rest + // continuations + let fin = if made.fin { 0x80 } else { 0 }; + let first = if made.first { + 0x40 | Op::of(kind).code() + } else { + Op::Continuation.code() + }; + let len = u64::try_from(made.len).unwrap_or(u64::MAX); + frame_header(fin | first, len, mask, &mut self.out); + self.app = App::Deflating { + kind, + owed, + frame: Some(Going { + sent: 0, + mask, + fin: made.fin, + }), + }; + Some(0) + } + /// The application is done: the connection closes once what it sent /// has gone, without a close frame, as C's /// `lws_raw_transaction_completed()`. @@ -954,6 +1367,17 @@ impl<P: Role> Ws<P> { written = written.saturating_add(self.out.drain(room)); continue; } + #[cfg(feature = "pmd")] + if matches!(self.app, App::Deflating { .. }) { + match self.tx_deflated(room, src) { + Some(n) => { + written = written.saturating_add(n); + continue; + } + // the application has more to give first + None => return written, + } + } if let App::Sending { owed, mask, at } = self.app { let cap = usize::try_from(owed).unwrap_or(usize::MAX).min(room.len()); let piece = room.get_mut(..cap).unwrap_or_default(); @@ -1118,7 +1542,7 @@ mod tests { (0x1_0000, b"\x82\x7f\0\0\0\0\0\x01\0\0"), ] { let mut out = Out::new(); - frame_header(Op::Binary, len, None, &mut out); + frame_header(0x82, len, None, &mut out); assert_eq!(&out.buf[..out.len], want, "{len}"); } } diff --git a/crates/npro-ws/src/handshake.rs b/crates/npro-ws/src/handshake.rs index 4f841d2..78270eb 100644 --- a/crates/npro-ws/src/handshake.rs +++ b/crates/npro-ws/src/handshake.rs @@ -257,14 +257,16 @@ fn accept_of(key: &[u8]) -> Option<[u8; ACCEPT_LEN]> { /// The most a 101 C writes may have here. pub const MAX_101: usize = 256; -/// C's 101 for `a`, `name` being its subprotocol's: written into `out`, +/// C's 101 for `a`, `name` being its subprotocol's, and `extensions` the +/// header lines, each ending CRLF, saying what extensions it takes, as +/// `pmd::ServerAccepted::header_lines` has them: written into `out`, /// returning how much of it. /// /// # Errors /// /// `None` if `out` is too small. #[must_use] -pub fn response_101(a: &Accepted, name: &[u8], out: &mut [u8]) -> Option<usize> { +pub fn response_101(a: &Accepted, name: &[u8], extensions: &[u8], out: &mut [u8]) -> Option<usize> { let mut at = 0usize; let mut put = |b: &[u8]| -> Option<()> { let end = at.checked_add(b.len())?; @@ -279,7 +281,9 @@ pub fn response_101(a: &Accepted, name: &[u8], out: &mut [u8]) -> Option<usize> put(b"\r\nSec-WebSocket-Protocol: ")?; put(name)?; } - put(b"\r\n\r\n")?; + put(b"\r\n")?; + put(extensions)?; + put(b"\r\n")?; Some(at) } @@ -287,7 +291,7 @@ pub fn response_101(a: &Accepted, name: &[u8], out: &mut [u8]) -> Option<usize> pub const KEY_LEN: usize = 24; /// The most the lines [`ClientKey::request_lines`] writes may have, but -/// for the subprotocols offered. +/// for the subprotocols and extensions offered. pub const MAX_REQUEST_LINES: usize = 160; /// Why a client fails the server's response to its upgrade: each is C's @@ -343,7 +347,7 @@ impl core::error::Error for ClientRefusal {} /// assert_eq!(key.key(), b"OvomtQpKCWUnZW7tMR6Gqw=="); /// /// let mut lines = [0u8; MAX_REQUEST_LINES + 32]; -/// let n = key.request_lines(Some(b"echo"), &mut lines).unwrap(); +/// let n = key.request_lines(Some(b"echo"), None, &mut lines).unwrap(); /// let mut c = Client::new([0u8; 1024], Request { /// method: b"GET", /// path: b"/echo", @@ -361,8 +365,8 @@ impl core::error::Error for ClientRefusal {} /// Connection: Upgrade\r\nSec-WebSocket-Protocol: echo\r\n\ /// Sec-WebSocket-Accept: rSsJf/ZKQdiul0BGIJ6uQGawdU8=\r\n\r\n")?; /// assert_eq!(rx.event, Some(Event::Response)); -/// let chosen = key.check(c.status(), c.response(), Some(b"echo"))?; -/// assert_eq!(chosen, Some(&b"echo"[..])); +/// let checked = key.check(c.status(), c.response(), Some(b"echo"), None)?; +/// assert_eq!(checked.protocol, Some(&b"echo"[..])); /// # Ok::<(), Box<dyn core::error::Error>>(()) /// ``` #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -395,15 +399,23 @@ impl ClientKey { } /// The lines asking for the upgrade, in C's order, offering - /// `protocols`, a comma separated list, if any: for the request's + /// `protocols`, a comma separated list, and `extensions`, such as + /// `pmd::OFFER`, if any: for the request's /// [`npro_h1::client::Connection::Upgrade`]. Written into `out`, /// returning how much of it. /// - /// `None` if `out` is too small, or `protocols` would break the line: - /// it may not hold a CR, LF or NUL. + /// `None` if `out` is too small, or `protocols` or `extensions` would + /// break the line: they may not hold a CR, LF or NUL. #[must_use] - pub fn request_lines(&self, protocols: Option<&[u8]>, out: &mut [u8]) -> Option<usize> { - if protocols.is_some_and(|p| p.iter().any(|c| matches!(c, b'\r' | b'\n' | 0))) { + pub fn request_lines( + &self, + protocols: Option<&[u8]>, + extensions: Option<&[u8]>, + out: &mut [u8], + ) -> Option<usize> { + let breaks = + |v: Option<&[u8]>| v.is_some_and(|p| p.iter().any(|c| matches!(c, b'\r' | b'\n' | 0))); + if breaks(protocols) || breaks(extensions) { return None; } let mut at = 0usize; @@ -421,23 +433,33 @@ impl ClientKey { put(p)?; put(b"\r\n")?; } + if let Some(e) = extensions { + put(b"Sec-WebSocket-Extensions: ")?; + put(e)?; + put(b"\r\n")?; + } put(b"Sec-WebSocket-Version: 13\r\n")?; Some(at) } /// Checks the server's final response, its status `status` and its - /// headers `t`, to a request that offered `offered`: C's checks, in C's - /// order. Returns the subprotocol the server named, if it named one. + /// headers `t`, to a request that offered the subprotocols `offered` + /// and the extensions `extensions`: C's checks, in C's order. Returns + /// the subprotocol the server named, and what it said of the + /// extensions, if anything, which the extension then reads, as + /// `pmd::client_accept` does. /// /// # Errors /// - /// The [`ClientRefusal`]: the connection fails. + /// The [`ClientRefusal`]: the connection fails. Extensions, where none + /// were offered, or said in more than one header, are refused. pub fn check<'t, S: AsRef<[u8]> + AsMut<[u8]>>( &self, status: Option<u16>, t: &'t HeaderTable<S>, offered: Option<&[u8]>, - ) -> Result<Option<&'t [u8]>, ClientRefusal> { + extensions: Option<&[u8]>, + ) -> Result<Checked<'t>, ClientRefusal> { if status != Some(101) { return Err(ClientRefusal::NotSwitching); } @@ -490,16 +512,34 @@ impl ClientKey { Some(name) }; - if t.total_len(Token::WsExtensions) > 0 { - return Err(ClientRefusal::Extension); - } + let said = if t.total_len(Token::WsExtensions) == 0 { + None + } else { + let mut f = t.fragments(Token::WsExtensions); + match (extensions, f.next(), f.next()) { + (Some(_), Some(value), None) => Some(value), + (None | Some(_), _, _) => return Err(ClientRefusal::Extension), + } + }; if t.first(Token::WsAccept) != Some(self.accept.as_slice()) { return Err(ClientRefusal::Accept); } - Ok(chosen) + Ok(Checked { + protocol: chosen, + extensions: said, + }) } } +/// What a client's check of the server's 101 found. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Checked<'t> { + /// The subprotocol the server named, if it named one. + pub protocol: Option<&'t [u8]>, + /// What the server said of the extensions offered, if anything. + pub extensions: Option<&'t [u8]>, +} + #[cfg(test)] mod tests { use super::*; @@ -532,7 +572,9 @@ mod tests { fn verdict(response: &str) -> Verdict { let key = ClientKey::new(&mut SeededRandom::new(1)).unwrap(); let mut lines = [0u8; MAX_REQUEST_LINES]; - let n = key.request_lines(Some(b"echo, chat"), &mut lines).unwrap(); + let n = key + .request_lines(Some(b"echo, chat"), None, &mut lines) + .unwrap(); let mut c = Client::new( [0u8; 1024], Request { @@ -550,8 +592,8 @@ mod tests { let _ = c.tx(&mut out); let _ = c.rx(response.as_bytes()).unwrap(); assert!(c.is_upgraded(), "{response}"); - match key.check(c.status(), c.response(), Some(b"echo, chat")) { - Ok(p) => Verdict::Took(p.map(<[u8]>::len)), + match key.check(c.status(), c.response(), Some(b"echo, chat"), None) { + Ok(c) => Verdict::Took(c.protocol.map(<[u8]>::len)), Err(r) => Verdict::Refused(r), } } @@ -621,9 +663,10 @@ mod tests { fn the_request_lines_refuse_a_protocol_list_that_breaks_the_line() { let key = ClientKey::new(&mut SeededRandom::new(1)).unwrap(); let mut out = [0u8; MAX_REQUEST_LINES]; - assert_eq!(key.request_lines(Some(b"a\r\nX: y"), &mut out), None); - assert_eq!(key.request_lines(None, &mut out[..10]), None); - let n = key.request_lines(None, &mut out).unwrap(); + assert_eq!(key.request_lines(Some(b"a\r\nX: y"), None, &mut out), None); + assert_eq!(key.request_lines(None, Some(b"x\ny"), &mut out), None); + assert_eq!(key.request_lines(None, None, &mut out[..10]), None); + let n = key.request_lines(None, None, &mut out).unwrap(); assert_eq!( &out[..n], b"Upgrade: websocket\r\nConnection: Upgrade\r\n\ diff --git a/crates/npro-ws/src/lib.rs b/crates/npro-ws/src/lib.rs index df69843..73b107d 100644 --- a/crates/npro-ws/src/lib.rs +++ b/crates/npro-ws/src/lib.rs @@ -6,14 +6,17 @@ //! and a client's key, its request's upgrade lines and its checks of the //! server's response; //! - [`conn`]: a ws connection, either end, its frames in and out, and its -//! close. +//! close; +//! - `pmd`, with the opt-in feature of that name: permessage-deflate, its +//! negotiation and its compression, which needs `alloc`. #![no_std] #![forbid(unsafe_code)] +#[cfg(feature = "pmd")] +extern crate alloc; + pub mod conn; pub mod handshake; - -// admitted for permessage-deflate, which is to use it #[cfg(feature = "pmd")] -use miniz_oxide as _; +pub mod pmd; diff --git a/crates/npro-ws/src/pmd.rs b/crates/npro-ws/src/pmd.rs new file mode 100644 index 0000000..b07326a --- /dev/null +++ b/crates/npro-ws/src/pmd.rs @@ -0,0 +1,1274 @@ +//! permessage-deflate (RFC 7692): C's `extension-permessage-deflate.c`, +//! its negotiation in `lws_extension_server_handshake()` and +//! `lws_client_ws_upgrade()`, and the inflater and deflater, which are +//! `miniz_oxide`'s where C's are zlib's. +//! +//! **Negotiation.** A server takes the first `permessage-deflate` offer +//! in the request's `Sec-WebSocket-Extensions` ([`server_accept`]), with +//! the parameters it understands, and says what it took in its 101 +//! ([`ServerAccepted::header_lines`]). A client offers [`OFFER`] and +//! takes the server's answer if its parameters are RFC 7692's, with C's +//! ranges ([`client_accept`]). Either way the result is the connection's +//! [`Params`], given to [`crate::conn::Ws::with_pmd`]. +//! +//! **Messages.** A message whose first frame has RSV1 is compressed: its +//! payload is inflated as it comes, the four bytes `00 00 ff ff` its +//! sender removed put back at its end, and the application given what it +//! inflates to, at most [`RX_CHUNK`] bytes a call. A message inflating +//! to more than [`Params::max_message`], C's 256MiB by default, is a zip +//! bomb. Data after the peer's deflate stream ended in a message is +//! refused, as C refuses it. Each of these, and data that does not +//! inflate, drops the connection without a close, as C marks the socket +//! unusable. What the application sends is deflated into frames of at +//! most [`TX_CHUNK`] bytes, the first with RSV1, each sync flushed +//! message's trailing `00 00 ff ff` removed. A side that agreed to no +//! context takeover starts its deflater afresh for each message; the +//! inflater starts afresh when the peer agreed to it, or its stream ended. + +use alloc::boxed::Box; +use alloc::vec::Vec; + +use miniz_oxide::deflate::core::{CompressionStrategy, CompressorOxide}; +use miniz_oxide::inflate::stream::InflateState; +use miniz_oxide::{DataFormat, MZError, MZFlush, MZStatus}; +use npro_h1::table::HeaderTable; +use npro_h1::token::Token; + +use crate::conn::Side; +use crate::handshake::ClientRefusal; + +/// The extension's name. +pub const NAME: &[u8] = b"permessage-deflate"; + +/// What a client offers: the extension with no parameters, as C's +/// `api-test-sansio` offers it. +pub const OFFER: &[u8] = NAME; + +/// The most a message may inflate to by default: C's 256MiB. +pub const MAX_MESSAGE: u64 = 0x1000_0000; + +/// The most inflated data given the application in one call: C's +/// default `rx_buf_size`, its drain budget. +pub const RX_CHUNK: usize = 1024; + +/// The most compressed payload in one frame written: C's default +/// `tx_buf_size`. +pub const TX_CHUNK: usize = 1024; + +/// The most compressed input held between calls. +const HOLD: usize = 1024; + +/// The longest `Sec-WebSocket-Extensions` C takes from a client. +const MAX_OFFER: usize = 255; + +/// The trailer RFC 7692 7.2.1 has a sender remove, and its receiver put +/// back. +const TRAILER: [u8; 4] = [0, 0, 0xff, 0xff]; + +/// The compression level C uses, for both ends. +const LEVEL: u8 = 1; + +/// Whether an end keeps its compression context between messages. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Takeover { + /// It may refer back into earlier messages: the default. + Kept, + /// It agreed not to: `*_no_context_takeover`. + NotKept, +} + +/// An LZ77 window's size, as a power of two: 8 to 15, RFC 7692's range, +/// which C checks too. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct WindowBits(u8); + +impl WindowBits { + /// 32KiB, the default. + pub const MAX: Self = Self(15); + + /// `bits`, if it is in RFC 7692's range. + #[must_use] + pub const fn new(bits: u8) -> Option<Self> { + if 8 <= bits && bits <= 15 { + Some(Self(bits)) + } else { + None + } + } + + /// The power of two. + #[must_use] + pub const fn get(self) -> u8 { + self.0 + } +} + +/// What a connection's two ends agreed, made by [`server_accept`] or +/// [`client_accept`]. RFC 7692 names each parameter for the end it +/// constrains: `server_*` the server's deflater, and so the client's +/// inflater, and `client_*` the other way round. +/// +/// npro's own deflater always has the whole 32KiB window: npro never +/// agrees to less, since `miniz_oxide`'s smaller windows still refer +/// further back than they say (zlib finds distances too far back with any +/// window under 14 bits). The peer's window may be any. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Params { + server_takeover: Takeover, + client_takeover: Takeover, + server_window: WindowBits, + client_window: WindowBits, + max_message: u64, +} + +impl Params { + /// Whether the server keeps its context between messages. + #[must_use] + pub const fn server_takeover(&self) -> Takeover { + self.server_takeover + } + + /// Whether the client keeps its context between messages. + #[must_use] + pub const fn client_takeover(&self) -> Takeover { + self.client_takeover + } + + /// The server's deflate window. + #[must_use] + pub const fn server_window(&self) -> WindowBits { + self.server_window + } + + /// The client's deflate window. + #[must_use] + pub const fn client_window(&self) -> WindowBits { + self.client_window + } + + /// The most a message may inflate to; past it, the connection is + /// dropped. + #[must_use] + pub const fn max_message(&self) -> u64 { + self.max_message + } + + /// The same, a message inflating to at most `max` bytes, where C's + /// limit is [`MAX_MESSAGE`]. + #[must_use] + pub const fn with_max_message(mut self, max: u64) -> Self { + self.max_message = max; + self + } + + /// RFC 7692's defaults, and C's limit on a message. + pub const DEFAULT: Self = Self { + server_takeover: Takeover::Kept, + client_takeover: Takeover::Kept, + server_window: WindowBits::MAX, + client_window: WindowBits::MAX, + max_message: MAX_MESSAGE, + }; + + /// Whether `side`'s own deflater keeps its context. + const fn own_takeover(&self, side: Side) -> Takeover { + match side { + Side::Server => self.server_takeover, + Side::Client => self.client_takeover, + } + } + + /// The peer's takeover, which governs `side`'s inflater. + const fn peer_takeover(&self, side: Side) -> Takeover { + match side { + Side::Server => self.client_takeover, + Side::Client => self.server_takeover, + } + } +} + +impl Default for Params { + fn default() -> Self { + Self::DEFAULT + } +} + +/// The parameters RFC 7692 defines. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Param { + ServerNoContextTakeover, + ClientNoContextTakeover, + ServerMaxWindowBits, + ClientMaxWindowBits, +} + +impl Param { + /// Its place in a list of what was seen. + const fn index(self) -> usize { + match self { + Self::ServerNoContextTakeover => 0, + Self::ClientNoContextTakeover => 1, + Self::ServerMaxWindowBits => 2, + Self::ClientMaxWindowBits => 3, + } + } + + const fn named(name: &[u8]) -> Option<Self> { + match name { + b"server_no_context_takeover" => Some(Self::ServerNoContextTakeover), + b"client_no_context_takeover" => Some(Self::ClientNoContextTakeover), + b"server_max_window_bits" => Some(Self::ServerMaxWindowBits), + b"client_max_window_bits" => Some(Self::ClientMaxWindowBits), + _ => None, + } + } +} + +/// `b` without the spaces and tabs around it. +fn trim(b: &[u8]) -> &[u8] { + let blank = |c: &u8| *c == b' ' || *c == b'\t'; + let start = b.iter().position(|c| !blank(c)).unwrap_or(b.len()); + let end = b + .iter() + .rposition(|c| !blank(c)) + .map_or(start, |n| n.saturating_add(1)); + b.get(start..end).unwrap_or_default() +} + +/// A parameter, `name` or `name=value`, the value perhaps quoted. +fn param(p: &[u8]) -> (&[u8], Option<&[u8]>) { + let mut kv = p.splitn(2, |c| *c == b'='); + let name = trim(kv.next().unwrap_or_default()); + let value = kv.next().map(|v| { + let v = trim(v); + v.strip_prefix(b"\"") + .and_then(|v| v.strip_suffix(b"\"")) + .unwrap_or(v) + }); + (name, value) +} + +/// A window size's value, as C reads it: decimal, in range. +fn window_bits(v: &[u8]) -> Option<WindowBits> { + if v.is_empty() || v.len() > 2 || !v.iter().all(u8::is_ascii_digit) { + return None; + } + let n = v.iter().fold(0u8, |n, d| { + n.wrapping_mul(10).wrapping_add(d.wrapping_sub(b'0')) + }); + WindowBits::new(n) +} + +/// What a server took of a client's offer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ServerAccepted { + params: Params, + line: [u8; 160], + len: usize, +} + +impl ServerAccepted { + /// The connection's parameters. + #[must_use] + pub const fn params(&self) -> Params { + self.params + } + + /// The header line saying what was taken, for the 101: C's, the + /// parameters taken in the order the client gave them. + #[must_use] + pub fn header_lines(&self) -> &[u8] { + self.line.get(..self.len).unwrap_or_default() + } +} + +/// Why a server will not answer an offer: C drops the connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OfferRefused { + /// The extensions offered are longer than C takes. + TooLong, +} + +impl core::fmt::Display for OfferRefused { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("extension offer too long") + } +} + +impl core::error::Error for OfferRefused {} + +/// A server's answer to a request's `Sec-WebSocket-Extensions`: the first +/// `permessage-deflate` offer, with C's limit on the list, and these of +/// its parameters, the rest left alone as C leaves them: +/// `server_no_context_takeover` and `client_no_context_takeover`, as C +/// takes them. An offer asking for a server window under 32KiB, which +/// npro's deflater cannot keep to, is declined, and the next tried: RFC +/// 7692 7.1.2.1, where C takes it and leaves the parameter out. `None` if +/// nothing was offered, or nothing it could take. +/// +/// ``` +/// use npro_h1::head::{Config, Head, Side}; +/// use npro_ws::pmd::{server_accept, Takeover}; +/// +/// let mut h = Head::new([0u8; 1024], Side::Server, Config::new())?; +/// h.rx(b"GET / HTTP/1.1\r\nSec-WebSocket-Extensions: x-foo, \ +/// permessage-deflate; client_max_window_bits; \ +/// client_no_context_takeover\r\n\r\n")?; +/// let a = server_accept(h.table())?.unwrap(); +/// assert_eq!(a.params().client_takeover(), Takeover::NotKept); +/// assert_eq!( +/// a.header_lines(), +/// b"sec-websocket-extensions: permessage-deflate; client_no_context_takeover\r\n" +/// ); +/// # Ok::<(), Box<dyn core::error::Error>>(()) +/// ``` +/// +/// # Errors +/// +/// [`OfferRefused`]: C drops the connection. +pub fn server_accept<S: AsRef<[u8]> + AsMut<[u8]>>( + t: &HeaderTable<S>, +) -> Result<Option<ServerAccepted>, OfferRefused> { + let total = t.total_len(Token::WsExtensions); + if total == 0 { + return Ok(None); + } + let mut buf = [0u8; MAX_OFFER]; + let offer = t + .copy(Token::WsExtensions, &mut buf) + .ok() + .and_then(|n| buf.get(..n)) + .ok_or(OfferRefused::TooLong)?; + Ok(offer + .split(|c| *c == b',') + .filter(|e| trim(e.split(|c| *c == b';').next().unwrap_or_default()) == NAME) + .find_map(take_offer)) +} + +/// What a server takes of one `permessage-deflate` offer, if it can take +/// it. +fn take_offer(entry: &[u8]) -> Option<ServerAccepted> { + let mut a = ServerAccepted { + params: Params::DEFAULT, + line: [0; 160], + len: 0, + }; + let put = |acc: &mut ServerAccepted, b: &[u8]| { + let end = acc.len.saturating_add(b.len()); + if let Some(d) = acc.line.get_mut(acc.len..end) { + d.copy_from_slice(b); + acc.len = end; + } + }; + put(&mut a, b"sec-websocket-extensions: "); + put(&mut a, NAME); + for p in entry.split(|c| *c == b';').skip(1) { + let (name, value) = param(p); + match (Param::named(name), value) { + (Some(Param::ServerNoContextTakeover), None) + if a.params.server_takeover == Takeover::Kept => + { + a.params.server_takeover = Takeover::NotKept; + put(&mut a, b"; server_no_context_takeover"); + } + (Some(Param::ClientNoContextTakeover), None) + if a.params.client_takeover == Takeover::Kept => + { + a.params.client_takeover = Takeover::NotKept; + put(&mut a, b"; client_no_context_takeover"); + } + // a window it can keep to, which is only the whole one; a + // smaller one, or one not in range, declines the offer, as + // RFC 7692 7.1.2.1 has it, where C takes the offer and + // leaves the parameter out + (Some(Param::ServerMaxWindowBits), Some(v)) => { + if window_bits(v) != Some(WindowBits::MAX) { + return None; + } + put(&mut a, b"; server_max_window_bits=15"); + } + // what the server leaves alone: the client's window is its + // own unless the server limits it, which it need not; and + // what it does not know, or has already + (Some(_) | None, _) => {} + } + } + put(&mut a, b"\r\n"); + Some(a) +} + +/// A client's reading of the server's `Sec-WebSocket-Extensions`, having +/// offered [`OFFER`]: `permessage-deflate` alone, with RFC 7692's +/// parameters, each at most once, `*_no_context_takeover` with no value +/// and `server_max_window_bits` with one in C's range. A +/// `client_max_window_bits` of less than 15 is refused: npro did not offer +/// it, and its deflater cannot keep to it. +/// +/// C also takes its own local options from a server, which no server has +/// reason to send; npro does not. +/// +/// # Errors +/// +/// [`ClientRefusal::Extension`]: C's "HS: EXT: unknown ext" and "HS: EXT: +/// failed parsing options". +pub fn client_accept(said: &[u8]) -> Result<Params, ClientRefusal> { + let mut entries = said.split(|c| *c == b','); + let entry = entries.next().unwrap_or_default(); + if entries.next().is_some() { + return Err(ClientRefusal::Extension); + } + let mut parts = entry.split(|c| *c == b';'); + if trim(parts.next().unwrap_or_default()) != NAME { + return Err(ClientRefusal::Extension); + } + let mut params = Params::DEFAULT; + let mut seen = [false; 4]; + for p in parts { + let (name, value) = param(p); + let which = Param::named(name).ok_or(ClientRefusal::Extension)?; + let slot = seen + .get_mut(which.index()) + .ok_or(ClientRefusal::Extension)?; + if core::mem::replace(slot, true) { + return Err(ClientRefusal::Extension); + } + match (which, value) { + (Param::ServerNoContextTakeover, None) => params.server_takeover = Takeover::NotKept, + (Param::ClientNoContextTakeover, None) => params.client_takeover = Takeover::NotKept, + (Param::ServerMaxWindowBits, Some(v)) => { + params.server_window = window_bits(v).ok_or(ClientRefusal::Extension)?; + } + // npro offers no client_max_window_bits, so a server may not + // limit the client's window (RFC 7692 7.1.2.2); and npro's + // deflater can keep to no smaller one. C takes it. + (Param::ClientMaxWindowBits, Some(v)) => { + if window_bits(v) != Some(WindowBits::MAX) { + return Err(ClientRefusal::Extension); + } + } + (Param::ServerNoContextTakeover | Param::ClientNoContextTakeover, Some(_)) + | (Param::ServerMaxWindowBits | Param::ClientMaxWindowBits, None) => { + return Err(ClientRefusal::Extension); + } + } + } + Ok(params) +} + +/// Why permessage-deflate drops the connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Fail { + /// The payload does not inflate. + Data, + /// A message inflated past its limit. + ZipBomb, + /// Data after the end of the peer's deflate stream. + AfterEnd, + /// The deflater failed, or could not end a flush as RFC 7692 has it. + Deflate, +} + +/// Where the inflater is within a message. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RxStream { + /// Taking the message's payload. + Running, + /// Taking the trailer: this much of it has gone in. + Trailer(u8), + /// The peer ended its deflate stream: only padding may follow. + Ended, +} + +/// What a call to [`Codec::inflate`] came to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Inflated { + /// How much it put in [`Codec::rx_out`]. + pub(crate) produced: usize, + /// Whether the message is over. + pub(crate) done: bool, +} + +/// What the deflater is doing for the application's message. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TxStream { + /// No message. + Idle, + /// Taking it; frames written so far. + Running { frames: u32 }, + /// Its flush is done: what is left is its last frame. + Flushed { frames: u32 }, +} + +/// A frame of compressed payload ready to go. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct TxFrame { + /// Its payload's length, in [`Codec::tx_frame`]. + pub(crate) len: usize, + /// Whether it is the message's first, which carries RSV1 and the + /// opcode. + pub(crate) first: bool, + /// Whether it ends the message. + pub(crate) fin: bool, +} + +/// One connection's permessage-deflate. +#[derive(Clone)] +pub(crate) struct Codec { + side: Side, + params: Params, + inflater: Option<Box<InflateState>>, + held: Vec<u8>, + held_at: usize, + rx_out: Vec<u8>, + rx_total: u64, + rx_stream: RxStream, + /// The last inflate filled its output: the inflater may owe more. + rx_full: bool, + /// Inflating failed after giving what it gave first: the next call + /// says so. + rx_failed: Option<Fail>, + deflater: Option<Box<CompressorOxide>>, + tx_in: Vec<u8>, + tx_in_at: usize, + /// Compressed bytes not yet in a frame: the last four are kept back + /// until the message ends, so its trailer can be removed. + tx_out: Vec<u8>, + tx_frame: Vec<u8>, + tx_stream: TxStream, +} + +impl core::fmt::Debug for Codec { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Codec") + .field("side", &self.side) + .field("params", &self.params) + .field("rx_stream", &self.rx_stream) + .field("tx_stream", &self.tx_stream) + .finish_non_exhaustive() + } +} + +impl Codec { + pub(crate) const fn new(side: Side, params: Params) -> Self { + Self { + side, + params, + inflater: None, + held: Vec::new(), + held_at: 0, + rx_out: Vec::new(), + rx_total: 0, + rx_stream: RxStream::Running, + rx_full: false, + rx_failed: None, + deflater: None, + tx_in: Vec::new(), + tx_in_at: 0, + tx_out: Vec::new(), + tx_frame: Vec::new(), + tx_stream: TxStream::Idle, + } + } + + /// Whether compressed input is held, not yet inflated. + pub(crate) fn holding(&self) -> bool { + self.held_at < self.held.len() + } + + /// Whether inflating can go on without more input: input is held, or + /// the inflater may owe output. + pub(crate) fn owes(&self) -> bool { + self.holding() || self.rx_full || self.rx_failed.is_some() + } + + /// How much more compressed input may be held now. + pub(crate) fn room(&self) -> usize { + if self.held_at < self.held.len() { + 0 + } else { + HOLD + } + } + + /// Holds `piece`, compressed payload, unmasked with `mask` from `at`. + pub(crate) fn hold(&mut self, piece: &[u8], mask: Option<[u8; 4]>, at: u64) { + self.held.clear(); + self.held_at = 0; + self.held.extend_from_slice(piece); + if let Some(m) = mask { + crate::conn::apply_mask(&mut self.held, m, at); + } + } + + /// What the last [`Codec::inflate`] produced. + pub(crate) fn rx_out(&self, n: usize) -> &[u8] { + self.rx_out.get(..n).unwrap_or_default() + } + + /// Inflates what is held, and at the message's end, `end`, the + /// trailer, into [`Codec::rx_out`]. + /// + /// What inflated before a failure is given first, and the failure on + /// the next call, so the application, and the check of text, see the + /// stream up to where it went wrong however it was split. + pub(crate) fn inflate(&mut self, end: bool) -> Result<Inflated, Fail> { + if let Some(f) = self.rx_failed { + return Err(f); + } + let first = self.inflate_once(end)?; + // the payload is all in, and gave nothing more: on to the trailer + if first.produced == 0 && !first.done && matches!(self.rx_stream, RxStream::Trailer(0)) { + return self.inflate_once(end); + } + Ok(first) + } + + fn inflate_once(&mut self, end: bool) -> Result<Inflated, Fail> { + if self.rx_out.len() < RX_CHUNK { + self.rx_out.resize(RX_CHUNK, 0); + } + let held = self.held.get(self.held_at..).unwrap_or_default(); + if self.rx_stream == RxStream::Ended { + // only the padding of the stored block it flushed with + if held.iter().any(|b| *b != 0) { + return Err(Fail::AfterEnd); + } + self.held_at = self.held.len(); + return self.produced(0, end); + } + let (input, flush) = match self.rx_stream { + RxStream::Trailer(fed) => ( + TRAILER.get(usize::from(fed)..).unwrap_or_default(), + MZFlush::Sync, + ), + RxStream::Running | RxStream::Ended => (held, MZFlush::None), + }; + let inflater = self + .inflater + .get_or_insert_with(|| InflateState::new_boxed(DataFormat::Raw)); + let r = miniz_oxide::inflate::stream::inflate(inflater, input, &mut self.rx_out, flush); + let progress = r.bytes_consumed > 0 || r.bytes_written > 0; + match r.status { + Ok(MZStatus::StreamEnd) => { + // what it did not take is our trailer, or must be padding + let rest = input.get(r.bytes_consumed..).unwrap_or_default(); + if matches!(self.rx_stream, RxStream::Running) && rest.iter().any(|b| *b != 0) { + return self.failed_after(Fail::AfterEnd, r.bytes_written); + } + if matches!(self.rx_stream, RxStream::Running) { + self.held_at = self.held.len(); + } + self.rx_stream = RxStream::Ended; + return self.produced(r.bytes_written, end); + } + // it can go no further without more input, or more room: + // stuck only if it took and gave nothing, below + Ok(MZStatus::Ok) | Err(MZError::Buf) => {} + Ok(MZStatus::NeedDict) | Err(_) => { + return self.failed_after(Fail::Data, r.bytes_written); + } + } + match self.rx_stream { + RxStream::Running => { + self.held_at = self.held_at.saturating_add(r.bytes_consumed); + } + RxStream::Trailer(fed) => { + let fed = usize::from(fed).saturating_add(r.bytes_consumed); + self.rx_stream = RxStream::Trailer(u8::try_from(fed).unwrap_or(u8::MAX)); + } + RxStream::Ended => {} + } + if !progress && !input.is_empty() { + // C's "inflate made no progress" + return Err(Fail::Data); + } + if end && !self.holding() && self.rx_stream == RxStream::Running { + self.rx_stream = RxStream::Trailer(0); + } + self.produced(r.bytes_written, end) + } + + /// Counts what was produced against the message's limit, and ends + /// the message once its input and trailer are in and nothing more is + /// owed. + fn produced(&mut self, produced: usize, end: bool) -> Result<Inflated, Fail> { + let room = self.params.max_message.saturating_sub(self.rx_total); + let n = u64::try_from(produced).unwrap_or(u64::MAX); + if n > room { + // up to the limit, then the bomb + let upto = usize::try_from(room).unwrap_or(produced); + self.rx_total = self.params.max_message; + return self.failed_after(Fail::ZipBomb, upto); + } + self.rx_total = self.rx_total.saturating_add(n); + let trailer_in = match self.rx_stream { + RxStream::Trailer(fed) => usize::from(fed) >= TRAILER.len(), + RxStream::Ended => true, + RxStream::Running => false, + }; + self.rx_full = produced >= self.rx_out.len(); + let done = end && !self.holding() && trailer_in && !self.rx_full; + if done { + self.message_received(); + } + Ok(Inflated { produced, done }) + } + + /// Inflating failed with `f`, having given `produced` first: that is + /// given now, and `f` on the next call. + const fn failed_after(&mut self, f: Fail, produced: usize) -> Result<Inflated, Fail> { + if produced == 0 { + return Err(f); + } + self.rx_failed = Some(f); + Ok(Inflated { + produced, + done: false, + }) + } + + /// The message is over: the inflater starts afresh if the peer agreed + /// to no context takeover, or its stream ended. + fn message_received(&mut self) { + let fresh = self.rx_stream == RxStream::Ended + || self.params.peer_takeover(self.side) == Takeover::NotKept; + if fresh { + if let Some(i) = self.inflater.as_mut() { + i.reset(DataFormat::Raw); + } + } + self.rx_total = 0; + self.rx_stream = RxStream::Running; + self.rx_full = false; + } + + /// Begins deflating a message. + pub(crate) fn begin_message(&mut self) { + self.tx_stream = TxStream::Running { frames: 0 }; + self.tx_in.clear(); + self.tx_in_at = 0; + self.tx_out.clear(); + } + + /// The payload of the frame [`Codec::next_frame`] made. + pub(crate) fn tx_frame(&self) -> &[u8] { + &self.tx_frame + } + + /// Takes what the application gives, `pull` filling a buffer and + /// saying how much, `owed` being what it still owes, and makes the + /// next frame if one is ready. `None` if the application must give + /// more first. + pub(crate) fn next_frame( + &mut self, + owed: &mut u64, + pull: &mut dyn FnMut(&mut [u8]) -> usize, + ) -> Result<Option<TxFrame>, Fail> { + loop { + let frames = match self.tx_stream { + TxStream::Idle => return Ok(None), + TxStream::Flushed { frames } => { + let fin = self.tx_out.len() <= TX_CHUNK; + return Ok(Some(self.frame(TX_CHUNK, frames, fin))); + } + TxStream::Running { frames } => frames, + }; + // a frame's worth, but for the four kept back + let keep = TRAILER.len(); + if self.tx_out.len() >= TX_CHUNK.saturating_add(keep) { + return Ok(Some(self.frame(TX_CHUNK, frames, false))); + } + if self.tx_in_at >= self.tx_in.len() && *owed > 0 { + self.tx_in.resize(TX_CHUNK, 0); + let want = usize::try_from(*owed).unwrap_or(usize::MAX).min(TX_CHUNK); + let got = pull(self.tx_in.get_mut(..want).unwrap_or_default()).min(want); + self.tx_in.truncate(got); + self.tx_in_at = 0; + *owed = owed.saturating_sub(u64::try_from(got).unwrap_or(*owed)); + if got == 0 { + return Ok(None); + } + } + let last_input = *owed == 0; + let flush = if last_input { + MZFlush::Sync + } else { + MZFlush::None + }; + let deflater = self.deflater.get_or_insert_with(|| { + Box::new(CompressorOxide::with_params( + DataFormat::Raw, + LEVEL, + CompressionStrategy::Default, + WindowBits::MAX.get(), + )) + }); + let at = self.tx_out.len(); + self.tx_out.resize(at.saturating_add(TX_CHUNK), 0); + let input = self.tx_in.get(self.tx_in_at..).unwrap_or_default(); + let out = self.tx_out.get_mut(at..).unwrap_or_default(); + let r = miniz_oxide::deflate::stream::deflate(deflater, input, out, flush); + self.tx_out.truncate(at.saturating_add(r.bytes_written)); + self.tx_in_at = self.tx_in_at.saturating_add(r.bytes_consumed); + let flushed = match r.status { + Ok(MZStatus::Ok | MZStatus::StreamEnd) => { + last_input && self.tx_in_at >= self.tx_in.len() && r.bytes_written < TX_CHUNK + } + // nothing new since the last flush: the stream is byte + // aligned, and the empty stored block is all there is + Err(MZError::Buf) if last_input && r.bytes_written == 0 => true, + Ok(MZStatus::NeedDict) | Err(_) => return Err(Fail::Deflate), + }; + if flushed { + self.end_flush()?; + self.tx_stream = TxStream::Flushed { frames }; + } + } + } + + /// Removes the trailer of the message's flush, as RFC 7692 7.2.1 has + /// it; an empty message after a flush is the empty stored block's + /// first octet, as C sends it. + fn end_flush(&mut self) -> Result<(), Fail> { + if self.tx_out.is_empty() { + self.tx_out.push(0); + return Ok(()); + } + let n = self.tx_out.len(); + if n < TRAILER.len() || self.tx_out.get(n.saturating_sub(4)..) != Some(&TRAILER[..]) { + return Err(Fail::Deflate); + } + self.tx_out.truncate(n.saturating_sub(4)); + if self.tx_out.is_empty() { + self.tx_out.push(0); + } + Ok(()) + } + + /// Moves `n` compressed bytes into the frame. + fn frame(&mut self, n: usize, frames: u32, fin: bool) -> TxFrame { + let n = n.min(self.tx_out.len()); + self.tx_frame.clear(); + self.tx_frame.extend(self.tx_out.drain(..n)); + let frames_now = frames.saturating_add(1); + self.tx_stream = match (fin, self.tx_stream) { + (true, _) => { + self.message_sent(); + TxStream::Idle + } + (false, TxStream::Flushed { .. }) => TxStream::Flushed { frames: frames_now }, + (false, TxStream::Idle | TxStream::Running { .. }) => { + TxStream::Running { frames: frames_now } + } + }; + TxFrame { + len: n, + first: frames == 0, + fin, + } + } + + /// The message has gone: the deflater starts afresh if this end agreed + /// to no context takeover. + fn message_sent(&mut self) { + if self.params.own_takeover(self.side) == Takeover::NotKept { + if let Some(d) = self.deflater.as_mut() { + d.reset(); + } + } + self.tx_in.clear(); + self.tx_in_at = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::conn::{AsClient, Close, Event, Kind, Role, Ws}; + use alloc::vec; + use npro_core::random::SeededRandom; + use npro_h1::server::TxSource; + + /// A message's payload, given a piece at a time. + struct Src<'a> { + data: &'a [u8], + at: usize, + } + + impl TxSource for Src<'_> { + fn fill(&mut self, buf: &mut [u8]) -> usize { + let rest = self.data.get(self.at..).unwrap_or_default(); + let n = rest.len().min(buf.len()); + buf[..n].copy_from_slice(&rest[..n]); + self.at = self.at.checked_add(n).unwrap(); + n + } + } + + /// What `ws` makes of `frames` handed in pieces of `piece`: the whole + /// messages, and how it closed. + fn reads<P: Role>( + ws: &mut Ws<P>, + frames: &[u8], + piece: usize, + ) -> (Vec<(Kind, Vec<u8>)>, Option<Close>) { + let mut got = Vec::new(); + let mut open: Option<(Kind, Vec<u8>)> = None; + let mut buf = frames.to_vec(); + for chunk in buf.chunks_mut(piece.max(1)) { + let len = chunk.len(); + let mut at = 0; + loop { + let rx = ws.rx(&mut chunk[at..]); + let consumed = rx.consumed; + let given = match rx.event { + Some(Event::Message { + kind, + data, + first, + last, + }) => Some((kind, data.to_vec(), first, last)), + Some(Event::Pong(_) | Event::PeerClose(_)) | None => None, + }; + at = at.checked_add(consumed).unwrap(); + let Some((kind, data, first, last)) = given else { + // what it took to its end, and drained + if (at == len && !ws.rx_pending()) || (consumed == 0 && at < len) { + break; + } + continue; + }; + let m = open.get_or_insert_with(|| (kind, Vec::new())); + assert_eq!(first, m.1.is_empty() && first, "first"); + m.1.extend_from_slice(&data); + if last { + got.push(open.take().unwrap()); + } + } + } + (got, ws.close()) + } + + /// The frames `ws` writes for `msgs`, `limit` bytes at a time. + fn writes<P: Role>(ws: &mut Ws<P>, msgs: &[(Kind, &[u8])], limit: usize) -> Vec<u8> { + let mut wrote = Vec::new(); + let mut buf = vec![0u8; limit]; + for (kind, data) in msgs { + ws.send(*kind, u64::try_from(data.len()).unwrap()).unwrap(); + let mut src = Src { data, at: 0 }; + loop { + let n = ws.tx(&mut buf, &mut src); + if n == 0 { + break; + } + wrote.extend_from_slice(&buf[..n]); + } + assert_eq!(src.at, data.len(), "all of it pulled"); + } + wrote + } + + fn client(params: Params) -> Ws<AsClient<SeededRandom>> { + Ws::client(SeededRandom::new(7)).with_pmd(params) + } + + #[test] + fn rfc_7692s_examples_inflate() { + for (frames, want) in [ + // 7.2.3.1: a message in one frame + ( + &b"\xc1\x07\xf2\x48\xcd\xc9\xc9\x07\x00"[..], + &[&b"Hello"[..]][..], + ), + // 7.2.3.1: the same, in two frames + (b"\x41\x03\xf2\x48\xcd\x80\x04\xc9\xc9\x07\x00", &[b"Hello"]), + // 7.2.3.2: a second message sharing the first's context + ( + b"\xc1\x07\xf2\x48\xcd\xc9\xc9\x07\x00\xc1\x05\xf2\x00\x11\x00\x00", + &[b"Hello", b"Hello"], + ), + // 7.2.3.3: a stored block + ( + b"\xc1\x0b\x00\x05\x00\xfa\xff\x48\x65\x6c\x6c\x6f\x00", + &[b"Hello"], + ), + // 7.2.3.4: BFINAL, then the padding of the flush + (b"\xc1\x08\xf3\x48\xcd\xc9\xc9\x07\x00\x00", &[b"Hello"]), + // 7.2.3.5: two blocks in one message + ( + b"\xc1\x0d\xf2\x48\x05\x00\x00\x00\xff\xff\xca\xc9\xc9\x07\x00", + &[b"Hello"], + ), + ] { + for piece in [1, 3, 64] { + let (got, close) = reads(&mut client(Params::DEFAULT), frames, piece); + let got: Vec<&[u8]> = got.iter().map(|(_, m)| m.as_slice()).collect(); + assert_eq!(got, want, "{} in {piece}s", frames.escape_ascii()); + assert_eq!(close, None); + } + } + } + + #[test] + fn data_after_bfinal_drops_the_connection() { + let (got, close) = reads( + &mut client(Params::DEFAULT), + b"\xc1\x09\xf3\x48\xcd\xc9\xc9\x07\x00\x00\x01", + 64, + ); + assert_eq!(got, Vec::new()); + assert_eq!(close, Some(Close::Release)); + } + + #[test] + fn what_does_not_inflate_drops_the_connection() { + // a block of the reserved type 3 + let (got, close) = reads(&mut client(Params::DEFAULT), b"\xc1\x02\x07\x00", 64); + assert_eq!(got, Vec::new()); + assert_eq!(close, Some(Close::Release)); + } + + /// Bytes from a fixed stream, which do not compress. + fn noise(n: usize) -> Vec<u8> { + let mut r = SeededRandom::new(3); + (0..n).map(|_| r.next_u64().to_le_bytes()[0]).collect() + } + + /// Text that compresses well. + fn words(n: usize) -> Vec<u8> { + b"the quick brown fox jumps over the lazy dog " + .iter() + .copied() + .cycle() + .take(n) + .collect() + } + + #[test] + fn messages_go_both_ways_and_come_back_whole() { + // Miri interprets, thousands of times slower: the bounds, but not + // a message of many chunks + let sizes: &[usize] = if cfg!(miri) { + &[0, 1, 1023, 1025] + } else { + &[0, 1, 5, 1023, 1024, 1025, 3000, 70_000] + }; + let no_takeover = Params { + server_takeover: Takeover::NotKept, + client_takeover: Takeover::NotKept, + ..Params::DEFAULT + }; + // a server may say its window is small: npro's is not, nor need + // its inflater mind + let small = Params { + server_window: WindowBits::new(9).unwrap(), + ..Params::DEFAULT + }; + for params in [Params::DEFAULT, no_takeover, small] { + let mut payloads = Vec::new(); + for &n in sizes { + payloads.push((Kind::Text, words(n))); + payloads.push((Kind::Binary, noise(n))); + } + // an empty message straight after another + payloads.push((Kind::Text, Vec::new())); + let msgs: Vec<(Kind, &[u8])> = + payloads.iter().map(|(k, d)| (*k, d.as_slice())).collect(); + + let splits: &[(usize, usize)] = if cfg!(miri) { + &[(64, 7)] + } else { + &[(4, 7), (1500, 4096), (64, 1)] + }; + for &(limit, piece) in splits { + let how = (params, limit, piece); + // client to server + let up = writes(&mut client(params), &msgs, limit); + assert_eq!( + reads(&mut Ws::server(b"").with_pmd(params), &up, piece), + (payloads.clone(), None), + "up {how:?}" + ); + // server to client + let down = writes(&mut Ws::server(b"").with_pmd(params), &msgs, limit); + assert_eq!( + reads(&mut client(params), &down, piece), + (payloads.clone(), None), + "down {how:?}" + ); + } + } + } + + #[test] + fn compressed_frames_carry_rsv1_on_the_first_only() { + let data = noise(3000); + let frames = writes( + &mut Ws::server(b"").with_pmd(Params::DEFAULT), + &[(Kind::Binary, &data)], + 4096, + ); + // three frames of noise: the first binary with RSV1, then + // continuations, the last with FIN + assert_eq!(frames[0], 0x42); + let mut at = 0; + let mut firsts = Vec::new(); + while at < frames.len() { + firsts.push(frames[at]); + let len = match frames[at + 1] & 0x7f { + 126 => { + let l = usize::from(u16::from_be_bytes([frames[at + 2], frames[at + 3]])); + at += 4; + l + } + n => { + at += 2; + usize::from(n) + } + }; + assert!(len <= TX_CHUNK); + at += len; + } + assert_eq!(firsts.first(), Some(&0x42)); + assert_eq!(firsts.last(), Some(&0x80)); + assert!(firsts[1..firsts.len() - 1].iter().all(|b| *b == 0)); + } + + #[test] + fn a_message_past_its_limit_is_a_zip_bomb() { + let zeros = vec![0u8; 5000]; + let frames = writes( + &mut client(Params::DEFAULT), + &[(Kind::Binary, &zeros)], + 4096, + ); + assert!(frames.len() < 100, "zeros compress"); + let limited = Params::DEFAULT.with_max_message(4999); + assert_eq!( + reads(&mut Ws::server(b"").with_pmd(limited), &frames, 4096), + (Vec::new(), Some(Close::Release)) + ); + // and at the limit, it is not + let exact = Params::DEFAULT.with_max_message(5000); + assert_eq!( + reads(&mut Ws::server(b"").with_pmd(exact), &frames, 4096), + (vec![(Kind::Binary, zeros)], None) + ); + } + + #[test] + fn rsv1_is_refused_where_rfc_7692_forbids_it() { + for (frames, close) in [ + // on a continuation + ( + &b"\x01\x82\0\0\0\0He\xc0\x83\0\0\0\0llo"[..], + &b"\x88\x0a\x03\xearsv bits"[..], + ), + // on a control frame + (b"\xc9\x80\0\0\0\0", b"\x88\x0a\x03\xearsv bits"), + // RSV2 with it + (b"\xe1\x80\0\0\0\0", b"\x88\x0a\x03\xearsv bits"), + ] { + let mut ws = Ws::server(b"").with_pmd(Params::DEFAULT); + let (got, _) = reads(&mut ws, frames, 64); + assert_eq!(got, Vec::new()); + let mut out = [0u8; 64]; + let n = ws.tx(&mut out, &mut Src { data: b"", at: 0 }); + assert_eq!(&out[..n], close, "{}", frames.escape_ascii()); + } + } + + #[test] + fn a_server_takes_the_first_offer_and_echoes_what_it_took() { + use npro_h1::head::{Config, Head, Side as HeadSide}; + for (offer, line, params) in [ + ( + &b"permessage-deflate"[..], + Some(&b"permessage-deflate"[..]), + Params::DEFAULT, + ), + ( + b"x-webkit-deflate-frame, permessage-deflate; server_no_context_takeover; \ + client_max_window_bits, permessage-deflate", + Some(b"permessage-deflate; server_no_context_takeover"), + Params { + server_takeover: Takeover::NotKept, + ..Params::DEFAULT + }, + ), + // a smaller window than it keeps to declines the offer, and + // the next is taken + ( + b"permessage-deflate; server_max_window_bits=\"10\", \ + permessage-deflate; client_no_context_takeover", + Some(b"permessage-deflate; client_no_context_takeover"), + Params { + client_takeover: Takeover::NotKept, + ..Params::DEFAULT + }, + ), + ( + b"permessage-deflate; server_max_window_bits=16", + None, + Params::DEFAULT, + ), + ( + b"permessage-deflate; server_max_window_bits=15", + Some(b"permessage-deflate; server_max_window_bits=15"), + Params::DEFAULT, + ), + (b"x-foo", None, Params::DEFAULT), + ] { + let mut req = b"GET / HTTP/1.1\r\nSec-WebSocket-Extensions: ".to_vec(); + req.extend_from_slice(offer); + req.extend_from_slice(b"\r\n\r\n"); + let mut h = Head::new([0u8; 2048], HeadSide::Server, Config::new()).unwrap(); + h.rx(&req).unwrap(); + let a = server_accept(h.table()).unwrap(); + match (a, line) { + (Some(a), Some(line)) => { + let mut want = b"sec-websocket-extensions: ".to_vec(); + want.extend_from_slice(line); + want.extend_from_slice(b"\r\n"); + assert_eq!(a.header_lines(), want.as_slice()); + assert_eq!(a.params(), params); + } + (None, None) => {} + (a, line) => panic!("{a:?} for {line:?}"), + } + } + } + + #[test] + fn a_client_takes_only_rfc_7692s_answers() { + assert_eq!(client_accept(b"permessage-deflate"), Ok(Params::DEFAULT)); + assert_eq!( + client_accept( + b"permessage-deflate; server_no_context_takeover; client_max_window_bits=15; \ + server_max_window_bits=\"12\"" + ), + Ok(Params { + server_takeover: Takeover::NotKept, + server_window: WindowBits::new(12).unwrap(), + ..Params::DEFAULT + }) + ); + for bad in [ + &b"x-foo"[..], + b"permessage-deflate, permessage-deflate", + b"permessage-deflate; rx_buf_size=10", + b"permessage-deflate; server_max_window_bits", + b"permessage-deflate; server_max_window_bits=7", + b"permessage-deflate; client_max_window_bits=14", + b"permessage-deflate; server_no_context_takeover=1", + b"permessage-deflate; server_no_context_takeover; server_no_context_takeover", + ] { + assert_eq!( + client_accept(bad), + Err(ClientRefusal::Extension), + "{}", + bad.escape_ascii() + ); + } + } +}
Page fetched 0s ago, creation time: 9ms (vhost etag hits: 0%, cache hits: 0%)