_writers.py 5.0 KB

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