You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

304 lines
12 KiB

  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. from ._events import *
  114. from ._util import LocalProtocolError, make_sentinel
  115. # Everything in __all__ gets re-exported as part of the h11 public API.
  116. __all__ = []
  117. # Be careful of trailing whitespace here:
  118. sentinels = ("CLIENT SERVER "
  119. # States
  120. "IDLE SEND_RESPONSE SEND_BODY DONE MUST_CLOSE CLOSED "
  121. "MIGHT_SWITCH_PROTOCOL SWITCHED_PROTOCOL ERROR "
  122. # Switch types
  123. "_SWITCH_UPGRADE _SWITCH_CONNECT").split()
  124. for token in sentinels:
  125. globals()[token] = make_sentinel(token)
  126. __all__ += [s for s in sentinels if not s.startswith("_")]
  127. EVENT_TRIGGERED_TRANSITIONS = {
  128. CLIENT: {
  129. IDLE: {
  130. Request: SEND_BODY,
  131. ConnectionClosed: CLOSED,
  132. },
  133. SEND_BODY: {
  134. Data: SEND_BODY,
  135. EndOfMessage: DONE,
  136. },
  137. DONE: {
  138. ConnectionClosed: CLOSED,
  139. },
  140. MUST_CLOSE: {
  141. ConnectionClosed: CLOSED,
  142. },
  143. CLOSED: {
  144. ConnectionClosed: CLOSED,
  145. },
  146. MIGHT_SWITCH_PROTOCOL: {},
  147. SWITCHED_PROTOCOL: {},
  148. ERROR: {},
  149. },
  150. SERVER: {
  151. IDLE: {
  152. ConnectionClosed: CLOSED,
  153. Response: SEND_BODY,
  154. # Special case: server sees client Request events, in this form
  155. (Request, CLIENT): SEND_RESPONSE,
  156. },
  157. SEND_RESPONSE: {
  158. InformationalResponse: SEND_RESPONSE,
  159. Response: SEND_BODY,
  160. (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL,
  161. (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL,
  162. },
  163. SEND_BODY: {
  164. Data: SEND_BODY,
  165. EndOfMessage: DONE,
  166. },
  167. DONE: {
  168. ConnectionClosed: CLOSED,
  169. },
  170. MUST_CLOSE: {
  171. ConnectionClosed: CLOSED,
  172. },
  173. CLOSED: {
  174. ConnectionClosed: CLOSED,
  175. },
  176. SWITCHED_PROTOCOL: {},
  177. ERROR: {},
  178. },
  179. }
  180. # NB: there are also some special-case state-triggered transitions hard-coded
  181. # into _fire_state_triggered_transitions below.
  182. STATE_TRIGGERED_TRANSITIONS = {
  183. # (Client state, Server state) -> new states
  184. # Protocol negotiation
  185. (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL},
  186. # Socket shutdown
  187. (CLOSED, DONE): {SERVER: MUST_CLOSE},
  188. (CLOSED, IDLE): {SERVER: MUST_CLOSE},
  189. (ERROR, DONE): {SERVER: MUST_CLOSE},
  190. (DONE, CLOSED): {CLIENT: MUST_CLOSE},
  191. (IDLE, CLOSED): {CLIENT: MUST_CLOSE},
  192. (DONE, ERROR): {CLIENT: MUST_CLOSE},
  193. }
  194. class ConnectionState(object):
  195. def __init__(self):
  196. # Extra bits of state that don't quite fit into the state model.
  197. # If this is False then it enables the automatic DONE -> MUST_CLOSE
  198. # transition. Don't set this directly; call .keep_alive_disabled()
  199. self.keep_alive = True
  200. # This is a subset of {UPGRADE, CONNECT}, containing the proposals
  201. # made by the client for switching protocols.
  202. self.pending_switch_proposals = set()
  203. self.states = {CLIENT: IDLE, SERVER: IDLE}
  204. def process_error(self, role):
  205. self.states[role] = ERROR
  206. self._fire_state_triggered_transitions()
  207. def process_keep_alive_disabled(self):
  208. self.keep_alive = False
  209. self._fire_state_triggered_transitions()
  210. def process_client_switch_proposal(self, switch_event):
  211. self.pending_switch_proposals.add(switch_event)
  212. self._fire_state_triggered_transitions()
  213. def process_event(self, role, event_type, server_switch_event=None):
  214. if server_switch_event is not None:
  215. assert role is SERVER
  216. if server_switch_event not in self.pending_switch_proposals:
  217. raise LocalProtocolError(
  218. "Received server {} event without a pending proposal"
  219. .format(server_switch_event))
  220. event_type = (event_type, server_switch_event)
  221. if server_switch_event is None and event_type is Response:
  222. self.pending_switch_proposals = set()
  223. self._fire_event_triggered_transitions(role, event_type)
  224. # Special case: the server state does get to see Request
  225. # events.
  226. if event_type is Request:
  227. assert role is CLIENT
  228. self._fire_event_triggered_transitions(SERVER, (Request, CLIENT))
  229. self._fire_state_triggered_transitions()
  230. def _fire_event_triggered_transitions(self, role, event_type):
  231. state = self.states[role]
  232. try:
  233. new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
  234. except KeyError:
  235. raise LocalProtocolError(
  236. "can't handle event type {} when role={} and state={}"
  237. .format(event_type.__name__, role, self.states[role]))
  238. self.states[role] = new_state
  239. def _fire_state_triggered_transitions(self):
  240. # We apply these rules repeatedly until converging on a fixed point
  241. while True:
  242. start_states = dict(self.states)
  243. # It could happen that both these special-case transitions are
  244. # enabled at the same time:
  245. #
  246. # DONE -> MIGHT_SWITCH_PROTOCOL
  247. # DONE -> MUST_CLOSE
  248. #
  249. # For example, this will always be true of a HTTP/1.0 client
  250. # requesting CONNECT. If this happens, the protocol switch takes
  251. # priority. From there the client will either go to
  252. # SWITCHED_PROTOCOL, in which case it's none of our business when
  253. # they close the connection, or else the server will deny the
  254. # request, in which case the client will go back to DONE and then
  255. # from there to MUST_CLOSE.
  256. if self.pending_switch_proposals:
  257. if self.states[CLIENT] is DONE:
  258. self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL
  259. if not self.pending_switch_proposals:
  260. if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL:
  261. self.states[CLIENT] = DONE
  262. if not self.keep_alive:
  263. for role in (CLIENT, SERVER):
  264. if self.states[role] is DONE:
  265. self.states[role] = MUST_CLOSE
  266. # Tabular state-triggered transitions
  267. joint_state = (self.states[CLIENT], self.states[SERVER])
  268. changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {})
  269. self.states.update(changes)
  270. if self.states == start_states:
  271. # Fixed point reached
  272. return
  273. def start_next_cycle(self):
  274. if self.states != {CLIENT: DONE, SERVER: DONE}:
  275. raise LocalProtocolError("not in a reusable state")
  276. # Can't reach DONE/DONE with any of these active, but still, let's be
  277. # sure.
  278. assert self.keep_alive
  279. assert not self.pending_switch_proposals
  280. self.states = {CLIENT: IDLE, SERVER: IDLE}