http_parser.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125
  1. import abc
  2. import asyncio
  3. import re
  4. import string
  5. from contextlib import suppress
  6. from enum import IntEnum
  7. from typing import (
  8. Any,
  9. ClassVar,
  10. Final,
  11. Generic,
  12. List,
  13. Literal,
  14. NamedTuple,
  15. Optional,
  16. Pattern,
  17. Set,
  18. Tuple,
  19. Type,
  20. TypeVar,
  21. Union,
  22. )
  23. from multidict import CIMultiDict, CIMultiDictProxy, istr
  24. from yarl import URL
  25. from . import hdrs
  26. from .base_protocol import BaseProtocol
  27. from .compression_utils import (
  28. DEFAULT_MAX_DECOMPRESS_SIZE,
  29. HAS_BROTLI,
  30. HAS_ZSTD,
  31. BrotliDecompressor,
  32. ZLibDecompressor,
  33. ZSTDDecompressor,
  34. )
  35. from .helpers import (
  36. _EXC_SENTINEL,
  37. DEBUG,
  38. EMPTY_BODY_METHODS,
  39. EMPTY_BODY_STATUS_CODES,
  40. NO_EXTENSIONS,
  41. BaseTimerContext,
  42. set_exception,
  43. )
  44. from .http_exceptions import (
  45. BadHttpMessage,
  46. BadHttpMethod,
  47. BadStatusLine,
  48. ContentEncodingError,
  49. ContentLengthError,
  50. DecompressSizeError,
  51. InvalidHeader,
  52. InvalidURLError,
  53. LineTooLong,
  54. TransferEncodingError,
  55. )
  56. from .http_writer import HttpVersion, HttpVersion10
  57. from .streams import EMPTY_PAYLOAD, StreamReader
  58. from .typedefs import RawHeaders
  59. __all__ = (
  60. "HeadersParser",
  61. "HttpParser",
  62. "HttpRequestParser",
  63. "HttpResponseParser",
  64. "RawRequestMessage",
  65. "RawResponseMessage",
  66. )
  67. _SEP = Literal[b"\r\n", b"\n"]
  68. ASCIISET: Final[Set[str]] = set(string.printable)
  69. # See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview
  70. # and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens
  71. #
  72. # method = token
  73. # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
  74. # "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
  75. # token = 1*tchar
  76. _TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~")
  77. TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+")
  78. # https://www.rfc-editor.org/rfc/rfc9110#section-5.5-5
  79. _FIELD_VALUE_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile(
  80. r"[\x00-\x08\x0a-\x1f\x7f]"
  81. )
  82. VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII)
  83. DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII)
  84. HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+")
  85. # RFC 9110 singleton headers — duplicates are rejected in strict mode.
  86. # In lax mode (response parser default), the check is skipped entirely
  87. # since real-world servers (e.g. Google APIs, Werkzeug) commonly send
  88. # duplicate headers like Content-Type or Server.
  89. # Lowercased for case-insensitive matching against wire names.
  90. SINGLETON_HEADERS: Final[frozenset[str]] = frozenset(
  91. {
  92. "content-length",
  93. "content-location",
  94. "content-range",
  95. "content-type",
  96. "etag",
  97. "host",
  98. "max-forwards",
  99. "server",
  100. "transfer-encoding",
  101. "user-agent",
  102. }
  103. )
  104. class RawRequestMessage(NamedTuple):
  105. method: str
  106. path: str
  107. version: HttpVersion
  108. headers: "CIMultiDictProxy[str]"
  109. raw_headers: RawHeaders
  110. should_close: bool
  111. compression: Optional[str]
  112. upgrade: bool
  113. chunked: bool
  114. url: URL
  115. class RawResponseMessage(NamedTuple):
  116. version: HttpVersion
  117. code: int
  118. reason: str
  119. headers: CIMultiDictProxy[str]
  120. raw_headers: RawHeaders
  121. should_close: bool
  122. compression: Optional[str]
  123. upgrade: bool
  124. chunked: bool
  125. _MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage)
  126. class ParseState(IntEnum):
  127. PARSE_NONE = 0
  128. PARSE_LENGTH = 1
  129. PARSE_CHUNKED = 2
  130. PARSE_UNTIL_EOF = 3
  131. class ChunkState(IntEnum):
  132. PARSE_CHUNKED_SIZE = 0
  133. PARSE_CHUNKED_CHUNK = 1
  134. PARSE_CHUNKED_CHUNK_EOF = 2
  135. PARSE_MAYBE_TRAILERS = 3
  136. PARSE_TRAILERS = 4
  137. class HeadersParser:
  138. def __init__(
  139. self,
  140. max_line_size: int = 8190,
  141. max_headers: int = 32768,
  142. max_field_size: int = 8190,
  143. lax: bool = False,
  144. ) -> None:
  145. self.max_line_size = max_line_size
  146. self.max_headers = max_headers
  147. self.max_field_size = max_field_size
  148. self._lax = lax
  149. def parse_headers(
  150. self, lines: List[bytes]
  151. ) -> Tuple["CIMultiDictProxy[str]", RawHeaders]:
  152. headers: CIMultiDict[str] = CIMultiDict()
  153. # note: "raw" does not mean inclusion of OWS before/after the field value
  154. raw_headers = []
  155. lines_idx = 0
  156. line = lines[lines_idx]
  157. line_count = len(lines)
  158. while line:
  159. # Parse initial header name : value pair.
  160. try:
  161. bname, bvalue = line.split(b":", 1)
  162. except ValueError:
  163. raise InvalidHeader(line) from None
  164. if len(bname) == 0:
  165. raise InvalidHeader(bname)
  166. # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2
  167. if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"}
  168. raise InvalidHeader(line)
  169. bvalue = bvalue.lstrip(b" \t")
  170. name = bname.decode("utf-8", "surrogateescape")
  171. if not TOKENRE.fullmatch(name):
  172. raise InvalidHeader(bname)
  173. # next line
  174. lines_idx += 1
  175. line = lines[lines_idx]
  176. # consume continuation lines
  177. continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')
  178. # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
  179. if continuation:
  180. header_length = len(bvalue)
  181. bvalue_lst = [bvalue]
  182. while continuation:
  183. header_length += len(line)
  184. if header_length > self.max_field_size:
  185. header_line = bname + b": " + b"".join(bvalue_lst)
  186. raise LineTooLong(
  187. header_line[:100] + b"...", self.max_field_size
  188. )
  189. bvalue_lst.append(line)
  190. # next line
  191. lines_idx += 1
  192. if lines_idx < line_count:
  193. line = lines[lines_idx]
  194. if line:
  195. continuation = line[0] in (32, 9) # (' ', '\t')
  196. else:
  197. line = b""
  198. break
  199. bvalue = b"".join(bvalue_lst)
  200. bvalue = bvalue.strip(b" \t")
  201. value = bvalue.decode("utf-8", "surrogateescape")
  202. # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5
  203. if self._lax:
  204. if "\n" in value or "\r" in value or "\x00" in value:
  205. raise InvalidHeader(bvalue)
  206. elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value):
  207. raise InvalidHeader(bvalue)
  208. if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS:
  209. raise BadHttpMessage(f"Duplicate '{name}' header found.")
  210. headers.add(name, value)
  211. raw_headers.append((bname, bvalue))
  212. return (CIMultiDictProxy(headers), tuple(raw_headers))
  213. def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool:
  214. """Check if the upgrade header is supported."""
  215. u = headers.get(hdrs.UPGRADE, "")
  216. # .lower() can transform non-ascii characters.
  217. return u.isascii() and u.lower() in {"tcp", "websocket"}
  218. class HttpParser(abc.ABC, Generic[_MsgT]):
  219. lax: ClassVar[bool] = False
  220. def __init__(
  221. self,
  222. protocol: Optional[BaseProtocol] = None,
  223. loop: Optional[asyncio.AbstractEventLoop] = None,
  224. limit: int = 2**16,
  225. max_line_size: int = 8190,
  226. max_headers: int = 128,
  227. max_field_size: int = 8190,
  228. timer: Optional[BaseTimerContext] = None,
  229. code: Optional[int] = None,
  230. method: Optional[str] = None,
  231. payload_exception: Optional[Type[BaseException]] = None,
  232. response_with_body: bool = True,
  233. read_until_eof: bool = False,
  234. auto_decompress: bool = True,
  235. ) -> None:
  236. self.protocol = protocol
  237. self.loop = loop
  238. self.max_line_size = max_line_size
  239. self.max_headers = max_headers
  240. self.max_field_size = max_field_size
  241. self.max_headers = max_headers
  242. self.timer = timer
  243. self.code = code
  244. self.method = method
  245. self.payload_exception = payload_exception
  246. self.response_with_body = response_with_body
  247. self.read_until_eof = read_until_eof
  248. self._lines: List[bytes] = []
  249. self._tail = b""
  250. self._upgraded = False
  251. self._payload = None
  252. self._payload_parser: Optional[HttpPayloadParser] = None
  253. self._auto_decompress = auto_decompress
  254. self._limit = limit
  255. self._headers_parser = HeadersParser(
  256. max_line_size, max_headers, max_field_size, self.lax
  257. )
  258. @abc.abstractmethod
  259. def parse_message(self, lines: List[bytes]) -> _MsgT: ...
  260. @abc.abstractmethod
  261. def _is_chunked_te(self, te: str) -> bool: ...
  262. def feed_eof(self) -> Optional[_MsgT]:
  263. if self._payload_parser is not None:
  264. self._payload_parser.feed_eof()
  265. self._payload_parser = None
  266. else:
  267. # try to extract partial message
  268. if self._tail:
  269. self._lines.append(self._tail)
  270. if self._lines:
  271. if self._lines[-1] != "\r\n":
  272. self._lines.append(b"")
  273. with suppress(Exception):
  274. return self.parse_message(self._lines)
  275. return None
  276. def feed_data(
  277. self,
  278. data: bytes,
  279. SEP: _SEP = b"\r\n",
  280. EMPTY: bytes = b"",
  281. CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH,
  282. METH_CONNECT: str = hdrs.METH_CONNECT,
  283. SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1,
  284. ) -> Tuple[List[Tuple[_MsgT, StreamReader]], bool, bytes]:
  285. messages = []
  286. if self._tail:
  287. data, self._tail = self._tail + data, b""
  288. data_len = len(data)
  289. start_pos = 0
  290. loop = self.loop
  291. max_line_length = self.max_line_size
  292. should_close = False
  293. while start_pos < data_len:
  294. # read HTTP message (request/response line + headers), \r\n\r\n
  295. # and split by lines
  296. if self._payload_parser is None and not self._upgraded:
  297. pos = data.find(SEP, start_pos)
  298. # consume \r\n
  299. if pos == start_pos and not self._lines:
  300. start_pos = pos + len(SEP)
  301. continue
  302. if pos >= start_pos:
  303. if should_close:
  304. raise BadHttpMessage("Data after `Connection: close`")
  305. # line found
  306. line = data[start_pos:pos]
  307. if SEP == b"\n": # For lax response parsing
  308. line = line.rstrip(b"\r")
  309. if len(line) > max_line_length:
  310. raise LineTooLong(line[:100] + b"...", max_line_length)
  311. self._lines.append(line)
  312. # After processing the status/request line, everything is a header.
  313. max_line_length = self.max_field_size
  314. if len(self._lines) > self.max_headers:
  315. raise BadHttpMessage("Too many headers received")
  316. start_pos = pos + len(SEP)
  317. # \r\n\r\n found
  318. if self._lines[-1] == EMPTY:
  319. max_trailers = self.max_headers - len(self._lines)
  320. try:
  321. msg: _MsgT = self.parse_message(self._lines)
  322. finally:
  323. self._lines.clear()
  324. def get_content_length() -> Optional[int]:
  325. # payload length
  326. length_hdr = msg.headers.get(CONTENT_LENGTH)
  327. if length_hdr is None:
  328. return None
  329. # Shouldn't allow +/- or other number formats.
  330. # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2
  331. # msg.headers is already stripped of leading/trailing wsp
  332. if not DIGITS.fullmatch(length_hdr):
  333. raise InvalidHeader(CONTENT_LENGTH)
  334. return int(length_hdr)
  335. length = get_content_length()
  336. # do not support old websocket spec
  337. if SEC_WEBSOCKET_KEY1 in msg.headers:
  338. raise InvalidHeader(SEC_WEBSOCKET_KEY1)
  339. self._upgraded = msg.upgrade and _is_supported_upgrade(
  340. msg.headers
  341. )
  342. method = getattr(msg, "method", self.method)
  343. # code is only present on responses
  344. code = getattr(msg, "code", 0)
  345. assert self.protocol is not None
  346. # calculate payload
  347. empty_body = code in EMPTY_BODY_STATUS_CODES or bool(
  348. method and method in EMPTY_BODY_METHODS
  349. )
  350. if not empty_body and (
  351. ((length is not None and length > 0) or msg.chunked)
  352. and not self._upgraded
  353. ):
  354. payload = StreamReader(
  355. self.protocol,
  356. timer=self.timer,
  357. loop=loop,
  358. limit=self._limit,
  359. )
  360. payload_parser = HttpPayloadParser(
  361. payload,
  362. length=length,
  363. chunked=msg.chunked,
  364. method=method,
  365. compression=msg.compression,
  366. code=self.code,
  367. response_with_body=self.response_with_body,
  368. auto_decompress=self._auto_decompress,
  369. lax=self.lax,
  370. headers_parser=self._headers_parser,
  371. max_line_size=self.max_line_size,
  372. max_field_size=self.max_field_size,
  373. max_trailers=max_trailers,
  374. )
  375. if not payload_parser.done:
  376. self._payload_parser = payload_parser
  377. elif method == METH_CONNECT:
  378. assert isinstance(msg, RawRequestMessage)
  379. payload = StreamReader(
  380. self.protocol,
  381. timer=self.timer,
  382. loop=loop,
  383. limit=self._limit,
  384. )
  385. self._upgraded = True
  386. self._payload_parser = HttpPayloadParser(
  387. payload,
  388. method=msg.method,
  389. compression=msg.compression,
  390. auto_decompress=self._auto_decompress,
  391. lax=self.lax,
  392. headers_parser=self._headers_parser,
  393. max_line_size=self.max_line_size,
  394. max_field_size=self.max_field_size,
  395. max_trailers=max_trailers,
  396. )
  397. elif not empty_body and length is None and self.read_until_eof:
  398. payload = StreamReader(
  399. self.protocol,
  400. timer=self.timer,
  401. loop=loop,
  402. limit=self._limit,
  403. )
  404. payload_parser = HttpPayloadParser(
  405. payload,
  406. length=length,
  407. chunked=msg.chunked,
  408. method=method,
  409. compression=msg.compression,
  410. code=self.code,
  411. response_with_body=self.response_with_body,
  412. auto_decompress=self._auto_decompress,
  413. lax=self.lax,
  414. headers_parser=self._headers_parser,
  415. max_line_size=self.max_line_size,
  416. max_field_size=self.max_field_size,
  417. max_trailers=max_trailers,
  418. )
  419. if not payload_parser.done:
  420. self._payload_parser = payload_parser
  421. else:
  422. payload = EMPTY_PAYLOAD
  423. messages.append((msg, payload))
  424. should_close = msg.should_close
  425. else:
  426. self._tail = data[start_pos:]
  427. if len(self._tail) > self.max_line_size:
  428. raise LineTooLong(self._tail[:100] + b"...", self.max_line_size)
  429. data = EMPTY
  430. break
  431. # no parser, just store
  432. elif self._payload_parser is None and self._upgraded:
  433. assert not self._lines
  434. break
  435. # feed payload
  436. elif data and start_pos < data_len:
  437. assert not self._lines
  438. assert self._payload_parser is not None
  439. try:
  440. eof, data = self._payload_parser.feed_data(data[start_pos:], SEP)
  441. except BaseException as underlying_exc:
  442. reraised_exc = underlying_exc
  443. if self.payload_exception is not None:
  444. reraised_exc = self.payload_exception(str(underlying_exc))
  445. set_exception(
  446. self._payload_parser.payload,
  447. reraised_exc,
  448. underlying_exc,
  449. )
  450. eof = True
  451. data = b""
  452. if isinstance(
  453. underlying_exc, (InvalidHeader, TransferEncodingError)
  454. ):
  455. raise
  456. if eof:
  457. start_pos = 0
  458. data_len = len(data)
  459. self._payload_parser = None
  460. continue
  461. else:
  462. break
  463. if data and start_pos < data_len:
  464. data = data[start_pos:]
  465. else:
  466. data = EMPTY
  467. return messages, self._upgraded, data
  468. def parse_headers(
  469. self, lines: List[bytes]
  470. ) -> Tuple[
  471. "CIMultiDictProxy[str]", RawHeaders, Optional[bool], Optional[str], bool, bool
  472. ]:
  473. """Parses RFC 5322 headers from a stream.
  474. Line continuations are supported. Returns list of header name
  475. and value pairs. Header name is in upper case.
  476. """
  477. headers, raw_headers = self._headers_parser.parse_headers(lines)
  478. close_conn = None
  479. encoding = None
  480. upgrade = False
  481. chunked = False
  482. # keep-alive and protocol switching
  483. # RFC 9110 section 7.6.1 defines Connection as a comma-separated list.
  484. conn_values = headers.getall(hdrs.CONNECTION, ())
  485. if conn_values:
  486. conn_tokens = {
  487. token.lower()
  488. for conn_value in conn_values
  489. for token in (part.strip(" \t") for part in conn_value.split(","))
  490. if token and token.isascii()
  491. }
  492. if "close" in conn_tokens:
  493. close_conn = True
  494. elif "keep-alive" in conn_tokens:
  495. close_conn = False
  496. # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
  497. if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE):
  498. upgrade = True
  499. # encoding
  500. enc = headers.get(hdrs.CONTENT_ENCODING, "")
  501. if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
  502. encoding = enc
  503. # chunking
  504. te = headers.get(hdrs.TRANSFER_ENCODING)
  505. if te is not None:
  506. if self._is_chunked_te(te):
  507. chunked = True
  508. if hdrs.CONTENT_LENGTH in headers:
  509. raise BadHttpMessage(
  510. "Transfer-Encoding can't be present with Content-Length",
  511. )
  512. return (headers, raw_headers, close_conn, encoding, upgrade, chunked)
  513. def set_upgraded(self, val: bool) -> None:
  514. """Set connection upgraded (to websocket) mode.
  515. :param bool val: new state.
  516. """
  517. self._upgraded = val
  518. class HttpRequestParser(HttpParser[RawRequestMessage]):
  519. """Read request status line.
  520. Exception .http_exceptions.BadStatusLine
  521. could be raised in case of any errors in status line.
  522. Returns RawRequestMessage.
  523. """
  524. def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
  525. # request line
  526. line = lines[0].decode("utf-8", "surrogateescape")
  527. try:
  528. method, path, version = line.split(" ", maxsplit=2)
  529. except ValueError:
  530. raise BadHttpMethod(line) from None
  531. # method
  532. if not TOKENRE.fullmatch(method):
  533. raise BadHttpMethod(method)
  534. # version
  535. match = VERSRE.fullmatch(version)
  536. if match is None:
  537. raise BadStatusLine(line)
  538. version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
  539. if method == "CONNECT":
  540. # authority-form,
  541. # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
  542. url = URL.build(authority=path, encoded=True)
  543. elif path.startswith("/"):
  544. # origin-form,
  545. # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
  546. path_part, _hash_separator, url_fragment = path.partition("#")
  547. path_part, _question_mark_separator, qs_part = path_part.partition("?")
  548. # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based
  549. # NOTE: parser does, otherwise it results into the same
  550. # NOTE: HTTP Request-Line input producing different
  551. # NOTE: `yarl.URL()` objects
  552. url = URL.build(
  553. path=path_part,
  554. query_string=qs_part,
  555. fragment=url_fragment,
  556. encoded=True,
  557. )
  558. elif path == "*" and method == "OPTIONS":
  559. # asterisk-form,
  560. url = URL(path, encoded=True)
  561. else:
  562. # absolute-form for proxy maybe,
  563. # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
  564. url = URL(path, encoded=True)
  565. if url.scheme == "":
  566. # not absolute-form
  567. raise InvalidURLError(
  568. path.encode(errors="surrogateescape").decode("latin1")
  569. )
  570. # read headers
  571. (
  572. headers,
  573. raw_headers,
  574. close,
  575. compression,
  576. upgrade,
  577. chunked,
  578. ) = self.parse_headers(lines[1:])
  579. if close is None: # then the headers weren't set in the request
  580. if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
  581. close = True
  582. else: # HTTP 1.1 must ask to close.
  583. close = False
  584. return RawRequestMessage(
  585. method,
  586. path,
  587. version_o,
  588. headers,
  589. raw_headers,
  590. close,
  591. compression,
  592. upgrade,
  593. chunked,
  594. url,
  595. )
  596. def _is_chunked_te(self, te: str) -> bool:
  597. te = te.rsplit(",", maxsplit=1)[-1].strip(" \t")
  598. # .lower() transforms some non-ascii chars, so must check first.
  599. if te.isascii() and te.lower() == "chunked":
  600. return True
  601. # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
  602. raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
  603. class HttpResponseParser(HttpParser[RawResponseMessage]):
  604. """Read response status line and headers.
  605. BadStatusLine could be raised in case of any errors in status line.
  606. Returns RawResponseMessage.
  607. """
  608. # Lax mode should only be enabled on response parser.
  609. lax = not DEBUG
  610. def feed_data(
  611. self,
  612. data: bytes,
  613. SEP: Optional[_SEP] = None,
  614. *args: Any,
  615. **kwargs: Any,
  616. ) -> Tuple[List[Tuple[RawResponseMessage, StreamReader]], bool, bytes]:
  617. if SEP is None:
  618. SEP = b"\r\n" if DEBUG else b"\n"
  619. return super().feed_data(data, SEP, *args, **kwargs)
  620. def parse_message(self, lines: List[bytes]) -> RawResponseMessage:
  621. line = lines[0].decode("utf-8", "surrogateescape")
  622. try:
  623. version, status = line.split(maxsplit=1)
  624. except ValueError:
  625. raise BadStatusLine(line) from None
  626. try:
  627. status, reason = status.split(maxsplit=1)
  628. except ValueError:
  629. status = status.strip()
  630. reason = ""
  631. # version
  632. match = VERSRE.fullmatch(version)
  633. if match is None:
  634. raise BadStatusLine(line)
  635. version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
  636. # The status code is a three-digit ASCII number, no padding
  637. if len(status) != 3 or not DIGITS.fullmatch(status):
  638. raise BadStatusLine(line)
  639. status_i = int(status)
  640. # read headers
  641. (
  642. headers,
  643. raw_headers,
  644. close,
  645. compression,
  646. upgrade,
  647. chunked,
  648. ) = self.parse_headers(lines[1:])
  649. if close is None:
  650. if version_o <= HttpVersion10:
  651. close = True
  652. # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length
  653. elif 100 <= status_i < 200 or status_i in {204, 304}:
  654. close = False
  655. elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers:
  656. close = False
  657. else:
  658. # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8
  659. close = True
  660. return RawResponseMessage(
  661. version_o,
  662. status_i,
  663. reason.strip(),
  664. headers,
  665. raw_headers,
  666. close,
  667. compression,
  668. upgrade,
  669. chunked,
  670. )
  671. def _is_chunked_te(self, te: str) -> bool:
  672. # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2
  673. return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked"
  674. class HttpPayloadParser:
  675. def __init__(
  676. self,
  677. payload: StreamReader,
  678. length: Optional[int] = None,
  679. chunked: bool = False,
  680. compression: Optional[str] = None,
  681. code: Optional[int] = None,
  682. method: Optional[str] = None,
  683. response_with_body: bool = True,
  684. auto_decompress: bool = True,
  685. lax: bool = False,
  686. *,
  687. headers_parser: HeadersParser,
  688. max_line_size: int = 8190,
  689. max_field_size: int = 8190,
  690. max_trailers: int = 128,
  691. ) -> None:
  692. self._length = 0
  693. self._type = ParseState.PARSE_UNTIL_EOF
  694. self._chunk = ChunkState.PARSE_CHUNKED_SIZE
  695. self._chunk_size = 0
  696. self._chunk_tail = b""
  697. self._auto_decompress = auto_decompress
  698. self._lax = lax
  699. self._headers_parser = headers_parser
  700. self._max_line_size = max_line_size
  701. self._max_field_size = max_field_size
  702. self._max_trailers = max_trailers
  703. self._trailer_lines: list[bytes] = []
  704. self.done = False
  705. # payload decompression wrapper
  706. if response_with_body and compression and self._auto_decompress:
  707. real_payload: Union[StreamReader, DeflateBuffer] = DeflateBuffer(
  708. payload, compression
  709. )
  710. else:
  711. real_payload = payload
  712. # payload parser
  713. if not response_with_body:
  714. # don't parse payload if it's not expected to be received
  715. self._type = ParseState.PARSE_NONE
  716. real_payload.feed_eof()
  717. self.done = True
  718. elif chunked:
  719. self._type = ParseState.PARSE_CHUNKED
  720. elif length is not None:
  721. self._type = ParseState.PARSE_LENGTH
  722. self._length = length
  723. if self._length == 0:
  724. real_payload.feed_eof()
  725. self.done = True
  726. self.payload = real_payload
  727. def feed_eof(self) -> None:
  728. if self._type == ParseState.PARSE_UNTIL_EOF:
  729. self.payload.feed_eof()
  730. elif self._type == ParseState.PARSE_LENGTH:
  731. raise ContentLengthError(
  732. "Not enough data to satisfy content length header."
  733. )
  734. elif self._type == ParseState.PARSE_CHUNKED:
  735. raise TransferEncodingError(
  736. "Not enough data to satisfy transfer length header."
  737. )
  738. def feed_data(
  739. self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";"
  740. ) -> Tuple[bool, bytes]:
  741. # Read specified amount of bytes
  742. if self._type == ParseState.PARSE_LENGTH:
  743. required = self._length
  744. chunk_len = len(chunk)
  745. if required >= chunk_len:
  746. self._length = required - chunk_len
  747. self.payload.feed_data(chunk, chunk_len)
  748. if self._length == 0:
  749. self.payload.feed_eof()
  750. return True, b""
  751. else:
  752. self._length = 0
  753. self.payload.feed_data(chunk[:required], required)
  754. self.payload.feed_eof()
  755. return True, chunk[required:]
  756. # Chunked transfer encoding parser
  757. elif self._type == ParseState.PARSE_CHUNKED:
  758. if self._chunk_tail:
  759. # We should never have a tail if we're inside the payload body.
  760. assert self._chunk != ChunkState.PARSE_CHUNKED_CHUNK
  761. # We should check the length is sane.
  762. max_line_length = self._max_line_size
  763. if self._chunk == ChunkState.PARSE_TRAILERS:
  764. max_line_length = self._max_field_size
  765. if len(self._chunk_tail) > max_line_length:
  766. raise LineTooLong(self._chunk_tail[:100] + b"...", max_line_length)
  767. chunk = self._chunk_tail + chunk
  768. self._chunk_tail = b""
  769. while chunk:
  770. # read next chunk size
  771. if self._chunk == ChunkState.PARSE_CHUNKED_SIZE:
  772. pos = chunk.find(SEP)
  773. if pos >= 0:
  774. i = chunk.find(CHUNK_EXT, 0, pos)
  775. if i >= 0:
  776. size_b = chunk[:i] # strip chunk-extensions
  777. # Verify no LF in the chunk-extension
  778. if b"\n" in (ext := chunk[i:pos]):
  779. exc = TransferEncodingError(
  780. f"Unexpected LF in chunk-extension: {ext!r}"
  781. )
  782. set_exception(self.payload, exc)
  783. raise exc
  784. else:
  785. size_b = chunk[:pos]
  786. if self._lax: # Allow whitespace in lax mode.
  787. size_b = size_b.strip()
  788. if not re.fullmatch(HEXDIGITS, size_b):
  789. exc = TransferEncodingError(
  790. chunk[:pos].decode("ascii", "surrogateescape")
  791. )
  792. set_exception(self.payload, exc)
  793. raise exc
  794. size = int(bytes(size_b), 16)
  795. chunk = chunk[pos + len(SEP) :]
  796. if size == 0: # eof marker
  797. self._chunk = ChunkState.PARSE_TRAILERS
  798. if self._lax and chunk.startswith(b"\r"):
  799. chunk = chunk[1:]
  800. else:
  801. self._chunk = ChunkState.PARSE_CHUNKED_CHUNK
  802. self._chunk_size = size
  803. self.payload.begin_http_chunk_receiving()
  804. else:
  805. self._chunk_tail = chunk
  806. return False, b""
  807. # read chunk and feed buffer
  808. if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK:
  809. required = self._chunk_size
  810. chunk_len = len(chunk)
  811. if required > chunk_len:
  812. self._chunk_size = required - chunk_len
  813. self.payload.feed_data(chunk, chunk_len)
  814. return False, b""
  815. else:
  816. self._chunk_size = 0
  817. self.payload.feed_data(chunk[:required], required)
  818. chunk = chunk[required:]
  819. self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
  820. self.payload.end_http_chunk_receiving()
  821. # toss the CRLF at the end of the chunk
  822. if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF:
  823. if self._lax and chunk.startswith(b"\r"):
  824. chunk = chunk[1:]
  825. if chunk[: len(SEP)] == SEP:
  826. chunk = chunk[len(SEP) :]
  827. self._chunk = ChunkState.PARSE_CHUNKED_SIZE
  828. elif len(chunk) >= len(SEP) or chunk != SEP[: len(chunk)]:
  829. exc = TransferEncodingError(
  830. "Chunk size mismatch: expected CRLF after chunk data"
  831. )
  832. set_exception(self.payload, exc)
  833. raise exc
  834. else:
  835. self._chunk_tail = chunk
  836. return False, b""
  837. if self._chunk == ChunkState.PARSE_TRAILERS:
  838. pos = chunk.find(SEP)
  839. if pos < 0: # No line found
  840. self._chunk_tail = chunk
  841. return False, b""
  842. line = chunk[:pos]
  843. chunk = chunk[pos + len(SEP) :]
  844. if SEP == b"\n": # For lax response parsing
  845. line = line.rstrip(b"\r")
  846. if len(line) > self._max_field_size:
  847. raise LineTooLong(line[:100] + b"...", self._max_field_size)
  848. self._trailer_lines.append(line)
  849. if len(self._trailer_lines) > self._max_trailers:
  850. raise BadHttpMessage("Too many trailers received")
  851. # \r\n\r\n found, end of stream
  852. if self._trailer_lines[-1] == b"":
  853. # Headers and trailers are defined the same way,
  854. # so we reuse the HeadersParser here.
  855. try:
  856. trailers, raw_trailers = self._headers_parser.parse_headers(
  857. self._trailer_lines
  858. )
  859. finally:
  860. self._trailer_lines.clear()
  861. self.payload.feed_eof()
  862. return True, chunk
  863. # Read all bytes until eof
  864. elif self._type == ParseState.PARSE_UNTIL_EOF:
  865. self.payload.feed_data(chunk, len(chunk))
  866. return False, b""
  867. class DeflateBuffer:
  868. """DeflateStream decompress stream and feed data into specified stream."""
  869. decompressor: Any
  870. def __init__(
  871. self,
  872. out: StreamReader,
  873. encoding: Optional[str],
  874. max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
  875. ) -> None:
  876. self.out = out
  877. self.size = 0
  878. out.total_compressed_bytes = self.size
  879. self.encoding = encoding
  880. self._started_decoding = False
  881. self.decompressor: Union[BrotliDecompressor, ZLibDecompressor, ZSTDDecompressor]
  882. if encoding == "br":
  883. if not HAS_BROTLI: # pragma: no cover
  884. raise ContentEncodingError(
  885. "Can not decode content-encoding: brotli (br). "
  886. "Please install `Brotli`"
  887. )
  888. self.decompressor = BrotliDecompressor()
  889. elif encoding == "zstd":
  890. if not HAS_ZSTD:
  891. raise ContentEncodingError(
  892. "Can not decode content-encoding: zstandard (zstd). "
  893. "Please install `backports.zstd`"
  894. )
  895. self.decompressor = ZSTDDecompressor()
  896. else:
  897. self.decompressor = ZLibDecompressor(encoding=encoding)
  898. self._max_decompress_size = max_decompress_size
  899. def set_exception(
  900. self,
  901. exc: BaseException,
  902. exc_cause: BaseException = _EXC_SENTINEL,
  903. ) -> None:
  904. set_exception(self.out, exc, exc_cause)
  905. def feed_data(self, chunk: bytes, size: int) -> None:
  906. if not size:
  907. return
  908. self.size += size
  909. self.out.total_compressed_bytes = self.size
  910. # RFC1950
  911. # bits 0..3 = CM = 0b1000 = 8 = "deflate"
  912. # bits 4..7 = CINFO = 1..7 = windows size.
  913. if (
  914. not self._started_decoding
  915. and self.encoding == "deflate"
  916. and chunk[0] & 0xF != 8
  917. ):
  918. # Change the decoder to decompress incorrectly compressed data
  919. # Actually we should issue a warning about non-RFC-compliant data.
  920. self.decompressor = ZLibDecompressor(
  921. encoding=self.encoding, suppress_deflate_header=True
  922. )
  923. try:
  924. # Decompress with limit + 1 so we can detect if output exceeds limit
  925. chunk = self.decompressor.decompress_sync(
  926. chunk, max_length=self._max_decompress_size + 1
  927. )
  928. except Exception:
  929. raise ContentEncodingError(
  930. "Can not decode content-encoding: %s" % self.encoding
  931. )
  932. self._started_decoding = True
  933. # Check if decompression limit was exceeded
  934. if len(chunk) > self._max_decompress_size:
  935. raise DecompressSizeError(
  936. "Decompressed data exceeds the configured limit of %d bytes"
  937. % self._max_decompress_size
  938. )
  939. if chunk:
  940. self.out.feed_data(chunk, len(chunk))
  941. def feed_eof(self) -> None:
  942. chunk = self.decompressor.flush()
  943. if chunk or self.size > 0:
  944. self.out.feed_data(chunk, len(chunk))
  945. if self.encoding == "deflate" and not self.decompressor.eof:
  946. raise ContentEncodingError("deflate")
  947. self.out.feed_eof()
  948. def begin_http_chunk_receiving(self) -> None:
  949. self.out.begin_http_chunk_receiving()
  950. def end_http_chunk_receiving(self) -> None:
  951. self.out.end_http_chunk_receiving()
  952. HttpRequestParserPy = HttpRequestParser
  953. HttpResponseParserPy = HttpResponseParser
  954. RawRequestMessagePy = RawRequestMessage
  955. RawResponseMessagePy = RawResponseMessage
  956. try:
  957. if not NO_EXTENSIONS:
  958. from ._http_parser import ( # type: ignore[import-not-found,no-redef]
  959. HttpRequestParser,
  960. HttpResponseParser,
  961. RawRequestMessage,
  962. RawResponseMessage,
  963. )
  964. HttpRequestParserC = HttpRequestParser
  965. HttpResponseParserC = HttpResponseParser
  966. RawRequestMessageC = RawRequestMessage
  967. RawResponseMessageC = RawResponseMessage
  968. except ImportError: # pragma: no cover
  969. pass