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

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

657 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_TARGET_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile(r"[\x00-\x1f\x7f]") 

90 

91 

92def _has_authority(target: str) -> bool: 

93 """Tell if an absolute-form request target has "//" and a non-empty authority. 

94 

95 RFC 9110 section 4.2 requires a host for http and https. yarl's default 

96 WHATWG mode reads a host from "http:host/p" or "http:///host/p", so the 

97 target is checked before it is parsed. 

98 """ 

99 rest = target.partition(":")[2] 

100 return rest[:2] == "//" and rest[2:3] not in ("", "/", "?", "#") 

101 

102 

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

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

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

106# duplicate headers like Content-Type or Server. 

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

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

109 { 

110 "content-length", 

111 "content-location", 

112 "content-range", 

113 "content-type", 

114 "etag", 

115 "host", 

116 "max-forwards", 

117 "server", 

118 "transfer-encoding", 

119 "user-agent", 

120 } 

121) 

122 

123 

124class RawRequestMessage(NamedTuple): 

125 method: str 

126 path: str 

127 version: HttpVersion 

128 headers: "CIMultiDictProxy[str]" 

129 raw_headers: RawHeaders 

130 should_close: bool 

131 compression: str | None 

132 upgrade: bool 

133 chunked: bool 

134 url: URL 

135 

136 

137class RawResponseMessage(NamedTuple): 

138 version: HttpVersion 

139 code: int 

140 reason: str 

141 headers: CIMultiDictProxy[str] 

142 raw_headers: RawHeaders 

143 should_close: bool 

144 compression: str | None 

145 upgrade: bool 

146 chunked: bool 

147 

148 

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

150 

151 

152class PayloadState(IntEnum): 

153 PAYLOAD_COMPLETE = 0 

154 PAYLOAD_NEEDS_INPUT = 1 

155 PAYLOAD_HAS_PENDING_INPUT = 2 

156 

157 

158class ParseState(IntEnum): 

159 

160 PARSE_NONE = 0 

161 PARSE_LENGTH = 1 

162 PARSE_CHUNKED = 2 

163 PARSE_UNTIL_EOF = 3 

164 

165 

166class ChunkState(IntEnum): 

167 PARSE_CHUNKED_SIZE = 0 

168 PARSE_CHUNKED_CHUNK = 1 

169 PARSE_CHUNKED_CHUNK_EOF = 2 

170 PARSE_MAYBE_TRAILERS = 3 

171 PARSE_TRAILERS = 4 

172 

173 

174class HeadersParser: 

175 def __init__( 

176 self, 

177 max_line_size: int = 8190, 

178 max_headers: int = 32768, 

179 max_field_size: int = 8190, 

180 lax: bool = False, 

181 ) -> None: 

182 self.max_line_size = max_line_size 

183 self.max_headers = max_headers 

184 self.max_field_size = max_field_size 

185 self._lax = lax 

186 

187 def parse_headers( 

188 self, lines: list[bytes] 

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

190 headers: CIMultiDict[str] = CIMultiDict() 

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

192 raw_headers = [] 

193 

194 lines_idx = 0 

195 line = lines[lines_idx] 

196 line_count = len(lines) 

197 

198 while line: 

199 # Parse initial header name : value pair. 

200 try: 

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

202 except ValueError: 

203 raise InvalidHeader(line) from None 

204 

205 if len(bname) == 0: 

206 raise InvalidHeader(bname) 

207 

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

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

210 raise InvalidHeader(line) 

211 

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

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

214 if not TOKENRE.fullmatch(name): 

215 raise InvalidHeader(bname) 

216 

217 # next line 

218 lines_idx += 1 

219 line = lines[lines_idx] 

220 

221 # consume continuation lines 

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

223 

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

225 if continuation: 

226 header_length = len(bvalue) 

227 bvalue_lst = [bvalue] 

228 while continuation: 

229 header_length += len(line) 

230 if header_length > self.max_field_size: 

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

232 raise LineTooLong( 

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

234 ) 

235 bvalue_lst.append(line) 

236 

237 # next line 

238 lines_idx += 1 

239 if lines_idx < line_count: 

240 line = lines[lines_idx] 

241 if line: 

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

243 else: 

244 line = b"" 

245 break 

246 bvalue = b"".join(bvalue_lst) 

247 

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

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

250 

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

252 if self._lax: 

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

254 raise InvalidHeader(bvalue) 

255 elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): 

256 raise InvalidHeader(bvalue) 

257 

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

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

260 headers.add(name, value) 

261 raw_headers.append((bname, bvalue)) 

262 

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

264 

265 

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

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

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

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

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

271 

272 

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

274 lax: ClassVar[bool] = False 

275 

276 def __init__( 

277 self, 

278 protocol: BaseProtocol | None = None, 

279 loop: asyncio.AbstractEventLoop | None = None, 

280 limit: int = 2**16, 

281 max_line_size: int = 8190, 

282 max_headers: int = 128, 

283 max_field_size: int = 8190, 

284 timer: BaseTimerContext | None = None, 

285 code: int | None = None, 

286 method: str | None = None, 

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

288 response_with_body: bool = True, 

289 read_until_eof: bool = False, 

290 auto_decompress: bool = True, 

291 max_msg_queue_size: int = 0, 

292 ) -> None: 

293 self.protocol = protocol 

294 self.loop = loop 

295 self.max_line_size = max_line_size 

296 self.max_headers = max_headers 

297 self.max_field_size = max_field_size 

298 self.max_headers = max_headers 

299 self.timer = timer 

300 self.code = code 

301 self.method = method 

302 self.payload_exception = payload_exception 

303 self.response_with_body = response_with_body 

304 self.read_until_eof = read_until_eof 

305 

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

307 self._tail = b"" 

308 self._upgraded = False 

309 self._pending_upgrade = False 

310 self._payload = None 

311 self._payload_parser: HttpPayloadParser | None = None 

312 self._payload_has_more_data = False 

313 self._auto_decompress = auto_decompress 

314 self._limit = limit 

315 self._headers_parser = HeadersParser( 

316 max_line_size, max_headers, max_field_size, self.lax 

317 ) 

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

319 self._max_msg_queue_size = max_msg_queue_size 

320 self._msg_in_flight = 0 

321 

322 @abc.abstractmethod 

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

324 

325 @abc.abstractmethod 

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

327 

328 def pause_reading(self) -> None: 

329 assert self._payload_parser is not None 

330 self._payload_parser.pause_reading() 

331 

332 def message_consumed(self) -> None: 

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

334 if self._msg_in_flight > 0: 

335 self._msg_in_flight -= 1 

336 

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

338 if self._payload_parser is not None: 

339 self._payload_parser.feed_eof() 

340 if self._payload_parser.done: 

341 self._payload_parser = None 

342 else: 

343 # try to extract partial message 

344 if self._tail: 

345 self._lines.append(self._tail) 

346 

347 if self._lines: 

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

349 self._lines.append(b"") 

350 with suppress(Exception): 

351 return self.parse_message(self._lines) 

352 return None 

353 

354 def feed_data( 

355 self, 

356 data: bytes, 

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

358 EMPTY: bytes = b"", 

359 CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH, 

360 METH_CONNECT: str = hdrs.METH_CONNECT, 

361 SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1, 

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

363 

364 messages = [] 

365 

366 if self._tail: 

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

368 

369 data_len = len(data) 

370 start_pos = 0 

371 loop = self.loop 

372 max_line_length = self.max_line_size 

373 

374 should_close = False 

375 while start_pos < data_len or self._payload_has_more_data: 

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

377 # and split by lines 

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

379 if ( 

380 self._max_msg_queue_size 

381 and self._msg_in_flight >= self._max_msg_queue_size 

382 ): 

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

384 # any preceding body is consumed before the next request 

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

386 self._tail = data[start_pos:] 

387 # The remainder now lives in self._tail only. Don't return it. 

388 data = EMPTY 

389 break 

390 pos = data.find(SEP, start_pos) 

391 # consume \r\n 

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

393 start_pos = pos + len(SEP) 

394 continue 

395 

396 if pos >= start_pos: 

397 if should_close: 

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

399 

400 # line found 

401 line = data[start_pos:pos] 

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

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

404 if len(line) > max_line_length: 

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

406 

407 self._lines.append(line) 

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

409 max_line_length = self.max_field_size 

410 

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

412 raise BadHttpMessage("Too many headers received") 

413 

414 start_pos = pos + len(SEP) 

415 

416 # \r\n\r\n found 

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

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

419 try: 

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

421 finally: 

422 self._lines.clear() 

423 

424 def get_content_length() -> int | None: 

425 # payload length 

426 length_hdr = msg.headers.get(CONTENT_LENGTH) 

427 if length_hdr is None: 

428 return None 

429 

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

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

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

433 if not DIGITS.fullmatch(length_hdr): 

434 raise InvalidHeader(CONTENT_LENGTH) 

435 

436 return int(length_hdr) 

437 

438 length = get_content_length() 

439 # do not support old websocket spec 

440 if SEC_WEBSOCKET_KEY1 in msg.headers: 

441 raise InvalidHeader(SEC_WEBSOCKET_KEY1) 

442 

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

444 

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

446 # code is only present on responses 

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

448 

449 assert self.protocol is not None 

450 # calculate payload 

451 # https://www.rfc-editor.org/info/rfc9112/#name-message-body-length 

452 # https://www.rfc-editor.org/info/rfc9110/#section-9.3.1-6 

453 # EMPTY_BODY_METHODS should only apply to responses. 

454 # self.method is None on request parser. 

455 empty_body = code in EMPTY_BODY_STATUS_CODES or bool( 

456 self.method and self.method in EMPTY_BODY_METHODS 

457 ) 

458 if not empty_body and ( 

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

460 ): 

461 payload = StreamReader( 

462 self.protocol, 

463 timer=self.timer, 

464 loop=loop, 

465 limit=self._limit, 

466 ) 

467 payload_parser = HttpPayloadParser( 

468 payload, 

469 length=length, 

470 chunked=msg.chunked, 

471 method=method, 

472 compression=msg.compression, 

473 code=self.code, 

474 response_with_body=self.response_with_body, 

475 auto_decompress=self._auto_decompress, 

476 lax=self.lax, 

477 headers_parser=self._headers_parser, 

478 max_line_size=self.max_line_size, 

479 max_field_size=self.max_field_size, 

480 max_trailers=max_trailers, 

481 limit=self._limit, 

482 ) 

483 if not payload_parser.done: 

484 self._payload_parser = payload_parser 

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

486 # Defer any requested upgrade until the 

487 # complete request has been read. 

488 self._pending_upgrade = upgraded 

489 elif method == METH_CONNECT: 

490 assert isinstance(msg, RawRequestMessage) 

491 payload = StreamReader( 

492 self.protocol, 

493 timer=self.timer, 

494 loop=loop, 

495 limit=self._limit, 

496 ) 

497 self._upgraded = True 

498 self._payload_parser = HttpPayloadParser( 

499 payload, 

500 method=msg.method, 

501 compression=msg.compression, 

502 auto_decompress=self._auto_decompress, 

503 lax=self.lax, 

504 headers_parser=self._headers_parser, 

505 max_line_size=self.max_line_size, 

506 max_field_size=self.max_field_size, 

507 max_trailers=max_trailers, 

508 limit=self._limit, 

509 ) 

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

511 payload = StreamReader( 

512 self.protocol, 

513 timer=self.timer, 

514 loop=loop, 

515 limit=self._limit, 

516 ) 

517 payload_parser = HttpPayloadParser( 

518 payload, 

519 length=length, 

520 chunked=msg.chunked, 

521 method=method, 

522 compression=msg.compression, 

523 code=self.code, 

524 response_with_body=self.response_with_body, 

525 auto_decompress=self._auto_decompress, 

526 lax=self.lax, 

527 headers_parser=self._headers_parser, 

528 max_line_size=self.max_line_size, 

529 max_field_size=self.max_field_size, 

530 max_trailers=max_trailers, 

531 limit=self._limit, 

532 ) 

533 if not payload_parser.done: 

534 self._payload_parser = payload_parser 

535 elif upgraded: 

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

537 # the upgraded protocol immediately. 

538 self._upgraded = True 

539 payload = EMPTY_PAYLOAD 

540 else: 

541 payload = EMPTY_PAYLOAD 

542 

543 messages.append((msg, payload)) 

544 if self._max_msg_queue_size: 

545 self._msg_in_flight += 1 

546 should_close = msg.should_close 

547 else: 

548 self._tail = data[start_pos:] 

549 # A bare LF here means CRLF was required: 

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

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

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

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

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

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

556 data = EMPTY 

557 break 

558 

559 # no parser, just store 

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

561 assert not self._lines 

562 break 

563 

564 # feed payload 

565 else: 

566 assert not self._lines 

567 assert self._payload_parser is not None 

568 try: 

569 payload_state, data = self._payload_parser.feed_data( 

570 data[start_pos:], SEP 

571 ) 

572 except Exception as underlying_exc: 

573 reraised_exc: BaseException = underlying_exc 

574 if self.payload_exception is not None: 

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

576 

577 set_exception( 

578 self._payload_parser.payload, 

579 reraised_exc, 

580 underlying_exc, 

581 ) 

582 

583 payload_state = PayloadState.PAYLOAD_COMPLETE 

584 data = b"" 

585 if isinstance( 

586 underlying_exc, (InvalidHeader, TransferEncodingError) 

587 ): 

588 raise 

589 

590 self._payload_has_more_data = ( 

591 payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT 

592 ) 

593 

594 if payload_state is not PayloadState.PAYLOAD_COMPLETE: 

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

596 # until the reader buffer is freed up. 

597 break 

598 

599 start_pos = 0 

600 data_len = len(data) 

601 self._payload_parser = None 

602 if self._pending_upgrade: 

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

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

605 self._upgraded = True 

606 self._pending_upgrade = False 

607 

608 if data and start_pos < data_len: 

609 data = data[start_pos:] 

610 else: 

611 data = EMPTY 

612 

613 return messages, self._upgraded, data 

614 

615 def parse_headers( 

616 self, lines: list[bytes] 

617 ) -> tuple[ 

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

619 ]: 

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

621 

622 Line continuations are supported. Returns list of header name 

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

624 """ 

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

626 close_conn = None 

627 encoding = None 

628 upgrade = False 

629 chunked = False 

630 

631 # keep-alive and protocol switching 

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

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

634 if conn_values: 

635 conn_tokens = { 

636 token.lower() 

637 for conn_value in conn_values 

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

639 if token and token.isascii() 

640 } 

641 

642 if "close" in conn_tokens: 

643 close_conn = True 

644 elif "keep-alive" in conn_tokens: 

645 close_conn = False 

646 

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

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

649 upgrade = True 

650 

651 # encoding 

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

653 if enc.isascii(): # Must be checked before .lower() 

654 enc = enc.lower() 

655 if enc in {"gzip", "deflate", "br", "zstd"}: 

656 encoding = enc 

657 

658 # chunking 

659 te = headers.get(hdrs.TRANSFER_ENCODING) 

660 if te is not None: 

661 if self._is_chunked_te(te): 

662 chunked = True 

663 

664 if hdrs.CONTENT_LENGTH in headers: 

665 raise BadHttpMessage( 

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

667 ) 

668 

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

670 

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

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

673 

674 :param bool val: new state. 

675 """ 

676 self._upgraded = val 

677 

678 

679class HttpRequestParser(HttpParser[RawRequestMessage]): 

680 """Read request status line. 

681 

682 Exception .http_exceptions.BadStatusLine 

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

684 Returns RawRequestMessage. 

685 """ 

686 

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

688 # request line 

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

690 try: 

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

692 except ValueError: 

693 raise BadHttpMethod(line) from None 

694 

695 # method 

696 if not TOKENRE.fullmatch(method): 

697 raise BadHttpMethod(method) 

698 method = method.upper() 

699 

700 # https://www.rfc-editor.org/rfc/rfc9112#section-3.2-4 

701 if _TARGET_FORBIDDEN_CTL_RE.search(path): 

702 raise InvalidURLError( 

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

704 ) 

705 

706 # version 

707 match = VERSRE.fullmatch(version) 

708 if match is None: 

709 raise BadStatusLine(line) 

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

711 

712 if method == "CONNECT": 

713 # authority-form, 

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

715 try: 

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

717 host = url.raw_host 

718 except ValueError: 

719 # Duplicated so mypy understands that url must be defined below. 

720 raise InvalidURLError( 

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

722 ) 

723 if host is None: 

724 raise InvalidURLError( 

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

726 ) 

727 elif path.startswith("/"): 

728 # origin-form, 

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

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

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

732 

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

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

735 # NOTE: HTTP Request-Line input producing different 

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

737 url = URL.build( 

738 path=path_part, 

739 query_string=qs_part, 

740 fragment=url_fragment, 

741 encoded=True, 

742 ) 

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

744 # asterisk-form, 

745 url = URL(path, encoded=True) 

746 else: 

747 # absolute-form for proxy maybe, 

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

749 if not _has_authority(path): 

750 raise InvalidURLError( 

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

752 ) 

753 try: 

754 url = URL(path, encoded=True) 

755 host = url.raw_host 

756 except ValueError: 

757 # Duplicated so mypy understands that url must be defined below. 

758 raise InvalidURLError( 

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

760 ) 

761 # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4 

762 if host is None: 

763 raise InvalidURLError( 

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

765 ) 

766 

767 # read headers 

768 ( 

769 headers, 

770 raw_headers, 

771 close, 

772 compression, 

773 upgrade, 

774 chunked, 

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

776 

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

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

779 

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

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

782 close = True 

783 else: # HTTP 1.1 must ask to close. 

784 close = False 

785 

786 return RawRequestMessage( 

787 method, 

788 path, 

789 version_o, 

790 headers, 

791 raw_headers, 

792 close, 

793 compression, 

794 upgrade, 

795 chunked, 

796 url, 

797 ) 

798 

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

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

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

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

803 return True 

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

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

806 

807 

808class HttpResponseParser(HttpParser[RawResponseMessage]): 

809 """Read response status line and headers. 

810 

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

812 Returns RawResponseMessage. 

813 """ 

814 

815 protocol: "ResponseHandler" 

816 

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

818 lax = not DEBUG 

819 

820 def feed_data( 

821 self, 

822 data: bytes, 

823 SEP: _SEP | None = None, 

824 *args: Any, 

825 **kwargs: Any, 

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

827 if SEP is None: 

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

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

830 

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

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

833 try: 

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

835 except ValueError: 

836 raise BadStatusLine(line) from None 

837 

838 try: 

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

840 except ValueError: 

841 status = status.strip() 

842 reason = "" 

843 

844 # version 

845 match = VERSRE.fullmatch(version) 

846 if match is None: 

847 raise BadStatusLine(line) 

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

849 

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

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

852 raise BadStatusLine(line) 

853 status_i = int(status) 

854 

855 # read headers 

856 ( 

857 headers, 

858 raw_headers, 

859 close, 

860 compression, 

861 upgrade, 

862 chunked, 

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

864 

865 if close is None: 

866 if version_o <= HttpVersion10: 

867 close = True 

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

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

870 close = False 

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

872 close = False 

873 else: 

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

875 close = True 

876 

877 return RawResponseMessage( 

878 version_o, 

879 status_i, 

880 reason.strip(), 

881 headers, 

882 raw_headers, 

883 close, 

884 compression, 

885 upgrade, 

886 chunked, 

887 ) 

888 

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

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

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

892 

893 

894class HttpPayloadParser: 

895 def __init__( 

896 self, 

897 payload: StreamReader, 

898 length: int | None = None, 

899 chunked: bool = False, 

900 compression: str | None = None, 

901 code: int | None = None, 

902 method: str | None = None, 

903 response_with_body: bool = True, 

904 auto_decompress: bool = True, 

905 lax: bool = False, 

906 *, 

907 headers_parser: HeadersParser, 

908 max_line_size: int = 8190, 

909 max_field_size: int = 8190, 

910 max_trailers: int = 128, 

911 limit: int = DEFAULT_CHUNK_SIZE, 

912 ) -> None: 

913 self._length = 0 

914 self._paused = False 

915 self._type = ParseState.PARSE_UNTIL_EOF 

916 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

917 self._chunk_size = 0 

918 self._chunk_tail = b"" 

919 self._auto_decompress = auto_decompress 

920 self._lax = lax 

921 self._headers_parser = headers_parser 

922 self._max_line_size = max_line_size 

923 self._max_field_size = max_field_size 

924 self._max_trailers = max_trailers 

925 self._more_data_available = False 

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

927 self.done = False 

928 self._eof_pending = False 

929 

930 # payload decompression wrapper 

931 if response_with_body and compression and self._auto_decompress: 

932 real_payload: StreamReader | DeflateBuffer = DeflateBuffer( 

933 payload, compression, max_decompress_size=limit 

934 ) 

935 else: 

936 real_payload = payload 

937 

938 # payload parser 

939 if not response_with_body: 

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

941 self._type = ParseState.PARSE_NONE 

942 real_payload.feed_eof() 

943 self.done = True 

944 elif chunked: 

945 self._type = ParseState.PARSE_CHUNKED 

946 elif length is not None: 

947 self._type = ParseState.PARSE_LENGTH 

948 self._length = length 

949 self._length_expected = length 

950 if self._length == 0: 

951 real_payload.feed_eof() 

952 self.done = True 

953 

954 self.payload = real_payload 

955 

956 def pause_reading(self) -> None: 

957 self._paused = True 

958 

959 def feed_eof(self) -> None: 

960 if self._type == ParseState.PARSE_UNTIL_EOF: 

961 self._eof_pending = True 

962 while self._more_data_available: 

963 if self._paused: 

964 self._paused = False 

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

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

967 self.payload.feed_eof() 

968 self.done = True 

969 self._eof_pending = False 

970 elif self._type == ParseState.PARSE_LENGTH: 

971 if self._length: 

972 received = self._length_expected - self._length 

973 raise ContentLengthError( 

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

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

976 ) 

977 # Body has already been received, but parser paused. 

978 while self._more_data_available: 

979 if self._paused: 

980 self._paused = False 

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

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

983 self.payload.feed_eof() 

984 self.done = True 

985 elif self._type == ParseState.PARSE_CHUNKED: 

986 raise TransferEncodingError( 

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

988 ) 

989 

990 def feed_data( 

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

992 ) -> tuple[PayloadState, bytes]: 

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

994 

995 Return: 

996 PayloadState - The current state of payload processing. 

997 This function may be called with empty bytes after returning 

998 PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause. 

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

1000 next message/payload, b"" otherwise. 

1001 """ 

1002 # Read specified amount of bytes 

1003 if self._type == ParseState.PARSE_LENGTH: 

1004 if self._chunk_tail: 

1005 chunk = self._chunk_tail + chunk 

1006 self._chunk_tail = b"" 

1007 

1008 required = self._length 

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

1010 self._more_data_available = self.payload.feed_data( 

1011 chunk[:required], required 

1012 ) 

1013 while self._more_data_available: 

1014 if self._paused: 

1015 self._paused = False 

1016 self._chunk_tail = chunk[required:] 

1017 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

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

1019 

1020 if self._length == 0: 

1021 self.payload.feed_eof() 

1022 return PayloadState.PAYLOAD_COMPLETE, chunk[required:] 

1023 # Chunked transfer encoding parser 

1024 elif self._type == ParseState.PARSE_CHUNKED: 

1025 if self._chunk_tail: 

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

1027 if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK: 

1028 max_line_length = self._max_line_size 

1029 if self._chunk == ChunkState.PARSE_TRAILERS: 

1030 max_line_length = self._max_field_size 

1031 if len(self._chunk_tail) > max_line_length: 

1032 raise LineTooLong( 

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

1034 ) 

1035 

1036 chunk = self._chunk_tail + chunk 

1037 self._chunk_tail = b"" 

1038 

1039 while chunk or self._more_data_available: 

1040 # read next chunk size 

1041 if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: 

1042 pos = chunk.find(SEP) 

1043 if pos >= 0: 

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

1045 # _max_field_size separately in PARSE_TRAILERS below. 

1046 if pos > self._max_line_size: 

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

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

1049 if i >= 0: 

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

1051 # Verify no LF in the chunk-extension 

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

1053 exc = TransferEncodingError( 

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

1055 ) 

1056 set_exception(self.payload, exc) 

1057 raise exc 

1058 else: 

1059 size_b = chunk[:pos] 

1060 

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

1062 size_b = size_b.strip() 

1063 

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

1065 exc = TransferEncodingError( 

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

1067 ) 

1068 set_exception(self.payload, exc) 

1069 raise exc 

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

1071 

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

1073 if size == 0: # eof marker 

1074 self._chunk = ChunkState.PARSE_TRAILERS 

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

1076 chunk = chunk[1:] 

1077 else: 

1078 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK 

1079 self._chunk_size = size 

1080 self.payload.begin_http_chunk_receiving() 

1081 else: 

1082 if b"\n" in chunk: 

1083 exc = TransferEncodingError( 

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

1085 ) 

1086 set_exception(self.payload, exc) 

1087 raise exc 

1088 self._chunk_tail = chunk 

1089 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1090 

1091 # read chunk and feed buffer 

1092 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: 

1093 if self._paused: 

1094 self._paused = False 

1095 self._chunk_tail = chunk 

1096 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

1097 

1098 required = self._chunk_size 

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

1100 self._more_data_available = self.payload.feed_data( 

1101 chunk[:required], required 

1102 ) 

1103 chunk = chunk[required:] 

1104 

1105 if self._more_data_available: 

1106 continue 

1107 

1108 if self._chunk_size: 

1109 self._paused = False 

1110 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1111 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF 

1112 self.payload.end_http_chunk_receiving() 

1113 

1114 # toss the CRLF at the end of the chunk 

1115 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: 

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

1117 chunk = chunk[1:] 

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

1119 chunk = chunk[len(SEP) :] 

1120 self._chunk = ChunkState.PARSE_CHUNKED_SIZE 

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

1122 exc = TransferEncodingError( 

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

1124 ) 

1125 set_exception(self.payload, exc) 

1126 raise exc 

1127 else: 

1128 self._chunk_tail = chunk 

1129 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1130 

1131 if self._chunk == ChunkState.PARSE_TRAILERS: 

1132 pos = chunk.find(SEP) 

1133 if pos < 0: # No line found 

1134 if b"\n" in chunk: 

1135 exc = TransferEncodingError( 

1136 "Bad trailer line ending, expected CRLF" 

1137 ) 

1138 set_exception(self.payload, exc) 

1139 raise exc 

1140 self._chunk_tail = chunk 

1141 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1142 

1143 line = chunk[:pos] 

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

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

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

1147 

1148 if len(line) > self._max_field_size: 

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

1150 

1151 self._trailer_lines.append(line) 

1152 

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

1154 raise BadHttpMessage("Too many trailers received") 

1155 

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

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

1158 # Headers and trailers are defined the same way, 

1159 # so we reuse the HeadersParser here. 

1160 try: 

1161 trailers, raw_trailers = self._headers_parser.parse_headers( 

1162 self._trailer_lines 

1163 ) 

1164 finally: 

1165 self._trailer_lines.clear() 

1166 self.payload.feed_eof() 

1167 return PayloadState.PAYLOAD_COMPLETE, chunk 

1168 

1169 # Read all bytes until eof 

1170 elif self._type == ParseState.PARSE_UNTIL_EOF: 

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

1172 while self._more_data_available: 

1173 if self._paused: 

1174 self._paused = False 

1175 return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" 

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

1177 

1178 if self._eof_pending: 

1179 self.payload.feed_eof() 

1180 self.done = True 

1181 self._eof_pending = False 

1182 return PayloadState.PAYLOAD_COMPLETE, b"" 

1183 

1184 return PayloadState.PAYLOAD_NEEDS_INPUT, b"" 

1185 

1186 

1187class DeflateBuffer: 

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

1189 

1190 decompressor: Any 

1191 

1192 def __init__( 

1193 self, 

1194 out: StreamReader, 

1195 encoding: str | None, 

1196 max_decompress_size: int = DEFAULT_CHUNK_SIZE, 

1197 ) -> None: 

1198 self.out = out 

1199 self.size = 0 

1200 out.total_compressed_bytes = self.size 

1201 self.encoding = encoding 

1202 self._started_decoding = False 

1203 

1204 self.decompressor: BrotliDecompressor | ZLibDecompressor | ZSTDDecompressor 

1205 if encoding == "br": 

1206 if not HAS_BROTLI: # pragma: no cover 

1207 raise ContentEncodingError( 

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

1209 "Please install `Brotli`" 

1210 ) 

1211 self.decompressor = BrotliDecompressor() 

1212 elif encoding == "zstd": 

1213 if not HAS_ZSTD: 

1214 raise ContentEncodingError( 

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

1216 "Please install `backports.zstd`" 

1217 ) 

1218 self.decompressor = ZSTDDecompressor() 

1219 else: 

1220 self.decompressor = ZLibDecompressor(encoding=encoding) 

1221 

1222 self._max_decompress_size = max_decompress_size 

1223 

1224 def set_exception( 

1225 self, 

1226 exc: BaseException, 

1227 exc_cause: BaseException = _EXC_SENTINEL, 

1228 ) -> None: 

1229 set_exception(self.out, exc, exc_cause) 

1230 

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

1232 self.size += size 

1233 self.out.total_compressed_bytes = self.size 

1234 

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

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

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

1238 if not self._started_decoding and chunk: 

1239 # RFC1950 

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

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

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

1243 # Change the decoder to decompress incorrectly compressed data 

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

1245 self.decompressor = ZLibDecompressor( 

1246 encoding=self.encoding, suppress_deflate_header=True 

1247 ) 

1248 self._started_decoding = True 

1249 

1250 low_water = self.out._low_water 

1251 max_length = ( 

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

1253 ) 

1254 try: 

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

1256 except Exception: 

1257 raise ContentEncodingError( 

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

1259 ) 

1260 

1261 if chunk: 

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

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

1264 

1265 def feed_eof(self) -> None: 

1266 chunk = self.decompressor.flush() 

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

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

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

1270 assert not chunk 

1271 

1272 if self.size > 0: 

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

1274 raise ContentEncodingError("deflate") 

1275 

1276 self.out.feed_eof() 

1277 

1278 def begin_http_chunk_receiving(self) -> None: 

1279 self.out.begin_http_chunk_receiving() 

1280 

1281 def end_http_chunk_receiving(self) -> None: 

1282 self.out.end_http_chunk_receiving() 

1283 

1284 

1285HttpRequestParserPy = HttpRequestParser 

1286HttpResponseParserPy = HttpResponseParser 

1287RawRequestMessagePy = RawRequestMessage 

1288RawResponseMessagePy = RawResponseMessage 

1289 

1290try: 

1291 if not NO_EXTENSIONS: 

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

1293 HttpRequestParser, 

1294 HttpResponseParser, 

1295 RawRequestMessage, 

1296 RawResponseMessage, 

1297 ) 

1298 

1299 HttpRequestParserC = HttpRequestParser 

1300 HttpResponseParserC = HttpResponseParser 

1301 RawRequestMessageC = RawRequestMessage 

1302 RawResponseMessageC = RawResponseMessage 

1303except ImportError: # pragma: no cover 

1304 pass