| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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, |
| }, |
| } |
|
|