Skip to content

Instantly share code, notes, and snippets.

@guest271314
Last active November 2, 2021 05:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guest271314/5f567517f94572e84026e76ec6e7cd89 to your computer and use it in GitHub Desktop.
Save guest271314/5f567517f94572e84026e76ec6e7cd89 to your computer and use it in GitHub Desktop.
WebTransport crashes the tab, does not stream data

WebTransport crashes tab on Chromium 97 when attempting to stream data.

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;
}
<!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>
// 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();
}
}
#!/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