Skip to content

Commit 904652f

Browse files
authored
[WebTransport] Fix up capsule handling logic (#31062)
- We need to check DataReceived events, not WebTransportDataReceived, on the CONNECT stream. - Fix a bug on H3CapsuleDecoder that handles empty bytes incorrectly. - Introduce `_allow_datagrams` and set it to true when the server sees REGISTER_DATAGRAM_NO_CONTEXT (when the datagram-04 is supported). - When `_allow_datagrams` is false ignore datagrams.
1 parent 3ce28a9 commit 904652f

File tree

3 files changed

+49
-25
lines changed

3 files changed

+49
-25
lines changed

tools/webtransport/h3/capsule.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def append(self, data: bytes) -> None:
6060
"""
6161
assert not self._final
6262

63+
if len(data) == 0:
64+
return
6365
if self._buffer:
6466
remaining = self._buffer.pull_bytes(
6567
self._buffer.capacity - self._buffer.tell())

tools/webtransport/h3/test_capsule.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ def test_final(self) -> None:
103103
self.assertEqual(capsule1.type, 1, 'type')
104104
self.assertEqual(capsule1.data, b'a', 'data')
105105

106+
@pytest.mark.skipif(not has_aioquic, reason='not having aioquic')
107+
def test_empty_bytes_before_fin(self) -> None:
108+
decoder = H3CapsuleDecoder()
109+
decoder.append(b'')
110+
decoder.final()
111+
112+
it = iter(decoder)
113+
with self.assertRaises(StopIteration):
114+
next(it)
115+
106116
@pytest.mark.skipif(not has_aioquic, reason='not having aioquic')
107117
def test_final_invalid(self) -> None:
108118
decoder = H3CapsuleDecoder()

tools/webtransport/h3/webtransport_h3_server.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from aioquic.asyncio import QuicConnectionProtocol, serve # type: ignore
1313
from aioquic.asyncio.client import connect # type: ignore
1414
from aioquic.h3.connection import H3_ALPN, FrameType, H3Connection, ProtocolError, Setting # type: ignore
15-
from aioquic.h3.events import H3Event, HeadersReceived, WebTransportStreamDataReceived, DatagramReceived # type: ignore
15+
from aioquic.h3.events import H3Event, HeadersReceived, WebTransportStreamDataReceived, DatagramReceived, DataReceived # type: ignore
1616
from aioquic.quic.configuration import QuicConfiguration # type: ignore
1717
from aioquic.quic.connection import stream_is_unidirectional # type: ignore
1818
from aioquic.quic.events import QuicEvent, ProtocolNegotiated, ConnectionTerminated, StreamReset # type: ignore
@@ -67,6 +67,7 @@ def supports_h3_datagram_04(self) -> bool:
6767
"""
6868
return self._supports_h3_datagram_04
6969

70+
7071
class WebTransportH3Protocol(QuicConnectionProtocol):
7172
def __init__(self, *args: Any, **kwargs: Any) -> None:
7273
super().__init__(*args, **kwargs)
@@ -77,11 +78,14 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
7778
self._capsule_decoder_for_session_stream: H3CapsuleDecoder =\
7879
H3CapsuleDecoder()
7980
self._allow_calling_session_closed = True
81+
self._allow_datagrams = False
8082

8183
def quic_event_received(self, event: QuicEvent) -> None:
8284
if isinstance(event, ProtocolNegotiated):
8385
self._http = H3ConnectionWithDatagram04(
8486
self._quic, enable_webtransport=True)
87+
if not self._http.supports_h3_datagram_04:
88+
self._allow_datagrams = True
8589

8690
if self._http is not None:
8791
for http_event in self._http.handle_event(event):
@@ -110,7 +114,7 @@ def _h3_event_received(self, event: H3Event) -> None:
110114
else:
111115
self._send_error_response(event.stream_id, 400)
112116

113-
if isinstance(event, WebTransportStreamDataReceived) and\
117+
if isinstance(event, DataReceived) and\
114118
self._session_stream_id == event.stream_id:
115119
if self._http and not self._http.supports_h3_datagram_04 and\
116120
len(event.data) > 0:
@@ -124,47 +128,51 @@ def _h3_event_received(self, event: H3Event) -> None:
124128
data=event.data,
125129
stream_ended=event.stream_ended)
126130
elif isinstance(event, DatagramReceived):
127-
self._handler.datagram_received(data=event.data)
131+
if self._allow_datagrams:
132+
self._handler.datagram_received(data=event.data)
128133

129134
def _receive_data_on_session_stream(self, data: bytes, fin: bool) -> None:
130135
self._capsule_decoder_for_session_stream.append(data)
131136
if fin:
132137
self._capsule_decoder_for_session_stream.final()
133138
for capsule in self._capsule_decoder_for_session_stream:
134-
if capsule.type == CapsuleType.DATAGRAM:
135-
raise ProtocolError(
136-
"Unimplemented capsule type: {}".format(capsule.type))
137-
if capsule.type == CapsuleType.REGISTER_DATAGRAM_CONTEXT:
139+
if capsule.type in {CapsuleType.DATAGRAM,
140+
CapsuleType.REGISTER_DATAGRAM_CONTEXT,
141+
CapsuleType.CLOSE_DATAGRAM_CONTEXT}:
138142
raise ProtocolError(
139143
"Unimplemented capsule type: {}".format(capsule.type))
140-
elif capsule.type == CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT:
141-
# TODO(yutakahirano): Check the Datagram Format Type.
142-
# TODO(yutakahirano): Check that this arrives before any
143-
# datagrams/streams requests.
144-
if self._close_info is not None:
145-
raise ProtocolError(
146-
"REGISTER_DATAGRAM_NO_CONTEXT after " +
147-
"CLOSE_WEBTRANSPORT_SESSION")
148-
elif capsule.type == CapsuleType.CLOSE_DATAGRAM_CONTEXT:
149-
raise ProtocolError(
150-
"Unimplemented capsule type: {}".format(capsule.type))
151-
elif capsule.type == CapsuleType.CLOSE_WEBTRANSPORT_SESSION:
152-
if self._close_info is not None:
153-
raise ProtocolError(
154-
"CLOSE_WEBTRANSPORT_SESSION arrives twice")
144+
if capsule.type in {CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT,
145+
CapsuleType.CLOSE_WEBTRANSPORT_SESSION}:
146+
# We'll handle this case below.
147+
pass
155148
else:
156149
# We should ignore unknown capsules.
157150
continue
158151

152+
if self._close_info is not None:
153+
raise ProtocolError((
154+
"Receiving a capsule with type = {} after receiving " +
155+
"CLOSE_WEBTRANSPORT_SESSION").format(capsule.type))
156+
157+
if capsule.type == CapsuleType.REGISTER_DATAGRAM_NO_CONTEXT:
158+
buffer = Buffer(data=capsule.data)
159+
format_type = buffer.pull_uint_var()
160+
# https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#name-datagram-format-type
161+
WEBTRANPORT_FORMAT_TYPE = 0xff7c00
162+
if format_type != WEBTRANPORT_FORMAT_TYPE:
163+
raise ProtocolError(
164+
"Unexpected datagram format type: {}".format(
165+
format_type))
166+
self._allow_datagrams = True
167+
elif capsule.type == CapsuleType.CLOSE_WEBTRANSPORT_SESSION:
159168
buffer = Buffer(data=capsule.data)
160169
code = buffer.pull_uint32()
161170
# TODO(yutakahirano): Make sure `reason` is a
162171
# UTF-8 text.
163172
reason = buffer.data
164173
self._close_info = (code, reason)
165-
# TODO(yutakahirano): Make sure this is the last capsule.
166-
if fin:
167-
self._call_session_closed(self._close_info, abruptly=False)
174+
if fin:
175+
self._call_session_closed(self._close_info, abruptly=False)
168176

169177
def _send_error_response(self, stream_id: int, status_code: int) -> None:
170178
assert self._http is not None
@@ -337,6 +345,10 @@ def send_datagram(self, data: bytes) -> None:
337345
338346
:param data: The data to send.
339347
"""
348+
if not self._protocol._allow_datagrams:
349+
_logger.warn(
350+
"Sending a datagram while that's now allowed - discarding it")
351+
return
340352
flow_id = self.session_id
341353
if self._http.supports_h3_datagram_04:
342354
# The REGISTER_DATAGRAM_NO_CONTEXT capsule was on the session

0 commit comments

Comments
 (0)