use std::collections::BTreeMap;
use reqwest::Url;
use crate::core::egress::config::ProxySource;
use crate::core::error::{OlError, ERR_PAC_UNAVAILABLE};
use super::{rung, Context, Ladder, PacAnswer, PacBinding, Route, RungResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostPort {
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProxyDict {
pub https: Option<HostPort>,
pub http: Option<HostPort>,
pub socks: Option<HostPort>,
pub pac_enabled: bool,
pub pac_url: Option<String>,
pub auto_discovery: bool,
pub scoped: BTreeMap<String, ProxyDict>,
}
impl ProxyDict {
fn is_configured(&self) -> bool {
self.https.is_some()
|| self.http.is_some()
|| self.socks.is_some()
|| (self.pac_enabled && self.pac_url.is_some())
|| self.auto_discovery
}
fn static_route(&self) -> Option<Url> {
let (hp, scheme) = if let Some(hp) = &self.https {
(hp, "http")
} else if let Some(hp) = &self.http {
(hp, "http")
} else if let Some(hp) = &self.socks {
(hp, "socks5h")
} else {
return None;
};
Url::parse(&format!("{scheme}://{}:{}", hp.host, hp.port)).ok()
}
}
pub trait MacSource {
fn proxies(&self) -> Option<ProxyDict>;
fn eval_pac(&self, pac: Option<&Url>, target: &Url) -> Result<PacAnswer, OlError>;
}
pub fn walk(ladder: &mut Ladder, ctx: Context, src: &dyn MacSource, target: &Url) {
let _ = ctx;
let global = src.proxies().unwrap_or_default();
let scope = active_scope(&global);
if ladder.offer(rung::SC_SCOPED, scoped_rung(scope)) {
return;
}
if ladder.offer(rung::SC_STATIC, static_rung(&global, scope.is_some())) {
return;
}
let effective = scope.map_or(&global, |(_, d)| d);
ladder.offer(rung::CFNET_PAC, pac_rung(src, effective, target));
}
fn active_scope(global: &ProxyDict) -> Option<(&str, &ProxyDict)> {
let usable = || {
global
.scoped
.iter()
.filter(|(_, d)| d.is_configured())
.map(|(k, d)| (k.as_str(), d))
};
usable()
.find(|(iface, _)| iface.starts_with("utun") || iface.starts_with("ppp"))
.or_else(|| usable().next())
}
fn scoped_rung(scope: Option<(&str, &ProxyDict)>) -> RungResult {
let Some((iface, dict)) = scope else {
return RungResult::empty(ProxySource::Macos);
};
match dict.static_route() {
Some(url) => RungResult::static_route(ProxySource::Macos, url),
None => RungResult::empty_with(
ProxySource::Macos,
format!("the __SCOPED__ entry for {iface} names no static proxy"),
),
}
}
fn static_rung(global: &ProxyDict, superseded: bool) -> RungResult {
if superseded {
return RungResult::empty_with(
ProxySource::Macos,
"the global dictionary is superseded wholesale by a __SCOPED__ entry",
);
}
match global.static_route() {
Some(url) => RungResult::static_route(ProxySource::Macos, url),
None => RungResult::empty(ProxySource::Macos),
}
}
fn pac_rung(src: &dyn MacSource, dict: &ProxyDict, target: &Url) -> RungResult {
let (source, pac_url) = if dict.pac_enabled {
match dict.pac_url.as_deref().and_then(|u| Url::parse(u).ok()) {
Some(u) => (ProxySource::Pac, Some(u)),
None => {
return RungResult::skipped(
ProxySource::Pac,
crate::core::error::ERR_PROXY_CONFIG_INVALID,
"ProxyAutoConfigEnable is on but ProxyAutoConfigURLString is not a URL",
)
}
}
} else if dict.auto_discovery {
(ProxySource::Wpad, None)
} else {
return RungResult::empty_with(
ProxySource::Pac,
"neither ProxyAutoConfigEnable nor ProxyAutoDiscoveryEnable is set",
);
};
match src.eval_pac(pac_url.as_ref(), target) {
Ok(answer) => {
let probe_via = answer.first_route();
if probe_via.is_none() && answer.names_a_proxy() {
return RungResult::skipped(
source,
crate::core::error::ERR_PROXY_CONFIG_INVALID,
format!("the PAC named no usable proxy: {:?}", answer.proxies),
);
}
RungResult::Candidate {
source,
route: Route::PacSource { pac_url },
probe_via,
}
}
Err(e) => RungResult::skipped(source, e.code, e.message),
}
}
pub fn eval_pac(
src: &dyn MacSource,
target: &Url,
binding: &PacBinding,
) -> Result<Option<Url>, OlError> {
let answer = src.eval_pac(binding.pac_url.as_ref(), target)?;
Ok(answer.first_route())
}
pub fn native() -> UnreadSystemConfiguration {
UnreadSystemConfiguration
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UnreadSystemConfiguration;
impl MacSource for UnreadSystemConfiguration {
fn proxies(&self) -> Option<ProxyDict> {
None
}
fn eval_pac(&self, _pac: Option<&Url>, _target: &Url) -> Result<PacAnswer, OlError> {
Err(OlError::new(
ERR_PAC_UNAVAILABLE,
"this build has no CFNetwork PAC evaluator",
)
.with_suggestion(
"Set an explicit proxy with `openlatch proxy set <url>`, or [proxy] url in \
config.toml.",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::egress::discovery::tests::ScriptedProbe;
use crate::core::egress::discovery::{CandidateAttempt, CandidateOutcome};
use std::cell::RefCell;
#[derive(Default)]
struct FakeMac {
dict: Option<ProxyDict>,
answer: Option<PacAnswer>,
error: Option<&'static str>,
calls: RefCell<Vec<Option<String>>>,
}
impl MacSource for FakeMac {
fn proxies(&self) -> Option<ProxyDict> {
self.dict.clone()
}
fn eval_pac(&self, pac: Option<&Url>, _target: &Url) -> Result<PacAnswer, OlError> {
self.calls.borrow_mut().push(pac.map(Url::to_string));
match self.error {
Some(code) => Err(OlError::new(code, "fixture PAC failure")),
None => Ok(self.answer.clone().unwrap_or_default()),
}
}
}
fn hp(host: &str, port: u16) -> Option<HostPort> {
Some(HostPort {
host: host.to_string(),
port,
})
}
fn target() -> Url {
Url::parse("https://app.openlatch.ai/api/v1/health").expect("target")
}
fn walk_with(src: &FakeMac, probe: &ScriptedProbe) -> Vec<CandidateAttempt> {
let mut ladder = Ladder::new(probe);
walk(&mut ladder, Context::UserSession, src, &target());
ladder.finish().1
}
#[test]
fn the_global_static_rung_wins_when_nothing_is_scoped() {
let src = FakeMac {
dict: Some(ProxyDict {
https: hp("global.corp", 8080),
..Default::default()
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(6)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, trace) = ladder.finish();
let won = won.expect("static win");
assert_eq!(won.source, ProxySource::Macos);
assert_eq!(
won.route,
Route::Static(Url::parse("http://global.corp:8080").expect("url"))
);
assert_eq!(trace[0].rung, rung::SC_SCOPED);
assert_eq!(trace[0].probe, CandidateOutcome::NotConfigured);
}
#[test]
fn a_scoped_entry_replaces_the_global_dictionary_wholesale() {
let mut scoped = BTreeMap::new();
scoped.insert(
"utun3".to_string(),
ProxyDict {
https: hp("vpn.corp", 3128),
..Default::default()
},
);
let src = FakeMac {
dict: Some(ProxyDict {
https: hp("global.corp", 8080),
http: hp("global-http.corp", 80),
pac_enabled: true,
pac_url: Some("http://global.corp/proxy.pac".into()),
scoped,
..Default::default()
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(4)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, trace) = ladder.finish();
let won = won.expect("the scoped entry wins");
assert_eq!(
won.route,
Route::Static(Url::parse("http://vpn.corp:3128").expect("url"))
);
let rendered = format!("{trace:?}");
assert!(
!rendered.contains("global"),
"a field from the superseded global dictionary leaked: {rendered}"
);
}
#[test]
fn a_superseded_global_rung_says_so() {
let mut scoped = BTreeMap::new();
scoped.insert(
"utun3".to_string(),
ProxyDict {
pac_enabled: true,
pac_url: Some("http://vpn.corp/proxy.pac".into()),
..Default::default()
},
);
let src = FakeMac {
dict: Some(ProxyDict {
https: hp("global.corp", 8080),
scoped,
..Default::default()
}),
answer: Some(PacAnswer {
proxies: vec!["PROXY vpnpac.corp:3128".into()],
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(3)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, trace) = ladder.finish();
let global_row = trace
.iter()
.find(|a| a.rung == rung::SC_STATIC)
.expect("static row");
assert_eq!(global_row.probe, CandidateOutcome::NotConfigured);
assert!(global_row
.detail
.as_deref()
.is_some_and(|d| d.contains("superseded")));
assert_eq!(
src.calls.borrow().as_slice(),
&[Some("http://vpn.corp/proxy.pac".to_string())]
);
assert_eq!(won.expect("pac win").source, ProxySource::Pac);
}
#[test]
fn wpad_runs_only_when_auto_discovery_is_enabled() {
let src = FakeMac {
dict: Some(ProxyDict::default()),
..Default::default()
};
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, &probe);
let pac = trace
.iter()
.find(|a| a.rung == rung::CFNET_PAC)
.expect("pac row");
assert_eq!(pac.probe, CandidateOutcome::NotConfigured);
assert!(
src.calls.borrow().is_empty(),
"auto-discovery is off; nothing may touch the network"
);
let src = FakeMac {
dict: Some(ProxyDict {
auto_discovery: true,
..Default::default()
}),
answer: Some(PacAnswer {
proxies: vec!["PROXY wpad.corp:3128".into()],
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(8)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let won = ladder.finish().0.expect("wpad win");
assert_eq!(won.source, ProxySource::Wpad);
assert_eq!(won.route, Route::PacSource { pac_url: None });
}
#[test]
fn a_pac_win_persists_the_script_never_its_answer() {
let src = FakeMac {
dict: Some(ProxyDict {
pac_enabled: true,
pac_url: Some("http://corp/proxy.pac".into()),
..Default::default()
}),
answer: Some(PacAnswer {
proxies: vec!["PROXY chosen.corp:3128".into(), "DIRECT".into()],
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(9)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, trace) = ladder.finish();
let won = won.expect("pac win");
assert!(matches!(won.route, Route::PacSource { .. }));
let row = trace
.iter()
.find(|a| a.rung == rung::CFNET_PAC)
.expect("pac row");
assert_eq!(row.url_masked, "pac:http://corp/proxy.pac");
assert!(
!row.url_masked.contains("chosen.corp"),
"the PAC's answer must never be what the trace persists"
);
}
#[test]
fn socks_is_normalised_to_socks5h() {
let dict = ProxyDict {
socks: hp("socks.corp", 1080),
..Default::default()
};
assert_eq!(
dict.static_route().map(|u| u.to_string()),
Some("socks5h://socks.corp:1080".to_string())
);
}
#[test]
fn the_unread_backend_finds_nothing_and_says_why() {
let src = native();
assert!(src.proxies().is_none());
let err = src
.eval_pac(None, &target())
.expect_err("there is no evaluator to succeed");
assert_eq!(err.code, ERR_PAC_UNAVAILABLE);
assert!(err.suggestion.is_some(), "a refusal owes a remedy");
}
}