Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"web-transport",
"web-transport-proto",
"web-transport-quiche",
"web-transport-quinn",
"web-transport-trait",
"web-transport-wasm",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
75 changes: 75 additions & 0 deletions web-demo/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// @ts-expect-error embed the certificate fingerprint using bundler
import fingerprintHex from "bundle-text:../dev/localhost.hex";

// Convert the hex to binary.
const fingerprint = [];
for (let c = 0; c < fingerprintHex.length - 1; c += 2) {
fingerprint.push(parseInt(fingerprintHex.substring(c, c + 2), 16));
}

const params = new URLSearchParams(window.location.search);

const url = params.get("url") || "https://localhost:4443";
const datagram = params.get("datagram") || false;
Comment on lines +10 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix datagram parameter parsing logic.

Line 13 has a logic error: params.get("datagram") returns either a string or null, so || false will evaluate to the string value (e.g., "false") rather than a boolean. Any non-empty string (including "false") is truthy in JavaScript.

Apply this fix:

-const datagram = params.get("datagram") || false;
+const datagram = params.get("datagram") === "true";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = new URLSearchParams(window.location.search);
const url = params.get("url") || "https://localhost:4443";
const datagram = params.get("datagram") || false;
const params = new URLSearchParams(window.location.search);
const url = params.get("url") || "https://localhost:4443";
const datagram = params.get("datagram") === "true";
🤖 Prompt for AI Agents
In web-demo/client.js around lines 10 to 13 the datagram parsing uses
`params.get("datagram") || false` which preserves string values like "false" as
truthy; change the logic to explicitly convert the query value to a boolean,
e.g., replace that line with a comparison that yields true only when the
parameter equals "true" (or use params.has("datagram") if presence-only
semantics are desired), ensuring the result is a real boolean.


function log(msg) {
const element = document.createElement("div");
element.innerText = msg;

document.body.appendChild(element);
}

async function run() {
// Connect using the hex fingerprint in the cert folder.
const transport = new WebTransport(url, {
serverCertificateHashes: [
{
algorithm: "sha-256",
value: new Uint8Array(fingerprint),
},
],
});
await transport.ready;

log("connected");

let writer;
let reader;

if (!datagram) {
// Create a bidirectional stream
const stream = await transport.createBidirectionalStream();
log("created stream");

writer = stream.writable.getWriter();
reader = stream.readable.getReader();
} else {
log("using datagram");

// Create a datagram
writer = transport.datagrams.writable.getWriter();
reader = transport.datagrams.readable.getReader();
}

// Create a message
const msg = "Hello, world!";
const encoded = new TextEncoder().encode(msg);

await writer.write(encoded);
await writer.close();
writer.releaseLock();

log("send: " + msg);

// Read a message from it
// TODO handle partial reads
const { value } = await reader.read();

const recv = new TextDecoder().decode(value);
log("recv: " + recv);

transport.close();
log("closed");
}

run();
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions web-transport-proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ categories = ["network-programming", "web-programming"]
bytes = "1"
http = "1"
thiserror = "2"

# Just for AsyncRead and AsyncWrite traits
tokio = { version = "1", default-features = false }
url = "2"
39 changes: 37 additions & 2 deletions web-transport-proto/src/capsule.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use bytes::{Buf, BufMut, Bytes};
use std::sync::Arc;

use bytes::{Buf, BufMut, Bytes, BytesMut};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

use crate::{VarInt, VarIntUnexpectedEnd};

Expand Down Expand Up @@ -68,6 +71,22 @@ impl Capsule {
}
}

pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, CapsuleError> {
let mut buf = Vec::new();
loop {
if stream.read_buf(&mut buf).await? == 0 {
return Err(CapsuleError::UnexpectedEnd);
}

let mut limit = std::io::Cursor::new(&buf);
match Self::decode(&mut limit) {
Ok(capsule) => return Ok(capsule),
Err(CapsuleError::UnexpectedEnd) => continue,
Err(e) => return Err(e),
}
}
}

pub fn encode<B: BufMut>(&self, buf: &mut B) {
match self {
Self::CloseWebTransportSession {
Expand Down Expand Up @@ -101,6 +120,13 @@ impl Capsule {
}
}
}

pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), CapsuleError> {
let mut buf = BytesMut::new();
self.encode(&mut buf);
stream.write_all_buf(&mut buf).await?;
Ok(())
}
}

fn is_grease(val: u64) -> bool {
Expand All @@ -113,7 +139,7 @@ fn is_grease(val: u64) -> bool {
}
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[derive(Debug, Clone, thiserror::Error)]
pub enum CapsuleError {
#[error("unexpected end of buffer")]
UnexpectedEnd,
Expand All @@ -129,6 +155,15 @@ pub enum CapsuleError {

#[error("varint decode error: {0:?}")]
VarInt(#[from] VarIntUnexpectedEnd),

#[error("io error: {0}")]
Io(Arc<std::io::Error>),
}

impl From<std::io::Error> for CapsuleError {
fn from(err: std::io::Error) -> Self {
CapsuleError::Io(Arc::new(err))
}
}

#[cfg(test)]
Expand Down
60 changes: 58 additions & 2 deletions web-transport-proto/src/connect.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::str::FromStr;
use std::{str::FromStr, sync::Arc};

use bytes::{Buf, BufMut};
use bytes::{Buf, BufMut, BytesMut};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use url::Url;

use super::{qpack, Frame, VarInt};
Expand Down Expand Up @@ -48,6 +49,15 @@ pub enum ConnectError {

#[error("non-200 status: {0:?}")]
ErrorStatus(http::StatusCode),

#[error("io error: {0}")]
Io(Arc<std::io::Error>),
}

impl From<std::io::Error> for ConnectError {
fn from(err: std::io::Error) -> Self {
ConnectError::Io(Arc::new(err))
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -97,6 +107,22 @@ impl ConnectRequest {
Ok(Self { url })
}

pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, ConnectError> {
let mut buf = Vec::new();
loop {
if stream.read_buf(&mut buf).await? == 0 {
return Err(ConnectError::UnexpectedEnd);
}

let mut limit = std::io::Cursor::new(&buf);
match Self::decode(&mut limit) {
Ok(request) => return Ok(request),
Err(ConnectError::UnexpectedEnd) => continue,
Err(e) => return Err(e),
}
}
}

pub fn encode<B: BufMut>(&self, buf: &mut B) {
let mut headers = qpack::Headers::default();
headers.set(":method", "CONNECT");
Expand All @@ -118,6 +144,13 @@ impl ConnectRequest {
size.encode(buf);
buf.put_slice(&tmp);
}

pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), ConnectError> {
let mut buf = BytesMut::new();
self.encode(&mut buf);
stream.write_all_buf(&mut buf).await?;
Ok(())
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -148,6 +181,22 @@ impl ConnectResponse {
Ok(Self { status })
}

pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, ConnectError> {
let mut buf = Vec::new();
loop {
if stream.read_buf(&mut buf).await? == 0 {
return Err(ConnectError::UnexpectedEnd);
}

let mut limit = std::io::Cursor::new(&buf);
match Self::decode(&mut limit) {
Ok(response) => return Ok(response),
Err(ConnectError::UnexpectedEnd) => continue,
Err(e) => return Err(e),
}
}
}

pub fn encode<B: BufMut>(&self, buf: &mut B) {
let mut headers = qpack::Headers::default();
headers.set(":status", self.status.as_str());
Expand All @@ -162,4 +211,11 @@ impl ConnectResponse {
size.encode(buf);
buf.put_slice(&tmp);
}

pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), ConnectError> {
let mut buf = BytesMut::new();
self.encode(&mut buf);
stream.write_all_buf(&mut buf).await?;
Ok(())
}
}
10 changes: 5 additions & 5 deletions web-transport-proto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
const ERROR_FIRST: u64 = 0x52e4a40fa8db;
const ERROR_LAST: u64 = 0x52e5ac983162;

pub fn error_from_http3(code: u64) -> Option<u32> {
if !(ERROR_FIRST..=ERROR_LAST).contains(&code) {
pub const fn error_from_http3(code: u64) -> Option<u32> {
if code < ERROR_FIRST || code > ERROR_LAST {
return None;
}

let code = code - ERROR_FIRST;
let code = code / 0x1f;
let code = code - code / 0x1f;

Some(code.try_into().unwrap())
Some(code as u32)
}

pub fn error_to_http3(code: u32) -> u64 {
pub const fn error_to_http3(code: u32) -> u64 {
ERROR_FIRST + code as u64 + code as u64 / 0x1e
}
41 changes: 40 additions & 1 deletion web-transport-proto/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ use std::{
collections::HashMap,
fmt::Debug,
ops::{Deref, DerefMut},
sync::Arc,
};

use bytes::{Buf, BufMut};
use bytes::{Buf, BufMut, BytesMut};

use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

use super::{Frame, StreamUni, VarInt, VarIntUnexpectedEnd};

Expand Down Expand Up @@ -96,6 +98,15 @@ pub enum SettingsError {

#[error("invalid size")]
InvalidSize,

#[error("io error: {0}")]
Io(Arc<std::io::Error>),
}

impl From<std::io::Error> for SettingsError {
fn from(err: std::io::Error) -> Self {
SettingsError::Io(Arc::new(err))
}
}

// A map of settings to values.
Expand Down Expand Up @@ -128,11 +139,31 @@ impl Settings {
Ok(settings)
}

pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, SettingsError> {
let mut buf = Vec::new();

loop {
if stream.read_buf(&mut buf).await? == 0 {
return Err(SettingsError::UnexpectedEnd);
}

// Look at the buffer we've already read.
let mut limit = std::io::Cursor::new(&buf);

match Settings::decode(&mut limit) {
Ok(settings) => return Ok(settings),
Err(SettingsError::UnexpectedEnd) => continue, // More data needed.
Err(e) => return Err(e),
};
}
}

pub fn encode<B: BufMut>(&self, buf: &mut B) {
StreamUni::CONTROL.encode(buf);
Frame::SETTINGS.encode(buf);

// Encode to a temporary buffer so we can learn the length.
// TODO avoid doing this, just use a fixed size varint.
let mut tmp = Vec::new();
for (id, value) in &self.0 {
id.encode(&mut tmp);
Expand All @@ -143,6 +174,14 @@ impl Settings {
buf.put_slice(&tmp);
}

pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), SettingsError> {
// TODO avoid allocating to the heap
let mut buf = BytesMut::new();
self.encode(&mut buf);
stream.write_all_buf(&mut buf).await?;
Ok(())
}

pub fn enable_webtransport(&mut self, max_sessions: u32) {
let max = VarInt::from_u32(max_sessions);

Expand Down
Loading
Loading