Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a152907
Adds WebTransport over HTTP/3 support
SimaoMoreira5228 Oct 8, 2025
ef5e3aa
Adds WebTransport datagram support over HTTP/3
SimaoMoreira5228 Oct 11, 2025
3e2a1db
refactor(http): webtransport client html into file
lennartkloock Oct 11, 2025
cdf8c0b
docs(http): fix
lennartkloock Oct 11, 2025
ab34f15
fix(http): remove unused h3_quinn feature
lennartkloock Oct 11, 2025
b093ab2
Revert "fix(http): remove unused h3_quinn feature"
lennartkloock Oct 11, 2025
e03276e
fix(http): test
lennartkloock Oct 11, 2025
ff21176
feat(http): some improvements to the wt example
lennartkloock Oct 12, 2025
15eac12
chore: fmt + vendor
lennartkloock Oct 12, 2025
aacab99
fix(http): webtransport connections
lennartkloock Oct 12, 2025
d7f7903
feat(http): add webtransport server options
lennartkloock Oct 12, 2025
acf3ece
chore: fmt
lennartkloock Oct 12, 2025
0a5a83b
fix: sync readme and feature gate
lennartkloock Oct 12, 2025
9744bf7
fix(http): doctests
lennartkloock Oct 12, 2025
a266c11
docs: add changelog file
lennartkloock Oct 12, 2025
fddc437
fix(http): doctests
lennartkloock Oct 12, 2025
15601cc
refactor(http): rename example
lennartkloock Oct 12, 2025
b27e9d3
fix(http): clean up wt example
lennartkloock Oct 12, 2025
20f9379
feat(http): add into_inner functions
lennartkloock Oct 13, 2025
5c3998b
chore: vendor
lennartkloock Oct 13, 2025
5d8a3b3
fix(http): add webtransport example bazel target
lennartkloock Oct 13, 2025
07a3339
fix(http): wt example
lennartkloock Oct 13, 2025
ee41cb0
fix(http): clean up feature flags
lennartkloock Oct 13, 2025
f4267a7
feat(http): include wt session as an extension
lennartkloock Oct 15, 2025
737074f
fix(http): sync readme
lennartkloock Oct 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions changes.d/pr-621.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[[scuffle-http]]
category = "feat"
description = "WebTransport over HTTP/3 support"
authors = ["@SimaoMoreira5228"]
6 changes: 6 additions & 0 deletions crates/http/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ scuffle_example(
name = "examples_echo_tls",
srcs = ["examples/echo_tls.rs"],
)

scuffle_example(
name = "examples_webtransport",
srcs = ["examples/webtransport.rs"],
data = ["examples/webtransport_client.html"],
)
9 changes: 9 additions & 0 deletions crates/http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ name = "scuffle-http-axum"
path = "examples/axum.rs"
required-features = ["default", "tls-rustls", "http3", "tower", "tracing"]

[[example]]
name = "scuffle-http-webtransport"
path = "examples/webtransport.rs"
required-features = ["default", "tls-rustls", "http3", "webtransport", "tracing"]

[features]
default = ["http1", "http2", "tower"]
## Enables tracing support
Expand Down Expand Up @@ -56,6 +61,8 @@ http2 = [
]
## Enables http3 support
http3 = ["dep:quinn", "dep:h3-quinn", "dep:h3"]
## Enables WebTransport over HTTP/3 support
webtransport = ["dep:h3-webtransport", "dep:h3-datagram", "h3-quinn?/datagram"]
## Enables tls via rustls
tls-rustls = ["dep:tokio-rustls"]
## Alias for ["http3", "tls-rustls"]
Expand Down Expand Up @@ -88,7 +95,9 @@ libc = { default-features = false, optional = true, version = "0.2" }

# QUIC + HTTP/3
h3 = { default-features = false, optional = true, version = "0.0.8" }
h3-datagram = { default-features = false, optional = true, version = "0.0.2" }
h3-quinn = { default-features = false, optional = true, version = "0.0.10" }
h3-webtransport = { default-features = false, optional = true, version = "0.1" }
quinn = { default-features = false, features = ["platform-verifier", "runtime-tokio", "rustls-aws-lc-rs"], optional = true, version = "0.11" }

# TLS
Expand Down
4 changes: 2 additions & 2 deletions crates/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# scuffle-http
<!-- sync-readme ]] -->

> [!WARNING]
> [!WARNING]
> This crate is under active development and may not be stable.

<!-- sync-readme badge [[ -->
Expand Down Expand Up @@ -32,6 +32,7 @@ See the [changelog](./CHANGELOG.md) for a full release history.
* **`http1`** *(enabled by default)* — Enables http1 support
* **`http2`** *(enabled by default)* — Enabled http2 support
* **`http3`** — Enables http3 support
* **`webtransport`** — Enables WebTransport over HTTP/3 support
* **`tls-rustls`** — Enables tls via rustls
* **`http3-tls-rustls`** — Alias for \[“http3”, “tls-rustls”\]
* **`tower`** *(enabled by default)* — Enables tower service support
Expand Down Expand Up @@ -68,7 +69,6 @@ scuffle_http::HttpServer::builder()

#### Missing Features

* HTTP/3 webtransport support
* Upgrading to websocket connections from HTTP/3 connections (this is usually done via HTTP/1.1 anyway)

### License
Expand Down
7 changes: 4 additions & 3 deletions crates/http/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Examples of using the `scuffle-http` crate.

- [echo](./src/echo.rs) - A simple echo server.
- [echo-tls](./src/echo_tls.rs) - A simple echo server with encryption (HTTPS) and HTTP/3.
- [axum](./src/axum.rs) - Example of using the `axum` web framework with `scuffle-http`.
- [echo](./echo.rs) - A simple echo server.
- [echo-tls](./echo_tls.rs) - A simple echo server with encryption (HTTPS) and HTTP/3.
- [axum](./axum.rs) - Example of using the `axum` web framework with `scuffle-http`.
- [webtransport](./webtransport.rs) - Example of setting up a WebTransport server.
181 changes: 181 additions & 0 deletions crates/http/examples/webtransport.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use std::convert::Infallible;
use std::net::SocketAddr;

use http::{Method, StatusCode};
use scuffle_http as http_srv;
use scuffle_http::service::{fn_http_service, service_clone_factory};
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};

fn assets_path(item: &str) -> std::path::PathBuf {
if let Some(env) = std::env::var_os("ASSETS_DIR") {
std::path::PathBuf::from(env).join(item)
} else {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../assets/{item}"))
}
}

fn rustls_config() -> tokio_rustls::rustls::ServerConfig {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
tokio_rustls::rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("failed to install aws lc provider");
});

let certs = CertificateDer::pem_file_iter(assets_path("cert.pem"))
.expect("failed to load certfile")
.collect::<Result<Vec<_>, _>>()
.expect("failed to load cert");
let key = PrivateKeyDer::from_pem_file(assets_path("key.pem")).expect("failed to load key");

tokio_rustls::rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("failed to build config")
}

const WT_CLIENT_HTML: &str = include_str!("webtransport_client.html");

#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();

let addr: SocketAddr = "[::]:4443".parse().unwrap();

let service = fn_http_service(|req: http_srv::IncomingRequest| async move {
if req.uri().path() == "/" && req.method() == Method::GET {
let resp = http::Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(WT_CLIENT_HTML.to_string())
.unwrap();
Ok::<_, Infallible>(resp)
} else if req.uri().path() == "/wt" && req.method() == Method::CONNECT {
// Extract the WebTransport session from the request
if let Some(session) = req
.extensions()
.get::<http_srv::backend::h3::webtransport::WebTransportSession>()
{
let session = session.clone();
tracing::info!("WebTransport session established");

// Spawn a task to handle incoming bidirectional streams
tokio::spawn({
let session = session.clone();
async move {
use http_srv::backend::h3::webtransport::AcceptedBi;
while let Ok(Some(accepted)) = session.accept_bi().await {
match accepted {
AcceptedBi::BidiStream(mut stream) => {
tokio::spawn(async move {
// Echo server: read all data and send it back
match stream.read_to_end(64 * 1024).await {
Ok(data) => {
tracing::info!("Received {} bytes on bidi stream, echoing back", data.len());
if let Err(e) = stream.write(data.clone()).await {
tracing::warn!("Failed to write to bidi stream: {}", e);
} else if let Err(e) = stream.finish().await {
tracing::warn!("Failed to finish bidi stream: {}", e);
}
}
Err(e) => {
tracing::warn!("Failed to read from bidi stream: {}", e);
}
}
});
}
AcceptedBi::Request(_req, _stream) => {
tracing::info!("Received HTTP request over WebTransport");
// Handle HTTP-over-WebTransport requests if needed
}
}
}
tracing::info!("Bidi stream acceptor finished");
}
});

// Spawn a task to handle incoming unidirectional streams
tokio::spawn({
let session = session.clone();
async move {
while let Ok(Some((_id, mut stream))) = session.accept_uni().await {
tokio::spawn(async move {
match stream.read_to_end(64 * 1024).await {
Ok(data) => {
tracing::info!(
"Received {} bytes on uni stream: {:?}",
data.len(),
String::from_utf8_lossy(&data)
);
}
Err(e) => {
tracing::warn!("Failed to read from uni stream: {}", e);
}
}
});
}
tracing::info!("Uni stream acceptor finished");
}
});

// Spawn a task to handle incoming datagrams
tokio::spawn({
let session = session.clone();
async move {
let mut datagram_reader = session.datagram_reader();
let mut datagram_sender = session.datagram_sender();

loop {
match datagram_reader.read_datagram().await {
Ok(datagram) => {
let payload = datagram.into_payload();
tracing::info!("Received datagram: {} bytes", payload.len());
let response = format!("Echo: {}", String::from_utf8_lossy(&payload));
if let Err(e) = datagram_sender.send_datagram(bytes::Bytes::from(response)) {
tracing::warn!("Failed to send datagram response: {}", e);
break;
}
}
Err(e) => {
tracing::warn!("Failed to read datagram: {}", e);
break;
}
}
}
tracing::info!("Datagram handler finished");
}
});

return Ok::<_, Infallible>(http::Response::builder().status(StatusCode::OK).body(String::new()).unwrap());
}

Ok::<_, Infallible>(
http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("WebTransport session not found".to_string())
.unwrap(),
)
} else {
Ok::<_, Infallible>(
http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::new())
.unwrap(),
)
}
});

let server = http_srv::HttpServer::builder()
.service_factory(service_clone_factory(service))
.bind(addr)
.rustls_config(rustls_config())
.enable_http3(true)
.build();

tracing::info!(%addr, "serving WebTransport demo over TLS (HTTP/3)");
if let Err(e) = server.run().await {
eprintln!("server error: {e}");
}
}
Loading