WebTransport
crashes tab on Chromium 97 when attempting to stream data.
Last active
November 2, 2021 05:56
-
-
Save guest271314/5f567517f94572e84026e76ec6e7cd89 to your computer and use it in GitHub Desktop.
WebTransport crashes the tab, does not stream data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: sans-serif; | |
} | |
h1 { | |
margin: 0 auto; | |
width: fit-content; | |
} | |
h2 { | |
border-bottom: 1px dotted #333; | |
font-size: 120%; | |
font-weight: normal; | |
padding-bottom: 0.2em; | |
padding-top: 0.5em; | |
} | |
code { | |
background-color: #eee; | |
} | |
input[type=text], textarea { | |
font-family: monospace; | |
} | |
#top { | |
display: flex; | |
flex-direction: row-reverse; | |
flex-wrap: wrap; | |
justify-content: center; | |
} | |
#explanation { | |
border: 1px dotted black; | |
font-size: 90%; | |
height: fit-content; | |
margin-bottom: 1em; | |
padding: 1em; | |
width: 13em; | |
} | |
#tool { | |
flex-grow: 1; | |
margin: 0 auto; | |
max-width: 26em; | |
padding: 0 1em; | |
width: 26em; | |
} | |
.input-line { | |
display: flex; | |
} | |
.input-line input[type=text] { | |
flex-grow: 1; | |
margin: 0 0.5em; | |
} | |
textarea { | |
height: 3em; | |
width: 100%; | |
} | |
#send { | |
margin-top: 0.5em; | |
width: 15em; | |
} | |
#event-log { | |
border: 1px dotted black; | |
font-family: monospace; | |
height: 12em; | |
overflow: scroll; | |
padding-bottom: 1em; | |
padding-top: 1em; | |
} | |
.log-error { | |
color: darkred; | |
} | |
#explanation ul { | |
padding-left: 1em; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<title>WebTransport over HTTP/3 client</title> | |
<meta charset="utf-8"> | |
<!-- WebTransport origin trial token. See https://developer.chrome.com/origintrials/#/view_trial/793759434324049921 --> | |
<meta http-equiv="origin-trial" content="AkSQvBVsfMTgBtlakApX94hWGyBPQJXerRc2Aq8g/sKTMF+yG62+bFUB2yIxaK1furrNH3KNNeJV00UZSZHicw4AAABceyJvcmlnaW4iOiJodHRwczovL2dvb2dsZWNocm9tZS5naXRodWIuaW86NDQzIiwiZmVhdHVyZSI6IldlYlRyYW5zcG9ydCIsImV4cGlyeSI6MTY0Mzc1OTk5OX0="> | |
<script src="client.js"></script> | |
<link rel="stylesheet" href="client.css"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<body> | |
<div id="top"> | |
<div id="explanation"> | |
This tool can be used to connect to an arbitrary WebTransport server. | |
It has several limitations: | |
<ul> | |
<li>It can only send an entirety of a stream at once. Once the stream | |
is opened, all of the data is immediately sent, and the write side of | |
the steam is closed.</li> | |
<li>This tool does not listen to server-initiated bidirectional | |
streams.</li> | |
<li>Stream IDs are different from the one used by QUIC on the wire, as | |
the on-the-wire IDs are not exposed via the Web API.</li> | |
<li>The <code>WebTransport</code> object can be accessed using the developer console via <code>currentTransport</code>.</li> | |
</ul> | |
</div> | |
<div id="tool"> | |
<h1>WebTransport over HTTP/3 client</h1> | |
<div> | |
<h2>Establish WebTransport connection</h2> | |
<div class="input-line"> | |
<label for="url">URL:</label> | |
<input type="text" name="url" id="url" | |
value="https://localhost:4433/counter"> | |
<input type="button" id="connect" value="Connect" onclick="connect()"> | |
</div> | |
</div> | |
<div> | |
<h2>Send data over WebTransport</h2> | |
<form name="sending"> | |
<textarea name="data" id="data"></textarea> | |
<div> | |
<input type="radio" name="sendtype" value="datagram" | |
id="datagram" checked> | |
<label for="datagram">Send a datagram</label> | |
</div> | |
<div> | |
<input type="radio" name="sendtype" value="unidi" id="unidi-stream"> | |
<label for="unidi-stream">Open a unidirectional stream</label> | |
</div> | |
<div> | |
<input type="radio" name="sendtype" value="bidi" id="bidi-stream"> | |
<label for="bidi-stream">Open a bidirectional stream</label> | |
</div> | |
<input type="button" id="send" name="send" value="Send data" | |
disabled onclick="sendData()"> | |
</form> | |
</div> | |
<div> | |
<h2>Event log</h2> | |
<ul id="event-log"> | |
</ul> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Adds an entry to the event log on the page, optionally applying a specified | |
// CSS class. | |
let currentTransport, streamNumber, currentTransportDatagramWriter; | |
// "Connect" button handler. | |
async function connect() { | |
const url = document.getElementById('url').value; | |
try { | |
var transport = new WebTransport(url); | |
addToEventLog('Initiating connection...'); | |
} catch (e) { | |
addToEventLog('Failed to create connection object. ' + e, 'error'); | |
return; | |
} | |
try { | |
await transport.ready; | |
addToEventLog('Connection ready.'); | |
} catch (e) { | |
addToEventLog('Connection failed. ' + e, 'error'); | |
return; | |
} | |
transport.closed | |
.then(() => { | |
addToEventLog('Connection closed normally.'); | |
}) | |
.catch(() => { | |
addToEventLog('Connection closed abruptly.', 'error'); | |
}); | |
currentTransport = transport; | |
streamNumber = 1; | |
try { | |
currentTransportDatagramWriter = transport.datagrams.writable.getWriter(); | |
addToEventLog('Datagram writer ready.'); | |
} catch (e) { | |
addToEventLog('Sending datagrams not supported: ' + e, 'error'); | |
return; | |
} | |
readDatagrams(transport); | |
acceptUnidirectionalStreams(transport); | |
document.forms.sending.elements.send.disabled = false; | |
document.getElementById('connect').disabled = true; | |
} | |
// "Send data" button handler. | |
async function sendData() { | |
let form = document.forms.sending.elements; | |
let encoder = new TextEncoder('utf-8'); | |
let rawData = sending.data.value; | |
let data = encoder.encode(rawData); | |
let transport = currentTransport; | |
try { | |
switch (form.sendtype.value) { | |
case 'datagram': | |
await currentTransportDatagramWriter.write(data); | |
addToEventLog('Sent datagram: ' + rawData); | |
break; | |
case 'unidi': { | |
let stream = await transport.createUnidirectionalStream(); | |
let writer = stream.getWriter(); | |
await writer.write(data); | |
await writer.close(); | |
addToEventLog('Sent a unidirectional stream with data: ' + rawData); | |
break; | |
} | |
case 'bidi': { | |
let stream = await transport.createBidirectionalStream(); | |
let number = streamNumber++; | |
readFromIncomingStream(stream, number); | |
let writer = stream.writable.getWriter(); | |
await writer.write(data); | |
await writer.close(); | |
addToEventLog( | |
'Opened bidirectional stream #' + number + | |
' with data: ' + rawData); | |
break; | |
} | |
} | |
} catch (e) { | |
addToEventLog('Error while sending data: ' + e, 'error'); | |
} | |
} | |
// Reads datagrams from |transport| into the event log until EOF is reached. | |
async function readDatagrams(transport) { | |
try { | |
var reader = transport.datagrams.readable.getReader(); | |
addToEventLog('Datagram reader ready.'); | |
} catch (e) { | |
addToEventLog('Receiving datagrams not supported: ' + e, 'error'); | |
return; | |
} | |
let decoder = new TextDecoder('utf-8'); | |
try { | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) { | |
addToEventLog('Done reading datagrams!'); | |
return; | |
} | |
let data = decoder.decode(value); | |
addToEventLog('Datagram received: ' + data); | |
} | |
} catch (e) { | |
addToEventLog('Error while reading datagrams: ' + e, 'error'); | |
} | |
} | |
async function acceptUnidirectionalStreams(transport) { | |
let reader = transport.incomingUnidirectionalStreams.getReader(); | |
try { | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) { | |
addToEventLog('Done accepting unidirectional streams!'); | |
return; | |
} | |
let stream = value; | |
let number = streamNumber++; | |
addToEventLog('New incoming unidirectional stream #' + number); | |
readFromIncomingStream(stream, number); | |
} | |
} catch (e) { | |
addToEventLog('Error while accepting streams: ' + e, 'error'); | |
} | |
} | |
async function readFromIncomingStream(stream, number) { | |
let decoder = new TextDecoderStream('utf-8'); | |
if (stream instanceof WebTransportBidirectionalStream) { | |
stream = stream.readable; | |
} | |
let reader = stream.pipeThrough(decoder).getReader(); | |
try { | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) { | |
addToEventLog('Stream #' + number + ' closed'); | |
return; | |
} | |
// let data = value; | |
console.log(value); | |
// addToEventLog('Received data on stream #' + number + ': ' + data); | |
} | |
} catch (e) { | |
addToEventLog( | |
'Error while reading from stream #' + number + ': ' + e, 'error'); | |
addToEventLog(' ' + e.message); | |
} | |
} | |
function addToEventLog(text, severity = 'info') { | |
let log = document.getElementById('event-log'); | |
let mostRecentEntry = log.lastElementChild; | |
let entry = document.createElement('li'); | |
entry.innerText = text; | |
entry.className = 'log-' + severity; | |
log.appendChild(entry); | |
// If the most recent entry in the log was visible, scroll the log to the | |
// newly added element. | |
if (mostRecentEntry != null && | |
mostRecentEntry.getBoundingClientRect().top < | |
log.getBoundingClientRect().bottom) { | |
entry.scrollIntoView(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S python3 -u | |
# Copyright 2020 Google LLC | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# https://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
""" | |
An example WebTransport over HTTP/3 server based on the aioquic library. | |
Processes incoming streams and datagrams, and | |
replies with the ASCII-encoded length of the data sent in bytes. | |
Example use: | |
python3 webtransport_server.py certificate.pem certificate.key | |
Example use from JavaScript: | |
let transport = new WebTransport("https://localhost:4433/counter"); | |
await transport.ready; | |
let stream = await transport.createBidirectionalStream(); | |
let encoder = new TextEncoder(); | |
let writer = stream.writable.getWriter(); | |
await writer.write(encoder.encode("Hello, world!")) | |
writer.close(); | |
console.log(await new Response(stream.readable).text()); | |
This will output "13" (the length of "Hello, world!") into the console. | |
""" | |
# ---- Dependencies ---- | |
# | |
# This server only depends on Python standard library and aioquic 0.9.15 or | |
# later. See https://github.com/aiortc/aioquic for instructions on how to | |
# install aioquic. | |
# | |
# ---- Certificates ---- | |
# | |
# HTTP/3 always operates using TLS, meaning that running a WebTransport over | |
# HTTP/3 server requires a valid TLS certificate. The easiest way to do this | |
# is to get a certificate from a real publicly trusted CA like | |
# <https://letsencrypt.org/>. | |
# https://developers.google.com/web/fundamentals/security/encrypt-in-transit/enable-https | |
# contains a detailed explanation of how to achieve that. | |
# | |
# As an alternative, Chromium can be instructed to trust a self-signed | |
# certificate using command-line flags. Here are step-by-step instructions on | |
# how to do that: | |
# | |
# 1. Generate a certificate and a private key: | |
# openssl req -newkey rsa:2048 -nodes -keyout certificate.key \ | |
# -x509 -out certificate.pem -subj '/CN=Test Certificate' \ | |
# -addext "subjectAltName = DNS:localhost" | |
# | |
# 2. Compute the fingerprint of the certificate: | |
# openssl x509 -pubkey -noout -in certificate.pem | | |
# openssl rsa -pubin -outform der | | |
# openssl dgst -sha256 -binary | base64 | |
# The result should be a base64-encoded blob that looks like this: | |
# "Gi/HIwdiMcPZo2KBjnstF5kQdLI5bPrYJ8i3Vi6Ybck=" | |
# | |
# 3. Pass a flag to Chromium indicating what host and port should be allowed | |
# to use the self-signed certificate. For instance, if the host is | |
# localhost, and the port is 4433, the flag would be: | |
# --origin-to-force-quic-on=localhost:4433 | |
# | |
# 4. Pass a flag to Chromium indicating which certificate needs to be trusted. | |
# For the example above, that flag would be: | |
# --ignore-certificate-errors-spki-list=Gi/HIwdiMcPZo2KBjnstF5kQdLI5bPrYJ8i3Vi6Ybck= | |
# | |
# See https://www.chromium.org/developers/how-tos/run-chromium-with-flags for | |
# details on how to run Chromium with flags. | |
import itertools | |
import os | |
import time | |
import subprocess | |
from shlex import split | |
from random import choice | |
from string import digits | |
import argparse | |
import asyncio | |
import logging | |
from collections import defaultdict | |
from typing import Dict, Optional | |
from aioquic.asyncio import QuicConnectionProtocol, serve | |
from aioquic.h3.connection import H3_ALPN, H3Connection, Setting | |
from aioquic.h3.events import H3Event, HeadersReceived, WebTransportStreamDataReceived, DatagramReceived | |
from aioquic.quic.configuration import QuicConfiguration | |
from aioquic.quic.connection import stream_is_unidirectional | |
from aioquic.quic.events import ProtocolNegotiated, StreamReset, QuicEvent | |
BIND_ADDRESS = '::1' | |
BIND_PORT = 4433 | |
logger = logging.getLogger(__name__) | |
# https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-05#section-9.1 | |
H3_DATAGRAM_05 = 0xffd277 | |
class H3ConnectionWithDatagram(H3Connection): | |
def __init__(self, *args, **kwargs) -> None: | |
super().__init__(*args, **kwargs) | |
# Overrides H3Connection._validate_settings() to enable HTTP Datagram | |
def _validate_settings(self, settings: Dict[int, int]) -> None: | |
settings[Setting.H3_DATAGRAM] = 1 | |
return super()._validate_settings(settings) | |
# Overrides H3Connection._get_local_settings() to enable HTTP Datagram | |
def _get_local_settings(self) -> Dict[int, int]: | |
settings = super()._get_local_settings() | |
settings[H3_DATAGRAM_05] = 1 | |
return settings | |
# CounterHandler implements a really simple protocol: | |
# - For every incoming bidirectional stream, it counts bytes it receives on | |
# that stream until the stream is closed, and then replies with that byte | |
# count on the same stream. | |
# - For every incoming unidirectional stream, it counts bytes it receives on | |
# that stream until the stream is closed, and then replies with that byte | |
# count on a new unidirectional stream. | |
# - For every incoming datagram, it sends a datagram with the length of | |
# datagram that was just received. | |
class CounterHandler: | |
def __init__(self, session_id, http: H3ConnectionWithDatagram) -> None: | |
self._session_id = session_id | |
self._http = http | |
self._counters = defaultdict(int) | |
def h3_event_received(self, event: H3Event) -> None: | |
if isinstance(event, DatagramReceived): | |
payload = str(len(event.data)).encode('ascii') | |
self._http.send_datagram(self._session_id, payload) | |
if isinstance(event, WebTransportStreamDataReceived): | |
if event.data != b'': | |
for c in iter(lambda: ''.join(choice(digits) for i in range(512)).encode('utf8'), b''): | |
if c is not None: | |
self._http._quic.send_stream_data( | |
event.stream_id, c, end_stream=False) | |
print(c) | |
# break | |
''' | |
cmd = 'parec', '-d', 'alsa_output.pci-0000_00_14.2.analog-stereo.monitor' | |
process = subprocess.Popen(cmd, stdout=subprocess.PIPE) | |
os.set_blocking(process.stdout.fileno(), False) | |
for c in iter(lambda: process.stdout.read(512), b''): | |
if c is not None: | |
self._http._quic.send_stream_data( | |
event.stream_id, c, end_stream=False) | |
print(c) | |
''' | |
self._counters[event.stream_id] += len(event.data) | |
if event.stream_ended: | |
if stream_is_unidirectional(event.stream_id): | |
response_id = self._http.create_webtransport_stream( | |
self._session_id, is_unidirectional=True) | |
else: | |
response_id = event.stream_id | |
self._http._quic.send_stream_data( | |
response_id, b'', end_stream=True) | |
self.stream_closed(event.stream_id) | |
def stream_closed(self, stream_id: int) -> None: | |
try: | |
del self._counters[stream_id] | |
except KeyError: | |
pass | |
# WebTransportProtocol handles the beginning of a WebTransport connection: it | |
# responses to an extended CONNECT method request, and routes the transport | |
# events to a relevant handler (in this example, CounterHandler). | |
class WebTransportProtocol(QuicConnectionProtocol): | |
def __init__(self, *args, **kwargs) -> None: | |
super().__init__(*args, **kwargs) | |
self._http: Optional[H3ConnectionWithDatagram] = None | |
self._handler: Optional[CounterHandler] = None | |
def quic_event_received(self, event: QuicEvent) -> None: | |
if isinstance(event, ProtocolNegotiated): | |
self._http = H3ConnectionWithDatagram( | |
self._quic, enable_webtransport=True) | |
elif isinstance(event, StreamReset) and self._handler is not None: | |
# Streams in QUIC can be closed in two ways: normal (FIN) and | |
# abnormal (resets). FIN is handled by the handler; the code | |
# below handles the resets. | |
self._handler.stream_closed(event.stream_id) | |
if self._http is not None: | |
for h3_event in self._http.handle_event(event): | |
self._h3_event_received(h3_event) | |
def _h3_event_received(self, event: H3Event) -> None: | |
if isinstance(event, HeadersReceived): | |
headers = {} | |
for header, value in event.headers: | |
headers[header] = value | |
if (headers.get(b":method") == b"CONNECT" and | |
headers.get(b":protocol") == b"webtransport"): | |
self._handshake_webtransport(event.stream_id, headers) | |
else: | |
self._send_response(event.stream_id, 400, end_stream=True) | |
if self._handler: | |
self._handler.h3_event_received(event) | |
def _handshake_webtransport(self, | |
stream_id: int, | |
request_headers: Dict[bytes, bytes]) -> None: | |
authority = request_headers.get(b":authority") | |
path = request_headers.get(b":path") | |
if authority is None or path is None: | |
# `:authority` and `:path` must be provided. | |
self._send_response(stream_id, 400, end_stream=True) | |
return | |
if path == b"/counter": | |
assert(self._handler is None) | |
self._handler = CounterHandler(stream_id, self._http) | |
self._send_response(stream_id, 200) | |
else: | |
self._send_response(stream_id, 404, end_stream=True) | |
def _send_response(self, | |
stream_id: int, | |
status_code: int, | |
end_stream=False) -> None: | |
headers = [(b":status", str(status_code).encode())] | |
self._http.send_headers( | |
stream_id=stream_id, headers=headers, end_stream=end_stream) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('certificate') | |
parser.add_argument('key') | |
args = parser.parse_args() | |
configuration = QuicConfiguration( | |
alpn_protocols=H3_ALPN, | |
is_client=False, | |
max_datagram_frame_size=65536, | |
) | |
configuration.load_cert_chain(args.certificate, args.key) | |
loop = asyncio.get_event_loop() | |
loop.run_until_complete( | |
serve( | |
BIND_ADDRESS, | |
BIND_PORT, | |
configuration=configuration, | |
create_protocol=WebTransportProtocol, | |
)) | |
try: | |
logging.info( | |
"Listening on https://{}:{}".format(BIND_ADDRESS, BIND_PORT)) | |
loop.run_forever() | |
except KeyboardInterrupt: | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment