Files
syn-chat-bot/.venv/lib/python3.9/site-packages/qh3/h3/connection.py
Hyungi Ahn c2257d3a86 fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:53:55 +09:00

1216 lines
42 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import contextlib
import logging
import re
import typing
from enum import Enum, IntEnum
from .._compat import UINT_VAR_MAX_SIZE
from .._hazmat import (
Buffer,
BufferReadError,
DecoderStreamError,
DecompressionFailed,
EncoderStreamError,
QpackDecoder,
QpackEncoder,
StreamBlocked,
encode_uint_var,
)
from ..quic.connection import QuicConnection, stream_is_unidirectional
from ..quic.events import DatagramFrameReceived, QuicEvent, StreamDataReceived
from ..quic.logger import QuicLoggerTrace
from .events import (
DatagramReceived,
DataReceived,
H3Event,
Headers,
HeadersReceived,
InformationalHeadersReceived,
PushPromiseReceived,
WebTransportStreamDataReceived,
)
from .exceptions import NoAvailablePushIDError
logger = logging.getLogger("http3")
H3_ALPN = ["h3"]
RESERVED_SETTINGS = (0x0, 0x2, 0x3, 0x4, 0x5)
UPPERCASE = re.compile(b"[A-Z]")
class ErrorCode(IntEnum):
H3_NO_ERROR = 0x100
H3_GENERAL_PROTOCOL_ERROR = 0x101
H3_INTERNAL_ERROR = 0x102
H3_STREAM_CREATION_ERROR = 0x103
H3_CLOSED_CRITICAL_STREAM = 0x104
H3_FRAME_UNEXPECTED = 0x105
H3_FRAME_ERROR = 0x106
H3_EXCESSIVE_LOAD = 0x107
H3_ID_ERROR = 0x108
H3_SETTINGS_ERROR = 0x109
H3_MISSING_SETTINGS = 0x10A
H3_REQUEST_REJECTED = 0x10B
H3_REQUEST_CANCELLED = 0x10C
H3_REQUEST_INCOMPLETE = 0x10D
H3_MESSAGE_ERROR = 0x10E
H3_CONNECT_ERROR = 0x10F
H3_VERSION_FALLBACK = 0x110
QPACK_DECOMPRESSION_FAILED = 0x200
QPACK_ENCODER_STREAM_ERROR = 0x201
QPACK_DECODER_STREAM_ERROR = 0x202
class FrameType(IntEnum):
DATA = 0x0
HEADERS = 0x1
PRIORITY = 0x2
CANCEL_PUSH = 0x3
SETTINGS = 0x4
PUSH_PROMISE = 0x5
GOAWAY = 0x7
MAX_PUSH_ID = 0xD
DUPLICATE_PUSH = 0xE
WEBTRANSPORT_STREAM = 0x41
class HeadersState(Enum):
INITIAL = 0
AFTER_HEADERS = 1
AFTER_TRAILERS = 2
class Setting(IntEnum):
QPACK_MAX_TABLE_CAPACITY = 0x1
MAX_FIELD_SECTION_SIZE = 0x6
QPACK_BLOCKED_STREAMS = 0x7
# https://datatracker.ietf.org/doc/html/rfc9220#section-5
ENABLE_CONNECT_PROTOCOL = 0x8
# https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-05#section-9.1
H3_DATAGRAM = 0xFFD277
# https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http2-02#section-10.1
ENABLE_WEBTRANSPORT = 0x2B603742
# Dummy setting to check it is correctly ignored by the peer.
# https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.4.1
DUMMY = 0x21
class StreamType(IntEnum):
CONTROL = 0
PUSH = 1
QPACK_ENCODER = 2
QPACK_DECODER = 3
WEBTRANSPORT = 0x54
class ProtocolError(Exception):
"""
Base class for protocol errors.
These errors are not exposed to the API user, they are handled
in :meth:`H3Connection.handle_event`.
"""
error_code = ErrorCode.H3_GENERAL_PROTOCOL_ERROR
def __init__(self, reason_phrase: str = ""):
self.reason_phrase = reason_phrase
class QpackDecompressionFailed(ProtocolError):
error_code = ErrorCode.QPACK_DECOMPRESSION_FAILED
class QpackDecoderStreamError(ProtocolError):
error_code = ErrorCode.QPACK_DECODER_STREAM_ERROR
class QpackEncoderStreamError(ProtocolError):
error_code = ErrorCode.QPACK_ENCODER_STREAM_ERROR
class ClosedCriticalStream(ProtocolError):
error_code = ErrorCode.H3_CLOSED_CRITICAL_STREAM
class FrameUnexpected(ProtocolError):
error_code = ErrorCode.H3_FRAME_UNEXPECTED
class MessageError(ProtocolError):
error_code = ErrorCode.H3_MESSAGE_ERROR
class MissingSettingsError(ProtocolError):
error_code = ErrorCode.H3_MISSING_SETTINGS
class SettingsError(ProtocolError):
error_code = ErrorCode.H3_SETTINGS_ERROR
class StreamCreationError(ProtocolError):
error_code = ErrorCode.H3_STREAM_CREATION_ERROR
def encode_frame(frame_type: int, frame_data: bytes) -> bytes:
frame_length = len(frame_data)
buf = Buffer(capacity=frame_length + 2 * UINT_VAR_MAX_SIZE)
buf.push_uint_var(frame_type)
buf.push_uint_var(frame_length)
buf.push_bytes(frame_data)
return buf.data
def encode_settings(settings: dict[int, int]) -> bytes:
buf = Buffer(capacity=1024)
for setting, value in settings.items():
buf.push_uint_var(setting)
buf.push_uint_var(value)
return buf.data
def parse_max_push_id(data: bytes) -> int:
buf = Buffer(data=data)
max_push_id = buf.pull_uint_var()
assert buf.eof()
return max_push_id
def parse_settings(data: bytes) -> dict[int, int]:
buf = Buffer(data=data)
settings: dict[int, int] = {}
while not buf.eof():
setting = buf.pull_uint_var()
value = buf.pull_uint_var()
if setting in RESERVED_SETTINGS:
raise SettingsError(f"Setting identifier 0x{setting:x} is reserved")
if setting in settings:
raise SettingsError(f"Setting identifier 0x{setting:x} is included twice")
settings[setting] = value
return dict(settings)
def validate_headers(
headers: Headers,
allowed_pseudo_headers: frozenset[bytes],
required_pseudo_headers: frozenset[bytes],
extract_header: bytes | None = None,
) -> bytes | None:
after_pseudo_headers = False
authority: bytes | None = None
path: bytes | None = None
scheme: bytes | None = None
seen_pseudo_headers: set[bytes] = set()
extracted_header_value: bytes | None = None
for key, value in headers:
if UPPERCASE.search(key):
raise MessageError(f"Header {key!r} contains uppercase letters")
if extract_header is not None and extracted_header_value is None:
extracted_header_value = value
if key.startswith(b":"):
# pseudo-headers
if after_pseudo_headers:
raise MessageError(
f"Pseudo-header {key!r} is not allowed after regular headers"
)
if key not in allowed_pseudo_headers:
raise MessageError(f"Pseudo-header {key!r} is not valid")
if key in seen_pseudo_headers:
raise MessageError(f"Pseudo-header {key!r} is included twice")
seen_pseudo_headers.add(key)
# store value
if key == b":authority":
authority = value
elif key == b":path":
path = value
elif key == b":scheme":
scheme = value
else:
# regular headers
after_pseudo_headers = True
# check required pseudo-headers are present
missing = required_pseudo_headers.difference(seen_pseudo_headers)
if missing:
raise MessageError(f"Pseudo-headers {sorted(missing)} are missing")
if scheme in (b"http", b"https"):
if not authority:
raise MessageError("Pseudo-header b':authority' cannot be empty")
if not path:
raise MessageError("Pseudo-header b':path' cannot be empty")
return extracted_header_value
def validate_push_promise_headers(headers: Headers) -> None:
validate_headers(
headers,
allowed_pseudo_headers=frozenset(
(b":method", b":scheme", b":authority", b":path")
),
required_pseudo_headers=frozenset(
(b":method", b":scheme", b":authority", b":path")
),
)
def validate_request_headers(headers: Headers) -> None:
validate_headers(
headers,
allowed_pseudo_headers=frozenset(
# FIXME: The pseudo-header :protocol is not actually defined, but
# we use it for the WebSocket demo.
(b":method", b":scheme", b":authority", b":path", b":protocol")
),
required_pseudo_headers=frozenset((b":method", b":authority")),
)
def validate_response_headers(headers: Headers) -> int | None:
status_code: bytes | None = validate_headers(
headers,
allowed_pseudo_headers=frozenset((b":status",)),
required_pseudo_headers=frozenset((b":status",)),
extract_header=b":status",
)
if status_code is None:
return None
try:
return int(status_code)
except ValueError: # Defensive:
return None
def validate_trailers(headers: Headers) -> None:
validate_headers(
headers,
allowed_pseudo_headers=frozenset(),
required_pseudo_headers=frozenset(),
)
class H3Stream:
def __init__(self, stream_id: int) -> None:
self.blocked = False
self.blocked_frame_size: int | None = None
self.buffer = b""
self.ended = False
self.frame_size: int | None = None
self.frame_type: int | None = None
self.headers_recv_state: HeadersState = HeadersState.INITIAL
self.headers_send_state: HeadersState = HeadersState.INITIAL
self.push_id: int | None = None
self.session_id: int | None = None
self.stream_id = stream_id
self.stream_type: int | None = None
self.receiving_ended = False
self.sending_ended = False
def is_ended(self) -> bool:
return self.sending_ended and self.receiving_ended and not self.blocked
def finish_sending(self) -> None:
if self.sending_ended:
raise FrameUnexpected("stream was already ended")
self.sending_ended = True
class H3Connection:
"""
A low-level HTTP/3 connection object.
:param quic: A :class:`~qh3.connection.QuicConnection` instance.
"""
def __init__(self, quic: QuicConnection, enable_webtransport: bool = False) -> None:
# settings
self._max_table_capacity = 4096
self._blocked_streams = 16
self._enable_webtransport = enable_webtransport
self._is_client = quic.configuration.is_client
self._is_done = False
self._quic = quic
self._quic_logger: QuicLoggerTrace | None = quic._quic_logger
self._decoder = QpackDecoder(self._max_table_capacity, self._blocked_streams)
self._decoder_bytes_received = 0
self._decoder_bytes_sent = 0
self._encoder = QpackEncoder()
self._encoder_bytes_received = 0
self._encoder_bytes_sent = 0
self._settings_received = False
self._stream: dict[int, H3Stream] = {}
self._max_push_id: int | None = 8 if self._is_client else None
self._next_push_id: int = 0
self._local_control_stream_id: int | None = None
self._local_decoder_stream_id: int | None = None
self._local_encoder_stream_id: int | None = None
self._peer_control_stream_id: int | None = None
self._peer_decoder_stream_id: int | None = None
self._peer_encoder_stream_id: int | None = None
self._received_settings: dict[int, int] | None = None
self._sent_settings: dict[int, int] | None = None
self._blocked_stream_map: dict[int, H3Stream] = {}
self._init_connection()
def create_webtransport_stream(
self, session_id: int, is_unidirectional: bool = False
) -> int:
"""
Create a WebTransport stream and return the stream ID.
:param session_id: The WebTransport session identifier.
:param is_unidirectional: Whether to create a unidirectional stream.
"""
if is_unidirectional:
stream_id = self._create_uni_stream(StreamType.WEBTRANSPORT)
self._quic.send_stream_data(stream_id, encode_uint_var(session_id))
else:
stream_id = self._quic.get_next_available_stream_id()
self._log_stream_type(
stream_id=stream_id, stream_type=StreamType.WEBTRANSPORT
)
self._quic.send_stream_data(
stream_id,
encode_uint_var(FrameType.WEBTRANSPORT_STREAM)
+ encode_uint_var(session_id),
)
return stream_id
def handle_event(self, event: QuicEvent) -> list[H3Event]:
"""
Handle a QUIC event and return a list of HTTP events.
:param event: The QUIC event to handle.
"""
if not self._is_done:
try:
if isinstance(event, StreamDataReceived):
return self._receive_stream_data(event)
elif isinstance(event, DatagramFrameReceived):
return self._receive_datagram(event.data)
except ProtocolError as exc:
self._is_done = True
self._quic.close(
error_code=exc.error_code, reason_phrase=exc.reason_phrase
)
return []
def send_datagram(self, flow_id: int, data: bytes) -> None:
"""
Send a datagram for the specified flow.
:param flow_id: The flow ID.
:param data: The HTTP/3 datagram payload.
"""
self._quic.send_datagram_frame(encode_uint_var(flow_id) + data)
def send_push_promise(self, stream_id: int, headers: Headers) -> int:
"""
Send a push promise related to the specified stream.
Returns the stream ID on which headers and data can be sent.
:param stream_id: The stream ID on which to send the data.
:param headers: The HTTP request headers for this push.
"""
assert not self._is_client, "Only servers may send a push promise."
if self._max_push_id is None or self._next_push_id >= self._max_push_id:
raise NoAvailablePushIDError
# send push promise
push_id = self._next_push_id
self._next_push_id += 1
self._quic.send_stream_data(
stream_id,
encode_frame(
FrameType.PUSH_PROMISE,
encode_uint_var(push_id) + self._encode_headers(stream_id, headers),
),
)
#  create push stream
push_stream_id = self._create_uni_stream(StreamType.PUSH, push_id=push_id)
self._quic.send_stream_data(push_stream_id, encode_uint_var(push_id))
return push_stream_id
def send_data(self, stream_id: int, data: bytes, end_stream: bool) -> None:
"""
Send data on the given stream.
To retrieve datagram which need to be sent over the network call the QUIC
connection's :meth:`~qh3.connection.QuicConnection.datagrams_to_send`
method.
:param stream_id: The stream ID on which to send the data.
:param data: The data to send.
:param end_stream: Whether to end the stream.
"""
# check DATA frame is allowed
with self._get_or_create_stream(stream_id) as stream:
if stream.headers_send_state != HeadersState.AFTER_HEADERS:
raise FrameUnexpected("DATA frame is not allowed in this state")
if end_stream:
stream.finish_sending()
# log frame
if self._quic_logger is not None:
self._quic_logger.log_event(
category="http",
event="frame_created",
data=self._quic_logger.encode_http3_data_frame(
length=len(data), stream_id=stream_id
),
)
self._quic.send_stream_data(
stream_id, encode_frame(FrameType.DATA, data), end_stream
)
def send_headers(
self, stream_id: int, headers: Headers, end_stream: bool = False
) -> None:
"""
Send headers on the given stream.
To retrieve datagram which need to be sent over the network call the QUIC
connection's :meth:`~qh3.connection.QuicConnection.datagrams_to_send`
method.
:param stream_id: The stream ID on which to send the headers.
:param headers: The HTTP headers to send.
:param end_stream: Whether to end the stream.
"""
# check HEADERS frame is allowed
with self._get_or_create_stream(stream_id) as stream:
if stream.headers_send_state == HeadersState.AFTER_TRAILERS:
raise FrameUnexpected("HEADERS frame is not allowed in this state")
if end_stream:
stream.finish_sending()
frame_data = self._encode_headers(stream_id, headers)
# log frame
if self._quic_logger is not None:
self._quic_logger.log_event(
category="http",
event="frame_created",
data=self._quic_logger.encode_http3_headers_frame(
length=len(frame_data), headers=headers, stream_id=stream_id
),
)
# update state and send headers
if stream.headers_send_state == HeadersState.INITIAL:
is_informational_headers = False
if not self._is_client and not end_stream:
for k, v in headers:
if k == b":status":
if int(v) < 200:
is_informational_headers = True
break
elif not k.startswith(b":"):
break
# only the server is allowed to do so!
if not is_informational_headers:
stream.headers_send_state = HeadersState.AFTER_HEADERS
else:
stream.headers_send_state = HeadersState.AFTER_TRAILERS
self._quic.send_stream_data(
stream_id, encode_frame(FrameType.HEADERS, frame_data), end_stream
)
@property
def received_settings(self) -> dict[int, int] | None:
"""
Return the received SETTINGS frame, or None.
"""
return self._received_settings
@property
def sent_settings(self) -> dict[int, int] | None:
"""
Return the sent SETTINGS frame, or None.
"""
return self._sent_settings
def _create_uni_stream(self, stream_type: int, push_id: int | None = None) -> int:
"""
Create an unidirectional stream of the given type.
"""
stream_id = self._quic.get_next_available_stream_id(is_unidirectional=True)
self._log_stream_type(
push_id=push_id, stream_id=stream_id, stream_type=stream_type
)
self._quic.send_stream_data(stream_id, encode_uint_var(stream_type))
return stream_id
def _decode_headers(self, stream_id: int, frame_data: bytes | None) -> Headers:
"""
Decode a HEADERS block and send decoder updates on the decoder stream.
This is called with frame_data=None when a stream becomes unblocked.
"""
try:
if frame_data is None:
decoder, headers = self._blocked_stream_map[stream_id]._pending # type: ignore[attr-defined]
del self._blocked_stream_map[stream_id]._pending # type: ignore[attr-defined]
else:
# todo: investigate why the underlying implementation
# seems to ignore bad frames..
if not frame_data:
raise DecompressionFailed()
decoder, headers = self._decoder.feed_header(stream_id, frame_data)
self._decoder_bytes_sent += len(decoder)
self._quic.send_stream_data(self._local_decoder_stream_id, decoder)
except DecompressionFailed as exc:
raise QpackDecompressionFailed() from exc
return headers
def _encode_headers(self, stream_id: int, headers: Headers) -> bytes:
"""
Encode a HEADERS block and send encoder updates on the encoder stream.
"""
try:
encoder, frame_data = self._encoder.encode(stream_id, headers)
except EncoderStreamError as exc:
raise QpackEncoderStreamError() from exc
self._encoder_bytes_sent += len(encoder)
self._quic.send_stream_data(self._local_encoder_stream_id, encoder)
return frame_data
@contextlib.contextmanager
def _get_or_create_stream(self, stream_id: int) -> typing.Generator[H3Stream]:
if stream_id not in self._stream:
self._stream[stream_id] = H3Stream(stream_id)
stream = self._stream[stream_id]
try:
yield stream
finally:
# Don't forget to delete stream objects when they are done
if stream.is_ended():
del self._stream[stream_id]
def _get_local_settings(self) -> dict[int, int]:
"""
Return the local HTTP/3 settings.
"""
settings: dict[int, int] = {
Setting.QPACK_MAX_TABLE_CAPACITY: self._max_table_capacity,
Setting.QPACK_BLOCKED_STREAMS: self._blocked_streams,
Setting.ENABLE_CONNECT_PROTOCOL: 1,
Setting.DUMMY: 1,
}
if self._enable_webtransport:
settings[Setting.H3_DATAGRAM] = 1
settings[Setting.ENABLE_WEBTRANSPORT] = 1
return settings
def _handle_control_frame(self, frame_type: int, frame_data: bytes) -> None:
"""
Handle a frame received on the peer's control stream.
"""
if frame_type != FrameType.SETTINGS and not self._settings_received:
raise MissingSettingsError
if frame_type == FrameType.SETTINGS:
if self._settings_received:
raise FrameUnexpected("SETTINGS have already been received")
settings = parse_settings(frame_data)
self._validate_settings(settings)
self._received_settings = settings
encoder = self._encoder.apply_settings(
max_table_capacity=settings.get(Setting.QPACK_MAX_TABLE_CAPACITY, 0),
dyn_table_capacity=settings.get(Setting.QPACK_MAX_TABLE_CAPACITY, 0),
blocked_streams=settings.get(Setting.QPACK_BLOCKED_STREAMS, 0),
)
self._quic.send_stream_data(self._local_encoder_stream_id, encoder)
self._settings_received = True
elif frame_type == FrameType.MAX_PUSH_ID:
if self._is_client:
raise FrameUnexpected("Servers must not send MAX_PUSH_ID")
self._max_push_id = parse_max_push_id(frame_data)
elif frame_type in (
FrameType.DATA,
FrameType.HEADERS,
FrameType.PUSH_PROMISE,
FrameType.DUPLICATE_PUSH,
):
raise FrameUnexpected("Invalid frame type on control stream")
def _handle_request_or_push_frame(
self,
frame_type: int,
frame_data: bytes | None,
stream: H3Stream,
stream_ended: bool,
) -> list[H3Event]:
"""
Handle a frame received on a request or push stream.
"""
http_events: list[H3Event] = []
if frame_type == FrameType.DATA:
# check DATA frame is allowed
if stream.headers_recv_state != HeadersState.AFTER_HEADERS:
raise FrameUnexpected("DATA frame is not allowed in this state")
if stream_ended or frame_data:
http_events.append(
DataReceived(
data=frame_data,
push_id=stream.push_id,
stream_ended=stream_ended,
stream_id=stream.stream_id,
)
)
elif frame_type == FrameType.HEADERS:
# check HEADERS frame is allowed
if stream.headers_recv_state == HeadersState.AFTER_TRAILERS:
raise FrameUnexpected("HEADERS frame is not allowed in this state")
# try to decode HEADERS, may raise pylsqpack.StreamBlocked
headers = self._decode_headers(stream.stream_id, frame_data)
status_code: int | None = None
# validate headers
if stream.headers_recv_state == HeadersState.INITIAL:
if self._is_client:
status_code = validate_response_headers(headers)
else:
validate_request_headers(headers)
else:
validate_trailers(headers)
# log frame
if self._quic_logger is not None:
self._quic_logger.log_event(
category="http",
event="frame_parsed",
data=self._quic_logger.encode_http3_headers_frame(
length=(
stream.blocked_frame_size
if frame_data is None
else len(frame_data)
),
headers=headers,
stream_id=stream.stream_id,
),
)
# update state and emit headers
if stream.headers_recv_state == HeadersState.INITIAL:
# Informational Response MUST be taken as-is without
# skipping the main response.
if status_code is None or status_code >= 200 or stream_ended:
stream.headers_recv_state = HeadersState.AFTER_HEADERS
else:
stream.headers_recv_state = HeadersState.AFTER_TRAILERS
if (
stream.headers_recv_state == HeadersState.INITIAL
and status_code is not None
and status_code < 200
):
http_events.append(
InformationalHeadersReceived(
headers=headers,
stream_id=stream.stream_id,
)
)
else:
http_events.append(
HeadersReceived(
headers=headers,
push_id=stream.push_id,
stream_id=stream.stream_id,
stream_ended=stream_ended,
)
)
elif frame_type == FrameType.PUSH_PROMISE and stream.push_id is None:
if not self._is_client:
raise FrameUnexpected("Clients must not send PUSH_PROMISE")
frame_buf = Buffer(data=frame_data)
push_id = frame_buf.pull_uint_var()
headers = self._decode_headers(
stream.stream_id, frame_data[frame_buf.tell() :]
)
# validate headers
validate_push_promise_headers(headers)
# log frame
if self._quic_logger is not None:
self._quic_logger.log_event(
category="http",
event="frame_parsed",
data=self._quic_logger.encode_http3_push_promise_frame(
length=len(frame_data),
headers=headers,
push_id=push_id,
stream_id=stream.stream_id,
),
)
# emit event
http_events.append(
PushPromiseReceived(
headers=headers, push_id=push_id, stream_id=stream.stream_id
)
)
elif frame_type in (
FrameType.PRIORITY,
FrameType.CANCEL_PUSH,
FrameType.SETTINGS,
FrameType.PUSH_PROMISE,
FrameType.GOAWAY,
FrameType.MAX_PUSH_ID,
FrameType.DUPLICATE_PUSH,
):
raise FrameUnexpected(
"Invalid frame type on request stream"
if stream.push_id is None
else "Invalid frame type on push stream"
)
return http_events
def _init_connection(self) -> None:
# send our settings
self._local_control_stream_id = self._create_uni_stream(StreamType.CONTROL)
self._sent_settings = self._get_local_settings()
self._quic.send_stream_data(
self._local_control_stream_id,
encode_frame(FrameType.SETTINGS, encode_settings(self._sent_settings)),
)
if self._is_client and self._max_push_id is not None:
self._quic.send_stream_data(
self._local_control_stream_id,
encode_frame(FrameType.MAX_PUSH_ID, encode_uint_var(self._max_push_id)),
)
# create encoder and decoder streams
self._local_encoder_stream_id = self._create_uni_stream(
StreamType.QPACK_ENCODER
)
self._local_decoder_stream_id = self._create_uni_stream(
StreamType.QPACK_DECODER
)
def _log_stream_type(
self, stream_id: int, stream_type: int, push_id: int | None = None
) -> None:
if self._quic_logger is not None:
type_name = {
0: "control",
1: "push",
2: "qpack_encoder",
3: "qpack_decoder",
0x54: "webtransport", # NOTE: not standardized yet
}.get(stream_type, "unknown")
data = {"new": type_name, "stream_id": stream_id}
if push_id is not None:
data["associated_push_id"] = push_id
self._quic_logger.log_event(
category="http",
event="stream_type_set",
data=data,
)
def _receive_datagram(self, data: bytes) -> list[H3Event]:
"""
Handle a datagram.
"""
buf = Buffer(data=data)
try:
flow_id = buf.pull_uint_var()
except BufferReadError:
raise ProtocolError("Could not parse flow ID")
return [DatagramReceived(data=data[buf.tell() :], flow_id=flow_id)]
def _receive_stream_data(self, event: StreamDataReceived) -> list[H3Event]:
stream_id = event.stream_id
with self._get_or_create_stream(stream_id) as stream:
if stream_is_unidirectional(stream_id):
return self._receive_stream_data_uni(
stream, event.data, event.end_stream
)
else:
return self._receive_request_or_push_data(
stream, event.data, event.end_stream
)
def _receive_request_or_push_data(
self, stream: H3Stream, data: bytes, stream_ended: bool
) -> list[H3Event]:
"""
Handle data received on a request or push stream.
"""
http_events: list[H3Event] = []
stream.buffer += data
if stream_ended:
stream.receiving_ended = True
if stream.blocked:
return http_events
# shortcut for WEBTRANSPORT_STREAM frame fragments
if (
stream.frame_type == FrameType.WEBTRANSPORT_STREAM
and stream.session_id is not None
):
http_events.append(
WebTransportStreamDataReceived(
data=stream.buffer,
session_id=stream.session_id,
stream_id=stream.stream_id,
stream_ended=stream_ended,
)
)
stream.buffer = b""
return http_events
# shortcut for DATA frame fragments
if (
stream.frame_type == FrameType.DATA
and stream.frame_size is not None
and len(stream.buffer) < stream.frame_size
):
http_events.append(
DataReceived(
data=stream.buffer,
push_id=stream.push_id,
stream_id=stream.stream_id,
stream_ended=False,
)
)
stream.frame_size -= len(stream.buffer)
stream.buffer = b""
return http_events
# handle lone FIN
if stream_ended and not stream.buffer:
http_events.append(
DataReceived(
data=b"",
push_id=stream.push_id,
stream_id=stream.stream_id,
stream_ended=True,
)
)
return http_events
buf = Buffer(data=stream.buffer)
consumed = 0
while not buf.eof():
# fetch next frame header
if stream.frame_size is None:
try:
stream.frame_type = buf.pull_uint_var()
stream.frame_size = buf.pull_uint_var()
except BufferReadError:
break
consumed = buf.tell()
# WEBTRANSPORT_STREAM frames last until the end of the stream
if stream.frame_type == FrameType.WEBTRANSPORT_STREAM:
stream.session_id = stream.frame_size
stream.frame_size = None
frame_data = stream.buffer[consumed:]
stream.buffer = b""
self._log_stream_type(
stream_id=stream.stream_id, stream_type=StreamType.WEBTRANSPORT
)
if frame_data or stream_ended:
http_events.append(
WebTransportStreamDataReceived(
data=frame_data,
session_id=stream.session_id,
stream_id=stream.stream_id,
stream_ended=stream_ended,
)
)
return http_events
# log frame
if (
self._quic_logger is not None
and stream.frame_type == FrameType.DATA
):
self._quic_logger.log_event(
category="http",
event="frame_parsed",
data=self._quic_logger.encode_http3_data_frame(
length=stream.frame_size, stream_id=stream.stream_id
),
)
# check how much data is available
chunk_size = min(stream.frame_size, buf.capacity - consumed)
if stream.frame_type != FrameType.DATA and chunk_size < stream.frame_size:
break
# read available data
frame_data = buf.pull_bytes(chunk_size)
frame_type = stream.frame_type
consumed = buf.tell()
# detect end of frame
stream.frame_size -= chunk_size
if not stream.frame_size:
stream.frame_size = None
stream.frame_type = None
try:
http_events.extend(
self._handle_request_or_push_frame(
frame_type=frame_type,
frame_data=frame_data,
stream=stream,
stream_ended=stream.receiving_ended and buf.eof(),
)
)
except StreamBlocked:
stream.blocked = True
stream.blocked_frame_size = len(frame_data)
self._blocked_stream_map[stream.stream_id] = stream
break
# remove processed data from buffer
stream.buffer = stream.buffer[consumed:]
return http_events
def _receive_stream_data_uni(
self, stream: H3Stream, data: bytes, stream_ended: bool
) -> list[H3Event]:
http_events: list[H3Event] = []
stream.buffer += data
if stream_ended:
stream.receiving_ended = True
buf = Buffer(data=stream.buffer)
consumed = 0
unblocked_streams: set[int] = set()
while (
stream.stream_type
in (StreamType.PUSH, StreamType.CONTROL, StreamType.WEBTRANSPORT)
or not buf.eof()
):
# fetch stream type for unidirectional streams
if stream.stream_type is None:
try:
stream.stream_type = buf.pull_uint_var()
except BufferReadError:
break
consumed = buf.tell()
# check unicity
if stream.stream_type == StreamType.CONTROL:
if self._peer_control_stream_id is not None:
raise StreamCreationError("Only one control stream is allowed")
self._peer_control_stream_id = stream.stream_id
elif stream.stream_type == StreamType.QPACK_DECODER:
if self._peer_decoder_stream_id is not None:
raise StreamCreationError(
"Only one QPACK decoder stream is allowed"
)
self._peer_decoder_stream_id = stream.stream_id
elif stream.stream_type == StreamType.QPACK_ENCODER:
if self._peer_encoder_stream_id is not None:
raise StreamCreationError(
"Only one QPACK encoder stream is allowed"
)
self._peer_encoder_stream_id = stream.stream_id
# for PUSH, logging is performed once the push_id is known
if stream.stream_type != StreamType.PUSH:
self._log_stream_type(
stream_id=stream.stream_id, stream_type=stream.stream_type
)
if stream.stream_type == StreamType.CONTROL:
if stream_ended:
raise ClosedCriticalStream("Closing control stream is not allowed")
# fetch next frame
try:
frame_type = buf.pull_uint_var()
frame_length = buf.pull_uint_var()
frame_data = buf.pull_bytes(frame_length)
except BufferReadError:
break
consumed = buf.tell()
self._handle_control_frame(frame_type, frame_data)
elif stream.stream_type == StreamType.PUSH:
# fetch push id
if stream.push_id is None:
try:
stream.push_id = buf.pull_uint_var()
except BufferReadError:
break
consumed = buf.tell()
self._log_stream_type(
push_id=stream.push_id,
stream_id=stream.stream_id,
stream_type=stream.stream_type,
)
# remove processed data from buffer
stream.buffer = stream.buffer[consumed:]
return self._receive_request_or_push_data(stream, b"", stream_ended)
elif stream.stream_type == StreamType.WEBTRANSPORT:
# fetch session id
if stream.session_id is None:
try:
stream.session_id = buf.pull_uint_var()
except BufferReadError:
break
consumed = buf.tell()
frame_data = stream.buffer[consumed:]
stream.buffer = b""
if frame_data or stream_ended:
http_events.append(
WebTransportStreamDataReceived(
data=frame_data,
session_id=stream.session_id,
stream_ended=stream.receiving_ended,
stream_id=stream.stream_id,
)
)
return http_events
elif stream.stream_type == StreamType.QPACK_DECODER:
# feed unframed data to decoder
data = buf.pull_bytes(buf.capacity - buf.tell())
consumed = buf.tell()
try:
self._encoder.feed_decoder(data)
except DecoderStreamError as exc:
raise QpackDecoderStreamError() from exc
self._decoder_bytes_received += len(data)
elif stream.stream_type == StreamType.QPACK_ENCODER:
# feed unframed data to encoder
data = buf.pull_bytes(buf.capacity - buf.tell())
consumed = buf.tell()
try:
self._decoder.feed_encoder(data)
except EncoderStreamError as exc:
raise QpackEncoderStreamError() from exc
for blocked_id, blocked_stream in self._blocked_stream_map.items():
try:
stream_data, headers = self._decoder.resume_header(blocked_id)
blocked_stream._pending = ( # type: ignore[attr-defined]
stream_data,
headers,
)
unblocked_streams.add(blocked_id)
except StreamBlocked:
continue
self._encoder_bytes_received += len(data)
else:
# unknown stream type, discard data
buf.seek(buf.capacity)
consumed = buf.tell()
# remove processed data from buffer
stream.buffer = stream.buffer[consumed:]
# process unblocked streams
for stream_id in unblocked_streams:
stream = self._stream[stream_id]
# resume headers
http_events.extend(
self._handle_request_or_push_frame(
frame_type=FrameType.HEADERS,
frame_data=None,
stream=stream,
stream_ended=stream.receiving_ended and not stream.buffer,
)
)
stream.blocked = False
stream.blocked_frame_size = None
del self._blocked_stream_map[stream_id]
# resume processing
if stream.buffer:
http_events.extend(
self._receive_request_or_push_data(
stream,
b"",
stream.receiving_ended,
)
)
return http_events
def _validate_settings(self, settings: dict[int, int]) -> None:
for setting in [
Setting.ENABLE_CONNECT_PROTOCOL,
Setting.ENABLE_WEBTRANSPORT,
Setting.H3_DATAGRAM,
]:
if setting in settings and settings[setting] not in (0, 1):
raise SettingsError(f"{setting.name} setting must be 0 or 1")
if (
settings.get(Setting.H3_DATAGRAM) == 1
and self._quic._remote_max_datagram_frame_size is None
):
raise SettingsError(
"H3_DATAGRAM requires max_datagram_frame_size transport parameter"
)
if (
settings.get(Setting.ENABLE_WEBTRANSPORT) == 1
and settings.get(Setting.H3_DATAGRAM) != 1
):
raise SettingsError("ENABLE_WEBTRANSPORT requires H3_DATAGRAM")