http.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import asyncio
  2. import re
  3. from typing import Tuple
  4. from ..datastructures import Headers
  5. from ..exceptions import SecurityError
  6. __all__ = ["read_request", "read_response"]
  7. MAX_HEADERS = 256
  8. MAX_LINE = 4110
  9. def d(value: bytes) -> str:
  10. """
  11. Decode a bytestring for interpolating into an error message.
  12. """
  13. return value.decode(errors="backslashreplace")
  14. # See https://tools.ietf.org/html/rfc7230#appendix-B.
  15. # Regex for validating header names.
  16. _token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
  17. # Regex for validating header values.
  18. # We don't attempt to support obsolete line folding.
  19. # Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff).
  20. # The ABNF is complicated because it attempts to express that optional
  21. # whitespace is ignored. We strip whitespace and don't revalidate that.
  22. # See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
  23. _value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
  24. async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]:
  25. """
  26. Read an HTTP/1.1 GET request and return ``(path, headers)``.
  27. ``path`` isn't URL-decoded or validated in any way.
  28. ``path`` and ``headers`` are expected to contain only ASCII characters.
  29. Other characters are represented with surrogate escapes.
  30. :func:`read_request` doesn't attempt to read the request body because
  31. WebSocket handshake requests don't have one. If the request contains a
  32. body, it may be read from ``stream`` after this coroutine returns.
  33. :param stream: input to read the request from
  34. :raises EOFError: if the connection is closed without a full HTTP request
  35. :raises SecurityError: if the request exceeds a security limit
  36. :raises ValueError: if the request isn't well formatted
  37. """
  38. # https://tools.ietf.org/html/rfc7230#section-3.1.1
  39. # Parsing is simple because fixed values are expected for method and
  40. # version and because path isn't checked. Since WebSocket software tends
  41. # to implement HTTP/1.1 strictly, there's little need for lenient parsing.
  42. try:
  43. request_line = await read_line(stream)
  44. except EOFError as exc:
  45. raise EOFError("connection closed while reading HTTP request line") from exc
  46. try:
  47. method, raw_path, version = request_line.split(b" ", 2)
  48. except ValueError: # not enough values to unpack (expected 3, got 1-2)
  49. raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None
  50. if method != b"GET":
  51. raise ValueError(f"unsupported HTTP method: {d(method)}")
  52. if version != b"HTTP/1.1":
  53. raise ValueError(f"unsupported HTTP version: {d(version)}")
  54. path = raw_path.decode("ascii", "surrogateescape")
  55. headers = await read_headers(stream)
  56. return path, headers
  57. async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers]:
  58. """
  59. Read an HTTP/1.1 response and return ``(status_code, reason, headers)``.
  60. ``reason`` and ``headers`` are expected to contain only ASCII characters.
  61. Other characters are represented with surrogate escapes.
  62. :func:`read_request` doesn't attempt to read the response body because
  63. WebSocket handshake responses don't have one. If the response contains a
  64. body, it may be read from ``stream`` after this coroutine returns.
  65. :param stream: input to read the response from
  66. :raises EOFError: if the connection is closed without a full HTTP response
  67. :raises SecurityError: if the response exceeds a security limit
  68. :raises ValueError: if the response isn't well formatted
  69. """
  70. # https://tools.ietf.org/html/rfc7230#section-3.1.2
  71. # As in read_request, parsing is simple because a fixed value is expected
  72. # for version, status_code is a 3-digit number, and reason can be ignored.
  73. try:
  74. status_line = await read_line(stream)
  75. except EOFError as exc:
  76. raise EOFError("connection closed while reading HTTP status line") from exc
  77. try:
  78. version, raw_status_code, raw_reason = status_line.split(b" ", 2)
  79. except ValueError: # not enough values to unpack (expected 3, got 1-2)
  80. raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None
  81. if version != b"HTTP/1.1":
  82. raise ValueError(f"unsupported HTTP version: {d(version)}")
  83. try:
  84. status_code = int(raw_status_code)
  85. except ValueError: # invalid literal for int() with base 10
  86. raise ValueError(f"invalid HTTP status code: {d(raw_status_code)}") from None
  87. if not 100 <= status_code < 1000:
  88. raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}")
  89. if not _value_re.fullmatch(raw_reason):
  90. raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}")
  91. reason = raw_reason.decode()
  92. headers = await read_headers(stream)
  93. return status_code, reason, headers
  94. async def read_headers(stream: asyncio.StreamReader) -> Headers:
  95. """
  96. Read HTTP headers from ``stream``.
  97. Non-ASCII characters are represented with surrogate escapes.
  98. """
  99. # https://tools.ietf.org/html/rfc7230#section-3.2
  100. # We don't attempt to support obsolete line folding.
  101. headers = Headers()
  102. for _ in range(MAX_HEADERS + 1):
  103. try:
  104. line = await read_line(stream)
  105. except EOFError as exc:
  106. raise EOFError("connection closed while reading HTTP headers") from exc
  107. if line == b"":
  108. break
  109. try:
  110. raw_name, raw_value = line.split(b":", 1)
  111. except ValueError: # not enough values to unpack (expected 2, got 1)
  112. raise ValueError(f"invalid HTTP header line: {d(line)}") from None
  113. if not _token_re.fullmatch(raw_name):
  114. raise ValueError(f"invalid HTTP header name: {d(raw_name)}")
  115. raw_value = raw_value.strip(b" \t")
  116. if not _value_re.fullmatch(raw_value):
  117. raise ValueError(f"invalid HTTP header value: {d(raw_value)}")
  118. name = raw_name.decode("ascii") # guaranteed to be ASCII at this point
  119. value = raw_value.decode("ascii", "surrogateescape")
  120. headers[name] = value
  121. else:
  122. raise SecurityError("too many HTTP headers")
  123. return headers
  124. async def read_line(stream: asyncio.StreamReader) -> bytes:
  125. """
  126. Read a single line from ``stream``.
  127. CRLF is stripped from the return value.
  128. """
  129. # Security: this is bounded by the StreamReader's limit (default = 32 KiB).
  130. line = await stream.readline()
  131. # Security: this guarantees header values are small (hard-coded = 4 KiB)
  132. if len(line) > MAX_LINE:
  133. raise SecurityError("line too long")
  134. # Not mandatory but safe - https://tools.ietf.org/html/rfc7230#section-3.5
  135. if not line.endswith(b"\r\n"):
  136. raise EOFError("line without CRLF")
  137. return line[:-2]