|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
assert False |
|
|
|
def send_data(self, data: bytes, write: Writer) -> None: |
|
pass |
|
|
|
def send_eom(self, headers: Headers, write: Writer) -> None: |
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 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") |
|
|
|
|
|
|
|
|
|
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, |
|
}, |
|
} |
|
|