| //! 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);
}
}
}
|