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.
- boring_http/__init__.py +58 -0
- boring_http/client.py +692 -0
- boring_http/codings.py +39 -0
- boring_http/cookies.py +127 -0
- boring_http/errors.py +49 -0
- boring_http/hpack.py +247 -0
- boring_http/hpack_tables.py +326 -0
- boring_http/http1.py +259 -0
- boring_http/http2.py +685 -0
- boring_http/http3.py +551 -0
- boring_http/message.py +598 -0
- boring_http/pool.py +96 -0
- boring_http/py.typed +0 -0
- boring_http/qpack.py +245 -0
- boring_http/qpack_tables.py +104 -0
- boring_http/quic/__init__.py +0 -0
- boring_http/quic/connection.py +1261 -0
- boring_http/quic/packet.py +655 -0
- boring_http/quic/protocol.py +474 -0
- boring_http/quic/recovery.py +303 -0
- boring_http/quic/streams.py +154 -0
- boring_http/timing.py +63 -0
- boring_http/websocket.py +216 -0
- boring_http-1.0.0.dist-info/METADATA +303 -0
- boring_http-1.0.0.dist-info/RECORD +26 -0
- boring_http-1.0.0.dist-info/WHEEL +4 -0
boring_http/http2.py
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import struct
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum, IntFlag
|
|
6
|
+
from typing import Final, final
|
|
7
|
+
|
|
8
|
+
from ssl2 import (
|
|
9
|
+
AkamaiFingerprint,
|
|
10
|
+
AsyncTransport,
|
|
11
|
+
Http2Priority,
|
|
12
|
+
Http2Setting,
|
|
13
|
+
Http2SettingIdentifier,
|
|
14
|
+
PseudoHeader,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from boring_http.errors import (
|
|
18
|
+
ProtocolError,
|
|
19
|
+
RemoteDisconnected,
|
|
20
|
+
StreamRefused,
|
|
21
|
+
StreamResetError,
|
|
22
|
+
)
|
|
23
|
+
from boring_http.hpack import Decoder, Encoder
|
|
24
|
+
from boring_http.message import Headers, HTTPVersion, Request, Response
|
|
25
|
+
from boring_http.timing import Deadline
|
|
26
|
+
|
|
27
|
+
PREFACE: Final = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
28
|
+
DEFAULT_WINDOW: Final = 65535
|
|
29
|
+
DEFAULT_FRAME_SIZE: Final = 16384
|
|
30
|
+
DEFAULT_TABLE_SIZE: Final = 4096
|
|
31
|
+
DEFAULT_HEADER_LIST_SIZE: Final = 262144
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FrameType(IntEnum):
|
|
35
|
+
DATA = 0x0
|
|
36
|
+
HEADERS = 0x1
|
|
37
|
+
PRIORITY = 0x2
|
|
38
|
+
RST_STREAM = 0x3
|
|
39
|
+
SETTINGS = 0x4
|
|
40
|
+
PUSH_PROMISE = 0x5
|
|
41
|
+
PING = 0x6
|
|
42
|
+
GOAWAY = 0x7
|
|
43
|
+
WINDOW_UPDATE = 0x8
|
|
44
|
+
CONTINUATION = 0x9
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Flag(IntFlag):
|
|
48
|
+
END_STREAM = 0x1
|
|
49
|
+
ACK = 0x1
|
|
50
|
+
END_HEADERS = 0x4
|
|
51
|
+
PADDED = 0x8
|
|
52
|
+
PRIORITY = 0x20
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# plain integers for the frame reader: an IntFlag operand routes every `&` through Python-level enum code
|
|
56
|
+
END_STREAM: Final = Flag.END_STREAM.value
|
|
57
|
+
ACK: Final = Flag.ACK.value
|
|
58
|
+
END_HEADERS: Final = Flag.END_HEADERS.value
|
|
59
|
+
PADDED: Final = Flag.PADDED.value
|
|
60
|
+
PRIORITY: Final = Flag.PRIORITY.value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ErrorCode(IntEnum):
|
|
64
|
+
NO_ERROR = 0x0
|
|
65
|
+
PROTOCOL_ERROR = 0x1
|
|
66
|
+
INTERNAL_ERROR = 0x2
|
|
67
|
+
FLOW_CONTROL_ERROR = 0x3
|
|
68
|
+
SETTINGS_TIMEOUT = 0x4
|
|
69
|
+
STREAM_CLOSED = 0x5
|
|
70
|
+
FRAME_SIZE_ERROR = 0x6
|
|
71
|
+
REFUSED_STREAM = 0x7
|
|
72
|
+
CANCEL = 0x8
|
|
73
|
+
COMPRESSION_ERROR = 0x9
|
|
74
|
+
CONNECT_ERROR = 0xA
|
|
75
|
+
ENHANCE_YOUR_CALM = 0xB
|
|
76
|
+
INADEQUATE_SECURITY = 0xC
|
|
77
|
+
HTTP_1_1_REQUIRED = 0xD
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@final
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class Http2Profile:
|
|
83
|
+
fingerprint: AkamaiFingerprint
|
|
84
|
+
headers_priority: Http2Priority | None = None
|
|
85
|
+
|
|
86
|
+
def setting(self, identifier: Http2SettingIdentifier, default: int) -> int:
|
|
87
|
+
return next(
|
|
88
|
+
(
|
|
89
|
+
setting.value
|
|
90
|
+
for setting in self.fingerprint.settings
|
|
91
|
+
if setting.identifier is identifier
|
|
92
|
+
),
|
|
93
|
+
default,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
CHROME: Final = Http2Profile(
|
|
98
|
+
AkamaiFingerprint(
|
|
99
|
+
settings=(
|
|
100
|
+
Http2Setting(Http2SettingIdentifier.HEADER_TABLE_SIZE, 65536),
|
|
101
|
+
Http2Setting(Http2SettingIdentifier.ENABLE_PUSH, 0),
|
|
102
|
+
Http2Setting(Http2SettingIdentifier.INITIAL_WINDOW_SIZE, 6291456),
|
|
103
|
+
Http2Setting(Http2SettingIdentifier.MAX_HEADER_LIST_SIZE, 262144),
|
|
104
|
+
),
|
|
105
|
+
window_update=15663105,
|
|
106
|
+
priorities=(),
|
|
107
|
+
pseudo_header_order=(
|
|
108
|
+
PseudoHeader.METHOD,
|
|
109
|
+
PseudoHeader.AUTHORITY,
|
|
110
|
+
PseudoHeader.SCHEME,
|
|
111
|
+
PseudoHeader.PATH,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
headers_priority=Http2Priority(
|
|
115
|
+
stream_id=0, exclusive=True, dependency=0, weight=256
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@final
|
|
121
|
+
class Http2Stream:
|
|
122
|
+
def __init__(self, identifier: int, send_window: int) -> None:
|
|
123
|
+
self.identifier = identifier
|
|
124
|
+
self.send_window = send_window
|
|
125
|
+
self.window_changed = asyncio.Event()
|
|
126
|
+
self.headers: asyncio.Future[tuple[int, Headers]] = (
|
|
127
|
+
asyncio.get_running_loop().create_future()
|
|
128
|
+
)
|
|
129
|
+
self.data = bytearray()
|
|
130
|
+
self.error: BaseException | None = None
|
|
131
|
+
self.waiter: asyncio.Future[None] | None = None
|
|
132
|
+
self.trailers = Headers()
|
|
133
|
+
self.block = bytearray()
|
|
134
|
+
self.received = 0
|
|
135
|
+
self.finished = False
|
|
136
|
+
|
|
137
|
+
def fail(self, error: BaseException) -> None:
|
|
138
|
+
if not self.headers.done():
|
|
139
|
+
self.headers.set_exception(error)
|
|
140
|
+
self.error = error
|
|
141
|
+
self.finished = True
|
|
142
|
+
self.wake()
|
|
143
|
+
|
|
144
|
+
def wake(self) -> None:
|
|
145
|
+
if self.waiter is not None and not self.waiter.done():
|
|
146
|
+
self.waiter.set_result(None)
|
|
147
|
+
|
|
148
|
+
async def wait(self) -> None:
|
|
149
|
+
self.waiter = asyncio.get_running_loop().create_future()
|
|
150
|
+
try:
|
|
151
|
+
await self.waiter
|
|
152
|
+
finally:
|
|
153
|
+
self.waiter = None
|
|
154
|
+
|
|
155
|
+
def take(self) -> bytes:
|
|
156
|
+
data = bytes(self.data)
|
|
157
|
+
self.data = bytearray()
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@final
|
|
162
|
+
class Http2Connection:
|
|
163
|
+
def __init__(self, transport: AsyncTransport, profile: Http2Profile) -> None:
|
|
164
|
+
self._transport = transport
|
|
165
|
+
self._profile = profile
|
|
166
|
+
self._encoder = Encoder()
|
|
167
|
+
self._decoder = Decoder(
|
|
168
|
+
profile.setting(
|
|
169
|
+
Http2SettingIdentifier.HEADER_TABLE_SIZE, DEFAULT_TABLE_SIZE
|
|
170
|
+
),
|
|
171
|
+
profile.setting(
|
|
172
|
+
Http2SettingIdentifier.MAX_HEADER_LIST_SIZE, DEFAULT_HEADER_LIST_SIZE
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
self._streams: dict[int, Http2Stream] = {}
|
|
176
|
+
self._next_stream = 1
|
|
177
|
+
self._write_lock = asyncio.Lock()
|
|
178
|
+
self._buffer = bytearray()
|
|
179
|
+
self._offset = 0
|
|
180
|
+
self._send_window: int = DEFAULT_WINDOW
|
|
181
|
+
self._window_changed = asyncio.Event()
|
|
182
|
+
self._peer_frame_size = DEFAULT_FRAME_SIZE
|
|
183
|
+
self._peer_window = DEFAULT_WINDOW
|
|
184
|
+
self._peer_max_streams = 2**31
|
|
185
|
+
self._local_window = profile.setting(
|
|
186
|
+
Http2SettingIdentifier.INITIAL_WINDOW_SIZE, DEFAULT_WINDOW
|
|
187
|
+
)
|
|
188
|
+
self._connection_window = DEFAULT_WINDOW + profile.fingerprint.window_update
|
|
189
|
+
self._connection_received = 0
|
|
190
|
+
self._goaway: int | None = None
|
|
191
|
+
self._reader: asyncio.Task[None] | None = None
|
|
192
|
+
self.connect_protocol = False
|
|
193
|
+
self._settings_received: asyncio.Future[None] = (
|
|
194
|
+
asyncio.get_running_loop().create_future()
|
|
195
|
+
)
|
|
196
|
+
self.closed = False
|
|
197
|
+
self.reused = False
|
|
198
|
+
self.released: Callable[[], None] = self._nothing
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def available(self) -> bool:
|
|
202
|
+
return (
|
|
203
|
+
not self.closed
|
|
204
|
+
and self._goaway is None
|
|
205
|
+
and len(self._streams) < self._peer_max_streams
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def shared(self) -> bool:
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
def take(self) -> None:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
async def start(self) -> None:
|
|
216
|
+
fingerprint = self._profile.fingerprint
|
|
217
|
+
settings = b"".join(
|
|
218
|
+
struct.pack("!HI", int(setting.identifier), setting.value)
|
|
219
|
+
for setting in fingerprint.settings
|
|
220
|
+
)
|
|
221
|
+
frames = self._frame(FrameType.SETTINGS, 0, 0, settings)
|
|
222
|
+
if fingerprint.window_update:
|
|
223
|
+
frames += self._frame(
|
|
224
|
+
FrameType.WINDOW_UPDATE,
|
|
225
|
+
0,
|
|
226
|
+
0,
|
|
227
|
+
struct.pack("!I", fingerprint.window_update),
|
|
228
|
+
)
|
|
229
|
+
for priority in fingerprint.priorities:
|
|
230
|
+
frames += self._frame(
|
|
231
|
+
FrameType.PRIORITY,
|
|
232
|
+
0,
|
|
233
|
+
priority.stream_id,
|
|
234
|
+
self._priority_bytes(priority),
|
|
235
|
+
)
|
|
236
|
+
await self._transport.write(PREFACE + frames)
|
|
237
|
+
self._reader = asyncio.create_task(self._read_loop())
|
|
238
|
+
await self._settings_received
|
|
239
|
+
|
|
240
|
+
async def send(
|
|
241
|
+
self, request: Request, deadline: Deadline, *, decode: bool
|
|
242
|
+
) -> Response:
|
|
243
|
+
stream = self._open()
|
|
244
|
+
block = self._encoder.encode(self._fields(request))
|
|
245
|
+
flags = Flag.END_HEADERS | (
|
|
246
|
+
Flag.END_STREAM if request.body is None else Flag(0)
|
|
247
|
+
)
|
|
248
|
+
payload = block
|
|
249
|
+
if self._profile.headers_priority is not None:
|
|
250
|
+
flags |= Flag.PRIORITY
|
|
251
|
+
payload = self._priority_bytes(self._profile.headers_priority) + block
|
|
252
|
+
try:
|
|
253
|
+
async with deadline.phase("write"):
|
|
254
|
+
await self._write_headers(stream.identifier, payload, flags)
|
|
255
|
+
if request.body is not None:
|
|
256
|
+
await self._write_body(stream, request)
|
|
257
|
+
async with deadline.phase("read"):
|
|
258
|
+
status, headers = await stream.headers
|
|
259
|
+
except BaseException:
|
|
260
|
+
await self._reset(stream, ErrorCode.CANCEL)
|
|
261
|
+
raise
|
|
262
|
+
return Response(
|
|
263
|
+
status=status,
|
|
264
|
+
reason="",
|
|
265
|
+
version=HTTPVersion.HTTP_2,
|
|
266
|
+
headers=headers,
|
|
267
|
+
request=request,
|
|
268
|
+
raw=self._body(stream, deadline),
|
|
269
|
+
trailers=stream.trailers,
|
|
270
|
+
decode=decode,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def tunnel(
|
|
274
|
+
self, request: Request, deadline: Deadline
|
|
275
|
+
) -> tuple[int, Headers, AsyncTransport]:
|
|
276
|
+
stream = self._open()
|
|
277
|
+
fields = self._fields(request, protocol=b"websocket")
|
|
278
|
+
try:
|
|
279
|
+
async with deadline.phase("write"):
|
|
280
|
+
await self._write_headers(
|
|
281
|
+
stream.identifier, self._encoder.encode(fields), Flag.END_HEADERS
|
|
282
|
+
)
|
|
283
|
+
async with deadline.phase("read"):
|
|
284
|
+
status, headers = await stream.headers
|
|
285
|
+
except BaseException:
|
|
286
|
+
await self._reset(stream, ErrorCode.CANCEL)
|
|
287
|
+
raise
|
|
288
|
+
return status, headers, Http2StreamTransport(self, stream, deadline)
|
|
289
|
+
|
|
290
|
+
async def write_data(self, stream: Http2Stream, data: bytes, *, end: bool) -> None:
|
|
291
|
+
view = memoryview(data)
|
|
292
|
+
while view or end:
|
|
293
|
+
size = min(
|
|
294
|
+
len(view), self._peer_frame_size, stream.send_window, self._send_window
|
|
295
|
+
)
|
|
296
|
+
if size == 0 and view:
|
|
297
|
+
stream.window_changed.clear()
|
|
298
|
+
self._window_changed.clear()
|
|
299
|
+
_ = await asyncio.wait(
|
|
300
|
+
{
|
|
301
|
+
asyncio.ensure_future(stream.window_changed.wait()),
|
|
302
|
+
asyncio.ensure_future(self._window_changed.wait()),
|
|
303
|
+
},
|
|
304
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
305
|
+
)
|
|
306
|
+
continue
|
|
307
|
+
last = end and size == len(view)
|
|
308
|
+
await self._write(
|
|
309
|
+
self._frame(
|
|
310
|
+
FrameType.DATA,
|
|
311
|
+
Flag.END_STREAM if last else 0,
|
|
312
|
+
stream.identifier,
|
|
313
|
+
bytes(view[:size]),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
stream.send_window -= size
|
|
317
|
+
self._send_window -= size
|
|
318
|
+
view = view[size:]
|
|
319
|
+
if last:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
async def close(self) -> None:
|
|
323
|
+
if self.closed:
|
|
324
|
+
return
|
|
325
|
+
self.closed = True
|
|
326
|
+
if self._reader is not None and self._reader is not asyncio.current_task():
|
|
327
|
+
_ = self._reader.cancel()
|
|
328
|
+
_ = await asyncio.wait({self._reader})
|
|
329
|
+
for stream in list(self._streams.values()):
|
|
330
|
+
stream.fail(RemoteDisconnected("the HTTP/2 connection was closed"))
|
|
331
|
+
self._streams.clear()
|
|
332
|
+
await self._transport.close()
|
|
333
|
+
self.released()
|
|
334
|
+
|
|
335
|
+
def _open(self) -> Http2Stream:
|
|
336
|
+
if not self.available:
|
|
337
|
+
raise RemoteDisconnected("the HTTP/2 connection accepts no new streams")
|
|
338
|
+
stream = Http2Stream(self._next_stream, self._peer_window)
|
|
339
|
+
self._next_stream += 2
|
|
340
|
+
self._streams[stream.identifier] = stream
|
|
341
|
+
return stream
|
|
342
|
+
|
|
343
|
+
def _fields(
|
|
344
|
+
self, request: Request, *, protocol: bytes | None = None
|
|
345
|
+
) -> list[tuple[bytes, bytes]]:
|
|
346
|
+
wire = request.wire_headers(HTTPVersion.HTTP_2)
|
|
347
|
+
authority = request.headers.get("host") or request.url.authority
|
|
348
|
+
scheme = {"ws": "http", "wss": "https"}.get(
|
|
349
|
+
request.url.scheme, request.url.scheme
|
|
350
|
+
)
|
|
351
|
+
pseudo = {
|
|
352
|
+
PseudoHeader.METHOD: (b":method", request.method.encode()),
|
|
353
|
+
PseudoHeader.AUTHORITY: (b":authority", authority.encode()),
|
|
354
|
+
PseudoHeader.SCHEME: (b":scheme", scheme.encode()),
|
|
355
|
+
PseudoHeader.PATH: (b":path", request.url.target.encode()),
|
|
356
|
+
}
|
|
357
|
+
fields = [
|
|
358
|
+
pseudo[header] for header in self._profile.fingerprint.pseudo_header_order
|
|
359
|
+
]
|
|
360
|
+
if protocol is not None:
|
|
361
|
+
fields.insert(1, (b":protocol", protocol))
|
|
362
|
+
fields += [(name.encode(), value.encode()) for name, value in wire]
|
|
363
|
+
return fields
|
|
364
|
+
|
|
365
|
+
async def _write_headers(
|
|
366
|
+
self, identifier: int, payload: bytes, flags: Flag
|
|
367
|
+
) -> None:
|
|
368
|
+
first, rest = payload[: self._peer_frame_size], payload[self._peer_frame_size :]
|
|
369
|
+
frames = self._frame(
|
|
370
|
+
FrameType.HEADERS,
|
|
371
|
+
flags & ~Flag.END_HEADERS if rest else flags,
|
|
372
|
+
identifier,
|
|
373
|
+
first,
|
|
374
|
+
)
|
|
375
|
+
while rest:
|
|
376
|
+
piece, rest = rest[: self._peer_frame_size], rest[self._peer_frame_size :]
|
|
377
|
+
frames += self._frame(
|
|
378
|
+
FrameType.CONTINUATION,
|
|
379
|
+
Flag(0) if rest else Flag.END_HEADERS,
|
|
380
|
+
identifier,
|
|
381
|
+
piece,
|
|
382
|
+
)
|
|
383
|
+
await self._write(frames)
|
|
384
|
+
|
|
385
|
+
async def _write_body(self, stream: Http2Stream, request: Request) -> None:
|
|
386
|
+
if request.body is None:
|
|
387
|
+
return
|
|
388
|
+
remaining = request.body.length
|
|
389
|
+
async for chunk in request.body.chunks():
|
|
390
|
+
if remaining is not None:
|
|
391
|
+
remaining -= len(chunk)
|
|
392
|
+
await self.write_data(stream, chunk, end=remaining == 0)
|
|
393
|
+
if remaining != 0:
|
|
394
|
+
await self.write_data(stream, b"", end=True)
|
|
395
|
+
|
|
396
|
+
async def _body(
|
|
397
|
+
self, stream: Http2Stream, deadline: Deadline
|
|
398
|
+
) -> AsyncGenerator[bytes]:
|
|
399
|
+
try:
|
|
400
|
+
while True:
|
|
401
|
+
if stream.data:
|
|
402
|
+
yield stream.take()
|
|
403
|
+
elif stream.error is not None:
|
|
404
|
+
raise stream.error
|
|
405
|
+
elif stream.finished:
|
|
406
|
+
break
|
|
407
|
+
else:
|
|
408
|
+
async with deadline.phase("read"):
|
|
409
|
+
await stream.wait()
|
|
410
|
+
except BaseException:
|
|
411
|
+
await self._reset(stream, ErrorCode.CANCEL)
|
|
412
|
+
raise
|
|
413
|
+
finally:
|
|
414
|
+
_ = self._streams.pop(stream.identifier, None)
|
|
415
|
+
self.released()
|
|
416
|
+
|
|
417
|
+
async def _reset(self, stream: Http2Stream, code: ErrorCode) -> None:
|
|
418
|
+
if (
|
|
419
|
+
self._streams.pop(stream.identifier, None) is not None
|
|
420
|
+
and not self.closed
|
|
421
|
+
and not stream.finished
|
|
422
|
+
):
|
|
423
|
+
await self._write(
|
|
424
|
+
self._frame(
|
|
425
|
+
FrameType.RST_STREAM, 0, stream.identifier, struct.pack("!I", code)
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
async def _write(self, data: bytes) -> None:
|
|
430
|
+
async with self._write_lock:
|
|
431
|
+
await self._transport.write(data)
|
|
432
|
+
|
|
433
|
+
async def _read_loop(self) -> None:
|
|
434
|
+
try:
|
|
435
|
+
while True:
|
|
436
|
+
head = await self._read_exactly(9)
|
|
437
|
+
length = int.from_bytes(head[:3], "big")
|
|
438
|
+
kind, flags = head[3], head[4]
|
|
439
|
+
identifier = int.from_bytes(head[5:9], "big") & 0x7FFFFFFF
|
|
440
|
+
await self._handle(
|
|
441
|
+
kind, flags, identifier, await self._read_exactly(length)
|
|
442
|
+
)
|
|
443
|
+
except asyncio.CancelledError:
|
|
444
|
+
raise
|
|
445
|
+
except Exception as error: # noqa: BLE001 the reader task is where any failure becomes a closed connection
|
|
446
|
+
self.closed = True
|
|
447
|
+
failure = (
|
|
448
|
+
error
|
|
449
|
+
if isinstance(error, (RemoteDisconnected, ProtocolError))
|
|
450
|
+
else RemoteDisconnected(f"the HTTP/2 connection failed: {error}")
|
|
451
|
+
)
|
|
452
|
+
if not self._settings_received.done():
|
|
453
|
+
self._settings_received.set_exception(failure)
|
|
454
|
+
for stream in list(self._streams.values()):
|
|
455
|
+
stream.fail(failure)
|
|
456
|
+
self._streams.clear()
|
|
457
|
+
await self._transport.close()
|
|
458
|
+
self.released()
|
|
459
|
+
|
|
460
|
+
async def _read_exactly(self, length: int) -> bytes:
|
|
461
|
+
buffer = self._buffer
|
|
462
|
+
while len(buffer) - self._offset < length:
|
|
463
|
+
if self._offset:
|
|
464
|
+
del buffer[: self._offset]
|
|
465
|
+
self._offset = 0
|
|
466
|
+
piece = await self._transport.read(65536)
|
|
467
|
+
if not piece:
|
|
468
|
+
raise RemoteDisconnected("the server closed the HTTP/2 connection")
|
|
469
|
+
buffer += piece
|
|
470
|
+
start = self._offset
|
|
471
|
+
self._offset = start + length
|
|
472
|
+
return bytes(memoryview(buffer)[start : self._offset])
|
|
473
|
+
|
|
474
|
+
async def _handle(
|
|
475
|
+
self, kind: int, flags: int, identifier: int, payload: bytes
|
|
476
|
+
) -> None:
|
|
477
|
+
match kind:
|
|
478
|
+
case FrameType.DATA:
|
|
479
|
+
await self._data(flags, identifier, payload)
|
|
480
|
+
case FrameType.HEADERS | FrameType.CONTINUATION:
|
|
481
|
+
self._headers(FrameType(kind), flags, identifier, payload)
|
|
482
|
+
case FrameType.SETTINGS:
|
|
483
|
+
await self._settings(flags, payload)
|
|
484
|
+
case FrameType.WINDOW_UPDATE:
|
|
485
|
+
self._window_update(identifier, payload)
|
|
486
|
+
case FrameType.RST_STREAM:
|
|
487
|
+
stream = self._streams.pop(identifier, None)
|
|
488
|
+
if stream is not None:
|
|
489
|
+
code = int.from_bytes(payload[:4], "big")
|
|
490
|
+
stream.fail(
|
|
491
|
+
StreamRefused(f"stream {identifier} refused by the server")
|
|
492
|
+
if code == ErrorCode.REFUSED_STREAM
|
|
493
|
+
else StreamResetError(identifier, code)
|
|
494
|
+
)
|
|
495
|
+
case FrameType.PING:
|
|
496
|
+
if not flags & ACK:
|
|
497
|
+
await self._write(self._frame(FrameType.PING, Flag.ACK, 0, payload))
|
|
498
|
+
case FrameType.GOAWAY:
|
|
499
|
+
last = int.from_bytes(payload[:4], "big") & 0x7FFFFFFF
|
|
500
|
+
self._goaway = last
|
|
501
|
+
for stream_id, stream in list(self._streams.items()):
|
|
502
|
+
if stream_id > last:
|
|
503
|
+
stream.fail(
|
|
504
|
+
StreamRefused(
|
|
505
|
+
f"stream {stream_id} refused by GOAWAY ({self._error_name(payload[4:8])})"
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
del self._streams[stream_id]
|
|
509
|
+
case FrameType.PUSH_PROMISE:
|
|
510
|
+
raise ProtocolError(
|
|
511
|
+
"the server pushed a stream although push is disabled"
|
|
512
|
+
)
|
|
513
|
+
case _:
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
async def _data(self, flags: int, identifier: int, payload: bytes) -> None:
|
|
517
|
+
if flags & PADDED:
|
|
518
|
+
payload = payload[1 : len(payload) - payload[0]]
|
|
519
|
+
stream = self._streams.get(identifier)
|
|
520
|
+
if stream is not None:
|
|
521
|
+
stream.data += payload
|
|
522
|
+
if flags & END_STREAM:
|
|
523
|
+
stream.finished = True
|
|
524
|
+
stream.wake()
|
|
525
|
+
stream.received += len(payload)
|
|
526
|
+
if stream.received > self._local_window // 2 and not stream.finished:
|
|
527
|
+
await self._write(
|
|
528
|
+
self._frame(
|
|
529
|
+
FrameType.WINDOW_UPDATE,
|
|
530
|
+
0,
|
|
531
|
+
identifier,
|
|
532
|
+
struct.pack("!I", stream.received),
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
stream.received = 0
|
|
536
|
+
self._connection_received += len(payload)
|
|
537
|
+
if self._connection_received > self._connection_window // 2:
|
|
538
|
+
await self._write(
|
|
539
|
+
self._frame(
|
|
540
|
+
FrameType.WINDOW_UPDATE,
|
|
541
|
+
0,
|
|
542
|
+
0,
|
|
543
|
+
struct.pack("!I", self._connection_received),
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
self._connection_received = 0
|
|
547
|
+
|
|
548
|
+
def _headers(
|
|
549
|
+
self, kind: FrameType, flags: int, identifier: int, payload: bytes
|
|
550
|
+
) -> None:
|
|
551
|
+
stream = self._streams.get(identifier)
|
|
552
|
+
if stream is None:
|
|
553
|
+
return
|
|
554
|
+
if kind is FrameType.HEADERS:
|
|
555
|
+
if flags & PADDED:
|
|
556
|
+
payload = payload[1 : len(payload) - payload[0]]
|
|
557
|
+
if flags & PRIORITY:
|
|
558
|
+
payload = payload[5:]
|
|
559
|
+
stream.block = bytearray()
|
|
560
|
+
stream.block += payload
|
|
561
|
+
if not flags & END_HEADERS:
|
|
562
|
+
return
|
|
563
|
+
decoded = [
|
|
564
|
+
(name.decode("latin-1"), value.decode("latin-1"))
|
|
565
|
+
for name, value in self._decoder.decode(bytes(stream.block))
|
|
566
|
+
]
|
|
567
|
+
stream.block = bytearray()
|
|
568
|
+
pseudo = {name: value for name, value in decoded if name.startswith(":")}
|
|
569
|
+
try:
|
|
570
|
+
regular = Headers(
|
|
571
|
+
[(name, value) for name, value in decoded if not name.startswith(":")]
|
|
572
|
+
)
|
|
573
|
+
except ValueError as error:
|
|
574
|
+
stream.fail(ProtocolError(f"malformed header from the server: {error}"))
|
|
575
|
+
return
|
|
576
|
+
if not stream.headers.done():
|
|
577
|
+
status = pseudo.get(":status", "")
|
|
578
|
+
if not (status.isascii() and status.isdigit()):
|
|
579
|
+
stream.fail(ProtocolError("HTTP/2 response without a numeric :status"))
|
|
580
|
+
return
|
|
581
|
+
if 100 <= int(status) < 200:
|
|
582
|
+
return
|
|
583
|
+
stream.headers.set_result((int(status), regular))
|
|
584
|
+
else:
|
|
585
|
+
for name, value in regular:
|
|
586
|
+
stream.trailers.add(name, value)
|
|
587
|
+
if flags & END_STREAM:
|
|
588
|
+
stream.finished = True
|
|
589
|
+
stream.wake()
|
|
590
|
+
|
|
591
|
+
async def _settings(self, flags: int, payload: bytes) -> None:
|
|
592
|
+
if flags & ACK:
|
|
593
|
+
return
|
|
594
|
+
for offset in range(0, len(payload) - 5, 6):
|
|
595
|
+
identifier, value = (
|
|
596
|
+
int.from_bytes(payload[offset : offset + 2], "big"),
|
|
597
|
+
int.from_bytes(payload[offset + 2 : offset + 6], "big"),
|
|
598
|
+
)
|
|
599
|
+
match identifier:
|
|
600
|
+
case Http2SettingIdentifier.HEADER_TABLE_SIZE:
|
|
601
|
+
self._encoder.set_capacity(value)
|
|
602
|
+
case Http2SettingIdentifier.INITIAL_WINDOW_SIZE:
|
|
603
|
+
for stream in self._streams.values():
|
|
604
|
+
stream.send_window += value - self._peer_window
|
|
605
|
+
stream.window_changed.set()
|
|
606
|
+
self._peer_window = value
|
|
607
|
+
case Http2SettingIdentifier.MAX_FRAME_SIZE:
|
|
608
|
+
self._peer_frame_size = value
|
|
609
|
+
case Http2SettingIdentifier.MAX_CONCURRENT_STREAMS:
|
|
610
|
+
self._peer_max_streams = value
|
|
611
|
+
case Http2SettingIdentifier.ENABLE_CONNECT_PROTOCOL:
|
|
612
|
+
self.connect_protocol = value == 1
|
|
613
|
+
case _:
|
|
614
|
+
pass
|
|
615
|
+
if not self._settings_received.done():
|
|
616
|
+
self._settings_received.set_result(None)
|
|
617
|
+
await self._write(self._frame(FrameType.SETTINGS, Flag.ACK, 0, b""))
|
|
618
|
+
|
|
619
|
+
def _window_update(self, identifier: int, payload: bytes) -> None:
|
|
620
|
+
increment = int.from_bytes(payload[:4], "big") & 0x7FFFFFFF
|
|
621
|
+
if identifier == 0:
|
|
622
|
+
self._send_window += increment
|
|
623
|
+
self._window_changed.set()
|
|
624
|
+
elif (stream := self._streams.get(identifier)) is not None:
|
|
625
|
+
stream.send_window += increment
|
|
626
|
+
stream.window_changed.set()
|
|
627
|
+
|
|
628
|
+
@staticmethod
|
|
629
|
+
def _frame(kind: FrameType, flags: int, identifier: int, payload: bytes) -> bytes:
|
|
630
|
+
return (
|
|
631
|
+
len(payload).to_bytes(3, "big")
|
|
632
|
+
+ bytes((kind, flags))
|
|
633
|
+
+ struct.pack("!I", identifier)
|
|
634
|
+
+ payload
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
@staticmethod
|
|
638
|
+
def _priority_bytes(priority: Http2Priority) -> bytes:
|
|
639
|
+
return struct.pack(
|
|
640
|
+
"!I", priority.dependency | (priority.exclusive << 31)
|
|
641
|
+
) + bytes((priority.weight - 1,))
|
|
642
|
+
|
|
643
|
+
@staticmethod
|
|
644
|
+
def _error_name(payload: bytes) -> str:
|
|
645
|
+
code = int.from_bytes(payload[:4], "big")
|
|
646
|
+
return ErrorCode(code).name if code in ErrorCode else f"{code:#x}"
|
|
647
|
+
|
|
648
|
+
@staticmethod
|
|
649
|
+
def _nothing() -> None:
|
|
650
|
+
pass
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@final
|
|
654
|
+
class Http2StreamTransport:
|
|
655
|
+
def __init__(
|
|
656
|
+
self, connection: Http2Connection, stream: Http2Stream, deadline: Deadline
|
|
657
|
+
) -> None:
|
|
658
|
+
self._connection = connection
|
|
659
|
+
self._stream = stream
|
|
660
|
+
self._deadline = deadline
|
|
661
|
+
self._pending = b""
|
|
662
|
+
self._closed = False
|
|
663
|
+
|
|
664
|
+
async def read(self, limit: int) -> bytes:
|
|
665
|
+
stream = self._stream
|
|
666
|
+
while not self._pending:
|
|
667
|
+
if stream.data:
|
|
668
|
+
self._pending = stream.take()
|
|
669
|
+
elif stream.error is not None:
|
|
670
|
+
raise stream.error
|
|
671
|
+
elif stream.finished:
|
|
672
|
+
return b""
|
|
673
|
+
else:
|
|
674
|
+
async with self._deadline.phase("read"):
|
|
675
|
+
await stream.wait()
|
|
676
|
+
piece, self._pending = self._pending[:limit], self._pending[limit:]
|
|
677
|
+
return piece
|
|
678
|
+
|
|
679
|
+
async def write(self, data: bytes) -> None:
|
|
680
|
+
await self._connection.write_data(self._stream, data, end=False)
|
|
681
|
+
|
|
682
|
+
async def close(self) -> None:
|
|
683
|
+
if not self._closed:
|
|
684
|
+
self._closed = True
|
|
685
|
+
await self._connection.write_data(self._stream, b"", end=True)
|