From 49bc41ee876fc3a0702a683c4bcd30c0830798bf Mon Sep 17 00:00:00 2001 From: Christopher Young Date: Wed, 9 Aug 2023 15:55:33 +0800 Subject: [PATCH] Update webtransport example --- examples/static-dir-list/src/main.rs | 2 + examples/webtransport/Cargo.toml | 2 +- examples/webtransport/src/main.rs | 67 +-------- examples/webtransport/static/client.css | 84 ++++++++++++ examples/webtransport/static/client.html | 66 +++++++++ examples/webtransport/static/client.js | 168 +++++++++++++++++++++++ 6 files changed, 326 insertions(+), 63 deletions(-) create mode 100644 examples/webtransport/static/client.css create mode 100644 examples/webtransport/static/client.html create mode 100644 examples/webtransport/static/client.js diff --git a/examples/static-dir-list/src/main.rs b/examples/static-dir-list/src/main.rs index 50c44c5ef..ba0563d17 100644 --- a/examples/static-dir-list/src/main.rs +++ b/examples/static-dir-list/src/main.rs @@ -9,6 +9,8 @@ async fn main() { StaticDir::new([ "static-dir-list/static/boy", "static-dir-list/static/girl", + "static/boy", + "static/girl", ]) .defaults("index.html") .listing(true), diff --git a/examples/webtransport/Cargo.toml b/examples/webtransport/Cargo.toml index f389ad1a1..99f89121a 100644 --- a/examples/webtransport/Cargo.toml +++ b/examples/webtransport/Cargo.toml @@ -8,7 +8,7 @@ publish.workspace = true [dependencies] anyhow.workspace = true futures-util.workspace = true -salvo = { workspace = true, features = ["quinn", "anyhow"] } +salvo = { workspace = true, features = ["quinn", "anyhow", "serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true diff --git a/examples/webtransport/src/main.rs b/examples/webtransport/src/main.rs index b3a240ab4..7686b027c 100644 --- a/examples/webtransport/src/main.rs +++ b/examples/webtransport/src/main.rs @@ -1,4 +1,4 @@ -use std::{time::Duration}; +use std::time::Duration; use anyhow::{Context, Result}; use bytes::{BufMut, Bytes, BytesMut}; @@ -113,20 +113,16 @@ where Ok(()) } -#[handler] -async fn index(res: &mut Response) { - res.render(Text::Html(INDEX_HTML)); -} - #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); + let cert = include_bytes!("../certs/cert.pem").to_vec(); let key = include_bytes!("../certs/key.pem").to_vec(); - let router = Router::new() - .get(index) - .push(Router::with_path("webtransport").handle(connect)); + let router = Router::new().push(Router::with_path("counter").handle(connect)).push( + Router::with_path("<**path>").get(StaticDir::new(["webtransport/static", "./static"]).defaults("client.html")), + ); let config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); let listener = TcpListener::new(("127.0.0.1", 5800)).rustls(config.clone()); @@ -138,56 +134,3 @@ async fn main() { Server::new(acceptor).serve(router).await; } - -static INDEX_HTML: &str = r#" - - - WebTransport - - -

WebTransport

-
-

Connecting...

-
- - - -"#; diff --git a/examples/webtransport/static/client.css b/examples/webtransport/static/client.css new file mode 100644 index 000000000..f6ba94cf8 --- /dev/null +++ b/examples/webtransport/static/client.css @@ -0,0 +1,84 @@ +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; + } \ No newline at end of file diff --git a/examples/webtransport/static/client.html b/examples/webtransport/static/client.html new file mode 100644 index 000000000..9f4d7160c --- /dev/null +++ b/examples/webtransport/static/client.html @@ -0,0 +1,66 @@ + + + WebTransport over HTTP/3 client + + + + + + + +
+
+ This tool can be used to connect to an arbitrary WebTransport server. + It has several limitations: +
    +
  • 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.
  • +
  • This tool does not listen to server-initiated bidirectional + streams.
  • +
  • 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.
  • +
  • The WebTransport object can be accessed using the developer console via currentTransport.
  • +
+
+
+

WebTransport over HTTP/3 client

+
+

Establish WebTransport connection

+
+ + + +
+
+
+

Send data over WebTransport

+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

Event log

+
    +
+
+
+
+ + \ No newline at end of file diff --git a/examples/webtransport/static/client.js b/examples/webtransport/static/client.js new file mode 100644 index 000000000..2b4f182ab --- /dev/null +++ b/examples/webtransport/static/client.js @@ -0,0 +1,168 @@ +// https://github.com/GoogleChrome/samples/blob/gh-pages/webtransport/client.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'); + 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; + 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(); + } +} \ No newline at end of file