Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 54 additions & 2 deletions .github/workflows/publish-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,60 @@ jobs:
mode=dry-run
fi

package_name=h3x
package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')"

crate_state="$(
python3 - <<'PY' "$package_name" "$package_version"
import sys
import urllib.error
import urllib.request

name, version = sys.argv[1], sys.argv[2]
headers = {"User-Agent": "genmeta h3x publish workflow"}
version_url = f"https://crates.io/api/v1/crates/{name}/{version}"
version_request = urllib.request.Request(version_url, headers=headers)
try:
with urllib.request.urlopen(version_request, timeout=20) as response:
if response.status == 200:
print("published_version")
else:
raise SystemExit(f"unexpected crates.io status for {name} {version}: {response.status}")
except urllib.error.HTTPError as error:
if error.code == 404:
crate_url = f"https://crates.io/api/v1/crates/{name}"
crate_request = urllib.request.Request(crate_url, headers=headers)
try:
with urllib.request.urlopen(crate_request, timeout=20) as response:
if response.status == 200:
print("missing_version")
else:
raise SystemExit(f"unexpected crates.io crate status for {name}: {response.status}")
except urllib.error.HTTPError as crate_error:
if crate_error.code == 404:
print("missing_crate")
else:
raise
else:
raise
PY
)"

if [[ "$crate_state" == "published_version" ]]; then
echo "skip $package_name $package_version (already on crates.io)"
exit 0
fi

if [[ "$crate_state" == "missing_crate" ]]; then
echo "skip $package_name $package_version (crate not yet initialized on crates.io)"
exit 0
fi

if [[ "$mode" == "dry-run" ]]; then
echo "dry-run $package_name $package_version"
cargo publish --dry-run
else
cargo publish
exit 0
fi

echo "publish $package_name $package_version"
cargo publish
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "h3x"
description = "Peer-to-peer DHTTP/3 transport over QUIC"
version = "0.3.1"
version = "0.4.0"
edition = "2024"
readme = "README.md"
repository = "https://github.com/genmeta/h3x"
Expand All @@ -28,7 +28,7 @@ tokio-util = { version = "0.7", features = ["codec", "io", "rt"] }
tracing = "0.1"
tower-service = "0.3"

dhttp-identity = "0.1.0"
dhttp-identity = "0.2.0"

# feature dquic
dquic = { version = "0.5.1", optional = true }
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ Peer-to-peer DHTTP/3 transport over QUIC, implemented in Rust.
h3x includes `dquic` as its built-in QUIC backend (feature `dquic`, enabled by default). Wrap a `QuicEndpoint` in an `H3Endpoint` to get HTTP/3 client and server semantics on top of QUIC.

```rust,no_run
use bytes::Bytes;
use h3x::{dquic::QuicEndpoint, endpoint::H3Endpoint};
use http_body_util::{BodyExt, Empty};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let endpoint = H3Endpoint::new(QuicEndpoint::new().await);

let mut response = endpoint.get("https://example.com:4433/hello".parse()?).await?;
let connection = endpoint.connect("example.com:4433".parse()?).await?;
let request = http::Request::get("https://example.com:4433/hello")
.body(Empty::<Bytes>::new())?;
let response = connection.execute_hyper_request(request).await?;
assert_eq!(response.status(), http::StatusCode::OK);
println!("{}", response.read_to_string().await?);
let body = response.into_body().collect().await?.to_bytes();
println!("{}", String::from_utf8_lossy(&body));
Ok(())
}
```

```rust,no_run
```rust,ignore
use std::sync::Arc;
use axum::{Router as AxumRouter, routing::get};
use h3x::{
Expand All @@ -43,7 +49,7 @@ use h3x::{
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let identity = Arc::new(Identity {
name: Name::from_static("localhost")?,
name: "localhost".parse()?,
certs: todo!("load your certificate chain"),
key: todo!("load your private key"),
ocsp: Arc::new(None),
Expand Down Expand Up @@ -73,7 +79,7 @@ h3x provides adapters to bridge the Tower / hyper service ecosystem into DHTTP/3
> - `h3x::hyper::upgrade` — stream takeover for Extended CONNECT tunnels (instead of `hyper::upgrade`)
> - `h3x::hyper::ext::Protocol` — protocol indication in CONNECT requests (instead of `hyper::ext::Protocol`)

```rust,no_run
```rust,ignore
use std::sync::Arc;
use axum::{Router as AxumRouter, routing::get};
use h3x::{
Expand All @@ -89,7 +95,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let service = TowerService(router.into_service());

let identity = Arc::new(Identity {
name: Name::from_static("localhost")?,
name: "localhost".parse()?,
certs: todo!("load your certificate chain"),
key: todo!("load your private key"),
ocsp: Arc::new(None),
Expand All @@ -107,15 +113,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
```

```rust,no_run
use bytes::Bytes;
use h3x::{dquic::QuicEndpoint, endpoint::H3Endpoint};
use http_body_util::{BodyExt, Empty};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let endpoint = H3Endpoint::new(QuicEndpoint::new().await);

let mut response = endpoint.get("https://example.com:4433/hello".parse()?).await?;
let connection = endpoint.connect("example.com:4433".parse()?).await?;
let request = http::Request::get("https://example.com:4433/hello")
.body(Empty::<Bytes>::new())?;
let response = connection.execute_hyper_request(request).await?;
assert_eq!(response.status(), http::StatusCode::OK);
let body = response.read_to_bytes().await?;
let body = response.into_body().collect().await?.to_bytes();
println!("{}", String::from_utf8_lossy(&body));
Ok(())
}
Expand Down
17 changes: 6 additions & 11 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,21 +713,17 @@ pub(crate) mod tests {
use futures::{Sink, SinkExt, future::BoxFuture, stream::Stream};
use tracing::Instrument;

#[cfg(feature = "dquic")]
use super::ConnectionBuilder;
use super::{Connection, ConnectionState, LifecycleExt, StreamError};
#[cfg(feature = "dquic")]
use super::{Connection, ConnectionBuilder, ConnectionState, LifecycleExt, StreamError};
use crate::{
codec::{BoxPeekableStreamReader, BoxStreamWriter},
dhttp::settings::{MaxFieldSectionSize, Settings},
protocol::{ProductProtocol, Protocol, StreamVerdict},
};
use crate::{
dhttp::settings::Settings,
error::{Code, H3MessageError, H3MissingSettings},
protocol::Protocols,
protocol::{Protocol, Protocols, StreamVerdict},
quic::{self, ConnectionError, ResetStreamExt, StopStreamExt},
varint::VarInt,
};
#[cfg(feature = "dquic")]
use crate::{dhttp::settings::MaxFieldSectionSize, protocol::ProductProtocol};

#[derive(Debug)]
pub(crate) struct TestLocalAuthority;
Expand Down Expand Up @@ -879,6 +875,7 @@ pub(crate) mod tests {
.store(true, std::sync::atomic::Ordering::Relaxed);
}

#[cfg(feature = "dquic")]
pub(crate) fn disable_stream_ops(&self) {
self.state
.stream_ops_available
Expand Down Expand Up @@ -1049,11 +1046,9 @@ pub(crate) mod tests {
}

/// Local mock protocol for builder tests.
#[cfg(feature = "dquic")]
#[derive(Debug)]
struct MockProtocol;

#[cfg(feature = "dquic")]
impl Protocol for MockProtocol {
fn accept_uni<'a>(
&'a self,
Expand Down
1 change: 1 addition & 0 deletions src/dhttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pub mod message;
pub mod protocol;
pub mod settings;
pub mod stream;
#[cfg(feature = "webtransport")]
pub mod webtransport;
51 changes: 43 additions & 8 deletions src/dhttp/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,18 @@ impl H3ConnectionError for InvalidSettingValue {
const fn is_boolean_setting(id: VarInt) -> bool {
let id = id.into_inner();
id == crate::extended_connect::settings::EnableConnectProtocol::ID.into_inner()
|| id == crate::dhttp::webtransport::settings::EnableWebTransport::ID.into_inner()
|| id == crate::dhttp::datagram::settings::H3Datagram::ID.into_inner()
|| is_webtransport_boolean_setting(id)
}

#[cfg(feature = "webtransport")]
const fn is_webtransport_boolean_setting(id: u64) -> bool {
id == crate::dhttp::webtransport::settings::EnableWebTransport::ID.into_inner()
}

#[cfg(not(feature = "webtransport"))]
const fn is_webtransport_boolean_setting(_id: u64) -> bool {
false
}

impl<S: AsyncRead + Send> DecodeFrom<S> for Setting {
Expand Down Expand Up @@ -354,10 +364,12 @@ mod tests {
use tokio::io::AsyncWriteExt;

use super::*;
#[cfg(feature = "webtransport")]
use crate::dhttp::webtransport::settings::EnableWebTransport;
use crate::{
codec::{DecodeError, DecodeExt, EncodeExt},
connection,
dhttp::{datagram::settings::H3Datagram, webtransport::settings::EnableWebTransport},
dhttp::datagram::settings::H3Datagram,
extended_connect::settings::EnableConnectProtocol,
quic,
varint::VarInt,
Expand Down Expand Up @@ -513,16 +525,20 @@ mod tests {

#[test]
fn boolean_setting_validation_uses_new_owner_modules() {
for id in [
EnableConnectProtocol::ID,
EnableWebTransport::ID,
H3Datagram::ID,
] {
for id in [EnableConnectProtocol::ID, H3Datagram::ID] {
let err = Setting::new(id, VarInt::from_u32(2))
.check()
.expect_err("boolean setting value 2 must be rejected");
assert!(matches!(err, InvalidSettingValue::BoolSetting { .. }));
}

#[cfg(feature = "webtransport")]
{
let err = Setting::new(EnableWebTransport::ID, VarInt::from_u32(2))
.check()
.expect_err("webtransport boolean setting value 2 must be rejected");
assert!(matches!(err, InvalidSettingValue::BoolSetting { .. }));
}
}

#[test]
Expand Down Expand Up @@ -596,6 +612,7 @@ mod tests {
);
assert!(settings.enable_connect_protocol());
assert!(settings.h3_datagram());
#[cfg(feature = "webtransport")]
assert!(!settings.enable_webtransport());

let borrowed: Vec<_> = (&settings).into_iter().collect();
Expand Down Expand Up @@ -754,7 +771,7 @@ mod tests {

let mut invalid = BufList::new();
invalid
.encode_one(Setting::new(EnableWebTransport::ID, VarInt::from_u32(2)))
.encode_one(Setting::new(H3Datagram::ID, VarInt::from_u32(2)))
.await
.expect("setting encoding into buflist is infallible");
let error = invalid
Expand All @@ -765,6 +782,24 @@ mod tests {
assert_h3_connection_code(error, Code::H3_SETTINGS_ERROR);
}

#[cfg(feature = "webtransport")]
#[tokio::test]
async fn setting_decode_rejects_invalid_webtransport_bool_when_feature_enabled() {
let mut invalid = BufList::new();
invalid
.encode_one(Setting::new(EnableWebTransport::ID, VarInt::from_u32(2)))
.await
.expect("setting encoding into buflist is infallible");

let error = invalid
.decode::<Setting>()
.await
.err()
.expect("invalid webtransport boolean setting must fail to decode");

assert_h3_connection_code(error, Code::H3_SETTINGS_ERROR);
}

#[tokio::test]
async fn settings_encode_to_frame_and_decode_payload() {
let settings = Settings::from_iter([
Expand Down
Loading
Loading