Загрузить файлы в «venv/Lib/site-packages/h11»
This commit is contained in:
250
venv/Lib/site-packages/h11/_readers.py
Normal file
250
venv/Lib/site-packages/h11/_readers.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# Code to read HTTP data
|
||||||
|
#
|
||||||
|
# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
|
||||||
|
# either:
|
||||||
|
# 1) consumes some of it and returns an Event
|
||||||
|
# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
|
||||||
|
# and it might raise a LocalProtocolError, so simpler just to always use
|
||||||
|
# this)
|
||||||
|
# 3) returns None, meaning "I need more data"
|
||||||
|
#
|
||||||
|
# If they have a .read_eof attribute, then this will be called if an EOF is
|
||||||
|
# received -- but this is optional. Either way, the actual ConnectionClosed
|
||||||
|
# event will be generated afterwards.
|
||||||
|
#
|
||||||
|
# READERS is a dict describing how to pick a reader. It maps states to either:
|
||||||
|
# - a reader
|
||||||
|
# - or, for body readers, a dict of per-framing reader factories
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
from ._abnf import chunk_header, header_field, request_line, status_line
|
||||||
|
from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
|
||||||
|
from ._receivebuffer import ReceiveBuffer
|
||||||
|
from ._state import (
|
||||||
|
CLIENT,
|
||||||
|
CLOSED,
|
||||||
|
DONE,
|
||||||
|
IDLE,
|
||||||
|
MUST_CLOSE,
|
||||||
|
SEND_BODY,
|
||||||
|
SEND_RESPONSE,
|
||||||
|
SERVER,
|
||||||
|
)
|
||||||
|
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
|
||||||
|
|
||||||
|
__all__ = ["READERS"]
|
||||||
|
|
||||||
|
header_field_re = re.compile(header_field.encode("ascii"))
|
||||||
|
obs_fold_re = re.compile(rb"[ \t]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
|
||||||
|
it = iter(lines)
|
||||||
|
last: Optional[bytes] = None
|
||||||
|
for line in it:
|
||||||
|
match = obs_fold_re.match(line)
|
||||||
|
if match:
|
||||||
|
if last is None:
|
||||||
|
raise LocalProtocolError("continuation line at start of headers")
|
||||||
|
if not isinstance(last, bytearray):
|
||||||
|
# Cast to a mutable type, avoiding copy on append to ensure O(n) time
|
||||||
|
last = bytearray(last)
|
||||||
|
last += b" "
|
||||||
|
last += line[match.end() :]
|
||||||
|
else:
|
||||||
|
if last is not None:
|
||||||
|
yield last
|
||||||
|
last = line
|
||||||
|
if last is not None:
|
||||||
|
yield last
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_header_lines(
|
||||||
|
lines: Iterable[bytes],
|
||||||
|
) -> Iterable[Tuple[bytes, bytes]]:
|
||||||
|
for line in _obsolete_line_fold(lines):
|
||||||
|
matches = validate(header_field_re, line, "illegal header line: {!r}", line)
|
||||||
|
yield (matches["field_name"], matches["field_value"])
|
||||||
|
|
||||||
|
|
||||||
|
request_line_re = re.compile(request_line.encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
|
||||||
|
lines = buf.maybe_extract_lines()
|
||||||
|
if lines is None:
|
||||||
|
if buf.is_next_line_obviously_invalid_request_line():
|
||||||
|
raise LocalProtocolError("illegal request line")
|
||||||
|
return None
|
||||||
|
if not lines:
|
||||||
|
raise LocalProtocolError("no request line received")
|
||||||
|
matches = validate(
|
||||||
|
request_line_re, lines[0], "illegal request line: {!r}", lines[0]
|
||||||
|
)
|
||||||
|
return Request(
|
||||||
|
headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
status_line_re = re.compile(status_line.encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_read_from_SEND_RESPONSE_server(
|
||||||
|
buf: ReceiveBuffer,
|
||||||
|
) -> Union[InformationalResponse, Response, None]:
|
||||||
|
lines = buf.maybe_extract_lines()
|
||||||
|
if lines is None:
|
||||||
|
if buf.is_next_line_obviously_invalid_request_line():
|
||||||
|
raise LocalProtocolError("illegal request line")
|
||||||
|
return None
|
||||||
|
if not lines:
|
||||||
|
raise LocalProtocolError("no response line received")
|
||||||
|
matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
|
||||||
|
http_version = (
|
||||||
|
b"1.1" if matches["http_version"] is None else matches["http_version"]
|
||||||
|
)
|
||||||
|
reason = b"" if matches["reason"] is None else matches["reason"]
|
||||||
|
status_code = int(matches["status_code"])
|
||||||
|
class_: Union[Type[InformationalResponse], Type[Response]] = (
|
||||||
|
InformationalResponse if status_code < 200 else Response
|
||||||
|
)
|
||||||
|
return class_(
|
||||||
|
headers=list(_decode_header_lines(lines[1:])),
|
||||||
|
_parsed=True,
|
||||||
|
status_code=status_code,
|
||||||
|
reason=reason,
|
||||||
|
http_version=http_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentLengthReader:
|
||||||
|
def __init__(self, length: int) -> None:
|
||||||
|
self._length = length
|
||||||
|
self._remaining = length
|
||||||
|
|
||||||
|
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
|
||||||
|
if self._remaining == 0:
|
||||||
|
return EndOfMessage()
|
||||||
|
data = buf.maybe_extract_at_most(self._remaining)
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
self._remaining -= len(data)
|
||||||
|
return Data(data=data)
|
||||||
|
|
||||||
|
def read_eof(self) -> NoReturn:
|
||||||
|
raise RemoteProtocolError(
|
||||||
|
"peer closed connection without sending complete message body "
|
||||||
|
"(received {} bytes, expected {})".format(
|
||||||
|
self._length - self._remaining, self._length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
chunk_header_re = re.compile(chunk_header.encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkedReader:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._bytes_in_chunk = 0
|
||||||
|
# After reading a chunk, we have to throw away the trailing \r\n.
|
||||||
|
# This tracks the bytes that we need to match and throw away.
|
||||||
|
self._bytes_to_discard = b""
|
||||||
|
self._reading_trailer = False
|
||||||
|
|
||||||
|
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
|
||||||
|
if self._reading_trailer:
|
||||||
|
lines = buf.maybe_extract_lines()
|
||||||
|
if lines is None:
|
||||||
|
return None
|
||||||
|
return EndOfMessage(headers=list(_decode_header_lines(lines)))
|
||||||
|
if self._bytes_to_discard:
|
||||||
|
data = buf.maybe_extract_at_most(len(self._bytes_to_discard))
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
if data != self._bytes_to_discard[: len(data)]:
|
||||||
|
raise LocalProtocolError(
|
||||||
|
f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})"
|
||||||
|
)
|
||||||
|
self._bytes_to_discard = self._bytes_to_discard[len(data) :]
|
||||||
|
if self._bytes_to_discard:
|
||||||
|
return None
|
||||||
|
# else, fall through and read some more
|
||||||
|
assert self._bytes_to_discard == b""
|
||||||
|
if self._bytes_in_chunk == 0:
|
||||||
|
# We need to refill our chunk count
|
||||||
|
chunk_header = buf.maybe_extract_next_line()
|
||||||
|
if chunk_header is None:
|
||||||
|
return None
|
||||||
|
matches = validate(
|
||||||
|
chunk_header_re,
|
||||||
|
chunk_header,
|
||||||
|
"illegal chunk header: {!r}",
|
||||||
|
chunk_header,
|
||||||
|
)
|
||||||
|
# XX FIXME: we discard chunk extensions. Does anyone care?
|
||||||
|
self._bytes_in_chunk = int(matches["chunk_size"], base=16)
|
||||||
|
if self._bytes_in_chunk == 0:
|
||||||
|
self._reading_trailer = True
|
||||||
|
return self(buf)
|
||||||
|
chunk_start = True
|
||||||
|
else:
|
||||||
|
chunk_start = False
|
||||||
|
assert self._bytes_in_chunk > 0
|
||||||
|
data = buf.maybe_extract_at_most(self._bytes_in_chunk)
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
self._bytes_in_chunk -= len(data)
|
||||||
|
if self._bytes_in_chunk == 0:
|
||||||
|
self._bytes_to_discard = b"\r\n"
|
||||||
|
chunk_end = True
|
||||||
|
else:
|
||||||
|
chunk_end = False
|
||||||
|
return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
|
||||||
|
|
||||||
|
def read_eof(self) -> NoReturn:
|
||||||
|
raise RemoteProtocolError(
|
||||||
|
"peer closed connection without sending complete message body "
|
||||||
|
"(incomplete chunked read)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Http10Reader:
|
||||||
|
def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
|
||||||
|
data = buf.maybe_extract_at_most(999999999)
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return Data(data=data)
|
||||||
|
|
||||||
|
def read_eof(self) -> EndOfMessage:
|
||||||
|
return EndOfMessage()
|
||||||
|
|
||||||
|
|
||||||
|
def expect_nothing(buf: ReceiveBuffer) -> None:
|
||||||
|
if buf:
|
||||||
|
raise LocalProtocolError("Got data when expecting EOF")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
ReadersType = Dict[
|
||||||
|
Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
|
||||||
|
Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
|
||||||
|
]
|
||||||
|
|
||||||
|
READERS: ReadersType = {
|
||||||
|
(CLIENT, IDLE): maybe_read_from_IDLE_client,
|
||||||
|
(SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
|
||||||
|
(SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
|
||||||
|
(CLIENT, DONE): expect_nothing,
|
||||||
|
(CLIENT, MUST_CLOSE): expect_nothing,
|
||||||
|
(CLIENT, CLOSED): expect_nothing,
|
||||||
|
(SERVER, DONE): expect_nothing,
|
||||||
|
(SERVER, MUST_CLOSE): expect_nothing,
|
||||||
|
(SERVER, CLOSED): expect_nothing,
|
||||||
|
SEND_BODY: {
|
||||||
|
"chunked": ChunkedReader,
|
||||||
|
"content-length": ContentLengthReader,
|
||||||
|
"http/1.0": Http10Reader,
|
||||||
|
},
|
||||||
|
}
|
||||||
153
venv/Lib/site-packages/h11/_receivebuffer.py
Normal file
153
venv/Lib/site-packages/h11/_receivebuffer.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
__all__ = ["ReceiveBuffer"]
|
||||||
|
|
||||||
|
|
||||||
|
# Operations we want to support:
|
||||||
|
# - find next \r\n or \r\n\r\n (\n or \n\n are also acceptable),
|
||||||
|
# or wait until there is one
|
||||||
|
# - read at-most-N bytes
|
||||||
|
# Goals:
|
||||||
|
# - on average, do this fast
|
||||||
|
# - worst case, do this in O(n) where n is the number of bytes processed
|
||||||
|
# Plan:
|
||||||
|
# - store bytearray, offset, how far we've searched for a separator token
|
||||||
|
# - use the how-far-we've-searched data to avoid rescanning
|
||||||
|
# - while doing a stream of uninterrupted processing, advance offset instead
|
||||||
|
# of constantly copying
|
||||||
|
# WARNING:
|
||||||
|
# - I haven't benchmarked or profiled any of this yet.
|
||||||
|
#
|
||||||
|
# Note that starting in Python 3.4, deleting the initial n bytes from a
|
||||||
|
# bytearray is amortized O(n), thanks to some excellent work by Antoine
|
||||||
|
# Martin:
|
||||||
|
#
|
||||||
|
# https://bugs.python.org/issue19087
|
||||||
|
#
|
||||||
|
# This means that if we only supported 3.4+, we could get rid of the code here
|
||||||
|
# involving self._start and self.compress, because it's doing exactly the same
|
||||||
|
# thing that bytearray now does internally.
|
||||||
|
#
|
||||||
|
# BUT unfortunately, we still support 2.7, and reading short segments out of a
|
||||||
|
# long buffer MUST be O(bytes read) to avoid DoS issues, so we can't actually
|
||||||
|
# delete this code. Yet:
|
||||||
|
#
|
||||||
|
# https://pythonclock.org/
|
||||||
|
#
|
||||||
|
# (Two things to double-check first though: make sure PyPy also has the
|
||||||
|
# optimization, and benchmark to make sure it's a win, since we do have a
|
||||||
|
# slightly clever thing where we delay calling compress() until we've
|
||||||
|
# processed a whole event, which could in theory be slightly more efficient
|
||||||
|
# than the internal bytearray support.)
|
||||||
|
blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiveBuffer:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._data = bytearray()
|
||||||
|
self._next_line_search = 0
|
||||||
|
self._multiple_lines_search = 0
|
||||||
|
|
||||||
|
def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer":
|
||||||
|
self._data += byteslike
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(len(self))
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
# for @property unprocessed_data
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return bytes(self._data)
|
||||||
|
|
||||||
|
def _extract(self, count: int) -> bytearray:
|
||||||
|
# extracting an initial slice of the data buffer and return it
|
||||||
|
out = self._data[:count]
|
||||||
|
del self._data[:count]
|
||||||
|
|
||||||
|
self._next_line_search = 0
|
||||||
|
self._multiple_lines_search = 0
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def maybe_extract_at_most(self, count: int) -> Optional[bytearray]:
|
||||||
|
"""
|
||||||
|
Extract a fixed number of bytes from the buffer.
|
||||||
|
"""
|
||||||
|
out = self._data[:count]
|
||||||
|
if not out:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._extract(count)
|
||||||
|
|
||||||
|
def maybe_extract_next_line(self) -> Optional[bytearray]:
|
||||||
|
"""
|
||||||
|
Extract the first line, if it is completed in the buffer.
|
||||||
|
"""
|
||||||
|
# Only search in buffer space that we've not already looked at.
|
||||||
|
search_start_index = max(0, self._next_line_search - 1)
|
||||||
|
partial_idx = self._data.find(b"\r\n", search_start_index)
|
||||||
|
|
||||||
|
if partial_idx == -1:
|
||||||
|
self._next_line_search = len(self._data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# + 2 is to compensate len(b"\r\n")
|
||||||
|
idx = partial_idx + 2
|
||||||
|
|
||||||
|
return self._extract(idx)
|
||||||
|
|
||||||
|
def maybe_extract_lines(self) -> Optional[List[bytearray]]:
|
||||||
|
"""
|
||||||
|
Extract everything up to the first blank line, and return a list of lines.
|
||||||
|
"""
|
||||||
|
# Handle the case where we have an immediate empty line.
|
||||||
|
if self._data[:1] == b"\n":
|
||||||
|
self._extract(1)
|
||||||
|
return []
|
||||||
|
|
||||||
|
if self._data[:2] == b"\r\n":
|
||||||
|
self._extract(2)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Only search in buffer space that we've not already looked at.
|
||||||
|
match = blank_line_regex.search(self._data, self._multiple_lines_search)
|
||||||
|
if match is None:
|
||||||
|
self._multiple_lines_search = max(0, len(self._data) - 2)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Truncate the buffer and return it.
|
||||||
|
idx = match.span(0)[-1]
|
||||||
|
out = self._extract(idx)
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.endswith(b"\r"):
|
||||||
|
del line[-1]
|
||||||
|
|
||||||
|
assert lines[-2] == lines[-1] == b""
|
||||||
|
|
||||||
|
del lines[-2:]
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# In theory we should wait until `\r\n` before starting to validate
|
||||||
|
# incoming data. However it's interesting to detect (very) invalid data
|
||||||
|
# early given they might not even contain `\r\n` at all (hence only
|
||||||
|
# timeout will get rid of them).
|
||||||
|
# This is not a 100% effective detection but more of a cheap sanity check
|
||||||
|
# allowing for early abort in some useful cases.
|
||||||
|
# This is especially interesting when peer is messing up with HTTPS and
|
||||||
|
# sent us a TLS stream where we were expecting plain HTTP given all
|
||||||
|
# versions of TLS so far start handshake with a 0x16 message type code.
|
||||||
|
def is_next_line_obviously_invalid_request_line(self) -> bool:
|
||||||
|
try:
|
||||||
|
# HTTP header line must not contain non-printable characters
|
||||||
|
# and should not start with a space
|
||||||
|
return self._data[0] < 0x21
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
365
venv/Lib/site-packages/h11/_state.py
Normal file
365
venv/Lib/site-packages/h11/_state.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
################################################################
|
||||||
|
# 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 _SWITCH_UPGRADE event without a pending proposal"
|
||||||
|
)
|
||||||
|
_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(
|
||||||
|
f"not in a reusable state. self.states={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}
|
||||||
135
venv/Lib/site-packages/h11/_util.py
Normal file
135
venv/Lib/site-packages/h11/_util.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ProtocolError",
|
||||||
|
"LocalProtocolError",
|
||||||
|
"RemoteProtocolError",
|
||||||
|
"validate",
|
||||||
|
"bytesify",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolError(Exception):
|
||||||
|
"""Exception indicating a violation of the HTTP/1.1 protocol.
|
||||||
|
|
||||||
|
This as an abstract base class, with two concrete base classes:
|
||||||
|
:exc:`LocalProtocolError`, which indicates that you tried to do something
|
||||||
|
that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which
|
||||||
|
indicates that the remote peer tried to do something that HTTP/1.1 says is
|
||||||
|
illegal. See :ref:`error-handling` for details.
|
||||||
|
|
||||||
|
In addition to the normal :exc:`Exception` features, it has one attribute:
|
||||||
|
|
||||||
|
.. attribute:: error_status_hint
|
||||||
|
|
||||||
|
This gives a suggestion as to what status code a server might use if
|
||||||
|
this error occurred as part of a request.
|
||||||
|
|
||||||
|
For a :exc:`RemoteProtocolError`, this is useful as a suggestion for
|
||||||
|
how you might want to respond to a misbehaving peer, if you're
|
||||||
|
implementing a server.
|
||||||
|
|
||||||
|
For a :exc:`LocalProtocolError`, this can be taken as a suggestion for
|
||||||
|
how your peer might have responded to *you* if h11 had allowed you to
|
||||||
|
continue.
|
||||||
|
|
||||||
|
The default is 400 Bad Request, a generic catch-all for protocol
|
||||||
|
violations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, msg: str, error_status_hint: int = 400) -> None:
|
||||||
|
if type(self) is ProtocolError:
|
||||||
|
raise TypeError("tried to directly instantiate ProtocolError")
|
||||||
|
Exception.__init__(self, msg)
|
||||||
|
self.error_status_hint = error_status_hint
|
||||||
|
|
||||||
|
|
||||||
|
# Strategy: there are a number of public APIs where a LocalProtocolError can
|
||||||
|
# be raised (send(), all the different event constructors, ...), and only one
|
||||||
|
# public API where RemoteProtocolError can be raised
|
||||||
|
# (receive_data()). Therefore we always raise LocalProtocolError internally,
|
||||||
|
# and then receive_data will translate this into a RemoteProtocolError.
|
||||||
|
#
|
||||||
|
# Internally:
|
||||||
|
# LocalProtocolError is the generic "ProtocolError".
|
||||||
|
# Externally:
|
||||||
|
# LocalProtocolError is for local errors and RemoteProtocolError is for
|
||||||
|
# remote errors.
|
||||||
|
class LocalProtocolError(ProtocolError):
|
||||||
|
def _reraise_as_remote_protocol_error(self) -> NoReturn:
|
||||||
|
# After catching a LocalProtocolError, use this method to re-raise it
|
||||||
|
# as a RemoteProtocolError. This method must be called from inside an
|
||||||
|
# except: block.
|
||||||
|
#
|
||||||
|
# An easy way to get an equivalent RemoteProtocolError is just to
|
||||||
|
# modify 'self' in place.
|
||||||
|
self.__class__ = RemoteProtocolError # type: ignore
|
||||||
|
# But the re-raising is somewhat non-trivial -- you might think that
|
||||||
|
# now that we've modified the in-flight exception object, that just
|
||||||
|
# doing 'raise' to re-raise it would be enough. But it turns out that
|
||||||
|
# this doesn't work, because Python tracks the exception type
|
||||||
|
# (exc_info[0]) separately from the exception object (exc_info[1]),
|
||||||
|
# and we only modified the latter. So we really do need to re-raise
|
||||||
|
# the new type explicitly.
|
||||||
|
# On py3, the traceback is part of the exception object, so our
|
||||||
|
# in-place modification preserved it and we can just re-raise:
|
||||||
|
raise self
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteProtocolError(ProtocolError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any
|
||||||
|
) -> Dict[str, bytes]:
|
||||||
|
match = regex.fullmatch(data)
|
||||||
|
if not match:
|
||||||
|
if format_args:
|
||||||
|
msg = msg.format(*format_args)
|
||||||
|
raise LocalProtocolError(msg)
|
||||||
|
return match.groupdict()
|
||||||
|
|
||||||
|
|
||||||
|
# Sentinel values
|
||||||
|
#
|
||||||
|
# - Inherit identity-based comparison and hashing from object
|
||||||
|
# - Have a nice repr
|
||||||
|
# - Have a *bonus property*: type(sentinel) is sentinel
|
||||||
|
#
|
||||||
|
# The bonus property is useful if you want to take the return value from
|
||||||
|
# next_event() and do some sort of dispatch based on type(event).
|
||||||
|
|
||||||
|
_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel")
|
||||||
|
|
||||||
|
|
||||||
|
class Sentinel(type):
|
||||||
|
def __new__(
|
||||||
|
cls: Type[_T_Sentinel],
|
||||||
|
name: str,
|
||||||
|
bases: Tuple[type, ...],
|
||||||
|
namespace: Dict[str, Any],
|
||||||
|
**kwds: Any
|
||||||
|
) -> _T_Sentinel:
|
||||||
|
assert bases == (Sentinel,)
|
||||||
|
v = super().__new__(cls, name, bases, namespace, **kwds)
|
||||||
|
v.__class__ = v # type: ignore
|
||||||
|
return v
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__name__
|
||||||
|
|
||||||
|
|
||||||
|
# Used for methods, request targets, HTTP versions, header names, and header
|
||||||
|
# values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always
|
||||||
|
# returns bytes.
|
||||||
|
def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes:
|
||||||
|
# Fast-path:
|
||||||
|
if type(s) is bytes:
|
||||||
|
return s
|
||||||
|
if isinstance(s, str):
|
||||||
|
s = s.encode("ascii")
|
||||||
|
if isinstance(s, int):
|
||||||
|
raise TypeError("expected bytes-like object, not int")
|
||||||
|
return bytes(s)
|
||||||
16
venv/Lib/site-packages/h11/_version.py
Normal file
16
venv/Lib/site-packages/h11/_version.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# This file must be kept very simple, because it is consumed from several
|
||||||
|
# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc.
|
||||||
|
|
||||||
|
# We use a simple scheme:
|
||||||
|
# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev
|
||||||
|
# where the +dev versions are never released into the wild, they're just what
|
||||||
|
# we stick into the VCS in between releases.
|
||||||
|
#
|
||||||
|
# This is compatible with PEP 440:
|
||||||
|
# http://legacy.python.org/dev/peps/pep-0440/
|
||||||
|
# via the use of the "local suffix" "+dev", which is disallowed on index
|
||||||
|
# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we
|
||||||
|
# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before*
|
||||||
|
# 1.0.0.)
|
||||||
|
|
||||||
|
__version__ = "0.16.0"
|
||||||
Reference in New Issue
Block a user