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 / requests / headers-repeated.http
Author[]Andy Green <andy@warmcat.com> 2026-10-05 16:38 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 19:18 UTC
Tree4e7547cd5130568bf547f64b1e9da36dd210072f   Raw Patch
 
npro-test: npro's ws client replays C's ws client transcripts
npro-test: npro's ws client replays C's ws client transcripts

tests/ws_client_replay.rs makes C's api-test-sansio client connection, a
GET of /echo offering "echo", with C's seeded random: npro's h1 client
writes the request with npro-ws' upgrade lines, the response is checked
as C checks it, and the connection is then npro-ws', masking from the
same stream.  The app is C's callback_client, which sends "Hello" once
established unless the case has it quiet.

The request, every masked frame written four bytes at a time, the app's
messages and where C closed must be the transcript's, for ws-client,
ws-client-interim, ws-client-ping-close, ws-client-huge-frame,
ws-client-rsv1-no-ext and ws-client-rsv2.

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/Cargo.toml b/crates/npro-test/Cargo.toml index 7d31f67..100fc95 100644 --- a/crates/npro-test/Cargo.toml +++ b/crates/npro-test/Cargo.toml @@ -10,7 +10,7 @@ homepage.workspace = true repository.workspace = true [dev-dependencies] -npro-core = { path = "../npro-core" } +npro-core = { path = "../npro-core", features = ["replay"] } npro-h1 = { path = "../npro-h1" } npro-ws = { path = "../npro-ws" } diff --git a/crates/npro-test/tests/ws_client_replay.rs b/crates/npro-test/tests/ws_client_replay.rs new file mode 100644 index 0000000..fa92cfe --- /dev/null +++ b/crates/npro-test/tests/ws_client_replay.rs @@ -0,0 +1,235 @@ +//! npro's ws client replays C's ws client transcripts byte for byte. +//! +//! Each is one connection C's `api-test-sansio` makes: a GET of `/echo` to +//! `sansio`, port 80, with C's defaults (no-cache headers, an `Origin`), +//! offering the subprotocol `echo`, its random C's seeded stream. npro's +//! h1 client writes the request, its upgrade lines and key from npro-ws; +//! the response is checked as C checks it, and the connection is then +//! npro-ws', masking from the same stream. The app is C's +//! `callback_client`: once established it sends `Hello`, unless the case +//! has it quiet. +//! +//! What npro writes after each `rx` must be the transcript's `tx` bytes, +//! the messages it hands the app its `app_rx` bytes, and it must ask to be +//! done with the connection exactly where C closed it, its `close`. It +//! writes four bytes at a time, so every frame goes in pieces. + +// held to clippy's rules for tests +#[cfg(test)] +mod ws_client_replay { + use npro_core::random::SeededRandom; + use npro_h1::client::{Client, Connection, Event as H1Event, Request, Scheme}; + use npro_h1::server::TxSource; + use npro_h1::table::DEFAULT_CAPACITY; + use npro_test::{StepKind, Transcript, vendored}; + use npro_ws::conn::{AsClient, Event, Kind, Ws}; + use npro_ws::handshake::{ClientKey, MAX_REQUEST_LINES}; + + /// The ws client transcripts this phase replays, and whether the app + /// is quiet: not the permessage-deflate ones, the next phase's, nor the + /// digest retry, which needs digest auth. + const CASES: [(&str, bool); 6] = [ + ("ws-client", false), + ("ws-client-interim", false), + ("ws-client-ping-close", true), + ("ws-client-huge-frame", false), + ("ws-client-rsv1-no-ext", false), + ("ws-client-rsv2", false), + ]; + + /// The subprotocols offered. + const OFFERED: &[u8] = b"echo"; + + /// How much is written at a time. + const TX_LIMIT: usize = 4; + + /// C's `callback_client`, as far as ws goes. + #[derive(Default)] + struct ClientApp { + quiet: bool, + /// What it was given, for the transcript's `app_rx`. + app_rx: Vec<u8>, + out: Vec<u8>, + at: usize, + } + + impl TxSource for ClientApp { + fn fill(&mut self, buf: &mut [u8]) -> usize { + let rest = &self.out[self.at..]; + let n = rest.len().min(buf.len()); + buf[..n].copy_from_slice(&rest[..n]); + self.at = self.at.checked_add(n).unwrap(); + n + } + } + + impl ClientApp { + /// `LWS_CALLBACK_CLIENT_ESTABLISHED`, then its writeable. + fn established(&mut self, ws: &mut Ws<AsClient<SeededRandom>>) { + if self.quiet { + return; + } + self.out = b"Hello".to_vec(); + self.at = 0; + ws.send(Kind::Text, 5).unwrap(); + } + } + + /// The connection: h1 until the 101 is in, then ws, which takes the + /// random stream on from the key's draw. + enum Conn { + H1 { + client: Box<Client<Vec<u8>>>, + key: ClientKey, + random: Option<SeededRandom>, + }, + Ws(Box<Ws<AsClient<SeededRandom>>>), + } + + /// Hands `input` to the connection, with the app answering, until + /// neither takes or writes anything more; returns what was written. + fn feed(conn: &mut Conn, app: &mut ClientApp, input: &mut [u8]) -> Vec<u8> { + let mut wrote = Vec::new(); + let mut buf = [0u8; TX_LIMIT]; + let mut at = 0usize; + loop { + let mut progress = false; + match conn { + Conn::H1 { + client, + key, + random, + } => { + let rx = client.rx(&input[at..]).unwrap(); + at = at.checked_add(rx.consumed).unwrap(); + progress |= rx.consumed > 0; + if rx.event == Some(H1Event::Response) { + assert!(client.is_upgraded()); + let chosen = key + .check(client.status(), client.response(), Some(OFFERED)) + .unwrap(); + assert_eq!(chosen, Some(OFFERED)); + let mut ws = Ws::client(random.take().unwrap()); + app.established(&mut ws); + *conn = Conn::Ws(Box::new(ws)); + continue; + } + } + Conn::Ws(ws) => { + let rx = ws.rx(&mut input[at..]); + let consumed = rx.consumed; + if let Some(Event::Message { data, .. }) = rx.event { + app.app_rx.extend_from_slice(data); + } + at = at.checked_add(consumed).unwrap(); + progress |= consumed > 0; + loop { + let n = ws.tx(&mut buf, app); + wrote.extend_from_slice(&buf[..n]); + if n == 0 { + break; + } + progress = true; + } + } + } + if !progress { + return wrote; + } + } + } + + fn replay(t: &Transcript, quiet: bool) { + // C's run was seeded: its random is in the transcript + let seed = t.seed.unwrap_or_else(|| panic!("{}: not seeded", t.case)); + 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 mut client = Client::new( + vec![0u8; DEFAULT_CAPACITY], + Request { + method: b"GET", + path: b"/echo", + host: Some(b"sansio"), + origin: Some(b"sansio"), + scheme: Scheme::Http, + no_cache: true, + connection: Connection::Upgrade(&lines[..lines_len]), + }, + ) + .unwrap(); + + let mut steps = t.steps.iter().peekable(); + let Some(StepKind::Tx(request)) = steps.next().map(|s| &s.kind) else { + panic!("{}: does not start with the request", t.case); + }; + let mut out = [0u8; 512]; + let n = client.tx(&mut out); + assert_eq!( + out[..n].escape_ascii().to_string(), + request.escape_ascii().to_string(), + "{}: the request", + t.case + ); + + let mut conn = Conn::H1 { + client: Box::new(client), + key, + random: Some(random), + }; + let mut app = ClientApp { + quiet, + ..ClientApp::default() + }; + while let Some(step) = steps.next() { + let StepKind::Rx(rx) = &step.kind else { + panic!("{}: {:?} with no rx before it", t.case, step.kind); + }; + let (mut want, mut want_app, mut want_close) = (Vec::new(), Vec::new(), false); + while let Some(next) = steps.peek() { + match &next.kind { + StepKind::Tx(b) => want.extend_from_slice(b), + StepKind::AppRx(b) => want_app.extend_from_slice(b), + StepKind::Close => want_close = true, + StepKind::Rx(_) => break, + } + steps.next(); + } + let mut input = rx.clone(); + let got = feed(&mut conn, &mut app, &mut input); + assert_eq!( + got.escape_ascii().to_string(), + want.escape_ascii().to_string(), + "{} at {}us", + t.case, + step.t_us + ); + assert_eq!( + core::mem::take(&mut app.app_rx), + want_app, + "{} at {}us: the app's", + t.case, + step.t_us + ); + let closed = match &conn { + Conn::Ws(ws) => ws.close().is_some(), + Conn::H1 { .. } => false, + }; + assert_eq!(closed, want_close, "{} at {}us: closed", t.case, step.t_us); + } + } + + #[test] + #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")] + fn cs_ws_client_transcripts_replay_byte_for_byte() { + let all = vendored().unwrap(); + for (case, quiet) in CASES { + let t = all + .iter() + .find(|t| t.case == case) + .unwrap_or_else(|| panic!("no transcript {case}")); + replay(t, quiet); + } + } +}
Page fetched 0s ago, creation time: 2ms (vhost etag hits: 0%, cache hits: 0%)