Spaces:
Running
Running
File size: 5,081 Bytes
b72ab63 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# Code to read HTTP data
#
# Strategy: each writer takes an event + a write-some-bytes function, which is
# calls.
#
# WRITERS is a dict describing how to pick a reader. It maps states to either:
# - a writer
# - or, for body writers, a dict of framin-dependent writer factories
from typing import Any, Callable, Dict, List, Tuple, Type, Union
from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response
from ._headers import Headers
from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER
from ._util import LocalProtocolError, Sentinel
__all__ = ["WRITERS"]
Writer = Callable[[bytes], Any]
def write_headers(headers: Headers, write: Writer) -> None:
# "Since the Host field-value is critical information for handling a
# request, a user agent SHOULD generate Host as the first header field
# following the request-line." - RFC 7230
raw_items = headers._full_items
for raw_name, name, value in raw_items:
if name == b"host":
write(b"%s: %s\r\n" % (raw_name, value))
for raw_name, name, value in raw_items:
if name != b"host":
write(b"%s: %s\r\n" % (raw_name, value))
write(b"\r\n")
def write_request(request: Request, write: Writer) -> None:
if request.http_version != b"1.1":
raise LocalProtocolError("I only send HTTP/1.1")
write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target))
write_headers(request.headers, write)
# Shared between InformationalResponse and Response
def write_any_response(
response: Union[InformationalResponse, Response], write: Writer
) -> None:
if response.http_version != b"1.1":
raise LocalProtocolError("I only send HTTP/1.1")
status_bytes = str(response.status_code).encode("ascii")
# We don't bother sending ascii status messages like "OK"; they're
# optional and ignored by the protocol. (But the space after the numeric
# status code is mandatory.)
#
# XX FIXME: could at least make an effort to pull out the status message
# from stdlib's http.HTTPStatus table. Or maybe just steal their enums
# (either by import or copy/paste). We already accept them as status codes
# since they're of type IntEnum < int.
write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason))
write_headers(response.headers, write)
class BodyWriter:
def __call__(self, event: Event, write: Writer) -> None:
if type(event) is Data:
self.send_data(event.data, write)
elif type(event) is EndOfMessage:
self.send_eom(event.headers, write)
else: # pragma: no cover
assert False
def send_data(self, data: bytes, write: Writer) -> None:
pass
def send_eom(self, headers: Headers, write: Writer) -> None:
pass
#
# These are all careful not to do anything to 'data' except call len(data) and
# write(data). This allows us to transparently pass-through funny objects,
# like placeholder objects referring to files on disk that will be sent via
# sendfile(2).
#
class ContentLengthWriter(BodyWriter):
def __init__(self, length: int) -> None:
self._length = length
def send_data(self, data: bytes, write: Writer) -> None:
self._length -= len(data)
if self._length < 0:
raise LocalProtocolError("Too much data for declared Content-Length")
write(data)
def send_eom(self, headers: Headers, write: Writer) -> None:
if self._length != 0:
raise LocalProtocolError("Too little data for declared Content-Length")
if headers:
raise LocalProtocolError("Content-Length and trailers don't mix")
class ChunkedWriter(BodyWriter):
def send_data(self, data: bytes, write: Writer) -> None:
# if we encoded 0-length data in the naive way, it would look like an
# end-of-message.
if not data:
return
write(b"%x\r\n" % len(data))
write(data)
write(b"\r\n")
def send_eom(self, headers: Headers, write: Writer) -> None:
write(b"0\r\n")
write_headers(headers, write)
class Http10Writer(BodyWriter):
def send_data(self, data: bytes, write: Writer) -> None:
write(data)
def send_eom(self, headers: Headers, write: Writer) -> None:
if headers:
raise LocalProtocolError("can't send trailers to HTTP/1.0 client")
# no need to close the socket ourselves, that will be taken care of by
# Connection: close machinery
WritersType = Dict[
Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]],
Union[
Dict[str, Type[BodyWriter]],
Callable[[Union[InformationalResponse, Response], Writer], None],
Callable[[Request, Writer], None],
],
]
WRITERS: WritersType = {
(CLIENT, IDLE): write_request,
(SERVER, IDLE): write_any_response,
(SERVER, SEND_RESPONSE): write_any_response,
SEND_BODY: {
"chunked": ChunkedWriter,
"content-length": ContentLengthWriter,
"http/1.0": Http10Writer,
},
}
|