123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- ################################################################
- # The core state machine
- ################################################################
- #
- # Rule 1: everything that affects the state machine and state transitions must
- # live here in this file. 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. This ensures that we
- # don't cheat and apply different rules to local and remote parties.
- #
- #
- # Theory of operation
- # ===================
- #
- # Possibly the simplest way to think about this is that we actually have 5
- # different state machines here. Yes, 5. These are:
- #
- # 1) The client state, with its complicated automaton (see the docs)
- # 2) The server state, with its complicated automaton (see the docs)
- # 3) The keep-alive state, with possible states {True, False}
- # 4) The SWITCH_CONNECT state, with possible states {False, True}
- # 5) The SWITCH_UPGRADE state, with possible states {False, True}
- #
- # For (3)-(5), the first state listed is the initial state.
- #
- # (1)-(3) are stored explicitly in member variables. The last
- # two are stored implicitly in the pending_switch_proposals set as:
- # (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals)
- # (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals)
- #
- # And each of these machines has two different kinds of transitions:
- #
- # a) Event-triggered
- # b) State-triggered
- #
- # Event triggered is the obvious thing that you'd think it is: some event
- # happens, and if it's the right event at the right time then a transition
- # happens. But there are somewhat complicated rules for which machines can
- # "see" which events. (As a rule of thumb, if a machine "sees" an event, this
- # means two things: the event can affect the machine, and if the machine is
- # not in a state where it expects that event then it's an error.) These rules
- # are:
- #
- # 1) The client machine sees all h11.events objects emitted by the client.
- #
- # 2) The server machine sees all h11.events objects emitted by the server.
- #
- # It also sees the client's Request event.
- #
- # And sometimes, server events are annotated with a _SWITCH_* event. For
- # example, we can have a (Response, _SWITCH_CONNECT) event, which is
- # different from a regular Response event.
- #
- # 3) The keep-alive machine sees the process_keep_alive_disabled() event
- # (which is derived from Request/Response events), and this event
- # transitions it from True -> False, or from False -> False. There's no way
- # to transition back.
- #
- # 4&5) The _SWITCH_* machines transition from False->True when we get a
- # Request that proposes the relevant type of switch (via
- # process_client_switch_proposals), and they go from True->False when we
- # get a Response that has no _SWITCH_* annotation.
- #
- # So that's event-triggered transitions.
- #
- # State-triggered transitions are less standard. What they do here is couple
- # the machines together. The way this works is, when certain *joint*
- # configurations of states are achieved, then we automatically transition to a
- # new *joint* state. So, for example, if we're ever in a joint state with
- #
- # client: DONE
- # keep-alive: False
- #
- # then the client state immediately transitions to:
- #
- # client: MUST_CLOSE
- #
- # This is fundamentally different from an event-based transition, because it
- # doesn't matter how we arrived at the {client: DONE, keep-alive: False} state
- # -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive
- # transitioned True -> False. Either way, once this precondition is satisfied,
- # this transition is immediately triggered.
- #
- # What if two conflicting state-based transitions get enabled at the same
- # time? In practice there's only one case where this arises (client DONE ->
- # MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by
- # explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition.
- #
- # Implementation
- # --------------
- #
- # The event-triggered transitions for the server and client machines are all
- # stored explicitly in a table. Ditto for the state-triggered transitions that
- # involve just the server and client state.
- #
- # The transitions for the other machines, and the state-triggered transitions
- # that involve the other machines, are written out as explicit Python code.
- #
- # It'd be nice if there were some cleaner way to do all this. This isn't
- # *too* terrible, but I feel like it could probably be better.
- #
- # WARNING
- # -------
- #
- # The script that generates the state machine diagrams for the docs knows how
- # to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS
- # tables. But it can't automatically read the transitions that are written
- # directly in Python code. So if you touch those, you need to also update the
- # script to keep it in sync!
- from typing import cast, Dict, Optional, Set, Tuple, Type, Union
- from ._events import *
- from ._util import LocalProtocolError, Sentinel
- # Everything in __all__ gets re-exported as part of the h11 public API.
- __all__ = [
- "CLIENT",
- "SERVER",
- "IDLE",
- "SEND_RESPONSE",
- "SEND_BODY",
- "DONE",
- "MUST_CLOSE",
- "CLOSED",
- "MIGHT_SWITCH_PROTOCOL",
- "SWITCHED_PROTOCOL",
- "ERROR",
- ]
- class CLIENT(Sentinel, metaclass=Sentinel):
- pass
- class SERVER(Sentinel, metaclass=Sentinel):
- pass
- # States
- class IDLE(Sentinel, metaclass=Sentinel):
- pass
- class SEND_RESPONSE(Sentinel, metaclass=Sentinel):
- pass
- class SEND_BODY(Sentinel, metaclass=Sentinel):
- pass
- class DONE(Sentinel, metaclass=Sentinel):
- pass
- class MUST_CLOSE(Sentinel, metaclass=Sentinel):
- pass
- class CLOSED(Sentinel, metaclass=Sentinel):
- pass
- class ERROR(Sentinel, metaclass=Sentinel):
- pass
- # Switch types
- class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):
- pass
- class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):
- pass
- class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):
- pass
- class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
- pass
- EventTransitionType = Dict[
- Type[Sentinel],
- Dict[
- Type[Sentinel],
- Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],
- ],
- ]
- EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = {
- CLIENT: {
- IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED},
- SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
- DONE: {ConnectionClosed: CLOSED},
- MUST_CLOSE: {ConnectionClosed: CLOSED},
- CLOSED: {ConnectionClosed: CLOSED},
- MIGHT_SWITCH_PROTOCOL: {},
- SWITCHED_PROTOCOL: {},
- ERROR: {},
- },
- SERVER: {
- IDLE: {
- ConnectionClosed: CLOSED,
- Response: SEND_BODY,
- # Special case: server sees client Request events, in this form
- (Request, CLIENT): SEND_RESPONSE,
- },
- SEND_RESPONSE: {
- InformationalResponse: SEND_RESPONSE,
- Response: SEND_BODY,
- (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL,
- (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL,
- },
- SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
- DONE: {ConnectionClosed: CLOSED},
- MUST_CLOSE: {ConnectionClosed: CLOSED},
- CLOSED: {ConnectionClosed: CLOSED},
- SWITCHED_PROTOCOL: {},
- ERROR: {},
- },
- }
- StateTransitionType = Dict[
- Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]]
- ]
- # NB: there are also some special-case state-triggered transitions hard-coded
- # into _fire_state_triggered_transitions below.
- STATE_TRIGGERED_TRANSITIONS: StateTransitionType = {
- # (Client state, Server state) -> new states
- # Protocol negotiation
- (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL},
- # Socket shutdown
- (CLOSED, DONE): {SERVER: MUST_CLOSE},
- (CLOSED, IDLE): {SERVER: MUST_CLOSE},
- (ERROR, DONE): {SERVER: MUST_CLOSE},
- (DONE, CLOSED): {CLIENT: MUST_CLOSE},
- (IDLE, CLOSED): {CLIENT: MUST_CLOSE},
- (DONE, ERROR): {CLIENT: MUST_CLOSE},
- }
- class ConnectionState:
- def __init__(self) -> None:
- # Extra bits of state that don't quite fit into the state model.
- # If this is False then it enables the automatic DONE -> MUST_CLOSE
- # transition. Don't set this directly; call .keep_alive_disabled()
- self.keep_alive = True
- # This is a subset of {UPGRADE, CONNECT}, containing the proposals
- # made by the client for switching protocols.
- self.pending_switch_proposals: Set[Type[Sentinel]] = set()
- self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}
- def process_error(self, role: Type[Sentinel]) -> None:
- self.states[role] = ERROR
- self._fire_state_triggered_transitions()
- def process_keep_alive_disabled(self) -> None:
- self.keep_alive = False
- self._fire_state_triggered_transitions()
- def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:
- self.pending_switch_proposals.add(switch_event)
- self._fire_state_triggered_transitions()
- def process_event(
- self,
- role: Type[Sentinel],
- event_type: Type[Event],
- server_switch_event: Optional[Type[Sentinel]] = None,
- ) -> None:
- _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type
- if server_switch_event is not None:
- assert role is SERVER
- if server_switch_event not in self.pending_switch_proposals:
- raise LocalProtocolError(
- "Received server {} event without a pending proposal".format(
- server_switch_event
- )
- )
- _event_type = (event_type, server_switch_event)
- if server_switch_event is None and _event_type is Response:
- self.pending_switch_proposals = set()
- self._fire_event_triggered_transitions(role, _event_type)
- # Special case: the server state does get to see Request
- # events.
- if _event_type is Request:
- assert role is CLIENT
- self._fire_event_triggered_transitions(SERVER, (Request, CLIENT))
- self._fire_state_triggered_transitions()
- def _fire_event_triggered_transitions(
- self,
- role: Type[Sentinel],
- event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],
- ) -> None:
- state = self.states[role]
- try:
- new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
- except KeyError:
- event_type = cast(Type[Event], event_type)
- raise LocalProtocolError(
- "can't handle event type {} when role={} and state={}".format(
- event_type.__name__, role, self.states[role]
- )
- ) from None
- self.states[role] = new_state
- def _fire_state_triggered_transitions(self) -> None:
- # We apply these rules repeatedly until converging on a fixed point
- while True:
- start_states = dict(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:
- if self.states[CLIENT] is DONE:
- self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL
- if not self.pending_switch_proposals:
- if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL:
- self.states[CLIENT] = DONE
- if not self.keep_alive:
- for role in (CLIENT, SERVER):
- if self.states[role] is DONE:
- self.states[role] = MUST_CLOSE
- # Tabular state-triggered transitions
- joint_state = (self.states[CLIENT], self.states[SERVER])
- changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {})
- self.states.update(changes)
- if self.states == start_states:
- # Fixed point reached
- return
- def start_next_cycle(self) -> None:
- if self.states != {CLIENT: DONE, SERVER: DONE}:
- raise LocalProtocolError(
- "not in a reusable state. self.states={}".format(self.states)
- )
- # Can't reach DONE/DONE with any of these active, but still, let's be
- # sure.
- assert self.keep_alive
- assert not self.pending_switch_proposals
- self.states = {CLIENT: IDLE, SERVER: IDLE}
|