Skip to content

Commit

Permalink
[webtransport_h3_server] Support RFC version of HTTP Datagrams (web-p…
Browse files Browse the repository at this point in the history
…latform-tests#39201)

* [webtransport_h3_server] Support RFC version of HTTP Datagrams

There is no significant semantics changes between draft04[1] and rfc[2].
* Capsule protocol was simlified.
* HTTP Setting value has changed.

[1]
https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-04
[2] https://datatracker.ietf.org/doc/html/rfc9297
  • Loading branch information
bashi authored and cookiecrook committed Apr 8, 2023
1 parent 7a205e0 commit 0d45236
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 64 deletions.
15 changes: 10 additions & 5 deletions tools/webtransport/h3/capsule.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

class CapsuleType(IntEnum):
# Defined in
# https://www.ietf.org/archive/id/draft-ietf-masque-h3-datagram-03.html.
DATAGRAM = 0xff37a0
REGISTER_DATAGRAM_CONTEXT = 0xff37a1
REGISTER_DATAGRAM_NO_CONTEXT = 0xff37a2
CLOSE_DATAGRAM_CONTEXT = 0xff37a3
# https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-04#section-8.2
DATAGRAM_DRAFT04 = 0xff37a0
REGISTER_DATAGRAM_CONTEXT_DRAFT04 = 0xff37a1
REGISTER_DATAGRAM_NO_CONTEXT_DRAFT04 = 0xff37a2
CLOSE_DATAGRAM_CONTEXT_DRAFT04 = 0xff37a3
# Defined in
# https://datatracker.ietf.org/doc/html/rfc9297#section-5.4
DATAGRAM_RFC = 0x00
# Defined in
# https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-01.html.
CLOSE_WEBTRANSPORT_SESSION = 0x2843
Expand All @@ -25,6 +28,7 @@ class H3Capsule:
Represents the Capsule concept defined in
https://ietf-wg-masque.github.io/draft-ietf-masque-h3-datagram/draft-ietf-masque-h3-datagram.html#name-capsules.
"""

def __init__(self, type: int, data: bytes) -> None:
"""
:param type the type of this Capsule. We don't use CapsuleType here
Expand All @@ -50,6 +54,7 @@ class H3CapsuleDecoder:
A decoder of H3Capsule. This is a streaming decoder and can handle multiple
decoders.
"""

def __init__(self) -> None:
self._buffer: Optional[Buffer] = None
self._type: Optional[int] = None
Expand Down
152 changes: 93 additions & 59 deletions tools/webtransport/h3/webtransport_h3_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import sys
import threading
import traceback
from enum import IntEnum
from urllib.parse import urlparse
from typing import Any, Dict, List, Optional, Tuple

# TODO(bashi): Remove import check suppressions once aioquic dependency is resolved.
from aioquic.buffer import Buffer # type: ignore
from aioquic.asyncio import QuicConnectionProtocol, serve # type: ignore
from aioquic.asyncio.client import connect # type: ignore
from aioquic.h3.connection import H3_ALPN, FrameType, H3Connection, ProtocolError, Setting # type: ignore
from aioquic.h3.connection import H3_ALPN, FrameType, H3Connection, ProtocolError # type: ignore
from aioquic.h3.events import H3Event, HeadersReceived, WebTransportStreamDataReceived, DatagramReceived, DataReceived # type: ignore
from aioquic.quic.configuration import QuicConfiguration # type: ignore
from aioquic.quic.connection import logger as quic_connection_logger # type: ignore
Expand Down Expand Up @@ -44,46 +45,49 @@
quic_connection_logger.setLevel(logging.WARNING)


class H3ConnectionWithDatagram04(H3Connection):
class H3DatagramSetting(IntEnum):
# https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-04#section-8.1
DRAFT04 = 0xffd277
# https://datatracker.ietf.org/doc/html/rfc9220#section-5-2.2.1
RFC = 0x33


class H3ConnectionWithDatagram(H3Connection):
"""
A H3Connection subclass, to make it work with the latest
HTTP Datagram protocol.
"""
H3_DATAGRAM_04 = 0xffd277
# https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-h3-websockets-00#section-5
# https://datatracker.ietf.org/doc/html/rfc9220#name-iana-considerations
ENABLE_CONNECT_PROTOCOL = 0x08

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._supports_h3_datagram_04 = False
self._datagram_setting: Optional[H3DatagramSetting] = None

def _validate_settings(self, settings: Dict[int, int]) -> None:
H3_DATAGRAM_04 = H3ConnectionWithDatagram04.H3_DATAGRAM_04
if H3_DATAGRAM_04 in settings and settings[H3_DATAGRAM_04] == 1:
settings[Setting.H3_DATAGRAM] = 1
self._supports_h3_datagram_04 = True
return super()._validate_settings(settings)
super()._validate_settings(settings)
if settings.get(H3DatagramSetting.RFC) == 1:
self._datagram_setting = H3DatagramSetting.RFC
elif settings.get(H3DatagramSetting.DRAFT04) == 1:
self._datagram_setting = H3DatagramSetting.DRAFT04

def _get_local_settings(self) -> Dict[int, int]:
H3_DATAGRAM_04 = H3ConnectionWithDatagram04.H3_DATAGRAM_04
settings = super()._get_local_settings()
settings[H3_DATAGRAM_04] = 1
settings[H3ConnectionWithDatagram04.ENABLE_CONNECT_PROTOCOL] = 1
settings[H3DatagramSetting.RFC] = 1
settings[H3DatagramSetting.DRAFT04] = 1
settings[H3ConnectionWithDatagram.ENABLE_CONNECT_PROTOCOL] = 1
return settings

@property
def supports_h3_datagram_04(self) -> bool:
"""
True if the client supports the latest HTTP Datagram protocol.
"""
return self._supports_h3_datagram_04
def datagram_setting(self) -> Optional[H3DatagramSetting]:
return self._datagram_setting


class WebTransportH3Protocol(QuicConnectionProtocol):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._handler: Optional[Any] = None
self._http: Optional[H3ConnectionWithDatagram04] = None
self._http: Optional[H3ConnectionWithDatagram] = None
self._session_stream_id: Optional[int] = None
self._close_info: Optional[Tuple[int, bytes]] = None
self._capsule_decoder_for_session_stream: H3CapsuleDecoder =\
Expand All @@ -93,9 +97,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:

def quic_event_received(self, event: QuicEvent) -> None:
if isinstance(event, ProtocolNegotiated):
self._http = H3ConnectionWithDatagram04(
self._http = H3ConnectionWithDatagram(
self._quic, enable_webtransport=True)
if not self._http.supports_h3_datagram_04:
if self._http.datagram_setting != H3DatagramSetting.DRAFT04:
self._allow_datagrams = True

if self._http is not None:
Expand Down Expand Up @@ -130,7 +134,7 @@ def _h3_event_received(self, event: H3Event) -> None:

if isinstance(event, DataReceived) and\
self._session_stream_id == event.stream_id:
if self._http and not self._http.supports_h3_datagram_04 and\
if self._http and not self._http.datagram_setting and\
len(event.data) > 0:
raise ProtocolError('Unexpected data on the session stream')
self._receive_data_on_session_stream(
Expand All @@ -146,47 +150,74 @@ def _h3_event_received(self, event: H3Event) -> None:
self._handler.datagram_received(data=event.data)

def _receive_data_on_session_stream(self, data: bytes, fin: bool) -> None:
self._capsule_decoder_for_session_stream.append(data)
assert self._http is not None
if len(data) > 0:
self._capsule_decoder_for_session_stream.append(data)
if fin:
self._capsule_decoder_for_session_stream.final()
for capsule in self._capsule_decoder_for_session_stream:
if capsule.type in {CapsuleType.DATAGRAM,
CapsuleType.REGISTER_DATAGRAM_CONTEXT,
CapsuleType.CLOSE_DATAGRAM_CONTEXT}:
raise ProtocolError(
f"Unimplemented capsule type: {capsule.type}")
if capsule.type in {CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT,
CapsuleType.CLOSE_WEBTRANSPORT_SESSION}:
# We'll handle this case below.
pass
else:
# We should ignore unknown capsules.
continue

if self._close_info is not None:
raise ProtocolError((
"Receiving a capsule with type = {} after receiving " +
"CLOSE_WEBTRANSPORT_SESSION").format(capsule.type))
assert self._http.datagram_setting is not None
if self._http.datagram_setting == H3DatagramSetting.RFC:
self._receive_h3_datagram_rfc_capsule_data(
capsule=capsule, fin=fin)
elif self._http.datagram_setting == H3DatagramSetting.DRAFT04:
self._receive_h3_datagram_draft04_capsule_data(
capsule=capsule, fin=fin)

def _receive_h3_datagram_rfc_capsule_data(self, capsule: H3Capsule, fin: bool) -> None:
if capsule.type == CapsuleType.DATAGRAM_RFC:
raise ProtocolError(
f"Unimplemented capsule type: {capsule.type}")
elif capsule.type == CapsuleType.CLOSE_WEBTRANSPORT_SESSION:
self._set_close_info_and_may_close_session(
data=capsule.data, fin=fin)
else:
# Ignore unknown capsules.
return

if capsule.type == CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT:
buffer = Buffer(data=capsule.data)
format_type = buffer.pull_uint_var()
# https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#name-datagram-format-type
WEBTRANPORT_FORMAT_TYPE = 0xff7c00
if format_type != WEBTRANPORT_FORMAT_TYPE:
raise ProtocolError(
"Unexpected datagram format type: {}".format(
format_type))
self._allow_datagrams = True
elif capsule.type == CapsuleType.CLOSE_WEBTRANSPORT_SESSION:
buffer = Buffer(data=capsule.data)
code = buffer.pull_uint32()
# 4 bytes for the uint32.
reason = buffer.pull_bytes(len(capsule.data) - 4)
# TODO(yutakahirano): Make sure `reason` is a UTF-8 text.
self._close_info = (code, reason)
if fin:
self._call_session_closed(self._close_info, abruptly=False)
def _receive_h3_datagram_draft04_capsule_data(
self, capsule: H3Capsule, fin: bool) -> None:
if capsule.type in {CapsuleType.DATAGRAM_DRAFT04,
CapsuleType.REGISTER_DATAGRAM_CONTEXT_DRAFT04,
CapsuleType.CLOSE_DATAGRAM_CONTEXT_DRAFT04}:
raise ProtocolError(
f"Unimplemented capsule type: {capsule.type}")
if capsule.type in {CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT_DRAFT04,
CapsuleType.CLOSE_WEBTRANSPORT_SESSION}:
# We'll handle this case below.
pass
else:
# We should ignore unknown capsules.
return

if capsule.type == CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT_DRAFT04:
buffer = Buffer(data=capsule.data)
format_type = buffer.pull_uint_var()
# https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#name-datagram-format-type
WEBTRANPORT_FORMAT_TYPE = 0xff7c00
if format_type != WEBTRANPORT_FORMAT_TYPE:
raise ProtocolError(
"Unexpected datagram format type: {}".format(
format_type))
self._allow_datagrams = True
elif capsule.type == CapsuleType.CLOSE_WEBTRANSPORT_SESSION:
self._set_close_info_and_may_close_session(
data=capsule.data, fin=fin)

def _set_close_info_and_may_close_session(
self, data: bytes, fin: bool) -> None:
buffer = Buffer(data=data)
code = buffer.pull_uint32()
# 4 bytes for the uint32.
reason = buffer.pull_bytes(len(data) - 4)
# TODO(bashi): Make sure `reason` is a UTF-8 text.
self._close_info = (code, reason)
if fin:
self._call_session_closed(self._close_info, abruptly=False)

def _send_error_response(self, stream_id: int, status_code: int) -> None:
assert self._http is not None
Expand Down Expand Up @@ -308,7 +339,8 @@ def close(self, close_info: Optional[Tuple[int, bytes]]) -> None:
buffer.push_bytes(reason)
capsule =\
H3Capsule(CapsuleType.CLOSE_WEBTRANSPORT_SESSION, buffer.data)
self._http.send_data(session_stream_id, capsule.encode(), end_stream=False)
self._http.send_data(
session_stream_id, capsule.encode(), end_stream=False)

self._http.send_data(session_stream_id, b'', end_stream=True)
# TODO(yutakahirano): Reset all other streams.
Expand Down Expand Up @@ -366,9 +398,9 @@ def send_datagram(self, data: bytes) -> None:
"Sending a datagram while that's now allowed - discarding it")
return
flow_id = self.session_id
if self._http.supports_h3_datagram_04:
# The REGISTER_DATAGRAM_NO_CONTEXT capsule was on the session
# stream, so we must have the ID of the stream.
if self._http.datagram_setting is not None:
# We must have a WebTransport Session ID at this point because
# an extended CONNECT request is already received.
assert self._protocol._session_stream_id is not None
# TODO(yutakahirano): Make sure if this is the correct logic.
# Chrome always use 0 for the initial stream and the initial flow
Expand Down Expand Up @@ -439,6 +471,7 @@ class SessionTicketStore:
"""
Simple in-memory store for session tickets.
"""

def __init__(self) -> None:
self.tickets: Dict[bytes, SessionTicket] = {}

Expand All @@ -460,6 +493,7 @@ class WebTransportH3Server:
:param key_path: Path to key file to use.
:param logger: a Logger object for this server.
"""

def __init__(self, host: str, port: int, doc_root: str, cert_path: str,
key_path: str, logger: Optional[logging.Logger]) -> None:
self.host = host
Expand Down

0 comments on commit 0d45236

Please sign in to comment.