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/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)