boring-http 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ # generated from the text of RFC 7541 appendices A and B, so regenerate rather than edit
2
+ from typing import Final
3
+
4
+ STATIC_TABLE: Final[tuple[tuple[bytes, bytes], ...]] = (
5
+ (b":authority", b""),
6
+ (b":method", b"GET"),
7
+ (b":method", b"POST"),
8
+ (b":path", b"/"),
9
+ (b":path", b"/index.html"),
10
+ (b":scheme", b"http"),
11
+ (b":scheme", b"https"),
12
+ (b":status", b"200"),
13
+ (b":status", b"204"),
14
+ (b":status", b"206"),
15
+ (b":status", b"304"),
16
+ (b":status", b"400"),
17
+ (b":status", b"404"),
18
+ (b":status", b"500"),
19
+ (b"accept-charset", b""),
20
+ (b"accept-encoding", b"gzip, deflate"),
21
+ (b"accept-language", b""),
22
+ (b"accept-ranges", b""),
23
+ (b"accept", b""),
24
+ (b"access-control-allow-origin", b""),
25
+ (b"age", b""),
26
+ (b"allow", b""),
27
+ (b"authorization", b""),
28
+ (b"cache-control", b""),
29
+ (b"content-disposition", b""),
30
+ (b"content-encoding", b""),
31
+ (b"content-language", b""),
32
+ (b"content-length", b""),
33
+ (b"content-location", b""),
34
+ (b"content-range", b""),
35
+ (b"content-type", b""),
36
+ (b"cookie", b""),
37
+ (b"date", b""),
38
+ (b"etag", b""),
39
+ (b"expect", b""),
40
+ (b"expires", b""),
41
+ (b"from", b""),
42
+ (b"host", b""),
43
+ (b"if-match", b""),
44
+ (b"if-modified-since", b""),
45
+ (b"if-none-match", b""),
46
+ (b"if-range", b""),
47
+ (b"if-unmodified-since", b""),
48
+ (b"last-modified", b""),
49
+ (b"link", b""),
50
+ (b"location", b""),
51
+ (b"max-forwards", b""),
52
+ (b"proxy-authenticate", b""),
53
+ (b"proxy-authorization", b""),
54
+ (b"range", b""),
55
+ (b"referer", b""),
56
+ (b"refresh", b""),
57
+ (b"retry-after", b""),
58
+ (b"server", b""),
59
+ (b"set-cookie", b""),
60
+ (b"strict-transport-security", b""),
61
+ (b"transfer-encoding", b""),
62
+ (b"user-agent", b""),
63
+ (b"vary", b""),
64
+ (b"via", b""),
65
+ (b"www-authenticate", b""),
66
+ )
67
+
68
+ HUFFMAN_CODES: Final[tuple[tuple[int, int], ...]] = (
69
+ (0x1FF8, 13),
70
+ (0x7FFFD8, 23),
71
+ (0xFFFFFE2, 28),
72
+ (0xFFFFFE3, 28),
73
+ (0xFFFFFE4, 28),
74
+ (0xFFFFFE5, 28),
75
+ (0xFFFFFE6, 28),
76
+ (0xFFFFFE7, 28),
77
+ (0xFFFFFE8, 28),
78
+ (0xFFFFEA, 24),
79
+ (0x3FFFFFFC, 30),
80
+ (0xFFFFFE9, 28),
81
+ (0xFFFFFEA, 28),
82
+ (0x3FFFFFFD, 30),
83
+ (0xFFFFFEB, 28),
84
+ (0xFFFFFEC, 28),
85
+ (0xFFFFFED, 28),
86
+ (0xFFFFFEE, 28),
87
+ (0xFFFFFEF, 28),
88
+ (0xFFFFFF0, 28),
89
+ (0xFFFFFF1, 28),
90
+ (0xFFFFFF2, 28),
91
+ (0x3FFFFFFE, 30),
92
+ (0xFFFFFF3, 28),
93
+ (0xFFFFFF4, 28),
94
+ (0xFFFFFF5, 28),
95
+ (0xFFFFFF6, 28),
96
+ (0xFFFFFF7, 28),
97
+ (0xFFFFFF8, 28),
98
+ (0xFFFFFF9, 28),
99
+ (0xFFFFFFA, 28),
100
+ (0xFFFFFFB, 28),
101
+ (0x14, 6),
102
+ (0x3F8, 10),
103
+ (0x3F9, 10),
104
+ (0xFFA, 12),
105
+ (0x1FF9, 13),
106
+ (0x15, 6),
107
+ (0xF8, 8),
108
+ (0x7FA, 11),
109
+ (0x3FA, 10),
110
+ (0x3FB, 10),
111
+ (0xF9, 8),
112
+ (0x7FB, 11),
113
+ (0xFA, 8),
114
+ (0x16, 6),
115
+ (0x17, 6),
116
+ (0x18, 6),
117
+ (0x0, 5),
118
+ (0x1, 5),
119
+ (0x2, 5),
120
+ (0x19, 6),
121
+ (0x1A, 6),
122
+ (0x1B, 6),
123
+ (0x1C, 6),
124
+ (0x1D, 6),
125
+ (0x1E, 6),
126
+ (0x1F, 6),
127
+ (0x5C, 7),
128
+ (0xFB, 8),
129
+ (0x7FFC, 15),
130
+ (0x20, 6),
131
+ (0xFFB, 12),
132
+ (0x3FC, 10),
133
+ (0x1FFA, 13),
134
+ (0x21, 6),
135
+ (0x5D, 7),
136
+ (0x5E, 7),
137
+ (0x5F, 7),
138
+ (0x60, 7),
139
+ (0x61, 7),
140
+ (0x62, 7),
141
+ (0x63, 7),
142
+ (0x64, 7),
143
+ (0x65, 7),
144
+ (0x66, 7),
145
+ (0x67, 7),
146
+ (0x68, 7),
147
+ (0x69, 7),
148
+ (0x6A, 7),
149
+ (0x6B, 7),
150
+ (0x6C, 7),
151
+ (0x6D, 7),
152
+ (0x6E, 7),
153
+ (0x6F, 7),
154
+ (0x70, 7),
155
+ (0x71, 7),
156
+ (0x72, 7),
157
+ (0xFC, 8),
158
+ (0x73, 7),
159
+ (0xFD, 8),
160
+ (0x1FFB, 13),
161
+ (0x7FFF0, 19),
162
+ (0x1FFC, 13),
163
+ (0x3FFC, 14),
164
+ (0x22, 6),
165
+ (0x7FFD, 15),
166
+ (0x3, 5),
167
+ (0x23, 6),
168
+ (0x4, 5),
169
+ (0x24, 6),
170
+ (0x5, 5),
171
+ (0x25, 6),
172
+ (0x26, 6),
173
+ (0x27, 6),
174
+ (0x6, 5),
175
+ (0x74, 7),
176
+ (0x75, 7),
177
+ (0x28, 6),
178
+ (0x29, 6),
179
+ (0x2A, 6),
180
+ (0x7, 5),
181
+ (0x2B, 6),
182
+ (0x76, 7),
183
+ (0x2C, 6),
184
+ (0x8, 5),
185
+ (0x9, 5),
186
+ (0x2D, 6),
187
+ (0x77, 7),
188
+ (0x78, 7),
189
+ (0x79, 7),
190
+ (0x7A, 7),
191
+ (0x7B, 7),
192
+ (0x7FFE, 15),
193
+ (0x7FC, 11),
194
+ (0x3FFD, 14),
195
+ (0x1FFD, 13),
196
+ (0xFFFFFFC, 28),
197
+ (0xFFFE6, 20),
198
+ (0x3FFFD2, 22),
199
+ (0xFFFE7, 20),
200
+ (0xFFFE8, 20),
201
+ (0x3FFFD3, 22),
202
+ (0x3FFFD4, 22),
203
+ (0x3FFFD5, 22),
204
+ (0x7FFFD9, 23),
205
+ (0x3FFFD6, 22),
206
+ (0x7FFFDA, 23),
207
+ (0x7FFFDB, 23),
208
+ (0x7FFFDC, 23),
209
+ (0x7FFFDD, 23),
210
+ (0x7FFFDE, 23),
211
+ (0xFFFFEB, 24),
212
+ (0x7FFFDF, 23),
213
+ (0xFFFFEC, 24),
214
+ (0xFFFFED, 24),
215
+ (0x3FFFD7, 22),
216
+ (0x7FFFE0, 23),
217
+ (0xFFFFEE, 24),
218
+ (0x7FFFE1, 23),
219
+ (0x7FFFE2, 23),
220
+ (0x7FFFE3, 23),
221
+ (0x7FFFE4, 23),
222
+ (0x1FFFDC, 21),
223
+ (0x3FFFD8, 22),
224
+ (0x7FFFE5, 23),
225
+ (0x3FFFD9, 22),
226
+ (0x7FFFE6, 23),
227
+ (0x7FFFE7, 23),
228
+ (0xFFFFEF, 24),
229
+ (0x3FFFDA, 22),
230
+ (0x1FFFDD, 21),
231
+ (0xFFFE9, 20),
232
+ (0x3FFFDB, 22),
233
+ (0x3FFFDC, 22),
234
+ (0x7FFFE8, 23),
235
+ (0x7FFFE9, 23),
236
+ (0x1FFFDE, 21),
237
+ (0x7FFFEA, 23),
238
+ (0x3FFFDD, 22),
239
+ (0x3FFFDE, 22),
240
+ (0xFFFFF0, 24),
241
+ (0x1FFFDF, 21),
242
+ (0x3FFFDF, 22),
243
+ (0x7FFFEB, 23),
244
+ (0x7FFFEC, 23),
245
+ (0x1FFFE0, 21),
246
+ (0x1FFFE1, 21),
247
+ (0x3FFFE0, 22),
248
+ (0x1FFFE2, 21),
249
+ (0x7FFFED, 23),
250
+ (0x3FFFE1, 22),
251
+ (0x7FFFEE, 23),
252
+ (0x7FFFEF, 23),
253
+ (0xFFFEA, 20),
254
+ (0x3FFFE2, 22),
255
+ (0x3FFFE3, 22),
256
+ (0x3FFFE4, 22),
257
+ (0x7FFFF0, 23),
258
+ (0x3FFFE5, 22),
259
+ (0x3FFFE6, 22),
260
+ (0x7FFFF1, 23),
261
+ (0x3FFFFE0, 26),
262
+ (0x3FFFFE1, 26),
263
+ (0xFFFEB, 20),
264
+ (0x7FFF1, 19),
265
+ (0x3FFFE7, 22),
266
+ (0x7FFFF2, 23),
267
+ (0x3FFFE8, 22),
268
+ (0x1FFFFEC, 25),
269
+ (0x3FFFFE2, 26),
270
+ (0x3FFFFE3, 26),
271
+ (0x3FFFFE4, 26),
272
+ (0x7FFFFDE, 27),
273
+ (0x7FFFFDF, 27),
274
+ (0x3FFFFE5, 26),
275
+ (0xFFFFF1, 24),
276
+ (0x1FFFFED, 25),
277
+ (0x7FFF2, 19),
278
+ (0x1FFFE3, 21),
279
+ (0x3FFFFE6, 26),
280
+ (0x7FFFFE0, 27),
281
+ (0x7FFFFE1, 27),
282
+ (0x3FFFFE7, 26),
283
+ (0x7FFFFE2, 27),
284
+ (0xFFFFF2, 24),
285
+ (0x1FFFE4, 21),
286
+ (0x1FFFE5, 21),
287
+ (0x3FFFFE8, 26),
288
+ (0x3FFFFE9, 26),
289
+ (0xFFFFFFD, 28),
290
+ (0x7FFFFE3, 27),
291
+ (0x7FFFFE4, 27),
292
+ (0x7FFFFE5, 27),
293
+ (0xFFFEC, 20),
294
+ (0xFFFFF3, 24),
295
+ (0xFFFED, 20),
296
+ (0x1FFFE6, 21),
297
+ (0x3FFFE9, 22),
298
+ (0x1FFFE7, 21),
299
+ (0x1FFFE8, 21),
300
+ (0x7FFFF3, 23),
301
+ (0x3FFFEA, 22),
302
+ (0x3FFFEB, 22),
303
+ (0x1FFFFEE, 25),
304
+ (0x1FFFFEF, 25),
305
+ (0xFFFFF4, 24),
306
+ (0xFFFFF5, 24),
307
+ (0x3FFFFEA, 26),
308
+ (0x7FFFF4, 23),
309
+ (0x3FFFFEB, 26),
310
+ (0x7FFFFE6, 27),
311
+ (0x3FFFFEC, 26),
312
+ (0x3FFFFED, 26),
313
+ (0x7FFFFE7, 27),
314
+ (0x7FFFFE8, 27),
315
+ (0x7FFFFE9, 27),
316
+ (0x7FFFFEA, 27),
317
+ (0x7FFFFEB, 27),
318
+ (0xFFFFFFE, 28),
319
+ (0x7FFFFEC, 27),
320
+ (0x7FFFFED, 27),
321
+ (0x7FFFFEE, 27),
322
+ (0x7FFFFEF, 27),
323
+ (0x7FFFFF0, 27),
324
+ (0x3FFFFEE, 26),
325
+ (0x3FFFFFFF, 30),
326
+ )
boring_http/http1.py ADDED
@@ -0,0 +1,259 @@
1
+ from collections.abc import AsyncGenerator, Callable
2
+ from typing import Final, final
3
+
4
+ from ssl2 import AsyncTransport
5
+
6
+ from boring_http.errors import ProtocolError, RemoteDisconnected
7
+ from boring_http.message import Headers, HTTPVersion, Request, Response
8
+ from boring_http.timing import Deadline
9
+
10
+ READ_SIZE: Final = 65536
11
+ HEAD_LIMIT: Final = 65536
12
+ LINE_LIMIT: Final = 8192
13
+
14
+
15
+ @final
16
+ class Http1Connection:
17
+ def __init__(
18
+ self,
19
+ transport: AsyncTransport,
20
+ *,
21
+ leftover: bytes = b"",
22
+ absolute_form: bool = False,
23
+ proxy_authorization: str | None = None,
24
+ ) -> None:
25
+ self._transport = transport
26
+ self._buffer = bytearray(leftover)
27
+ self._absolute_form = absolute_form
28
+ self._proxy_authorization = proxy_authorization
29
+ self._keep_alive = True
30
+ self.idle = True
31
+ self.closed = False
32
+ self.reused = False
33
+ self.released: Callable[[], None] = self._nothing
34
+
35
+ @property
36
+ def available(self) -> bool:
37
+ return self.idle and not self.closed
38
+
39
+ @property
40
+ def shared(self) -> bool:
41
+ return False
42
+
43
+ def take(self) -> None:
44
+ self.idle = False
45
+
46
+ async def send(
47
+ self, request: Request, deadline: Deadline, *, decode: bool
48
+ ) -> Response:
49
+ self.idle = False
50
+ headers = request.wire_headers(HTTPVersion.HTTP_1)
51
+ if (
52
+ self._proxy_authorization is not None
53
+ and "proxy-authorization" not in headers
54
+ ):
55
+ headers.add("Proxy-Authorization", self._proxy_authorization)
56
+ target = (
57
+ request.url.origin + request.url.target
58
+ if self._absolute_form
59
+ else request.url.target
60
+ )
61
+ lines = [
62
+ f"{request.method} {target} HTTP/1.1",
63
+ *(f"{name}: {value}" for name, value in headers),
64
+ ]
65
+ try:
66
+ async with deadline.phase("write"):
67
+ await self._transport.write("\r\n".join(lines).encode() + b"\r\n\r\n")
68
+ if request.body is not None:
69
+ await self._write_body(
70
+ request,
71
+ headers.get("transfer-encoding", "").lower() == "chunked",
72
+ )
73
+ status, reason, response_headers, old = await self._read_head(deadline)
74
+ except BaseException:
75
+ await self.close()
76
+ raise
77
+ tokens = {
78
+ token.strip().lower()
79
+ for token in (
80
+ headers.get("connection", "")
81
+ + ","
82
+ + response_headers.get("connection", "")
83
+ ).split(",")
84
+ }
85
+ self._keep_alive = "close" not in tokens and (not old or "keep-alive" in tokens)
86
+ chunked, length = self._framing(request.method, status, response_headers)
87
+ trailers = Headers()
88
+ body = self._body(chunked, length, trailers, deadline)
89
+ return Response(
90
+ status=status,
91
+ reason=reason,
92
+ version=HTTPVersion.HTTP_1,
93
+ headers=response_headers,
94
+ request=request,
95
+ raw=body,
96
+ trailers=trailers,
97
+ decode=decode,
98
+ )
99
+
100
+ def detach(self) -> tuple[AsyncTransport, bytes]:
101
+ self.closed = True
102
+ self.released()
103
+ return self._transport, bytes(self._buffer)
104
+
105
+ async def close(self) -> None:
106
+ if not self.closed:
107
+ self.closed = True
108
+ await self._transport.close()
109
+ self.released()
110
+
111
+ async def _write_body(self, request: Request, chunked: bool) -> None:
112
+ if request.body is None:
113
+ return
114
+ async for chunk in request.body.chunks():
115
+ if chunked:
116
+ await self._transport.write(
117
+ f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n"
118
+ )
119
+ else:
120
+ await self._transport.write(chunk)
121
+ if chunked:
122
+ await self._transport.write(b"0\r\n\r\n")
123
+
124
+ async def _read_head(self, deadline: Deadline) -> tuple[int, str, Headers, bool]:
125
+ while True:
126
+ head = (await self._read_until(b"\r\n\r\n", HEAD_LIMIT, deadline)).decode(
127
+ "latin-1"
128
+ )
129
+ status_line, *lines = head.split("\r\n")
130
+ version, _, rest = status_line.partition(" ")
131
+ code, _, reason = rest.partition(" ")
132
+ if (
133
+ version not in ("HTTP/1.1", "HTTP/1.0")
134
+ or len(code) != 3
135
+ or not (code.isascii() and code.isdigit())
136
+ ):
137
+ raise ProtocolError(f"malformed status line {status_line!r}")
138
+ headers = Headers()
139
+ for line in lines:
140
+ name, separator, value = line.partition(":")
141
+ if not separator or line[:1] in (" ", "\t"):
142
+ raise ProtocolError(f"malformed header line {line!r}")
143
+ try:
144
+ headers.add(name.strip(), value)
145
+ except ValueError as error:
146
+ raise ProtocolError(str(error)) from None
147
+ status = int(code)
148
+ if 100 <= status < 200 and status != 101:
149
+ continue
150
+ return status, reason, headers, version == "HTTP/1.0"
151
+
152
+ @staticmethod
153
+ def _framing(method: str, status: int, headers: Headers) -> tuple[bool, int | None]:
154
+ if method == "HEAD" or status in (101, 204, 304):
155
+ return False, 0
156
+ encoding = headers.get("transfer-encoding")
157
+ if encoding is not None:
158
+ return encoding.split(",")[-1].strip().lower() == "chunked", None
159
+ length = headers.get("content-length")
160
+ if length is None:
161
+ return False, None
162
+ # str.isdigit accepts superscripts and other Unicode digits that int rejects
163
+ if not (length.isascii() and length.isdigit()):
164
+ raise ProtocolError(f"malformed Content-Length {length!r}")
165
+ return False, int(length)
166
+
167
+ async def _body(
168
+ self, chunked: bool, length: int | None, trailers: Headers, deadline: Deadline
169
+ ) -> AsyncGenerator[bytes]:
170
+ try:
171
+ if chunked:
172
+ async for chunk in self._chunked(trailers, deadline):
173
+ yield chunk
174
+ elif length is None:
175
+ self._keep_alive = False
176
+ while piece := await self._read_some(READ_SIZE, deadline):
177
+ yield piece
178
+ else:
179
+ async for piece in self._exactly(length, deadline):
180
+ yield piece
181
+ except BaseException:
182
+ await self.close()
183
+ raise
184
+ if self._keep_alive:
185
+ self.idle = True
186
+ self.released()
187
+ else:
188
+ await self.close()
189
+
190
+ async def _chunked(
191
+ self, trailers: Headers, deadline: Deadline
192
+ ) -> AsyncGenerator[bytes]:
193
+ while True:
194
+ line = await self._read_line(deadline)
195
+ try:
196
+ size = int(line.split(b";", 1)[0].strip(), 16)
197
+ except ValueError:
198
+ raise ProtocolError(f"malformed chunk size {line!r}") from None
199
+ if size == 0:
200
+ break
201
+ async for piece in self._exactly(size, deadline):
202
+ yield piece
203
+ if await self._read_line(deadline):
204
+ raise ProtocolError("chunk data not followed by CRLF")
205
+ while line := await self._read_line(deadline):
206
+ name, separator, value = line.decode("latin-1").partition(":")
207
+ if not separator:
208
+ raise ProtocolError(f"malformed trailer line {line!r}")
209
+ try:
210
+ trailers.add(name.strip(), value)
211
+ except ValueError as error:
212
+ raise ProtocolError(str(error)) from None
213
+
214
+ async def _exactly(self, length: int, deadline: Deadline) -> AsyncGenerator[bytes]:
215
+ remaining = length
216
+ while remaining:
217
+ piece = await self._read_some(remaining, deadline)
218
+ if not piece:
219
+ raise ProtocolError(
220
+ f"the connection closed with {remaining} body bytes missing"
221
+ )
222
+ remaining -= len(piece)
223
+ yield piece
224
+
225
+ async def _read_some(self, limit: int, deadline: Deadline) -> bytes:
226
+ if not self._buffer:
227
+ async with deadline.phase("read"):
228
+ return await self._transport.read(min(limit, READ_SIZE))
229
+ piece = bytes(self._buffer[:limit])
230
+ del self._buffer[:limit]
231
+ return piece
232
+
233
+ async def _read_until(
234
+ self, terminator: bytes, limit: int, deadline: Deadline
235
+ ) -> bytes:
236
+ while (end := self._buffer.find(terminator)) < 0:
237
+ if len(self._buffer) > limit:
238
+ raise ProtocolError(f"the response head exceeds {limit} bytes")
239
+ async with deadline.phase("read"):
240
+ piece = await self._transport.read(READ_SIZE)
241
+ if not piece:
242
+ if self._buffer:
243
+ raise ProtocolError(
244
+ "the connection closed in the middle of the response head"
245
+ )
246
+ raise RemoteDisconnected(
247
+ "the server closed the connection before responding"
248
+ )
249
+ self._buffer += piece
250
+ head = bytes(self._buffer[:end])
251
+ del self._buffer[: end + len(terminator)]
252
+ return head
253
+
254
+ async def _read_line(self, deadline: Deadline) -> bytes:
255
+ return await self._read_until(b"\r\n", LINE_LIMIT, deadline)
256
+
257
+ @staticmethod
258
+ def _nothing() -> None:
259
+ pass