From beba562df28f553d29948a3076e310ff5fcbcbad Mon Sep 17 00:00:00 2001 From: Mega Mind <146339422+M3gA-Mind@users.noreply.github.com> Date: Thu, 21 May 2026 23:29:01 +0530 Subject: [PATCH] fix(windows): make pnpm dev:app:win work behind TLS-inspecting proxies (#2449) --- Cargo.lock | 3 + Cargo.toml | 15 +- app/src-tauri/Cargo.lock | 54 ++++++++ scripts/run-dev-win.sh | 130 +++++++++++++++--- src/api/rest.rs | 7 +- src/openhuman/app_state/ops.rs | 4 +- src/openhuman/composio/client.rs | 9 +- src/openhuman/config/schema/proxy.rs | 22 ++- .../inference/provider/compatible.rs | 16 ++- src/openhuman/integrations/client.rs | 15 +- src/openhuman/integrations/searxng.rs | 4 +- src/openhuman/integrations/seltz.rs | 4 +- src/openhuman/mod.rs | 1 + src/openhuman/tls.rs | 34 +++++ 14 files changed, 261 insertions(+), 57 deletions(-) create mode 100644 src/openhuman/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 1f8818db2..b49862334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7734,9 +7734,11 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", + "native-tls", "rustls", "rustls-pki-types", "tokio", + "tokio-native-tls", "tokio-rustls", "tungstenite 0.24.0", "webpki-roots 0.26.11", @@ -8010,6 +8012,7 @@ dependencies = [ "http 1.4.0", "httparse", "log", + "native-tls", "rand 0.8.6", "rustls", "rustls-pki-types", diff --git a/Cargo.toml b/Cargo.toml index 58ef0e58d..547ea5b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,9 @@ async-trait = "0.1" chacha20poly1305 = "0.10" hex = "0.4" tokio-util = { version = "0.7", features = ["rt", "io"] } -tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } +# tokio-tungstenite is declared per-target below so the TLS backend +# (native-tls on Windows, rustls on macOS / Linux) matches the reqwest +# backend selected at each TLS call site. futures = "0.3" rusqlite = { version = "0.37", features = ["bundled"] } chrono = { version = "0.4", features = ["serde"] } @@ -158,6 +160,17 @@ whatsapp-rust-tokio-transport = { version = "0.5", optional = true, default-feat whatsapp-rust-ureq-http-client = { version = "0.5", optional = true } wacore = { version = "0.5", optional = true, default-features = false } +[target.'cfg(windows)'.dependencies] +# Windows: tokio-tungstenite uses native-tls (schannel) so wss:// +# connections honor the Windows cert store, including corporate CAs +# installed by AV / TLS-inspection proxies. See run-dev-win.sh notes. +tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "handshake", "native-tls"] } + +[target.'cfg(not(windows))'.dependencies] +# macOS / Linux: keep rustls + Mozilla webpki-roots — the historical +# default. Avoids pulling OpenSSL as a runtime dep on Linux. +tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "handshake", "rustls-tls-webpki-roots"] } + [target.'cfg(target_os = "macos")'.dependencies] whisper-rs = { version = "0.16", features = ["metal"] } # Contacts framework bindings for address book seeding. diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 54350c952..eadcf4884 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -2114,6 +2114,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -3384,9 +3393,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -4309,6 +4320,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "motosan-ai-oauth" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16994a67367076b08479af83ca05503c4d423fc6631f849fb92fa787956ad557" +dependencies = [ + "base64 0.22.1", + "percent-encoding", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "sha2 0.10.9", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -5064,6 +5091,7 @@ dependencies = [ "lettre", "log", "mail-parser", + "motosan-ai-oauth", "nu-ansi-term 0.46.0", "objc2 0.6.4", "objc2-contacts", @@ -6363,6 +6391,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -6376,6 +6405,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "mime_guess", "native-tls", "percent-encoding", @@ -7714,6 +7744,27 @@ dependencies = [ "windows 0.57.0", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -8452,9 +8503,11 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", + "native-tls", "rustls", "rustls-pki-types", "tokio", + "tokio-native-tls", "tokio-rustls", "tungstenite 0.24.0", "webpki-roots 0.26.11", @@ -8772,6 +8825,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand 0.8.6", "rustls", "rustls-pki-types", diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh index aa2bdcef1..d30c716fc 100644 --- a/scripts/run-dev-win.sh +++ b/scripts/run-dev-win.sh @@ -530,6 +530,24 @@ PATH_PREFIX="/c/Program Files/CMake/bin:$(dirname "$NINJA_EXE")" if [[ -n "${CEF_RUNTIME_PATH:-}" ]]; then PATH_PREFIX="$PATH_PREFIX:$CEF_RUNTIME_PATH" fi +# Ensure the workspace node_modules/.bin is on PATH so pnpm's child +# spawns (e.g. `pnpm tauri dev` → `tauri.CMD`) can resolve the shims. +# Pnpm normally prepends `./node_modules/.bin` for script execution, but +# when the script body is `tauri "dev"` and the child shell is cmd.exe +# under the long bash→cmd→bash chain, the relative entry sometimes +# resolves against the wrong cwd and tauri.CMD is not found. +PATH_PREFIX="$APP_DIR/node_modules/.bin:$PATH_PREFIX" + +# Ensure pnpm itself stays on PATH for cargo-tauri's beforeDevCommand +# (`pnpm run dev` → vite). When run-dev-win.sh restores the Windows PATH +# via cmd.exe %PATH%, some setups (WinGet-installed pnpm with no +# AppData/Roaming/npm entry) don't surface a pnpm dir consistently +# downstream. Prepend the resolved $PNPM_EXE dir to guarantee it. +if [[ -n "${PNPM_EXE:-}" ]]; then + PNPM_EXE_DIR="$(dirname "$PNPM_EXE")" + PATH_PREFIX="$PNPM_EXE_DIR:$PATH_PREFIX" +fi + export PATH="$PATH_PREFIX:$PATH" "$PNPM_EXE" tauri:ensure @@ -599,28 +617,98 @@ else DEV_PORT=1420 fi -# Tauri spawns beforeDevCommand (`pnpm run dev`) via a native `cmd /S /C` -# inheriting THIS process's env. By here PATH has the full system PATH stacked -# several times over (vcvars rebuild + Git-Bash /etc/profile re-runs + pnpm -# .bin layering); the MSYS→Windows conversion overflows the process -# environment-block limit, so the child inherits an EMPTY PATH and Tauri dies -# with "'pnpm' is not recognized" (even `where` is gone). Collapse PATH to -# first-seen entries (clean POSIX `/c/...` entries, so ':' split is safe). -_dedup_seen=":" -_dedup_new="" -IFS=':' read -ra _dedup_parts <<< "$PATH" -for _dp in "${_dedup_parts[@]}"; do - [[ -z "$_dp" ]] && continue - case "$_dedup_seen" in *":$_dp:"*) continue ;; esac - _dedup_seen="${_dedup_seen}${_dp}:" - _dedup_new="${_dedup_new:+$_dedup_new:}$_dp" -done -export PATH="$_dedup_new" -echo "[run-dev-win] PATH de-duplicated: ${#_dedup_parts[@]} → $(awk -v RS=: 'END{print NR}' <<< "$_dedup_new") entries" +# Invoke cargo-tauri directly rather than going through `pnpm tauri dev`. +# +# The pnpm chain (pnpm.exe → cmd.exe → tauri.CMD) is fragile on Windows: +# whether `tauri.CMD` is resolvable in the spawned cmd subprocess depends +# on which pnpm shim was picked up by `find_pnpm`. The self-managing +# `~/AppData/Local/pnpm/.tools/.../pnpm` variant auto-prepends +# `node_modules/.bin` for script children; the WinGet-installed +# `pnpm.exe` does not, so the script body `tauri "dev"` then fails with +# "'tauri' is not recognized" inside cmd. +# +# `ensure-tauri-cli.sh` already installed the vendored CEF-aware +# cargo-tauri at `$REPO_ROOT/.cache/cargo-install/bin/cargo-tauri.exe`, +# so we can invoke that binary directly and skip the wrapper layer. +# +# Historical note: a previous version of this script ran a PATH +# deduplication loop (collapsing repeated entries that MSYS→Windows +# conversion stacked during vcvars / Git-Bash re-runs / pnpm layering). +# That loop was needed because the overflowing env block left child +# processes with an EMPTY PATH — even `where.exe` was gone, causing +# "'pnpm' is not recognized". Direct cargo-tauri.exe invocation with +# absolute paths in the .bat wrapper makes the env block size irrelevant: +# beforeDevCommand no longer needs PATH at all. +CARGO_TAURI_EXE="$REPO_ROOT/.cache/cargo-install/bin/cargo-tauri.exe" +if [[ ! -x "$CARGO_TAURI_EXE" ]]; then + echo "[run-dev-win] cargo-tauri.exe not found at $CARGO_TAURI_EXE" >&2 + echo "[run-dev-win] tauri:ensure should have installed it. Aborting." >&2 + exit 1 +fi +# Build a tauri.conf.json `-c` JSON merge that: +# - pins `beforeDevCommand` to the absolute pnpm path so cargo-tauri's +# cmd.exe child can find pnpm regardless of any PATH stripping +# between bash → cargo-tauri → cmd. The default in tauri.conf.json +# is `"pnpm run dev"` (bare name) which depends on PATH. +# - overrides `devUrl` when OPENHUMAN_DEV_PORT is non-default. +# Point beforeDevCommand at vite via a wrapper batch file in a +# space-free temp directory. +# +# Why a wrapper instead of the absolute path directly: +# cargo-tauri runs beforeDevCommand as `cmd.exe /S /C `. Rust's +# argv-to-cmd argument escaping strips literal double-quotes from the +# string, so if our `` is `"E:\Office Files\…\vite.CMD"`, +# cmd ends up parsing `E:\Office` as the program name and the rest as +# arguments — "'E:\Office' is not recognized". 8.3 short-name fallback +# also fails when 8dot3name is disabled on the drive (as it is on this +# workspace's E: drive). +# +# The fix is to call the spacey path from INSIDE a .bat file, where we +# can quote it however we want without involving cargo-tauri's outer +# escaping. The wrapper lives under %TEMP% (which is normally +# space-free) so its own path doesn't need quoting either. +VITE_JS_UNIX="$APP_DIR/node_modules/vite/bin/vite.js" +if [[ ! -f "$VITE_JS_UNIX" ]]; then + echo "[run-dev-win] vite entry not found at $VITE_JS_UNIX" >&2 + echo "[run-dev-win] Did 'pnpm install' run? Aborting." >&2 + exit 1 +fi +VITE_JS_WIN="$(cygpath -w "$VITE_JS_UNIX" 2>/dev/null || printf '%s' "$VITE_JS_UNIX")" + +# Resolve node.exe absolute path so the wrapper doesn't depend on +# whatever PATH cargo-tauri hands to its cmd child. +NODE_EXE_UNIX="$(command -v node.exe 2>/dev/null || command -v node 2>/dev/null)" +if [[ -z "$NODE_EXE_UNIX" || ! -f "$NODE_EXE_UNIX" ]]; then + echo "[run-dev-win] node.exe not findable on bash PATH at wrapper-build time" >&2 + exit 1 +fi +NODE_EXE_WIN="$(cygpath -w "$NODE_EXE_UNIX" 2>/dev/null || printf '%s' "$NODE_EXE_UNIX")" + +WRAPPER_DIR_UNIX="$(cygpath -u "${TEMP:-${TMP:-/tmp}}" 2>/dev/null || echo /tmp)/openhuman-dev" +mkdir -p "$WRAPPER_DIR_UNIX" +VITE_WRAPPER_UNIX="$WRAPPER_DIR_UNIX/run-vite.bat" +# Invoke node.exe with vite's JS entry directly. The vite.CMD shim +# falls back to bare `node` when its sibling doesn't have node.exe, +# which fails inside cargo-tauri's cmd child (no node on PATH). +{ + printf '@echo off\r\n' + printf '"%s" "%s" %%*\r\n' "$NODE_EXE_WIN" "$VITE_JS_WIN" +} > "$VITE_WRAPPER_UNIX" +VITE_WRAPPER_WIN="$(cygpath -w "$VITE_WRAPPER_UNIX" 2>/dev/null || printf '%s' "$VITE_WRAPPER_UNIX")" +if [[ "$VITE_WRAPPER_WIN" == *" "* ]]; then + echo "[run-dev-win] wrapper path contains spaces: $VITE_WRAPPER_WIN" >&2 + echo "[run-dev-win] set TEMP/TMP to a space-free path (e.g. C:\\Temp) and retry." >&2 + exit 1 +fi +echo "[run-dev-win] vite wrapper at: $VITE_WRAPPER_WIN" +BEFORE_DEV_CMD="${VITE_WRAPPER_WIN//\\/\\\\}" +CONFIG_OVERRIDE="{\"build\":{\"beforeDevCommand\":\"$BEFORE_DEV_CMD\"" if (( DEV_PORT != 1420 )); then echo "[run-dev-win] OPENHUMAN_DEV_PORT=$DEV_PORT — overriding tauri devUrl" - "$PNPM_EXE" tauri dev -c "{\"build\":{\"devUrl\":\"http://localhost:$DEV_PORT\"}}" -else - "$PNPM_EXE" tauri dev + CONFIG_OVERRIDE+=",\"devUrl\":\"http://localhost:$DEV_PORT\"" fi +CONFIG_OVERRIDE+="}}" + +echo "[run-dev-win] tauri config override: $CONFIG_OVERRIDE" +"$CARGO_TAURI_EXE" dev -c "$CONFIG_OVERRIDE" diff --git a/src/api/rest.rs b/src/api/rest.rs index 3c2e481bb..74bf02c6d 100644 --- a/src/api/rest.rs +++ b/src/api/rest.rs @@ -78,10 +78,11 @@ fn build_backend_reqwest_client() -> Result { ); } - // Force rustls for consistent cross-platform TLS behavior. - Client::builder() + // Platform-appropriate TLS backend: Windows → schannel (honors the OS + // cert store, required for corporate TLS-inspection proxies); macOS / + // Linux → rustls. See [`crate::openhuman::tls::tls_client_builder`]. + crate::openhuman::tls::tls_client_builder() .default_headers(default_headers) - .use_rustls_tls() .http1_only() .timeout(Duration::from_secs(120)) .connect_timeout(Duration::from_secs(15)) diff --git a/src/openhuman/app_state/ops.rs b/src/openhuman/app_state/ops.rs index 5e6517423..cd609b9c0 100644 --- a/src/openhuman/app_state/ops.rs +++ b/src/openhuman/app_state/ops.rs @@ -263,8 +263,8 @@ fn save_stored_app_state(config: &Config, state: &StoredAppState) -> Result<(), } fn build_client() -> Result { - Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + crate::openhuman::tls::tls_client_builder() .http1_only() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(10)) diff --git a/src/openhuman/composio/client.rs b/src/openhuman/composio/client.rs index 27d355067..d55a80501 100644 --- a/src/openhuman/composio/client.rs +++ b/src/openhuman/composio/client.rs @@ -455,11 +455,10 @@ impl ComposioClient { // from `IntegrationClient`, which we intentionally avoid so the // public surface of that type doesn't widen for one caller. // - // Mirror the TLS settings of the shared client - // (`use_rustls_tls + http1_only`) so this path has the same - // connection behaviour as the other backend calls. - let http_client = reqwest::Client::builder() - .use_rustls_tls() + // Mirror the TLS settings of the shared client so this path has the + // same connection behaviour as the other backend calls. + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let http_client = crate::openhuman::tls::tls_client_builder() .http1_only() .timeout(std::time::Duration::from_secs(60)) .connect_timeout(std::time::Duration::from_secs(15)) diff --git a/src/openhuman/config/schema/proxy.rs b/src/openhuman/config/schema/proxy.rs index b843d397e..56dc883cd 100644 --- a/src/openhuman/config/schema/proxy.rs +++ b/src/openhuman/config/schema/proxy.rs @@ -441,10 +441,15 @@ pub fn build_runtime_proxy_client(service_key: &str) -> reqwest::Client { return client; } - let builder = apply_runtime_proxy_to_builder(reqwest::Client::builder(), service_key); + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let builder = + apply_runtime_proxy_to_builder(crate::openhuman::tls::tls_client_builder(), service_key); let client = builder.build().unwrap_or_else(|error| { tracing::warn!(service_key, "Failed to build proxied client: {error}"); - reqwest::Client::new() + // Apply the same platform TLS selection on the fallback path so the + // error-path client also honors the Windows cert store. + let fb = crate::openhuman::tls::tls_client_builder(); + fb.build().unwrap_or_default() }); set_runtime_proxy_cached_client(cache_key, client.clone()); client @@ -461,16 +466,23 @@ pub fn build_runtime_proxy_client_with_timeouts( return client; } - let builder = reqwest::Client::builder() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let raw = crate::openhuman::tls::tls_client_builder() .timeout(std::time::Duration::from_secs(timeout_secs)) .connect_timeout(std::time::Duration::from_secs(connect_timeout_secs)); - let builder = apply_runtime_proxy_to_builder(builder, service_key); + let builder = apply_runtime_proxy_to_builder(raw, service_key); let client = builder.build().unwrap_or_else(|error| { tracing::warn!( service_key, "Failed to build proxied timeout client: {error}" ); - reqwest::Client::new() + // Apply the same platform TLS selection and timeouts on the fallback + // path so the error-path client also honors the Windows cert store + // and remains bounded. + let fb = crate::openhuman::tls::tls_client_builder() + .timeout(std::time::Duration::from_secs(timeout_secs)) + .connect_timeout(std::time::Duration::from_secs(connect_timeout_secs)); + fb.build().unwrap_or_default() }); set_runtime_proxy_cached_client(cache_key, client.clone()); client diff --git a/src/openhuman/inference/provider/compatible.rs b/src/openhuman/inference/provider/compatible.rs index dccfea5fe..b289cc574 100644 --- a/src/openhuman/inference/provider/compatible.rs +++ b/src/openhuman/inference/provider/compatible.rs @@ -278,8 +278,8 @@ impl OpenAiCompatibleProvider { headers.insert(USER_AGENT, value); } - let builder = Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let builder = crate::openhuman::tls::tls_client_builder() .timeout(std::time::Duration::from_secs(120)) .connect_timeout(std::time::Duration::from_secs(10)) .default_headers(headers); @@ -290,12 +290,14 @@ impl OpenAiCompatibleProvider { return builder.build().unwrap_or_else(|error| { tracing::warn!("Failed to build proxied timeout client with user-agent: {error}"); - Client::new() + crate::openhuman::tls::tls_client_builder() + .build() + .unwrap_or_default() }); } - let builder = Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let builder = crate::openhuman::tls::tls_client_builder() .timeout(std::time::Duration::from_secs(120)) .connect_timeout(std::time::Duration::from_secs(10)); let builder = crate::openhuman::config::apply_runtime_proxy_to_builder( @@ -304,7 +306,9 @@ impl OpenAiCompatibleProvider { ); builder.build().unwrap_or_else(|error| { tracing::warn!("Failed to build proxied timeout client: {error}"); - Client::new() + crate::openhuman::tls::tls_client_builder() + .build() + .unwrap_or_default() }) } diff --git a/src/openhuman/integrations/client.rs b/src/openhuman/integrations/client.rs index 1b3546816..ab77f8d57 100644 --- a/src/openhuman/integrations/client.rs +++ b/src/openhuman/integrations/client.rs @@ -92,16 +92,11 @@ impl IntegrationClient { // to fix up the input so the regression is observable in logs. let backend_url = sanitize_backend_url(&backend_url); - // Match the TLS config used by `BackendOAuthClient` in - // `src/api/rest.rs`: force rustls + HTTP/1.1 so we get the same - // consistent cross-platform behaviour every other backend-proxied - // domain (billing, team, webhooks, referral, …) already relies - // on. The default builder picks up native-tls on macOS, which - // has historically failed on staging TLS handshakes while - // rustls succeeds — so the integrations client was the odd one - // out with raw "error sending request" failures. - let http_client = reqwest::Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + // Windows uses schannel (native-tls) to honor the OS cert store; + // macOS / Linux keep rustls which avoids the OpenSSL runtime dep and + // has historically been more reliable on staging TLS handshakes. + let http_client = crate::openhuman::tls::tls_client_builder() .http1_only() .timeout(Duration::from_secs(60)) .connect_timeout(Duration::from_secs(15)) diff --git a/src/openhuman/integrations/searxng.rs b/src/openhuman/integrations/searxng.rs index 6eb878b14..4f7649946 100644 --- a/src/openhuman/integrations/searxng.rs +++ b/src/openhuman/integrations/searxng.rs @@ -23,8 +23,8 @@ fn shared_http_client() -> reqwest::Client { SHARED_HTTP_CLIENT .get_or_init(|| { tracing::debug!("[searxng] initializing shared HTTP client"); - reqwest::Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + crate::openhuman::tls::tls_client_builder() .build() .expect("failed to build shared SearXNG HTTP client") }) diff --git a/src/openhuman/integrations/seltz.rs b/src/openhuman/integrations/seltz.rs index 22c12a48e..a9e4b4a60 100644 --- a/src/openhuman/integrations/seltz.rs +++ b/src/openhuman/integrations/seltz.rs @@ -64,8 +64,8 @@ impl SeltzSearchTool { timeout_secs: u64, ) -> Self { let timeout = timeout_secs.max(1); - let http_client = reqwest::Client::builder() - .use_rustls_tls() + // Platform-appropriate TLS backend — see [`crate::openhuman::tls`]. + let http_client = crate::openhuman::tls::tls_client_builder() .http1_only() .timeout(Duration::from_secs(timeout)) .connect_timeout(Duration::from_secs(10)) diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index 5c081d3c7..6fc21db60 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -74,6 +74,7 @@ pub mod team; pub mod test_support; pub mod text_input; pub mod threads; +pub mod tls; pub mod todos; pub mod tokenjuice; pub mod tool_registry; diff --git a/src/openhuman/tls.rs b/src/openhuman/tls.rs new file mode 100644 index 000000000..cb9a64442 --- /dev/null +++ b/src/openhuman/tls.rs @@ -0,0 +1,34 @@ +//! Platform-conditional TLS backend selection for reqwest clients. +//! +//! Centralises the `#[cfg(target_os = "windows")]` / `#[cfg(not(target_os = "windows"))]` +//! guard so every HTTP-client construction site stays at one line and future +//! policy changes (e.g. adding native-tls on macOS) only require editing this file. +//! +//! # Policy +//! - **Windows**: `native-tls` (schannel) — honors the Windows certificate store, +//! including any corporate CA installed by AV / TLS-inspecting proxies that +//! re-sign certificates with a private root. `rustls` + webpki-roots only knows +//! Mozilla CAs and fails such environments with `UnknownIssuer`. +//! - **macOS / Linux**: `rustls` + webpki-roots — avoids the OpenSSL runtime +//! dependency on Linux and has historically been more reliable on macOS staging +//! TLS handshakes than `native-tls`. + +/// Return a `reqwest::ClientBuilder` pre-configured with the platform-appropriate +/// TLS backend. +/// +/// Use this as the starting point for every client that needs to reach external +/// HTTPS endpoints: +/// ```rust,ignore +/// let client = tls_client_builder() +/// .http1_only() +/// .timeout(Duration::from_secs(30)) +/// .build()?; +/// ``` +pub fn tls_client_builder() -> reqwest::ClientBuilder { + let b = reqwest::Client::builder(); + #[cfg(target_os = "windows")] + let b = b.use_native_tls(); + #[cfg(not(target_os = "windows"))] + let b = b.use_rustls_tls(); + b +}