diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0ec415c3..680d47194 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: target: x86_64-unknown-linux-gnu - build: ubuntu-lts os: ubuntu-24.04 - rust: '1.82' + rust: '1.85' docker: linux64 target: x86_64-unknown-linux-gnu - build: x86_64-beta diff --git a/.github/workflows/update-curl.yml b/.github/workflows/update-curl.yml new file mode 100644 index 000000000..0a113a8f9 --- /dev/null +++ b/.github/workflows/update-curl.yml @@ -0,0 +1,48 @@ +# This workflow automates some of the process of updating curl-sys to the latest +# version of curl upstream. + +name: Update curl + +on: + schedule: + - cron: '0 12 * * 5' + workflow_dispatch: + +jobs: + update-submodule: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v7 + with: + submodules: true + + - name: Update submodule to latest release + id: update + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + run: | + LATEST_TAG=$(gh release view --repo curl/curl --json tagName --jq .tagName) + cd curl-sys/curl + git fetch --tags + git checkout $LATEST_TAG + echo "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 }}" + body: | + Update the bundled curl submodule to version ${{ steps.update.outputs.tag }}. + + 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 + `build.rs` script. + labels: curl-update diff --git a/.github/workflows/websocket.yml b/.github/workflows/websocket.yml index d451b928e..17e169023 100644 --- a/.github/workflows/websocket.yml +++ b/.github/workflows/websocket.yml @@ -2,9 +2,9 @@ name: WebSocket on: push: - branches: [main, moli, moli-apple-sectrust] + branches: [main, moli, moli-apple-sectrust, feat/prerequest-callback] pull_request: - branches: [main, moli, moli-apple-sectrust] + branches: [main, moli, moli-apple-sectrust, feat/prerequest-callback] permissions: contents: read @@ -24,6 +24,6 @@ jobs: - name: Install Rust run: rustup update stable --no-self-update && rustup default stable - name: Test HTTP and WebSocket APIs - run: cargo test --workspace --features static-curl,http2,brotli,websocket,ws_start_frame_8_16_0 + run: cargo test --workspace --features static-curl,http2,brotli,apple-sectrust,websocket,ws_start_frame_8_16_0 - name: Check native ABI - run: cargo run -p systest --features curl-sys/static-curl,systest/websocket,systest/ws_start_frame_8_16_0 + run: cargo run -p systest --features curl-sys/static-curl,curl-sys/apple-sectrust,systest/websocket,systest/ws_start_frame_8_16_0 diff --git a/Cargo.toml b/Cargo.toml index d46abe3c6..18bf5369f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "curl" -version = "0.4.49" -authors = ["Alex Crichton "] +version = "0.4.51" +authors = ["Alex Crichton ", "Stephen M. Coakley "] license = "MIT" repository = "https://github.com/lexmount/curl-rust" homepage = "https://github.com/lexmount/curl-rust" @@ -12,28 +12,37 @@ readme = "README.md" autotests = true edition = "2018" +[badges.maintenance] +status = "passively-maintained" + [dependencies] libc = "0.2.42" -curl-sys = { path = "curl-sys", version = "0.4.83", default-features = false } +curl-sys = { path = "curl-sys", version = "0.4.89", default-features = false } socket2 = "0.6.0" # Unix platforms use OpenSSL for now to provide SSL functionality [target.'cfg(all(unix))'.dependencies] -openssl-sys = { version = "0.9.64", optional = true } +openssl-sys = { workspace = true, optional = true } openssl-probe = { version = "0.1.2", optional = true } [target.'cfg(target_env = "msvc")'.dependencies] schannel = "0.1.13" -windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_LibraryLoader", "Win32_Security_Cryptography"] } +windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_LibraryLoader", "Win32_Security_Cryptography"] } [dev-dependencies] mio = "0.6" mio-extras = "2.0.3" anyhow = "1.0.31" +static_assertions = "1.1" brotlic-sys = "0.2.2" [workspace] -members = ["systest"] +members = ["curl-sys", "systest"] + +[workspace.dependencies] +cc = "1.0" +openssl-sys = "0.9.64" +windows-sys = ">=0.59.0, <0.62.0" [features] default = ["ssl"] @@ -44,6 +53,7 @@ http2 = ["curl-sys/http2"] spnego = ["curl-sys/spnego"] rustls = ["curl-sys/rustls"] static-curl = ["curl-sys/static-curl"] +apple-sectrust = ["ssl", "static-curl", "curl-sys/apple-sectrust"] static-ssl = ["curl-sys/static-ssl"] windows-static-ssl = ["static-curl", "curl-sys/windows-static-ssl"] force-system-lib-on-osx = ['curl-sys/force-system-lib-on-osx'] diff --git a/README.md b/README.md index c5a9ffcca..c4e76b4cc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ on the `moli` branch. Moli-specific changes should be based on that branch. [![Latest Version](https://img.shields.io/crates/v/curl.svg)](https://crates.io/crates/curl) [![Documentation](https://docs.rs/curl/badge.svg)](https://docs.rs/curl) -[![License](https://img.shields.io/github/license/alexcrichton/curl-rust.svg)](LICENSE) +[![License](https://img.shields.io/github/license/sagebind/curl-rust.svg)](LICENSE) +![Maintenance](https://img.shields.io/badge/maintenance-passively--maintained-brightgreen.svg) [![Build](https://github.com/lexmount/curl-rust/actions/workflows/websocket.yml/badge.svg?branch=moli)](https://github.com/lexmount/curl-rust/actions/workflows/websocket.yml) ## WebSocket clients @@ -18,7 +19,7 @@ library's protocol configuration. To use this fork throughout a workspace: ```toml [dependencies] -curl = { version = "0.4.49", features = ["static-curl", "websocket"] } +curl = { version = "0.4.51", features = ["static-curl", "websocket"] } [patch.crates-io] curl = { git = "https://github.com/lexmount/curl-rust", branch = "moli" } @@ -70,14 +71,20 @@ can pause reading. System library builds do not get these fixes. Bundled builds also preserve application-owned `CONNECT_ONLY` sockets when the per-host connection limit is reached, matching the total-limit eviction policy. A quiet upgraded WebSocket still consumes a connection slot; a new -transfer waits for capacity instead of evicting it. This fix is generated in -`OUT_DIR/conncache.c`, with the same submodule and patch-location checks. +transfer waits for capacity instead of evicting it. Bundled libcurl 8.23.0 +includes the [upstream fix](https://github.com/curl/curl/commit/9e6673fb7d55b40d18383f49f465f1bd1cfce1ee), +so this fork compiles `conncache.c` directly without a generated patch. + +For bundled OpenSSL builds on Apple platforms, enable the upstream +`apple-sectrust` feature to use the system trust store. Explicit `SSL_CERT_FILE` +and `SSL_CERT_DIR` overrides remain supported; no certificate file is selected +automatically when SecTrust is available. Local validation (including C structure and function ABI checks): ```sh -cargo test --workspace --features static-curl,http2,brotli,websocket,ws_start_frame_8_16_0 -cargo run -p systest --features curl-sys/static-curl,systest/websocket,systest/ws_start_frame_8_16_0 +cargo test --workspace --features static-curl,http2,brotli,apple-sectrust,websocket,ws_start_frame_8_16_0 +cargo run -p systest --features curl-sys/static-curl,curl-sys/apple-sectrust,systest/websocket,systest/ws_start_frame_8_16_0 ``` ## Quick Start @@ -195,13 +202,15 @@ By default, this crate will attempt to dynamically link to the system-wide libcurl and the system-wide SSL library. Some of this behavior can be customized with various Cargo features: -- `ssl`: Enable SSL/TLS support using the platform-default TLS backend. On Windows this is [Schannel], on macOS [Secure Transport], and [OpenSSL] (or equivalent) on all other platforms. Enabled by default. +- `ssl`: Enable SSL/TLS support using the platform-default TLS backend. On Windows this is [Schannel], on macOS [Secure Transport], and [OpenSSL] (or equivalent) on all other platforms. Enabled by default. - `rustls` Enable SSL/TLS support via [Rustls], a well-received alternative TLS backend written in Rust. Rustls is always statically linked. Disabled by default. 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. + - `brotli`: Enable Brotli content encoding support. With vendored libcurl this uses `brotlic-sys`; with a system libcurl, the detected library must already have Brotli enabled. Disabled by default. - `http2`: Enable HTTP/2 support via libnghttp2. 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. - `spnego`: Enable SPNEGO support. Disabled by default. - `upkeep_7_62_0`: Enable curl_easy_upkeep() support, introduced in curl 7.62.0. Disabled by default. @@ -210,6 +219,7 @@ with various Cargo features: - `windows-static-ssl`: Enable Openssl support on Windows via the static build provided by vcpkg. Incompatible with `ssl` (use `--no-default-features`). Disabled by default. Note that to install openssl on windows via vcpkg the following commands needs to be ran: + ```shell git clone https://github.com/microsoft/vcpkg cd vcpkg @@ -241,16 +251,15 @@ against the NSS SSL library, the NSS PEM PKCS#11 module (`libnsspem.so`) needs t In order to avoid this failure you can either - * install the missing library (e.g. Debian: `nss-plugin-pem`), or - * remove the libcurl NSS development files (e.g. Debian: `libcurl4-nss-dev`) and - rebuild curl-rust. +- install the missing library (e.g. Debian: `nss-plugin-pem`), or +- remove the libcurl NSS development files (e.g. Debian: `libcurl4-nss-dev`) and + rebuild curl-rust. ## License The `curl-rust` crate is licensed under the MIT license, see [`LICENSE`](LICENSE) for more details. - [libcurl]: https://curl.haxx.se/libcurl/ [OpenSSL]: https://www.openssl.org/ [Rustls]: https://github.com/ctz/rustls diff --git a/build.rs b/build.rs index cf4206352..fe0450fb2 100644 --- a/build.rs +++ b/build.rs @@ -24,8 +24,12 @@ fn main() { }; if use_openssl { + let apple_sectrust = env::var_os("CARGO_FEATURE_APPLE_SECTRUST").is_some() + && env::var("CARGO_CFG_TARGET_VENDOR").ok().as_deref() == Some("apple"); + // The system libcurl should have the default certificate paths configured. - if env::var_os("DEP_CURL_APPLE_SECTRUST").is_some() { + // Apple SecTrust likewise supplies the native trust anchors itself. + if apple_sectrust { println!("cargo:rustc-cfg=apple_sectrust"); } else if env::var_os("DEP_CURL_STATIC").is_some() { println!("cargo:rustc-cfg=need_openssl_probe"); diff --git a/ci/run.sh b/ci/run.sh index c296e0662..e6a69a336 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -41,6 +41,10 @@ if [ -z "$NO_RUN" ]; then cargo test --target $TARGET --features static-curl,protocol-ftp $features cargo test --target $TARGET --features static-curl,http2 $features + case "$TARGET" in + *-apple-*) cargo test --target $TARGET --features apple-sectrust $features ;; + esac + # Note that `-Clink-dead-code` is passed here to suppress `--gc-sections` to # help confirm that we're compiling everything necessary for curl itself. RUSTFLAGS=-Clink-dead-code \ diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index 0a376dd1f..afd5d701c 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -1,17 +1,21 @@ [package] name = "curl-sys" -version = "0.4.87+curl-8.19.0" -authors = ["Alex Crichton "] +version = "0.4.91+curl-8.23.0" +authors = ["Alex Crichton ", "Stephen M. Coakley "] links = "curl" build = "build.rs" license = "MIT" repository = "https://github.com/lexmount/curl-rust" description = "Native bindings to the libcurl library" documentation = "https://docs.rs/curl-sys" +readme = "../README.md" categories = ["external-ffi-bindings"] edition = "2018" exclude = ["curl/docs/", "curl/tests/"] +[badges.maintenance] +status = "passively-maintained" + [lib] name = "curl_sys" path = "lib.rs" @@ -28,17 +32,17 @@ optional = true features = ["no_log_capture"] [target.'cfg(all(unix))'.dependencies] -openssl-sys = { version = "0.9.64", optional = true } +openssl-sys = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Networking_WinSock"] } +windows-sys = { workspace = true, features = ["Win32_Networking_WinSock"] } [target.'cfg(target_env = "msvc")'.build-dependencies] vcpkg = "0.2" [build-dependencies] +cc.workspace = true pkg-config = "0.3.3" -cc = "1.0" [features] default = ["ssl"] @@ -48,6 +52,7 @@ brotli = ["brotlic-sys"] mesalink = [] rustls = ["rustls-ffi"] static-curl = [] +apple-sectrust = ["ssl", "static-curl"] windows-static-ssl = [] static-ssl = ["openssl-sys/vendored"] spnego = [] diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 0f25bc8bc..2e52cd4c8 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -3,6 +3,36 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +// curl 8.22 limits config-win32.h to old Visual Studio project builds. +// curl-sys builds libcurl directly with cc, so reuse the platform settings +// as curl_config.h without the build-system guard. +fn write_windows_config(dst: &Path) { + let config = fs::read_to_string("curl/lib/config-win32.h").unwrap(); + let mut lines = config.lines(); + let mut output = String::new(); + let mut removed_version_guard = false; + + while let Some(line) = lines.next() { + if line == "#if !defined(_MSC_VER) || _MSC_VER > 1800" { + assert_eq!( + lines.next(), + Some( + "#error This manual configuration requires MSVC 2010-2013 (IDE Project builds)" + ) + ); + assert_eq!(lines.next(), Some("#endif")); + removed_version_guard = true; + continue; + } + + output.push_str(line); + output.push('\n'); + } + + assert!(removed_version_guard); + fs::write(dst.join("curl_config.h"), output).unwrap(); +} + fn main() { println!("cargo:rerun-if-changed=curl"); println!( @@ -55,6 +85,9 @@ fn main() { let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); let include = dst.join("include"); let build = dst.join("build"); + if windows { + write_windows_config(&dst); + } println!("cargo:root={}", dst.display()); println!("cargo:include={}", include.display()); println!("cargo:static=1"); @@ -98,7 +131,7 @@ fn main() { .replace("@LIBCURL_LIBS@", "") .replace("@SUPPORT_FEATURES@", "") .replace("@SUPPORT_PROTOCOLS@", "") - .replace("@CURLVERSION@", "8.19.0"), + .replace("@CURLVERSION@", "8.23.0"), ) .unwrap(); @@ -119,7 +152,7 @@ fn main() { .define("CURL_DISABLE_TELNET", None) .define("CURL_DISABLE_TFTP", None) .define("CURL_STATICLIB", None) - .define("ENABLE_IPV6", None) + .define("USE_IPV6", None) .define("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", None) .define("HAVE_ASSERT_H", None) .define("CURL_OS", "\"unknown\"") // TODO @@ -129,28 +162,31 @@ fn main() { .define("HAVE_BOOL_T", None) .define("HAVE_STDBOOL_H", None) .define("HAVE_STDINT_H", None) + .define("USE_RESOLV_THREADED", None) .file("curl/lib/altsvc.c") - .file("curl/lib/asyn-base.c") - .file("curl/lib/asyn-thrdd.c") + .file("curl/lib/api.c") .file("curl/lib/bufq.c") .file("curl/lib/bufref.c") .file("curl/lib/cf-h1-proxy.c") .file("curl/lib/cf-haproxy.c") .file("curl/lib/cf-https-connect.c") .file("curl/lib/cf-ip-happy.c") + .file("curl/lib/cf-recvbuf.c") + .file("curl/lib/cf-setup.c") .file("curl/lib/cf-socket.c") .file("curl/lib/cfilters.c") - .file(connection_cache_source(&dst)) + .file("curl/lib/conncache.c") .file("curl/lib/connect.c") .file("curl/lib/content_encoding.c") .file("curl/lib/cookie.c") + .file("curl/lib/creds.c") .file("curl/lib/cshutdn.c") .file("curl/lib/curl_addrinfo.c") + .file("curl/lib/curl_ed25519.c") .file("curl/lib/curl_fopen.c") .file("curl/lib/curl_get_line.c") .file("curl/lib/curl_memrchr.c") .file("curl/lib/curl_range.c") - .file("curl/lib/curl_rtmp.c") .file("curl/lib/curl_sha512_256.c") .file("curl/lib/curl_share.c") .file("curl/lib/curl_threads.c") @@ -158,7 +194,7 @@ fn main() { .file("curl/lib/curlx/base64.c") .file("curl/lib/curlx/basename.c") .file("curl/lib/curlx/dynbuf.c") - .file("curl/lib/curlx/fopen.c") + .file("curl/lib/curlx/win32-fopen.c") .file("curl/lib/curlx/inet_ntop.c") .file("curl/lib/curlx/inet_pton.c") .file("curl/lib/curlx/nonblock.c") @@ -174,7 +210,6 @@ fn main() { .file("curl/lib/cw-out.c") .file("curl/lib/cw-pause.c") .file("curl/lib/dict.c") - .file("curl/lib/doh.c") .file("curl/lib/dynhds.c") .file("curl/lib/easy.c") .file("curl/lib/escape.c") @@ -188,14 +223,13 @@ fn main() { .file("curl/lib/hash.c") .file("curl/lib/headers.c") .file("curl/lib/hmac.c") - .file("curl/lib/hostip.c") - .file("curl/lib/hostip6.c") .file("curl/lib/hsts.c") .file("curl/lib/http.c") .file("curl/lib/http1.c") .file("curl/lib/http_aws_sigv4.c") .file("curl/lib/http_chunks.c") .file("curl/lib/http_digest.c") + .file("curl/lib/http_httpsig.c") .file("curl/lib/http_proxy.c") .file("curl/lib/idn.c") .file("curl/lib/if2ip.c") @@ -211,10 +245,13 @@ fn main() { .file("curl/lib/multi_ev.c") .file("curl/lib/multi_ntfy.c") .file("curl/lib/netrc.c") - .file("curl/lib/noproxy.c") .file("curl/lib/parsedate.c") + .file("curl/lib/peer.c") .file("curl/lib/pop3.c") .file("curl/lib/progress.c") + .file("curl/lib/protocol.c") + .file("curl/lib/proxy.c") + .file("curl/lib/ptrarray.c") .file("curl/lib/rand.c") .file("curl/lib/ratelimit.c") .file("curl/lib/request.c") @@ -224,7 +261,6 @@ fn main() { .file("curl/lib/setopt.c") .file("curl/lib/sha256.c") .file("curl/lib/slist.c") - .file("curl/lib/smb.c") .file("curl/lib/smtp.c") .file("curl/lib/socketpair.c") .file("curl/lib/socks.c") @@ -234,24 +270,33 @@ fn main() { .file("curl/lib/strerror.c") .file("curl/lib/telnet.c") .file("curl/lib/tftp.c") + .file("curl/lib/thrdpool.c") + .file("curl/lib/thrdqueue.c") .file("curl/lib/transfer.c") + .file("curl/lib/u8_strset.c") + .file("curl/lib/u32_ptrset.c") .file("curl/lib/uint-bset.c") - .file("curl/lib/uint-hash.c") .file("curl/lib/uint-spbset.c") .file("curl/lib/uint-table.c") .file("curl/lib/url.c") .file("curl/lib/urlapi.c") .file("curl/lib/vauth/digest.c") .file("curl/lib/vauth/vauth.c") + .file("curl/lib/vdns/asyn-base.c") + .file("curl/lib/vdns/asyn-thrdd.c") + .file("curl/lib/vdns/cf-dns.c") + .file("curl/lib/vdns/dnscache.c") + .file("curl/lib/vdns/doh.c") + .file("curl/lib/vdns/hostip.c") + .file("curl/lib/vdns/hostip6.c") .file("curl/lib/version.c") - .file("curl/lib/vquic/curl_ngtcp2.c") - .file("curl/lib/vquic/curl_quiche.c") .file("curl/lib/vquic/vquic-tls.c") .file("curl/lib/vquic/vquic.c") .file("curl/lib/vssh/vssh.c") .file("curl/lib/vtls/hostcheck.c") .file("curl/lib/vtls/keylog.c") .file("curl/lib/vtls/vtls.c") + .file("curl/lib/vtls/vtls_config.c") .file("curl/lib/vtls/vtls_scache.c") .file(websocket_source(&dst)) .define("HAVE_GETADDRINFO", None) @@ -357,13 +402,9 @@ fn main() { cfg.define("USE_OPENSSL", None) .file("curl/lib/vtls/openssl.c"); - // Keep the OpenSSL-compatible TLS backend (including AWS-LC), but - // use the platform verifier by default on macOS. Both the macro - // and the implementation are required by vtls/openssl.c. - if target.ends_with("-apple-darwin") { + if target.contains("-apple-") && cfg!(feature = "apple-sectrust") { cfg.define("USE_APPLE_SECTRUST", None) .file("curl/lib/vtls/apple.c"); - println!("cargo:apple_sectrust=1"); } println!("cargo:rustc-cfg=link_openssl"); @@ -375,12 +416,13 @@ fn main() { // Configure platform-specific details. if windows { - cfg.define("WIN32", None) + cfg.define("HAVE_CONFIG_H", None) + .include(&dst) + .define("WIN32", None) .define("USE_THREADS_WIN32", None) .define("HAVE_IOCTLSOCKET_FIONBIO", None) .define("USE_WINSOCK", None) .file("curl/lib/bufref.c") - .file("curl/lib/system_win32.c") .file("curl/lib/vauth/digest_sspi.c") .file("curl/lib/curlx/multibyte.c") .file("curl/lib/curlx/version_win32.c") @@ -412,7 +454,7 @@ fn main() { .define("HAVE_SOCKETPAIR", None) .define("HAVE_STRUCT_TIMEVAL", None) .define("HAVE_SYS_UN_H", None) - .define("USE_THREADS_POSIX", None) + .define("HAVE_THREADS_POSIX", None) .define("USE_UNIX_SOCKETS", None) .define("RECV_TYPE_ARG2", "void*") .define("RECV_TYPE_ARG3", "size_t") @@ -475,6 +517,9 @@ fn main() { println!("cargo:rustc-link-lib=ws2_32"); println!("cargo:rustc-link-lib=crypt32"); println!("cargo:rustc-link-lib=iphlpapi"); + println!("cargo:rustc-link-lib=advapi32"); + println!("cargo:rustc-link-lib=secur32"); + println!("cargo:rustc-link-lib=bcrypt"); } // Illumos/Solaris requires explicit linking with libnsl @@ -578,24 +623,6 @@ fn try_pkg_config() -> bool { true } -fn connection_cache_source(dst: &Path) -> PathBuf { - // CONNECT_ONLY transfers detach after the handshake, but their sockets - // still belong to the application. Per-host eviction must exclude them - // just like cpool_get_oldest_idle does for the total connection limit. - let source = PathBuf::from("curl/lib/conncache.c"); - let contents = fs::read_to_string(&source).unwrap().replace("\r\n", "\n"); - let before = " if(!CONN_INUSE(conn)) {\n /* Set higher score for the age passed since the connection was used */"; - let after = " if(!CONN_INUSE(conn) && !conn->bits.close && !conn->connect_only) {\n /* Set higher score for the age passed since the connection was used */"; - assert_eq!( - contents.matches(before).count(), - 1, - "recheck the connect-only connection eviction patch against bundled libcurl" - ); - let patched = dst.join("conncache.c"); - fs::write(&patched, contents.replacen(before, after, 1)).unwrap(); - patched -} - fn websocket_source(dst: &Path) -> PathBuf { let source = PathBuf::from("curl/lib/ws.c"); if !cfg!(feature = "websocket") { @@ -608,7 +635,7 @@ fn websocket_source(dst: &Path) -> PathBuf { // patched translation unit without modifying the pinned upstream submodule // or requiring a patch executable on Windows. Recheck on libcurl updates. let before = " k->upgr101 = UPGR101_RECEIVED;\n k->header = FALSE; /* we will not get more responses */\n"; - let after = " k->upgr101 = UPGR101_RECEIVED;\n k->header = FALSE; /* we will not get more responses */\n\n /* HTTP persistence no longer governs an accepted protocol upgrade. */\n if(k->httpversion == 10)\n connkeep(data->conn, \"WebSocket upgrade\");\n"; + let after = " k->upgr101 = UPGR101_RECEIVED;\n k->header = FALSE; /* we will not get more responses */\n\n /* HTTP persistence no longer governs an accepted protocol upgrade. */\n if(k->httpversion == 10)\n connkeep(data->conn);\n"; let mut contents = fs::read_to_string(&source).unwrap().replace("\r\n", "\n"); assert_eq!( contents.matches(before).count(), @@ -621,8 +648,15 @@ fn websocket_source(dst: &Path) -> PathBuf { // can prefetch an entire stream before returning one payload chunk and // prevent the caller from applying receive backpressure. Read one bounded // buffer chunk instead, then let the WebSocket decoder return to the caller. - let before = "Curl_bufq_slurp(&ws->recvbuf, nw_in_recv, data, &n)"; - let after = "Curl_bufq_sipn(&ws->recvbuf, 0, nw_in_recv, data, &n)"; + // Newer libcurl queues automatic PONGs. Flush them before another read: + // a peer may wait for the PONG before sending any application payload. + let before = " result = Curl_bufq_slurp(&ws->recvbuf, nw_in_recv, data, &n);"; + let after = r#" if(!data->set.ws_raw_mode && !ws->using_reader && ws->pending.type) { + result = ws_flush(data, ws, Curl_api_is_in_callback(data)); + if(result && result != CURLE_AGAIN) + goto out; + } + result = Curl_bufq_sipn(&ws->recvbuf, 0, nw_in_recv, data, &n);"#; assert_eq!( contents.matches(before).count(), 1, diff --git a/curl-sys/curl b/curl-sys/curl index 8c908d2d0..3163ec065 160000 --- a/curl-sys/curl +++ b/curl-sys/curl @@ -1 +1 @@ -Subproject commit 8c908d2d0a6d32abdedda2c52e90bd56ec76c24d +Subproject commit 3163ec065643ba20709f190111f42c3e1fe95273 diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index a9c9bbc00..cfdd1037d 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -318,9 +318,15 @@ pub const CURLAUTH_NTLM: c_ulong = 1 << 3; pub const CURLAUTH_DIGEST_IE: c_ulong = 1 << 4; pub const CURLAUTH_NTLM_WB: c_ulong = 1 << 5; pub const CURLAUTH_AWS_SIGV4: c_ulong = 1 << 7; +pub const CURLAUTH_HTTPSIG: c_ulong = 1 << 8; // pub const CURLAUTH_ONLY: c_ulong = 1 << 31; -pub const CURLAUTH_ANY: c_ulong = (!CURLAUTH_DIGEST_IE) & 0xffffffff; -pub const CURLAUTH_ANYSAFE: c_ulong = (!(CURLAUTH_BASIC | CURLAUTH_DIGEST_IE)) & 0xffffffff; +pub const CURLAUTH_ANY: c_ulong = (!(CURLAUTH_DIGEST_IE | CURLAUTH_HTTPSIG)) & 0xffffffff; +pub const CURLAUTH_ANYSAFE: c_ulong = + (!(CURLAUTH_BASIC | CURLAUTH_DIGEST_IE | CURLAUTH_HTTPSIG)) & 0xffffffff; + +pub const CURLHTTPSIG_NONE: c_long = 0; +pub const CURLHTTPSIG_ED25519: c_long = 1; +pub const CURLHTTPSIG_HMAC_SHA256: c_long = 2; // pub const CURLSSH_AUTH_ANY: c_ulong = !0; // pub const CURLSSH_AUTH_NONE: c_ulong = 0; @@ -643,6 +649,10 @@ pub const CURLOPT_DOH_SSL_VERIFYSTATUS: CURLoption = CURLOPTTYPE_LONG + 308; pub const CURLOPT_CAINFO_BLOB: CURLoption = CURLOPTTYPE_BLOB + 309; pub const CURLOPT_PROXY_CAINFO_BLOB: CURLoption = CURLOPTTYPE_BLOB + 310; +pub const CURLOPT_HTTPSIG_ALGORITHM: CURLoption = CURLOPTTYPE_VALUES + 329; +pub const CURLOPT_HTTPSIG_KEY: CURLoption = CURLOPTTYPE_OBJECTPOINT + 330; +pub const CURLOPT_HTTPSIG_KEYID: CURLoption = CURLOPTTYPE_OBJECTPOINT + 331; +pub const CURLOPT_HTTPSIG_HEADERS: CURLoption = CURLOPTTYPE_OBJECTPOINT + 332; pub const CURLOPT_PREREQFUNCTION: CURLoption = CURLOPTTYPE_FUNCTIONPOINT + 312; pub const CURLOPT_PREREQDATA: CURLoption = CURLOPTTYPE_OBJECTPOINT + 313; diff --git a/src/easy/handler.rs b/src/easy/handler.rs index 82e203e7e..1bfe03dad 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::cell::UnsafeCell; use std::convert::TryFrom; use std::ffi::{CStr, CString}; use std::fmt; @@ -408,11 +408,12 @@ pub struct Easy2 { struct Inner { handle: *mut curl_sys::CURL, header_list: Option, - proxy_header_list: Option, resolve_list: Option, connect_to_list: Option, + proxy_header_list: Option, form: Option
, - error_buf: RefCell>, + // see Easy2::take_error_buf for an explanation of when this is accessed + error_buf: Box>, handler: H, } @@ -619,11 +620,11 @@ impl Easy2 { inner: Box::new(Inner { handle, header_list: None, - proxy_header_list: None, resolve_list: None, connect_to_list: None, + proxy_header_list: None, form: None, - error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), + error_buf: Box::new(UnsafeCell::new([0; curl_sys::CURL_ERROR_SIZE])), handler, }), }; @@ -664,7 +665,7 @@ impl Easy2 { fn default_configure(&mut self) { self.setopt_ptr( curl_sys::CURLOPT_ERRORBUFFER, - self.inner.error_buf.borrow().as_ptr() as *const _, + self.inner.error_buf.get() as *const u8 as *const _, ) .expect("failed to set error buffer"); let _ = self.signal(false); @@ -738,11 +739,9 @@ impl Easy2 { #[cfg(apple_sectrust)] fn ssl_configure(&mut self) { - // Setting any CA option, even to an automatically discovered file, - // disables libcurl's default SecTrust selection. Only explicit - // environment overrides should select file-based verification here. - // Do not require the paths to exist: a bad override must fail instead - // of silently falling back to a different trust source. + // Let SecTrust supply the native roots unless the caller explicitly + // selects certificate paths. Preserve invalid overrides as well; + // silently ignoring them would choose a different trust source. if let Some(path) = std::env::var_os("SSL_CERT_FILE") { let _ = self.cainfo(Path::new(&path)); } @@ -3711,13 +3710,25 @@ impl Easy2 { /// This function will clear the internal buffer, so this is an operation /// that mutates the handle internally. pub fn take_error_buf(&self) -> Option { - let mut buf = self.inner.error_buf.borrow_mut(); - if buf[0] == 0 { - return None; + let buf_ptr = self.inner.error_buf.get(); + let msg = { + // SAFETY: The only places where this buffer is accessed are this function and libcurl + // code. Only *mut pointers and a shared reference are created, but never mutable + // references. + // The curl documentation isn't very detailed about this, but we can probably assume + // that the buffer can be safely read and cleared as long as no libcurl code is + // currently running on this thread. + let buf = unsafe { &*buf_ptr }; + if buf[0] == 0 { + return None; + } + let pos = buf.iter().position(|i| *i == 0).unwrap_or(buf.len()); + String::from_utf8_lossy(&buf[..pos]).into_owned() + }; + // SAFETY: No shared reference exists anymore. + unsafe { + (buf_ptr as *mut u8).write(0); } - let pos = buf.iter().position(|i| *i == 0).unwrap_or(buf.len()); - let msg = String::from_utf8_lossy(&buf[..pos]).into_owned(); - buf[0] = 0; Some(msg) } diff --git a/src/multi.rs b/src/multi.rs index 96ddfe388..365394996 100644 --- a/src/multi.rs +++ b/src/multi.rs @@ -1,8 +1,10 @@ //! Multi - initiating multiple requests simultaneously +use std::cell::RefCell; use std::fmt; use std::marker; use std::ptr; +use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -31,8 +33,7 @@ use crate::{Error, MultiError}; /// /// [multi tutorial]: https://curl.haxx.se/libcurl/c/libcurl-multi.html pub struct Multi { - raw: Arc, - data: Box, + inner: Rc, } #[derive(Debug)] @@ -40,9 +41,15 @@ struct RawMulti { handle: *mut curl_sys::CURLM, } -struct MultiData { - socket: Box, - timer: Box) -> bool + Send>, +/// Inner data of a safe multi handle. Holds a reference to the raw multi +/// handle, as well as callback functions set by the user. This struct is +/// `!Send` and is locked to whatever thread the `Multi` is created on. However, +/// the inner `RawMulti` can be shared with another thread, but only to be used +/// for calling thread-safe functions. +struct MultiInner { + raw: Arc, + socket_function: RefCell>, + timer_function: RefCell) -> bool + Send>>, } /// Message from the `messages` function of a multi handle. @@ -82,7 +89,7 @@ pub struct Easy2Handle { /// A guard struct which guarantees that `curl_multi_remove_handle` will be /// called on an easy handle, either manually or on drop. struct DetachGuard { - multi: Arc, + multi: Rc, easy: *mut curl_sys::CURL, } @@ -133,10 +140,10 @@ impl Multi { let ptr = curl_sys::curl_multi_init(); assert!(!ptr.is_null()); Multi { - raw: Arc::new(RawMulti { handle: ptr }), - data: Box::new(MultiData { - socket: Box::new(|_, _, _| ()), - timer: Box::new(|_| true), + inner: Rc::new(MultiInner { + raw: Arc::new(RawMulti { handle: ptr }), + socket_function: RefCell::new(Box::new(|_, _, _| ())), + timer_function: RefCell::new(Box::new(|_| true)), }), } } @@ -167,14 +174,14 @@ impl Multi { &mut self, f: Box, ) -> Result<(), MultiError> { - self.data.socket = f; + let _ = self.inner.socket_function.replace(f); let cb: curl_sys::curl_socket_callback = cb; self.setopt_ptr( curl_sys::CURLMOPT_SOCKETFUNCTION, cb as usize as *const c_char, )?; - let ptr = &*self.data as *const _; - self.setopt_ptr(curl_sys::CURLMOPT_SOCKETDATA, ptr as *const c_char)?; + let ptr = Rc::as_ptr(&self.inner).cast(); + self.setopt_ptr(curl_sys::CURLMOPT_SOCKETDATA, ptr)?; return Ok(()); // TODO: figure out how to expose `_easy` @@ -186,10 +193,15 @@ impl Multi { socketp: *mut c_void, ) -> c_int { panic::catch(|| unsafe { - let f = &mut (*(userptr as *mut MultiData)).socket; - f(socket, SocketEvents { bits: what }, socketp as usize) - }); - 0 + let inner = &*userptr.cast_const().cast::(); + if let Ok(mut f) = inner.socket_function.try_borrow_mut() { + f(socket, SocketEvents { bits: what }, socketp as usize); + 0 + } else { + -1 + } + }) + .unwrap_or(-1) } } @@ -228,7 +240,7 @@ impl Multi { pub fn assign(&self, socket: Socket, token: usize) -> Result<(), MultiError> { unsafe { cvt(curl_sys::curl_multi_assign( - self.raw.handle, + self.inner.raw.handle, socket, token as *mut _, ))?; @@ -266,14 +278,14 @@ impl Multi { &mut self, f: Box) -> bool + Send>, ) -> Result<(), MultiError> { - self.data.timer = f; + let _ = self.inner.timer_function.replace(f); let cb: curl_sys::curl_multi_timer_callback = cb; self.setopt_ptr( curl_sys::CURLMOPT_TIMERFUNCTION, cb as usize as *const c_char, )?; - let ptr = &*self.data as *const _; - self.setopt_ptr(curl_sys::CURLMOPT_TIMERDATA, ptr as *const c_char)?; + let ptr = Rc::as_ptr(&self.inner).cast(); + self.setopt_ptr(curl_sys::CURLMOPT_TIMERDATA, ptr)?; return Ok(()); // TODO: figure out how to expose `_multi` @@ -283,11 +295,15 @@ impl Multi { user: *mut c_void, ) -> c_int { let keep_going = panic::catch(|| unsafe { - let f = &mut (*(user as *mut MultiData)).timer; - if timeout_ms == -1 { - f(None) + let inner = &*user.cast_const().cast::(); + if let Ok(mut f) = inner.timer_function.try_borrow_mut() { + if timeout_ms == -1 { + f(None) + } else { + f(Some(Duration::from_millis(timeout_ms as u64))) + } } else { - f(Some(Duration::from_millis(timeout_ms as u64))) + false } }) .unwrap_or(false); @@ -382,7 +398,7 @@ impl Multi { } fn setopt_long(&mut self, opt: curl_sys::CURLMoption, val: c_long) -> Result<(), MultiError> { - unsafe { cvt(curl_sys::curl_multi_setopt(self.raw.handle, opt, val)) } + unsafe { cvt(curl_sys::curl_multi_setopt(self.inner.raw.handle, opt, val)) } } fn setopt_ptr( @@ -390,7 +406,7 @@ impl Multi { opt: curl_sys::CURLMoption, val: *const c_char, ) -> Result<(), MultiError> { - unsafe { cvt(curl_sys::curl_multi_setopt(self.raw.handle, opt, val)) } + unsafe { cvt(curl_sys::curl_multi_setopt(self.inner.raw.handle, opt, val)) } } /// Add an easy handle to a multi session @@ -418,11 +434,14 @@ impl Multi { easy.transfer(); unsafe { - cvt(curl_sys::curl_multi_add_handle(self.raw.handle, easy.raw()))?; + cvt(curl_sys::curl_multi_add_handle( + self.inner.raw.handle, + easy.raw(), + ))?; } Ok(EasyHandle { guard: DetachGuard { - multi: self.raw.clone(), + multi: self.inner.clone(), easy: easy.raw(), }, easy, @@ -433,11 +452,14 @@ impl Multi { /// Same as `add`, but works with the `Easy2` type. pub fn add2(&self, easy: Easy2) -> Result, MultiError> { unsafe { - cvt(curl_sys::curl_multi_add_handle(self.raw.handle, easy.raw()))?; + cvt(curl_sys::curl_multi_add_handle( + self.inner.raw.handle, + easy.raw(), + ))?; } Ok(Easy2Handle { guard: DetachGuard { - multi: self.raw.clone(), + multi: self.inner.clone(), easy: easy.raw(), }, easy, @@ -484,7 +506,7 @@ impl Multi { let mut queue = 0; unsafe { loop { - let ptr = curl_sys::curl_multi_info_read(self.raw.handle, &mut queue); + let ptr = curl_sys::curl_multi_info_read(self.inner.raw.handle, &mut queue); if ptr.is_null() { break; } @@ -518,7 +540,7 @@ impl Multi { let mut remaining = 0; unsafe { let result = curl_sys::curl_multi_socket_action( - self.raw.handle, + self.inner.raw.handle, socket, events.bits, &mut remaining, @@ -548,7 +570,7 @@ impl Multi { let mut remaining = 0; unsafe { let result = curl_sys::curl_multi_socket_action( - self.raw.handle, + self.inner.raw.handle, curl_sys::CURL_SOCKET_BAD, 0, &mut remaining, @@ -579,7 +601,7 @@ impl Multi { pub fn get_timeout(&self) -> Result, MultiError> { let mut ms = 0; unsafe { - cvt(curl_sys::curl_multi_timeout(self.raw.handle, &mut ms))?; + cvt(curl_sys::curl_multi_timeout(self.inner.raw.handle, &mut ms))?; if ms == -1 { Ok(None) } else { @@ -618,7 +640,7 @@ impl Multi { unsafe { let mut ret = 0; cvt(curl_sys::curl_multi_wait( - self.raw.handle, + self.inner.raw.handle, waitfds.as_mut_ptr() as *mut _, waitfds.len() as u32, timeout_ms, @@ -629,13 +651,8 @@ impl Multi { } fn timeout_i32(timeout: Duration) -> i32 { - let secs = timeout.as_secs(); - if secs > (i32::MAX / 1000) as u64 { - // Duration too large, clamp at maximum value. - i32::MAX - } else { - secs as i32 * 1000 + timeout.subsec_nanos() as i32 / 1_000_000 - } + // Duration too large, clamp at maximum value. + timeout.as_millis().min(i32::MAX as u128) as i32 } /// Block until activity is detected or a timeout passes. @@ -679,7 +696,7 @@ impl Multi { unsafe { let mut ret = 0; cvt(curl_sys::curl_multi_poll( - self.raw.handle, + self.inner.raw.handle, waitfds.as_mut_ptr() as *mut _, waitfds.len() as u32, timeout_ms, @@ -693,7 +710,7 @@ impl Multi { /// currently blocked in [Multi::poll]. #[cfg(feature = "poll_7_68_0")] pub fn waker(&self) -> MultiWaker { - MultiWaker::new(Arc::downgrade(&self.raw)) + MultiWaker::new(Arc::downgrade(&self.inner.raw)) } /// Reads/writes available data from each easy handle. @@ -739,7 +756,7 @@ impl Multi { pub fn perform(&self) -> Result { unsafe { let mut ret = 0; - let result = curl_sys::curl_multi_perform(self.raw.handle, &mut ret); + let result = curl_sys::curl_multi_perform(self.inner.raw.handle, &mut ret); // Re-throw only after libcurl has returned, just as Easy2::perform // does. Leaving the stored panic behind suppresses later callbacks // on this thread, even for otherwise unrelated transfers. @@ -792,7 +809,7 @@ impl Multi { let write = write.map(|r| r as *mut _).unwrap_or(ptr::null_mut()); let except = except.map(|r| r as *mut _).unwrap_or(ptr::null_mut()); cvt(curl_sys::curl_multi_fdset( - self.raw.handle, + self.inner.raw.handle, read, write, except, @@ -822,7 +839,40 @@ impl Multi { /// Get a pointer to the raw underlying CURLM handle. pub fn raw(&self) -> *mut curl_sys::CURLM { - self.raw.handle + self.inner.raw.handle + } +} + +impl Drop for MultiInner { + fn drop(&mut self) { + // The actual raw multi handle can outlive our wrapper (any attached + // easy handles will keep it alive until they are also dropped), so we + // need to make sure to remove any references to local data that we may + // have set on the multi handle before we are dropped. + + // Unset callback functions that are stored in our own struct. + unsafe { + curl_sys::curl_multi_setopt( + self.raw.handle, + curl_sys::CURLMOPT_SOCKETFUNCTION, + ptr::null::(), + ); + curl_sys::curl_multi_setopt( + self.raw.handle, + curl_sys::CURLMOPT_SOCKETDATA, + ptr::null::(), + ); + curl_sys::curl_multi_setopt( + self.raw.handle, + curl_sys::CURLMOPT_TIMERFUNCTION, + ptr::null::(), + ); + curl_sys::curl_multi_setopt( + self.raw.handle, + curl_sys::CURLMOPT_TIMERDATA, + ptr::null::(), + ); + } } } @@ -867,7 +917,9 @@ fn cvt(code: curl_sys::CURLMcode) -> Result<(), MultiError> { impl fmt::Debug for Multi { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Multi").field("raw", &self.raw).finish() + f.debug_struct("Multi") + .field("raw", &self.inner.raw) + .finish() } } @@ -1116,7 +1168,7 @@ impl DetachGuard { if !self.easy.is_null() { unsafe { cvt(curl_sys::curl_multi_remove_handle( - self.multi.handle, + self.multi.raw.handle, self.easy, ))? } @@ -1403,3 +1455,25 @@ impl fmt::Debug for WaitFd { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + static_assertions::assert_not_impl_any!(Multi: Send, Sync); + + #[test] + fn timeout_i32_clamps_at_max() { + assert_eq!(Multi::timeout_i32(Duration::from_millis(1500)), 1500); + assert_eq!( + Multi::timeout_i32(Duration::new(2_147_483, 647_999_999)), + i32::MAX + ); + assert_eq!( + Multi::timeout_i32(Duration::new(2_147_483, 648_000_000)), + i32::MAX + ); + assert_eq!(Multi::timeout_i32(Duration::MAX), i32::MAX); + } +} diff --git a/systest/Cargo.toml b/systest/Cargo.toml index 16c51c276..c0fb3e8ce 100644 --- a/systest/Cargo.toml +++ b/systest/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "systest" version = "0.1.0" -authors = ["Alex Crichton "] +authors = ["Alex Crichton ", "Stephen M. Coakley "] build = "build.rs" publish = false edition = "2018" @@ -11,8 +11,8 @@ curl-sys = { path = "../curl-sys" } libc = "0.2" [build-dependencies] +cc.workspace = true ctest2 = "0.4" -cc = "1.0" [features] static-ssl = ['curl-sys/static-ssl'] diff --git a/systest/build.rs b/systest/build.rs index c959d276c..3715e5514 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -100,10 +100,18 @@ fn main() { if version < (7, 45) && matches!(s, "CURLINFO_SOCKET" | "CURLINFO_ACTIVESOCKET") { return true; } - if version < (8, 19) { + if version < (8, 22) { match s { - // This constant changed value. - "CURLAUTH_ANY" | "CURLAUTH_ANYSAFE" => return true, + "CURLAUTH_HTTPSIG" + | "CURLHTTPSIG_NONE" + | "CURLHTTPSIG_ED25519" + | "CURLHTTPSIG_HMAC_SHA256" + | "CURLOPT_HTTPSIG_ALGORITHM" + | "CURLOPT_HTTPSIG_KEY" + | "CURLOPT_HTTPSIG_KEYID" + | "CURLOPT_HTTPSIG_HEADERS" + | "CURLAUTH_ANY" + | "CURLAUTH_ANYSAFE" => return true, _ => {} } } diff --git a/tests/apple_sectrust.rs b/tests/apple_sectrust.rs new file mode 100644 index 000000000..d5f6cdb93 --- /dev/null +++ b/tests/apple_sectrust.rs @@ -0,0 +1,67 @@ +#[cfg(all(target_vendor = "apple", feature = "apple-sectrust"))] +#[test] +fn bundled_build_enables_apple_sectrust() { + use std::ffi::CStr; + + assert!(curl::Version::get().vendored()); + + unsafe { + let info = curl_sys::curl_version_info(curl_sys::CURLVERSION_NOW); + assert!(!info.is_null()); + + let mut feature = (*info).feature_names; + assert!(!feature.is_null()); + + while !(*feature).is_null() { + if CStr::from_ptr(*feature).to_bytes() == b"AppleSecTrust" { + return; + } + feature = feature.add(1); + } + } + + panic!("bundled libcurl was not built with Apple SecTrust"); +} + +#[cfg(all(target_vendor = "apple", feature = "apple-sectrust"))] +#[test] +fn explicit_certificate_file_is_honored_after_new_and_reset() { + use std::net::TcpListener; + use std::process::Command; + use std::time::Duration; + + const CHILD: &str = "CURL_RUST_TEST_INVALID_CA_OVERRIDE"; + if std::env::var_os(CHILD).is_none() { + // Give the child its own environment; other tests may create handles + // concurrently and must keep their normal trust configuration. + let missing = + std::env::temp_dir().join(format!("curl-rust-missing-ca-{}.pem", std::process::id())); + assert!(!missing.exists()); + let output = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "explicit_certificate_file_is_honored_after_new_and_reset", + ]) + .env(CHILD, "1") + .env("SSL_CERT_FILE", missing) + .env_remove("SSL_CERT_DIR") + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + return; + } + + // Connecting works, but configuring TLS must reject the explicit missing + // CA file before it waits for a server handshake or falls back to SecTrust. + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("https://{}/", listener.local_addr().unwrap()); + let mut easy = curl::easy::Easy::new(); + for _ in 0..2 { + easy.url(&url).unwrap(); + easy.proxy("").unwrap(); + easy.timeout(Duration::from_secs(2)).unwrap(); + let error = easy.perform().unwrap_err(); + assert!(error.is_ssl_cacert_badfile(), "{error}"); + easy.reset(); + } +} diff --git a/tests/easy.rs b/tests/easy.rs index fad633417..654b3ef1d 100644 --- a/tests/easy.rs +++ b/tests/easy.rs @@ -1058,7 +1058,7 @@ fn test_connect_timeout() { assert_eq!( Error::new(curl_sys::CURLE_BAD_FUNCTION_ARGUMENT), easy2 - .connect_timeout(Duration::from_secs(std::u64::MAX)) + .connect_timeout(Duration::from_secs(u64::MAX)) .unwrap_err() ); @@ -1087,9 +1087,7 @@ fn test_timeout() { // Overflow value test must return an Error assert_eq!( Error::new(curl_sys::CURLE_BAD_FUNCTION_ARGUMENT), - easy2 - .timeout(Duration::from_secs(std::u64::MAX)) - .unwrap_err() + easy2.timeout(Duration::from_secs(u64::MAX)).unwrap_err() ); // Valid value diff --git a/tests/multi.rs b/tests/multi.rs index 44178e153..f21a108a5 100644 --- a/tests/multi.rs +++ b/tests/multi.rs @@ -1,6 +1,7 @@ #![cfg(unix)] use std::collections::HashMap; +use std::error::Error; use std::io::{Cursor, Read}; use std::time::Duration; @@ -352,3 +353,32 @@ fn pollfds() { assert!(waitfd.received_read()); } } + +#[test] +fn drop_easy_handle_before_multi() -> Result<(), Box> { + // Set up basically any request. + let mut easy = Easy::new(); + easy.url("http://example.org")?; + + // Set up a multi with any socket callback. + let mut multi = Multi::new(); + multi.socket_function(|_, _, _| {})?; + + // Attach the easy handle to the multi handle. + let handle = multi.add(easy)?; + + // Execute the initial steps of establishing a socket, but without waiting + // for the request to fully finish. + multi.perform()?; + multi.wait(&mut [], Duration::from_secs(1))?; + multi.perform()?; + + // Drop the multi handle. This will free the callback functions, leaving + // curl with dangling pointers. + drop(multi); + + // This should not segfault! + drop(handle); + + Ok(()) +} diff --git a/tests/server/mod.rs b/tests/server/mod.rs index bb25caf9b..b9477f41c 100644 --- a/tests/server/mod.rs +++ b/tests/server/mod.rs @@ -74,9 +74,11 @@ fn run(stream: impl Read + Write, rx: &Receiver) { actual, expected_headers ); } - for header in expected_headers { - panic!("expected header but not found: {:?}", header); - } + assert!( + expected_headers.is_empty(), + "expected headers but not found: {:?}", + expected_headers + ); let mut line = String::new(); let mut socket = match expected_len {