Update 2025-04-13_16:26:56
This commit is contained in:
367
venv/lib/python3.11/site-packages/h11/_state.py
Normal file
367
venv/lib/python3.11/site-packages/h11/_state.py
Normal file
@ -0,0 +1,367 @@
|
||||
################################################################
|
||||
# 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}
|
Reference in New Issue
Block a user