1################################################################ 2# The core state machine 3################################################################ 4# 5# Rule 1: everything that affects the state machine and state transitions must 6# live here in this file. As much as possible goes into the table-based 7# representation, but for the bits that don't quite fit, the actual code and 8# state must nonetheless live here. 9# 10# Rule 2: this file does not know about what role we're playing; it only knows 11# about HTTP request/response cycles in the abstract. This ensures that we 12# don't cheat and apply different rules to local and remote parties. 13# 14# 15# Theory of operation 16# =================== 17# 18# Possibly the simplest way to think about this is that we actually have 5 19# different state machines here. Yes, 5. These are: 20# 21# 1) The client state, with its complicated automaton (see the docs) 22# 2) The server state, with its complicated automaton (see the docs) 23# 3) The keep-alive state, with possible states {True, False} 24# 4) The SWITCH_CONNECT state, with possible states {False, True} 25# 5) The SWITCH_UPGRADE state, with possible states {False, True} 26# 27# For (3)-(5), the first state listed is the initial state. 28# 29# (1)-(3) are stored explicitly in member variables. The last 30# two are stored implicitly in the pending_switch_proposals set as: 31# (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals) 32# (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals) 33# 34# And each of these machines has two different kinds of transitions: 35# 36# a) Event-triggered 37# b) State-triggered 38# 39# Event triggered is the obvious thing that you'd think it is: some event 40# happens, and if it's the right event at the right time then a transition 41# happens. But there are somewhat complicated rules for which machines can 42# "see" which events. (As a rule of thumb, if a machine "sees" an event, this 43# means two things: the event can affect the machine, and if the machine is 44# not in a state where it expects that event then it's an error.) These rules 45# are: 46# 47# 1) The client machine sees all h11.events objects emitted by the client. 48# 49# 2) The server machine sees all h11.events objects emitted by the server. 50# 51# It also sees the client's Request event. 52# 53# And sometimes, server events are annotated with a _SWITCH_* event. For 54# example, we can have a (Response, _SWITCH_CONNECT) event, which is 55# different from a regular Response event. 56# 57# 3) The keep-alive machine sees the process_keep_alive_disabled() event 58# (which is derived from Request/Response events), and this event 59# transitions it from True -> False, or from False -> False. There's no way 60# to transition back. 61# 62# 4&5) The _SWITCH_* machines transition from False->True when we get a 63# Request that proposes the relevant type of switch (via 64# process_client_switch_proposals), and they go from True->False when we 65# get a Response that has no _SWITCH_* annotation. 66# 67# So that's event-triggered transitions. 68# 69# State-triggered transitions are less standard. What they do here is couple 70# the machines together. The way this works is, when certain *joint* 71# configurations of states are achieved, then we automatically transition to a 72# new *joint* state. So, for example, if we're ever in a joint state with 73# 74# client: DONE 75# keep-alive: False 76# 77# then the client state immediately transitions to: 78# 79# client: MUST_CLOSE 80# 81# This is fundamentally different from an event-based transition, because it 82# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state 83# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive 84# transitioned True -> False. Either way, once this precondition is satisfied, 85# this transition is immediately triggered. 86# 87# What if two conflicting state-based transitions get enabled at the same 88# time? In practice there's only one case where this arises (client DONE -> 89# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by 90# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition. 91# 92# Implementation 93# -------------- 94# 95# The event-triggered transitions for the server and client machines are all 96# stored explicitly in a table. Ditto for the state-triggered transitions that 97# involve just the server and client state. 98# 99# The transitions for the other machines, and the state-triggered transitions 100# that involve the other machines, are written out as explicit Python code. 101# 102# It'd be nice if there were some cleaner way to do all this. This isn't 103# *too* terrible, but I feel like it could probably be better. 104# 105# WARNING 106# ------- 107# 108# The script that generates the state machine diagrams for the docs knows how 109# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS 110# tables. But it can't automatically read the transitions that are written 111# directly in Python code. So if you touch those, you need to also update the 112# script to keep it in sync! 113 114from ._events import * 115from ._util import LocalProtocolError, make_sentinel 116 117# Everything in __all__ gets re-exported as part of the h11 public API. 118__all__ = [ 119 "CLIENT", 120 "SERVER", 121 "IDLE", 122 "SEND_RESPONSE", 123 "SEND_BODY", 124 "DONE", 125 "MUST_CLOSE", 126 "CLOSED", 127 "MIGHT_SWITCH_PROTOCOL", 128 "SWITCHED_PROTOCOL", 129 "ERROR", 130] 131 132CLIENT = make_sentinel("CLIENT") 133SERVER = make_sentinel("SERVER") 134 135# States 136IDLE = make_sentinel("IDLE") 137SEND_RESPONSE = make_sentinel("SEND_RESPONSE") 138SEND_BODY = make_sentinel("SEND_BODY") 139DONE = make_sentinel("DONE") 140MUST_CLOSE = make_sentinel("MUST_CLOSE") 141CLOSED = make_sentinel("CLOSED") 142ERROR = make_sentinel("ERROR") 143 144# Switch types 145MIGHT_SWITCH_PROTOCOL = make_sentinel("MIGHT_SWITCH_PROTOCOL") 146SWITCHED_PROTOCOL = make_sentinel("SWITCHED_PROTOCOL") 147 148_SWITCH_UPGRADE = make_sentinel("_SWITCH_UPGRADE") 149_SWITCH_CONNECT = make_sentinel("_SWITCH_CONNECT") 150 151EVENT_TRIGGERED_TRANSITIONS = { 152 CLIENT: { 153 IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED}, 154 SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, 155 DONE: {ConnectionClosed: CLOSED}, 156 MUST_CLOSE: {ConnectionClosed: CLOSED}, 157 CLOSED: {ConnectionClosed: CLOSED}, 158 MIGHT_SWITCH_PROTOCOL: {}, 159 SWITCHED_PROTOCOL: {}, 160 ERROR: {}, 161 }, 162 SERVER: { 163 IDLE: { 164 ConnectionClosed: CLOSED, 165 Response: SEND_BODY, 166 # Special case: server sees client Request events, in this form 167 (Request, CLIENT): SEND_RESPONSE, 168 }, 169 SEND_RESPONSE: { 170 InformationalResponse: SEND_RESPONSE, 171 Response: SEND_BODY, 172 (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL, 173 (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL, 174 }, 175 SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, 176 DONE: {ConnectionClosed: CLOSED}, 177 MUST_CLOSE: {ConnectionClosed: CLOSED}, 178 CLOSED: {ConnectionClosed: CLOSED}, 179 SWITCHED_PROTOCOL: {}, 180 ERROR: {}, 181 }, 182} 183 184# NB: there are also some special-case state-triggered transitions hard-coded 185# into _fire_state_triggered_transitions below. 186STATE_TRIGGERED_TRANSITIONS = { 187 # (Client state, Server state) -> new states 188 # Protocol negotiation 189 (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL}, 190 # Socket shutdown 191 (CLOSED, DONE): {SERVER: MUST_CLOSE}, 192 (CLOSED, IDLE): {SERVER: MUST_CLOSE}, 193 (ERROR, DONE): {SERVER: MUST_CLOSE}, 194 (DONE, CLOSED): {CLIENT: MUST_CLOSE}, 195 (IDLE, CLOSED): {CLIENT: MUST_CLOSE}, 196 (DONE, ERROR): {CLIENT: MUST_CLOSE}, 197} 198 199 200class ConnectionState: 201 def __init__(self): 202 # Extra bits of state that don't quite fit into the state model. 203 204 # If this is False then it enables the automatic DONE -> MUST_CLOSE 205 # transition. Don't set this directly; call .keep_alive_disabled() 206 self.keep_alive = True 207 208 # This is a subset of {UPGRADE, CONNECT}, containing the proposals 209 # made by the client for switching protocols. 210 self.pending_switch_proposals = set() 211 212 self.states = {CLIENT: IDLE, SERVER: IDLE} 213 214 def process_error(self, role): 215 self.states[role] = ERROR 216 self._fire_state_triggered_transitions() 217 218 def process_keep_alive_disabled(self): 219 self.keep_alive = False 220 self._fire_state_triggered_transitions() 221 222 def process_client_switch_proposal(self, switch_event): 223 self.pending_switch_proposals.add(switch_event) 224 self._fire_state_triggered_transitions() 225 226 def process_event(self, role, event_type, server_switch_event=None): 227 if server_switch_event is not None: 228 assert role is SERVER 229 if server_switch_event not in self.pending_switch_proposals: 230 raise LocalProtocolError( 231 "Received server {} event without a pending proposal".format( 232 server_switch_event 233 ) 234 ) 235 event_type = (event_type, server_switch_event) 236 if server_switch_event is None and event_type is Response: 237 self.pending_switch_proposals = set() 238 self._fire_event_triggered_transitions(role, event_type) 239 # Special case: the server state does get to see Request 240 # events. 241 if event_type is Request: 242 assert role is CLIENT 243 self._fire_event_triggered_transitions(SERVER, (Request, CLIENT)) 244 self._fire_state_triggered_transitions() 245 246 def _fire_event_triggered_transitions(self, role, event_type): 247 state = self.states[role] 248 try: 249 new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type] 250 except KeyError: 251 raise LocalProtocolError( 252 "can't handle event type {} when role={} and state={}".format( 253 event_type.__name__, role, self.states[role] 254 ) 255 ) 256 self.states[role] = new_state 257 258 def _fire_state_triggered_transitions(self): 259 # We apply these rules repeatedly until converging on a fixed point 260 while True: 261 start_states = dict(self.states) 262 263 # It could happen that both these special-case transitions are 264 # enabled at the same time: 265 # 266 # DONE -> MIGHT_SWITCH_PROTOCOL 267 # DONE -> MUST_CLOSE 268 # 269 # For example, this will always be true of a HTTP/1.0 client 270 # requesting CONNECT. If this happens, the protocol switch takes 271 # priority. From there the client will either go to 272 # SWITCHED_PROTOCOL, in which case it's none of our business when 273 # they close the connection, or else the server will deny the 274 # request, in which case the client will go back to DONE and then 275 # from there to MUST_CLOSE. 276 if self.pending_switch_proposals: 277 if self.states[CLIENT] is DONE: 278 self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL 279 280 if not self.pending_switch_proposals: 281 if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL: 282 self.states[CLIENT] = DONE 283 284 if not self.keep_alive: 285 for role in (CLIENT, SERVER): 286 if self.states[role] is DONE: 287 self.states[role] = MUST_CLOSE 288 289 # Tabular state-triggered transitions 290 joint_state = (self.states[CLIENT], self.states[SERVER]) 291 changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {}) 292 self.states.update(changes) 293 294 if self.states == start_states: 295 # Fixed point reached 296 return 297 298 def start_next_cycle(self): 299 if self.states != {CLIENT: DONE, SERVER: DONE}: 300 raise LocalProtocolError( 301 "not in a reusable state. self.states={}".format(self.states) 302 ) 303 # Can't reach DONE/DONE with any of these active, but still, let's be 304 # sure. 305 assert self.keep_alive 306 assert not self.pending_switch_proposals 307 self.states = {CLIENT: IDLE, SERVER: IDLE} 308