Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/aiohttp/http_parser.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

628 statements  

1import abc 

2import asyncio 

3import re 

4import string 

5import sys 

6from contextlib import suppress 

7from enum import IntEnum 

8from re import Pattern 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 ClassVar, 

13 Final, 

14 Generic, 

15 Literal, 

16 NamedTuple, 

17 TypeVar, 

18) 

19 

20from multidict import CIMultiDict, CIMultiDictProxy, istr 

21from yarl import URL 

22 

23from . import hdrs 

24from .base_protocol import BaseProtocol 

25from .compression_utils import ( 

26 HAS_BROTLI, 

27 HAS_ZSTD, 

28 BrotliDecompressor, 

29 ZLibDecompressor, 

30 ZSTDDecompressor, 

31) 

32from .helpers import ( 

33 _EXC_SENTINEL, 

34 DEBUG, 

35 DEFAULT_CHUNK_SIZE, 

36 EMPTY_BODY_METHODS, 

37 EMPTY_BODY_STATUS_CODES, 

38 NO_EXTENSIONS, 

39 BaseTimerContext, 

40 set_exception, 

41) 

42from .http_exceptions import ( 

43 BadHttpMessage, 

44 BadHttpMethod, 

45 BadStatusLine, 

46 ContentEncodingError, 

47 ContentLengthError, 

48 InvalidHeader, 

49 InvalidURLError, 

50 LineTooLong, 

51 TransferEncodingError, 

52) 

53from .http_writer import HttpVersion, HttpVersion10, HttpVersion11 

54from .streams import EMPTY_PAYLOAD, StreamReader 

55from .typedefs import RawHeaders 

56 

57if TYPE_CHECKING: 

58 from .client_proto import ResponseHandler 

59 

60__all__ = ( 

61 "HeadersParser", 

62 "HttpParser", 

63 "HttpRequestParser", 

64 "HttpResponseParser", 

65 "RawRequestMessage", 

66 "RawResponseMessage", 

67) 

68 

69_SEP = Literal[b"\r\n", b"\n"] 

70 

71ASCIISET: Final[set[str]] = set(string.printable) 

72 

73# See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview 

74# and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens 

75# 

76# method = token 

77# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / 

78# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA 

79# token = 1*tchar 

80_TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~") 

81TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+") 

82VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) 

83DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) 

84HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") 

85# https://www.rfc-editor.org/rfc/rfc9110#section-5.5-5 

86_FIELD_VALUE_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile( 

87 r"[\x00-\x08\x0a-\x1f\x7f]" 

88) 

89 

90# RFC 9110 singleton headers — duplicates are rejected in strict mode. 

91# In lax mode (response parser default), the check is skipped entirely 

92# since real-world servers (e.g. Google APIs, Werkzeug) commonly send 

93# duplicate headers like Content-Type or Server. 

94# Lowercased for case-insensitive matching against wire names. 

95SINGLETON_HEADERS: Final[frozenset[str]] = frozenset( 

96 { 

97 "content-length", 

98 "content-location", 

99 "content-range", 

100 "content-type", 

101 "etag", 

102 "host", 

103 "max-forwards", 

104 "server", 

105 "transfer-encoding", 

106 "user-agent", 

107 } 

108) 

109 

110 

111class RawRequestMessage(NamedTuple): 

112 method: str 

113 path: str 

114 version: HttpVersion 

115 headers: "CIMultiDictProxy[str]" 

116 raw_headers: RawHeaders 

117 should_close: bool 

118 compression: str | None 

119 upgrade: bool 

120 chunked: bool 

121 url: URL 

122 

123 

124class RawResponseMessage(NamedTuple): 

125 version: HttpVersion 

126 code: int 

127 reason: str 

128 headers: CIMultiDictProxy[str] 

129 raw_headers: RawHeaders 

130 should_close: bool 

131 compression: str | None 

132 upgrade: bool 

133 chunked: bool 

134 

135 

136_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage) 

137 

138 

139class PayloadState(IntEnum): 

140 PAYLOAD_COMPLETE = 0 

141 PAYLOAD_NEEDS_INPUT = 1 

142 PAYLOAD_HAS_PENDING_INPUT = 2 

143 

144 

145class ParseState(IntEnum): 

146 

147 PARSE_NONE = 0 

148 PARSE_LENGTH = 1 

149 PARSE_CHUNKED = 2 

150 PARSE_UNTIL_EOF = 3 

151 

152 

153class ChunkState(IntEnum): 

154 PARSE_CHUNKED_SIZE = 0 

155 PARSE_CHUNKED_CHUNK = 1 

156 PARSE_CHUNKED_CHUNK_EOF = 2 

157 PARSE_MAYBE_TRAILERS = 3 

158 PARSE_TRAILERS = 4 

159 

160 

161class HeadersParser: 

162 def __init__( 

163 self, 

164 max_line_size: int = 8190, 

165 max_headers: int = 32768, 

166 max_field_size: int = 8190, 

167 lax: bool = False, 

168 ) -> None: 

169 self.max_line_size = max_line_size 

170 self.max_headers = max_headers 

171 self.max_field_size = max_field_size 

172 self._lax = lax 

173 

174 def parse_headers( 

175 self, lines: list[bytes] 

176 ) -> tuple["CIMultiDictProxy[str]", RawHeaders]: 

177 headers: CIMultiDict[str] = CIMultiDict() 

178 # note: "raw" does not mean inclusion of OWS before/after the field value 

179 raw_headers = [] 

180 

181 lines_idx = 0 

182 line = lines[lines_idx] 

183 line_count = len(lines) 

184 

185 while line: 

186 # Parse initial header name : value pair. 

187 try: 

188 bname, bvalue = line.split(b":", 1) 

189 except ValueError: 

190 raise InvalidHeader(line) from None 

191 

192 if len(bname) == 0: 

193 raise InvalidHeader(bname) 

194 

195 # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 

196 if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"} 

197 raise InvalidHeader(line) 

198 

199 bvalue = bvalue.lstrip(b" \t") 

200 name = bname.decode("utf-8", "surrogateescape") 

201 if not TOKENRE.fullmatch(name): 

202 raise InvalidHeader(bname) 

203 

204 # next line 

205 lines_idx += 1 

206 line = lines[lines_idx] 

207 

208 # consume continuation lines 

209 continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t') 

210 

211 # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding 

212 if continuation: 

213 header_length = len(bvalue) 

214 bvalue_lst = [bvalue] 

215 while continuation: 

216 header_length += len(line) 

217 if header_length > self.max_field_size: 

218 header_line = bname + b": " + b"".join(bvalue_lst) 

219 raise LineTooLong( 

220 header_line[:100] + b"...", self.max_field_size 

221 ) 

222 bvalue_lst.append(line) 

223 

224 # next line 

225 lines_idx += 1 

226 if lines_idx < line_count: 

227 line = lines[lines_idx] 

228 if line: 

229 continuation = line[0] in (32, 9) # (' ', '\t') 

230 else: 

231 line = b"" 

232 break 

233 bvalue = b"".join(bvalue_lst) 

234 

235 bvalue = bvalue.strip(b" \t") 

236 value = bvalue.decode("utf-8", "surrogateescape") 

237 

238 # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 

239 if self._lax: 

240 if "\n" in value or "\r" in value or "\x00" in value: 

241 raise InvalidHeader(bvalue) 

242 elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): 

243 raise InvalidHeader(bvalue) 

244 

245 if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS: 

246 raise BadHttpMessage(f"Duplicate '{name}' header found.") 

247 headers.add(name, value) 

248 raw_headers.append((bname, bvalue)) 

249 

250 return (CIMultiDictProxy(headers), tuple(raw_headers)) 

251 

252 

253def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool: 

254 """Check if the upgrade header is supported.""" 

255 u = headers.get(hdrs.UPGRADE, "") 

256 # .lower() can transform non-ascii characters. 

257 return u.isascii() and u.lower() in {"tcp", "websocket"} 

258 

259 

260class HttpParser(abc.ABC, Generic[_MsgT]): 

261 lax: ClassVar[bool] = False 

262 

263 def __init__( 

264 self, 

265 protocol: BaseProtocol | None = None, 

266 loop: asyncio.AbstractEventLoop | None = None, 

267 limit: int = 2**16, 

268 max_line_size: int = 8190, 

269 max_headers: int = 128, 

270 max_field_size: int = 8190, 

271 timer: BaseTimerContext | None = None, 

272 code: int | None = None, 

273 method: str | None = None, 

274 payload_exception: type[BaseException] | None = None, 

275 response_with_body: bool = True, 

276 read_until_eof: bool = False, 

277 auto_decompress: bool = True, 

278 max_msg_queue_size: int = 0, 

279 ) -> None: 

280 self.protocol = protocol 

281 self.loop = loop 

282 self.max_line_size = max_line_size 

283 self.max_headers = max_headers 

284 self.max_field_size = max_field_size 

285 self.max_headers = max_headers 

286 self.timer = timer 

287 self.code = code 

288 self.method = method 

289 self.payload_exception = payload_exception 

290 self.response_with_body = response_with_body 

291 self.read_until_eof = read_until_eof 

292 

293 self._lines: list[bytes] = [] 

294 self._tail = b"" 

295 self._upgraded = False 

296 self._pending_upgrade = False 

297 self._payload = None 

298 self._payload_parser: HttpPayloadParser | None = None 

299 self._payload_has_more_data = False 

300 self._auto_decompress = auto_decompress 

301 self._limit = limit 

302 self._headers_parser = HeadersParser( 

303 max_line_size, max_headers, max_field_size, self.lax 

304 ) 

305 # Stop emitting messages once this many are queued unconsumed (0 = off). 

306 self._max_msg_queue_size = max_msg_queue_size 

307 self._msg_in_flight = 0 

308 

309 @abc.abstractmethod 

310 def parse_message(self, lines: list[bytes]) -> _MsgT: ... 

311 

312 @abc.abstractmethod 

313 def _is_chunked_te(self, te: str) -> bool: ... 

314 

315 def pause_reading(self) -> None: 

316 assert self._payload_parser is not None 

317 self._payload_parser.pause_reading() 

318 

319 def message_consumed(self) -> None: 

320 """Protocol drained a queued message; free a slot for parsing.""" 

321 if self._msg_in_flight > 0: 

322 self._msg_in_flight -= 1 

323 

324 def feed_eof(self) -> _MsgT | None: 

325 if self._payload_parser is not None: 

326 self._payload_parser.feed_eof() 

327 if self._payload_parser.done: 

328 self._payload_parser = None 

329 else: 

330 # try to extract partial message 

331 if self._tail: 

332 self._lines.append(self._tail) 

333 

334 if self._lines: 

335 if self._lines[-1] != "\r\n": 

336 self._lines.append(b"") 

337 with suppress(Exception): 

338 return self.parse_message(self._lines) 

339 return None 

340 

341 def feed_data( 

342 self, 

343 data: bytes, 

344 SEP: _SEP = b"\r\n", 

345 EMPTY: bytes = b"", 

346 CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH, 

347 METH_CONNECT: str = hdrs.METH_CONNECT, 

348 SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1, 

349 ) -> tuple[list[tuple[_MsgT, StreamReader]], bool, bytes]: 

350 

351 messages = [] 

352 

353 if self._tail: 

354 data, self._tail = self._tail + data, b"" 

355 

356 data_len = len(data) 

357 start_pos = 0 

358 loop = self.loop 

359 max_line_length = self.max_line_size 

360 

361 should_close = False 

362 while start_pos < data_len or self._payload_has_more_data: 

363 # read HTTP message (request/response line + headers), \r\n\r\n 

364 # and split by lines 

365 if self._payload_parser is None and not self._upgraded: 

366 if ( 

367 self._max_msg_queue_size 

368 and self._msg_in_flight >= self._max_msg_queue_size 

369 ): 

370 # Queue full: buffer the rest and stop. Safe pause point; 

371 # any preceding body is consumed before the next request 

372 # line. Resumes via feed_data(b"") when the queue drains. 

373 self._tail = data[start_pos:] 

374 break 

375 pos = data.find(SEP, start_pos) 

376 # consume \r\n 

377 if pos == start_pos and not self._lines: 

378 start_pos = pos + len(SEP) 

379 continue 

380 

381 if pos >= start_pos: 

382 if should_close: 

383 raise BadHttpMessage("Data after `Connection: close`") 

384 

385 # line found 

386 line = data[start_pos:pos] 

387 if SEP == b"\n": # For lax response parsing 

388 line = line.rstrip(b"\r") 

389 if len(line) > max_line_length: 

390 raise LineTooLong(line[:100] + b"...", max_line_length) 

391 

392 self._lines.append(line) 

393 # After processing the status/request line, everything is a header. 

394 max_line_length = self.max_field_size 

395 

396 if len(self._lines) > self.max_headers: 

397 raise BadHttpMessage("Too many headers received") 

398 

399 start_pos = pos + len(SEP) 

400 

401 # \r\n\r\n found 

402 if self._lines[-1] == EMPTY: 

403 max_trailers = self.max_headers - len(self._lines) 

404 try: 

405 msg: _MsgT = self.parse_message(self._lines) 

406 finally: 

407 self._lines.clear() 

408 

409 def get_content_length() -> int | None: 

410 # payload length 

411 length_hdr = msg.headers.get(CONTENT_LENGTH) 

412 if length_hdr is None: 

413 return None 

414 

415 # Shouldn't allow +/- or other number formats. 

416 # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 

417 # msg.headers is already stripped of leading/trailing wsp 

418 if not DIGITS.fullmatch(length_hdr): 

419 raise InvalidHeader(CONTENT_LENGTH) 

420 

421 return int(length_hdr) 

422 

423 length = get_content_length() 

424 # do not support old websocket spec 

425 if SEC_WEBSOCKET_KEY1 in msg.headers: 

426 raise InvalidHeader(SEC_WEBSOCKET_KEY1) 

427 

428 upgraded = msg.upgrade and _is_supported_upgrade(msg.headers) 

429 

430 method = getattr(msg, "method", self.method) 

431 # code is only present on responses 

432 code = getattr(msg, "code", 0) 

433 

434 assert self.protocol is not None 

435 # calculate payload 

436 empty_body = code in EMPTY_BODY_STATUS_CODES or bool( 

437 method and method in EMPTY_BODY_METHODS 

438 ) 

439 if not empty_body and ( 

440 (length is not None and length > 0) or msg.chunked 

441 ): 

442 payload = StreamReader( 

443 self.protocol, 

444 timer=self.timer, 

445 loop=loop, 

446 limit=self._limit, 

447 ) 

448 payload_parser = HttpPayloadParser( 

449 payload, 

450 length=length, 

451 chunked=msg.chunked, 

452 method=method, 

453 compression=msg.compression, 

454 code=self.code, 

455 response_with_body=self.response_with_body, 

456 auto_decompress=self._auto_decompress, 

457 lax=self.lax, 

458 headers_parser=self._headers_parser, 

459 max_line_size=self.max_line_size, 

460 max_field_size=self.max_field_size, 

461 max_trailers=max_trailers, 

462 limit=self._limit, 

463 ) 

464 if not payload_parser.done: 

465 self._payload_parser = payload_parser 

466 # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 

467 # Defer any requested upgrade until the 

468 # complete request has been read. 

469 self._pending_upgrade = upgraded 

470 elif method == METH_CONNECT: 

471 assert isinstance(msg, RawRequestMessage) 

472 payload = StreamReader( 

473 self.protocol, 

474 timer=self.timer, 

475 loop=loop, 

476 limit=self._limit, 

477 ) 

478 self._upgraded = True 

479 self._payload_parser = HttpPayloadParser( 

480 payload, 

481 method=msg.method, 

482 compression=msg.compression, 

483 auto_decompress=self._auto_decompress, 

484 lax=self.lax, 

485 headers_parser=self._headers_parser, 

486 max_line_size=self.max_line_size, 

487 max_field_size=self.max_field_size, 

488 max_trailers=max_trailers, 

489 limit=self._limit, 

490 ) 

491 elif not empty_body and length is None and self.read_until_eof: 

492 payload = StreamReader( 

493 self.protocol, 

494 timer=self.timer, 

495 loop=loop, 

496 limit=self._limit, 

497 ) 

498 payload_parser = HttpPayloadParser( 

499 payload, 

500 length=length, 

501 chunked=msg.chunked, 

502 method=method, 

503 compression=msg.compression, 

504 code=self.code, 

505 response_with_body=self.response_with_body, 

506 auto_decompress=self._auto_decompress, 

507 lax=self.lax, 

508 headers_parser=self._headers_parser, 

509 max_line_size=self.max_line_size, 

510 max_field_size=self.max_field_size, 

511 max_trailers=max_trailers, 

512 limit=self._limit, 

513 ) 

514 if not payload_parser.done: 

515 self._payload_parser = payload_parser 

516 elif upgraded: 

517 # No body to read, so the connection switches to 

518 # the upgraded protocol immediately. 

519 self._upgraded = True 

520 payload = EMPTY_PAYLOAD 

521 else: 

522 payload = EMPTY_PAYLOAD 

523 

524 messages.append((msg, payload)) 

525 if self._max_msg_queue_size: 

526 self._msg_in_flight += 1 

527 should_close = msg.should_close 

528 else: 

529 self._tail = data[start_pos:] 

530 # A bare LF here means CRLF was required: 

531 # reject instead of buffering, else a following request's 

532 # bytes get appended to this line and leak in the error. 

533 if b"\n" in self._tail: 

534 raise BadHttpMessage("Bad line ending, expected CRLF") 

535 if len(self._tail) > self.max_line_size: 

536 raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) 

537 data = EMPTY 

538 break 

539 

540 # no parser, just store 

541 elif self._payload_parser is None and self._upgraded: 

542 assert not self._lines 

543 break 

544 

545 # feed payload 

546 else: 

547 assert not self._lines 

548 assert self._payload_parser is not None 

549 try: 

550 payload_state, data = self._payload_parser.feed_data( 

551 data[start_pos:], SEP 

552 ) 

553 except Exception as underlying_exc: 

554 reraised_exc: BaseException = underlying_exc 

555 if self.payload_exception is not None: 

556 reraised_exc = self.payload_exception(str(underlying_exc)) 

557 

558 set_exception( 

559 self._payload_parser.payload, 

560 reraised_exc, 

561 underlying_exc, 

562 ) 

563 

564 payload_state = PayloadState.PAYLOAD_COMPLETE 

565 data = b"" 

566 if isinstance( 

567 underlying_exc, (InvalidHeader, TransferEncodingError) 

568 ): 

569 raise 

570 

571 self._payload_has_more_data = ( 

572 payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT 

573 ) 

574 

575 if payload_state is not PayloadState.PAYLOAD_COMPLETE: 

576 # We've either consumed all available data, or we're pausing 

577 # until the reader buffer is freed up. 

578 break 

579 

580 start_pos = 0 

581 data_len = len(data) 

582 self._payload_parser = None 

583 if self._pending_upgrade: 

584 # Body fully read: the deferred upgrade takes effect and 

585 # the rest of the connection is the upgraded protocol. 

586 self._upgraded = True 

587 self._pending_upgrade = False 

588 

589 if data and start_pos < data_len: 

590 data = data[start_pos:] 

591 else: 

592 data = EMPTY 

593 

594 return messages, self._upgraded, data 

595 

596 def parse_headers( 

597 self, lines: list[bytes] 

598 ) -> tuple[ 

599 "CIMultiDictProxy[str]", RawHeaders, bool | None, str | None, bool, bool 

600 ]: 

601 """Parses RFC 5322 headers from a stream. 

602 

603 Line continuations are supported. Returns list of header name 

604 and value pairs. Header name is in upper case. 

605 """ 

606 headers, raw_headers = self._headers_parser.parse_headers(lines) 

607 close_conn = None 

608 encoding = None 

609 upgrade = False 

610 chunked = False 

611 

612 # keep-alive and protocol switching 

613 # RFC 9110 section 7.6.1 defines Connection as a comma-separated list. 

614 conn_values = headers.getall(hdrs.CONNECTION, ()) 

615 if conn_values: 

616 conn_tokens = { 

617 token.lower() 

618 for conn_value in conn_values 

619 for token in (part.strip(" \t") for part in conn_value.split(",")) 

620 if token and token.isascii() 

621 } 

622 

623 if "close" in conn_tokens: 

624 close_conn = True 

625 elif "keep-alive" in conn_tokens: 

626 close_conn = False 

627 

628 # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols 

629 if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE): 

630 upgrade = True 

631 

632 # encoding 

633 enc = headers.get(hdrs.CONTENT_ENCODING, "") 

634 if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}: 

635 encoding = enc 

636 

637 # chunking 

638 te = headers.get(hdrs.TRANSFER_ENCODING) 

639 if te is not None: 

640 if self._is_chunked_te(te): 

641 chunked = True 

642 

643 if hdrs.CONTENT_LENGTH in headers: 

644 raise BadHttpMessage( 

645 "Transfer-Encoding can't be present with Content-Length", 

646 ) 

647 

648 return (headers, raw_headers, close_conn, encoding, upgrade, chunked) 

649 

650 def set_upgraded(self, val: bool) -> None: 

651 """Set connection upgraded (to websocket) mode. 

652 

653 :param bool val: new state. 

654 """ 

655 self._upgraded = val 

656 

657 

658class HttpRequestParser(HttpParser[RawRequestMessage]): 

659 """Read request status line. 

660 

661 Exception .http_exceptions.BadStatusLine 

662 could be raised in case of any errors in status line. 

663 Returns RawRequestMessage. 

664 """ 

665 

666 def parse_message(self, lines: list[bytes]) -> RawRequestMessage: 

667 # request line 

668 line = lines[0].decode("utf-8", "surrogateescape") 

669 try: 

670 method, path, version = line.split(" ", maxsplit=2) 

671 except ValueError: 

672 raise BadHttpMethod(line) from None 

673 

674 # method 

675 if not TOKENRE.fullmatch(method): 

676 raise BadHttpMethod(method) 

677 method = method.upper() 

678 

679 # version 

680 match = VERSRE.fullmatch(version) 

681 if match is None: 

682 raise BadStatusLine(line) 

683 version_o = HttpVersion(int(match.group(1)), int(match.group(2))) 

684 

685 if method == "CONNECT": 

686 # authority-form, 

687 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 

688 url = URL.build(authority=path, encoded=True) 

689 elif path.startswith("/"): 

690 # origin-form, 

691 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 

692 path_part, _hash_separator, url_fragment = path.partition("#") 

693 path_part, _question_mark_separator, qs_part = path_part.partition("?") 

694 

695 # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based 

696 # NOTE: parser does, otherwise it results into the same 

697 # NOTE: HTTP Request-Line input producing different 

698 # NOTE: `yarl.URL()` objects 

699 url = URL.build( 

700 path=path_part, 

701 query_string=qs_part, 

702 fragment=url_fragment, 

703 encoded=True, 

704 ) 

705 elif path == "*" and method == "OPTIONS": 

706 # asterisk-form, 

707 url = URL(path, encoded=True) 

708 else: 

709 # absolute-form for proxy maybe, 

710 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 

711 url = URL(path, encoded=True) 

712 if url.scheme == "": 

713 # not absolute-form 

714 raise InvalidURLError( 

715 path.encode(errors="surrogateescape").decode("latin1") 

716 ) 

717 

718 # read headers 

719 ( 

720 headers, 

721 raw_headers, 

722 close, 

723 compression, 

724 upgrade, 

725 chunked, 

726 ) = self.parse_headers(lines[1:]) 

727 

728 if version_o == HttpVersion11 and hdrs.HOST not in headers: 

729 raise BadHttpMessage("Missing 'Host' header in request.") 

730 

731 if close is None: # then the headers weren't set in the request 

732 if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close 

733 close = True 

734 else: # HTTP 1.1 must ask to close. 

735 close = False 

736 

737 return RawRequestMessage( 

738 method, 

739 path, 

740 version_o, 

741 headers, 

742 raw_headers, 

743 close, 

744 compression, 

745 upgrade, 

746 chunked, 

747 url, 

748 ) 

749 

750 def _is_chunked_te(self, te: str) -> bool: 

751 te = te.rsplit(",", maxsplit=1)[-1].strip(" \t") 

752 # .lower() transforms some non-ascii chars, so must check first. 

753 if te.isascii() and te.lower() == "chunked": 

754 return True 

755 # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 

756 raise BadHttpMessage("Request has invalid `Transfer-Encoding`") 

757 

758 

759class HttpResponseParser(HttpParser[RawResponseMessage]): 

760 """Read response status line and headers. 

761 

762 BadStatusLine could be raised in case of any errors in status line. 

763 Returns RawResponseMessage. 

764 """ 

765 

766 protocol: "ResponseHandler" 

767 

768 # Lax mode should only be enabled on response parser. 

769 lax = not DEBUG 

770 

771 def feed_data( 

772 self, 

773 data: bytes, 

774 SEP: _SEP | None = None, 

775 *args: Any, 

776 **kwargs: Any, 

777 ) -> tuple[list[tuple[RawResponseMessage, StreamReader]], bool, bytes]: 

778 if SEP is None: 

779 SEP = b"\r\n" if DEBUG else b"\n" 

780 return super().feed_data(data, SEP, *args, **kwargs) 

781 

782 def parse_message(self, lines: list[bytes]) -> RawResponseMessage: 

783 line = lines[0].decode("utf-8", "surrogateescape") 

784 try: 

785 version, status = line.split(maxsplit=1) 

786 except ValueError: 

787 raise BadStatusLine(line) from None 

788 

789 try: 

790 status, reason = status.split(maxsplit=1) 

791 except ValueError: 

792 status = status.strip() 

793 reason = "" 

794 

795 # version 

796 match = VERSRE.fullmatch(version) 

797 if match is None: 

798 raise BadStatusLine(line) 

799 version_o = HttpVersion(int(match.group(1)), int(match.group(2))) 

800 

801 # The status code is a three-digit ASCII number, no padding 

802 if len(status) != 3 or not DIGITS.fullmatch(status): 

803 raise BadStatusLine(line) 

804 status_i = int(status) 

805 

806 # read headers 

807 ( 

808 headers, 

809 raw_headers, 

810 close, 

811 compression, 

812 upgrade, 

813 chunked, 

814 ) = self.parse_headers(lines[1:]) 

815 

816 if close is None: 

817 if version_o <= HttpVersion10: 

818 close = True 

819 # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length 

820 elif 100 <= status_i < 200 or status_i in {204, 304}: 

821 close = False 

822 elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers: 

823 close = False 

824 else: 

825 # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8 

826 close = True 

827 

828 return RawResponseMessage( 

829 version_o, 

830 status_i, 

831 reason.strip(), 

832 headers, 

833 raw_headers, 

834 close, 

835 compression, 

836 upgrade, 

837 chunked, 

838 ) 

839 

840 def _is_chunked_te(self, te: str) -> bool: 

841 # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2 

842 return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked" 

843 

844 

845class HttpPayloadParser: 

846 def __init__( 

847 self, 

848 payload: StreamReader, 

849 length: int | None = None, 

850 chunked: bool = False, 

851 compression: str | None = None, 

852 code: int | None = None, 

853 method: str | None = None, 

854 response_with_body: bool = True, 

855 auto_decompress: bool = True, 

856 lax: bool = False, 

857 *, 

858 headers_parser: HeadersParser, 

859 max_line_size: int = 8190, 

860 max_field_size: int = 8190, 

861 max_trailers: int = 128, 

862 limit: int = DEFAULT_CHUNK_SIZE, 

863 ) -> None: 

864 self._length = 0 

865 self._paused = False 

866 self._type = ParseState.PARSE_UNTIL_EOF 

867 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

868 self._chunk_size = 0 

869 self._chunk_tail = b"" 

870 self._auto_decompress = auto_decompress 

871 self._lax = lax 

872 self._headers_parser = headers_parser 

873 self._max_line_size = max_line_size 

874 self._max_field_size = max_field_size 

875 self._max_trailers = max_trailers 

876 self._more_data_available = False 

877 self._trailer_lines: list[bytes] = [] 

878 self.done = False 

879 self._eof_pending = False 

880 

881 # payload decompression wrapper 

882 if response_with_body and compression and self._auto_decompress: 

883 real_payload: StreamReader | DeflateBuffer = DeflateBuffer( 

884 payload, compression, max_decompress_size=limit 

885 ) 

886 else: 

887 real_payload = payload 

888 

889 # payload parser 

890 if not response_with_body: 

891 # don't parse payload if it's not expected to be received 

892 self._type = ParseState.PARSE_NONE 

893 real_payload.feed_eof() 

894 self.done = True 

895 elif chunked: 

896 self._type = ParseState.PARSE_CHUNKED 

897 elif length is not None: 

898 self._type = ParseState.PARSE_LENGTH 

899 self._length = length 

900 self._length_expected = length 

901 if self._length == 0: 

902 real_payload.feed_eof() 

903 self.done = True 

904 

905 self.payload = real_payload 

906 

907 def pause_reading(self) -> None: 

908 self._paused = True 

909 

910 def feed_eof(self) -> None: 

911 if self._type == ParseState.PARSE_UNTIL_EOF: 

912 self._eof_pending = True 

913 while self._more_data_available: 

914 if self._paused: 

915 self._paused = False 

916 return # Will resume via feed_data(b"") later 

917 self._more_data_available = self.payload.feed_data(b"", 0) 

918 self.payload.feed_eof() 

919 self.done = True 

920 self._eof_pending = False 

921 elif self._type == ParseState.PARSE_LENGTH: 

922 received = self._length_expected - self._length 

923 raise ContentLengthError( 

924 f"Not enough data to satisfy content length header " 

925 f"(received {received} of {self._length_expected} bytes)." 

926 ) 

927 elif self._type == ParseState.PARSE_CHUNKED: 

928 raise TransferEncodingError( 

929 "Not enough data to satisfy transfer length header." 

930 ) 

931 

932 def feed_data( 

933 self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";" 

934 ) -> tuple[PayloadState, bytes]: 

935 """Receive a chunk of data to process. 

936 

937 Return: 

938 PayloadState - The current state of payload processing. 

939 This function may be called with empty bytes after returning 

940 PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause. 

941 bytes - If payload is complete, this is the unconsumed bytes intended for the 

942 next message/payload, b"" otherwise. 

943 """ 

944 # Read specified amount of bytes 

945 if self._type == ParseState.PARSE_LENGTH: 

946 if self._chunk_tail: 

947 chunk = self._chunk_tail + chunk 

948 self._chunk_tail = b"" 

949 

950 required = self._length 

951 self._length = max(required - len(chunk), 0) 

952 self._more_data_available = self.payload.feed_data( 

953 chunk[:required], required 

954 ) 

955 while self._more_data_available: 

956 if self._paused: 

957 self._paused = False 

958 self._chunk_tail = chunk[required:] 

959 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

960 self._more_data_available = self.payload.feed_data(b"", 0) 

961 

962 if self._length == 0: 

963 self.payload.feed_eof() 

964 return PayloadState.PAYLOAD_COMPLETE, chunk[required:] 

965 # Chunked transfer encoding parser 

966 elif self._type == ParseState.PARSE_CHUNKED: 

967 if self._chunk_tail: 

968 # We should check the length is sane when not processing payload body. 

969 if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK: 

970 max_line_length = self._max_line_size 

971 if self._chunk == ChunkState.PARSE_TRAILERS: 

972 max_line_length = self._max_field_size 

973 if len(self._chunk_tail) > max_line_length: 

974 raise LineTooLong( 

975 self._chunk_tail[:100] + b"...", max_line_length 

976 ) 

977 

978 chunk = self._chunk_tail + chunk 

979 self._chunk_tail = b"" 

980 

981 while chunk or self._more_data_available: 

982 # read next chunk size 

983 if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: 

984 pos = chunk.find(SEP) 

985 if pos >= 0: 

986 # Only chunk-size lines reach here; trailers enforce 

987 # _max_field_size separately in PARSE_TRAILERS below. 

988 if pos > self._max_line_size: 

989 raise LineTooLong(chunk[:100] + b"...", self._max_line_size) 

990 i = chunk.find(CHUNK_EXT, 0, pos) 

991 if i >= 0: 

992 size_b = chunk[:i] # strip chunk-extensions 

993 # Verify no LF in the chunk-extension 

994 if b"\n" in (ext := chunk[i:pos]): 

995 exc = TransferEncodingError( 

996 f"Unexpected LF in chunk-extension: {ext!r}" 

997 ) 

998 set_exception(self.payload, exc) 

999 raise exc 

1000 else: 

1001 size_b = chunk[:pos] 

1002 

1003 if self._lax: # Allow whitespace in lax mode. 

1004 size_b = size_b.strip() 

1005 

1006 if not re.fullmatch(HEXDIGITS, size_b): 

1007 exc = TransferEncodingError( 

1008 chunk[:pos].decode("ascii", "surrogateescape") 

1009 ) 

1010 set_exception(self.payload, exc) 

1011 raise exc 

1012 size = int(bytes(size_b), 16) 

1013 

1014 chunk = chunk[pos + len(SEP) :] 

1015 if size == 0: # eof marker 

1016 self._chunk = ChunkState.PARSE_TRAILERS 

1017 if self._lax and chunk.startswith(b"\r"): 

1018 chunk = chunk[1:] 

1019 else: 

1020 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK 

1021 self._chunk_size = size 

1022 self.payload.begin_http_chunk_receiving() 

1023 else: 

1024 if b"\n" in chunk: 

1025 exc = TransferEncodingError( 

1026 "Bad chunk-size line ending, expected CRLF" 

1027 ) 

1028 set_exception(self.payload, exc) 

1029 raise exc 

1030 self._chunk_tail = chunk 

1031 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1032 

1033 # read chunk and feed buffer 

1034 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: 

1035 if self._paused: 

1036 self._paused = False 

1037 self._chunk_tail = chunk 

1038 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

1039 

1040 required = self._chunk_size 

1041 self._chunk_size = max(required - len(chunk), 0) 

1042 self._more_data_available = self.payload.feed_data( 

1043 chunk[:required], required 

1044 ) 

1045 chunk = chunk[required:] 

1046 

1047 if self._more_data_available: 

1048 continue 

1049 

1050 if self._chunk_size: 

1051 self._paused = False 

1052 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1053 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF 

1054 self.payload.end_http_chunk_receiving() 

1055 

1056 # toss the CRLF at the end of the chunk 

1057 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: 

1058 if self._lax and chunk.startswith(b"\r"): 

1059 chunk = chunk[1:] 

1060 if chunk[: len(SEP)] == SEP: 

1061 chunk = chunk[len(SEP) :] 

1062 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

1063 elif len(chunk) >= len(SEP) or chunk != SEP[: len(chunk)]: 

1064 exc = TransferEncodingError( 

1065 "Chunk size mismatch: expected CRLF after chunk data" 

1066 ) 

1067 set_exception(self.payload, exc) 

1068 raise exc 

1069 else: 

1070 self._chunk_tail = chunk 

1071 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1072 

1073 if self._chunk == ChunkState.PARSE_TRAILERS: 

1074 pos = chunk.find(SEP) 

1075 if pos < 0: # No line found 

1076 if b"\n" in chunk: 

1077 exc = TransferEncodingError( 

1078 "Bad trailer line ending, expected CRLF" 

1079 ) 

1080 set_exception(self.payload, exc) 

1081 raise exc 

1082 self._chunk_tail = chunk 

1083 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1084 

1085 line = chunk[:pos] 

1086 chunk = chunk[pos + len(SEP) :] 

1087 if SEP == b"\n": # For lax response parsing 

1088 line = line.rstrip(b"\r") 

1089 

1090 if len(line) > self._max_field_size: 

1091 raise LineTooLong(line[:100] + b"...", self._max_field_size) 

1092 

1093 self._trailer_lines.append(line) 

1094 

1095 if len(self._trailer_lines) > self._max_trailers: 

1096 raise BadHttpMessage("Too many trailers received") 

1097 

1098 # \r\n\r\n found, end of stream 

1099 if self._trailer_lines[-1] == b"": 

1100 # Headers and trailers are defined the same way, 

1101 # so we reuse the HeadersParser here. 

1102 try: 

1103 trailers, raw_trailers = self._headers_parser.parse_headers( 

1104 self._trailer_lines 

1105 ) 

1106 finally: 

1107 self._trailer_lines.clear() 

1108 self.payload.feed_eof() 

1109 return PayloadState.PAYLOAD_COMPLETE, chunk 

1110 

1111 # Read all bytes until eof 

1112 elif self._type == ParseState.PARSE_UNTIL_EOF: 

1113 self._more_data_available = self.payload.feed_data(chunk, len(chunk)) 

1114 while self._more_data_available: 

1115 if self._paused: 

1116 self._paused = False 

1117 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

1118 self._more_data_available = self.payload.feed_data(b"", 0) 

1119 

1120 if self._eof_pending: 

1121 self.payload.feed_eof() 

1122 self.done = True 

1123 self._eof_pending = False 

1124 return PayloadState.PAYLOAD_COMPLETE, b"" 

1125 

1126 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1127 

1128 

1129class DeflateBuffer: 

1130 """DeflateStream decompress stream and feed data into specified stream.""" 

1131 

1132 decompressor: Any 

1133 

1134 def __init__( 

1135 self, 

1136 out: StreamReader, 

1137 encoding: str | None, 

1138 max_decompress_size: int = DEFAULT_CHUNK_SIZE, 

1139 ) -> None: 

1140 self.out = out 

1141 self.size = 0 

1142 out.total_compressed_bytes = self.size 

1143 self.encoding = encoding 

1144 self._started_decoding = False 

1145 

1146 self.decompressor: BrotliDecompressor | ZLibDecompressor | ZSTDDecompressor 

1147 if encoding == "br": 

1148 if not HAS_BROTLI: # pragma: no cover 

1149 raise ContentEncodingError( 

1150 "Can not decode content-encoding: brotli (br). " 

1151 "Please install `Brotli`" 

1152 ) 

1153 self.decompressor = BrotliDecompressor() 

1154 elif encoding == "zstd": 

1155 if not HAS_ZSTD: 

1156 raise ContentEncodingError( 

1157 "Can not decode content-encoding: zstandard (zstd). " 

1158 "Please install `backports.zstd`" 

1159 ) 

1160 self.decompressor = ZSTDDecompressor() 

1161 else: 

1162 self.decompressor = ZLibDecompressor(encoding=encoding) 

1163 

1164 self._max_decompress_size = max_decompress_size 

1165 

1166 def set_exception( 

1167 self, 

1168 exc: BaseException, 

1169 exc_cause: BaseException = _EXC_SENTINEL, 

1170 ) -> None: 

1171 set_exception(self.out, exc, exc_cause) 

1172 

1173 def feed_data(self, chunk: bytes, size: int) -> bool: 

1174 self.size += size 

1175 self.out.total_compressed_bytes = self.size 

1176 

1177 # Inspect the first real byte once to choose the decompressor. An empty 

1178 # chunk (e.g. a chunk-size line arriving without body bytes) has no 

1179 # header to sniff, so skip it and wait for the first data byte. 

1180 if not self._started_decoding and chunk: 

1181 # RFC1950 

1182 # bits 0..3 = CM = 0b1000 = 8 = "deflate" 

1183 # bits 4..7 = CINFO = 1..7 = windows size. 

1184 if self.encoding == "deflate" and chunk[0] & 0xF != 8: 

1185 # Change the decoder to decompress incorrectly compressed data 

1186 # Actually we should issue a warning about non-RFC-compliant data. 

1187 self.decompressor = ZLibDecompressor( 

1188 encoding=self.encoding, suppress_deflate_header=True 

1189 ) 

1190 self._started_decoding = True 

1191 

1192 low_water = self.out._low_water 

1193 max_length = ( 

1194 0 if low_water >= sys.maxsize else max(self._max_decompress_size, low_water) 

1195 ) 

1196 try: 

1197 chunk = self.decompressor.decompress_sync(chunk, max_length=max_length) 

1198 except Exception: 

1199 raise ContentEncodingError( 

1200 "Can not decode content-encoding: %s" % self.encoding 

1201 ) 

1202 

1203 if chunk: 

1204 self.out.feed_data(chunk, len(chunk)) 

1205 return self.decompressor.data_available # type: ignore[no-any-return] 

1206 

1207 def feed_eof(self) -> None: 

1208 chunk = self.decompressor.flush() 

1209 # This should never contain data as we defer the call until exhausting 

1210 # the decompression. If .flush() is returning data, this may indicate a 

1211 # zip bomb vulnerability as it will decompress all remaining data at once. 

1212 assert not chunk 

1213 

1214 if self.size > 0: 

1215 if self.encoding == "deflate" and not self.decompressor.eof: 

1216 raise ContentEncodingError("deflate") 

1217 

1218 self.out.feed_eof() 

1219 

1220 def begin_http_chunk_receiving(self) -> None: 

1221 self.out.begin_http_chunk_receiving() 

1222 

1223 def end_http_chunk_receiving(self) -> None: 

1224 self.out.end_http_chunk_receiving() 

1225 

1226 

1227HttpRequestParserPy = HttpRequestParser 

1228HttpResponseParserPy = HttpResponseParser 

1229RawRequestMessagePy = RawRequestMessage 

1230RawResponseMessagePy = RawResponseMessage 

1231 

1232try: 

1233 if not NO_EXTENSIONS: 

1234 from ._http_parser import ( # type: ignore[import-not-found,no-redef] 

1235 HttpRequestParser, 

1236 HttpResponseParser, 

1237 RawRequestMessage, 

1238 RawResponseMessage, 

1239 ) 

1240 

1241 HttpRequestParserC = HttpRequestParser 

1242 HttpResponseParserC = HttpResponseParser 

1243 RawRequestMessageC = RawRequestMessage 

1244 RawResponseMessageC = RawResponseMessage 

1245except ImportError: # pragma: no cover 

1246 pass