// The core state machine (port of h11/_state.py).
//
// Rule 1: everything that affects the state machine and state transitions
// must live here. As much as possible goes into the table-based
// representation, but for the bits that don't quite fit, the actual code and
// state must nonetheless live here.
//
// Rule 2: this file does not know about what role we're playing; it only
// knows about HTTP request/response cycles in the abstract.
//
// There are really 5 state machines here:
//
// 1) the client state, 2) the server state, 3) the keep-alive state
// ({true, false}), 4) the SWITCH_CONNECT state and 5) the SWITCH_UPGRADE
// state. (1)-(3) are stored explicitly; (4) and (5) are stored implicitly in
// `pending_switch_proposals`.
//
// Each machine has event-triggered transitions (some event happens) and
// state-triggered transitions (a joint configuration of states is reached,
// e.g. client DONE + keep-alive false => client MUST_CLOSE). The only
// conflict between state-triggered transitions (DONE -> MIGHT_SWITCH_PROTOCOL
// versus DONE -> MUST_CLOSE) is resolved in favour of the protocol switch.
///|
/// The two roles in an HTTP conversation.
pub(all) enum Role {
Client
Server
} derive(Eq, Hash, Debug)
///|
pub impl Show for Role with fn output(self, logger) {
logger.write_string(
match self {
Client => "CLIENT"
Server => "SERVER"
},
)
}
///|
/// The other role.
pub fn Role::other(self : Role) -> Role {
match self {
Client => Server
Server => Client
}
}
///|
/// The states of the client and server state machines. See the h11
/// documentation for the full diagrams.
pub(all) enum State {
Idle
SendResponse
SendBody
Done
MustClose
Closed
MightSwitchProtocol
SwitchedProtocol
Error
} derive(Eq, Hash, Debug)
///|
pub impl Show for State with fn output(self, logger) {
logger.write_string(
match self {
Idle => "IDLE"
SendResponse => "SEND_RESPONSE"
SendBody => "SEND_BODY"
Done => "DONE"
MustClose => "MUST_CLOSE"
Closed => "CLOSED"
MightSwitchProtocol => "MIGHT_SWITCH_PROTOCOL"
SwitchedProtocol => "SWITCHED_PROTOCOL"
Error => "ERROR"
},
)
}
///|
/// The joint state of both parties.
pub(all) struct States {
client : State
server : State
} derive(Eq, Hash, Debug)
///|
/// The state of `role`.
#alias("_[_]")
pub fn States::get(self : States, role : Role) -> State {
match role {
Client => self.client
Server => self.server
}
}
///|
pub impl Show for States with fn output(self, logger) {
logger.write_string("{CLIENT: \{self.client}, SERVER: \{self.server}}")
}
///|
/// The kinds of protocol switch a client can propose.
priv enum SwitchType {
SwitchUpgrade
SwitchConnect
} derive(Eq)
///|
/// Keys of the event-triggered transition table.
priv enum TransitionKey {
Plain(EventType)
/// The server machine sees the client's `Request` event in this form.
ClientRequest
/// A server event annotated with an accepted protocol switch.
Switch(EventType, SwitchType)
}
///|
/// EVENT_TRIGGERED_TRANSITIONS from the Python version.
fn event_triggered_transition(
role : Role,
state : State,
key : TransitionKey,
) -> State? {
match (role, state, key) {
// Client
(Client, Idle, Plain(Request)) => Some(SendBody)
(Client, Idle, Plain(ConnectionClosed)) => Some(Closed)
(Client, SendBody, Plain(Data)) => Some(SendBody)
(Client, SendBody, Plain(EndOfMessage)) => Some(Done)
(Client, Done | MustClose | Closed, Plain(ConnectionClosed)) => Some(Closed)
// Server
(Server, Idle, Plain(ConnectionClosed)) => Some(Closed)
(Server, Idle, Plain(Response)) => Some(SendBody)
(Server, Idle, ClientRequest) => Some(SendResponse)
(Server, SendResponse, Plain(InformationalResponse)) => Some(SendResponse)
(Server, SendResponse, Plain(Response)) => Some(SendBody)
(Server, SendResponse, Switch(InformationalResponse, SwitchUpgrade)) =>
Some(SwitchedProtocol)
(Server, SendResponse, Switch(Response, SwitchConnect)) =>
Some(SwitchedProtocol)
(Server, SendBody, Plain(Data)) => Some(SendBody)
(Server, SendBody, Plain(EndOfMessage)) => Some(Done)
(Server, Done | MustClose | Closed, Plain(ConnectionClosed)) => Some(Closed)
_ => None
}
}
///|
/// STATE_TRIGGERED_TRANSITIONS from the Python version:
/// (client state, server state) -> new states.
fn state_triggered_transition(client : State, server : State) -> States? {
match (client, server) {
// Protocol negotiation
(MightSwitchProtocol, SwitchedProtocol) =>
Some({ client: SwitchedProtocol, server, })
// Socket shutdown
(Closed, Done) | (Closed, Idle) | (Error, Done) =>
Some({ client, server: MustClose, })
(Done, Closed) | (Idle, Closed) | (Done, Error) =>
Some({ client: MustClose, server, })
_ => None
}
}
///|
priv struct ConnectionState {
/// If this is false then it enables the automatic DONE -> MUST_CLOSE
/// transition. Don't set this directly; call `process_keep_alive_disabled`.
mut keep_alive : Bool
/// Subset of {SwitchUpgrade, SwitchConnect}: the protocol switches the
/// client proposed.
mut pending_switch_proposals : Array[SwitchType]
mut states : States
}
///|
fn ConnectionState::new() -> ConnectionState {
{
keep_alive: true,
pending_switch_proposals: [],
states: { client: Idle, server: Idle, },
}
}
///|
fn ConnectionState::set_state(
self : ConnectionState,
role : Role,
state : State,
) -> Unit {
self.states = match role {
Client => { ..self.states, client: state, }
Server => { ..self.states, server: state, }
}
}
///|
fn ConnectionState::process_error(self : ConnectionState, role : Role) -> Unit {
self.set_state(role, Error)
self.fire_state_triggered_transitions()
}
///|
fn ConnectionState::process_keep_alive_disabled(self : ConnectionState) -> Unit {
self.keep_alive = false
self.fire_state_triggered_transitions()
}
///|
fn ConnectionState::process_client_switch_proposal(
self : ConnectionState,
switch_event : SwitchType,
) -> Unit {
if !self.pending_switch_proposals.contains(switch_event) {
self.pending_switch_proposals.push(switch_event)
}
self.fire_state_triggered_transitions()
}
///|
fn ConnectionState::process_event(
self : ConnectionState,
role : Role,
event_type : EventType,
server_switch_event? : SwitchType,
) -> Unit raise ProtocolError {
let key = match server_switch_event {
// Only server rows of the transition table accept `Switch` keys, so a
// client event with a switch annotation is rejected below.
Some(switch_event) => {
if !self.pending_switch_proposals.contains(switch_event) {
raise local_error(
"Received server _SWITCH_UPGRADE event without a pending proposal",
)
}
Switch(event_type, switch_event)
}
None => {
if event_type is Response {
self.pending_switch_proposals = []
}
Plain(event_type)
}
}
self.fire_event_triggered_transitions(role, key)
// Special case: the server state does get to see Request events.
// (A server-sent Request was already rejected by the table above.)
if key is Plain(Request) && role is Client {
self.fire_event_triggered_transitions(Server, ClientRequest)
}
self.fire_state_triggered_transitions()
}
///|
fn ConnectionState::fire_event_triggered_transitions(
self : ConnectionState,
role : Role,
key : TransitionKey,
) -> Unit raise ProtocolError {
let state = self.states.get(role)
guard event_triggered_transition(role, state, key) is Some(new_state) else {
let event_type = match key {
Plain(t) | Switch(t, _) => t
ClientRequest => Request
}
raise local_error(
"can't handle event type \{event_type} when role=\{role} and state=\{state}",
)
}
self.set_state(role, new_state)
}
///|
fn ConnectionState::fire_state_triggered_transitions(
self : ConnectionState,
) -> Unit {
// We apply these rules repeatedly until converging on a fixed point
for ;; {
let start_states = self.states
// It could happen that both these special-case transitions are enabled
// at the same time:
//
// DONE -> MIGHT_SWITCH_PROTOCOL
// DONE -> MUST_CLOSE
//
// For example, this will always be true of a HTTP/1.0 client requesting
// CONNECT. If this happens, the protocol switch takes priority. From
// there the client will either go to SWITCHED_PROTOCOL, in which case
// it's none of our business when they close the connection, or else the
// server will deny the request, in which case the client will go back to
// DONE and then from there to MUST_CLOSE.
if !self.pending_switch_proposals.is_empty() {
if self.states.client is Done {
self.set_state(Client, MightSwitchProtocol)
}
}
if self.pending_switch_proposals.is_empty() {
if self.states.client is MightSwitchProtocol {
self.set_state(Client, Done)
}
}
if !self.keep_alive {
for role in [Client, Server] {
if self.states.get(role) is Done {
self.set_state(role, MustClose)
}
}
}
// Tabular state-triggered transitions
if state_triggered_transition(self.states.client, self.states.server)
is Some(changes) {
self.states = changes
}
if self.states == start_states {
// Fixed point reached
return
}
}
}
///|
fn ConnectionState::start_next_cycle(
self : ConnectionState,
) -> Unit raise ProtocolError {
// (DONE/DONE can't be reached with keep-alive disabled or a pending
// protocol switch, but checking costs nothing and keeps this total.)
if self.states != { client: Done, server: Done, } ||
!self.keep_alive ||
!self.pending_switch_proposals.is_empty() {
raise local_error("not in a reusable state. self.states=\{self.states}")
}
self.states = { client: Idle, server: Idle, }
}