Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/h11/_readers.py: 24%
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
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
1# Code to read HTTP data
2#
3# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
4# either:
5# 1) consumes some of it and returns an Event
6# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
7# and it might raise a LocalProtocolError, so simpler just to always use
8# this)
9# 3) returns None, meaning "I need more data"
10#
11# If they have a .read_eof attribute, then this will be called if an EOF is
12# received -- but this is optional. Either way, the actual ConnectionClosed
13# event will be generated afterwards.
14#
15# READERS is a dict describing how to pick a reader. It maps states to either:
16# - a reader
17# - or, for body readers, a dict of per-framing reader factories
19import re
20from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
22from ._abnf import chunk_header, header_field, request_line, status_line
23from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
24from ._receivebuffer import ReceiveBuffer
25from ._state import (
26 CLIENT,
27 CLOSED,
28 DONE,
29 IDLE,
30 MUST_CLOSE,
31 SEND_BODY,
32 SEND_RESPONSE,
33 SERVER,
34)
35from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
37__all__ = ["READERS"]
39header_field_re = re.compile(header_field.encode("ascii"))
40obs_fold_re = re.compile(rb"[ \t]+")
43def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
44 it = iter(lines)
45 last: Optional[bytes] = None
46 for line in it:
47 match = obs_fold_re.match(line)
48 if match:
49 if last is None:
50 raise LocalProtocolError("continuation line at start of headers")
51 if not isinstance(last, bytearray):
52 # Cast to a mutable type, avoiding copy on append to ensure O(n) time
53 last = bytearray(last)
54 last += b" "
55 last += line[match.end() :]
56 else:
57 if last is not None:
58 yield last
59 last = line
60 if last is not None:
61 yield last
64def _decode_header_lines(
65 lines: Iterable[bytes],
66) -> Iterable[Tuple[bytes, bytes]]:
67 for line in _obsolete_line_fold(lines):
68 matches = validate(header_field_re, line, "illegal header line: {!r}", line)
69 yield (matches["field_name"], matches["field_value"])
72request_line_re = re.compile(request_line.encode("ascii"))
75def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
76 lines = buf.maybe_extract_lines()
77 if lines is None:
78 if buf.is_next_line_obviously_invalid_request_line():
79 raise LocalProtocolError("illegal request line")
80 return None
81 if not lines:
82 raise LocalProtocolError("no request line received")
83 matches = validate(
84 request_line_re, lines[0], "illegal request line: {!r}", lines[0]
85 )
86 return Request(
87 headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
88 )
91status_line_re = re.compile(status_line.encode("ascii"))
94def maybe_read_from_SEND_RESPONSE_server(
95 buf: ReceiveBuffer,
96) -> Union[InformationalResponse, Response, None]:
97 lines = buf.maybe_extract_lines()
98 if lines is None:
99 if buf.is_next_line_obviously_invalid_request_line():
100 raise LocalProtocolError("illegal request line")
101 return None
102 if not lines:
103 raise LocalProtocolError("no response line received")
104 matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
105 http_version = (
106 b"1.1" if matches["http_version"] is None else matches["http_version"]
107 )
108 reason = b"" if matches["reason"] is None else matches["reason"]
109 status_code = int(matches["status_code"])
110 class_: Union[Type[InformationalResponse], Type[Response]] = (
111 InformationalResponse if status_code < 200 else Response
112 )
113 return class_(
114 headers=list(_decode_header_lines(lines[1:])),
115 _parsed=True,
116 status_code=status_code,
117 reason=reason,
118 http_version=http_version,
119 )
122class ContentLengthReader:
123 def __init__(self, length: int) -> None:
124 self._length = length
125 self._remaining = length
127 def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
128 if self._remaining == 0:
129 return EndOfMessage()
130 data = buf.maybe_extract_at_most(self._remaining)
131 if data is None:
132 return None
133 self._remaining -= len(data)
134 return Data(data=data)
136 def read_eof(self) -> NoReturn:
137 raise RemoteProtocolError(
138 "peer closed connection without sending complete message body "
139 "(received {} bytes, expected {})".format(
140 self._length - self._remaining, self._length
141 )
142 )
145chunk_header_re = re.compile(chunk_header.encode("ascii"))
148class ChunkedReader:
149 def __init__(self) -> None:
150 self._bytes_in_chunk = 0
151 # After reading a chunk, we have to throw away the trailing \r\n; if
152 # this is >0 then we discard that many bytes before resuming regular
153 # de-chunkification.
154 self._bytes_to_discard = 0
155 self._reading_trailer = False
157 def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
158 if self._reading_trailer:
159 lines = buf.maybe_extract_lines()
160 if lines is None:
161 return None
162 return EndOfMessage(headers=list(_decode_header_lines(lines)))
163 if self._bytes_to_discard > 0:
164 data = buf.maybe_extract_at_most(self._bytes_to_discard)
165 if data is None:
166 return None
167 self._bytes_to_discard -= len(data)
168 if self._bytes_to_discard > 0:
169 return None
170 # else, fall through and read some more
171 assert self._bytes_to_discard == 0
172 if self._bytes_in_chunk == 0:
173 # We need to refill our chunk count
174 chunk_header = buf.maybe_extract_next_line()
175 if chunk_header is None:
176 return None
177 matches = validate(
178 chunk_header_re,
179 chunk_header,
180 "illegal chunk header: {!r}",
181 chunk_header,
182 )
183 # XX FIXME: we discard chunk extensions. Does anyone care?
184 self._bytes_in_chunk = int(matches["chunk_size"], base=16)
185 if self._bytes_in_chunk == 0:
186 self._reading_trailer = True
187 return self(buf)
188 chunk_start = True
189 else:
190 chunk_start = False
191 assert self._bytes_in_chunk > 0
192 data = buf.maybe_extract_at_most(self._bytes_in_chunk)
193 if data is None:
194 return None
195 self._bytes_in_chunk -= len(data)
196 if self._bytes_in_chunk == 0:
197 self._bytes_to_discard = 2
198 chunk_end = True
199 else:
200 chunk_end = False
201 return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
203 def read_eof(self) -> NoReturn:
204 raise RemoteProtocolError(
205 "peer closed connection without sending complete message body "
206 "(incomplete chunked read)"
207 )
210class Http10Reader:
211 def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
212 data = buf.maybe_extract_at_most(999999999)
213 if data is None:
214 return None
215 return Data(data=data)
217 def read_eof(self) -> EndOfMessage:
218 return EndOfMessage()
221def expect_nothing(buf: ReceiveBuffer) -> None:
222 if buf:
223 raise LocalProtocolError("Got data when expecting EOF")
224 return None
227ReadersType = Dict[
228 Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
229 Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
230]
232READERS: ReadersType = {
233 (CLIENT, IDLE): maybe_read_from_IDLE_client,
234 (SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
235 (SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
236 (CLIENT, DONE): expect_nothing,
237 (CLIENT, MUST_CLOSE): expect_nothing,
238 (CLIENT, CLOSED): expect_nothing,
239 (SERVER, DONE): expect_nothing,
240 (SERVER, MUST_CLOSE): expect_nothing,
241 (SERVER, CLOSED): expect_nothing,
242 SEND_BODY: {
243 "chunked": ChunkedReader,
244 "content-length": ContentLengthReader,
245 "http/1.0": Http10Reader,
246 },
247}