pub mod env;
pub mod linux;
pub mod macos;
pub mod windows;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use reqwest::Url;
use crate::core::error::{
OlError, ERR_EGRESS_TLS_FAILED, ERR_EGRESS_UNREACHABLE, ERR_PAC_UNAVAILABLE,
ERR_PROXY_AUTH_FAILED, ERR_PROXY_UNREACHABLE,
};
use super::config::{EgressConfig, ProxyMode, ProxySource};
use super::factory::{build_client, Consumer};
use super::{mask_userinfo, ProxyCandidate, ProxyResolver};
pub mod rung {
pub const ENV: &str = "env";
pub const WININET_USER: &str = "wininet-user";
pub const IE_PAC: &str = "ie-pac";
pub const WINHTTP_MACHINE: &str = "winhttp-machine";
pub const HKLM_INETSETTINGS: &str = "hklm-inetsettings";
pub const WPAD: &str = "wpad";
pub const SC_STATIC: &str = "sc-static";
pub const SC_SCOPED: &str = "sc-scoped";
pub const CFNET_PAC: &str = "cfnet-pac";
pub const GSETTINGS: &str = "gsettings";
pub const ALL: &[&str] = &[
ENV,
WININET_USER,
IE_PAC,
WINHTTP_MACHINE,
HKLM_INETSETTINGS,
WPAD,
SC_STATIC,
SC_SCOPED,
CFNET_PAC,
GSETTINGS,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Context {
UserSession,
DaemonService,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateOutcome {
Ok,
Failed(&'static str),
Skipped(&'static str),
NotConfigured,
}
impl CandidateOutcome {
pub fn as_str(&self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Failed(code) | Self::Skipped(code) => code,
Self::NotConfigured => "none",
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok)
}
}
impl serde::Serialize for CandidateOutcome {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CandidateAttempt {
pub source: ProxySource,
pub url_masked: String,
pub probe: CandidateOutcome,
pub latency_ms: u64,
#[serde(skip)]
pub rung: &'static str,
#[serde(skip)]
pub detail: Option<String>,
}
impl CandidateAttempt {
pub fn was_probed(&self) -> bool {
matches!(
self.probe,
CandidateOutcome::Ok | CandidateOutcome::Failed(_)
)
}
pub fn trace_line(&self) -> String {
let mut line = format!("{:<18} {}", self.rung, self.probe.as_str());
if !self.url_masked.is_empty() {
line.push_str(&format!(" {}", self.url_masked));
}
if self.was_probed() {
line.push_str(&format!(" [{} ms]", self.latency_ms));
}
if let Some(d) = &self.detail {
line.push_str(&format!(" — {d}"));
}
line
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Route {
Static(Url),
PacSource {
pac_url: Option<Url>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Discovered {
pub source: ProxySource,
pub route: Route,
}
pub trait CandidateProbe {
fn probe(&self, proxy: Option<&Url>) -> Result<u64, OlError>;
}
pub enum RungResult {
Candidate {
source: ProxySource,
route: Route,
probe_via: Option<Url>,
},
Skipped {
source: ProxySource,
code: &'static str,
detail: String,
},
NotConfigured {
source: ProxySource,
detail: Option<String>,
},
}
impl RungResult {
pub fn skipped(source: ProxySource, code: &'static str, detail: impl Into<String>) -> Self {
Self::Skipped {
source,
code,
detail: detail.into(),
}
}
pub fn empty(source: ProxySource) -> Self {
Self::NotConfigured {
source,
detail: None,
}
}
pub fn empty_with(source: ProxySource, detail: impl Into<String>) -> Self {
Self::NotConfigured {
source,
detail: Some(detail.into()),
}
}
pub fn static_route(source: ProxySource, url: Url) -> Self {
Self::Candidate {
source,
probe_via: Some(url.clone()),
route: Route::Static(url),
}
}
}
pub struct Ladder<'a> {
probe: &'a dyn CandidateProbe,
attempts: Vec<CandidateAttempt>,
winner: Option<Discovered>,
enumerate: bool,
}
impl<'a> Ladder<'a> {
pub fn new(probe: &'a dyn CandidateProbe) -> Self {
Self {
probe,
attempts: Vec::new(),
winner: None,
enumerate: false,
}
}
fn enumerating(probe: &'a dyn CandidateProbe) -> Self {
Self {
enumerate: true,
..Self::new(probe)
}
}
pub fn offer(&mut self, tag: &'static str, result: RungResult) -> bool {
let attempt = match result {
RungResult::NotConfigured { source, detail } => CandidateAttempt {
source,
url_masked: String::new(),
probe: CandidateOutcome::NotConfigured,
latency_ms: 0,
rung: tag,
detail,
},
RungResult::Skipped {
source,
code,
detail,
} => CandidateAttempt {
source,
url_masked: String::new(),
probe: CandidateOutcome::Skipped(code),
latency_ms: 0,
rung: tag,
detail: Some(detail),
},
RungResult::Candidate {
source,
route,
probe_via,
} => {
let detail = probe_detail(&route, probe_via.as_ref());
let started = Instant::now();
let (probe, latency_ms) = match self.probe.probe(probe_via.as_ref()) {
Ok(ms) => (CandidateOutcome::Ok, ms),
Err(e) => (CandidateOutcome::Failed(e.code), elapsed_ms(started)),
};
if probe.is_ok() && !self.enumerate {
self.winner = Some(Discovered {
source,
route: route.clone(),
});
}
CandidateAttempt {
source,
url_masked: mask_userinfo(&route_display(&route)),
probe,
latency_ms,
rung: tag,
detail,
}
}
};
self.attempts.push(attempt);
self.winner.is_some()
}
pub fn finish(self) -> (Option<Discovered>, Vec<CandidateAttempt>) {
(self.winner, self.attempts)
}
}
fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn route_display(route: &Route) -> String {
match route {
Route::Static(u) => authority_form(u),
Route::PacSource { pac_url: Some(u) } => format!("pac:{u}"),
Route::PacSource { pac_url: None } => "pac:wpad".to_string(),
}
}
fn probe_detail(route: &Route, probe_via: Option<&Url>) -> Option<String> {
match route {
Route::Static(_) => None,
Route::PacSource { .. } => Some(match probe_via {
Some(u) => format!("pac answered {} for the probe target", authority_form(u)),
None => "pac answered DIRECT for the probe target".to_string(),
}),
}
}
pub fn authority_form(u: &Url) -> String {
let host = u.host_str().unwrap_or_default();
match u.port_or_known_default() {
Some(p) => format!("{}://{host}:{p}", u.scheme()),
None => format!("{}://{host}", u.scheme()),
}
}
pub fn parse_discovered(raw: &str) -> Option<Url> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let with_scheme = if raw.contains("://") {
raw.to_string()
} else {
format!("http://{raw}")
};
let mut url = Url::parse(&with_scheme).ok()?;
if !matches!(url.scheme(), "http" | "https" | "socks5" | "socks5h") {
return None;
}
if url.host_str().is_none_or(str::is_empty) {
return None;
}
if url.scheme() == "socks5" {
url.set_scheme("socks5h").ok()?;
}
Some(url)
}
pub fn discover(
ctx: Context,
cfg: &EgressConfig,
target: &Url,
probe: &dyn CandidateProbe,
) -> (Option<Discovered>, Vec<CandidateAttempt>) {
if cfg.mode == ProxyMode::Direct {
return (None, Vec::new());
}
let mut ladder = Ladder::new(probe);
walk(&mut ladder, ctx, cfg, target);
ladder.finish()
}
fn walk(ladder: &mut Ladder, ctx: Context, cfg: &EgressConfig, target: &Url) {
if ladder.offer(rung::ENV, env::rung(cfg)) {
return;
}
os_rungs(ladder, ctx, target);
}
fn os_rungs(ladder: &mut Ladder, ctx: Context, target: &Url) {
#[cfg(windows)]
{
windows::walk(ladder, ctx, windows::native(), target);
}
#[cfg(target_os = "macos")]
{
macos::walk(ladder, ctx, &macos::native(), target);
}
#[cfg(target_os = "linux")]
{
linux::walk(ladder, ctx, &linux::native(), target);
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
{
let _ = (ladder, ctx, target);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PacAnswer {
pub proxies: Vec<String>,
}
impl PacAnswer {
pub fn first_route(&self) -> Option<Url> {
self.proxies.iter().find_map(|e| parse_pac_entry(e))
}
pub fn names_a_proxy(&self) -> bool {
self.proxies.iter().any(|e| !is_direct(e))
}
}
fn is_direct(entry: &str) -> bool {
pac_keyword(entry).0 == "DIRECT"
}
fn pac_keyword(entry: &str) -> (String, &str) {
let e = entry.trim();
match e.split_once(char::is_whitespace) {
Some((k, rest)) => (k.to_ascii_uppercase(), rest.trim()),
None => (e.to_ascii_uppercase(), ""),
}
}
fn parse_pac_entry(entry: &str) -> Option<Url> {
let (keyword, rest) = pac_keyword(entry);
match keyword.as_str() {
"DIRECT" => None,
"PROXY" | "HTTP" => parse_discovered(rest),
"HTTPS" => parse_discovered(&format!("https://{rest}")),
"SOCKS" | "SOCKS4" | "SOCKS5" => parse_discovered(&format!("socks5h://{rest}")),
_ => parse_discovered(entry),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacFacility {
WinHttp,
CfNetwork,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacBinding {
pub facility: PacFacility,
pub pac_url: Option<Url>,
}
pub fn native_pac_facility() -> Option<PacFacility> {
#[cfg(windows)]
{
Some(PacFacility::WinHttp)
}
#[cfg(target_os = "macos")]
{
Some(PacFacility::CfNetwork)
}
#[cfg(not(any(windows, target_os = "macos")))]
{
None
}
}
pub fn pac_binding(cfg: &EgressConfig) -> Option<PacBinding> {
if cfg.mode == ProxyMode::Direct {
return None;
}
if !matches!(cfg.source, Some(ProxySource::Pac) | Some(ProxySource::Wpad)) {
return None;
}
Some(PacBinding {
facility: native_pac_facility()?,
pac_url: cfg.pac_url.as_deref().and_then(|u| Url::parse(u).ok()),
})
}
const PAC_TTL: Duration = Duration::from_secs(60);
type PacCache = Mutex<HashMap<String, (Instant, Option<Url>)>>;
fn pac_cache() -> &'static PacCache {
static CACHE: OnceLock<PacCache> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn pac_route_for(target: &Url, binding: &PacBinding) -> Result<Option<Url>, OlError> {
pac_route_with(target, binding, &NativePac)
}
pub trait PacEvaluator {
fn evaluate(&self, target: &Url, binding: &PacBinding) -> Result<Option<Url>, OlError>;
}
pub struct NativePac;
impl PacEvaluator for NativePac {
fn evaluate(&self, target: &Url, binding: &PacBinding) -> Result<Option<Url>, OlError> {
eval_pac_native(target, binding)
}
}
pub fn pac_route_with(
target: &Url,
binding: &PacBinding,
eval: &dyn PacEvaluator,
) -> Result<Option<Url>, OlError> {
let key = target.host_str().unwrap_or_default().to_ascii_lowercase();
if let Ok(cache) = pac_cache().lock() {
if let Some((at, route)) = cache.get(&key) {
if at.elapsed() < PAC_TTL {
return Ok(route.clone());
}
}
}
let route = eval.evaluate(target, binding)?;
if let Ok(mut cache) = pac_cache().lock() {
cache.insert(key, (Instant::now(), route.clone()));
}
Ok(route)
}
pub fn clear_pac_cache() {
if let Ok(mut cache) = pac_cache().lock() {
cache.clear();
}
}
fn eval_pac_native(target: &Url, binding: &PacBinding) -> Result<Option<Url>, OlError> {
match binding.facility {
#[cfg(windows)]
PacFacility::WinHttp => windows::eval_pac(windows::native(), target, binding),
#[cfg(target_os = "macos")]
PacFacility::CfNetwork => macos::eval_pac(&macos::native(), target, binding),
other => Err(OlError::new(
ERR_PAC_UNAVAILABLE,
format!(
"no {other:?} PAC evaluator exists on this platform, so {} has no route",
target.host_str().unwrap_or("the destination")
),
)
.with_suggestion(
"Set an explicit proxy with `openlatch proxy set <url>`, or [proxy] url in \
config.toml.",
)),
}
}
pub struct HealthProbe {
base: EgressConfig,
health_url: String,
runtime: tokio::runtime::Runtime,
strict: bool,
}
impl HealthProbe {
pub fn new(api_url: &str, base: EgressConfig) -> Result<Self, OlError> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
OlError::new(
ERR_EGRESS_UNREACHABLE,
format!("could not start a runtime for the discovery probe: {e}"),
)
})?;
Ok(Self {
base,
health_url: format!("{}/api/v1/health", api_url.trim_end_matches('/')),
runtime,
strict: false,
})
}
pub fn strict(api_url: &str, base: EgressConfig) -> Result<Self, OlError> {
let mut probe = Self::new(api_url, base)?;
probe.strict = true;
Ok(probe)
}
}
impl CandidateProbe for HealthProbe {
fn probe(&self, proxy: Option<&Url>) -> Result<u64, OlError> {
let mut cfg = self.base.clone();
match proxy {
Some(u) => {
cfg.mode = ProxyMode::Auto;
cfg.url = Some(authority_form(u));
}
None => {
cfg.mode = ProxyMode::Direct;
cfg.url = None;
}
}
let client = build_client(Consumer::StatusProbe, &cfg)?;
let url = self.health_url.clone();
let started = Instant::now();
let result = self
.runtime
.block_on(async move { client.get(&url).send().await });
classify_probe(result, proxy.is_some(), elapsed_ms(started), self.strict)
}
}
fn classify_probe(
result: Result<reqwest::Response, reqwest::Error>,
via_proxy: bool,
ms: u64,
strict: bool,
) -> Result<u64, OlError> {
match result {
Ok(resp) => {
if resp.status() == reqwest::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
let offered = resp
.headers()
.get_all(reqwest::header::PROXY_AUTHENTICATE)
.iter()
.filter_map(|v| v.to_str().ok())
.collect::<Vec<_>>()
.join(", ");
if strict {
return Err(needs_credential(&offered));
}
return auth_required(&offered, ms);
}
if resp.status().is_success() {
return Ok(ms);
}
Err(OlError::new(
if via_proxy {
ERR_PROXY_UNREACHABLE
} else {
ERR_EGRESS_UNREACHABLE
},
format!(
"the platform health endpoint answered {} through this candidate",
resp.status().as_u16()
),
))
}
Err(e) => {
let chain = error_chain(&e);
if chain.contains("proxy authorization required") || chain.contains("407") {
if strict {
return Err(needs_credential("(unreadable through the tunnel error)"));
}
return Ok(ms);
}
Err(transport_error(&e, &chain, via_proxy))
}
}
}
fn needs_credential(offered: &str) -> OlError {
OlError::new(
ERR_PROXY_AUTH_FAILED,
format!("the proxy answered 407 and offers: {offered}"),
)
.with_suggestion(
"Provide the proxy credential — `openlatch proxy set <url>` prompts for it, and \
`OPENLATCH_PROXY` accepts it inline. It is stored in the OS credential store, \
never in config.toml.",
)
}
fn auth_required(offered: &str, ms: u64) -> Result<u64, OlError> {
let lower = offered.to_ascii_lowercase();
let viable =
lower.contains("basic") || lower.contains("negotiate") || lower.contains("kerberos");
if viable {
return Ok(ms);
}
Err(OlError::new(
ERR_PROXY_AUTH_FAILED,
format!("the proxy requires authentication and offers only: {offered}"),
)
.with_suggestion(
"This client speaks Basic and Negotiate (Kerberos). NTLM is not supported — ask \
for a Negotiate-capable path through the proxy.",
))
}
fn transport_error(e: &reqwest::Error, chain: &str, via_proxy: bool) -> OlError {
let unreachable = if via_proxy {
ERR_PROXY_UNREACHABLE
} else {
ERR_EGRESS_UNREACHABLE
};
if chain.contains("certificate") || chain.contains("tls") || chain.contains("handshake") {
return OlError::new(
ERR_EGRESS_TLS_FAILED,
format!("TLS to the platform failed through this candidate: {e}"),
)
.with_suggestion(
"An intercepting proxy re-signs traffic with its own CA. Point [proxy] \
ca_bundle at that root.",
);
}
OlError::new(
unreachable,
format!("the platform was not reachable through this candidate: {e}"),
)
}
fn error_chain(e: &(dyn std::error::Error + 'static)) -> String {
let mut out = e.to_string().to_ascii_lowercase();
let mut source = e.source();
while let Some(inner) = source {
out.push_str("; ");
out.push_str(&inner.to_string().to_ascii_lowercase());
source = inner.source();
}
out
}
struct RejectAll;
impl CandidateProbe for RejectAll {
fn probe(&self, _proxy: Option<&Url>) -> Result<u64, OlError> {
Err(OlError::new(
ERR_EGRESS_UNREACHABLE,
"enumeration only, not probed",
))
}
}
pub struct OsResolver {
ctx: Context,
cfg: EgressConfig,
}
impl OsResolver {
pub fn new(ctx: Context, cfg: EgressConfig) -> Self {
Self { ctx, cfg }
}
}
impl ProxyResolver for OsResolver {
fn candidates(&self, target: &str) -> Vec<ProxyCandidate> {
let Ok(url) = Url::parse(target) else {
return Vec::new();
};
let probe = RejectAll;
let mut ladder = Ladder::enumerating(&probe);
walk(&mut ladder, self.ctx, &self.cfg, &url);
let (_, attempts) = ladder.finish();
attempts
.into_iter()
.filter(|a| a.was_probed())
.map(|a| ProxyCandidate {
url: a.url_masked,
source: a.source,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
pub(crate) struct ScriptedProbe {
outcomes: RefCell<Vec<Result<u64, OlError>>>,
pub seen: RefCell<Vec<Option<String>>>,
}
impl ScriptedProbe {
pub(crate) fn new(outcomes: Vec<Result<u64, OlError>>) -> Self {
Self {
outcomes: RefCell::new(outcomes),
seen: RefCell::new(Vec::new()),
}
}
pub(crate) fn always_fails() -> Self {
Self::new(Vec::new())
}
}
impl CandidateProbe for ScriptedProbe {
fn probe(&self, proxy: Option<&Url>) -> Result<u64, OlError> {
self.seen.borrow_mut().push(proxy.map(authority_form));
let mut outcomes = self.outcomes.borrow_mut();
if outcomes.is_empty() {
return Err(OlError::new(ERR_PROXY_UNREACHABLE, "scripted failure"));
}
outcomes.remove(0)
}
}
fn cfg_with_env_proxy(url: &str) -> EgressConfig {
let mut cfg = EgressConfig::direct();
cfg.mode = ProxyMode::Auto;
cfg.url = Some(url.to_string());
cfg
}
fn target() -> Url {
Url::parse("https://app.openlatch.ai").expect("target url")
}
#[test]
fn every_os_backend_satisfies_its_seam_on_every_host() {
let probe = ScriptedProbe::always_fails();
let t = target();
let mut ladder = Ladder::new(&probe);
macos::walk(&mut ladder, Context::UserSession, &macos::native(), &t);
let (won, trace) = ladder.finish();
assert!(won.is_none());
assert!(!trace.is_empty(), "the macOS ladder must leave a trace");
let mut ladder = Ladder::new(&probe);
linux::walk(&mut ladder, Context::UserSession, &linux::native(), &t);
let (won, trace) = ladder.finish();
assert!(won.is_none());
assert_eq!(trace.len(), 1, "the Linux ladder is one rung");
#[cfg(windows)]
{
let _: &dyn windows::WinSource = windows::native();
}
}
#[test]
fn every_rung_tag_is_in_the_vocabulary() {
let mut seen = std::collections::HashSet::new();
for tag in rung::ALL {
assert!(seen.insert(*tag), "duplicate rung tag: {tag}");
assert!(!tag.is_empty());
}
assert_eq!(rung::ALL.len(), 10);
}
#[test]
fn the_env_rung_reuses_the_resolved_config_and_wins_first() {
let cfg = cfg_with_env_proxy("http://ambient.corp:3128");
let probe = ScriptedProbe::new(vec![Ok(12)]);
let (won, trace) = discover(Context::UserSession, &cfg, &target(), &probe);
let won = won.expect("the env rung must win when its probe passes");
assert_eq!(won.source, ProxySource::Env);
assert_eq!(
won.route,
Route::Static(Url::parse("http://ambient.corp:3128").expect("url"))
);
assert_eq!(trace.len(), 1, "no rung below the winner may be evaluated");
assert_eq!(trace[0].rung, rung::ENV);
assert_eq!(trace[0].probe, CandidateOutcome::Ok);
assert_eq!(trace[0].latency_ms, 12);
assert_eq!(trace[0].url_masked, "http://ambient.corp:3128");
}
#[test]
fn a_discovered_bare_socks5_becomes_socks5h() {
let cfg = cfg_with_env_proxy("socks5://socks.corp:1080");
let probe = ScriptedProbe::new(vec![Ok(3)]);
let (won, _) = discover(Context::UserSession, &cfg, &target(), &probe);
let Some(Discovered {
route: Route::Static(u),
..
}) = won
else {
panic!("expected a static socks candidate");
};
assert_eq!(u.scheme(), "socks5h");
}
#[test]
fn a_failed_rung_is_still_recorded() {
let cfg = cfg_with_env_proxy("http://ambient.corp:3128");
let probe = ScriptedProbe::always_fails();
let (won, trace) = discover(Context::UserSession, &cfg, &target(), &probe);
assert!(won.is_none());
let env_entry = trace
.iter()
.find(|a| a.rung == rung::ENV)
.expect("the env rung must appear in the trace even when it loses");
assert_eq!(
env_entry.probe,
CandidateOutcome::Failed(ERR_PROXY_UNREACHABLE)
);
assert!(env_entry.was_probed());
}
#[test]
fn direct_mode_never_walks_the_ladder() {
let cfg = EgressConfig::direct();
let probe = ScriptedProbe::new(vec![Ok(1)]);
let (won, trace) = discover(Context::UserSession, &cfg, &target(), &probe);
assert!(won.is_none());
assert!(trace.is_empty(), "direct mode must cost no probe at all");
assert!(probe.seen.borrow().is_empty());
}
#[test]
fn a_pac_win_carries_no_url_to_persist() {
let route = Route::PacSource {
pac_url: Url::parse("http://wpad.corp/proxy.pac").ok(),
};
let won = Discovered {
source: ProxySource::Pac,
route,
};
match won.route {
Route::Static(_) => panic!("a PAC win must never be a static route"),
Route::PacSource { pac_url } => {
assert_eq!(
pac_url.map(|u| u.to_string()).as_deref(),
Some("http://wpad.corp/proxy.pac")
);
}
}
}
#[test]
fn the_trace_serialises_as_the_frozen_candidate_object() {
let attempt = CandidateAttempt {
source: ProxySource::Env,
url_masked: "http://alice:*****@proxy.corp:8080".to_string(),
probe: CandidateOutcome::Failed(ERR_PROXY_UNREACHABLE),
latency_ms: 41,
rung: rung::ENV,
detail: Some("not on the wire".to_string()),
};
let json = serde_json::to_value(&attempt).expect("serialize");
let obj = json.as_object().expect("object");
let mut keys: Vec<_> = obj.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(keys, ["latency_ms", "probe", "source", "url_masked"]);
assert_eq!(obj["probe"], serde_json::json!("OL-1221"));
assert_eq!(obj["source"], serde_json::json!("env"));
}
#[test]
fn a_skipped_rung_is_not_a_candidate() {
let attempt = CandidateAttempt {
source: ProxySource::Wpad,
url_masked: String::new(),
probe: CandidateOutcome::Skipped(ERR_PAC_UNAVAILABLE),
latency_ms: 0,
rung: rung::WPAD,
detail: Some("DisableWpad = 1".to_string()),
};
assert!(!attempt.was_probed());
assert!(attempt.trace_line().contains("DisableWpad = 1"));
assert!(
!attempt.trace_line().contains("ms]"),
"a rung that never ran must not claim a latency"
);
}
#[test]
fn parse_discovered_accepts_the_os_forms_and_refuses_the_rest() {
assert_eq!(
parse_discovered("proxy.corp:8080").map(|u| authority_form(&u)),
Some("http://proxy.corp:8080".to_string())
);
assert_eq!(
parse_discovered(" https://secure.corp:443 ").map(|u| authority_form(&u)),
Some("https://secure.corp:443".to_string())
);
assert!(parse_discovered("").is_none());
assert!(parse_discovered("ftp://proxy.corp:21").is_none());
assert!(parse_discovered("http://").is_none());
}
#[test]
fn a_pac_answer_is_read_in_the_scripts_own_grammar() {
let answer = PacAnswer {
proxies: vec!["PROXY pac.corp:3128".into(), "DIRECT".into()],
};
assert_eq!(
answer.first_route().map(|u| authority_form(&u)).as_deref(),
Some("http://pac.corp:3128")
);
assert!(answer.names_a_proxy());
let direct = PacAnswer {
proxies: vec!["DIRECT".into()],
};
assert!(direct.first_route().is_none());
assert!(!direct.names_a_proxy());
assert!(!PacAnswer::default().names_a_proxy());
let bare = PacAnswer {
proxies: vec!["winhttp.corp:8080".into()],
};
assert_eq!(
bare.first_route().map(|u| authority_form(&u)).as_deref(),
Some("http://winhttp.corp:8080")
);
let socks = PacAnswer {
proxies: vec!["SOCKS5 socks.corp:1080".into()],
};
assert_eq!(
socks
.first_route()
.map(|u| u.scheme().to_string())
.as_deref(),
Some("socks5h")
);
let tls = PacAnswer {
proxies: vec!["HTTPS secure.corp:443".into()],
};
assert_eq!(
tls.first_route().map(|u| u.scheme().to_string()).as_deref(),
Some("https")
);
}
#[test]
fn a_pac_binding_only_exists_for_a_pac_source() {
let mut cfg = EgressConfig::direct();
cfg.mode = ProxyMode::Auto;
assert!(
pac_binding(&cfg).is_none(),
"no source means no PAC binding"
);
cfg.source = Some(ProxySource::Windows);
assert!(pac_binding(&cfg).is_none(), "a static source is not a PAC");
cfg.source = Some(ProxySource::Pac);
cfg.pac_url = Some("http://wpad.corp/proxy.pac".to_string());
let binding = pac_binding(&cfg);
if native_pac_facility().is_some() {
let binding = binding.expect("a PAC source binds on a PAC-capable OS");
assert_eq!(
binding.pac_url.map(|u| u.to_string()).as_deref(),
Some("http://wpad.corp/proxy.pac")
);
} else {
assert!(binding.is_none());
}
cfg.mode = ProxyMode::Direct;
assert!(pac_binding(&cfg).is_none(), "direct outranks a PAC source");
}
#[test]
fn a_pac_answer_is_reused_within_the_ttl() {
clear_pac_cache();
let binding = PacBinding {
facility: PacFacility::CfNetwork,
pac_url: None,
};
let t = Url::parse("https://cached.example/x").expect("url");
if cfg!(target_os = "macos") {
return;
}
assert!(pac_route_for(&t, &binding).is_err());
assert!(pac_route_for(&t, &binding).is_err());
clear_pac_cache();
}
#[test]
fn the_resolver_seam_enumerates_instead_of_selecting() {
let cfg = cfg_with_env_proxy("http://ambient.corp:3128");
let resolver = OsResolver::new(Context::UserSession, cfg);
let candidates = resolver.candidates("https://app.openlatch.ai");
assert!(
candidates
.iter()
.any(|c| c.source == ProxySource::Env && c.url == "http://ambient.corp:3128"),
"the env candidate must be enumerated: {candidates:?}"
);
}
#[test]
fn classify_treats_an_authenticating_proxy_as_a_pass() {
assert_eq!(auth_required("Basic realm=\"corp\"", 7).ok(), Some(7));
assert_eq!(auth_required("Negotiate", 9).ok(), Some(9));
let ntlm = auth_required("NTLM", 3).expect_err("NTLM alone is not viable");
assert_eq!(ntlm.code, ERR_PROXY_AUTH_FAILED);
}
}