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 / fuzz / seeds / h1-response / unknown-headers.http
Author[]Andy Green <andy@warmcat.com> 2026-10-04 06:24 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC
Treefcbc972e9db004e98f3975d4b0343efce8e736c3   Raw Patch
 
npro-core: the connection state machines, from C's event table
npro-core: the connection state machines, from C's event table

C lws' connection state is four machines, specified by C's
README.wsi-state-machines.md and driven by the event table in
lib/sansio/wsi-state.c.  Port them for the stage-1 roles: h1 client and
server, ws, and raw sockets.

npro_core::state::Machines holds the transport, carrier, live
(transaction) and close machines, each its own enum, with the role, the
side and the socket's usability, all private.  Nothing sets them but
Machines::event(), and event_as() for the events that bring a role the
site chooses: code driving a connection says what happened, as an
Event, and the table says where that leads, by role, side and the state
the connection reports.

The table is one match on the event, exhaustive, so no event exists
without its rows; within it the rows are C's, in C's order, first match
winning, with `_` for C's "*" and ANY.  The setters are C's: a live state
ends any transport phase, a handshake-named state is the carrier's until
the carrier is established, a role change keeps the close and the
socket's state except for a restart onto a new connection.

What C refuses, or LWS_WITH_STATE_CHECK aborts on, is refused, with the
machines unchanged: no row, the close going backwards, a live state
while a client restarts, a polite close phase entered with the socket
unusable, a staged shutdown on a raw socket.  Like C's check, an edge
that changes no reported state, role or side is not held to the
invariants.

Each change gives an Edge, whose Display is the line C's
LWS_WITH_STATE_TRACE writes for it, less the connection's tag, so the
port's edges compare with C's directly.  No feature gates it: the IO
side writes lines, if it wants them.

These are one enum per machine shared by the roles, as C's are, rather
than the per-role enums the port plan first described: the close
machine is one ordered sequence the ws phases sit inside, which a role
change carries, and keeping C's shape lets the port be held to C's table
row by row.  The plan says so.

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-core/src/lib.rs b/crates/npro-core/src/lib.rs index d3cb55e..879caa4 100644 --- a/crates/npro-core/src/lib.rs +++ b/crates/npro-core/src/lib.rs @@ -11,5 +11,6 @@ pub mod base64; pub mod random; pub mod sha1; +pub mod state; pub mod time; pub mod utf8; diff --git a/crates/npro-core/src/state/event.rs b/crates/npro-core/src/state/event.rs new file mode 100644 index 0000000..c73ab88 --- /dev/null +++ b/crates/npro-core/src/state/event.rs @@ -0,0 +1,235 @@ +//! What happened to a connection, as the code driving it reports it. + +/// Something that happened to a connection: C's `LWS_WSIEV_*`. +/// +/// Code driving a connection does not choose the state it goes to; it says +/// what happened, and the event table ([`Machines::event`]) says where that +/// leads by role, side and state. These are the events of the roles npro +/// has: h1 client and server, ws, and raw sockets. Those of h2, h3, quic, +/// mqtt and webtransport arrive with their roles. +/// +/// [`Machines::event`]: super::Machines::event +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Event { + /// The transport finished: connect and any tls are done. + TransportUp, + /// A server parsed a request's headers. + ReqHdrsComplete, + /// The h1 server decided a request asking to upgrade stays http. + ReqPlainHttp, + /// The server began acting on a request. + ActionBegin, + /// A request body began. + BodyBegin, + /// A request body is complete. + BodyComplete, + /// The user finished before reading the request body: drain it. + BodyDiscard, + /// The user completed the transaction while its answer was queued. + TxnCompleting, + /// The transaction completed. + TxnCompleted, + /// Writable after the completion, with buffered tx drained. + TxnDrained, + /// Serving a file began. + FileBegin, + /// A read of the file was handed to a worker. + FileReadQueued, + /// The worker's read of the file came back. + FileReadDone, + /// The file was sent. + FileComplete, + /// An h1 client's tcp is up, before its tls. + SocketConnected, + /// A client request was queued behind another connection. + Queued, + /// A client connection issues a request. + ReqIssue, + /// The request headers went, and no body follows. + ReqHdrsSent, + /// The request headers went, and a body follows. + ReqHdrsSentBody, + /// The request body went. + ReqBodySent, + /// A 1xx interim response arrived. + RespInterim, + /// A new connection is a server's. + ServerSide, + /// A connection was adopted, without tls. + Adopted, + /// A connection was adopted, to accept tls on. + AdoptedTls, + /// A client connection was bound to its role. + ClientBind, + /// A client restarts on a new connection: a redirect, a retry, a + /// fallback. + Restart, + /// The connection became ws: a server's upgrade decision, a client's + /// 101. + WsUpgraded, + /// A client's response headers arrived. + RespHdrs, + /// The connection became a raw socket. + RawUpgraded, + /// A client's dns lookup started. + DnsStart, + /// A client's dns lookup is to be tried again. + DnsRetry, + /// A client's connect started, to the first or the next address. + ConnectStart, + /// An http CONNECT went to the proxy. + ProxyConnectSent, + /// The socks5 greeting went. + SocksGreetingSent, + /// The socks5 authentication went. + SocksAuthSent, + /// The socks5 connect request went. + SocksConnectSent, + /// A client's tls handshake started. + TlsStart, + /// A server's tls accept is in progress. + TlsAcceptPending, + /// A server's tls accept was handed to a worker. + TlsAcceptQueued, + /// A client's connect failed, and the user was told. + ConnFailed, + /// A client is being retargeted, and will restart. + Retarget, + /// We started a ws close, and have a CLOSE to send. + WsCloseInitiated, + /// Our ws CLOSE went. + WsCloseSent, + /// The peer's ws CLOSE arrived. + WsPeerClose, + /// The close was entered. + CloseEntered, + /// The close drains buffered tx first. + CloseFlush, + /// A live connection is to close once its buffered tx drains. + CloseWhenFlushed, + /// A server half-closed, and waits for the peer's FIN. + CloseStaged, + /// The socket is gone. + SocketGone, + /// The user was told the connection closed. + UserTold, +} + +impl Event { + /// Every event. + pub const ALL: [Self; 50] = [ + Self::TransportUp, + Self::ReqHdrsComplete, + Self::ReqPlainHttp, + Self::ActionBegin, + Self::BodyBegin, + Self::BodyComplete, + Self::BodyDiscard, + Self::TxnCompleting, + Self::TxnCompleted, + Self::TxnDrained, + Self::FileBegin, + Self::FileReadQueued, + Self::FileReadDone, + Self::FileComplete, + Self::SocketConnected, + Self::Queued, + Self::ReqIssue, + Self::ReqHdrsSent, + Self::ReqHdrsSentBody, + Self::ReqBodySent, + Self::RespInterim, + Self::ServerSide, + Self::Adopted, + Self::AdoptedTls, + Self::ClientBind, + Self::Restart, + Self::WsUpgraded, + Self::RespHdrs, + Self::RawUpgraded, + Self::DnsStart, + Self::DnsRetry, + Self::ConnectStart, + Self::ProxyConnectSent, + Self::SocksGreetingSent, + Self::SocksAuthSent, + Self::SocksConnectSent, + Self::TlsStart, + Self::TlsAcceptPending, + Self::TlsAcceptQueued, + Self::ConnFailed, + Self::Retarget, + Self::WsCloseInitiated, + Self::WsCloseSent, + Self::WsPeerClose, + Self::CloseEntered, + Self::CloseFlush, + Self::CloseWhenFlushed, + Self::CloseStaged, + Self::SocketGone, + Self::UserTold, + ]; + + /// The event's name, as C's trace writes it after `ev=`. + /// + /// ``` + /// use npro_core::state::Event; + /// + /// assert_eq!(Event::ReqHdrsComplete.name(), "REQ_HDRS_COMPLETE"); + /// ``` + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::TransportUp => "TRANSPORT_UP", + Self::ReqHdrsComplete => "REQ_HDRS_COMPLETE", + Self::ReqPlainHttp => "REQ_PLAIN_HTTP", + Self::ActionBegin => "ACTION_BEGIN", + Self::BodyBegin => "BODY_BEGIN", + Self::BodyComplete => "BODY_COMPLETE", + Self::BodyDiscard => "BODY_DISCARD", + Self::TxnCompleting => "TXN_COMPLETING", + Self::TxnCompleted => "TXN_COMPLETED", + Self::TxnDrained => "TXN_DRAINED", + Self::FileBegin => "FILE_BEGIN", + Self::FileReadQueued => "FILE_READ_QUEUED", + Self::FileReadDone => "FILE_READ_DONE", + Self::FileComplete => "FILE_COMPLETE", + Self::SocketConnected => "SOCKET_CONNECTED", + Self::Queued => "QUEUED", + Self::ReqIssue => "REQ_ISSUE", + Self::ReqHdrsSent => "REQ_HDRS_SENT", + Self::ReqHdrsSentBody => "REQ_HDRS_SENT_BODY", + Self::ReqBodySent => "REQ_BODY_SENT", + Self::RespInterim => "RESP_INTERIM", + Self::ServerSide => "SERVER_SIDE", + Self::Adopted => "ADOPTED", + Self::AdoptedTls => "ADOPTED_TLS", + Self::ClientBind => "CLIENT_BIND", + Self::Restart => "RESTART", + Self::WsUpgraded => "WS_UPGRADED", + Self::RespHdrs => "RESP_HDRS", + Self::RawUpgraded => "RAW_UPGRADED", + Self::DnsStart => "DNS_START", + Self::DnsRetry => "DNS_RETRY", + Self::ConnectStart => "CONNECT_START", + Self::ProxyConnectSent => "PROXY_CONNECT_SENT", + Self::SocksGreetingSent => "SOCKS_GREETING_SENT", + Self::SocksAuthSent => "SOCKS_AUTH_SENT", + Self::SocksConnectSent => "SOCKS_CONNECT_SENT", + Self::TlsStart => "TLS_START", + Self::TlsAcceptPending => "TLS_ACCEPT_PENDING", + Self::TlsAcceptQueued => "TLS_ACCEPT_QUEUED", + Self::ConnFailed => "CONN_FAILED", + Self::Retarget => "RETARGET", + Self::WsCloseInitiated => "WS_CLOSE_INITIATED", + Self::WsCloseSent => "WS_CLOSE_SENT", + Self::WsPeerClose => "WS_PEER_CLOSE", + Self::CloseEntered => "CLOSE_ENTERED", + Self::CloseFlush => "CLOSE_FLUSH", + Self::CloseWhenFlushed => "CLOSE_WHEN_FLUSHED", + Self::CloseStaged => "CLOSE_STAGED", + Self::SocketGone => "SOCKET_GONE", + Self::UserTold => "USER_TOLD", + } + } +} diff --git a/crates/npro-core/src/state/mod.rs b/crates/npro-core/src/state/mod.rs new file mode 100644 index 0000000..48a724a --- /dev/null +++ b/crates/npro-core/src/state/mod.rs @@ -0,0 +1,987 @@ +//! The connection state machines: transport, carrier, transaction and +//! close, and the event table that drives them. +//! +//! This is C's `lib/sansio/wsi-state.c`, and its specification is C's +//! `READMEs/README.wsi-state-machines.md`. A connection's state is four +//! machines, each with its own enum: +//! +//! | machine | what it tracks | +//! |---|---| +//! | [`Transport`] | getting a socket to the peer: dns, connect, proxy or socks, the tls handshake or accept | +//! | [`Carrier`] | the protocol handshake on top of the socket: an h1 client's first request and reply, the h1 server's upgrade decision | +//! | [`Live`] | the transaction: the http request and response, its body and file phases, or `Established` for roles without transactions | +//! | [`Close`] | the polite ws close, draining buffered tx, the staged shutdown, dead | +//! +//! with the connection's [`Role`] and [`Side`], and whether its socket is +//! known [unusable](Socket). +//! +//! Nothing sets a machine directly. Code driving a connection reports what +//! happened as an [`Event`], and [`Machines::event`] looks up where that +//! leads, by role, side and the state the connection reports +//! ([`Machines::state`]): one row of C's event table per edge, in C's +//! order, the first match winning. An event with no row is refused, as is +//! anything C's `LWS_WITH_STATE_CHECK` would abort on: a close going back +//! to an earlier phase, a live state set while the connection restarts, +//! and a broken invariant. The machines are unchanged by a refusal. +//! +//! Each change gives an [`Edge`], whose `Display` is the line C's +//! `LWS_WITH_STATE_TRACE` writes for it, less the connection's tag. npro's +//! tests compare these with the edges C's test suite takes. +//! +//! These are the machines of the roles npro has: h1 client and server, ws, +//! and raw sockets. The rows, states and events of h2, h3, quic, mqtt and +//! webtransport arrive with those roles. +//! +//! ``` +//! use npro_core::state::{Event, Machines, Role, State}; +//! +//! // an h1 server connection: born, made a server's, adopted +//! let mut m = Machines::new(); +//! m.event(Event::ServerSide)?; +//! m.event_as(Event::Adopted, Role::H1)?; +//! assert_eq!(m.state(), State::Headers); +//! +//! // a request with an Upgrade: header, which becomes ws +//! let e = m.event(Event::ReqHdrsComplete)?; +//! assert_eq!( +//! e.to_string(), +//! "LRS h1/S:HEADERS -> h1/S:H1_UPGRADE set_state ev=REQ_HDRS_COMPLETE" +//! ); +//! m.event(Event::WsUpgraded)?; +//! assert_eq!((m.role(), m.state()), (Role::Ws, State::Established)); +//! +//! // a ws connection cannot be told the request body is complete +//! assert!(m.event(Event::BodyComplete).is_err()); +//! # Ok::<(), npro_core::state::Refused>(()) +//! ``` + +mod event; +mod table; + +use core::fmt; + +pub use event::Event; + +use table::{RoleTo, SideTo, To}; + +/// What a connection is: which protocol drives it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Role { + /// No role yet: a connection just born. + None, + /// http/1. + H1, + /// websockets, after the upgrade. + Ws, + /// A raw socket: bytes in and out, no protocol. + RawSkt, +} + +impl Role { + /// Every role. + pub const ALL: [Self; 4] = [Self::None, Self::H1, Self::Ws, Self::RawSkt]; + + /// The role's name, as C's trace writes it. + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::None => "(none)", + Self::H1 => "h1", + Self::Ws => "ws", + Self::RawSkt => "raw-skt", + } + } +} + +/// Which side of the connection we are. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Side { + /// Not decided yet: a connection just born. + Unset, + /// We connected to the peer. + Client, + /// The peer connected to us. + Server, +} + +impl Side { + /// Every side. + pub const ALL: [Self; 3] = [Self::Unset, Self::Client, Self::Server]; + + /// The side's letter, as C's trace writes it. + #[must_use] + pub const fn letter(self) -> char { + match self { + Self::Unset => '-', + Self::Client => 'C', + Self::Server => 'S', + } + } +} + +/// Whether the connection's socket can still be used. +/// +/// An attribute of the connection rather than a machine: it survives +/// changes of state and role, except a client's restart onto a new +/// connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Socket { + /// As far as anyone knows, the socket works. + Usable, + /// The socket is known dead: the close takes the abortive path, and + /// never the polite one. + Unusable, +} + +/// The transport machine: getting a socket to the peer. +/// +/// It ends implicitly: setting any live or carrier state clears it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Transport { + /// Not setting up a transport. + None, + /// A client is looking up the peer's address. + WaitingDns, + /// A client is connecting. + WaitingConnect, + /// A client sent an http CONNECT to a proxy, and waits for its reply. + WaitingProxyReply, + /// A client's tls handshake is in progress. + WaitingSsl, + /// A client sent the socks5 greeting. + WaitingSocksGreetingReply, + /// A client sent the socks5 connect request. + WaitingSocksConnectReply, + /// A client sent socks5 authentication. + WaitingSocksAuthReply, + /// A server's tls accept has not started. + SslInit, + /// A server's tls accept is in progress. + SslAckPending, + /// A server's tls accept is out on a worker. + AwaitingSslAccept, + /// A client's connect failed, and the user was told: the close must not + /// tell him again. + Failed, + /// A client is being retargeted, and will restart on a new connection. + Restarting, +} + +/// The carrier machine: the protocol handshake between the transport and +/// the first transaction. +/// +/// Once the first transaction state is set it is `Established`, and the +/// same names set again are per-transaction phases of the [`Live`] machine. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Carrier { + /// The handshake has not started. + None, + /// An h1 client, before tls. + H1cIssueHandshake, + /// An h1 client, sending its first request. + H1cIssueHandshake2, + /// A client waiting for its first response's headers. + WaitingServerReply, + /// A client request queued behind another connection. + H2WaitingToSendHeaders, + /// An h1 server deciding on an upgrade. + H1Upgrade, + /// The handshake is done: the transaction machine has begun. + Established, +} + +/// The live machine: the transaction, or `Established` for roles that have +/// none. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Live { + /// Nothing yet. + Unconnected, + /// An h1 client, before tls. + H1cIssueHandshake, + /// An h1 client sending a request. + H1cIssueHandshake2, + /// A client request is out, its response's headers pending. + WaitingServerReply, + /// A client request queued behind another connection. + H2WaitingToSendHeaders, + /// An h1 server deciding on an upgrade a request asked for. + H1Upgrade, + /// A client's request headers went, and it is sending the body. + IssueHttpBody, + /// A server idle between requests, or reading one's headers. + Headers, + /// A server acting on a request; a client receiving a response; a role + /// without transactions, in use. + Established, + /// A server's action is in progress, or a request body is complete and + /// its answer is to come. + DoingTransaction, + /// A request body is being delivered. + Body, + /// A request body is being drained, unread. + DiscardBody, + /// A file is being served. + IssuingFile, + /// A read of the file being served is out on a worker. + AwaitingFileRead, + /// The transaction was completed while its answer was still queued. + TxnCompleting, + /// The transaction completed; waiting for buffered tx to drain. + TxnCompleted, + /// A client with nothing in flight, kept for the next request. + Idling, +} + +/// The close machine. It only goes forwards: the variants are in order, +/// and no event takes a connection to an earlier one. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Close { + /// Not closing. + None, + /// A live connection closes once its buffered tx has drained. + CloseWhenFlushed, + /// The close was entered, with nothing yet to wait for. + Closing, + /// We started a ws close and have a CLOSE to send. + WaitingToSendClose, + /// The peer's ws CLOSE arrived first, and we answer it. + ReturnedClose, + /// Our ws CLOSE went, and we wait for the peer's. + AwaitingCloseAck, + /// The close drains buffered tx first. + FlushingBeforeClose, + /// A server half-closed, and waits for the peer's FIN. + Shutdown, + /// The socket is gone. + DeadSocket, + /// The socket is gone, and the user was told. + UserTold, +} + +/// The state a connection reports: one name for what it is doing now. +/// +/// The close machine if one is in progress, else the transport machine, +/// else the carrier handshake, else the live machine: C's `lwsi_state()`, +/// whose names these are. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[expect( + missing_docs, + reason = "each variant is the like-named state of the machine it reports" +)] +pub enum State { + Unconnected, + WaitingDns, + WaitingConnect, + WaitingProxyReply, + WaitingSsl, + WaitingSocksGreetingReply, + WaitingSocksConnectReply, + WaitingSocksAuthReply, + SslInit, + SslAckPending, + AwaitingSslAccept, + H1Upgrade, + WaitingServerReply, + H2WaitingToSendHeaders, + TxnCompleted, + Idling, + H1cIssueHandshake, + H1cIssueHandshake2, + IssueHttpBody, + IssuingFile, + Headers, + Body, + DiscardBody, + Established, + DoingTransaction, + WaitingToSendClose, + ReturnedClose, + AwaitingCloseAck, + FlushingBeforeClose, + Shutdown, + DeadSocket, + AwaitingFileRead, + TxnCompleting, +} + +impl State { + /// Every state. + pub const ALL: [Self; 33] = [ + Self::Unconnected, + Self::WaitingDns, + Self::WaitingConnect, + Self::WaitingProxyReply, + Self::WaitingSsl, + Self::WaitingSocksGreetingReply, + Self::WaitingSocksConnectReply, + Self::WaitingSocksAuthReply, + Self::SslInit, + Self::SslAckPending, + Self::AwaitingSslAccept, + Self::H1Upgrade, + Self::WaitingServerReply, + Self::H2WaitingToSendHeaders, + Self::TxnCompleted, + Self::Idling, + Self::H1cIssueHandshake, + Self::H1cIssueHandshake2, + Self::IssueHttpBody, + Self::IssuingFile, + Self::Headers, + Self::Body, + Self::DiscardBody, + Self::Established, + Self::DoingTransaction, + Self::WaitingToSendClose, + Self::ReturnedClose, + Self::AwaitingCloseAck, + Self::FlushingBeforeClose, + Self::Shutdown, + Self::DeadSocket, + Self::AwaitingFileRead, + Self::TxnCompleting, + ]; + + /// The state's name, as C's trace writes it. + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::Unconnected => "UNCONNECTED", + Self::WaitingDns => "WAITING_DNS", + Self::WaitingConnect => "WAITING_CONNECT", + Self::WaitingProxyReply => "WAITING_PROXY_REPLY", + Self::WaitingSsl => "WAITING_SSL", + Self::WaitingSocksGreetingReply => "WAITING_SOCKS_GREETING_REPLY", + Self::WaitingSocksConnectReply => "WAITING_SOCKS_CONNECT_REPLY", + Self::WaitingSocksAuthReply => "WAITING_SOCKS_AUTH_REPLY", + Self::SslInit => "SSL_INIT", + Self::SslAckPending => "SSL_ACK_PENDING", + Self::AwaitingSslAccept => "AWAITING_SSL_ACCEPT", + Self::H1Upgrade => "H1_UPGRADE", + Self::WaitingServerReply => "WAITING_SERVER_REPLY", + Self::H2WaitingToSendHeaders => "H2_WAITING_TO_SEND_HEADERS", + Self::TxnCompleted => "TXN_COMPLETED", + Self::Idling => "IDLING", + Self::H1cIssueHandshake => "H1C_ISSUE_HANDSHAKE", + Self::H1cIssueHandshake2 => "H1C_ISSUE_HANDSHAKE2", + Self::IssueHttpBody => "ISSUE_HTTP_BODY", + Self::IssuingFile => "ISSUING_FILE", + Self::Headers => "HEADERS", + Self::Body => "BODY", + Self::DiscardBody => "DISCARD_BODY", + Self::Established => "ESTABLISHED", + Self::DoingTransaction => "DOING_TRANSACTION", + Self::WaitingToSendClose => "WAITING_TO_SEND_CLOSE", + Self::ReturnedClose => "RETURNED_CLOSE", + Self::AwaitingCloseAck => "AWAITING_CLOSE_ACK", + Self::FlushingBeforeClose => "FLUSHING_BEFORE_CLOSE", + Self::Shutdown => "SHUTDOWN", + Self::DeadSocket => "DEAD_SOCKET", + Self::AwaitingFileRead => "AWAITING_FILE_READ", + Self::TxnCompleting => "TXN_COMPLETING", + } + } +} + +impl Live { + /// The carrier phase a live state names, if it names one: set while + /// the carrier is not established, it is the carrier's. + const fn carrier(self) -> Option<Carrier> { + match self { + Self::H1cIssueHandshake => Some(Carrier::H1cIssueHandshake), + Self::H1cIssueHandshake2 => Some(Carrier::H1cIssueHandshake2), + Self::WaitingServerReply => Some(Carrier::WaitingServerReply), + Self::H2WaitingToSendHeaders => Some(Carrier::H2WaitingToSendHeaders), + Self::H1Upgrade => Some(Carrier::H1Upgrade), + Self::Unconnected + | Self::IssueHttpBody + | Self::Headers + | Self::Established + | Self::DoingTransaction + | Self::Body + | Self::DiscardBody + | Self::IssuingFile + | Self::AwaitingFileRead + | Self::TxnCompleting + | Self::TxnCompleted + | Self::Idling => None, + } + } + + const fn state(self) -> State { + match self { + Self::Unconnected => State::Unconnected, + Self::H1cIssueHandshake => State::H1cIssueHandshake, + Self::H1cIssueHandshake2 => State::H1cIssueHandshake2, + Self::WaitingServerReply => State::WaitingServerReply, + Self::H2WaitingToSendHeaders => State::H2WaitingToSendHeaders, + Self::H1Upgrade => State::H1Upgrade, + Self::IssueHttpBody => State::IssueHttpBody, + Self::Headers => State::Headers, + Self::Established => State::Established, + Self::DoingTransaction => State::DoingTransaction, + Self::Body => State::Body, + Self::DiscardBody => State::DiscardBody, + Self::IssuingFile => State::IssuingFile, + Self::AwaitingFileRead => State::AwaitingFileRead, + Self::TxnCompleting => State::TxnCompleting, + Self::TxnCompleted => State::TxnCompleted, + Self::Idling => State::Idling, + } + } +} + +impl Transport { + /// The state it reports, if it reports one: a failed or restarting + /// client reports its live state, marked. + const fn state(self) -> Option<State> { + match self { + Self::WaitingDns => Some(State::WaitingDns), + Self::WaitingConnect => Some(State::WaitingConnect), + Self::WaitingProxyReply => Some(State::WaitingProxyReply), + Self::WaitingSsl => Some(State::WaitingSsl), + Self::WaitingSocksGreetingReply => Some(State::WaitingSocksGreetingReply), + Self::WaitingSocksConnectReply => Some(State::WaitingSocksConnectReply), + Self::WaitingSocksAuthReply => Some(State::WaitingSocksAuthReply), + Self::SslInit => Some(State::SslInit), + Self::SslAckPending => Some(State::SslAckPending), + Self::AwaitingSslAccept => Some(State::AwaitingSslAccept), + Self::Failed | Self::Restarting => Some(State::Unconnected), + Self::None => None, + } + } +} + +impl Carrier { + /// The state it reports while the handshake is in progress. + const fn state(self) -> Option<State> { + match self { + Self::H1cIssueHandshake => Some(State::H1cIssueHandshake), + Self::H1cIssueHandshake2 => Some(State::H1cIssueHandshake2), + Self::WaitingServerReply => Some(State::WaitingServerReply), + Self::H2WaitingToSendHeaders => Some(State::H2WaitingToSendHeaders), + Self::H1Upgrade => Some(State::H1Upgrade), + Self::None | Self::Established => None, + } + } +} + +impl Close { + /// The state it reports, if it reports one: a close entered with + /// nothing to wait for reports the live state, marked. + const fn state(self) -> Option<State> { + match self { + Self::CloseWhenFlushed | Self::FlushingBeforeClose => Some(State::FlushingBeforeClose), + Self::WaitingToSendClose => Some(State::WaitingToSendClose), + Self::ReturnedClose => Some(State::ReturnedClose), + Self::AwaitingCloseAck => Some(State::AwaitingCloseAck), + Self::Shutdown => Some(State::Shutdown), + Self::DeadSocket | Self::UserTold => Some(State::DeadSocket), + Self::None | Self::Closing => None, + } + } + + /// The polite close phases, which an unusable socket never enters. + const fn polite(self) -> bool { + match self { + Self::WaitingToSendClose + | Self::ReturnedClose + | Self::AwaitingCloseAck + | Self::Shutdown => true, + Self::None + | Self::CloseWhenFlushed + | Self::Closing + | Self::FlushingBeforeClose + | Self::DeadSocket + | Self::UserTold => false, + } + } +} + +/// Why an event was refused. The machines are left as they were. +/// +/// Each is a bug at the site that raised the event, as it is in C, where +/// `LWS_WITH_STATE_CHECK` aborts on them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refused { + /// No row of the event table takes this event from this role, side + /// and state. + NoRow, + /// The event would take the close machine back to an earlier phase. + CloseBackwards, + /// A live state, while the client restarts: it has none until the + /// restart. + Restarting, + /// A polite close phase, with the socket known unusable. + UnusableSocket, + /// A staged shutdown on a raw socket, which has no half-close to wait + /// for. + ShutdownRaw, +} + +impl fmt::Display for Refused { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::NoRow => "no row for the event in this state", + Self::CloseBackwards => "the close would go backwards", + Self::Restarting => "a live state while restarting", + Self::UnusableSocket => "a polite close with the socket unusable", + Self::ShutdownRaw => "a staged shutdown on a raw socket", + }) + } +} + +impl core::error::Error for Refused {} + +/// A connection's four machines, its role and side, and its socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Machines { + role: Role, + side: Side, + transport: Transport, + carrier: Carrier, + live: Live, + close: Close, + socket: Socket, +} + +impl Default for Machines { + fn default() -> Self { + Self::new() + } +} + +/// Which machine an edge changed, as C's trace names its setter. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum How { + /// The live or carrier machine. + SetState, + /// The transport machine. + SetTransport, + /// The close machine. + SetClose, + /// The role or side, or a birth. + RoleTransition, + /// The socket's usability. + SetUnusable, +} + +impl How { + /// The setter's name, as C's trace writes it. + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::SetState => "set_state", + Self::SetTransport => "set_transport", + Self::SetClose => "set_close", + Self::RoleTransition => "role_transition", + Self::SetUnusable => "set_unusable", + } + } +} + +/// A change of a connection's machines. +/// +/// Its `Display` is C's trace line for it, without the connection's tag. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Edge { + /// The machines before; `None` before a connection's birth. + pub from: Option<Machines>, + /// The machines after. + pub to: Machines, + /// Which setter made the change. + pub how: How, + /// The event that caused it; `None` for a birth or the socket's + /// usability changing. + pub event: Option<Event>, +} + +impl Edge { + /// Whether C's trace records the edge: it leaves out a change that + /// shows in no state, role, side or attribute. + #[must_use] + pub fn traced(&self) -> bool { + let Some(from) = self.from else { + return true; + }; + let to = self.to; + let same_state = from.role == to.role && from.side == to.side && from.state() == to.state(); + + !same_state + || from.socket != to.socket + || from.close != to.close + || from.transport != to.transport + } +} + +impl fmt::Display for Machines { + /// C's `role/side:STATE` with its attributes, eg, + /// `h1/C:DEAD_SOCKET+told+unusable`. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}/{}:{}", + self.role.name(), + self.side.letter(), + self.state().name() + )?; + if self.transport == Transport::Failed { + f.write_str("+failed")?; + } + if self.transport == Transport::Restarting { + f.write_str("+restarting")?; + } + if self.close == Close::UserTold { + f.write_str("+told")?; + } + if self.close == Close::Closing { + f.write_str("+closing")?; + } + if self.socket == Socket::Unusable { + f.write_str("+unusable")?; + } + Ok(()) + } +} + +impl fmt::Display for Edge { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("LRS ")?; + match self.from { + Some(m) => write!(f, "{m}")?, + None => f.write_str("(none)/-:(zero)")?, + } + write!(f, " -> {} {}", self.to, self.how.name())?; + if let Some(e) = self.event { + write!(f, " ev={}", e.name())?; + } + Ok(()) + } +} + +impl Machines { + /// A connection just born: no role, no side, unconnected. + #[must_use] + pub const fn new() -> Self { + Self { + role: Role::None, + side: Side::Unset, + transport: Transport::None, + carrier: Carrier::None, + live: Live::Unconnected, + close: Close::None, + socket: Socket::Usable, + } + } + + /// The edge of a connection's birth, as C's trace shows it. + #[must_use] + pub const fn birth() -> Edge { + Edge { + from: None, + to: Self::new(), + how: How::RoleTransition, + event: None, + } + } + + /// The connection's role. + #[must_use] + pub const fn role(&self) -> Role { + self.role + } + + /// The connection's side. + #[must_use] + pub const fn side(&self) -> Side { + self.side + } + + /// The transport machine. + #[must_use] + pub const fn transport(&self) -> Transport { + self.transport + } + + /// The carrier machine. + #[must_use] + pub const fn carrier(&self) -> Carrier { + self.carrier + } + + /// The live machine, whether or not a close is in progress over it. + #[must_use] + pub const fn live(&self) -> Live { + self.live + } + + /// The close machine. + #[must_use] + pub const fn close(&self) -> Close { + self.close + } + + /// Whether the socket is usable. + #[must_use] + pub const fn socket(&self) -> Socket { + self.socket + } + + /// The state the connection reports: the close machine's if a close is + /// in progress, else the transport machine's, else the carrier's while + /// it handshakes, else the live state. + #[must_use] + pub const fn state(&self) -> State { + if let Some(s) = self.close.state() { + return s; + } + if let Some(s) = self.transport.state() { + return s; + } + if let Some(s) = self.carrier.state() { + return s; + } + self.live.state() + } + + /// Takes an event, and gives the edge it caused. + /// + /// # Errors + /// + /// [`Refused`], if the event table has no row for it in this role, side + /// and state, or the row it has breaks a rule of the machines. The + /// machines are left as they were. + pub fn event(&mut self, ev: Event) -> Result<Edge, Refused> { + self.apply(ev, None) + } + + /// Takes an event that brings a role the site chooses: an adoption, + /// a client binding or restarting, a raw upgrade. Only rows that take + /// the site's role match. + /// + /// # Errors + /// + /// As [`Machines::event`]; and [`Refused::NoRow`] for [`Role::None`], + /// which is no role to bring. + pub fn event_as(&mut self, ev: Event, role: Role) -> Result<Edge, Refused> { + if role == Role::None { + return Err(Refused::NoRow); + } + self.apply(ev, Some(role)) + } + + /// Marks the socket usable or not, and gives the edge. + pub const fn set_socket(&mut self, socket: Socket) -> Edge { + let from = *self; + self.socket = socket; + Edge { + from: Some(from), + to: *self, + how: How::SetUnusable, + event: None, + } + } + + fn apply(&mut self, ev: Event, site_role: Option<Role>) -> Result<Edge, Refused> { + let to = + table::row(self.role, self.side, self.state(), ev, site_role).ok_or(Refused::NoRow)?; + let mut next = *self; + let how = match to { + To::Live(live) => { + if self.transport == Transport::Restarting { + return Err(Refused::Restarting); + } + next.set_live(live); + How::SetState + } + To::Transport(t) => { + next.transport = t; + How::SetTransport + } + To::Close(c) => { + if c < self.close { + return Err(Refused::CloseBackwards); + } + next.close = c; + How::SetClose + } + To::Role { role, side, state } => { + let role = match role { + RoleTo::Keep => self.role, + RoleTo::Site => site_role.unwrap_or(self.role), + RoleTo::Named(r) => r, + }; + let side = match side { + SideTo::Keep => self.side, + SideTo::Set(s) => s, + }; + next.role_transition(role, side, state); + How::RoleTransition + } + }; + // as C, an edge that changes no reported state, role or side is + // not held to the invariants + if next.role != self.role || next.side != self.side || next.state() != self.state() { + next.check()?; + } + + let from = *self; + *self = next; + Ok(Edge { + from: Some(from), + to: next, + how, + event: Some(ev), + }) + } + + /// C's `lws_wsi_set_state_ev()`: a live state ends any transport phase; + /// a handshake-named state while the carrier handshakes is the + /// carrier's, and anything else establishes the carrier and is the live + /// state. + const fn set_live(&mut self, live: Live) { + self.transport = Transport::None; + match live.carrier() { + Some(c) if !matches!(self.carrier, Carrier::Established) => self.carrier = c, + Some(_) | None => { + self.carrier = if matches!(live, Live::Unconnected) { + Carrier::None + } else { + Carrier::Established + }; + self.live = live; + } + } + } + + /// C's `lws_wsi_role_transition_ev()`: the role and side change, and + /// the machines start again from `state`. A restart to unconnected is + /// a new connection, which leaves the old one's close and socket + /// behind; any other role change keeps them. + const fn role_transition(&mut self, role: Role, side: Side, state: table::RoleState) { + let (transport, carrier, live) = match state { + table::RoleState::Transport(t) => (t, Carrier::None, Live::Unconnected), + table::RoleState::Live(Live::Unconnected) => { + (Transport::None, Carrier::None, Live::Unconnected) + } + table::RoleState::Live(l) => match l.carrier() { + Some(c) => (Transport::None, c, Live::Unconnected), + None => (Transport::None, Carrier::Established, l), + }, + }; + let restart = matches!(state, table::RoleState::Live(Live::Unconnected)); + + self.role = role; + self.side = side; + self.transport = transport; + self.carrier = carrier; + self.live = live; + if restart { + self.close = Close::None; + self.socket = Socket::Usable; + } + } + + /// The invariants C's `LWS_WITH_STATE_CHECK` holds every edge to. + const fn check(self) -> Result<(), Refused> { + if matches!(self.socket, Socket::Unusable) && self.close.polite() { + return Err(Refused::UnusableSocket); + } + if matches!(self.close, Close::Shutdown) && matches!(self.role, Role::RawSkt) { + return Err(Refused::ShutdownRaw); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn h1_server() -> Machines { + let mut m = Machines::new(); + m.event(Event::ServerSide).unwrap(); + m.event_as(Event::Adopted, Role::H1).unwrap(); + m + } + + #[test] + fn a_refused_event_changes_nothing() { + let mut m = h1_server(); + let before = m; + assert_eq!(m.event(Event::WsCloseSent), Err(Refused::NoRow)); + assert_eq!(m, before); + } + + #[test] + fn no_role_is_no_role_to_bring() { + let mut m = Machines::new(); + m.event(Event::ServerSide).unwrap(); + assert_eq!(m.event_as(Event::Adopted, Role::None), Err(Refused::NoRow)); + } + + #[test] + fn a_ws_close_we_start() { + let mut m = h1_server(); + m.event(Event::ReqHdrsComplete).unwrap(); + m.event(Event::WsUpgraded).unwrap(); + + let lines = [ + Event::WsCloseInitiated, + Event::WsCloseSent, + Event::CloseFlush, + Event::SocketGone, + Event::UserTold, + ] + .map(|e| m.event(e).unwrap().to_string()); + assert_eq!( + lines, + [ + "LRS ws/S:ESTABLISHED -> ws/S:WAITING_TO_SEND_CLOSE set_close ev=WS_CLOSE_INITIATED", + "LRS ws/S:WAITING_TO_SEND_CLOSE -> ws/S:AWAITING_CLOSE_ACK set_close ev=WS_CLOSE_SENT", + "LRS ws/S:AWAITING_CLOSE_ACK -> ws/S:FLUSHING_BEFORE_CLOSE set_close ev=CLOSE_FLUSH", + "LRS ws/S:FLUSHING_BEFORE_CLOSE -> ws/S:DEAD_SOCKET set_close ev=SOCKET_GONE", + "LRS ws/S:DEAD_SOCKET -> ws/S:DEAD_SOCKET+told set_close ev=USER_TOLD", + ] + ); + } + + #[test] + fn an_unusable_socket_never_closes_politely() { + let mut m = h1_server(); + m.event(Event::ReqHdrsComplete).unwrap(); + m.event(Event::WsUpgraded).unwrap(); + m.set_socket(Socket::Unusable); + assert_eq!( + m.event(Event::WsCloseInitiated), + Err(Refused::UnusableSocket) + ); + assert_eq!(m.event(Event::CloseStaged), Err(Refused::UnusableSocket)); + // the abortive path is still open + assert!(m.event(Event::SocketGone).is_ok()); + } + + #[test] + fn the_close_only_goes_forwards() { + let mut m = h1_server(); + m.event(Event::SocketGone).unwrap(); + assert_eq!(m.event(Event::CloseFlush), Err(Refused::CloseBackwards)); + // entering the close is the close, at its start + assert_eq!(m.event(Event::CloseEntered), Err(Refused::CloseBackwards)); + } + + #[test] + fn a_restart_is_a_new_connection() { + let mut m = Machines::new(); + m.event_as(Event::ClientBind, Role::H1).unwrap(); + m.event(Event::ConnectStart).unwrap(); + m.set_socket(Socket::Unusable); + m.event(Event::Retarget).unwrap(); + // no live state while restarting... + assert_eq!(m.event(Event::TransportUp), Err(Refused::Restarting)); + // ...until the restart, which leaves the old socket behind + m.event_as(Event::Restart, Role::H1).unwrap(); + assert_eq!(m.socket(), Socket::Usable); + assert_eq!(m.transport(), Transport::None); + } +} diff --git a/crates/npro-core/src/state/table.rs b/crates/npro-core/src/state/table.rs new file mode 100644 index 0000000..064de47 --- /dev/null +++ b/crates/npro-core/src/state/table.rs @@ -0,0 +1,550 @@ +//! The event table: what an event does, by role, side and state. +//! +//! This is the stage-1 part of `lws_wsi_event_edges[]` in C's +//! `lib/sansio/wsi-state.c`: the rows whose role and target are roles npro +//! has. The rows are in C's order, grouped by event, and the first that +//! matches wins, as in C. A row matching on `_` for a role or side is C's +//! `"*"`, and on `_` for the state is C's `ANY`. +//! +//! The site's role, when it gives one, is C's `ops` argument: a row that +//! takes it (C's `"?"`) matches only when one is given, and one that names +//! a role matches only when none is given or the site's is that role. Every +//! other row matches only when none is given. + +use super::{Close, Event, Live, Role, Side, State, Transport}; + +/// What a row does. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum To { + /// Sets the live state, or the carrier's while it handshakes. + Live(Live), + /// Sets the transport machine, over whatever the others are doing. + Transport(Transport), + /// Sets the close machine, over whatever the others are doing. + Close(Close), + /// Changes the role or side, and starts the machines from `state`. + Role { + role: RoleTo, + side: SideTo, + state: RoleState, + }, +} + +/// The role a role change leads to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RoleTo { + /// As it was. + Keep, + /// The one the site gave: C's `"?"`. + Site, + /// This one. + Named(Role), +} + +/// The side a role change leads to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum SideTo { + /// As it was. + Keep, + /// This one. + Set(Side), +} + +/// Where the machines start after a role change. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RoleState { + /// A transport phase, over an unconnected live state. + Transport(Transport), + /// A live state; unconnected is a restart on a new connection. + Live(Live), +} + +#[expect( + clippy::unnecessary_wraps, + reason = "a row's result: Some is the row, as None is no row" +)] +const fn live(l: Live) -> Option<To> { + Some(To::Live(l)) +} + +#[expect( + clippy::unnecessary_wraps, + reason = "a row's result: Some is the row, as None is no row" +)] +const fn transport(t: Transport) -> Option<To> { + Some(To::Transport(t)) +} + +#[expect( + clippy::unnecessary_wraps, + reason = "a row's result: Some is the row, as None is no row" +)] +const fn close(c: Close) -> Option<To> { + Some(To::Close(c)) +} + +#[expect( + clippy::unnecessary_wraps, + reason = "a row's result: Some is the row, as None is no row" +)] +const fn change(role: RoleTo, side: SideTo, state: RoleState) -> Option<To> { + Some(To::Role { role, side, state }) +} + +/// The row for `ev` from `role`, `side` and the reported state `from`, with +/// the site's role `site_role`, if it gave one. +#[expect( + clippy::too_many_lines, + reason = "one table, kept whole and in C's order so it reads against C's" +)] +#[expect( + clippy::match_same_arms, + reason = "a row per C row, even where two lead to the same place" +)] +pub(super) const fn row( + role: Role, + side: Side, + from: State, + ev: Event, + site_role: Option<Role>, +) -> Option<To> { + use Side::{Client, Server}; + use State as St; + + match ev { + // the transport finished: a server starts on the request, an h1 + // client on sending one, the rest are up + Event::TransportUp => match (role, side, from, site_role) { + ( + Role::H1, + Client, + St::Unconnected + | St::WaitingSsl + | St::WaitingConnect + | St::H1cIssueHandshake + | St::H1cIssueHandshake2, + None, + ) => live(Live::H1cIssueHandshake2), + (Role::H1, Client, St::H2WaitingToSendHeaders, None) => { + live(Live::H2WaitingToSendHeaders) + } + ( + Role::RawSkt, + Client, + St::WaitingConnect + | St::WaitingSsl + | St::WaitingSocksConnectReply + | St::WaitingProxyReply, + None, + ) => live(Live::Established), + (Role::H1, Server, St::SslAckPending | St::AwaitingSslAccept | St::SslInit, None) => { + live(Live::Headers) + } + (_, Server, St::SslAckPending | St::AwaitingSslAccept | St::SslInit, None) => { + live(Live::Established) + } + // the non-tls fallback established the connection before the + // accept path reports the transport up + (_, Server, St::Established, None) => live(Live::Established), + _ => None, + }, + + // an h1 client's tcp is up before its tls; a socks or CONNECT + // tunnel coming up is the socket connecting, for the protocol + Event::SocketConnected => match (role, side, from, site_role) { + ( + Role::H1, + Client, + St::WaitingConnect | St::WaitingSocksConnectReply | St::WaitingProxyReply, + None, + ) => live(Live::H1cIssueHandshake), + (Role::H1, Client, St::H1cIssueHandshake2, None) => live(Live::H1cIssueHandshake2), + _ => None, + }, + + // a client request queued on, or issued by, a connection + Event::Queued => match (side, from, site_role) { + (Client, St::Unconnected | St::H1cIssueHandshake2, None) => { + live(Live::H2WaitingToSendHeaders) + } + _ => None, + }, + Event::ReqIssue => match (role, side, from, site_role) { + ( + Role::H1, + Client, + St::H2WaitingToSendHeaders | St::Established | St::Idling | St::WaitingServerReply, + None, + ) => live(Live::H1cIssueHandshake2), + _ => None, + }, + + // the request goes out, then its response is pending + Event::ReqHdrsSent => match (role, side, from, site_role) { + ( + Role::H1, + Client, + St::WaitingSsl + | St::WaitingConnect + | St::H1cIssueHandshake + | St::H1cIssueHandshake2, + None, + ) => live(Live::WaitingServerReply), + _ => None, + }, + Event::ReqHdrsSentBody => match (role, side, from, site_role) { + ( + Role::H1, + Client, + St::WaitingSsl + | St::WaitingConnect + | St::H1cIssueHandshake + | St::H1cIssueHandshake2, + None, + ) => live(Live::IssueHttpBody), + _ => None, + }, + Event::ReqBodySent => match (side, from, site_role) { + (Client, St::IssueHttpBody, None) => live(Live::WaitingServerReply), + _ => None, + }, + // a ws client takes no response headers but the 101's: it is still + // waiting + Event::RespInterim => match (role, side, from, site_role) { + (Role::H1, Client, St::Established | St::WaitingServerReply, None) => { + live(Live::WaitingServerReply) + } + _ => None, + }, + + // the response is done and the client idles; the h1 server's + // transaction ends, from wherever it had got to + Event::TxnCompleted => match (role, side, from, site_role) { + (Role::H1, Client, St::Established, None) => live(Live::Idling), + (_, Client, St::Idling, None) => live(Live::Idling), + (Role::H1, Server, St::TxnCompleting, None) => live(Live::TxnCompleted), + ( + Role::H1, + Server, + St::Established + | St::Body + | St::DiscardBody + | St::DoingTransaction + | St::H1Upgrade + | St::TxnCompleted + | St::IssuingFile + | St::AwaitingFileRead, + None, + ) => live(Live::TxnCompleted), + _ => None, + }, + + // request headers: the h1 server decides on the upgrade + Event::ReqHdrsComplete => match (role, side, from, site_role) { + (Role::H1, Server, St::Headers | St::Established, None) => live(Live::H1Upgrade), + _ => None, + }, + Event::ReqPlainHttp => match (role, side, from, site_role) { + (Role::H1, Server, St::H1Upgrade, None) => live(Live::Established), + _ => None, + }, + + // acting on the request + Event::ActionBegin => match (role, side, from, site_role) { + (Role::H1, Server, St::Established, None) => live(Live::DoingTransaction), + _ => None, + }, + + // the request body + Event::BodyBegin => match (role, side, from, site_role) { + (Role::H1, Server, St::Established | St::DoingTransaction, None) => live(Live::Body), + _ => None, + }, + // an h1 body is complete before its answer is: the next request, + // pipelined behind it, waits for the transaction + Event::BodyComplete => match (role, side, from, site_role) { + (Role::H1, Server, St::Body, None) => live(Live::DoingTransaction), + _ => None, + }, + // the user completed the transaction before reading the body, maybe + // with a read of a file out on a worker, reaped by the completion + Event::BodyDiscard => match (role, side, from, site_role) { + ( + Role::H1, + Server, + St::Body + | St::Established + | St::DoingTransaction + | St::H1Upgrade + | St::IssuingFile + | St::AwaitingFileRead + | St::TxnCompleting, + None, + ) => live(Live::DiscardBody), + _ => None, + }, + + // the user completed the transaction with its answer still queued: + // completion waits for that to go + Event::TxnCompleting => match (role, side, from, site_role) { + ( + Role::H1, + Server, + St::Established + | St::Body + | St::DiscardBody + | St::DoingTransaction + | St::H1Upgrade + | St::TxnCompleted + | St::IssuingFile + | St::TxnCompleting + | St::AwaitingFileRead, + None, + ) => live(Live::TxnCompleting), + _ => None, + }, + // and the connection is reused for the next request + Event::TxnDrained => match (role, side, from, site_role) { + (Role::H1, Server, St::TxnCompleted, None) => live(Live::Headers), + _ => None, + }, + + // serving a file: from the request, its body's completion, or the + // upgrade's confirmation + Event::FileBegin => match (role, side, from, site_role) { + (_, Server, St::Established | St::DoingTransaction | St::Body, None) => { + live(Live::IssuingFile) + } + (Role::H1, Server, St::H1Upgrade, None) => live(Live::IssuingFile), + _ => None, + }, + Event::FileReadQueued => match (side, from, site_role) { + (Server, St::IssuingFile, None) => live(Live::AwaitingFileRead), + _ => None, + }, + Event::FileReadDone => match (side, from, site_role) { + (Server, St::AwaitingFileRead, None) => live(Live::IssuingFile), + _ => None, + }, + Event::FileComplete => match (side, from, site_role) { + (Server, St::IssuingFile, None) => live(Live::Established), + _ => None, + }, + + // ---- role changes ---- + + // birth on a server, adoption, the client bind, a restart + Event::ServerSide => match (role, side, from, site_role) { + (Role::None, Side::Unset, St::Unconnected, None) => change( + RoleTo::Keep, + SideTo::Set(Server), + RoleState::Live(Live::Unconnected), + ), + _ => None, + }, + Event::AdoptedTls => match (from, site_role) { + (St::Unconnected, Some(_)) => change( + RoleTo::Site, + SideTo::Keep, + RoleState::Transport(Transport::SslInit), + ), + _ => None, + }, + Event::Adopted => match (from, site_role) { + (St::Unconnected, None | Some(Role::H1)) => change( + RoleTo::Named(Role::H1), + SideTo::Keep, + RoleState::Live(Live::Headers), + ), + (St::Unconnected, Some(_)) => change( + RoleTo::Site, + SideTo::Keep, + RoleState::Live(Live::Established), + ), + _ => None, + }, + Event::ClientBind => match (from, site_role) { + (St::Unconnected, Some(_)) => change( + RoleTo::Site, + SideTo::Set(Client), + RoleState::Live(Live::Unconnected), + ), + _ => None, + }, + Event::Restart => match (side, site_role) { + (Client, Some(_)) => change( + RoleTo::Site, + SideTo::Set(Client), + RoleState::Live(Live::Unconnected), + ), + _ => None, + }, + + // ws, on a server from the upgrade decision, on a client from the + // 101 + Event::WsUpgraded => match (role, side, from, site_role) { + (Role::H1, Server, St::H1Upgrade, None | Some(Role::Ws)) + | (Role::H1, Client, St::WaitingServerReply, None | Some(Role::Ws)) => change( + RoleTo::Named(Role::Ws), + SideTo::Keep, + RoleState::Live(Live::Established), + ), + _ => None, + }, + + // a client's response headers; a server may answer before the + // request body is finished (401, 413...) + Event::RespHdrs => match (side, from, site_role) { + (Client, St::WaitingServerReply | St::IssueHttpBody, None) => live(Live::Established), + _ => None, + }, + + // raw: from the request, the upgrade, the non-tls fallback on a tls + // listener, a kept-alive connection or a listener already raw; an h1 + // client's own raw upgrade + Event::RawUpgraded => match (role, side, from, site_role) { + ( + _, + Server, + St::Headers | St::H1Upgrade | St::SslInit | St::SslAckPending | St::Established, + Some(_), + ) => change( + RoleTo::Site, + SideTo::Keep, + RoleState::Live(Live::Established), + ), + ( + Role::H1, + Client, + St::Established | St::WaitingServerReply, + None | Some(Role::RawSkt), + ) => change( + RoleTo::Named(Role::RawSkt), + SideTo::Keep, + RoleState::Live(Live::Established), + ), + _ => None, + }, + + // ---- the transport machine ---- + Event::DnsStart => match (side, from, site_role) { + (Client, St::Unconnected, None) => transport(Transport::WaitingDns), + _ => None, + }, + Event::DnsRetry => match (side, from, site_role) { + (Client, St::WaitingDns, None) => transport(Transport::None), + _ => None, + }, + // to the first address, the next one, or tcp after quic + Event::ConnectStart => match (side, from, site_role) { + ( + Client, + St::Unconnected | St::WaitingDns | St::WaitingConnect | St::WaitingSsl, + None, + ) => transport(Transport::WaitingConnect), + _ => None, + }, + Event::ProxyConnectSent => match (side, from, site_role) { + (Client, St::WaitingConnect, None) => transport(Transport::WaitingProxyReply), + _ => None, + }, + Event::SocksGreetingSent => match (side, from, site_role) { + (Client, St::WaitingConnect, None) => transport(Transport::WaitingSocksGreetingReply), + _ => None, + }, + Event::SocksAuthSent => match (side, from, site_role) { + (Client, St::WaitingSocksGreetingReply, None) => { + transport(Transport::WaitingSocksAuthReply) + } + _ => None, + }, + Event::SocksConnectSent => match (side, from, site_role) { + (Client, St::WaitingSocksGreetingReply | St::WaitingSocksAuthReply, None) => { + transport(Transport::WaitingSocksConnectReply) + } + _ => None, + }, + Event::TlsStart => match (role, side, from, site_role) { + ( + _, + Client, + St::WaitingConnect + | St::WaitingProxyReply + | St::WaitingSocksConnectReply + | St::H1cIssueHandshake + | St::WaitingSsl, + None, + ) => transport(Transport::WaitingSsl), + // STARTTLS: an established raw client starts tls inside its + // protocol + (Role::RawSkt, Client, St::Established, None) => transport(Transport::WaitingSsl), + _ => None, + }, + Event::TlsAcceptPending => match (side, from, site_role) { + (Server, St::SslInit | St::SslAckPending | St::AwaitingSslAccept, None) => { + transport(Transport::SslAckPending) + } + _ => None, + }, + Event::TlsAcceptQueued => match (side, from, site_role) { + (Server, St::SslInit | St::SslAckPending, None) => { + transport(Transport::AwaitingSslAccept) + } + _ => None, + }, + Event::ConnFailed => match (side, site_role) { + (Client, None) => transport(Transport::Failed), + _ => None, + }, + Event::Retarget => match (side, site_role) { + (Client, None) => transport(Transport::Restarting), + _ => None, + }, + + // ---- the close machine: the polite ws close is specific, the rest + // can come from anywhere ---- + Event::WsCloseInitiated => match (role, from, site_role) { + (Role::Ws, St::Established, None) => close(Close::WaitingToSendClose), + _ => None, + }, + Event::WsCloseSent => match (role, from, site_role) { + (Role::Ws, St::WaitingToSendClose, None) => close(Close::AwaitingCloseAck), + _ => None, + }, + // the peer's CLOSE, perhaps beating the one we were about to send: + // answer his and drop ours + Event::WsPeerClose => match (role, from, site_role) { + (Role::Ws, St::Established | St::WaitingToSendClose, None) => { + close(Close::ReturnedClose) + } + _ => None, + }, + // a flush the live connection had begun is now the close's own, else + // the close has begun with nothing to wait for + Event::CloseEntered => match (from, site_role) { + (St::FlushingBeforeClose, None) => close(Close::FlushingBeforeClose), + (_, None) => close(Close::Closing), + _ => None, + }, + Event::CloseFlush => match site_role { + None => close(Close::FlushingBeforeClose), + Some(_) => None, + }, + Event::CloseWhenFlushed => match site_role { + None => close(Close::CloseWhenFlushed), + Some(_) => None, + }, + Event::CloseStaged => match (side, site_role) { + (Server, None) => close(Close::Shutdown), + _ => None, + }, + Event::SocketGone => match site_role { + None => close(Close::DeadSocket), + Some(_) => None, + }, + Event::UserTold => match (from, site_role) { + (St::DeadSocket, None) => close(Close::UserTold), + _ => None, + }, + } +}
Page fetched 0s ago, creation time: 5ms (vhost etag hits: 0%, cache hits: 0%)