diff --git a/Cargo.lock b/Cargo.lock index d9d4ddb..78b689c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -159,6 +165,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.9.1" @@ -217,6 +233,22 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -805,6 +837,50 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -1041,6 +1117,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ordered-float" version = "4.6.0" @@ -1460,6 +1542,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.0" @@ -1469,6 +1563,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -1492,12 +1613,53 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1926,6 +2088,7 @@ dependencies = [ "percent-encoding", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "ureq-proto", @@ -2002,6 +2165,16 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2062,6 +2235,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.8" @@ -2159,6 +2341,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2171,13 +2362,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2189,34 +2389,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2229,24 +2462,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 9d8ce20..064ffa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,5 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" sha2 = "0.11.0" thiserror = "2.0.18" -ureq = { version = "3.3.0", features = ["json"] } +ureq = { version = "3.3.0", features = ["json", "platform-verifier"] } url = "2.5.8" diff --git a/README.md b/README.md index 432152e..0280947 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,13 @@ LANE_OAUTH_SCOPE=api and refresh tokens are stored under the Lane config directory with owner-only file permissions on Unix. +TLS certificate verification defaults to the platform trust store. For internal +GitLab instances behind a corporate CA, install the corporate root certificate in +the OS trust store and run Lane normally. Set `LANE_TLS_ROOTS=webpki` to force the +old bundled Mozilla roots behavior. As a last-resort diagnostic only, +`LANE_TLS_DISABLE_VERIFICATION=true` disables certificate verification and should +not be used with real tokens. + Press `?` inside the app for the full keyboard reference (`/` search, `n` new issue, `g` generate stories, `s` sync, `hjkl`/arrows to navigate). diff --git a/src/config.rs b/src/config.rs index 53e6584..f37b71d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -43,6 +43,18 @@ pub enum AuthMode { OAuth, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TlsRoots { + Platform, + WebPki, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TlsPolicy { + pub roots: TlsRoots, + pub disable_verification: bool, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct GitLabConfig { pub instance_url: Url, @@ -65,6 +77,7 @@ pub struct LlmConfig { pub struct AppConfig { pub gitlab: GitLabConfig, pub llm: LlmConfig, + pub tls: TlsPolicy, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -83,6 +96,8 @@ pub struct EnvConfig { pub oauth_scope: Option, pub oauth_refresh_token: Option, pub oauth_access_expires_at: Option, + pub tls_roots: Option, + pub tls_disable_verification: Option, } const DEFAULT_LLM_TEMPERATURE: f32 = 0.4; @@ -90,6 +105,7 @@ const DEFAULT_LLM_MODEL: &str = "gpt-oss:120b"; const DEFAULT_LLM_BASE_URL: &str = "https://ollama.com/v1"; const DEFAULT_LLM_ENABLED: bool = false; const DEFAULT_OAUTH_SCOPE: &str = "api"; +const DEFAULT_TLS_ROOTS: TlsRoots = TlsRoots::Platform; impl EnvConfig { pub fn from_env() -> Self { @@ -108,6 +124,8 @@ impl EnvConfig { oauth_scope: env::var("LANE_OAUTH_SCOPE").ok(), oauth_refresh_token: env::var("LANE_OAUTH_REFRESH_TOKEN").ok(), oauth_access_expires_at: env::var("LANE_OAUTH_ACCESS_EXPIRES_AT").ok(), + tls_roots: env::var("LANE_TLS_ROOTS").ok(), + tls_disable_verification: env::var("LANE_TLS_DISABLE_VERIFICATION").ok(), } } } @@ -145,6 +163,14 @@ impl AppConfig { env.llm_base_url .unwrap_or_else(|| DEFAULT_LLM_BASE_URL.to_string()), )?; + let tls_roots = match env.tls_roots { + Some(value) => parse_tls_roots("LANE_TLS_ROOTS", &value)?, + None => DEFAULT_TLS_ROOTS, + }; + let tls_disable_verification = match env.tls_disable_verification { + Some(value) => parse_bool("LANE_TLS_DISABLE_VERIFICATION", &value)?, + None => false, + }; let oauth_requested = env.oauth_client_id.is_some() || env.oauth_client_secret.is_some() @@ -211,10 +237,25 @@ impl AppConfig { enabled: llm_enabled, temperature: llm_temperature, }, + tls: TlsPolicy { + roots: tls_roots, + disable_verification: tls_disable_verification, + }, }) } } +fn parse_tls_roots(name: &'static str, value: &str) -> Result { + match value.to_lowercase().as_str() { + "platform" | "native" | "os" | "system" => Ok(TlsRoots::Platform), + "webpki" | "mozilla" | "bundled" => Ok(TlsRoots::WebPki), + other => Err(ConfigError::Invalid { + name, + reason: format!("expected platform or webpki but got {other}"), + }), + } +} + fn parse_bool(name: &'static str, value: &str) -> Result { match value.to_lowercase().as_str() { "1" | "true" | "t" | "yes" | "y" | "on" => Ok(true), @@ -276,6 +317,8 @@ mod tests { oauth_scope: None, oauth_refresh_token: None, oauth_access_expires_at: None, + tls_roots: None, + tls_disable_verification: None, }; let cfg = AppConfig::from_env_values(env).expect("config"); @@ -294,6 +337,8 @@ mod tests { assert_eq!(cfg.llm.model, "gpt-oss:120b"); assert!(!cfg.llm.enabled); assert_eq!(cfg.llm.temperature, Some(0.4)); + assert_eq!(cfg.tls.roots, TlsRoots::Platform); + assert!(!cfg.tls.disable_verification); } #[test] @@ -313,6 +358,8 @@ mod tests { oauth_scope: None, oauth_refresh_token: None, oauth_access_expires_at: None, + tls_roots: None, + tls_disable_verification: None, }; let err = AppConfig::from_env_values(env).expect_err("missing token"); @@ -337,6 +384,8 @@ mod tests { oauth_scope: None, oauth_refresh_token: Some("refresh".into()), oauth_access_expires_at: Some("1234".into()), + tls_roots: Some("webpki".into()), + tls_disable_verification: Some("true".into()), }; let cfg = AppConfig::from_env_values(env).unwrap(); @@ -372,6 +421,8 @@ mod tests { cfg.gitlab.oauth.as_ref().unwrap().access_expires_at_epoch, Some(1234) ); + assert_eq!(cfg.tls.roots, TlsRoots::WebPki); + assert!(cfg.tls.disable_verification); } #[test] diff --git a/src/gitlab.rs b/src/gitlab.rs index 2d824d4..d9aee43 100644 --- a/src/gitlab.rs +++ b/src/gitlab.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; -use std::time::Duration; use thiserror::Error; use url::Url; +use crate::config::TlsPolicy; +use crate::http_client; use crate::model::{GitLabIssue, GitLabLabel}; #[derive(Clone)] @@ -12,6 +13,7 @@ pub struct GitLabClientConfig { pub timeout_ms: u64, pub max_pages: usize, pub max_items: usize, + pub tls: TlsPolicy, } impl std::fmt::Debug for GitLabClientConfig { @@ -22,6 +24,7 @@ impl std::fmt::Debug for GitLabClientConfig { .field("timeout_ms", &self.timeout_ms) .field("max_pages", &self.max_pages) .field("max_items", &self.max_items) + .field("tls", &self.tls) .finish() } } @@ -414,16 +417,14 @@ impl GitLabClient { } fn agent(&self) -> ureq::Agent { - ureq::Agent::config_builder() - .timeout_global(Some(Duration::from_millis(self.cfg.timeout_ms))) - .build() - .into() + http_client::agent(self.cfg.timeout_ms, &self.cfg.tls) } } #[cfg(test)] mod tests { use super::*; + use crate::config::{TlsPolicy, TlsRoots}; fn client() -> GitLabClient { GitLabClient::new(GitLabClientConfig { @@ -432,6 +433,10 @@ mod tests { timeout_ms: 30_000, max_pages: 50, max_items: 5000, + tls: TlsPolicy { + roots: TlsRoots::Platform, + disable_verification: false, + }, }) } @@ -455,6 +460,10 @@ mod tests { timeout_ms: 30_000, max_pages: 50, max_items: 5000, + tls: TlsPolicy { + roots: TlsRoots::Platform, + disable_verification: false, + }, }); let url = client.api_url("/projects/42/issues", &[]).expect("url"); diff --git a/src/http_client.rs b/src/http_client.rs new file mode 100644 index 0000000..f642fa4 --- /dev/null +++ b/src/http_client.rs @@ -0,0 +1,22 @@ +use std::time::Duration; + +use ureq::tls::{RootCerts, TlsConfig}; + +use crate::config::{TlsPolicy, TlsRoots}; + +pub fn agent(timeout_ms: u64, tls: &TlsPolicy) -> ureq::Agent { + let root_certs = match tls.roots { + TlsRoots::Platform => RootCerts::PlatformVerifier, + TlsRoots::WebPki => RootCerts::WebPki, + }; + let tls_config = TlsConfig::builder() + .root_certs(root_certs) + .disable_verification(tls.disable_verification) + .build(); + + ureq::Agent::config_builder() + .timeout_global(Some(Duration::from_millis(timeout_ms))) + .tls_config(tls_config) + .build() + .into() +} diff --git a/src/lib.rs b/src/lib.rs index a7c1618..e5186c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ use crate::sync::SourceIssueKey; pub mod config; pub mod gitlab; +pub mod http_client; pub mod jobs; pub mod labels; pub mod llm; diff --git a/src/llm.rs b/src/llm.rs index d9bc7ee..21b6f6c 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; -use std::time::Duration; use thiserror::Error; use url::Url; +use crate::config::TlsPolicy; +use crate::http_client; use crate::model::GeneratedStory; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -18,6 +19,7 @@ pub struct LlmClientConfig { pub model: String, pub temperature: Option, pub timeout_ms: u64, + pub tls: TlsPolicy, } impl std::fmt::Debug for LlmClientConfig { @@ -28,6 +30,7 @@ impl std::fmt::Debug for LlmClientConfig { .field("model", &self.model) .field("temperature", &self.temperature) .field("timeout_ms", &self.timeout_ms) + .field("tls", &self.tls) .finish() } } @@ -86,10 +89,7 @@ impl LlmClient { stream: true, temperature: self.cfg.temperature, }; - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(Duration::from_millis(self.cfg.timeout_ms))) - .build() - .into(); + let agent = http_client::agent(self.cfg.timeout_ms, &self.cfg.tls); let mut request = agent .post(endpoint.as_str()) .header("Accept", "text/event-stream") @@ -280,6 +280,7 @@ fn empty(value: &str) -> &str { #[cfg(test)] mod tests { use super::*; + use crate::config::{TlsPolicy, TlsRoots}; #[test] fn parses_sse_chat_chunks() { @@ -317,6 +318,10 @@ mod tests { model: "gpt-oss:120b".into(), temperature: None, timeout_ms: 30_000, + tls: TlsPolicy { + roots: TlsRoots::Platform, + disable_verification: false, + }, }); assert_eq!( diff --git a/src/main.rs b/src/main.rs index 7b61fee..d5aeb4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -290,6 +290,7 @@ fn maybe_start_job( timeout_ms: 30_000, max_pages: 50, max_items: 5000, + tls: config.tls.clone(), }); let llm = LlmClient::new(LlmClientConfig { base_url: config.llm.base_url.clone(), @@ -301,6 +302,7 @@ fn maybe_start_job( model: config.llm.model.clone(), temperature: config.llm.temperature, timeout_ms: 120_000, + tls: config.tls.clone(), }); let result = jobs::execute_job( JobContext { @@ -387,6 +389,7 @@ fn oauth_client_config(config: &AppConfig) -> Result .map_err(|error| format!("invalid LANE_OAUTH_REDIRECT_URI: {error}"))?, scope: oauth.scope.clone(), timeout_ms: 120_000, + tls: config.tls.clone(), }) } @@ -841,6 +844,8 @@ mod tests { oauth_scope: None, oauth_refresh_token: None, oauth_access_expires_at: None, + tls_roots: None, + tls_disable_verification: None, }) .expect("config"); let mut app = App::demo(); diff --git a/src/oauth.rs b/src/oauth.rs index 9c7ee67..0069d21 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -7,6 +7,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use thiserror::Error; use url::Url; +use crate::config::TlsPolicy; +use crate::http_client; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct OAuthClientConfig { pub instance_url: Url, @@ -15,6 +18,7 @@ pub struct OAuthClientConfig { pub redirect_uri: Url, pub scope: String, pub timeout_ms: u64, + pub tls: TlsPolicy, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -189,10 +193,7 @@ fn post_token_form( form.push(("client_secret".to_string(), secret.clone())); } let endpoint = oauth_endpoint(&config.instance_url, "oauth/token")?; - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(Duration::from_millis(config.timeout_ms))) - .build() - .into(); + let agent = http_client::agent(config.timeout_ms, &config.tls); let mut response = agent .post(endpoint.as_str()) .config() @@ -374,6 +375,7 @@ fn now_epoch() -> u64 { #[cfg(test)] mod tests { use super::*; + use crate::config::{TlsPolicy, TlsRoots}; fn config() -> OAuthClientConfig { OAuthClientConfig { @@ -383,6 +385,10 @@ mod tests { redirect_uri: Url::parse("http://127.0.0.1:8910/oauth/callback").unwrap(), scope: "api".into(), timeout_ms: 60_000, + tls: TlsPolicy { + roots: TlsRoots::Platform, + disable_verification: false, + }, } }