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/http3.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import secrets
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from typing import Final, final
|
|
7
|
+
|
|
8
|
+
from ssl2 import AsyncTransport, ClientHello, PseudoHeader
|
|
9
|
+
|
|
10
|
+
from boring_http.errors import (
|
|
11
|
+
ProtocolError,
|
|
12
|
+
RemoteDisconnected,
|
|
13
|
+
StreamRefused,
|
|
14
|
+
StreamResetError,
|
|
15
|
+
)
|
|
16
|
+
from boring_http.message import Headers, HTTPVersion, Request, Response
|
|
17
|
+
from boring_http.qpack import Qpack
|
|
18
|
+
from boring_http.quic.connection import TransportParameters
|
|
19
|
+
from boring_http.quic.packet import QUIC_V1, Reader, Writer
|
|
20
|
+
from boring_http.quic.protocol import OriginMemory, QuicClient
|
|
21
|
+
from boring_http.timing import Deadline, Timeout
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FrameType(IntEnum):
|
|
25
|
+
DATA = 0x00
|
|
26
|
+
HEADERS = 0x01
|
|
27
|
+
CANCEL_PUSH = 0x03
|
|
28
|
+
SETTINGS = 0x04
|
|
29
|
+
PUSH_PROMISE = 0x05
|
|
30
|
+
GOAWAY = 0x07
|
|
31
|
+
MAX_PUSH_ID = 0x0D
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StreamType(IntEnum):
|
|
35
|
+
CONTROL = 0x00
|
|
36
|
+
PUSH = 0x01
|
|
37
|
+
QPACK_ENCODER = 0x02
|
|
38
|
+
QPACK_DECODER = 0x03
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Setting(IntEnum):
|
|
42
|
+
QPACK_MAX_TABLE_CAPACITY = 0x01
|
|
43
|
+
MAX_FIELD_SECTION_SIZE = 0x06
|
|
44
|
+
QPACK_BLOCKED_STREAMS = 0x07
|
|
45
|
+
ENABLE_CONNECT_PROTOCOL = 0x08
|
|
46
|
+
H3_DATAGRAM = 0x33
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ErrorCode(IntEnum):
|
|
50
|
+
NO_ERROR = 0x100
|
|
51
|
+
GENERAL_PROTOCOL_ERROR = 0x101
|
|
52
|
+
INTERNAL_ERROR = 0x102
|
|
53
|
+
STREAM_CREATION_ERROR = 0x103
|
|
54
|
+
CLOSED_CRITICAL_STREAM = 0x104
|
|
55
|
+
FRAME_UNEXPECTED = 0x105
|
|
56
|
+
FRAME_ERROR = 0x106
|
|
57
|
+
EXCESSIVE_LOAD = 0x107
|
|
58
|
+
ID_ERROR = 0x108
|
|
59
|
+
SETTINGS_ERROR = 0x109
|
|
60
|
+
MISSING_SETTINGS = 0x10A
|
|
61
|
+
REQUEST_REJECTED = 0x10B
|
|
62
|
+
REQUEST_CANCELLED = 0x10C
|
|
63
|
+
REQUEST_INCOMPLETE = 0x10D
|
|
64
|
+
MESSAGE_ERROR = 0x10E
|
|
65
|
+
CONNECT_ERROR = 0x10F
|
|
66
|
+
VERSION_FALLBACK = 0x110
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@final
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class Http3Profile:
|
|
72
|
+
settings: tuple[tuple[int, int], ...] = (
|
|
73
|
+
(Setting.QPACK_MAX_TABLE_CAPACITY, 65536),
|
|
74
|
+
(Setting.MAX_FIELD_SECTION_SIZE, 262144),
|
|
75
|
+
(Setting.QPACK_BLOCKED_STREAMS, 100),
|
|
76
|
+
(Setting.H3_DATAGRAM, 1),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def setting(self, identifier: Setting, default: int) -> int:
|
|
80
|
+
return next(
|
|
81
|
+
(value for key, value in self.settings if key == identifier), default
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
pseudo_header_order: tuple[PseudoHeader, ...] = (
|
|
85
|
+
PseudoHeader.METHOD,
|
|
86
|
+
PseudoHeader.AUTHORITY,
|
|
87
|
+
PseudoHeader.SCHEME,
|
|
88
|
+
PseudoHeader.PATH,
|
|
89
|
+
)
|
|
90
|
+
transport: TransportParameters = field(default_factory=TransportParameters)
|
|
91
|
+
quic_version: int = QUIC_V1
|
|
92
|
+
tls: ClientHello | None = None
|
|
93
|
+
grease: bool = True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
DEFAULT: Final = Http3Profile()
|
|
97
|
+
# RFC 8470 section 5.2: the requests a client may send before the handshake, since a server may see them twice
|
|
98
|
+
SAFE_METHODS: Final = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@final
|
|
102
|
+
class StreamFrames:
|
|
103
|
+
def __init__(self, client: QuicClient, stream_id: int, deadline: Deadline) -> None:
|
|
104
|
+
self._client = client
|
|
105
|
+
self._stream_id = stream_id
|
|
106
|
+
self._deadline = deadline
|
|
107
|
+
self._buffer = bytearray()
|
|
108
|
+
self.ended = False
|
|
109
|
+
|
|
110
|
+
async def next(self) -> tuple[int, bytes] | None:
|
|
111
|
+
kind = await self.varint()
|
|
112
|
+
if kind is None:
|
|
113
|
+
return None
|
|
114
|
+
length = await self.varint()
|
|
115
|
+
if length is None:
|
|
116
|
+
raise ProtocolError("HTTP/3 frame ended inside its length")
|
|
117
|
+
return kind, await self._exactly(length)
|
|
118
|
+
|
|
119
|
+
async def varint(self) -> int | None:
|
|
120
|
+
if not await self._fill(1):
|
|
121
|
+
return None
|
|
122
|
+
length = 1 << (self._buffer[0] >> 6)
|
|
123
|
+
if not await self._fill(length):
|
|
124
|
+
raise ProtocolError("HTTP/3 stream ended inside a varint")
|
|
125
|
+
return Reader(await self._exactly(length)).varint()
|
|
126
|
+
|
|
127
|
+
def drain(self) -> bytes:
|
|
128
|
+
data = bytes(self._buffer)
|
|
129
|
+
self._buffer.clear()
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
async def _exactly(self, length: int) -> bytes:
|
|
133
|
+
if not await self._fill(length):
|
|
134
|
+
raise ProtocolError("HTTP/3 stream ended inside a frame")
|
|
135
|
+
data = bytes(self._buffer[:length])
|
|
136
|
+
del self._buffer[:length]
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
async def _fill(self, length: int) -> bool:
|
|
140
|
+
while len(self._buffer) < length:
|
|
141
|
+
if self.ended:
|
|
142
|
+
return False
|
|
143
|
+
async with self._deadline.phase("read"):
|
|
144
|
+
piece = await self._client.receive(self._stream_id)
|
|
145
|
+
if not piece:
|
|
146
|
+
self.ended = True
|
|
147
|
+
return len(self._buffer) >= length
|
|
148
|
+
self._buffer += piece
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@final
|
|
153
|
+
class Http3Connection:
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
quic: QuicClient,
|
|
157
|
+
profile: Http3Profile,
|
|
158
|
+
memory: OriginMemory | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
self._quic = quic
|
|
161
|
+
self._profile = profile
|
|
162
|
+
self._qpack = Qpack(profile.setting(Setting.QPACK_MAX_TABLE_CAPACITY, 0))
|
|
163
|
+
self._decoder_stream = -1
|
|
164
|
+
self._peer_settings: dict[int, int] = {}
|
|
165
|
+
self._memory = memory
|
|
166
|
+
self._goaway = False
|
|
167
|
+
self._pending: set[int] = set()
|
|
168
|
+
self._readers: set[asyncio.Task[None]] = set()
|
|
169
|
+
self._settings_received: asyncio.Future[None] = (
|
|
170
|
+
asyncio.get_running_loop().create_future()
|
|
171
|
+
)
|
|
172
|
+
self.closed = False
|
|
173
|
+
self.reused = False
|
|
174
|
+
self.released: Callable[[], None] = self._nothing
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def available(self) -> bool:
|
|
178
|
+
return (
|
|
179
|
+
not self.closed
|
|
180
|
+
and not self._goaway
|
|
181
|
+
and not self._quic.closed
|
|
182
|
+
and self._quic.connection.can_open_stream()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def shared(self) -> bool:
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def connect_protocol(self) -> bool:
|
|
191
|
+
return self._peer_settings.get(Setting.ENABLE_CONNECT_PROTOCOL) == 1
|
|
192
|
+
|
|
193
|
+
def take(self) -> None:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
async def start(self) -> None:
|
|
197
|
+
control = self._quic.open_stream(bidirectional=False)
|
|
198
|
+
writer = Writer()
|
|
199
|
+
writer.varint(StreamType.CONTROL)
|
|
200
|
+
settings = Writer()
|
|
201
|
+
entries = list(self._profile.settings)
|
|
202
|
+
if self._profile.grease:
|
|
203
|
+
# RFC 9114 section 7.2.4.1: a reserved identifier the peer must ignore, placed at random like Chrome does
|
|
204
|
+
entries.insert(
|
|
205
|
+
secrets.randbelow(len(entries) + 1),
|
|
206
|
+
(0x1F * secrets.randbelow(1 << 16) + 0x21, secrets.randbelow(1 << 20)),
|
|
207
|
+
)
|
|
208
|
+
for identifier, value in entries:
|
|
209
|
+
settings.varint(identifier)
|
|
210
|
+
settings.varint(value)
|
|
211
|
+
writer.varint(FrameType.SETTINGS)
|
|
212
|
+
writer.varint(len(settings))
|
|
213
|
+
writer.put(settings.finish())
|
|
214
|
+
self._quic.send(control, writer.finish())
|
|
215
|
+
self._quic.send(
|
|
216
|
+
self._quic.open_stream(bidirectional=False),
|
|
217
|
+
Writer.encode_varint(StreamType.QPACK_ENCODER),
|
|
218
|
+
)
|
|
219
|
+
self._decoder_stream = self._quic.open_stream(bidirectional=False)
|
|
220
|
+
self._quic.send(
|
|
221
|
+
self._decoder_stream, Writer.encode_varint(StreamType.QPACK_DECODER)
|
|
222
|
+
)
|
|
223
|
+
self._readers.add(asyncio.create_task(self._read_peer_streams()))
|
|
224
|
+
if (
|
|
225
|
+
self._quic.connection.early_data_pending
|
|
226
|
+
and self._memory is not None
|
|
227
|
+
and self._memory.settings
|
|
228
|
+
):
|
|
229
|
+
# RFC 9114 section 7.2.4.2: 0-RTT requests run under the settings the server sent last time
|
|
230
|
+
self._peer_settings = dict(self._memory.settings)
|
|
231
|
+
return
|
|
232
|
+
_ = await asyncio.wait(
|
|
233
|
+
{self._settings_received, self._quic.terminated},
|
|
234
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
235
|
+
)
|
|
236
|
+
if not self._settings_received.done():
|
|
237
|
+
terminated = self._quic.terminated.result()
|
|
238
|
+
raise RemoteDisconnected(
|
|
239
|
+
f"QUIC connection terminated before the HTTP/3 SETTINGS frame: {terminated.reason or hex(terminated.error)}"
|
|
240
|
+
)
|
|
241
|
+
await self._settings_received
|
|
242
|
+
|
|
243
|
+
async def send(
|
|
244
|
+
self, request: Request, deadline: Deadline, *, decode: bool
|
|
245
|
+
) -> Response:
|
|
246
|
+
if request.method not in SAFE_METHODS:
|
|
247
|
+
await self._established(deadline)
|
|
248
|
+
early = self._quic.connection.early_data_pending
|
|
249
|
+
stream_id = self._quic.open_stream()
|
|
250
|
+
block = self._qpack.encode(self._fields(request))
|
|
251
|
+
self._quic.send(
|
|
252
|
+
stream_id, self._frame(FrameType.HEADERS, block), end=request.body is None
|
|
253
|
+
)
|
|
254
|
+
if request.body is not None:
|
|
255
|
+
async with deadline.phase("write"):
|
|
256
|
+
async for chunk in request.body.chunks():
|
|
257
|
+
self._quic.send(stream_id, self._frame(FrameType.DATA, chunk))
|
|
258
|
+
self._quic.send(stream_id, b"", end=True)
|
|
259
|
+
frames = StreamFrames(self._quic, stream_id, deadline)
|
|
260
|
+
status, headers = await self._response_head(frames, stream_id)
|
|
261
|
+
trailers = Headers()
|
|
262
|
+
if (
|
|
263
|
+
status == 425
|
|
264
|
+
and early
|
|
265
|
+
and (request.body is None or request.body.replayable)
|
|
266
|
+
):
|
|
267
|
+
# RFC 8470 section 5.2: the server wants this request again once the handshake has finished
|
|
268
|
+
await self._established(deadline)
|
|
269
|
+
async for _ in self._body(frames, stream_id, trailers):
|
|
270
|
+
pass
|
|
271
|
+
return await self.send(request, deadline, decode=decode)
|
|
272
|
+
return Response(
|
|
273
|
+
status=status,
|
|
274
|
+
reason="",
|
|
275
|
+
version=HTTPVersion.HTTP_3,
|
|
276
|
+
headers=headers,
|
|
277
|
+
request=request,
|
|
278
|
+
raw=self._body(frames, stream_id, trailers),
|
|
279
|
+
trailers=trailers,
|
|
280
|
+
decode=decode,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def tunnel(
|
|
284
|
+
self, request: Request, deadline: Deadline
|
|
285
|
+
) -> tuple[int, Headers, AsyncTransport]:
|
|
286
|
+
await self._established(deadline)
|
|
287
|
+
stream_id = self._quic.open_stream()
|
|
288
|
+
self._quic.send(
|
|
289
|
+
stream_id,
|
|
290
|
+
self._frame(
|
|
291
|
+
FrameType.HEADERS,
|
|
292
|
+
self._qpack.encode(self._fields(request, protocol=b"websocket")),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
frames = StreamFrames(self._quic, stream_id, deadline)
|
|
296
|
+
status, headers = await self._response_head(frames, stream_id)
|
|
297
|
+
return (
|
|
298
|
+
status,
|
|
299
|
+
headers,
|
|
300
|
+
Http3StreamTransport(self._quic, stream_id, deadline, frames),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
async def close(self) -> None:
|
|
304
|
+
if self.closed:
|
|
305
|
+
return
|
|
306
|
+
self.closed = True
|
|
307
|
+
for reader in self._readers:
|
|
308
|
+
_ = reader.cancel()
|
|
309
|
+
self._quic.close(ErrorCode.NO_ERROR)
|
|
310
|
+
self.released()
|
|
311
|
+
|
|
312
|
+
async def _established(self, deadline: Deadline) -> None:
|
|
313
|
+
if self._quic.connection.early_data_pending:
|
|
314
|
+
async with deadline.phase("connect"):
|
|
315
|
+
await self._quic.established()
|
|
316
|
+
|
|
317
|
+
def _fields(
|
|
318
|
+
self, request: Request, *, protocol: bytes | None = None
|
|
319
|
+
) -> list[tuple[bytes, bytes]]:
|
|
320
|
+
wire = request.wire_headers(HTTPVersion.HTTP_3)
|
|
321
|
+
authority = request.headers.get("host") or request.url.authority
|
|
322
|
+
scheme = {"ws": "http", "wss": "https"}.get(
|
|
323
|
+
request.url.scheme, request.url.scheme
|
|
324
|
+
)
|
|
325
|
+
pseudo = {
|
|
326
|
+
PseudoHeader.METHOD: (b":method", request.method.encode()),
|
|
327
|
+
PseudoHeader.AUTHORITY: (b":authority", authority.encode()),
|
|
328
|
+
PseudoHeader.SCHEME: (b":scheme", scheme.encode()),
|
|
329
|
+
PseudoHeader.PATH: (b":path", request.url.target.encode()),
|
|
330
|
+
}
|
|
331
|
+
fields = [pseudo[header] for header in self._profile.pseudo_header_order]
|
|
332
|
+
if protocol is not None:
|
|
333
|
+
fields.insert(1, (b":protocol", protocol))
|
|
334
|
+
return fields + [(name.encode(), value.encode()) for name, value in wire]
|
|
335
|
+
|
|
336
|
+
async def _response_head(
|
|
337
|
+
self, frames: StreamFrames, stream_id: int
|
|
338
|
+
) -> tuple[int, Headers]:
|
|
339
|
+
self._pending.add(stream_id)
|
|
340
|
+
try:
|
|
341
|
+
return await self._head_frames(frames, stream_id)
|
|
342
|
+
except BaseException as error:
|
|
343
|
+
self._qpack.cancel(stream_id)
|
|
344
|
+
self._flush_decoder()
|
|
345
|
+
if (
|
|
346
|
+
isinstance(error, StreamResetError)
|
|
347
|
+
and error.code == ErrorCode.REQUEST_REJECTED
|
|
348
|
+
):
|
|
349
|
+
raise StreamRefused(
|
|
350
|
+
f"HTTP/3 stream {stream_id} was rejected before processing"
|
|
351
|
+
) from error
|
|
352
|
+
raise
|
|
353
|
+
finally:
|
|
354
|
+
self._pending.discard(stream_id)
|
|
355
|
+
|
|
356
|
+
async def _head_frames(
|
|
357
|
+
self, frames: StreamFrames, stream_id: int
|
|
358
|
+
) -> tuple[int, Headers]:
|
|
359
|
+
while True:
|
|
360
|
+
frame = await frames.next()
|
|
361
|
+
if frame is None:
|
|
362
|
+
raise RemoteDisconnected(
|
|
363
|
+
f"HTTP/3 stream {stream_id} ended before the response head"
|
|
364
|
+
)
|
|
365
|
+
kind, payload = frame
|
|
366
|
+
if kind == FrameType.HEADERS:
|
|
367
|
+
decoded = await self._decode(payload, stream_id)
|
|
368
|
+
status = next(
|
|
369
|
+
(value for name, value in decoded if name == ":status"), ""
|
|
370
|
+
)
|
|
371
|
+
if not (status.isascii() and status.isdigit()):
|
|
372
|
+
raise ProtocolError("HTTP/3 response without a numeric :status")
|
|
373
|
+
if 100 <= int(status) < 200:
|
|
374
|
+
continue
|
|
375
|
+
return int(status), Headers(
|
|
376
|
+
[
|
|
377
|
+
(name, value)
|
|
378
|
+
for name, value in decoded
|
|
379
|
+
if not name.startswith(":")
|
|
380
|
+
]
|
|
381
|
+
)
|
|
382
|
+
if kind in (
|
|
383
|
+
FrameType.PUSH_PROMISE,
|
|
384
|
+
FrameType.SETTINGS,
|
|
385
|
+
FrameType.GOAWAY,
|
|
386
|
+
FrameType.MAX_PUSH_ID,
|
|
387
|
+
FrameType.CANCEL_PUSH,
|
|
388
|
+
):
|
|
389
|
+
raise ProtocolError(
|
|
390
|
+
f"unexpected HTTP/3 frame {FrameType(kind).name} on a request stream"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _body(
|
|
394
|
+
self, frames: StreamFrames, stream_id: int, trailers: Headers
|
|
395
|
+
) -> AsyncGenerator[bytes]:
|
|
396
|
+
try:
|
|
397
|
+
while (frame := await frames.next()) is not None:
|
|
398
|
+
kind, payload = frame
|
|
399
|
+
if kind == FrameType.DATA:
|
|
400
|
+
if payload:
|
|
401
|
+
yield payload
|
|
402
|
+
elif kind == FrameType.HEADERS:
|
|
403
|
+
for name, value in await self._decode(payload, stream_id):
|
|
404
|
+
trailers.add(name, value)
|
|
405
|
+
elif kind in (
|
|
406
|
+
FrameType.PUSH_PROMISE,
|
|
407
|
+
FrameType.SETTINGS,
|
|
408
|
+
FrameType.GOAWAY,
|
|
409
|
+
FrameType.MAX_PUSH_ID,
|
|
410
|
+
FrameType.CANCEL_PUSH,
|
|
411
|
+
):
|
|
412
|
+
raise ProtocolError(
|
|
413
|
+
f"unexpected HTTP/3 frame {FrameType(kind).name} on a request stream"
|
|
414
|
+
)
|
|
415
|
+
except BaseException:
|
|
416
|
+
self._quic.reset(stream_id, ErrorCode.REQUEST_CANCELLED)
|
|
417
|
+
self._quic.stop_sending(stream_id, ErrorCode.REQUEST_CANCELLED)
|
|
418
|
+
self._qpack.cancel(stream_id)
|
|
419
|
+
self._flush_decoder()
|
|
420
|
+
raise
|
|
421
|
+
finally:
|
|
422
|
+
self.released()
|
|
423
|
+
|
|
424
|
+
async def _decode(self, block: bytes, stream_id: int) -> list[tuple[str, str]]:
|
|
425
|
+
# a block that refers to dynamic table entries still on the encoder stream waits for them
|
|
426
|
+
while (fields := self._qpack.decode(block, stream_id)) is None:
|
|
427
|
+
await self._qpack.wait()
|
|
428
|
+
self._flush_decoder()
|
|
429
|
+
decoded = [
|
|
430
|
+
(name.decode("latin-1"), value.decode("latin-1")) for name, value in fields
|
|
431
|
+
]
|
|
432
|
+
try:
|
|
433
|
+
_ = Headers(
|
|
434
|
+
[(name, value) for name, value in decoded if not name.startswith(":")]
|
|
435
|
+
)
|
|
436
|
+
except ValueError as error:
|
|
437
|
+
raise ProtocolError(f"malformed header from the server: {error}") from None
|
|
438
|
+
return decoded
|
|
439
|
+
|
|
440
|
+
def _flush_decoder(self) -> None:
|
|
441
|
+
instructions = self._qpack.take_instructions()
|
|
442
|
+
if instructions and not self._quic.closed:
|
|
443
|
+
self._quic.send(self._decoder_stream, instructions)
|
|
444
|
+
|
|
445
|
+
async def _read_peer_streams(self) -> None:
|
|
446
|
+
while True:
|
|
447
|
+
stream_id = await self._quic.incoming.get()
|
|
448
|
+
reader = asyncio.create_task(self._read_peer_stream(stream_id))
|
|
449
|
+
self._readers.add(reader)
|
|
450
|
+
reader.add_done_callback(self._readers.discard)
|
|
451
|
+
|
|
452
|
+
async def _read_peer_stream(self, stream_id: int) -> None:
|
|
453
|
+
if stream_id % 4 != 3:
|
|
454
|
+
self._quic.stop_sending(stream_id, ErrorCode.STREAM_CREATION_ERROR)
|
|
455
|
+
return
|
|
456
|
+
try:
|
|
457
|
+
await self._read_unidirectional(stream_id)
|
|
458
|
+
except ProtocolError as error:
|
|
459
|
+
self._quic.close(ErrorCode.GENERAL_PROTOCOL_ERROR, str(error))
|
|
460
|
+
if not self._settings_received.done():
|
|
461
|
+
self._settings_received.set_exception(error)
|
|
462
|
+
except RemoteDisconnected as error:
|
|
463
|
+
if not self._settings_received.done():
|
|
464
|
+
self._settings_received.set_exception(error)
|
|
465
|
+
|
|
466
|
+
async def _read_unidirectional(self, stream_id: int) -> None:
|
|
467
|
+
frames = StreamFrames(self._quic, stream_id, Deadline(Timeout.of(None)))
|
|
468
|
+
kind = await frames.varint()
|
|
469
|
+
match kind:
|
|
470
|
+
case StreamType.CONTROL:
|
|
471
|
+
while (frame := await frames.next()) is not None:
|
|
472
|
+
frame_type, payload = frame
|
|
473
|
+
if frame_type == FrameType.SETTINGS:
|
|
474
|
+
reader = Reader(payload)
|
|
475
|
+
settings: dict[int, int] = {}
|
|
476
|
+
while reader.remaining:
|
|
477
|
+
identifier = reader.varint()
|
|
478
|
+
settings[identifier] = reader.varint()
|
|
479
|
+
self._peer_settings = settings
|
|
480
|
+
if self._memory is not None:
|
|
481
|
+
self._memory.settings = settings
|
|
482
|
+
if not self._settings_received.done():
|
|
483
|
+
self._settings_received.set_result(None)
|
|
484
|
+
elif frame_type == FrameType.GOAWAY:
|
|
485
|
+
self._goaway = True
|
|
486
|
+
first_unprocessed = Reader(payload).varint()
|
|
487
|
+
for pending in list(self._pending):
|
|
488
|
+
if pending >= first_unprocessed:
|
|
489
|
+
self._quic.fail_stream(
|
|
490
|
+
pending,
|
|
491
|
+
StreamRefused(
|
|
492
|
+
f"HTTP/3 stream {pending} was refused by GOAWAY"
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
case StreamType.QPACK_ENCODER:
|
|
496
|
+
# the stream type was read through the frame buffer, which may already hold the first instructions
|
|
497
|
+
self._qpack.feed_encoder(frames.drain())
|
|
498
|
+
self._flush_decoder()
|
|
499
|
+
while piece := await self._quic.receive(stream_id):
|
|
500
|
+
self._qpack.feed_encoder(piece)
|
|
501
|
+
self._flush_decoder()
|
|
502
|
+
case StreamType.QPACK_DECODER:
|
|
503
|
+
# our encoder never touches the dynamic table, so the peer's acknowledgements carry nothing to act on
|
|
504
|
+
while await self._quic.receive(stream_id):
|
|
505
|
+
pass
|
|
506
|
+
case _:
|
|
507
|
+
self._quic.stop_sending(stream_id, ErrorCode.STREAM_CREATION_ERROR)
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def _frame(kind: FrameType, payload: bytes) -> bytes:
|
|
511
|
+
return Writer.encode_varint(kind) + Writer.encode_varint(len(payload)) + payload
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _nothing() -> None:
|
|
515
|
+
pass
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@final
|
|
519
|
+
class Http3StreamTransport:
|
|
520
|
+
def __init__(
|
|
521
|
+
self, quic: QuicClient, stream_id: int, deadline: Deadline, frames: StreamFrames
|
|
522
|
+
) -> None:
|
|
523
|
+
self._quic = quic
|
|
524
|
+
self._stream_id = stream_id
|
|
525
|
+
self._frames = frames
|
|
526
|
+
self._deadline = deadline
|
|
527
|
+
self._pending = b""
|
|
528
|
+
self._closed = False
|
|
529
|
+
|
|
530
|
+
async def read(self, limit: int) -> bytes:
|
|
531
|
+
while not self._pending:
|
|
532
|
+
frame = await self._frames.next()
|
|
533
|
+
if frame is None:
|
|
534
|
+
return b""
|
|
535
|
+
if frame[0] == FrameType.DATA:
|
|
536
|
+
self._pending = frame[1]
|
|
537
|
+
piece, self._pending = self._pending[:limit], self._pending[limit:]
|
|
538
|
+
return piece
|
|
539
|
+
|
|
540
|
+
async def write(self, data: bytes) -> None:
|
|
541
|
+
self._quic.send(
|
|
542
|
+
self._stream_id,
|
|
543
|
+
Writer.encode_varint(FrameType.DATA)
|
|
544
|
+
+ Writer.encode_varint(len(data))
|
|
545
|
+
+ data,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
async def close(self) -> None:
|
|
549
|
+
if not self._closed:
|
|
550
|
+
self._closed = True
|
|
551
|
+
self._quic.send(self._stream_id, b"", end=True)
|