diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 680d47194..157119919 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,7 @@ jobs: rust: stable docker: linux64 target: x86_64-unknown-linux-gnu + http3: '1' - build: ubuntu-lts os: ubuntu-24.04 rust: '1.85' @@ -59,6 +60,7 @@ jobs: os: macos-latest rust: stable target: aarch64-apple-darwin + http3: '1' - build: win32 os: windows-latest rust: stable @@ -67,6 +69,7 @@ jobs: os: windows-latest rust: stable target: x86_64-pc-windows-msvc + http3: '1' steps: - uses: actions/checkout@v1 with: @@ -75,7 +78,7 @@ jobs: run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} shell: bash - run: rustup target add ${{ matrix.target }} - - run: TARGET=${{ matrix.target }} ./ci/run.sh + - run: HTTP3_TESTS=${{ matrix.http3 }} TARGET=${{ matrix.target }} ./ci/run.sh if: matrix.os != 'ubuntu-latest' name: Run non-docker tests shell: bash @@ -91,6 +94,7 @@ jobs: -v `pwd`/ci/.cargo:/src/.cargo:ro \ -v `rustc --print sysroot`:/usr/local:ro \ -e TARGET=${{ matrix.target }} \ + -e HTTP3_TESTS=${{ matrix.http3 }} \ -e NO_RUN=${{ matrix.no_run }} \ -e CARGO_TARGET_DIR=/src/target \ rust \ diff --git a/Cargo.toml b/Cargo.toml index 2ee0ad6c2..387376e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ default = ["ssl"] ssl = ["openssl-sys", "openssl-probe", "curl-sys/ssl"] # OpenSSL/system TLS backend mesalink = ["curl-sys/mesalink"] # MesaLink TLS backend http2 = ["curl-sys/http2"] +http3 = ["curl-sys/http3"] spnego = ["curl-sys/spnego"] rustls = ["curl-sys/rustls"] static-curl = ["curl-sys/static-curl"] diff --git a/README.md b/README.md index d0911bea9..719d2b254 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ with various Cargo features: Note that Rustls support is experimental within Curl itself and may have significant bugs, so we don't offer any sort of stability guarantee with this feature. - `http2`: Enable HTTP/2 support via libnghttp2. Disabled by default. +- `http3`: Enable HTTP/3 support via ngtcp2 and nghttp3 using AWS-LC as the TLS backend. This TLS backend takes precedence over the `ssl` and `rustls` features. Disabled by default. - `static-curl`: Use a bundled libcurl version and statically link to it. Disabled by default. - `apple-sectrust`: Use Apple SecTrust for certificate verification with bundled libcurl and OpenSSL on Apple platforms. Implies `ssl` and `static-curl`. Disabled by default. - `static-ssl`: Use a bundled OpenSSL version and statically link to it. Only applies on platforms that use OpenSSL. Disabled by default. diff --git a/ci/Dockerfile-linux64 b/ci/Dockerfile-linux64 index 29c6d53af..333d20a5e 100644 --- a/ci/Dockerfile-linux64 +++ b/ci/Dockerfile-linux64 @@ -2,6 +2,6 @@ FROM ubuntu:24.04 RUN apt-get update RUN apt-get install -y --no-install-recommends \ - gcc ca-certificates make libc6-dev \ + gcc g++ cmake libclang-dev ca-certificates make libc6-dev \ libssl-dev \ pkg-config diff --git a/ci/run.sh b/ci/run.sh index 0abfb5fd6..4c972389e 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -32,6 +32,11 @@ if [ "$TARGET" != "x86_64-pc-windows-gnu" ] && [ "$TARGET" != "i686-pc-windows-m then cargo test --target $TARGET --no-run --features rustls,static-curl $features fi +# HTTP/3 has additional native build dependencies and a higher dependency MSRV. +# Exercise it only in CI jobs that explicitly opt in to HTTP/3 coverage. +if [ "$HTTP3_TESTS" = "1" ]; then + cargo test --target $TARGET --no-run --features http3,static-curl $features +fi # Then with all extra protocols enabled. cargo test --target $TARGET --no-run --features static-curl,protocol-ftp,ntlm $features if [ -z "$NO_RUN" ]; then @@ -39,9 +44,17 @@ if [ -z "$NO_RUN" ]; then cargo test --target $TARGET --features static-curl $features cargo test --target $TARGET --features static-curl,protocol-ftp $features cargo test --target $TARGET --features static-curl,http2 $features + if [ "$HTTP3_TESTS" = "1" ]; then + cargo test --target $TARGET --features static-curl,http3 $features + fi case "$TARGET" in - *-apple-*) cargo test --target $TARGET --features apple-sectrust $features ;; + *-apple-*) + cargo test --target $TARGET --features apple-sectrust $features + if [ "$HTTP3_TESTS" = "1" ]; then + cargo test --target $TARGET --features apple-sectrust,http3 $features + fi + ;; esac # Note that `-Clink-dead-code` is passed here to suppress `--gc-sections` to diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index 170715dfe..afa0664e0 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -23,6 +23,9 @@ path = "lib.rs" libz-sys = { version = "1.0.18", default-features = false, features = ["libc"] } libc = "0.2.2" libnghttp2-sys = { optional = true, version = "0.1.3" } +nghttp3-sys = { optional = true, version = "0.2" } +ngtcp2-sys = { optional = true, version = "0.2" } +aws-lc-sys = { optional = true, version = "0.43", features = ["ssl"] } [dependencies.rustls-ffi] version = "0.15" @@ -46,6 +49,7 @@ pkg-config = "0.3.3" default = ["ssl"] ssl = ["openssl-sys"] http2 = ["libnghttp2-sys"] +http3 = ["nghttp3-sys", "ngtcp2-sys", "aws-lc-sys"] mesalink = [] rustls = ["rustls-ffi"] static-curl = [] diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 7d028d410..2a6ed010d 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -38,7 +38,10 @@ fn main() { println!( "cargo:rustc-check-cfg=cfg(\ libcurl_vendored,\ + link_aws_lc,\ link_libnghttp2,\ + link_libnghttp3,\ + link_libngtcp2,\ link_libz,\ link_openssl,\ )" @@ -61,13 +64,19 @@ fn main() { if !cfg!(feature = "static-curl") { // OSX ships libcurl by default, so we just use that version // so long as it has the right features enabled. - if target.contains("apple") && (!cfg!(feature = "http2") || curl_config_reports_http2()) { + if target.contains("apple") + && (!cfg!(feature = "http2") || curl_config_reports("HTTP2", "http-2")) + && (!cfg!(feature = "http3") || curl_config_reports("HTTP3", "http-3")) + { return println!("cargo:rustc-flags=-l curl"); } // Next, fall back and try to use pkg-config if its available. if windows { - if try_vcpkg() { + // vcpkg does not expose enough information to verify whether the + // discovered libcurl was built with HTTP/3 support. Build the + // vendored HTTP/3 stack instead so the feature is guaranteed. + if !cfg!(feature = "http3") && try_vcpkg() { return; } } else if try_pkg_config() { @@ -337,6 +346,24 @@ fn main() { } } + if cfg!(feature = "http3") { + cfg.define("USE_NGTCP2", None) + .define("USE_NGHTTP3", None) + .define("NGTCP2_STATICLIB", None) + .define("NGHTTP3_STATICLIB", None) + .file("curl/lib/vquic/capsule.c") + .file("curl/lib/vquic/cf-capsule.c") + .file("curl/lib/vquic/cf-ngtcp2.c") + .file("curl/lib/vquic/cf-ngtcp2-cmn.c"); + + println!("cargo:rustc-cfg=link_aws_lc"); + println!("cargo:rustc-cfg=link_libnghttp3"); + println!("cargo:rustc-cfg=link_libngtcp2"); + cfg.include(env::var_os("DEP_NGTCP2_INCLUDE").unwrap()) + .include(env::var_os("DEP_NGHTTP3_INCLUDE").unwrap()) + .include(aws_lc_include().unwrap()); + } + println!("cargo:rustc-cfg=link_libz"); if let Some(path) = env::var_os("DEP_Z_INCLUDE") { cfg.include(path); @@ -350,7 +377,21 @@ fn main() { // Configure TLS backend. Since Cargo does not support mutually exclusive // features, make sure we only compile one vtls. - if cfg!(feature = "rustls") { + if cfg!(feature = "http3") { + cfg.define("USE_OPENSSL", None) + .define("OPENSSL_IS_AWSLC", None) + .file("curl/lib/vtls/openssl.c"); + + if target.contains("-apple-") && cfg!(feature = "apple-sectrust") { + cfg.define("USE_APPLE_SECTRUST", None) + .file("curl/lib/vtls/apple.c"); + } + + if windows { + cfg.define("CURL_CA_NATIVE", None) + .define("CURL_DISABLE_CA_SEARCH", None); + } + } else if cfg!(feature = "rustls") { cfg.define("USE_RUSTLS", None) .file("curl/lib/vtls/cipher_suite.c") .file("curl/lib/vtls/rustls.c") @@ -602,9 +643,11 @@ fn try_pkg_config() -> bool { } }; - // Not all system builds of libcurl have http2 features enabled, so if we've - // got a http2-requested build then we may fall back to a build from source. - if cfg!(feature = "http2") && !curl_config_reports_http2() { + // Not all system builds of libcurl have optional HTTP features enabled, so + // fall back to a source build when a requested feature is missing. + if (cfg!(feature = "http2") && !curl_config_reports("HTTP2", "http-2")) + || (cfg!(feature = "http3") && !curl_config_reports("HTTP3", "http-3")) + { return false; } @@ -617,7 +660,7 @@ fn try_pkg_config() -> bool { true } -fn curl_config_reports_http2() -> bool { +fn curl_config_reports(feature: &str, description: &str) -> bool { let output = Command::new("curl-config").arg("--features").output(); let output = match output { Ok(out) => out, @@ -631,13 +674,21 @@ fn curl_config_reports_http2() -> bool { return false; } let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.contains("HTTP2") { + if !stdout.split_whitespace().any(|item| item == feature) { println!( - "failed to find http-2 feature enabled in pkg-config-found \ - libcurl, building from source" + "failed to find {} feature enabled in pkg-config-found \ + libcurl, building from source", + description ); return false; } true } + +fn aws_lc_include() -> Option { + env::vars_os().find_map(|(key, value)| { + let key = key.to_string_lossy(); + (key.starts_with("DEP_AWS_LC_") && key.ends_with("_INCLUDE")).then_some(value) + }) +} diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index 5e5b10c28..50ba3f9de 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -2,10 +2,16 @@ #![doc(html_root_url = "https://docs.rs/curl-sys/0.4")] // These `extern crate` are required for conditional linkages of curl. +#[cfg(link_aws_lc)] +extern crate aws_lc_sys; #[cfg(link_libnghttp2)] extern crate libnghttp2_sys; #[cfg(link_libz)] extern crate libz_sys; +#[cfg(link_libnghttp3)] +extern crate nghttp3_sys; +#[cfg(link_libngtcp2)] +extern crate ngtcp2_sys; #[cfg(link_openssl)] extern crate openssl_sys; #[cfg(feature = "rustls")] diff --git a/tests/protocols.rs b/tests/protocols.rs index 7215fe972..d3273cb69 100644 --- a/tests/protocols.rs +++ b/tests/protocols.rs @@ -17,3 +17,11 @@ fn static_with_ftp_enabled() { .next() .is_some()); } + +#[cfg(feature = "http3")] +#[test] +fn with_http3_enabled() { + let version = curl::Version::get(); + assert!(version.feature_http3()); + assert!(version.quic_version().is_some()); +}