use crate::error::Error;
use crate::resolve::ProxyStep;
#[derive(Clone, PartialEq, Eq)]
struct Entry {
kind: EntryKind,
host: Option<String>,
port: Option<i64>,
pac_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntryKind {
None,
Http,
Https,
Socks,
AutoConfigUrl,
Other,
}
fn entries_to_steps(entries: &[Entry]) -> Result<Vec<ProxyStep>, Error> {
let mut candidates = Vec::with_capacity(entries.len());
for entry in entries {
let keyword = match entry.kind {
EntryKind::None => {
candidates.push("DIRECT".to_owned());
continue;
}
EntryKind::Http => "PROXY",
EntryKind::Https => "HTTPS",
EntryKind::Socks => "SOCKS",
EntryKind::AutoConfigUrl | EntryKind::Other => continue,
};
let Some(host) = entry.host.as_deref().map(str::trim) else {
continue;
};
if host.is_empty()
|| host.contains(|c: char| c == ';' || c == '[' || c == ']' || c.is_whitespace())
{
continue;
}
let host = if host.contains(':') {
format!("[{host}]")
} else {
host.to_owned()
};
candidates.push(match entry.port {
Some(port @ 1..=65535) => format!("{keyword} {host}:{port}"),
_ => format!("{keyword} {host}"),
});
}
if candidates.is_empty() {
let seen: Vec<String> = entries
.iter()
.map(|entry| format!("{:?} {:?}:{:?}", entry.kind, entry.host, entry.port))
.collect();
return Err(Error::pac_invalid_result(format!(
"CFNetwork returned {} proxy entries, none of them usable: [{}]",
entries.len(),
seen.join(", ")
)));
}
super::parse_find_proxy_result(&candidates.join("; "))
}
fn walk_system_entries(
entries: &[Entry],
mut run: impl FnMut(&str) -> Result<Option<Vec<ProxyStep>>, Error>,
) -> Result<Vec<ProxyStep>, Error> {
if entries.is_empty() {
return entries_to_steps(entries);
}
for (i, entry) in entries.iter().enumerate() {
match (entry.kind, entry.pac_url.as_deref()) {
(EntryKind::AutoConfigUrl, Some(pac_url)) => {
if let Some(steps) = run(pac_url)? {
return Ok(steps);
}
}
_ => return entries_to_steps(&entries[i..]),
}
}
Ok(vec![ProxyStep::Direct])
}
fn is_wpad_miss(pac_url: &str, domain: &str, code: isize) -> bool {
domain == "NSURLErrorDomain"
&& code == -1003
&& url::Url::parse(pac_url).is_ok_and(|url| url.host_str() == Some("wpad"))
}
#[cfg(pac_cfnetwork)]
pub use self::native::{
CfNetworkPacEvaluator, CfNetworkPacResolver, DEFAULT_CFNETWORK_PAC_TIMEOUT,
};
#[cfg(pac_cfnetwork)]
mod native {
use std::ffi::c_void;
use std::io;
use std::ptr;
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
use core_foundation::array::{CFArray, CFArrayRef};
use core_foundation::base::{CFIndex, CFType, TCFType, kCFAllocatorDefault};
use core_foundation::dictionary::{CFDictionary, CFDictionaryRef};
use core_foundation::error::{CFError, CFErrorRef};
use core_foundation::number::CFNumber;
use core_foundation::runloop::{
CFRunLoop, CFRunLoopSource, CFRunLoopSourceInvalidate, CFRunLoopSourceRef,
};
use core_foundation::string::{CFString, CFStringRef, kCFStringEncodingUTF8};
use core_foundation::url::{CFURL, CFURLCreateWithBytes, CFURLRef};
use url::Url;
use super::{Entry, EntryKind, entries_to_steps, is_wpad_miss, walk_system_entries};
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::error::Error;
use crate::mode::ProxyMode;
use crate::pac::{PacEvaluator, PacScript};
use crate::resolve::ProxyStep;
pub const DEFAULT_CFNETWORK_PAC_TIMEOUT: Duration = Duration::from_secs(5);
const RUN_LOOP_MODE: &str = "proxy-watch.cfnetwork-pac";
#[repr(C)]
struct ClientContext {
version: CFIndex,
info: *mut c_void,
retain: Option<unsafe extern "C" fn(*const c_void) -> *const c_void>,
release: Option<unsafe extern "C" fn(*const c_void)>,
copy_description: Option<unsafe extern "C" fn(*const c_void) -> CFStringRef>,
}
type ResultCallback = unsafe extern "C" fn(*mut c_void, CFArrayRef, CFErrorRef);
#[link(name = "CFNetwork", kind = "framework")]
unsafe extern "C" {
static kCFProxyTypeKey: CFStringRef;
static kCFProxyHostNameKey: CFStringRef;
static kCFProxyPortNumberKey: CFStringRef;
static kCFProxyTypeNone: CFStringRef;
static kCFProxyTypeHTTP: CFStringRef;
static kCFProxyTypeHTTPS: CFStringRef;
static kCFProxyTypeSOCKS: CFStringRef;
static kCFProxyTypeAutoConfigurationURL: CFStringRef;
static kCFProxyAutoConfigurationURLKey: CFStringRef;
fn CFNetworkCopySystemProxySettings() -> CFDictionaryRef;
fn CFNetworkCopyProxiesForURL(url: CFURLRef, settings: CFDictionaryRef) -> CFArrayRef;
fn CFNetworkExecuteProxyAutoConfigurationScript(
script: CFStringRef,
target: CFURLRef,
callback: ResultCallback,
context: *mut ClientContext,
) -> CFRunLoopSourceRef;
fn CFNetworkExecuteProxyAutoConfigurationURL(
pac_url: CFURLRef,
target: CFURLRef,
callback: ResultCallback,
context: *mut ClientContext,
) -> CFRunLoopSourceRef;
}
#[cfg(target_os = "macos")]
const SYSTEM_SOURCE: ProxyConfigSource = ProxyConfigSource::SystemConfigurationState;
#[cfg(target_os = "ios")]
const SYSTEM_SOURCE: ProxyConfigSource = ProxyConfigSource::CfNetworkSystemSettings;
static LOCK: Mutex<()> = Mutex::new(());
fn text(value: &CFString) -> Option<String> {
std::panic::catch_unwind(|| value.to_string()).ok()
}
type Answer = Result<Option<CFArray<CFType>>, CFError>;
#[derive(Debug, Clone)]
pub struct CfNetworkPacEvaluator {
timeout: Duration,
}
impl CfNetworkPacEvaluator {
#[must_use]
pub fn new() -> Self {
Self {
timeout: DEFAULT_CFNETWORK_PAC_TIMEOUT,
}
}
pub fn with_timeout(timeout: Duration) -> Result<Self, Error> {
check_timeout(timeout)?;
Ok(Self { timeout })
}
#[must_use]
pub fn timeout(&self) -> Duration {
self.timeout
}
}
impl Default for CfNetworkPacEvaluator {
fn default() -> Self {
Self::new()
}
}
impl PacEvaluator for CfNetworkPacEvaluator {
fn evaluate(
&self,
script: &PacScript,
url: &Url,
_host: &str,
) -> Result<Vec<ProxyStep>, Error> {
evaluate_script(script.source(), url, self.timeout)
}
}
#[derive(Debug, Clone)]
pub struct CfNetworkPacResolver {
timeout: Duration,
wpad: bool,
}
impl CfNetworkPacResolver {
#[must_use]
pub fn new() -> Self {
Self {
timeout: DEFAULT_CFNETWORK_PAC_TIMEOUT,
wpad: false,
}
}
pub fn with_timeout(timeout: Duration) -> Result<Self, Error> {
check_timeout(timeout)?;
Ok(Self {
timeout,
wpad: false,
})
}
#[must_use]
pub fn timeout(&self) -> Duration {
self.timeout
}
#[must_use]
pub fn with_wpad(mut self, wpad: bool) -> Self {
self.wpad = wpad;
self
}
#[must_use]
pub fn wpad(&self) -> bool {
self.wpad
}
pub fn resolve(&self, url: &Url, pac_url: &Url) -> Result<Vec<ProxyStep>, Error> {
let deadline = Instant::now().checked_add(self.timeout);
let pac = cf_url(pac_url.as_str())?;
let target = cf_url(crate::pac::sanitize_url(url).as_str())?;
match self.execute_url(deadline, &pac, &target)? {
Ok(array) => array_to_steps(array.as_ref()),
Err(error) => Err(url_error(&error)),
}
}
fn execute_url(
&self,
deadline: Option<Instant>,
pac: &CFURL,
target: &CFURL,
) -> Result<Answer, Error> {
pump(self.timeout, deadline, target, |target, context| {
unsafe {
CFNetworkExecuteProxyAutoConfigurationURL(
pac.as_concrete_TypeRef(),
target,
on_result,
context,
)
}
})
}
fn resolve_wpad(&self, url: &Url) -> Result<Vec<ProxyStep>, Error> {
let deadline = Instant::now().checked_add(self.timeout);
let target = cf_url(crate::pac::sanitize_url(url).as_str())?;
let raw = unsafe { CFNetworkCopySystemProxySettings() };
if raw.is_null() {
return Err(Error::io(
"CFNetwork resolving WPAD",
io::Error::other("CFNetworkCopySystemProxySettings returned NULL"),
));
}
let settings: CFDictionary = unsafe { CFDictionary::wrap_under_create_rule(raw) };
self.resolve_with_settings(deadline, &target, &settings)
}
fn resolve_with_settings(
&self,
deadline: Option<Instant>,
target: &CFURL,
settings: &CFDictionary,
) -> Result<Vec<ProxyStep>, Error> {
if !runs_a_script(settings) {
return Err(Error::io(
"CFNetwork resolving WPAD",
io::Error::other("the system proxy settings no longer name a PAC script"),
));
}
let raw = unsafe {
CFNetworkCopyProxiesForURL(
target.as_concrete_TypeRef(),
settings.as_concrete_TypeRef(),
)
};
let array =
(!raw.is_null()).then(|| unsafe { CFArray::<CFType>::wrap_under_create_rule(raw) });
walk_system_entries(&read_entries(array.as_ref()), |pac_url| {
let pac = cf_url(pac_url)?;
match self.execute_url(deadline, &pac, target)? {
Ok(array) => array_to_steps(array.as_ref()).map(Some),
Err(error)
if is_wpad_miss(
pac_url,
&text(&error.domain()).unwrap_or_default(),
error.code(),
) =>
{
Ok(None)
}
Err(error) => Err(url_error(&error)),
}
})
}
pub fn resolve_config(
&self,
config: &ProxyConfig,
url: &Url,
) -> Result<Vec<ProxyStep>, Error> {
let mode = &config.effective;
let has_host = crate::endpoint::has_request_host(url);
match mode {
ProxyMode::Direct | ProxyMode::Manual { .. } => {
crate::resolve::resolve(config, url)
}
ProxyMode::Pac { .. } | ProxyMode::PacInline { .. } | ProxyMode::WpadAutoDetect
if !has_host =>
{
Ok(vec![ProxyStep::Direct])
}
ProxyMode::WpadAutoDetect
if self.wpad && config.source(SYSTEM_SOURCE) == Some(mode) =>
{
self.resolve_wpad(url)
}
ProxyMode::WpadAutoDetect => Err(Error::PacNotSupported { mode: "wpad" }),
ProxyMode::Pac { url: pac_url, .. } => self.resolve(url, pac_url),
ProxyMode::PacInline { script, .. } => evaluate_script(script, url, self.timeout),
}
}
}
impl Default for CfNetworkPacResolver {
fn default() -> Self {
Self::new()
}
}
fn check_timeout(timeout: Duration) -> Result<(), Error> {
if timeout.is_zero() {
return Err(Error::PacTimeout { timeout });
}
Ok(())
}
fn url_error(error: &CFError) -> Error {
Error::io(
"CFNetwork resolving a PAC URL",
io::Error::other(format!(
"{} error {}",
text(&error.domain())
.as_deref()
.unwrap_or("<unreadable domain>"),
error.code()
)),
)
}
fn runs_a_script(settings: &CFDictionary) -> bool {
["ProxyAutoConfigEnable", "ProxyAutoDiscoveryEnable"]
.into_iter()
.any(|key| {
let key = CFString::from_static_string(key);
settings
.find(key.as_concrete_TypeRef().cast::<c_void>())
.map(|value| unsafe { CFType::wrap_under_get_rule(*value) })
.and_then(|value| value.downcast::<CFNumber>())
.and_then(|value| value.to_i64())
== Some(1)
})
}
fn evaluate_script(
script: &str,
url: &Url,
timeout: Duration,
) -> Result<Vec<ProxyStep>, Error> {
let deadline = Instant::now().checked_add(timeout);
let script = CFString::new(script);
let target = cf_url(crate::pac::sanitize_url(url).as_str())?;
let answer = pump(timeout, deadline, &target, |target, context| {
unsafe {
CFNetworkExecuteProxyAutoConfigurationScript(
script.as_concrete_TypeRef(),
target,
on_result,
context,
)
}
})?;
match answer {
Ok(array) => array_to_steps(array.as_ref()),
Err(error) => Err(Error::pac_evaluation(format!(
"CFNetwork: {} (error {} {})",
text(&error.description())
.as_deref()
.unwrap_or("<unreadable description>"),
text(&error.domain())
.as_deref()
.unwrap_or("<unreadable domain>"),
error.code()
))),
}
}
fn cf_url(text: &str) -> Result<CFURL, Error> {
let length = CFIndex::try_from(text.len())
.map_err(|_| Error::io("building a CFURL", io::Error::other("URL too long")))?;
let raw = unsafe {
CFURLCreateWithBytes(
kCFAllocatorDefault,
text.as_ptr(),
length,
kCFStringEncodingUTF8,
ptr::null(),
)
};
if raw.is_null() {
return Err(Error::io(
"building a CFURL",
io::Error::other("CFURLCreateWithBytes returned NULL"),
));
}
Ok(unsafe { CFURL::wrap_under_create_rule(raw) })
}
fn pump(
timeout: Duration,
deadline: Option<Instant>,
target: &CFURL,
start: impl FnOnce(CFURLRef, *mut ClientContext) -> CFRunLoopSourceRef,
) -> Result<Answer, Error> {
let _serial = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
warm_up(target);
let mut answer: Option<Answer> = None;
let mut context = ClientContext {
version: 0,
info: (&raw mut answer).cast(),
retain: None,
release: None,
copy_description: None,
};
let raw = start(target.as_concrete_TypeRef(), &raw mut context);
if raw.is_null() {
return Err(Error::io(
"starting a CFNetwork PAC call",
io::Error::other("CFNetwork returned no run loop source"),
));
}
let source = unsafe { CFRunLoopSource::wrap_under_create_rule(raw) };
let mode = CFString::from_static_string(RUN_LOOP_MODE);
let run_loop = CFRunLoop::get_current();
run_loop.add_source(&source, mode.as_concrete_TypeRef());
while answer.is_none() {
let left = match deadline {
Some(deadline) => deadline.saturating_duration_since(Instant::now()),
None => Duration::from_secs(3600),
};
if left.is_zero() {
break;
}
CFRunLoop::run_in_mode(mode.as_concrete_TypeRef(), left, false);
}
unsafe { CFRunLoopSourceInvalidate(source.as_concrete_TypeRef()) };
run_loop.remove_source(&source, mode.as_concrete_TypeRef());
answer.ok_or(Error::PacTimeout { timeout })
}
fn warm_up(target: &CFURL) {
let empty: CFDictionary<CFType, CFType> = CFDictionary::from_CFType_pairs(&[]);
let raw = unsafe {
CFNetworkCopyProxiesForURL(target.as_concrete_TypeRef(), empty.as_concrete_TypeRef())
};
if !raw.is_null() {
drop(unsafe { CFArray::<CFType>::wrap_under_create_rule(raw) });
}
}
unsafe extern "C" fn on_result(client: *mut c_void, proxies: CFArrayRef, error: CFErrorRef) {
let answer = if error.is_null() {
Ok((!proxies.is_null())
.then(|| unsafe { CFArray::<CFType>::wrap_under_get_rule(proxies) }))
} else {
Err(unsafe { CFError::wrap_under_get_rule(error) })
};
unsafe { *client.cast::<Option<Answer>>() = Some(answer) };
CFRunLoop::get_current().stop();
}
fn array_to_steps(array: Option<&CFArray<CFType>>) -> Result<Vec<ProxyStep>, Error> {
entries_to_steps(&read_entries(array))
}
fn read_entries(array: Option<&CFArray<CFType>>) -> Vec<Entry> {
array
.map(|array| array.iter().filter_map(|item| read_entry(&item)).collect())
.unwrap_or_default()
}
fn read_entry(item: &CFType) -> Option<Entry> {
let dict = item.downcast::<CFDictionary>()?;
let get = |key: CFStringRef| {
dict.find(key.cast::<c_void>())
.map(|value| unsafe { CFType::wrap_under_get_rule(*value) })
};
let (type_key, host_key, port_key, pac_key) = unsafe {
(
kCFProxyTypeKey,
kCFProxyHostNameKey,
kCFProxyPortNumberKey,
kCFProxyAutoConfigurationURLKey,
)
};
let kind = get(type_key)?.downcast::<CFString>()?;
let kind = unsafe {
if kind == CFString::wrap_under_get_rule(kCFProxyTypeNone) {
EntryKind::None
} else if kind == CFString::wrap_under_get_rule(kCFProxyTypeHTTP) {
EntryKind::Http
} else if kind == CFString::wrap_under_get_rule(kCFProxyTypeHTTPS) {
EntryKind::Https
} else if kind == CFString::wrap_under_get_rule(kCFProxyTypeSOCKS) {
EntryKind::Socks
} else if kind == CFString::wrap_under_get_rule(kCFProxyTypeAutoConfigurationURL) {
EntryKind::AutoConfigUrl
} else {
EntryKind::Other
}
};
Some(Entry {
kind,
host: get(host_key)
.and_then(|host| host.downcast::<CFString>())
.and_then(|host| text(&host)),
port: get(port_key)
.and_then(|port| port.downcast::<CFNumber>())
.and_then(|port| port.to_i64()),
pac_url: get(pac_key)
.and_then(|pac| pac.downcast::<CFURL>())
.and_then(|pac| text(&pac.absolute().get_string())),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn settings(pairs: &[(&str, CFType)]) -> CFDictionary {
let pairs: Vec<(CFString, CFType)> = pairs
.iter()
.map(|(key, value)| (CFString::new(key), value.clone()))
.collect();
CFDictionary::from_CFType_pairs(&pairs).into_untyped()
}
#[test]
fn an_unreadable_host_drops_the_entry_instead_of_panicking() {
let chars: [core_foundation::string::UniChar; 1] = [0xD800];
let malformed = unsafe {
CFString::wrap_under_create_rule(
core_foundation::string::CFStringCreateWithCharacters(
kCFAllocatorDefault,
chars.as_ptr(),
1,
),
)
};
let (type_key, http, host_key) = unsafe {
(
CFString::wrap_under_get_rule(kCFProxyTypeKey),
CFString::wrap_under_get_rule(kCFProxyTypeHTTP),
CFString::wrap_under_get_rule(kCFProxyHostNameKey),
)
};
let entry = CFDictionary::from_CFType_pairs(&[
(type_key, http.as_CFType()),
(host_key, malformed.as_CFType()),
])
.into_untyped();
let read = read_entry(&entry.as_CFType()).expect("the type key is readable");
assert_eq!((read.kind, read.host), (EntryKind::Http, None));
assert!(matches!(
array_to_steps(Some(&CFArray::from_CFTypes(&[entry.as_CFType()]))),
Err(Error::PacInvalidResult { .. })
));
}
#[test]
#[ignore = "asks the network for `wpad` and connects to 127.0.0.1:1"]
fn system_settings_resolve_through_cfnetwork() {
let resolver = CfNetworkPacResolver::new();
let target = cf_url("http://example.com/").unwrap();
let run = |pairs: &[(&str, CFType)]| {
let deadline = Instant::now().checked_add(resolver.timeout());
resolver.resolve_with_settings(deadline, &target, &settings(pairs))
};
let on = CFNumber::from(1).as_CFType();
let pac = |url: &str| {
[
("ProxyAutoConfigEnable", on.clone()),
("ProxyAutoConfigURLString", CFString::new(url).as_CFType()),
]
};
let [enable, url] = pac("http://wpad/wpad.dat");
let discovery = [enable, url, ("ProxyAutoDiscoveryEnable", on.clone())];
assert_eq!(run(&discovery).unwrap(), vec![ProxyStep::Direct]);
assert!(matches!(
run(&pac("http://127.0.0.1:1/p.pac")),
Err(Error::Io { .. })
));
assert!(matches!(run(&[]), Err(Error::Io { .. })));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(kind: EntryKind, host: &str, port: i64) -> Entry {
Entry {
kind,
host: Some(host.to_owned()),
port: Some(port),
pac_url: None,
}
}
fn direct() -> Entry {
Entry {
kind: EntryKind::None,
host: None,
port: None,
pac_url: None,
}
}
fn pac(url: &str) -> Entry {
Entry {
kind: EntryKind::AutoConfigUrl,
host: None,
port: None,
pac_url: Some(url.to_owned()),
}
}
fn same_as(result: &str, entries: &[Entry]) {
assert_eq!(
entries_to_steps(entries).unwrap(),
crate::pac::parse_find_proxy_result(result).unwrap(),
"{result}"
);
}
#[test]
fn each_type_maps_as_the_js_engines_read_it() {
same_as(
"PROXY a.example:8080; HTTPS b.example:443; SOCKS c.example:1080; DIRECT",
&[
entry(EntryKind::Http, "a.example", 8080),
entry(EntryKind::Https, "b.example", 443),
entry(EntryKind::Socks, "c.example", 1080),
direct(),
],
);
}
#[test]
fn a_bare_ipv6_host_is_bracketed() {
same_as(
"PROXY [2001:db8::1]:3128",
&[entry(EntryKind::Http, "2001:db8::1", 3128)],
);
}
#[test]
fn a_missing_or_impossible_port_takes_the_default() {
let mut no_port = entry(EntryKind::Http, "a.example", 0);
no_port.port = None;
same_as("PROXY a.example", &[no_port]);
same_as(
"PROXY a.example",
&[entry(EntryKind::Http, "a.example", 70000)],
);
}
#[test]
fn other_types_and_unusable_hosts_are_skipped() {
same_as(
"PROXY ok.example:1",
&[
entry(EntryKind::Other, "pac.example", 80),
entry(EntryKind::Http, "x.example; PROXY evil.example", 1),
entry(EntryKind::Http, "", 1),
entry(EntryKind::Http, "[2001", 0),
entry(EntryKind::Http, "ok.example", 1),
],
);
}
#[test]
fn nothing_usable_is_an_error() {
assert!(matches!(
entries_to_steps(&[]),
Err(Error::PacInvalidResult { .. })
));
assert!(matches!(
entries_to_steps(&[entry(EntryKind::Other, "pac.example", 80)]),
Err(Error::PacInvalidResult { .. })
));
}
fn steps(result: &str) -> Vec<ProxyStep> {
crate::pac::parse_find_proxy_result(result).unwrap()
}
#[test]
fn the_first_pac_that_answers_is_the_answer() {
let mut ran = Vec::new();
let answer = walk_system_entries(
&[
pac("http://wpad/wpad.dat"),
pac("http://second/p.pac"),
direct(),
],
|url| {
ran.push(url.to_owned());
Ok((url != "http://wpad/wpad.dat").then(|| steps("PROXY p.example:1")))
},
);
assert_eq!(answer.unwrap(), steps("PROXY p.example:1"));
assert_eq!(ran, ["http://wpad/wpad.dat", "http://second/p.pac"]);
}
#[test]
fn misses_to_the_end_are_direct() {
let answer = walk_system_entries(&[pac("http://wpad/wpad.dat")], |_| Ok(None));
assert_eq!(answer.unwrap(), vec![ProxyStep::Direct]);
}
#[test]
fn a_miss_falls_to_the_entries_beneath_as_given() {
let answer = walk_system_entries(
&[
pac("http://wpad/wpad.dat"),
entry(EntryKind::Http, "beneath.example", 3128),
direct(),
],
|_| Ok(None),
);
assert_eq!(answer.unwrap(), steps("PROXY beneath.example:3128; DIRECT"));
}
#[test]
fn an_array_without_a_pac_is_taken_as_given() {
let answer = walk_system_entries(&[direct()], |_| panic!("no PAC to run"));
assert_eq!(answer.unwrap(), vec![ProxyStep::Direct]);
}
#[test]
fn a_failure_is_not_a_miss_and_stops_the_walk() {
let answer = walk_system_entries(&[pac("http://wpad/wpad.dat"), direct()], |_| {
Err(Error::PacTimeout {
timeout: std::time::Duration::from_secs(1),
})
});
assert!(
matches!(answer, Err(Error::PacTimeout { .. })),
"{answer:?}"
);
}
#[test]
fn an_empty_array_is_an_error() {
assert!(matches!(
walk_system_entries(&[], |_| Ok(None)),
Err(Error::PacInvalidResult { .. })
));
}
#[test]
fn only_an_unresolvable_wpad_host_is_a_miss() {
assert!(is_wpad_miss(
"http://wpad/wpad.dat",
"NSURLErrorDomain",
-1003
));
assert!(is_wpad_miss(
"http://WPAD/wpad.dat",
"NSURLErrorDomain",
-1003
));
assert!(!is_wpad_miss(
"http://wpad/wpad.dat",
"NSURLErrorDomain",
-1004
));
assert!(!is_wpad_miss(
"http://pac.corp/wpad.dat",
"NSURLErrorDomain",
-1003
));
assert!(!is_wpad_miss(
"http://wpad.corp/wpad.dat",
"NSURLErrorDomain",
-1003
));
assert!(!is_wpad_miss(
"http://wpad/wpad.dat",
"kCFErrorDomainCFNetwork",
-1003
));
}
}