Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ jobs:
if: matrix.os == 'ubuntu-latest'
name: Run docker tests

http3:
name: HTTP/3 (ngtcp2 on OpenSSL)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust
run: rustup update stable && rustup default stable
- run: cargo test --no-run --features http3,static-ssl
- run: cargo run --example http3 --features http3,static-ssl

rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
Expand Down
41 changes: 30 additions & 11 deletions .github/workflows/update-curl.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# This workflow automates some of the process of updating curl-sys to the latest
# version of curl upstream.
# This workflow automates some of the process of updating the bundled
# libraries (curl, and ngtcp2 and nghttp3 for the http3 feature) to their
# latest upstream releases. One pull request per library.

name: Update curl
name: Update bundled libraries

on:
schedule:
Expand All @@ -14,6 +15,22 @@ jobs:
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
include:
- name: curl
repo: curl/curl
path: curl-sys/curl
changelog: https://curl.se/ch/TAG.html
- name: ngtcp2
repo: ngtcp2/ngtcp2
path: curl-sys/ngtcp2
changelog: https://github.com/ngtcp2/ngtcp2/releases/tag/TAG
- name: nghttp3
repo: ngtcp2/nghttp3
path: curl-sys/nghttp3
changelog: https://github.com/ngtcp2/nghttp3/releases/tag/TAG
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -24,25 +41,27 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
LATEST_TAG=$(gh release view --repo curl/curl --json tagName --jq .tagName)
cd curl-sys/curl
LATEST_TAG=$(gh release view --repo ${{ matrix.repo }} --json tagName --jq .tagName)
cd ${{ matrix.path }}
git fetch --tags
git checkout $LATEST_TAG
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "changelog=${{ matrix.changelog }}" | sed "s/TAG/$LATEST_TAG/" >> $GITHUB_OUTPUT

- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GH_PAT }}
branch: update-curl-to-${{ steps.update.outputs.tag }}
commit-message: "chore: update curl to ${{ steps.update.outputs.tag }}"
title: "Update curl to ${{ steps.update.outputs.tag }}"
branch: update-${{ matrix.name }}-to-${{ steps.update.outputs.tag }}
commit-message: "chore: update ${{ matrix.name }} to ${{ steps.update.outputs.tag }}"
title: "Update ${{ matrix.name }} to ${{ steps.update.outputs.tag }}"
body: |
Update the bundled curl submodule to version ${{ steps.update.outputs.tag }}.
Update the bundled ${{ matrix.name }} submodule to version ${{ steps.update.outputs.tag }}.

Changelog: <https://curl.se/ch/${{ steps.update.outputs.tag }}.html>
Changelog: <${{ steps.update.outputs.changelog }}>

This PR was created in an automated fashion. Additional changes may
be required to get the new version of curl to work with our
be required to get the new version of ${{ matrix.name }} to work with our
`build.rs` script.
# one label for all bundled libraries; it already exists in the repo
labels: curl-update
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "curl-sys/curl"]
path = curl-sys/curl
url = https://github.com/curl/curl.git
[submodule "curl-sys/nghttp3"]
path = curl-sys/nghttp3
url = https://github.com/ngtcp2/nghttp3
[submodule "curl-sys/ngtcp2"]
path = curl-sys/ngtcp2
url = https://github.com/ngtcp2/ngtcp2
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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", "ssl", "static-curl"]
spnego = ["curl-sys/spnego"]
rustls = ["curl-sys/rustls"]
static-curl = ["curl-sys/static-curl"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 via libcurl's ngtcp2 backend, built from bundled ngtcp2 and nghttp3 sources on OpenSSL's QUIC API. Implies `ssl` and `static-curl`; needs OpenSSL 3.5 or newer, which `static-ssl` provides where the system's is older. Unix only. 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.
Expand Down
15 changes: 14 additions & 1 deletion curl-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@ documentation = "https://docs.rs/curl-sys"
readme = "../README.md"
categories = ["external-ffi-bindings"]
edition = "2018"
exclude = ["curl/docs/", "curl/tests/"]
exclude = [
"curl/docs/",
"curl/tests/",
"nghttp3/doc/",
"nghttp3/examples/",
"nghttp3/fuzz/",
"nghttp3/tests/",
"ngtcp2/doc/",
"ngtcp2/examples/",
"ngtcp2/fuzz/",
"ngtcp2/tests/",
"ngtcp2/third-party/",
]

[badges.maintenance]
status = "passively-maintained"
Expand Down Expand Up @@ -47,6 +59,7 @@ pkg-config = "0.3.3"
default = ["ssl"]
ssl = ["openssl-sys"]
http2 = ["libnghttp2-sys"]
http3 = ["ssl", "static-curl"]
mesalink = []
rustls = ["rustls-ffi"]
static-curl = []
Expand Down
124 changes: 124 additions & 0 deletions curl-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ fn write_windows_config(dst: &Path) {

fn main() {
println!("cargo:rerun-if-changed=curl");
println!("cargo:rerun-if-changed=nghttp3");
println!("cargo:rerun-if-changed=ngtcp2");
println!(
"cargo:rustc-check-cfg=cfg(\
libcurl_vendored,\
Expand Down Expand Up @@ -350,6 +352,10 @@ fn main() {

// Configure TLS backend. Since Cargo does not support mutually exclusive
// features, make sure we only compile one vtls.
if cfg!(feature = "http3") && (cfg!(feature = "rustls") || windows) {
// not a hard error, so that --all-features still builds
println!("cargo:warning=the http3 feature needs libcurl's OpenSSL backend on a unix target; ignored");
}
if cfg!(feature = "rustls") {
cfg.define("USE_RUSTLS", None)
.file("curl/lib/vtls/cipher_suite.c")
Expand Down Expand Up @@ -404,6 +410,9 @@ fn main() {
if let Some(path) = env::var_os("DEP_OPENSSL_INCLUDE") {
cfg.include(path);
}
if cfg!(feature = "http3") {
http3(&mut cfg, &dst, &target);
}
}
}

Expand Down Expand Up @@ -641,3 +650,118 @@ fn curl_config_reports_http2() -> bool {

true
}

/// HTTP/3 through libcurl's ngtcp2 backend on OpenSSL's QUIC API (3.5+).
/// ngtcp2 and nghttp3 are built from the bundled sources like libcurl is,
/// with `cc` and a generated `version.h` instead of their cmake setups.
fn http3(curl: &mut cc::Build, dst: &Path, target: &str) {
let version = env::var("DEP_OPENSSL_VERSION_NUMBER")
.ok()
.and_then(|v| u64::from_str_radix(&v, 16).ok())
.unwrap_or(0);
if version < 0x3_05_00_00_0 {
panic!(
"the http3 feature needs OpenSSL 3.5 or newer for its QUIC API \
(found 0x{version:x}); the static-ssl feature builds one from source"
);
}
let openssl_include = env::var_os("DEP_OPENSSL_INCLUDE").map(PathBuf::from);

// nghttp3/version.h and ngtcp2/version.h from their cmake templates
let generated = dst.join("quic-include");
for lib in ["nghttp3", "ngtcp2"] {
let cmake = fs::read_to_string(format!("{lib}/CMakeLists.txt")).unwrap();
let version = cmake
.lines()
.find_map(|l| l.trim().strip_prefix(&format!("project({lib} VERSION ")))
.and_then(|l| l.split_whitespace().next())
.expect("project version in CMakeLists.txt");
let num = version
.split('.')
.map(|p| p.parse::<u32>().unwrap())
.fold(0, |acc, p| (acc << 8) | p);
let template =
fs::read_to_string(format!("{lib}/lib/includes/{lib}/version.h.in")).unwrap();
let out = generated.join(lib);
fs::create_dir_all(&out).unwrap();
fs::write(
out.join("version.h"),
template
.replace("@PACKAGE_VERSION@", version)
.replace("@PACKAGE_VERSION_NUM@", &format!("0x{num:06x}")),
)
.unwrap();
}

let c_files = |dir: &str| {
let mut files: Vec<PathBuf> = fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|e| e == "c"))
.collect();
files.sort();
files
};
// what the cmake configure would have found on these platforms
let platform = |build: &mut cc::Build| {
build
.define("HAVE_ARPA_INET_H", None)
.define("HAVE_NETINET_IN_H", None)
.define("HAVE_UNISTD_H", None)
.warnings(false);
if target.contains("linux") || target.contains("android") {
build
.define("HAVE_ENDIAN_H", None)
.define("HAVE_BYTESWAP_H", None)
.define("HAVE_DECL_BE64TOH", "1")
.define("HAVE_DECL_BSWAP_64", "1");
} else if target.contains("bsd") {
build
.define("HAVE_SYS_ENDIAN_H", None)
.define("HAVE_DECL_BE64TOH", "1");
}
};

let mut nghttp3 = cc::Build::new();
platform(&mut nghttp3);
nghttp3
.include("nghttp3/lib/includes")
.include("nghttp3/lib")
.include(&generated)
.define("NGHTTP3_STATICLIB", None)
.files(c_files("nghttp3/lib"))
.file("nghttp3/lib/sfparse/sfparse.c")
.compile("nghttp3");

let mut ngtcp2 = cc::Build::new();
platform(&mut ngtcp2);
ngtcp2
.include("ngtcp2/lib/includes")
.include("ngtcp2/lib")
.include("ngtcp2/crypto/includes")
.include("ngtcp2/crypto")
.include(&generated)
.define("NGTCP2_STATICLIB", None)
.files(c_files("ngtcp2/lib"))
.file("ngtcp2/crypto/shared.c")
.file("ngtcp2/crypto/ossl/ossl.c");
if let Some(path) = &openssl_include {
ngtcp2.include(path);
}
ngtcp2.compile("ngtcp2");

curl.define("USE_NGTCP2", None)
.define("USE_NGHTTP3", None)
.define("OPENSSL_QUIC_API2", None)
.define("NGTCP2_STATICLIB", None)
.define("NGHTTP3_STATICLIB", None)
.define("HAVE_SENDMSG", None)
.define("HAVE_NETINET_UDP_H", None)
.include("nghttp3/lib/includes")
.include("ngtcp2/lib/includes")
.include("ngtcp2/crypto/includes")
.include(&generated)
.files(c_files("curl/lib/vquic"));
// no HAVE_SENDMMSG: its struct mmsghdr and IOV_MAX need _GNU_SOURCE,
// which this build does not set; sendmsg is the portable path
}
1 change: 1 addition & 0 deletions curl-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ pub const CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: c_int = 5;
/// Use HTTP/3, fallback to HTTP/2 or HTTP/1 if needed.
/// (Added in CURL 7.66.0)
pub const CURL_HTTP_VERSION_3: c_int = 30;
pub const CURL_HTTP_VERSION_3ONLY: c_int = 31;

// Note that the type here is wrong, it's just intended to just be an enum.
pub const CURL_SSLVERSION_DEFAULT: __enum_ty = 0;
Expand Down
1 change: 1 addition & 0 deletions curl-sys/nghttp3
Submodule nghttp3 added at dbfc24
1 change: 1 addition & 0 deletions curl-sys/ngtcp2
Submodule ngtcp2 added at f9e9ff
30 changes: 30 additions & 0 deletions examples/http3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Fetches a page over HTTP/3 and nothing else: `V3Only` makes libcurl
//! fail instead of falling back to HTTP/2 or HTTP/1.1.
//!
//! Needs the `http3` feature (and `static-ssl` where the system OpenSSL is
//! older than 3.5):
//!
//! cargo run --example http3 --features http3,static-ssl

use curl::easy::{Easy, HttpVersion};
use curl::Version;
use std::io::{stdout, Write};

fn main() -> Result<(), curl::Error> {
let version = Version::get();
assert!(
version.feature_http3(),
"this libcurl has no HTTP/3: {}",
version.version()
);
let mut easy = Easy::new();
easy.url("https://cloudflare-quic.com/")?;
easy.http_version(HttpVersion::V3Only)?;
easy.write_function(|data| {
stdout().write_all(data).unwrap();
Ok(data.len())
})?;
easy.perform()?;
eprintln!("\n{} over HTTP/3", easy.response_code()?);
Ok(())
}
4 changes: 4 additions & 0 deletions src/easy/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ pub enum HttpVersion {
///
/// (Added in CURL 7.66.0)
V3 = curl_sys::CURL_HTTP_VERSION_3 as isize,

/// Use HTTP/3 and fail if that is not possible, no fallback to earlier
/// versions. (Added in CURL 7.88.0)
V3Only = curl_sys::CURL_HTTP_VERSION_3ONLY as isize,
}

/// Possible values to pass to the `ssl_version` and `ssl_min_max_version` method.
Expand Down
Loading