Skip to main content

proxy_watch/pac/
dispatch.rs

1//! [`PacResolver`]: one entry point that hands each mode to the engine that can answer it.
2
3use url::Url;
4
5use super::{PacEvaluator, PacPolicy, PacScript};
6use crate::config::ProxyConfig;
7use crate::error::Error;
8use crate::resolve::ProxyStep;
9
10// The native engine this target has. The Windows and Apple ones download, and discover
11// after `with_wpad`; the Android one asks the system PAC service, which does both itself.
12#[cfg(pac_native)]
13use super::NativePacResolver as Native;
14
15/// Routes a whole [`ProxyConfig`] to whichever PAC engine answers its mode.
16///
17/// The order, first match wins:
18///
19/// 1. [`Direct`](crate::ProxyMode::Direct) / [`Manual`](crate::ProxyMode::Manual):
20///    [`resolve`](crate::resolve()), bypass list included. A `script` is ignored.
21/// 2. A URL with no host (`mailto:`, `data:`): Direct, and no engine runs.
22/// 3. A `script` the caller passed: the JS engine, whichever PAC mode is effective. A
23///    caller who fetched the body has already answered the question the mode asks.
24/// 4. [`PacInline`](crate::ProxyMode::PacInline): the JS engine, with the body the mode
25///    carries.
26///
27/// "The JS engine" is the evaluator attached with [`with_evaluator`](Self::with_evaluator)
28/// when there is one, else [`evaluate`](super::evaluate) under this resolver's [`PacPolicy`].
29/// 5. [`Pac`](crate::ProxyMode::Pac) / [`WpadAutoDetect`](crate::ProxyMode::WpadAutoDetect)
30///    with a native resolver attached (`with_native`): `WinHttpPacResolver::resolve_config`
31///    (Windows + `pac-windows-native`) or `CfNetworkPacResolver::resolve_config` (macOS +
32///    `pac-macos-native`, iOS + `pac-ios-native`), which downloads, and discovers once the resolver was built
33///    `with_wpad(true)`; `AndroidPacResolver::resolve_config` (Android +
34///    `pac-android-native`), which asks the system PAC service through `ProxySelector`.
35/// 6. Otherwise what [`resolve_with_pac`](crate::resolve_with_pac) says: `Pac` →
36///    [`Error::PacFetchRequired`], `WpadAutoDetect` → [`Error::PacNotSupported`].
37///
38/// A failure is reported, never retried on the other engine: a native error does not fall
39/// back to JS, and a JS error does not fall back to the native one. The two engines
40/// disagree about what a script is allowed to see: [`PacPolicy`] governs the JS engine
41/// only, and the native one uses real DNS and the real local address, so a silent retry
42/// would change which policy produced the answer.
43///
44/// The two budgets are independent: [`PacPolicy::timeout`] bounds a JS evaluation, and the
45/// WinHTTP and CFNetwork resolvers' `timeout` bounds a native resolution, download and
46/// discovery included. The Android resolver has no timeout of its own.
47///
48/// With no evaluator attached and no QuickJS engine compiled in (`pac-quickjs` off, or on
49/// Android or iOS, where the feature builds without one), the JS arms are
50/// [`Error::PacEngineUnavailable`].
51///
52/// ```
53/// # #[cfg(pac_quickjs)] {
54/// use proxy_watch::pac::{PacPolicy, PacResolver};
55/// use proxy_watch::{ProxyConfig, ProxyConfigSource, ProxyMode, ProxyStep, Url};
56///
57/// let mode = ProxyMode::pac_inline(
58///     "function FindProxyForURL(u, h) { return 'DIRECT'; }".to_owned(),
59/// );
60/// let config = ProxyConfig::from_source(ProxyConfigSource::Registry, mode);
61/// let url = Url::parse("http://example.net/").unwrap();
62///
63/// let resolver = PacResolver::new(PacPolicy::new());
64/// assert_eq!(resolver.resolve_config(&config, &url, None)?, vec![ProxyStep::Direct]);
65/// # }
66/// # Ok::<(), proxy_watch::Error>(())
67/// ```
68pub struct PacResolver {
69    policy: PacPolicy,
70    evaluator: Option<Box<dyn PacEvaluator + Send + Sync>>,
71    #[cfg(pac_native)]
72    native: Option<Native>,
73}
74
75impl PacResolver {
76    /// A resolver that runs PAC scripts under `policy` and has no native engine attached.
77    #[must_use]
78    pub fn new(policy: PacPolicy) -> Self {
79        Self {
80            policy,
81            evaluator: None,
82            #[cfg(pac_native)]
83            native: None,
84        }
85    }
86
87    /// Attach the native resolver (`WinHttpPacResolver` on Windows, `CfNetworkPacResolver`
88    /// on macOS and iOS, `AndroidPacResolver` on Android) for
89    /// [`Pac`](crate::ProxyMode::Pac) and
90    /// [`WpadAutoDetect`](crate::ProxyMode::WpadAutoDetect) with no caller-supplied script.
91    ///
92    /// Takes a built resolver rather than building one, so that a failure to open a
93    /// session is the caller's to see, not a silent fall back to "fetch it yourself", and
94    /// so that its `with_wpad` choice is the caller's.
95    #[cfg(pac_native)]
96    #[must_use]
97    pub fn with_native(mut self, native: Native) -> Self {
98        self.native = Some(native);
99        self
100    }
101
102    /// Attach this target's native resolver, built with its defaults (WPAD off), when a
103    /// `pac-*-native` feature enables one; otherwise return `self` unchanged. Available
104    /// without `cfg` guards in cross-platform applications.
105    ///
106    /// # Errors
107    ///
108    /// What building the native resolver returns: on Windows, a WinHTTP session that
109    /// fails to open.
110    pub fn with_system_native(self) -> Result<Self, Error> {
111        #[cfg(pac_native)]
112        let resolver = self.with_native(open_native()?);
113        #[cfg(not(pac_native))]
114        let resolver = self;
115        Ok(resolver)
116    }
117
118    /// The attached native resolver, if any.
119    #[cfg(pac_native)]
120    #[must_use]
121    pub fn native(&self) -> Option<&Native> {
122        self.native.as_ref()
123    }
124
125    /// Run the JS arms on `evaluator` instead of [`evaluate`](super::evaluate), for example
126    /// a `SubprocessEvaluator` (feature `pac-subprocess`), so that a script from the
127    /// configuration runs in a worker process.
128    ///
129    /// The evaluator runs under the policy it was built with; this resolver's
130    /// [`policy`](Self::policy) then governs nothing. The native arm is unchanged: a native
131    /// resolver still answers URL and WPAD modes when no script is passed.
132    #[must_use]
133    pub fn with_evaluator(mut self, evaluator: impl PacEvaluator + Send + Sync + 'static) -> Self {
134        self.evaluator = Some(Box::new(evaluator));
135        self
136    }
137
138    /// The policy [`evaluate`](super::evaluate) runs under when no evaluator is attached.
139    #[must_use]
140    pub fn policy(&self) -> &PacPolicy {
141        &self.policy
142    }
143
144    /// Resolve `url` under `config`, routing as the type documentation lists.
145    ///
146    /// # Errors
147    ///
148    /// Whatever the engine the mode routed to returns: [`resolve_with_pac`]'s errors for the
149    /// JS arms and the fall-through (an attached evaluator's own errors in place of
150    /// [`evaluate`](super::evaluate)'s), and the native resolver's `resolve_config`'s for the
151    /// native arm.
152    ///
153    /// [`resolve_with_pac`]: crate::resolve_with_pac
154    pub fn resolve_config(
155        &self,
156        config: &ProxyConfig,
157        url: &Url,
158        script: Option<&PacScript>,
159    ) -> Result<Vec<ProxyStep>, Error> {
160        #[cfg(pac_native)]
161        if let Some(native) = &self.native
162            && script.is_none()
163            && matches!(
164                config.effective,
165                crate::ProxyMode::Pac { .. } | crate::ProxyMode::WpadAutoDetect
166            )
167        {
168            return native.resolve_config(config, url);
169        }
170        crate::resolve::resolve_with_pac_using(config, url, script, |script| {
171            match &self.evaluator {
172                Some(evaluator) => evaluator.evaluate(
173                    script,
174                    &super::sanitize_url(url),
175                    url.host_str().unwrap_or_default(),
176                ),
177                None => super::evaluate(script, url, &self.policy),
178            }
179        })
180    }
181}
182
183#[cfg(all(windows, feature = "pac-windows-native"))]
184fn open_native() -> Result<Native, Error> {
185    Native::new()
186}
187
188#[cfg(all(pac_native, not(windows)))]
189fn open_native() -> Result<Native, Error> {
190    Ok(Native::new())
191}
192
193impl std::fmt::Debug for PacResolver {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        let mut debug = f.debug_struct("PacResolver");
196        debug
197            .field("policy", &self.policy)
198            .field("evaluator", &self.evaluator.as_ref().map(|_| "attached"));
199        #[cfg(pac_native)]
200        debug.field("native", &self.native);
201        debug.finish()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{ProxyConfigSource, ProxyMode};
209
210    // Whether or not this build has a native resolver, attaching it leaves the other arms
211    // answering as before.
212    #[test]
213    fn attaching_the_system_native_keeps_direct_direct() {
214        let resolver = PacResolver::new(PacPolicy::new())
215            .with_system_native()
216            .unwrap();
217        #[cfg(pac_native)]
218        assert!(resolver.native().is_some());
219        let config = ProxyConfig::from_source(ProxyConfigSource::Env, ProxyMode::Direct);
220        let url = Url::parse("http://example.net/").unwrap();
221        assert_eq!(
222            resolver.resolve_config(&config, &url, None).unwrap(),
223            vec![ProxyStep::Direct]
224        );
225    }
226}