[webtransport_h3_server] Support RFC version of HTTP Datagrams (#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
diff --git a/tools/webtransport/h3/capsule.py b/tools/webtransport/h3/capsule.py
index 8844dbc..74ca71a 100644
--- a/tools/webtransport/h3/capsule.py
+++ b/tools/webtransport/h3/capsule.py
@@ -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
@@ -25,6 +28,7 @@
     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
@@ -50,6 +54,7 @@
     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
diff --git a/tools/webtransport/h3/webtransport_h3_server.py b/tools/webtransport/h3/webtransport_h3_server.py
index c59ffcb..6384a5a 100644
--- a/tools/webtransport/h3/webtransport_h3_server.py
+++ b/tools/webtransport/h3/webtransport_h3_server.py
@@ -7,6 +7,7 @@
 import sys
 import threading
 import traceback
+from enum import IntEnum
 from urllib.parse import urlparse
 from typing import Any, Dict, List, Optional, Tuple
 
@@ -14,7 +15,7 @@
 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
@@ -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 =\
@@ -93,9 +97,9 @@
 
     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:
@@ -130,7 +134,7 @@
 
         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(
@@ -146,47 +150,74 @@
                     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)
 
-            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_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
+
+    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
@@ -308,7 +339,8 @@
             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.
@@ -366,9 +398,9 @@
                 "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
@@ -439,6 +471,7 @@
     """
     Simple in-memory store for session tickets.
     """
+
     def __init__(self) -> None:
         self.tickets: Dict[bytes, SessionTicket] = {}
 
@@ -460,6 +493,7 @@
     :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