Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/http_parser.py: 90%
473 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
1import abc
2import asyncio
3import re
4import string
5from contextlib import suppress
6from enum import IntEnum
7from typing import (
8 Final,
9 Generic,
10 List,
11 NamedTuple,
12 Optional,
13 Pattern,
14 Set,
15 Tuple,
16 Type,
17 TypeVar,
18 Union,
19)
21from multidict import CIMultiDict, CIMultiDictProxy, istr
22from yarl import URL
24from . import hdrs
25from .base_protocol import BaseProtocol
26from .compression_utils import HAS_BROTLI, BrotliDecompressor, ZLibDecompressor
27from .helpers import NO_EXTENSIONS, BaseTimerContext
28from .http_exceptions import (
29 BadHttpMessage,
30 BadStatusLine,
31 ContentEncodingError,
32 ContentLengthError,
33 InvalidHeader,
34 LineTooLong,
35 TransferEncodingError,
36)
37from .http_writer import HttpVersion, HttpVersion10
38from .log import internal_logger
39from .streams import EMPTY_PAYLOAD, StreamReader
40from .typedefs import RawHeaders
42__all__ = (
43 "HeadersParser",
44 "HttpParser",
45 "HttpRequestParser",
46 "HttpResponseParser",
47 "RawRequestMessage",
48 "RawResponseMessage",
49)
51ASCIISET: Final[Set[str]] = set(string.printable)
53# See https://tools.ietf.org/html/rfc7230#section-3.1.1
54# and https://tools.ietf.org/html/rfc7230#appendix-B
55#
56# method = token
57# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
58# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
59# token = 1*tchar
60METHRE: Final[Pattern[str]] = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
61VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d+).(\d+)")
62HDRRE: Final[Pattern[bytes]] = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]")
65class RawRequestMessage(NamedTuple):
66 method: str
67 path: str
68 version: HttpVersion
69 headers: CIMultiDictProxy[str]
70 raw_headers: RawHeaders
71 should_close: bool
72 compression: Optional[str]
73 upgrade: bool
74 chunked: bool
75 url: URL
78class RawResponseMessage(NamedTuple):
79 version: HttpVersion
80 code: int
81 reason: str
82 headers: CIMultiDictProxy[str]
83 raw_headers: RawHeaders
84 should_close: bool
85 compression: Optional[str]
86 upgrade: bool
87 chunked: bool
90_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage)
93class ParseState(IntEnum):
94 PARSE_NONE = 0
95 PARSE_LENGTH = 1
96 PARSE_CHUNKED = 2
97 PARSE_UNTIL_EOF = 3
100class ChunkState(IntEnum):
101 PARSE_CHUNKED_SIZE = 0
102 PARSE_CHUNKED_CHUNK = 1
103 PARSE_CHUNKED_CHUNK_EOF = 2
104 PARSE_MAYBE_TRAILERS = 3
105 PARSE_TRAILERS = 4
108class HeadersParser:
109 def __init__(
110 self,
111 max_line_size: int = 8190,
112 max_field_size: int = 8190,
113 ) -> None:
114 self.max_line_size = max_line_size
115 self.max_field_size = max_field_size
117 def parse_headers(
118 self, lines: List[bytes]
119 ) -> Tuple["CIMultiDictProxy[str]", RawHeaders]:
120 headers: CIMultiDict[str] = CIMultiDict()
121 raw_headers = []
123 lines_idx = 1
124 line = lines[1]
125 line_count = len(lines)
127 while line:
128 # Parse initial header name : value pair.
129 try:
130 bname, bvalue = line.split(b":", 1)
131 except ValueError:
132 raise InvalidHeader(line) from None
134 bname = bname.strip(b" \t")
135 bvalue = bvalue.lstrip()
136 if HDRRE.search(bname):
137 raise InvalidHeader(bname)
138 if len(bname) > self.max_field_size:
139 raise LineTooLong(
140 "request header name {}".format(
141 bname.decode("utf8", "backslashreplace")
142 ),
143 str(self.max_field_size),
144 str(len(bname)),
145 )
147 header_length = len(bvalue)
149 # next line
150 lines_idx += 1
151 line = lines[lines_idx]
153 # consume continuation lines
154 continuation = line and line[0] in (32, 9) # (' ', '\t')
156 if continuation:
157 bvalue_lst = [bvalue]
158 while continuation:
159 header_length += len(line)
160 if header_length > self.max_field_size:
161 raise LineTooLong(
162 "request header field {}".format(
163 bname.decode("utf8", "backslashreplace")
164 ),
165 str(self.max_field_size),
166 str(header_length),
167 )
168 bvalue_lst.append(line)
170 # next line
171 lines_idx += 1
172 if lines_idx < line_count:
173 line = lines[lines_idx]
174 if line:
175 continuation = line[0] in (32, 9) # (' ', '\t')
176 else:
177 line = b""
178 break
179 bvalue = b"".join(bvalue_lst)
180 else:
181 if header_length > self.max_field_size:
182 raise LineTooLong(
183 "request header field {}".format(
184 bname.decode("utf8", "backslashreplace")
185 ),
186 str(self.max_field_size),
187 str(header_length),
188 )
190 bvalue = bvalue.strip()
191 name = bname.decode("utf-8", "surrogateescape")
192 value = bvalue.decode("utf-8", "surrogateescape")
194 headers.add(name, value)
195 raw_headers.append((bname, bvalue))
197 return (CIMultiDictProxy(headers), tuple(raw_headers))
200class HttpParser(abc.ABC, Generic[_MsgT]):
201 def __init__(
202 self,
203 protocol: BaseProtocol,
204 loop: asyncio.AbstractEventLoop,
205 limit: int,
206 max_line_size: int = 8190,
207 max_field_size: int = 8190,
208 timer: Optional[BaseTimerContext] = None,
209 code: Optional[int] = None,
210 method: Optional[str] = None,
211 readall: bool = False,
212 payload_exception: Optional[Type[BaseException]] = None,
213 response_with_body: bool = True,
214 read_until_eof: bool = False,
215 auto_decompress: bool = True,
216 ) -> None:
217 self.protocol = protocol
218 self.loop = loop
219 self.max_line_size = max_line_size
220 self.max_field_size = max_field_size
221 self.timer = timer
222 self.code = code
223 self.method = method
224 self.readall = readall
225 self.payload_exception = payload_exception
226 self.response_with_body = response_with_body
227 self.read_until_eof = read_until_eof
229 self._lines: List[bytes] = []
230 self._tail = b""
231 self._upgraded = False
232 self._payload = None
233 self._payload_parser: Optional[HttpPayloadParser] = None
234 self._auto_decompress = auto_decompress
235 self._limit = limit
236 self._headers_parser = HeadersParser(max_line_size, max_field_size)
238 @abc.abstractmethod
239 def parse_message(self, lines: List[bytes]) -> _MsgT:
240 pass
242 def feed_eof(self) -> Optional[_MsgT]:
243 if self._payload_parser is not None:
244 self._payload_parser.feed_eof()
245 self._payload_parser = None
246 else:
247 # try to extract partial message
248 if self._tail:
249 self._lines.append(self._tail)
251 if self._lines:
252 if self._lines[-1] != "\r\n":
253 self._lines.append(b"")
254 with suppress(Exception):
255 return self.parse_message(self._lines)
256 return None
258 def feed_data(
259 self,
260 data: bytes,
261 SEP: bytes = b"\r\n",
262 EMPTY: bytes = b"",
263 CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH,
264 METH_CONNECT: str = hdrs.METH_CONNECT,
265 SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1,
266 ) -> Tuple[List[Tuple[_MsgT, StreamReader]], bool, bytes]:
267 messages = []
269 if self._tail:
270 data, self._tail = self._tail + data, b""
272 data_len = len(data)
273 start_pos = 0
274 loop = self.loop
276 while start_pos < data_len:
277 # read HTTP message (request/response line + headers), \r\n\r\n
278 # and split by lines
279 if self._payload_parser is None and not self._upgraded:
280 pos = data.find(SEP, start_pos)
281 # consume \r\n
282 if pos == start_pos and not self._lines:
283 start_pos = pos + 2
284 continue
286 if pos >= start_pos:
287 # line found
288 self._lines.append(data[start_pos:pos])
289 start_pos = pos + 2
291 # \r\n\r\n found
292 if self._lines[-1] == EMPTY:
293 try:
294 msg: _MsgT = self.parse_message(self._lines)
295 finally:
296 self._lines.clear()
298 def get_content_length() -> Optional[int]:
299 # payload length
300 length_hdr = msg.headers.get(CONTENT_LENGTH)
301 if length_hdr is None:
302 return None
304 try:
305 length = int(length_hdr)
306 except ValueError:
307 raise InvalidHeader(CONTENT_LENGTH)
309 if length < 0:
310 raise InvalidHeader(CONTENT_LENGTH)
312 return length
314 length = get_content_length()
315 # do not support old websocket spec
316 if SEC_WEBSOCKET_KEY1 in msg.headers:
317 raise InvalidHeader(SEC_WEBSOCKET_KEY1)
319 self._upgraded = msg.upgrade
321 method = getattr(msg, "method", self.method)
323 assert self.protocol is not None
324 # calculate payload
325 if (
326 (length is not None and length > 0)
327 or msg.chunked
328 and not msg.upgrade
329 ):
330 payload = StreamReader(
331 self.protocol,
332 timer=self.timer,
333 loop=loop,
334 limit=self._limit,
335 )
336 payload_parser = HttpPayloadParser(
337 payload,
338 length=length,
339 chunked=msg.chunked,
340 method=method,
341 compression=msg.compression,
342 code=self.code,
343 readall=self.readall,
344 response_with_body=self.response_with_body,
345 auto_decompress=self._auto_decompress,
346 )
347 if not payload_parser.done:
348 self._payload_parser = payload_parser
349 elif method == METH_CONNECT:
350 assert isinstance(msg, RawRequestMessage)
351 payload = StreamReader(
352 self.protocol,
353 timer=self.timer,
354 loop=loop,
355 limit=self._limit,
356 )
357 self._upgraded = True
358 self._payload_parser = HttpPayloadParser(
359 payload,
360 method=msg.method,
361 compression=msg.compression,
362 readall=True,
363 auto_decompress=self._auto_decompress,
364 )
365 else:
366 if (
367 getattr(msg, "code", 100) >= 199
368 and length is None
369 and self.read_until_eof
370 ):
371 payload = StreamReader(
372 self.protocol,
373 timer=self.timer,
374 loop=loop,
375 limit=self._limit,
376 )
377 payload_parser = HttpPayloadParser(
378 payload,
379 length=length,
380 chunked=msg.chunked,
381 method=method,
382 compression=msg.compression,
383 code=self.code,
384 readall=True,
385 response_with_body=self.response_with_body,
386 auto_decompress=self._auto_decompress,
387 )
388 if not payload_parser.done:
389 self._payload_parser = payload_parser
390 else:
391 payload = EMPTY_PAYLOAD
393 messages.append((msg, payload))
394 else:
395 self._tail = data[start_pos:]
396 data = EMPTY
397 break
399 # no parser, just store
400 elif self._payload_parser is None and self._upgraded:
401 assert not self._lines
402 break
404 # feed payload
405 elif data and start_pos < data_len:
406 assert not self._lines
407 assert self._payload_parser is not None
408 try:
409 eof, data = self._payload_parser.feed_data(data[start_pos:])
410 except BaseException as exc:
411 if self.payload_exception is not None:
412 self._payload_parser.payload.set_exception(
413 self.payload_exception(str(exc))
414 )
415 else:
416 self._payload_parser.payload.set_exception(exc)
418 eof = True
419 data = b""
421 if eof:
422 start_pos = 0
423 data_len = len(data)
424 self._payload_parser = None
425 continue
426 else:
427 break
429 if data and start_pos < data_len:
430 data = data[start_pos:]
431 else:
432 data = EMPTY
434 return messages, self._upgraded, data
436 def parse_headers(
437 self, lines: List[bytes]
438 ) -> Tuple[
439 "CIMultiDictProxy[str]", RawHeaders, Optional[bool], Optional[str], bool, bool
440 ]:
441 """Parses RFC 5322 headers from a stream.
443 Line continuations are supported. Returns list of header name
444 and value pairs. Header name is in upper case.
445 """
446 headers, raw_headers = self._headers_parser.parse_headers(lines)
447 close_conn = None
448 encoding = None
449 upgrade = False
450 chunked = False
452 # keep-alive
453 conn = headers.get(hdrs.CONNECTION)
454 if conn:
455 v = conn.lower()
456 if v == "close":
457 close_conn = True
458 elif v == "keep-alive":
459 close_conn = False
460 elif v == "upgrade":
461 upgrade = True
463 # encoding
464 enc = headers.get(hdrs.CONTENT_ENCODING)
465 if enc:
466 enc = enc.lower()
467 if enc in ("gzip", "deflate", "br"):
468 encoding = enc
470 # chunking
471 te = headers.get(hdrs.TRANSFER_ENCODING)
472 if te is not None:
473 if "chunked" == te.lower():
474 chunked = True
475 else:
476 raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
478 if hdrs.CONTENT_LENGTH in headers:
479 raise BadHttpMessage(
480 "Content-Length can't be present with Transfer-Encoding",
481 )
483 return (headers, raw_headers, close_conn, encoding, upgrade, chunked)
485 def set_upgraded(self, val: bool) -> None:
486 """Set connection upgraded (to websocket) mode.
488 :param bool val: new state.
489 """
490 self._upgraded = val
493class HttpRequestParser(HttpParser[RawRequestMessage]):
494 """Read request status line.
496 Exception .http_exceptions.BadStatusLine
497 could be raised in case of any errors in status line.
498 Returns RawRequestMessage.
499 """
501 def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
502 # request line
503 line = lines[0].decode("utf-8", "surrogateescape")
504 try:
505 method, path, version = line.split(None, 2)
506 except ValueError:
507 raise BadStatusLine(line) from None
509 if len(path) > self.max_line_size:
510 raise LineTooLong(
511 "Status line is too long", str(self.max_line_size), str(len(path))
512 )
514 # method
515 if not METHRE.match(method):
516 raise BadStatusLine(method)
518 # version
519 try:
520 if version.startswith("HTTP/"):
521 n1, n2 = version[5:].split(".", 1)
522 version_o = HttpVersion(int(n1), int(n2))
523 else:
524 raise BadStatusLine(version)
525 except Exception:
526 raise BadStatusLine(version)
528 if method == "CONNECT":
529 # authority-form,
530 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
531 url = URL.build(authority=path, encoded=True)
532 elif path.startswith("/"):
533 # origin-form,
534 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
535 path_part, _hash_separator, url_fragment = path.partition("#")
536 path_part, _question_mark_separator, qs_part = path_part.partition("?")
538 # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based
539 # NOTE: parser does, otherwise it results into the same
540 # NOTE: HTTP Request-Line input producing different
541 # NOTE: `yarl.URL()` objects
542 url = URL.build(
543 path=path_part,
544 query_string=qs_part,
545 fragment=url_fragment,
546 encoded=True,
547 )
548 else:
549 # absolute-form for proxy maybe,
550 # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
551 url = URL(path, encoded=True)
553 # read headers
554 (
555 headers,
556 raw_headers,
557 close,
558 compression,
559 upgrade,
560 chunked,
561 ) = self.parse_headers(lines)
563 if close is None: # then the headers weren't set in the request
564 if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
565 close = True
566 else: # HTTP 1.1 must ask to close.
567 close = False
569 return RawRequestMessage(
570 method,
571 path,
572 version_o,
573 headers,
574 raw_headers,
575 close,
576 compression,
577 upgrade,
578 chunked,
579 url,
580 )
583class HttpResponseParser(HttpParser[RawResponseMessage]):
584 """Read response status line and headers.
586 BadStatusLine could be raised in case of any errors in status line.
587 Returns RawResponseMessage.
588 """
590 def parse_message(self, lines: List[bytes]) -> RawResponseMessage:
591 line = lines[0].decode("utf-8", "surrogateescape")
592 try:
593 version, status = line.split(None, 1)
594 except ValueError:
595 raise BadStatusLine(line) from None
597 try:
598 status, reason = status.split(None, 1)
599 except ValueError:
600 reason = ""
602 if len(reason) > self.max_line_size:
603 raise LineTooLong(
604 "Status line is too long", str(self.max_line_size), str(len(reason))
605 )
607 # version
608 match = VERSRE.match(version)
609 if match is None:
610 raise BadStatusLine(line)
611 version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
613 # The status code is a three-digit number
614 try:
615 status_i = int(status)
616 except ValueError:
617 raise BadStatusLine(line) from None
619 if status_i > 999:
620 raise BadStatusLine(line)
622 # read headers
623 (
624 headers,
625 raw_headers,
626 close,
627 compression,
628 upgrade,
629 chunked,
630 ) = self.parse_headers(lines)
632 if close is None:
633 close = version_o <= HttpVersion10
635 return RawResponseMessage(
636 version_o,
637 status_i,
638 reason.strip(),
639 headers,
640 raw_headers,
641 close,
642 compression,
643 upgrade,
644 chunked,
645 )
648class HttpPayloadParser:
649 def __init__(
650 self,
651 payload: StreamReader,
652 length: Optional[int] = None,
653 chunked: bool = False,
654 compression: Optional[str] = None,
655 code: Optional[int] = None,
656 method: Optional[str] = None,
657 readall: bool = False,
658 response_with_body: bool = True,
659 auto_decompress: bool = True,
660 ) -> None:
661 self._length = 0
662 self._type = ParseState.PARSE_NONE
663 self._chunk = ChunkState.PARSE_CHUNKED_SIZE
664 self._chunk_size = 0
665 self._chunk_tail = b""
666 self._auto_decompress = auto_decompress
667 self.done = False
669 # payload decompression wrapper
670 if response_with_body and compression and self._auto_decompress:
671 real_payload: Union[StreamReader, DeflateBuffer] = DeflateBuffer(
672 payload, compression
673 )
674 else:
675 real_payload = payload
677 # payload parser
678 if not response_with_body:
679 # don't parse payload if it's not expected to be received
680 self._type = ParseState.PARSE_NONE
681 real_payload.feed_eof()
682 self.done = True
684 elif chunked:
685 self._type = ParseState.PARSE_CHUNKED
686 elif length is not None:
687 self._type = ParseState.PARSE_LENGTH
688 self._length = length
689 if self._length == 0:
690 real_payload.feed_eof()
691 self.done = True
692 else:
693 if readall and code != 204:
694 self._type = ParseState.PARSE_UNTIL_EOF
695 elif method in ("PUT", "POST"):
696 internal_logger.warning( # pragma: no cover
697 "Content-Length or Transfer-Encoding header is required"
698 )
699 self._type = ParseState.PARSE_NONE
700 real_payload.feed_eof()
701 self.done = True
703 self.payload = real_payload
705 def feed_eof(self) -> None:
706 if self._type == ParseState.PARSE_UNTIL_EOF:
707 self.payload.feed_eof()
708 elif self._type == ParseState.PARSE_LENGTH:
709 raise ContentLengthError(
710 "Not enough data for satisfy content length header."
711 )
712 elif self._type == ParseState.PARSE_CHUNKED:
713 raise TransferEncodingError(
714 "Not enough data for satisfy transfer length header."
715 )
717 def feed_data(
718 self, chunk: bytes, SEP: bytes = b"\r\n", CHUNK_EXT: bytes = b";"
719 ) -> Tuple[bool, bytes]:
720 # Read specified amount of bytes
721 if self._type == ParseState.PARSE_LENGTH:
722 required = self._length
723 chunk_len = len(chunk)
725 if required >= chunk_len:
726 self._length = required - chunk_len
727 self.payload.feed_data(chunk, chunk_len)
728 if self._length == 0:
729 self.payload.feed_eof()
730 return True, b""
731 else:
732 self._length = 0
733 self.payload.feed_data(chunk[:required], required)
734 self.payload.feed_eof()
735 return True, chunk[required:]
737 # Chunked transfer encoding parser
738 elif self._type == ParseState.PARSE_CHUNKED:
739 if self._chunk_tail:
740 chunk = self._chunk_tail + chunk
741 self._chunk_tail = b""
743 while chunk:
744 # read next chunk size
745 if self._chunk == ChunkState.PARSE_CHUNKED_SIZE:
746 pos = chunk.find(SEP)
747 if pos >= 0:
748 i = chunk.find(CHUNK_EXT, 0, pos)
749 if i >= 0:
750 size_b = chunk[:i] # strip chunk-extensions
751 else:
752 size_b = chunk[:pos]
754 try:
755 size = int(bytes(size_b), 16)
756 except ValueError:
757 exc = TransferEncodingError(
758 chunk[:pos].decode("ascii", "surrogateescape")
759 )
760 self.payload.set_exception(exc)
761 raise exc from None
763 chunk = chunk[pos + 2 :]
764 if size == 0: # eof marker
765 self._chunk = ChunkState.PARSE_MAYBE_TRAILERS
766 else:
767 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK
768 self._chunk_size = size
769 self.payload.begin_http_chunk_receiving()
770 else:
771 self._chunk_tail = chunk
772 return False, b""
774 # read chunk and feed buffer
775 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK:
776 required = self._chunk_size
777 chunk_len = len(chunk)
779 if required > chunk_len:
780 self._chunk_size = required - chunk_len
781 self.payload.feed_data(chunk, chunk_len)
782 return False, b""
783 else:
784 self._chunk_size = 0
785 self.payload.feed_data(chunk[:required], required)
786 chunk = chunk[required:]
787 self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
788 self.payload.end_http_chunk_receiving()
790 # toss the CRLF at the end of the chunk
791 if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF:
792 if chunk[:2] == SEP:
793 chunk = chunk[2:]
794 self._chunk = ChunkState.PARSE_CHUNKED_SIZE
795 else:
796 self._chunk_tail = chunk
797 return False, b""
799 # if stream does not contain trailer, after 0\r\n
800 # we should get another \r\n otherwise
801 # trailers needs to be skiped until \r\n\r\n
802 if self._chunk == ChunkState.PARSE_MAYBE_TRAILERS:
803 head = chunk[:2]
804 if head == SEP:
805 # end of stream
806 self.payload.feed_eof()
807 return True, chunk[2:]
808 # Both CR and LF, or only LF may not be received yet. It is
809 # expected that CRLF or LF will be shown at the very first
810 # byte next time, otherwise trailers should come. The last
811 # CRLF which marks the end of response might not be
812 # contained in the same TCP segment which delivered the
813 # size indicator.
814 if not head:
815 return False, b""
816 if head == SEP[:1]:
817 self._chunk_tail = head
818 return False, b""
819 self._chunk = ChunkState.PARSE_TRAILERS
821 # read and discard trailer up to the CRLF terminator
822 if self._chunk == ChunkState.PARSE_TRAILERS:
823 pos = chunk.find(SEP)
824 if pos >= 0:
825 chunk = chunk[pos + 2 :]
826 self._chunk = ChunkState.PARSE_MAYBE_TRAILERS
827 else:
828 self._chunk_tail = chunk
829 return False, b""
831 # Read all bytes until eof
832 elif self._type == ParseState.PARSE_UNTIL_EOF:
833 self.payload.feed_data(chunk, len(chunk))
835 return False, b""
838class DeflateBuffer:
839 """DeflateStream decompress stream and feed data into specified stream."""
841 def __init__(self, out: StreamReader, encoding: Optional[str]) -> None:
842 self.out = out
843 self.size = 0
844 self.encoding = encoding
845 self._started_decoding = False
847 self.decompressor: Union[BrotliDecompressor, ZLibDecompressor]
848 if encoding == "br":
849 if not HAS_BROTLI: # pragma: no cover
850 raise ContentEncodingError(
851 "Can not decode content-encoding: brotli (br). "
852 "Please install `Brotli`"
853 )
854 self.decompressor = BrotliDecompressor()
855 else:
856 self.decompressor = ZLibDecompressor(encoding=encoding)
858 def set_exception(self, exc: BaseException) -> None:
859 self.out.set_exception(exc)
861 def feed_data(self, chunk: bytes, size: int) -> None:
862 if not size:
863 return
865 self.size += size
867 # RFC1950
868 # bits 0..3 = CM = 0b1000 = 8 = "deflate"
869 # bits 4..7 = CINFO = 1..7 = windows size.
870 if (
871 not self._started_decoding
872 and self.encoding == "deflate"
873 and chunk[0] & 0xF != 8
874 ):
875 # Change the decoder to decompress incorrectly compressed data
876 # Actually we should issue a warning about non-RFC-compliant data.
877 self.decompressor = ZLibDecompressor(
878 encoding=self.encoding, suppress_deflate_header=True
879 )
881 try:
882 chunk = self.decompressor.decompress_sync(chunk)
883 except Exception:
884 raise ContentEncodingError(
885 "Can not decode content-encoding: %s" % self.encoding
886 )
888 self._started_decoding = True
890 if chunk:
891 self.out.feed_data(chunk, len(chunk))
893 def feed_eof(self) -> None:
894 chunk = self.decompressor.flush()
896 if chunk or self.size > 0:
897 self.out.feed_data(chunk, len(chunk))
898 # decompressor is not brotli unless encoding is "br"
899 if self.encoding == "deflate" and not self.decompressor.eof: # type: ignore[union-attr]
900 raise ContentEncodingError("deflate")
902 self.out.feed_eof()
904 def begin_http_chunk_receiving(self) -> None:
905 self.out.begin_http_chunk_receiving()
907 def end_http_chunk_receiving(self) -> None:
908 self.out.end_http_chunk_receiving()
911HttpRequestParserPy = HttpRequestParser
912HttpResponseParserPy = HttpResponseParser
913RawRequestMessagePy = RawRequestMessage
914RawResponseMessagePy = RawResponseMessage
916try:
917 if not NO_EXTENSIONS:
918 from ._http_parser import ( # type: ignore[import,no-redef]
919 HttpRequestParser,
920 HttpResponseParser,
921 RawRequestMessage,
922 RawResponseMessage,
923 )
925 HttpRequestParserC = HttpRequestParser
926 HttpResponseParserC = HttpResponseParser
927 RawRequestMessageC = RawRequestMessage
928 RawResponseMessageC = RawResponseMessage
929except ImportError: # pragma: no cover
930 pass