Skip to main content

rama_net/client/proxy/system/
mod.rs

1//! Route client requests through the operating system's proxy settings.
2
3use std::{
4    fmt,
5    sync::{
6        Arc, OnceLock,
7        atomic::{AtomicU64, Ordering},
8    },
9    time::{Duration, Instant},
10};
11
12use arc_swap::ArcSwapOption;
13use rama_core::{
14    Layer, Service,
15    error::{BoxError, BoxErrorExt as _, ErrorContext},
16    error_sink::{ErrorSink, TracingErrorSink},
17    extensions::{Extensions, ExtensionsRef},
18    service::{BoxService, service_fn},
19};
20use rama_utils::macros::generate_set_and_with;
21
22#[cfg(any(
23    test,
24    target_vendor = "apple",
25    target_os = "android",
26    target_os = "linux",
27    target_os = "freebsd",
28    target_os = "netbsd",
29    target_os = "openbsd",
30    target_os = "dragonfly"
31))]
32use crate::address::{Host, HostWithPort};
33use crate::{
34    Protocol,
35    address::{Authority, HostRef, HostWithOptPort, ProxyAddress},
36    input_ext::{AuthorityInputExt, ProtocolInputExt, UriInputExt},
37    uri::Uri,
38};
39
40use super::{
41    ProxyRoute, ProxyRoutes,
42    bypass::{BypassRule, BypassRuleDialect, is_simple_hostname, matches_any_rule},
43    load::LoadErrorPolicy,
44};
45
46mod platform;
47
48/// How long a [`SystemProxyLayer`] keeps a system proxy snapshot before lazily
49/// checking for changes.
50///
51/// The ten-second default follows the polling interval used by
52/// [Chromium's Windows proxy configuration service][chromium] where change
53/// notifications alone are insufficient. Use
54/// [`SystemProxyLayer::new_with_ttl`] to select a different value.
55///
56/// [chromium]: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/proxy_resolution/win/proxy_config_service_win.cc
57pub const DEFAULT_SYSTEM_PROXY_CONFIG_TTL: Duration = Duration::from_secs(10);
58
59/// How system proxy discovery handles bypass rules Rama cannot parse.
60///
61/// This policy applies independently of whether a platform uses ordinary or
62/// reversed bypass-list semantics. Rejecting invalid rules prevents a partial
63/// snapshot from making routing decisions with a different rule set than the
64/// operating system supplied.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum SystemProxyInvalidBypassRulePolicy {
68    /// Ignore an invalid rule and retain the rest of the system snapshot.
69    #[default]
70    Ignore,
71    /// Reject the complete system snapshot when any bypass rule is invalid.
72    Reject,
73}
74
75/// The request information passed to a system PAC resolver.
76///
77/// When produced by [`SystemProxyLayer`], the URI is absolute, has a root path
78/// when the original target omitted one, and omits the scheme's default port.
79/// The extension store is a cheap clone of the input's store. This keeps caller
80/// metadata available to custom PAC implementations without borrowing the
81/// request across an await point.
82#[derive(Debug, Clone)]
83pub struct SystemProxyPacRequest {
84    /// Metadata cloned from the routed service input.
85    pub extensions: Extensions,
86    /// The normalized absolute URI for which routes are requested.
87    pub uri: Uri,
88}
89
90impl SystemProxyPacRequest {
91    /// Create a PAC request from an absolute request URI and its extensions.
92    pub fn new(extensions: Extensions, uri: Uri) -> Result<Self, BoxError> {
93        if !uri.is_absolute() || uri.host().is_none() {
94            return Err(BoxError::from_static_str(
95                "system proxy PAC request URI must be absolute and have a host",
96            ));
97        }
98        Ok(Self { extensions, uri })
99    }
100}
101
102impl ExtensionsRef for SystemProxyPacRequest {
103    fn extensions(&self) -> &Extensions {
104        &self.extensions
105    }
106}
107
108impl UriInputExt for SystemProxyPacRequest {
109    fn uri(&self) -> &Uri {
110        &self.uri
111    }
112}
113
114/// Resolves proxy routes for a request using one system-configured PAC script.
115///
116/// Returning `None` asks the system layer to try the fixed proxy settings from
117/// the same snapshot, if any, and otherwise leave the request unchanged.
118/// The blanket implementation accepts any resolver error that converts into
119/// [`BoxError`]. Implementations may return a concrete service; no allocation
120/// or type erasure is required.
121pub trait SystemProxyPacResolver:
122    Service<SystemProxyPacRequest, Output = Option<ProxyRoutes>, Error: Into<BoxError>>
123{
124}
125
126impl<T> SystemProxyPacResolver for T where
127    T: Service<SystemProxyPacRequest, Output = Option<ProxyRoutes>, Error: Into<BoxError>>
128{
129}
130
131/// Supplies a resolver for a system-configured PAC URI.
132///
133/// The blanket implementation accepts any factory and resolver errors that
134/// convert into [`BoxError`]. The resolver output remains concrete so
135/// implementations can choose their own caching and sharing strategy.
136pub trait SystemProxyPacService:
137    Service<Uri, Error: Into<BoxError>, Output: SystemProxyPacResolver>
138{
139}
140
141impl<T> SystemProxyPacService for T where
142    T: Service<Uri, Error: Into<BoxError>, Output: SystemProxyPacResolver>
143{
144}
145
146/// A snapshot of the operating system's proxy configuration.
147///
148/// HTTP and HTTPS identify the destination scheme, not necessarily the
149/// transport protocol used to reach the proxy. A SOCKS5 proxy is used as a
150/// fallback when no scheme-specific proxy is configured. A PAC URI takes
151/// precedence over fixed proxies because it can make a per-request decision;
152/// each platform reader records whether its bypass entries apply before PAC.
153/// System proxy routing always bypasses loopback hosts, including before PAC
154/// evaluation, matching native proxy stacks.
155#[derive(Debug, Clone, Default)]
156pub struct SystemProxyConfig {
157    http: Option<ProxyAddress>,
158    https: Option<ProxyAddress>,
159    socks5: Option<ProxyAddress>,
160    pac_uri: Option<Uri>,
161    auto_detect: bool,
162    bypass: Arc<[BypassRule]>,
163    exclude_simple_hostnames: bool,
164    reversed_bypass: bool,
165    bypass_before_pac: bool,
166}
167
168impl SystemProxyConfig {
169    fn replace_bypass_ignoring_invalid(
170        &mut self,
171        bypass: impl IntoIterator<Item = impl Into<Box<str>>>,
172        dialect: BypassRuleDialect,
173    ) {
174        self.bypass = bypass
175            .into_iter()
176            .filter_map(
177                |value| match BypassRule::compile_with_dialect(value, dialect) {
178                    Ok(rule) => Some(rule),
179                    Err(error) => {
180                        rama_core::telemetry::tracing::debug!(
181                            error = %error,
182                            "ignoring invalid system proxy bypass pattern"
183                        );
184                        None
185                    }
186                },
187            )
188            .collect();
189    }
190
191    fn try_replace_bypass(
192        &mut self,
193        bypass: impl IntoIterator<Item = impl Into<Box<str>>>,
194        policy: SystemProxyInvalidBypassRulePolicy,
195        dialect: BypassRuleDialect,
196    ) -> Result<(), BoxError> {
197        match policy {
198            SystemProxyInvalidBypassRulePolicy::Ignore => {
199                self.replace_bypass_ignoring_invalid(bypass, dialect);
200            }
201            SystemProxyInvalidBypassRulePolicy::Reject => {
202                self.bypass = bypass
203                    .into_iter()
204                    .map(|value| BypassRule::compile_with_dialect(value, dialect))
205                    .collect::<Result<Vec<_>, _>>()?
206                    .into();
207            }
208        }
209        Ok(())
210    }
211
212    #[cfg(any(
213        test,
214        target_vendor = "apple",
215        target_os = "android",
216        target_os = "windows",
217        target_os = "linux",
218        target_os = "freebsd",
219        target_os = "netbsd",
220        target_os = "openbsd",
221        target_os = "dragonfly"
222    ))]
223    fn try_set_bypass_with_dialect(
224        &mut self,
225        bypass: impl IntoIterator<Item = impl Into<Box<str>>>,
226        policy: SystemProxyInvalidBypassRulePolicy,
227        dialect: BypassRuleDialect,
228    ) -> Result<(), BoxError> {
229        self.try_replace_bypass(bypass, policy, dialect)
230    }
231
232    /// Read the current platform proxy snapshot.
233    ///
234    /// - Windows uses the active user's WinINET/Internet Options settings;
235    /// - macOS and iOS use CFNetwork's system proxy dictionary;
236    /// - Android uses `ConnectivityManager.getDefaultProxy()`, with the legacy
237    ///   `Proxy` API on Android versions before API 23;
238    /// - Linux and BSD prefer KDE's `kioslaverc` on KDE desktops, otherwise
239    ///   reading GNOME `gsettings` before falling back to KDE.
240    ///
241    /// This deliberately does not inspect `http_proxy` or related environment
242    /// variables. Those are application configuration and are handled by
243    /// [`ProxyEnvLayer`][crate::client::ProxyEnvLayer] and
244    /// [`NoProxyEnvLayer`][crate::client::NoProxyEnvLayer] instead.
245    ///
246    /// Automatic discovery such as WPAD is recorded by [`Self::auto_detect`]
247    /// but is not attempted when the platform does not provide a concrete PAC
248    /// URI. Malformed non-empty proxy values are reported as errors rather
249    /// than silently bypassing a configured system policy.
250    ///
251    /// Platform operations that have asynchronous APIs are awaited directly.
252    /// Native platforms that only expose a synchronous snapshot call keep that
253    /// call narrowly isolated inside their platform reader.
254    pub async fn try_from_system() -> Result<Self, BoxError> {
255        Self::try_from_system_with_invalid_bypass_rule_policy(
256            SystemProxyInvalidBypassRulePolicy::Ignore,
257        )
258        .await
259    }
260
261    /// Read the current platform proxy snapshot with an explicit invalid
262    /// bypass-rule policy.
263    ///
264    /// [`Ignore`][SystemProxyInvalidBypassRulePolicy::Ignore] is the default
265    /// used by [`Self::try_from_system`]. Selecting
266    /// [`Reject`][SystemProxyInvalidBypassRulePolicy::Reject] returns an error
267    /// instead of accepting a snapshot with one or more discarded rules.
268    pub async fn try_from_system_with_invalid_bypass_rule_policy(
269        policy: SystemProxyInvalidBypassRulePolicy,
270    ) -> Result<Self, BoxError> {
271        platform::read(policy)
272            .await
273            .context("read system proxy configuration")
274    }
275
276    /// Return whether this snapshot contains no automatic, PAC, or fixed proxy
277    /// settings.
278    #[must_use]
279    pub fn is_empty(&self) -> bool {
280        self.http.is_none()
281            && self.https.is_none()
282            && self.socks5.is_none()
283            && self.pac_uri.is_none()
284            && !self.auto_detect
285    }
286
287    /// The proxy for HTTP destinations.
288    #[must_use]
289    pub const fn http_proxy(&self) -> Option<&ProxyAddress> {
290        self.http.as_ref()
291    }
292
293    /// The proxy for HTTPS destinations.
294    #[must_use]
295    pub const fn https_proxy(&self) -> Option<&ProxyAddress> {
296        self.https.as_ref()
297    }
298
299    /// The SOCKS5 fallback proxy.
300    #[must_use]
301    pub const fn socks5_proxy(&self) -> Option<&ProxyAddress> {
302        self.socks5.as_ref()
303    }
304
305    /// The configured PAC script URI.
306    #[must_use]
307    pub const fn pac_uri(&self) -> Option<&Uri> {
308        self.pac_uri.as_ref()
309    }
310
311    /// Whether the platform requested automatic proxy discovery (for example,
312    /// WPAD) without necessarily supplying a concrete PAC URI.
313    #[must_use]
314    pub const fn auto_detect(&self) -> bool {
315        self.auto_detect
316    }
317
318    /// Host patterns that bypass fixed proxies.
319    pub fn bypass(&self) -> impl Iterator<Item = &str> {
320        self.bypass.iter().map(BypassRule::raw)
321    }
322
323    /// Whether names without a dot bypass fixed proxies.
324    #[must_use]
325    pub const fn exclude_simple_hostnames(&self) -> bool {
326        self.exclude_simple_hostnames
327    }
328
329    /// Whether fixed proxies are used only for hosts matching [`bypass`][Self::bypass].
330    ///
331    /// KDE exposes this uncommon inverted exception-list mode. The default is
332    /// `false`, where matching hosts bypass the proxy in the usual way.
333    #[must_use]
334    pub const fn reversed_bypass(&self) -> bool {
335        self.reversed_bypass
336    }
337
338    generate_set_and_with! {
339        /// Set the proxy used for HTTP destinations.
340        pub fn http_proxy(mut self, proxy: Option<ProxyAddress>) -> Self {
341            self.http = proxy;
342            self
343        }
344    }
345
346    generate_set_and_with! {
347        /// Set the proxy used for HTTPS destinations.
348        pub fn https_proxy(mut self, proxy: Option<ProxyAddress>) -> Self {
349            self.https = proxy;
350            self
351        }
352    }
353
354    generate_set_and_with! {
355        /// Set the SOCKS5 fallback proxy.
356        pub fn socks5_proxy(mut self, proxy: Option<ProxyAddress>) -> Self {
357            self.socks5 = proxy;
358            self
359        }
360    }
361
362    generate_set_and_with! {
363        /// Set the PAC script URI.
364        pub fn pac_uri(mut self, pac_uri: Option<Uri>) -> Self {
365            self.pac_uri = pac_uri;
366            self
367        }
368    }
369
370    generate_set_and_with! {
371        /// Record whether automatic proxy discovery is enabled.
372        ///
373        /// Rama exposes this signal but does not perform WPAD itself.
374        pub fn auto_detect(mut self, auto_detect: bool) -> Self {
375            self.auto_detect = auto_detect;
376            self
377        }
378    }
379
380    generate_set_and_with! {
381        /// Replace the fixed-proxy bypass patterns, ignoring invalid entries.
382        ///
383        /// Use [`Self::try_set_bypass`] with
384        /// [`Reject`][SystemProxyInvalidBypassRulePolicy::Reject] when an invalid
385        /// entry should reject the complete update. Boxed strings transfer
386        /// directly into snapshot storage; borrowed strings are copied because
387        /// the snapshot owns its rules.
388        pub fn bypass(
389            mut self,
390            bypass: impl IntoIterator<Item = impl Into<Box<str>>>,
391        ) -> Self {
392            self.replace_bypass_ignoring_invalid(bypass, BypassRuleDialect::Rama);
393            self
394        }
395    }
396
397    generate_set_and_with! {
398        /// Replace the fixed-proxy bypass patterns using an explicit invalid-rule
399        /// policy.
400        ///
401        /// The update is atomic: when [`Reject`][SystemProxyInvalidBypassRulePolicy::Reject]
402        /// encounters an invalid rule, this returns an error and leaves the prior
403        /// bypass list unchanged. Boxed strings transfer directly into snapshot
404        /// storage; borrowed strings are copied because the snapshot owns its
405        /// rules.
406        pub fn bypass(
407            mut self,
408            bypass: impl IntoIterator<Item = impl Into<Box<str>>>,
409            policy: SystemProxyInvalidBypassRulePolicy,
410        ) -> Result<Self, BoxError> {
411            self.try_replace_bypass(bypass, policy, BypassRuleDialect::Rama)?;
412            Ok(self)
413        }
414    }
415
416    generate_set_and_with! {
417        /// Configure whether names without a dot bypass fixed proxies.
418        pub fn exclude_simple_hostnames(mut self, exclude: bool) -> Self {
419            self.exclude_simple_hostnames = exclude;
420            self
421        }
422    }
423
424    generate_set_and_with! {
425        /// Invert the meaning of fixed-proxy bypass patterns.
426        pub fn reversed_bypass(mut self, reversed: bool) -> Self {
427            self.reversed_bypass = reversed;
428            self
429        }
430    }
431
432    fn decision(&self, uri: &Uri) -> SystemProxyDecision {
433        if let Some(pac_uri) = &self.pac_uri {
434            if uri.host().is_some_and(|host| {
435                host.is_loopback()
436                    || (self.bypass_before_pac
437                        && self.bypasses(
438                            uri.scheme(),
439                            host,
440                            uri.port_u16()
441                                .or_else(|| uri.scheme().and_then(Protocol::default_port)),
442                        ))
443            }) {
444                return SystemProxyDecision::Route(ProxyRoute::Direct);
445            }
446            return SystemProxyDecision::Pac(pac_uri.clone());
447        }
448
449        self.fixed_route(uri)
450            .map(SystemProxyDecision::Route)
451            .unwrap_or(SystemProxyDecision::None)
452    }
453
454    fn fixed_route(&self, uri: &Uri) -> Option<ProxyRoute> {
455        let host = uri.host()?;
456        self.fixed_route_for(
457            uri.scheme(),
458            host,
459            uri.port_u16()
460                .or_else(|| uri.scheme().and_then(Protocol::default_port)),
461        )
462    }
463
464    fn fixed_route_for(
465        &self,
466        scheme: Option<&Protocol>,
467        host: HostRef<'_>,
468        port: Option<u16>,
469    ) -> Option<ProxyRoute> {
470        let proxy = match scheme {
471            Some(protocol) if *protocol == Protocol::HTTPS || *protocol == Protocol::WSS => {
472                self.https.as_ref()
473            }
474            Some(protocol) if *protocol == Protocol::HTTP || *protocol == Protocol::WS => {
475                self.http.as_ref()
476            }
477            _ => None,
478        }
479        .or(self.socks5.as_ref());
480        let proxy = proxy?;
481
482        // Platform proxy stacks implicitly keep loopback traffic local even
483        // when their user-visible bypass list does not mention it.
484        if host.is_loopback() || self.bypasses(scheme, host, port) {
485            return Some(ProxyRoute::Direct);
486        }
487        Some(ProxyRoute::Proxy(proxy.clone()))
488    }
489
490    fn bypasses(&self, scheme: Option<&Protocol>, host: HostRef<'_>, port: Option<u16>) -> bool {
491        let matches = (self.exclude_simple_hostnames && is_simple_hostname(host))
492            || matches_any_rule(&self.bypass, scheme, host, port);
493        if self.reversed_bypass {
494            !matches
495        } else {
496            matches
497        }
498    }
499}
500
501enum SystemProxyDecision {
502    None,
503    Route(ProxyRoute),
504    Pac(Uri),
505}
506
507type SystemProxyConfigReader = BoxService<(), SystemProxyConfig, BoxError>;
508type BoxSystemProxyConfigChangeTrigger = BoxService<(), bool, BoxError>;
509
510#[derive(Clone, Default)]
511struct LazyPlatformConfigChangeTrigger {
512    trigger: Arc<OnceLock<Arc<platform::PlatformConfigChangeTrigger>>>,
513}
514
515impl LazyPlatformConfigChangeTrigger {
516    fn poll(&self) -> Result<bool, BoxError> {
517        self.trigger
518            .get_or_init(platform::config_change_trigger)
519            .poll()
520    }
521
522    #[cfg(test)]
523    fn is_initialized(&self) -> bool {
524        self.trigger.get().is_some()
525    }
526}
527
528#[derive(Clone)]
529enum SystemProxyConfigChangeTrigger {
530    Platform(LazyPlatformConfigChangeTrigger),
531    Custom(BoxSystemProxyConfigChangeTrigger),
532}
533
534impl fmt::Debug for SystemProxyConfigChangeTrigger {
535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536        match self {
537            Self::Platform(_) => f.write_str("Platform(_)"),
538            Self::Custom(trigger) => f.debug_tuple("Custom").field(trigger).finish(),
539        }
540    }
541}
542
543#[derive(Debug, Default)]
544struct RefreshRequestState {
545    requested_generation: AtomicU64,
546    completed_generation: AtomicU64,
547}
548
549impl RefreshRequestState {
550    fn request(&self) -> u64 {
551        self.requested_generation
552            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |generation| {
553                Some(generation.saturating_add(1))
554            })
555            .unwrap_or_else(|generation| generation)
556            .saturating_add(1)
557    }
558}
559
560#[derive(Clone)]
561struct RefreshRequest {
562    state: Arc<RefreshRequestState>,
563    generation: u64,
564}
565
566impl RefreshRequest {
567    fn is_pending(&self) -> bool {
568        self.generation > self.state.completed_generation.load(Ordering::Acquire)
569    }
570
571    fn complete(&self) {
572        self.state
573            .completed_generation
574            .fetch_max(self.generation, Ordering::AcqRel);
575    }
576}
577
578#[derive(Clone)]
579struct SystemProxyConfigRefresh {
580    enabled: bool,
581    trigger: Option<SystemProxyConfigChangeTrigger>,
582    trigger_error_sink: Arc<dyn ErrorSink>,
583    requests: Arc<RefreshRequestState>,
584}
585
586impl fmt::Debug for SystemProxyConfigRefresh {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        f.debug_struct("SystemProxyConfigRefresh")
589            .field("enabled", &self.enabled)
590            .field("trigger", &self.trigger)
591            .finish_non_exhaustive()
592    }
593}
594
595impl Default for SystemProxyConfigRefresh {
596    fn default() -> Self {
597        Self {
598            enabled: true,
599            trigger: Some(SystemProxyConfigChangeTrigger::Platform(
600                LazyPlatformConfigChangeTrigger::default(),
601            )),
602            trigger_error_sink: Arc::new(TracingErrorSink::default()),
603            requests: Arc::new(RefreshRequestState::default()),
604        }
605    }
606}
607
608impl SystemProxyConfigRefresh {
609    async fn requested(&self) -> RefreshRequest {
610        let changed = if !self.enabled {
611            false
612        } else if let Some(trigger) = &self.trigger {
613            match trigger {
614                SystemProxyConfigChangeTrigger::Platform(trigger) => trigger.poll(),
615                SystemProxyConfigChangeTrigger::Custom(trigger) => trigger.serve(()).await,
616            }
617            .unwrap_or_else(|error| {
618                self.trigger_error_sink.sink_error(error);
619                false
620            })
621        } else {
622            false
623        };
624        let generation = if changed {
625            self.requests.request()
626        } else {
627            self.requests.requested_generation.load(Ordering::Acquire)
628        };
629        RefreshRequest {
630            state: self.requests.clone(),
631            generation,
632        }
633    }
634}
635
636#[derive(Debug)]
637struct IntoBoxErrorService<T>(T);
638
639impl<T> Service<()> for IntoBoxErrorService<T>
640where
641    T: Service<(), Output = bool>,
642    T::Error: Into<BoxError>,
643{
644    type Output = bool;
645    type Error = BoxError;
646
647    async fn serve(&self, (): ()) -> Result<Self::Output, Self::Error> {
648        self.0.serve(()).await.map_err(Into::into)
649    }
650}
651
652fn system_proxy_config_reader(
653    policy: SystemProxyInvalidBypassRulePolicy,
654) -> SystemProxyConfigReader {
655    BoxService::new(service_fn(move |()| async move {
656        SystemProxyConfig::try_from_system_with_invalid_bypass_rule_policy(policy).await
657    }))
658}
659
660struct SystemProxyConfigCache {
661    current: ArcSwapOption<SystemProxyConfig>,
662    ttl: Duration,
663    epoch: Instant,
664    refresh_after_nanos: AtomicU64,
665    cold_failure_generation: AtomicU64,
666    refresh_lock: tokio::sync::Mutex<()>,
667    reader: SystemProxyConfigReader,
668}
669
670impl fmt::Debug for SystemProxyConfigCache {
671    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
672        f.debug_struct("SystemProxyConfigCache")
673            .field("current", &self.current.load_full())
674            .field("ttl", &self.ttl)
675            .field(
676                "refresh_after_nanos",
677                &self.refresh_after_nanos.load(Ordering::Relaxed),
678            )
679            .field(
680                "cold_failure_generation",
681                &self.cold_failure_generation.load(Ordering::Relaxed),
682            )
683            .finish_non_exhaustive()
684    }
685}
686
687impl SystemProxyConfigCache {
688    fn new(
689        current: Option<SystemProxyConfig>,
690        ttl: Duration,
691        reader: SystemProxyConfigReader,
692    ) -> Self {
693        let epoch = Instant::now();
694        let refresh_after_nanos = if current.is_some() {
695            duration_nanos(ttl)
696        } else {
697            0
698        };
699        Self {
700            current: ArcSwapOption::from(current.map(Arc::new)),
701            ttl,
702            epoch,
703            refresh_after_nanos: AtomicU64::new(refresh_after_nanos),
704            cold_failure_generation: AtomicU64::new(0),
705            refresh_lock: tokio::sync::Mutex::new(()),
706            reader,
707        }
708    }
709
710    fn cached(&self) -> Option<Arc<SystemProxyConfig>> {
711        self.current.load_full()
712    }
713
714    fn is_fresh(&self, now: u64) -> bool {
715        now < self.refresh_after_nanos.load(Ordering::Acquire)
716    }
717
718    fn schedule_next_refresh(&self) {
719        let now = duration_nanos(self.epoch.elapsed());
720        self.refresh_after_nanos.store(
721            now.saturating_add(duration_nanos(self.ttl)),
722            Ordering::Release,
723        );
724    }
725
726    async fn refresh(
727        &self,
728        stale: Option<Arc<SystemProxyConfig>>,
729        load_error_policy: &LoadErrorPolicy,
730    ) -> Result<Arc<SystemProxyConfig>, BoxError> {
731        match self.reader.serve(()).await {
732            Ok(config) => {
733                let config = Arc::new(config);
734                self.current.store(Some(config.clone()));
735                self.schedule_next_refresh();
736                Ok(config)
737            }
738            Err(error) => {
739                let Some(stale) = stale else {
740                    load_error_policy.handle(error)?;
741                    let config = Arc::new(SystemProxyConfig::default());
742                    self.current.store(Some(config.clone()));
743                    self.schedule_next_refresh();
744                    return Ok(config);
745                };
746                self.schedule_next_refresh();
747                if let Err(error) = load_error_policy.handle(error) {
748                    rama_core::telemetry::tracing::warn!(
749                        error = %error,
750                        "failed to refresh system proxy configuration; retaining prior snapshot"
751                    );
752                }
753                Ok(stale)
754            }
755        }
756    }
757
758    async fn snapshot(
759        &self,
760        refresh_enabled: bool,
761        refresh_request: &RefreshRequest,
762        load_error_policy: &LoadErrorPolicy,
763    ) -> Result<Arc<SystemProxyConfig>, BoxError> {
764        let refresh_requested = refresh_request.is_pending();
765        let current = self.current.load_full();
766        let now = duration_nanos(self.epoch.elapsed());
767        if let Some(current) = current
768            .as_ref()
769            .filter(|_| !refresh_enabled || (!refresh_requested && self.is_fresh(now)))
770        {
771            return Ok(current.clone());
772        }
773
774        if let Some(stale) = current {
775            let Ok(_guard) = self.refresh_lock.try_lock() else {
776                return Ok(stale);
777            };
778            let latest = self.current.load_full().unwrap_or(stale);
779            let now = duration_nanos(self.epoch.elapsed());
780            if !refresh_request.is_pending() && self.is_fresh(now) {
781                return Ok(latest);
782            }
783            let result = self.refresh(Some(latest), load_error_policy).await;
784            refresh_request.complete();
785            return result;
786        }
787
788        // Waiters that observed the same cold state share one failed attempt.
789        // A later independent call still retries immediately, but a queue of
790        // requests cannot serially repeat one slow platform failure.
791        let observed_failure = self.cold_failure_generation.load(Ordering::Acquire);
792        let _guard = self.refresh_lock.lock().await;
793        if let Some(current) = self.current.load_full() {
794            if !refresh_request.is_pending() {
795                return Ok(current);
796            }
797            let result = self.refresh(Some(current), load_error_policy).await;
798            refresh_request.complete();
799            return result;
800        }
801        if observed_failure != self.cold_failure_generation.load(Ordering::Acquire) {
802            return Err(BoxError::from_static_str(
803                "system proxy configuration load failed while this request was waiting",
804            ));
805        }
806        let result = self.refresh(None, load_error_policy).await;
807        refresh_request.complete();
808        if result.is_err() {
809            self.cold_failure_generation.fetch_add(1, Ordering::Release);
810        }
811        result
812    }
813}
814
815fn duration_nanos(duration: Duration) -> u64 {
816    duration.as_nanos().try_into().unwrap_or(u64::MAX)
817}
818
819#[doc(hidden)]
820#[derive(Debug, Clone, Copy, Default)]
821pub struct SystemProxyPacDisabled;
822
823#[doc(hidden)]
824#[derive(Debug, Clone, Copy)]
825pub struct SystemProxyPacDisabledResolver;
826
827impl Service<Uri> for SystemProxyPacDisabled {
828    type Output = SystemProxyPacDisabledResolver;
829    type Error = std::convert::Infallible;
830
831    async fn serve(&self, _uri: Uri) -> Result<Self::Output, Self::Error> {
832        Ok(SystemProxyPacDisabledResolver)
833    }
834}
835
836impl Service<SystemProxyPacRequest> for SystemProxyPacDisabledResolver {
837    type Output = Option<ProxyRoutes>;
838    type Error = std::convert::Infallible;
839
840    async fn serve(&self, _request: SystemProxyPacRequest) -> Result<Self::Output, Self::Error> {
841        Ok(None)
842    }
843}
844
845/// Apply the operating system's proxy settings to client service inputs.
846///
847/// Existing [`ProxyRoute`] or [`ProxyRoutes`] extensions win by default. This
848/// makes the layer safe to place below explicit CLI/application proxy layers:
849/// a common priority chain is [`NoProxyEnvLayer`][crate::client::NoProxyEnvLayer],
850/// an explicit option, [`ProxyEnvLayer`][crate::client::ProxyEnvLayer], then
851/// this system layer. Use
852/// [`with_overwrite`][Self::with_overwrite] only when the system policy must
853/// replace a route already chosen by the caller.
854///
855/// Environment proxy variables are intentionally out of scope. Use
856/// [`ProxyEnvLayer`][crate::client::ProxyEnvLayer] for proxy variables and
857/// [`NoProxyEnvLayer`][crate::client::NoProxyEnvLayer] for bypass variables.
858///
859/// Configuration discovery is lazy. macOS, Windows, and Linux install a
860/// private native change monitor with the first load; the cache TTL remains a
861/// fallback on every platform. Applications can replace that monitor through
862/// [`with_config_change_trigger`][Self::with_config_change_trigger], retain
863/// TTL-only refreshes through
864/// [`without_config_change_trigger`][Self::without_config_change_trigger], or
865/// make the first snapshot immutable through
866/// [`with_config_refresh(false)`][Self::with_config_refresh].
867///
868/// Fixed proxies and bypass decisions publish one [`ProxyRoute`]. PAC verdicts
869/// retain their ordered [`ProxyRoutes`] plan even when it contains one entry.
870/// Compose [`ProxyRoutesLayer`][crate::client::ProxyRoutesLayer] after all route
871/// selectors and before route-aware middleware.
872///
873/// A configured PAC URI is used only after a service is supplied through
874/// [`with_pac_service`][Self::with_pac_service]. Without one the layer uses a
875/// fixed proxy from the same system snapshot when available, or leaves the
876/// request unchanged. Factory, fetch, or evaluation errors fail the request
877/// instead of silently bypassing the system proxy.
878#[derive(Clone)]
879pub struct SystemProxyLayer<P = SystemProxyPacDisabled> {
880    config: Arc<SystemProxyConfigCache>,
881    refresh: SystemProxyConfigRefresh,
882    load_error_policy: LoadErrorPolicy,
883    pac: P,
884    pac_enabled: bool,
885    overwrite: bool,
886}
887
888impl<P: fmt::Debug> fmt::Debug for SystemProxyLayer<P> {
889    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
890        f.debug_struct("SystemProxyLayer")
891            .field("config", &self.config)
892            .field("refresh", &self.refresh)
893            .field("load_error_policy", &self.load_error_policy)
894            .field("pac", &self.pac)
895            .field("pac_enabled", &self.pac_enabled)
896            .field("overwrite", &self.overwrite)
897            .finish()
898    }
899}
900
901impl SystemProxyLayer {
902    /// Create a lazy system-proxy layer without reading platform settings.
903    ///
904    /// The first unrouted request loads the settings. Call
905    /// [`warm_up`][Self::warm_up] to perform that asynchronous load eagerly.
906    #[must_use]
907    pub fn new() -> Self {
908        Self::new_with_ttl_and_invalid_bypass_rule_policy(
909            DEFAULT_SYSTEM_PROXY_CONFIG_TTL,
910            SystemProxyInvalidBypassRulePolicy::Ignore,
911        )
912    }
913
914    /// Create a lazy system-proxy layer with a custom cache TTL.
915    #[must_use]
916    pub fn new_with_ttl(ttl: Duration) -> Self {
917        Self::new_with_ttl_and_invalid_bypass_rule_policy(
918            ttl,
919            SystemProxyInvalidBypassRulePolicy::Ignore,
920        )
921    }
922
923    /// Create a lazy layer with explicit refresh and bypass-rule policies.
924    #[must_use]
925    pub fn new_with_ttl_and_invalid_bypass_rule_policy(
926        ttl: Duration,
927        policy: SystemProxyInvalidBypassRulePolicy,
928    ) -> Self {
929        Self::new_with_reader(ttl, system_proxy_config_reader(policy))
930    }
931
932    fn new_with_reader(ttl: Duration, reader: SystemProxyConfigReader) -> Self {
933        Self {
934            config: Arc::new(SystemProxyConfigCache::new(None, ttl, reader)),
935            refresh: SystemProxyConfigRefresh::default(),
936            load_error_policy: LoadErrorPolicy::Reject,
937            pac: SystemProxyPacDisabled,
938            pac_enabled: false,
939            overwrite: false,
940        }
941    }
942
943    /// Create a layer from a cached operating-system proxy snapshot.
944    ///
945    /// The supplied snapshot is used immediately and refreshed from the
946    /// operating system after [`DEFAULT_SYSTEM_PROXY_CONFIG_TTL`]. Use
947    /// [`try_from_system`][Self::try_from_system] to start with a fresh read.
948    #[must_use]
949    pub fn from_cached(config: SystemProxyConfig) -> Self {
950        Self::from_cached_with_invalid_bypass_rule_policy(
951            config,
952            SystemProxyInvalidBypassRulePolicy::Ignore,
953        )
954    }
955
956    /// Create a layer from a cached operating-system proxy snapshot with an
957    /// explicit invalid bypass-rule policy for subsequent refreshes.
958    #[must_use]
959    pub fn from_cached_with_invalid_bypass_rule_policy(
960        config: SystemProxyConfig,
961        policy: SystemProxyInvalidBypassRulePolicy,
962    ) -> Self {
963        Self::from_cached_with_reader(
964            config,
965            DEFAULT_SYSTEM_PROXY_CONFIG_TTL,
966            system_proxy_config_reader(policy),
967        )
968    }
969
970    fn from_cached_with_reader(
971        config: SystemProxyConfig,
972        ttl: Duration,
973        reader: SystemProxyConfigReader,
974    ) -> Self {
975        Self {
976            config: Arc::new(SystemProxyConfigCache::new(Some(config), ttl, reader)),
977            refresh: SystemProxyConfigRefresh::default(),
978            load_error_policy: LoadErrorPolicy::Reject,
979            pac: SystemProxyPacDisabled,
980            pac_enabled: false,
981            overwrite: false,
982        }
983    }
984
985    /// Create a layer and asynchronously warm its system proxy snapshot.
986    pub async fn try_from_system() -> Result<Self, BoxError> {
987        Self::try_from_system_with_invalid_bypass_rule_policy(
988            SystemProxyInvalidBypassRulePolicy::Ignore,
989        )
990        .await
991    }
992
993    /// Create a layer from the current operating system proxy settings using
994    /// an explicit invalid bypass-rule policy.
995    pub async fn try_from_system_with_invalid_bypass_rule_policy(
996        policy: SystemProxyInvalidBypassRulePolicy,
997    ) -> Result<Self, BoxError> {
998        Self::try_from_system_with_ttl_and_invalid_bypass_rule_policy(
999            DEFAULT_SYSTEM_PROXY_CONFIG_TTL,
1000            policy,
1001        )
1002        .await
1003    }
1004
1005    /// Create a refreshing layer from the current operating system settings.
1006    ///
1007    /// The initial read is awaited. Once `ttl` has elapsed, one request awaits
1008    /// the refresh while concurrent requests continue using the prior
1009    /// snapshot. A failed refresh retains that snapshot and is retried after
1010    /// another `ttl` interval.
1011    pub async fn try_from_system_with_ttl(ttl: Duration) -> Result<Self, BoxError> {
1012        Self::try_from_system_with_ttl_and_invalid_bypass_rule_policy(
1013            ttl,
1014            SystemProxyInvalidBypassRulePolicy::Ignore,
1015        )
1016        .await
1017    }
1018
1019    /// Create a refreshing layer with explicit refresh and invalid bypass-rule
1020    /// policies.
1021    pub async fn try_from_system_with_ttl_and_invalid_bypass_rule_policy(
1022        ttl: Duration,
1023        policy: SystemProxyInvalidBypassRulePolicy,
1024    ) -> Result<Self, BoxError> {
1025        Self::try_from_system_with_reader(ttl, system_proxy_config_reader(policy)).await
1026    }
1027
1028    async fn try_from_system_with_reader(
1029        ttl: Duration,
1030        reader: SystemProxyConfigReader,
1031    ) -> Result<Self, BoxError> {
1032        let layer = Self::new_with_reader(ttl, reader);
1033        layer.warm_up().await?;
1034        Ok(layer)
1035    }
1036}
1037
1038impl Default for SystemProxyLayer {
1039    fn default() -> Self {
1040        Self::new()
1041    }
1042}
1043
1044impl<P> SystemProxyLayer<P> {
1045    /// Load and return the current operating system proxy configuration.
1046    ///
1047    /// The first call performs lazy discovery. A native or custom change
1048    /// trigger can request an early refresh. Once the cache TTL expires, one
1049    /// caller awaits a refresh while concurrent callers use the stale
1050    /// snapshot.
1051    pub async fn config(&self) -> Result<Arc<SystemProxyConfig>, BoxError> {
1052        let refresh_request = self.refresh.requested().await;
1053        self.config
1054            .snapshot(
1055                self.refresh.enabled,
1056                &refresh_request,
1057                &self.load_error_policy,
1058            )
1059            .await
1060    }
1061
1062    /// Return the cached snapshot without loading or refreshing it.
1063    #[must_use]
1064    pub fn cached_config(&self) -> Option<Arc<SystemProxyConfig>> {
1065        self.config.cached()
1066    }
1067
1068    /// Asynchronously populate the cache before serving requests.
1069    pub async fn warm_up(&self) -> Result<(), BoxError> {
1070        self.config().await.map(drop)
1071    }
1072
1073    /// Supply a PAC resolver factory.
1074    ///
1075    /// The factory can be consulted for every request selected for PAC
1076    /// evaluation. Implementations should therefore reuse resolver state for
1077    /// the same script URI. Factory and resolver errors fail the request.
1078    #[must_use]
1079    pub fn with_pac_service<Q>(self, pac: Q) -> SystemProxyLayer<Q> {
1080        SystemProxyLayer {
1081            config: self.config,
1082            refresh: self.refresh,
1083            load_error_policy: self.load_error_policy,
1084            pac,
1085            pac_enabled: true,
1086            overwrite: self.overwrite,
1087        }
1088    }
1089
1090    generate_set_and_with! {
1091        /// Handle system configuration load errors with a sink.
1092        ///
1093        /// By default, an initial discovery failure rejects the request. With
1094        /// this opt-in policy, the error is sent to `error_sink` and an empty
1095        /// snapshot is cached for the configured TTL. Refresh failures retain
1096        /// the previous snapshot and are sent to the same sink.
1097        pub fn load_error_sink(mut self, error_sink: impl ErrorSink) -> Self {
1098            self.load_error_policy = LoadErrorPolicy::Handle(Arc::new(error_sink));
1099            self
1100        }
1101    }
1102
1103    generate_set_and_with! {
1104        /// Enable periodic and change-triggered configuration refreshes.
1105        ///
1106        /// Disabling refresh keeps the first loaded snapshot immutable. A
1107        /// layer created with [`from_cached`][Self::from_cached] therefore
1108        /// performs no operating-system reads when refresh is disabled.
1109        pub fn config_refresh(mut self, config_refresh: bool) -> Self {
1110            self.refresh.enabled = config_refresh;
1111            self
1112        }
1113    }
1114
1115    generate_set_and_with! {
1116        /// Replace the default platform configuration-change trigger.
1117        ///
1118        /// The service is polled before configuration access. Returning `true`
1119        /// requests an immediate refresh of an existing snapshot; returning
1120        /// `false` leaves the TTL as the fallback. Trigger errors are sent to the
1121        /// configured error sink and also fall back to the TTL.
1122        pub fn config_change_trigger(
1123            mut self,
1124            trigger: impl Service<(), Output = bool, Error: Into<BoxError>>,
1125        ) -> Self {
1126            self.refresh.trigger = Some(SystemProxyConfigChangeTrigger::Custom(BoxService::new(
1127                IntoBoxErrorService(trigger),
1128            )));
1129            self
1130        }
1131    }
1132
1133    /// Disable early change notifications while retaining TTL refreshes.
1134    #[must_use]
1135    pub fn without_config_change_trigger(mut self) -> Self {
1136        self.refresh.trigger = None;
1137        self
1138    }
1139
1140    /// Disable early change notifications while retaining TTL refreshes.
1141    pub fn unset_config_change_trigger(&mut self) -> &mut Self {
1142        self.refresh.trigger = None;
1143        self
1144    }
1145
1146    generate_set_and_with! {
1147        /// Replace the sink for configuration-change trigger errors.
1148        pub fn config_change_trigger_error_sink(
1149            mut self,
1150            error_sink: impl ErrorSink,
1151        ) -> Self {
1152            self.refresh.trigger_error_sink = Arc::new(error_sink);
1153            self
1154        }
1155    }
1156
1157    generate_set_and_with! {
1158        /// Replace an existing route decision (defaults to `false`).
1159        pub fn overwrite(mut self, overwrite: bool) -> Self {
1160            self.overwrite = overwrite;
1161            self
1162        }
1163    }
1164}
1165
1166impl<S, P> Layer<S> for SystemProxyLayer<P>
1167where
1168    P: Clone,
1169{
1170    type Service = SystemProxyService<S, P>;
1171
1172    fn layer(&self, inner: S) -> Self::Service {
1173        SystemProxyService {
1174            inner,
1175            layer: self.clone(),
1176        }
1177    }
1178
1179    fn into_layer(self, inner: S) -> Self::Service {
1180        SystemProxyService { inner, layer: self }
1181    }
1182}
1183
1184/// See [`SystemProxyLayer`].
1185#[derive(Debug, Clone)]
1186pub struct SystemProxyService<S, P = SystemProxyPacDisabled> {
1187    inner: S,
1188    layer: SystemProxyLayer<P>,
1189}
1190
1191impl<S, P> SystemProxyService<S, P> {
1192    /// Borrow the wrapped service.
1193    #[must_use]
1194    pub const fn inner(&self) -> &S {
1195        &self.inner
1196    }
1197
1198    /// Mutably borrow the wrapped service.
1199    #[must_use]
1200    pub fn inner_mut(&mut self) -> &mut S {
1201        &mut self.inner
1202    }
1203
1204    /// Consume this service and return the wrapped service.
1205    #[must_use]
1206    pub fn into_inner(self) -> S {
1207        self.inner
1208    }
1209}
1210
1211impl<S, P, Input> Service<Input> for SystemProxyService<S, P>
1212where
1213    S: Service<Input, Error: Into<BoxError>>,
1214    P: SystemProxyPacService,
1215    Input: UriInputExt + AuthorityInputExt + ProtocolInputExt + ExtensionsRef + Send + 'static,
1216{
1217    type Output = S::Output;
1218    type Error = BoxError;
1219
1220    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
1221        if !self.layer.overwrite && is_already_routed(&input) {
1222            return self.inner.serve(input).await.map_err(Into::into);
1223        }
1224        let config = self.layer.config().await?;
1225        if config.is_empty() {
1226            return self.inner.serve(input).await.map_err(Into::into);
1227        }
1228
1229        let mut normalized_uri = None;
1230        let decision = if self.layer.pac_enabled && config.pac_uri().is_some() {
1231            let uri = absolute_uri(&input)?;
1232            let decision = config.decision(&uri);
1233            normalized_uri = Some(uri);
1234            decision
1235        } else {
1236            let protocol = request_protocol(&input);
1237            let authority = input
1238                .uri()
1239                .authority()
1240                .map(|authority| authority.into_owned().address)
1241                .or_else(|| input.authority());
1242            if let Some(authority) = authority {
1243                config
1244                    .fixed_route_for(
1245                        Some(&protocol),
1246                        authority.host.view(),
1247                        authority.port_u16().or_else(|| protocol.default_port()),
1248                    )
1249                    .map(SystemProxyDecision::Route)
1250                    .unwrap_or(SystemProxyDecision::None)
1251            } else {
1252                rama_core::telemetry::tracing::debug!(
1253                    "fixed system proxy cannot route an input without an authority"
1254                );
1255                SystemProxyDecision::None
1256            }
1257        };
1258        match decision {
1259            SystemProxyDecision::Pac(pac_uri) => {
1260                let Some(uri) = normalized_uri else {
1261                    return Err(BoxError::from_static_str(
1262                        "system PAC decision is missing its normalized request URI",
1263                    ));
1264                };
1265                let resolver = self
1266                    .layer
1267                    .pac
1268                    .serve(pac_uri)
1269                    .await
1270                    .context("create system PAC resolver")?;
1271                match resolver
1272                    .serve(SystemProxyPacRequest::new(
1273                        input.extensions().clone(),
1274                        uri.clone(),
1275                    )?)
1276                    .await
1277                    .context("resolve system PAC routes")?
1278                {
1279                    Some(routes) => {
1280                        input.extensions().insert(routes);
1281                    }
1282                    None => {
1283                        if let Some(route) = config.fixed_route(&uri) {
1284                            input.extensions().insert(route);
1285                        }
1286                    }
1287                }
1288            }
1289            SystemProxyDecision::Route(route) => {
1290                input.extensions().insert(route);
1291            }
1292            SystemProxyDecision::None => {}
1293        }
1294        self.inner.serve(input).await.map_err(Into::into)
1295    }
1296}
1297
1298pub(super) fn absolute_uri<I>(input: &I) -> Result<Uri, BoxError>
1299where
1300    I: UriInputExt + AuthorityInputExt + ProtocolInputExt,
1301{
1302    let uri = input.uri();
1303    let protocol = request_protocol(input);
1304    proxy_request_uri(uri, input.authority(), protocol)
1305}
1306
1307pub(super) fn request_protocol<I>(input: &I) -> Protocol
1308where
1309    I: UriInputExt + ProtocolInputExt,
1310{
1311    input
1312        .uri()
1313        .scheme()
1314        .cloned()
1315        // Authority-form is the request-target form of CONNECT. The tunnel is
1316        // opaque and overwhelmingly TLS, so match the HTTP PAC layer and show
1317        // it as HTTPS regardless of the named port.
1318        .or_else(|| input.uri().authority().map(|_| Protocol::HTTPS))
1319        .or_else(|| input.protocol().cloned())
1320        .unwrap_or(Protocol::HTTP)
1321}
1322
1323/// Normalize a request target for fixed-proxy selection and PAC evaluation.
1324///
1325/// The result is absolute, has a root path when no path was supplied, and
1326/// omits the protocol's default port. The URI's own authority wins over the
1327/// fallback authority supplied by request metadata.
1328pub fn proxy_request_uri(
1329    uri: &Uri,
1330    fallback_authority: Option<HostWithOptPort>,
1331    protocol: Protocol,
1332) -> Result<Uri, BoxError> {
1333    let authority = uri
1334        .authority()
1335        .map(|authority| authority.into_owned().address)
1336        .or(fallback_authority)
1337        .ok_or_else(|| BoxError::from_static_str("request has no resolvable authority"))?
1338        .without_default_port_for(Some(&protocol));
1339
1340    let mut uri = if uri.is_asterisk() {
1341        Uri::from_authority(protocol, authority)
1342    } else {
1343        uri.clone()
1344            .with_authority(Authority::from(authority))
1345            .with_scheme(protocol)
1346    };
1347    uri.ensure_path_or_root();
1348    Ok(uri)
1349}
1350
1351pub(super) fn is_already_routed(input: &impl ExtensionsRef) -> bool {
1352    input.extensions().contains::<ProxyRoute>() || input.extensions().contains::<ProxyRoutes>()
1353}
1354
1355#[cfg(any(
1356    test,
1357    target_vendor = "apple",
1358    target_os = "android",
1359    target_os = "linux",
1360    target_os = "freebsd",
1361    target_os = "netbsd",
1362    target_os = "openbsd",
1363    target_os = "dragonfly"
1364))]
1365pub(super) fn proxy_address(
1366    protocol: Protocol,
1367    host: impl AsRef<str>,
1368    port: u16,
1369) -> Result<ProxyAddress, BoxError> {
1370    let value = host.as_ref().trim();
1371    let host = match Host::try_from(value) {
1372        Ok(host) => host,
1373        Err(error) if value.contains("://") => value
1374            .parse::<Uri>()
1375            .context("parse system proxy host URI")?
1376            .host()
1377            .map(|host| host.into_owned())
1378            .ok_or(error)
1379            .context("parse system proxy host")?,
1380        Err(error) => return Err(error).context("parse system proxy host"),
1381    };
1382    Ok(ProxyAddress {
1383        protocol: Some(protocol),
1384        address: HostWithPort::new(host, port),
1385        credential: None,
1386    })
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391    use std::convert::Infallible;
1392
1393    use parking_lot::Mutex;
1394    use rama_core::{
1395        extensions::{Extension, FromExtensions},
1396        service::service_fn,
1397    };
1398
1399    use super::*;
1400
1401    #[derive(Debug, Clone, Extension)]
1402    struct Marker(&'static str);
1403
1404    #[derive(FromExtensions)]
1405    enum RecordedProxyDecision {
1406        Route(Arc<ProxyRoute>),
1407        Routes(Arc<ProxyRoutes>),
1408    }
1409
1410    #[derive(Debug, Clone)]
1411    struct TestInput {
1412        uri: Uri,
1413        protocol: Option<Protocol>,
1414        authority: Option<crate::address::HostWithOptPort>,
1415        extensions: Extensions,
1416    }
1417
1418    impl TestInput {
1419        fn new(uri: &str) -> Self {
1420            Self {
1421                uri: uri.parse().unwrap(),
1422                protocol: None,
1423                authority: None,
1424                extensions: Extensions::new(),
1425            }
1426        }
1427
1428        fn origin_form(uri: &str, protocol: Protocol, authority: &str) -> Self {
1429            Self {
1430                uri: uri.parse().unwrap(),
1431                protocol: Some(protocol),
1432                authority: Some(authority.parse().unwrap()),
1433                extensions: Extensions::new(),
1434            }
1435        }
1436
1437        fn authority_form(authority: &str) -> Self {
1438            Self {
1439                uri: Uri::parse_authority_form(authority).unwrap(),
1440                protocol: None,
1441                authority: None,
1442                extensions: Extensions::new(),
1443            }
1444        }
1445    }
1446
1447    impl UriInputExt for TestInput {
1448        fn uri(&self) -> &Uri {
1449            &self.uri
1450        }
1451    }
1452
1453    impl AuthorityInputExt for TestInput {
1454        fn authority(&self) -> Option<crate::address::HostWithOptPort> {
1455            self.authority.clone().or_else(|| {
1456                self.uri
1457                    .authority()
1458                    .map(|authority| authority.into_owned().address)
1459            })
1460        }
1461    }
1462
1463    impl ProtocolInputExt for TestInput {
1464        fn protocol(&self) -> Option<&Protocol> {
1465            self.protocol.as_ref().or_else(|| self.uri.scheme())
1466        }
1467    }
1468
1469    impl ExtensionsRef for TestInput {
1470        fn extensions(&self) -> &Extensions {
1471            &self.extensions
1472        }
1473    }
1474
1475    fn proxy(protocol: Protocol, host: &'static str, port: u16) -> ProxyAddress {
1476        proxy_address(protocol, host, port).unwrap()
1477    }
1478
1479    fn recorder() -> (
1480        impl Service<TestInput, Output = (), Error = Infallible> + Clone,
1481        Arc<Mutex<Vec<Option<ProxyRoutes>>>>,
1482    ) {
1483        let seen = Arc::new(Mutex::new(Vec::new()));
1484        let service = service_fn({
1485            let seen = seen.clone();
1486            move |input: TestInput| {
1487                let routes = match RecordedProxyDecision::from_extensions(&input.extensions) {
1488                    Some(RecordedProxyDecision::Route(route)) => {
1489                        Some(ProxyRoutes::from(route.as_ref().clone()))
1490                    }
1491                    Some(RecordedProxyDecision::Routes(routes)) => Some(routes.as_ref().clone()),
1492                    None => None,
1493                };
1494                seen.lock().push(routes);
1495                async { Ok::<_, Infallible>(()) }
1496            }
1497        });
1498        (service, seen)
1499    }
1500
1501    #[tokio::test]
1502    async fn fixed_proxies_are_selected_by_destination_scheme() {
1503        let config = SystemProxyConfig::default()
1504            .with_http_proxy(proxy(Protocol::HTTP, "http.proxy", 8080))
1505            .with_https_proxy(proxy(Protocol::HTTP, "https.proxy", 8443));
1506        let (inner, seen) = recorder();
1507        let service = SystemProxyLayer::from_cached(config).into_layer(inner);
1508
1509        service
1510            .serve(TestInput::new("http://example.com/"))
1511            .await
1512            .unwrap();
1513        service
1514            .serve(TestInput::new("https://example.com/"))
1515            .await
1516            .unwrap();
1517        service
1518            .serve(TestInput::new("ws://example.com/"))
1519            .await
1520            .unwrap();
1521        service
1522            .serve(TestInput::new("wss://example.com/"))
1523            .await
1524            .unwrap();
1525
1526        let seen = seen.lock();
1527        assert_eq!(
1528            seen[0].as_ref().unwrap().as_slice()[0]
1529                .proxy_address()
1530                .unwrap()
1531                .address
1532                .host
1533                .to_str(),
1534            "http.proxy"
1535        );
1536        assert_eq!(
1537            seen[1].as_ref().unwrap().as_slice()[0]
1538                .proxy_address()
1539                .unwrap()
1540                .address
1541                .host
1542                .to_str(),
1543            "https.proxy"
1544        );
1545        assert_eq!(
1546            seen[2].as_ref().unwrap().as_slice()[0]
1547                .proxy_address()
1548                .unwrap()
1549                .address
1550                .host
1551                .to_str(),
1552            "http.proxy"
1553        );
1554        assert_eq!(
1555            seen[3].as_ref().unwrap().as_slice()[0]
1556                .proxy_address()
1557                .unwrap()
1558                .address
1559                .host
1560                .to_str(),
1561            "https.proxy"
1562        );
1563    }
1564
1565    #[tokio::test]
1566    async fn fixed_and_bypass_decisions_publish_singular_routes() {
1567        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1568            Protocol::HTTP,
1569            "system.proxy",
1570            8080,
1571        ));
1572        let service = SystemProxyLayer::from_cached(config).into_layer(service_fn(
1573            async |input: TestInput| Ok::<_, Infallible>(input),
1574        ));
1575
1576        let proxied = service
1577            .serve(TestInput::new("http://example.com/"))
1578            .await
1579            .unwrap();
1580        assert_eq!(
1581            proxied
1582                .extensions
1583                .get_ref::<ProxyRoute>()
1584                .and_then(ProxyRoute::proxy_address)
1585                .map(|address| address.address.host.to_string()),
1586            Some("system.proxy".to_owned())
1587        );
1588        assert!(!proxied.extensions.contains::<ProxyRoutes>());
1589
1590        let bypassed = service
1591            .serve(TestInput::new("http://localhost/"))
1592            .await
1593            .unwrap();
1594        assert_eq!(
1595            bypassed.extensions.get_ref::<ProxyRoute>(),
1596            Some(&ProxyRoute::Direct)
1597        );
1598        assert!(!bypassed.extensions.contains::<ProxyRoutes>());
1599    }
1600
1601    #[tokio::test]
1602    async fn system_decisions_override_configured_route_defaults() {
1603        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1604            Protocol::HTTP,
1605            "system.proxy",
1606            8080,
1607        ));
1608        let service = SystemProxyLayer::from_cached(config).into_layer(
1609            crate::client::ProxyRoutesLayer::with_routes(ProxyRoute::Proxy(proxy(
1610                Protocol::HTTP,
1611                "default.proxy",
1612                8080,
1613            )))
1614            .into_layer(service_fn(async |input: TestInput| {
1615                Ok::<_, Infallible>(input)
1616            })),
1617        );
1618
1619        let proxied = service
1620            .serve(TestInput::new("http://example.com/"))
1621            .await
1622            .unwrap();
1623        assert_eq!(
1624            proxied
1625                .extensions
1626                .get_ref::<ProxyRoute>()
1627                .and_then(ProxyRoute::proxy_address)
1628                .map(|address| address.address.host.to_string()),
1629            Some("system.proxy".to_owned())
1630        );
1631
1632        let bypassed = service
1633            .serve(TestInput::new("http://localhost/"))
1634            .await
1635            .unwrap();
1636        assert_eq!(
1637            bypassed.extensions.get_ref::<ProxyRoute>(),
1638            Some(&ProxyRoute::Direct)
1639        );
1640    }
1641
1642    #[tokio::test]
1643    async fn socks_is_the_scheme_independent_fallback() {
1644        let config = SystemProxyConfig::default().with_socks5_proxy(proxy(
1645            Protocol::SOCKS5,
1646            "socks.proxy",
1647            1080,
1648        ));
1649        let (inner, seen) = recorder();
1650        let service = SystemProxyLayer::from_cached(config).into_layer(inner);
1651
1652        service
1653            .serve(TestInput::new("https://example.com/"))
1654            .await
1655            .unwrap();
1656        service
1657            .serve(TestInput::new("ftp://example.com/file"))
1658            .await
1659            .unwrap();
1660
1661        let seen = seen.lock();
1662        for routes in seen.iter() {
1663            let address = routes.as_ref().unwrap().as_slice()[0]
1664                .proxy_address()
1665                .unwrap();
1666            assert_eq!(address.protocol, Some(Protocol::SOCKS5));
1667        }
1668    }
1669
1670    #[tokio::test]
1671    async fn scheme_specific_proxy_does_not_capture_other_protocols() {
1672        let config = SystemProxyConfig::default()
1673            .with_http_proxy(proxy(Protocol::HTTP, "http.proxy", 8080))
1674            .with_bypass(["example.com"]);
1675        let (inner, seen) = recorder();
1676
1677        SystemProxyLayer::from_cached(config)
1678            .into_layer(inner)
1679            .serve(TestInput::new("ftp://example.com/file"))
1680            .await
1681            .unwrap();
1682
1683        assert!(seen.lock()[0].is_none());
1684    }
1685
1686    #[tokio::test]
1687    async fn empty_config_does_not_require_routing_metadata() {
1688        let (inner, seen) = recorder();
1689
1690        SystemProxyLayer::from_cached(SystemProxyConfig::default())
1691            .into_layer(inner)
1692            .serve(TestInput::new("/relative"))
1693            .await
1694            .unwrap();
1695
1696        assert!(seen.lock()[0].is_none());
1697    }
1698
1699    #[tokio::test]
1700    async fn active_fixed_config_passes_input_without_an_authority() {
1701        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1702            Protocol::HTTP,
1703            "system.proxy",
1704            8080,
1705        ));
1706        let (inner, seen) = recorder();
1707
1708        SystemProxyLayer::from_cached(config)
1709            .into_layer(inner)
1710            .serve(TestInput::new("/relative"))
1711            .await
1712            .unwrap();
1713
1714        assert!(seen.lock()[0].is_none());
1715    }
1716
1717    #[tokio::test]
1718    async fn input_without_a_protocol_defaults_to_http() {
1719        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1720            Protocol::HTTP,
1721            "system.proxy",
1722            8080,
1723        ));
1724        let (inner, seen) = recorder();
1725        let mut input = TestInput::new("/relative");
1726        input.authority = Some("example.com".parse().unwrap());
1727
1728        SystemProxyLayer::from_cached(config)
1729            .into_layer(inner)
1730            .serve(input)
1731            .await
1732            .unwrap();
1733
1734        assert!(matches!(
1735            seen.lock()[0].as_ref().unwrap().as_slice(),
1736            [ProxyRoute::Proxy(_)]
1737        ));
1738    }
1739
1740    #[tokio::test]
1741    async fn existing_route_wins_unless_overwrite_is_enabled() {
1742        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1743            Protocol::HTTP,
1744            "system.proxy",
1745            8080,
1746        ));
1747        let (inner, seen) = recorder();
1748        let request = TestInput::new("http://example.com/");
1749        request.extensions.insert(ProxyRoutes::from(proxy(
1750            Protocol::HTTP,
1751            "explicit.proxy",
1752            9000,
1753        )));
1754
1755        SystemProxyLayer::from_cached(config.clone())
1756            .into_layer(inner.clone())
1757            .serve(request.clone())
1758            .await
1759            .unwrap();
1760        SystemProxyLayer::from_cached(config)
1761            .with_overwrite(true)
1762            .into_layer(inner)
1763            .serve(request)
1764            .await
1765            .unwrap();
1766
1767        let seen = seen.lock();
1768        let hosts: Vec<_> = seen
1769            .iter()
1770            .map(|routes| {
1771                routes.as_ref().unwrap().as_slice()[0]
1772                    .proxy_address()
1773                    .unwrap()
1774                    .address
1775                    .host
1776                    .to_str()
1777                    .into_owned()
1778            })
1779            .collect();
1780        assert_eq!(hosts, ["explicit.proxy", "system.proxy"]);
1781    }
1782
1783    #[tokio::test]
1784    async fn overwrite_route_takes_priority_over_an_existing_route() {
1785        let config = SystemProxyConfig::default().with_http_proxy(proxy(
1786            Protocol::HTTP,
1787            "system.proxy",
1788            8080,
1789        ));
1790        let request = TestInput::new("http://example.com/");
1791        request.extensions.insert(ProxyRoute::Direct);
1792        let (inner, seen) = recorder();
1793
1794        SystemProxyLayer::from_cached(config)
1795            .with_overwrite(true)
1796            .into_layer(inner)
1797            .serve(request)
1798            .await
1799            .unwrap();
1800
1801        let seen = seen.lock();
1802        let routes = seen[0].as_ref().unwrap();
1803        assert_eq!(
1804            routes.as_slice()[0]
1805                .proxy_address()
1806                .unwrap()
1807                .address
1808                .host
1809                .to_str(),
1810            "system.proxy"
1811        );
1812    }
1813
1814    #[tokio::test]
1815    async fn pac_receives_full_uri_and_cloned_extensions() {
1816        let pac_uri: Uri = "http://config.example/proxy.pac".parse().unwrap();
1817        let factory_seen = Arc::new(Mutex::new(Vec::new()));
1818        let resolver_seen = Arc::new(Mutex::new(Vec::new()));
1819        let factory = service_fn({
1820            let factory_seen = factory_seen.clone();
1821            let resolver_seen = resolver_seen.clone();
1822            move |uri: Uri| {
1823                factory_seen.lock().push(uri);
1824                let resolver_seen = resolver_seen.clone();
1825                async move {
1826                    Ok::<_, Infallible>(service_fn(move |request: SystemProxyPacRequest| {
1827                        resolver_seen.lock().push((
1828                            request.uri.clone(),
1829                            request.extensions().get_ref::<Marker>().cloned(),
1830                        ));
1831                        async move {
1832                            Ok::<_, Infallible>(Some(ProxyRoutes::from(proxy(
1833                                Protocol::HTTP,
1834                                "pac.proxy",
1835                                8080,
1836                            ))))
1837                        }
1838                    }))
1839                }
1840            }
1841        });
1842        let config = SystemProxyConfig::default().with_pac_uri(pac_uri.clone());
1843        let request = TestInput::new("https://example.com/private?q=1");
1844        request.extensions.insert(Marker("kept"));
1845        let inner = service_fn(async |input: TestInput| Ok::<_, Infallible>(input));
1846
1847        let output = SystemProxyLayer::from_cached(config)
1848            .with_pac_service(factory)
1849            .into_layer(inner)
1850            .serve(request)
1851            .await
1852            .unwrap();
1853
1854        assert_eq!(factory_seen.lock().as_slice(), [pac_uri]);
1855        let resolved = resolver_seen.lock();
1856        assert_eq!(resolved[0].0.to_string(), "https://example.com/private?q=1");
1857        assert_eq!(resolved[0].1.as_ref().unwrap().0, "kept");
1858        assert_eq!(
1859            output
1860                .extensions
1861                .get_ref::<ProxyRoutes>()
1862                .unwrap()
1863                .as_slice()[0]
1864                .proxy_address()
1865                .unwrap()
1866                .address
1867                .host
1868                .to_str(),
1869            "pac.proxy"
1870        );
1871        assert!(output.extensions.get_ref::<ProxyRoute>().is_none());
1872    }
1873
1874    #[tokio::test]
1875    async fn pac_factory_errors_fail_the_request_with_context() {
1876        let factory = service_fn(|_uri: Uri| async {
1877            Err::<SystemProxyPacDisabledResolver, _>(std::io::Error::other("PAC fetch failed"))
1878        });
1879        let config = SystemProxyConfig::default()
1880            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
1881        let (inner, seen) = recorder();
1882
1883        let error = SystemProxyLayer::from_cached(config)
1884            .with_pac_service(factory)
1885            .into_layer(inner)
1886            .serve(TestInput::new("https://example.com/"))
1887            .await
1888            .unwrap_err();
1889
1890        assert!(error.to_string().contains("create system PAC resolver"));
1891        assert!(seen.lock().is_empty());
1892    }
1893
1894    #[tokio::test]
1895    async fn pac_resolver_errors_fail_the_request_with_context() {
1896        let factory = service_fn(|_uri: Uri| async {
1897            Ok::<_, Infallible>(service_fn(|_request: SystemProxyPacRequest| async {
1898                Err::<Option<ProxyRoutes>, _>(std::io::Error::other("PAC evaluation failed"))
1899            }))
1900        });
1901        let config = SystemProxyConfig::default()
1902            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
1903        let (inner, seen) = recorder();
1904
1905        let error = SystemProxyLayer::from_cached(config)
1906            .with_pac_service(factory)
1907            .into_layer(inner)
1908            .serve(TestInput::new("https://example.com/"))
1909            .await
1910            .unwrap_err();
1911
1912        assert!(error.to_string().contains("resolve system PAC routes"));
1913        assert!(seen.lock().is_empty());
1914    }
1915
1916    #[tokio::test]
1917    async fn pac_receives_an_absolute_uri_for_origin_form_input() {
1918        let received = Arc::new(Mutex::new(None));
1919        let factory = service_fn({
1920            let received = received.clone();
1921            move |_uri: Uri| {
1922                let received = received.clone();
1923                async move {
1924                    Ok::<_, Infallible>(service_fn(move |request: SystemProxyPacRequest| {
1925                        *received.lock() = Some(request.uri);
1926                        async { Ok::<_, Infallible>(None) }
1927                    }))
1928                }
1929            }
1930        });
1931        let config = SystemProxyConfig::default()
1932            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
1933        let (inner, _) = recorder();
1934
1935        SystemProxyLayer::from_cached(config)
1936            .with_pac_service(factory)
1937            .into_layer(inner)
1938            .serve(TestInput::origin_form(
1939                "/private?q=1",
1940                Protocol::HTTPS,
1941                "example.com:8443",
1942            ))
1943            .await
1944            .unwrap();
1945
1946        assert_eq!(
1947            received.lock().as_ref().unwrap().to_string(),
1948            "https://example.com:8443/private?q=1"
1949        );
1950    }
1951
1952    #[tokio::test]
1953    async fn pac_normalizes_default_ports_with_an_unboxed_resolver() {
1954        let factory_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1955        let received = Arc::new(Mutex::new(Vec::new()));
1956        let factory = service_fn({
1957            let factory_calls = factory_calls.clone();
1958            let received = received.clone();
1959            move |_uri: Uri| {
1960                factory_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1961                let received = received.clone();
1962                async move {
1963                    Ok::<_, Infallible>(service_fn(move |request: SystemProxyPacRequest| {
1964                        received.lock().push(request.uri);
1965                        async { Ok::<_, Infallible>(None) }
1966                    }))
1967                }
1968            }
1969        });
1970        let config = SystemProxyConfig::default()
1971            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
1972        let (inner, _) = recorder();
1973        let service = SystemProxyLayer::from_cached(config)
1974            .with_pac_service(factory)
1975            .into_layer(inner);
1976
1977        for input in [
1978            TestInput::new("http://example.com:80/path"),
1979            TestInput::new("https://example.com:443/"),
1980            TestInput::new("http://example.com:8080/"),
1981            TestInput::authority_form("example.com:443"),
1982        ] {
1983            service.serve(input).await.unwrap();
1984        }
1985
1986        assert_eq!(factory_calls.load(std::sync::atomic::Ordering::Relaxed), 4);
1987        assert_eq!(
1988            received
1989                .lock()
1990                .iter()
1991                .map(ToString::to_string)
1992                .collect::<Vec<_>>(),
1993            [
1994                "http://example.com/path",
1995                "https://example.com/",
1996                "http://example.com:8080/",
1997                "https://example.com/",
1998            ]
1999        );
2000    }
2001
2002    #[tokio::test]
2003    async fn authority_form_selects_the_https_proxy() {
2004        let config = SystemProxyConfig::default().with_https_proxy(proxy(
2005            Protocol::HTTP,
2006            "https.proxy",
2007            8443,
2008        ));
2009        let (inner, seen) = recorder();
2010
2011        SystemProxyLayer::from_cached(config)
2012            .into_layer(inner)
2013            .serve(TestInput::authority_form("example.com:443"))
2014            .await
2015            .unwrap();
2016
2017        let routes = seen.lock();
2018        assert_eq!(
2019            routes[0].as_ref().unwrap().as_slice()[0]
2020                .proxy_address()
2021                .unwrap()
2022                .address
2023                .host
2024                .to_str(),
2025            "https.proxy"
2026        );
2027    }
2028
2029    #[tokio::test]
2030    async fn pac_without_a_service_leaves_the_request_undecided() {
2031        let config = SystemProxyConfig::default()
2032            .with_pac_uri("http://config.example/proxy.pac".parse().unwrap());
2033        let (inner, seen) = recorder();
2034
2035        SystemProxyLayer::from_cached(config)
2036            .into_layer(inner)
2037            .serve(TestInput::new("/relative"))
2038            .await
2039            .unwrap();
2040
2041        assert!(seen.lock()[0].is_none());
2042    }
2043
2044    #[tokio::test]
2045    async fn pac_without_a_service_uses_a_fixed_proxy_fallback() {
2046        let config = SystemProxyConfig::default()
2047            .with_http_proxy(proxy(Protocol::HTTP, "fixed.proxy", 8080))
2048            .with_pac_uri("http://config.example/proxy.pac".parse().unwrap());
2049        let (inner, seen) = recorder();
2050
2051        SystemProxyLayer::from_cached(config)
2052            .into_layer(inner)
2053            .serve(TestInput::new("http://example.com/"))
2054            .await
2055            .unwrap();
2056
2057        assert_eq!(
2058            seen.lock()[0].as_ref().unwrap().as_slice()[0]
2059                .proxy_address()
2060                .unwrap()
2061                .address
2062                .host
2063                .to_str(),
2064            "fixed.proxy"
2065        );
2066    }
2067
2068    #[tokio::test]
2069    async fn active_pac_requires_a_resolvable_authority() {
2070        let factory = service_fn(|_uri: Uri| async {
2071            Ok::<_, Infallible>(service_fn(|_request| async { Ok::<_, Infallible>(None) }))
2072        });
2073        let config = SystemProxyConfig::default()
2074            .with_pac_uri("http://config.example/proxy.pac".parse().unwrap());
2075        let (inner, _) = recorder();
2076
2077        SystemProxyLayer::from_cached(config)
2078            .with_pac_service(factory)
2079            .into_layer(inner)
2080            .serve(TestInput::new("/relative"))
2081            .await
2082            .unwrap_err();
2083    }
2084
2085    #[tokio::test]
2086    async fn singular_route_also_prevents_pac_lookup() {
2087        let factory_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2088        let factory = service_fn({
2089            let factory_calls = factory_calls.clone();
2090            move |_uri: Uri| {
2091                factory_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2092                async move {
2093                    Ok::<_, Infallible>(service_fn(|_request| async { Ok::<_, Infallible>(None) }))
2094                }
2095            }
2096        });
2097        let config = SystemProxyConfig::default()
2098            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
2099        let request = TestInput::new("https://example.com/");
2100        request.extensions.insert(ProxyRoute::Direct);
2101        let (inner, _) = recorder();
2102
2103        SystemProxyLayer::from_cached(config)
2104            .with_pac_service(factory)
2105            .into_layer(inner)
2106            .serve(request)
2107            .await
2108            .unwrap();
2109
2110        assert_eq!(factory_calls.load(std::sync::atomic::Ordering::Relaxed), 0);
2111    }
2112
2113    #[tokio::test]
2114    async fn an_existing_route_prevents_system_config_refresh() {
2115        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2116        let reader = BoxService::new(service_fn({
2117            let calls = calls.clone();
2118            move |()| {
2119                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2120                async {
2121                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2122                        Protocol::HTTP,
2123                        "system.proxy",
2124                        8080,
2125                    )))
2126                }
2127            }
2128        }));
2129        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::ZERO, reader)
2130            .await
2131            .unwrap();
2132        let request = TestInput::new("http://example.com/");
2133        request.extensions.insert(ProxyRoute::Direct);
2134        let (inner, _) = recorder();
2135
2136        layer.into_layer(inner).serve(request).await.unwrap();
2137        tokio::time::sleep(Duration::from_millis(50)).await;
2138
2139        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
2140    }
2141
2142    #[tokio::test]
2143    async fn bypass_and_inverted_bypass_select_direct_routes() {
2144        let base = SystemProxyConfig::default()
2145            .with_http_proxy(proxy(Protocol::HTTP, "system.proxy", 8080))
2146            .with_bypass([".example.com", "default-port.test:80"]);
2147        let (inner, seen) = recorder();
2148        let service = SystemProxyLayer::from_cached(base.clone()).into_layer(inner.clone());
2149        service
2150            .serve(TestInput::new("http://api.example.com/"))
2151            .await
2152            .unwrap();
2153        service
2154            .serve(TestInput::new("http://elsewhere.test/"))
2155            .await
2156            .unwrap();
2157        service
2158            .serve(TestInput::new("http://default-port.test/"))
2159            .await
2160            .unwrap();
2161
2162        let inverted =
2163            SystemProxyLayer::from_cached(base.with_reversed_bypass(true)).into_layer(inner);
2164        inverted
2165            .serve(TestInput::new("http://api.example.com/"))
2166            .await
2167            .unwrap();
2168        inverted
2169            .serve(TestInput::new("http://elsewhere.test/"))
2170            .await
2171            .unwrap();
2172
2173        let seen = seen.lock();
2174        assert!(matches!(
2175            seen[0].as_ref().unwrap().as_slice(),
2176            [ProxyRoute::Direct]
2177        ));
2178        assert!(matches!(
2179            seen[1].as_ref().unwrap().as_slice(),
2180            [ProxyRoute::Proxy(_)]
2181        ));
2182        assert!(matches!(
2183            seen[2].as_ref().unwrap().as_slice(),
2184            [ProxyRoute::Direct]
2185        ));
2186        assert!(matches!(
2187            seen[3].as_ref().unwrap().as_slice(),
2188            [ProxyRoute::Proxy(_)]
2189        ));
2190        assert!(matches!(
2191            seen[4].as_ref().unwrap().as_slice(),
2192            [ProxyRoute::Direct]
2193        ));
2194    }
2195
2196    #[tokio::test]
2197    async fn simple_hostname_bypass_is_opt_in() {
2198        let config = SystemProxyConfig::default()
2199            .with_http_proxy(proxy(Protocol::HTTP, "system.proxy", 8080))
2200            .with_exclude_simple_hostnames(true);
2201        let (inner, seen) = recorder();
2202        let service = SystemProxyLayer::from_cached(config).into_layer(inner);
2203
2204        service
2205            .serve(TestInput::new("http://printer/"))
2206            .await
2207            .unwrap();
2208        service
2209            .serve(TestInput::new("http://printer.example/"))
2210            .await
2211            .unwrap();
2212        service
2213            .serve(TestInput::new("http://[2001:db8::1]/"))
2214            .await
2215            .unwrap();
2216
2217        let seen = seen.lock();
2218        assert!(matches!(
2219            seen[0].as_ref().unwrap().as_slice(),
2220            [ProxyRoute::Direct]
2221        ));
2222        assert!(matches!(
2223            seen[1].as_ref().unwrap().as_slice(),
2224            [ProxyRoute::Proxy(_)]
2225        ));
2226        assert!(matches!(
2227            seen[2].as_ref().unwrap().as_slice(),
2228            [ProxyRoute::Proxy(_)]
2229        ));
2230    }
2231
2232    #[tokio::test]
2233    async fn inverted_simple_hostname_bypass_uses_only_simple_names() {
2234        let config = SystemProxyConfig::default()
2235            .with_http_proxy(proxy(Protocol::HTTP, "system.proxy", 8080))
2236            .with_exclude_simple_hostnames(true)
2237            .with_reversed_bypass(true);
2238        let (inner, seen) = recorder();
2239        let service = SystemProxyLayer::from_cached(config).into_layer(inner);
2240
2241        service
2242            .serve(TestInput::new("http://printer/"))
2243            .await
2244            .unwrap();
2245        service
2246            .serve(TestInput::new("http://printer.example/"))
2247            .await
2248            .unwrap();
2249
2250        let seen = seen.lock();
2251        assert!(matches!(
2252            seen[0].as_ref().unwrap().as_slice(),
2253            [ProxyRoute::Proxy(_)]
2254        ));
2255        assert!(matches!(
2256            seen[1].as_ref().unwrap().as_slice(),
2257            [ProxyRoute::Direct]
2258        ));
2259    }
2260
2261    #[tokio::test]
2262    async fn fixed_system_proxies_implicitly_bypass_loopback() {
2263        let config = SystemProxyConfig::default()
2264            .with_http_proxy(proxy(Protocol::HTTP, "system.proxy", 8080))
2265            .with_bypass(["localhost", "127.0.0.0/8", "::1", "remote.example"])
2266            .with_reversed_bypass(true);
2267        let (inner, seen) = recorder();
2268        let service = SystemProxyLayer::from_cached(config).into_layer(inner);
2269
2270        for uri in [
2271            "http://localhost/",
2272            "http://service.localhost/",
2273            "http://127.42.0.1/",
2274            "http://[::1]/",
2275            "http://remote.example/",
2276        ] {
2277            service.serve(TestInput::new(uri)).await.unwrap();
2278        }
2279
2280        let seen = seen.lock();
2281        for routes in &seen[..4] {
2282            assert!(matches!(
2283                routes.as_ref().unwrap().as_slice(),
2284                [ProxyRoute::Direct]
2285            ));
2286        }
2287        assert!(matches!(
2288            seen[4].as_ref().unwrap().as_slice(),
2289            [ProxyRoute::Proxy(_)]
2290        ));
2291    }
2292
2293    #[tokio::test]
2294    async fn pac_is_not_consulted_for_loopback() {
2295        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2296        let factory = service_fn({
2297            let calls = calls.clone();
2298            move |_uri: Uri| {
2299                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2300                async {
2301                    Ok::<_, Infallible>(service_fn(|_request| async {
2302                        Ok::<_, Infallible>(Some(ProxyRoutes::from(ProxyRoute::Direct)))
2303                    }))
2304                }
2305            }
2306        });
2307        let config = SystemProxyConfig::default()
2308            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap());
2309        let (inner, seen) = recorder();
2310        let service = SystemProxyLayer::from_cached(config)
2311            .with_pac_service(factory)
2312            .into_layer(inner);
2313
2314        for uri in [
2315            "http://localhost/",
2316            "http://service.localhost/",
2317            "http://127.42.0.1/",
2318            "http://[::1]/",
2319        ] {
2320            service.serve(TestInput::new(uri)).await.unwrap();
2321        }
2322
2323        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 0);
2324        assert!(
2325            seen.lock()
2326                .iter()
2327                .all(|routes| matches!(routes.as_ref().unwrap().as_slice(), [ProxyRoute::Direct]))
2328        );
2329    }
2330
2331    #[test]
2332    fn platform_bypass_precedence_controls_pac_decision() {
2333        let pac_uri: Uri = "https://config.example/proxy.pac".parse().unwrap();
2334        let uri: Uri = "https://bypass.example/".parse().unwrap();
2335        let mut config = SystemProxyConfig::default()
2336            .with_pac_uri(pac_uri.clone())
2337            .with_bypass(["bypass.example"]);
2338
2339        assert!(matches!(
2340            config.decision(&uri),
2341            SystemProxyDecision::Pac(uri) if uri == pac_uri
2342        ));
2343
2344        config.bypass_before_pac = true;
2345        assert!(matches!(
2346            config.decision(&uri),
2347            SystemProxyDecision::Route(ProxyRoute::Direct)
2348        ));
2349    }
2350
2351    #[test]
2352    fn config_accessors_and_public_pac_request_fields_round_trip() {
2353        let http = proxy(Protocol::HTTP, "http.proxy", 8080);
2354        let https = proxy(Protocol::HTTP, "https.proxy", 8443);
2355        let socks = proxy(Protocol::SOCKS5, "socks.proxy", 1080);
2356        let pac: Uri = "https://config.example/proxy.pac".parse().unwrap();
2357        let config = SystemProxyConfig::default()
2358            .with_http_proxy(http.clone())
2359            .with_https_proxy(https.clone())
2360            .with_socks5_proxy(socks.clone())
2361            .with_pac_uri(pac.clone())
2362            .with_bypass(["localhost"])
2363            .with_exclude_simple_hostnames(true)
2364            .with_reversed_bypass(true)
2365            .with_auto_detect(true);
2366
2367        assert!(!config.is_empty());
2368        assert_eq!(config.http_proxy(), Some(&http));
2369        assert_eq!(config.https_proxy(), Some(&https));
2370        assert_eq!(config.socks5_proxy(), Some(&socks));
2371        assert_eq!(config.pac_uri(), Some(&pac));
2372        assert_eq!(config.bypass().collect::<Vec<_>>(), ["localhost"]);
2373        assert!(config.exclude_simple_hostnames());
2374        assert!(config.reversed_bypass());
2375        assert!(config.auto_detect());
2376
2377        let extensions = Extensions::new();
2378        extensions.insert(Marker("parts"));
2379        let request =
2380            SystemProxyPacRequest::new(extensions, "http://example.com/path".parse().unwrap())
2381                .unwrap();
2382        assert_eq!(
2383            UriInputExt::uri(&request).to_string(),
2384            "http://example.com/path"
2385        );
2386        assert_eq!(
2387            ExtensionsRef::extensions(&request)
2388                .get_ref::<Marker>()
2389                .unwrap()
2390                .0,
2391            "parts"
2392        );
2393        assert_eq!(request.extensions.get_ref::<Marker>().unwrap().0, "parts");
2394        assert_eq!(request.uri.to_string(), "http://example.com/path");
2395    }
2396
2397    #[test]
2398    fn platform_proxy_host_accepts_a_scheme_prefix() {
2399        let proxy = proxy_address(Protocol::HTTP, "http://proxy.corp", 8080).unwrap();
2400        assert_eq!(proxy.to_string(), "http://proxy.corp:8080");
2401    }
2402
2403    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
2404    #[tokio::test]
2405    async fn native_system_proxy_snapshot_can_be_read() {
2406        SystemProxyConfig::try_from_system().await.unwrap();
2407    }
2408
2409    #[test]
2410    fn every_proxy_source_independently_makes_config_non_empty() {
2411        assert!(SystemProxyConfig::default().is_empty());
2412        for config in [
2413            SystemProxyConfig::default().with_http_proxy(proxy(Protocol::HTTP, "http.proxy", 8080)),
2414            SystemProxyConfig::default().with_https_proxy(proxy(
2415                Protocol::HTTP,
2416                "https.proxy",
2417                8443,
2418            )),
2419            SystemProxyConfig::default().with_socks5_proxy(proxy(
2420                Protocol::SOCKS5,
2421                "socks.proxy",
2422                1080,
2423            )),
2424            SystemProxyConfig::default()
2425                .with_pac_uri("https://config.example/proxy.pac".parse().unwrap()),
2426            SystemProxyConfig::default().with_auto_detect(true),
2427        ] {
2428            assert!(!config.is_empty());
2429        }
2430    }
2431
2432    #[test]
2433    fn pac_request_rejects_non_absolute_or_hostless_uri() {
2434        SystemProxyPacRequest::new(Extensions::new(), "/path".parse().unwrap()).unwrap_err();
2435        SystemProxyPacRequest::new(Extensions::new(), "data:text/plain,x".parse().unwrap())
2436            .unwrap_err();
2437    }
2438
2439    #[test]
2440    fn bypass_patterns_cover_domains_ports_ip_ranges_and_local_names() {
2441        for (pattern, scheme, host, port, expected) in [
2442            ("*", None, "anything.example", None, true),
2443            ("<local>", None, "printer", None, true),
2444            ("<local>", None, "printer.example", None, false),
2445            ("<local>", None, "2001:db8::1", None, false),
2446            ("192.168.*", None, "192.168.10.20", None, true),
2447            ("*corp*", None, "api.corp.example", None, true),
2448            ("*.example.com", None, "api.example.com", None, true),
2449            ("*.example.com", None, "example.com", None, true),
2450            (".example.com", None, "api.example.com.", None, true),
2451            (".example.com", None, "notexample.com", None, false),
2452            (
2453                "api.example.com:8443",
2454                None,
2455                "api.example.com",
2456                Some(8443),
2457                true,
2458            ),
2459            (
2460                "api.example.com:8443",
2461                None,
2462                "api.example.com",
2463                Some(443),
2464                false,
2465            ),
2466            ("10.0.0.0/8", None, "10.2.3.4", None, true),
2467            ("10.0.0.0/8", None, "11.2.3.4", None, false),
2468            ("[::1]", None, "::1", None, true),
2469            ("::1", None, "::1", None, true),
2470            ("[::1]:8443", None, "::1", Some(8443), true),
2471            ("[::1]:8443", None, "::1", Some(443), false),
2472            ("2001:db8::/32", None, "2001:db8::1", None, true),
2473            (
2474                "https://secure.example:443",
2475                Some(Protocol::HTTPS),
2476                "secure.example",
2477                Some(443),
2478                true,
2479            ),
2480            (
2481                "https://secure.example:443",
2482                Some(Protocol::HTTP),
2483                "secure.example",
2484                Some(443),
2485                false,
2486            ),
2487        ] {
2488            let host = Host::try_from(host).unwrap();
2489            let host_text = host.to_string();
2490            assert_eq!(
2491                BypassRule::compile(pattern).unwrap().matches(
2492                    scheme.as_ref(),
2493                    (&host).into(),
2494                    port,
2495                ),
2496                expected,
2497                "{pattern} {host_text:?} {port:?}"
2498            );
2499        }
2500    }
2501
2502    #[test]
2503    fn invalid_bypass_patterns_are_discarded() {
2504        let config =
2505            SystemProxyConfig::default().with_bypass(["example.com", ".not a valid domain", ""]);
2506
2507        assert_eq!(config.bypass().collect::<Vec<_>>(), ["example.com"]);
2508    }
2509
2510    #[test]
2511    fn invalid_bypass_policy_can_reject_an_update_atomically() {
2512        let mut config = SystemProxyConfig::default().with_bypass(["existing.example"]);
2513
2514        let error = config
2515            .try_set_bypass(
2516                ["replacement.example", ".not a valid domain"],
2517                SystemProxyInvalidBypassRulePolicy::Reject,
2518            )
2519            .unwrap_err();
2520
2521        assert!(
2522            error
2523                .to_string()
2524                .contains("parse system proxy bypass pattern")
2525        );
2526        assert_eq!(config.bypass().collect::<Vec<_>>(), ["existing.example"]);
2527
2528        config
2529            .try_set_bypass(
2530                ["replacement.example", ".not a valid domain"],
2531                SystemProxyInvalidBypassRulePolicy::Ignore,
2532            )
2533            .unwrap();
2534        assert_eq!(config.bypass().collect::<Vec<_>>(), ["replacement.example"]);
2535    }
2536
2537    #[tokio::test]
2538    async fn lazy_layer_construction_does_not_read_until_warmed() {
2539        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2540        let reader = BoxService::new(service_fn({
2541            let calls = calls.clone();
2542            move |()| {
2543                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2544                async {
2545                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2546                        Protocol::HTTP,
2547                        "lazy.proxy",
2548                        8080,
2549                    )))
2550                }
2551            }
2552        }));
2553        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
2554
2555        assert!(layer.cached_config().is_none());
2556        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 0);
2557
2558        layer.warm_up().await.unwrap();
2559        layer.warm_up().await.unwrap();
2560
2561        let config = layer.cached_config().unwrap();
2562        assert_eq!(
2563            config.http_proxy().unwrap().address.host.to_str(),
2564            "lazy.proxy"
2565        );
2566        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
2567    }
2568
2569    #[tokio::test]
2570    async fn change_trigger_refreshes_a_fresh_snapshot() {
2571        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2572        let changed = Arc::new(std::sync::atomic::AtomicBool::new(false));
2573        let reader = BoxService::new(service_fn({
2574            let calls = calls.clone();
2575            move |()| {
2576                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2577                async move {
2578                    let host = if call == 0 { "old.proxy" } else { "new.proxy" };
2579                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2580                        Protocol::HTTP,
2581                        host,
2582                        8080,
2583                    )))
2584                }
2585            }
2586        }));
2587        let trigger = service_fn({
2588            let changed = changed.clone();
2589            move |()| {
2590                let changed = changed.swap(false, std::sync::atomic::Ordering::AcqRel);
2591                async move { Ok::<_, std::convert::Infallible>(changed) }
2592            }
2593        });
2594        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader)
2595            .with_config_change_trigger(trigger);
2596
2597        let old = layer.config().await.unwrap();
2598        assert_eq!(old.http_proxy().unwrap().address.host.to_str(), "old.proxy");
2599        changed.store(true, std::sync::atomic::Ordering::Release);
2600        let new = layer.config().await.unwrap();
2601        let still_new = layer.config().await.unwrap();
2602
2603        assert_eq!(new.http_proxy().unwrap().address.host.to_str(), "new.proxy");
2604        assert!(Arc::ptr_eq(&new, &still_new));
2605        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
2606    }
2607
2608    #[tokio::test]
2609    async fn change_during_refresh_remains_pending_for_the_next_request() {
2610        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2611        let changed = Arc::new(std::sync::atomic::AtomicBool::new(false));
2612        let refresh_started = Arc::new(tokio::sync::Notify::new());
2613        let release_refresh = Arc::new(tokio::sync::Notify::new());
2614        let reader = BoxService::new(service_fn({
2615            let reads = reads.clone();
2616            let refresh_started = refresh_started.clone();
2617            let release_refresh = release_refresh.clone();
2618            move |()| {
2619                let call = reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2620                let refresh_started = refresh_started.clone();
2621                let release_refresh = release_refresh.clone();
2622                async move {
2623                    if call == 0 {
2624                        refresh_started.notify_one();
2625                        release_refresh.notified().await;
2626                    }
2627                    let host = if call == 0 {
2628                        "first-refresh.proxy"
2629                    } else {
2630                        "second-refresh.proxy"
2631                    };
2632                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2633                        Protocol::HTTP,
2634                        host,
2635                        8080,
2636                    )))
2637                }
2638            }
2639        }));
2640        let trigger = service_fn({
2641            let changed = changed.clone();
2642            move |()| {
2643                let changed = changed.swap(false, std::sync::atomic::Ordering::AcqRel);
2644                async move { Ok::<_, Infallible>(changed) }
2645            }
2646        });
2647        let cached = SystemProxyConfig::default().with_http_proxy(proxy(
2648            Protocol::HTTP,
2649            "cached.proxy",
2650            8080,
2651        ));
2652        let layer =
2653            SystemProxyLayer::from_cached_with_reader(cached, Duration::from_mins(1), reader)
2654                .with_config_change_trigger(trigger);
2655
2656        changed.store(true, std::sync::atomic::Ordering::Release);
2657        let first_layer = layer.clone();
2658        let first = tokio::spawn(async move { first_layer.config().await });
2659        refresh_started.notified().await;
2660
2661        changed.store(true, std::sync::atomic::Ordering::Release);
2662        let stale = layer.config().await.unwrap();
2663        assert_eq!(stale.http_proxy().unwrap().address.host, "cached.proxy");
2664
2665        release_refresh.notify_one();
2666        let first = first.await.unwrap().unwrap();
2667        assert_eq!(
2668            first.http_proxy().unwrap().address.host,
2669            "first-refresh.proxy"
2670        );
2671        let second = layer.config().await.unwrap();
2672        assert_eq!(
2673            second.http_proxy().unwrap().address.host,
2674            "second-refresh.proxy"
2675        );
2676        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 2);
2677    }
2678
2679    #[tokio::test]
2680    async fn cancelled_triggered_refresh_does_not_consume_the_request() {
2681        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2682        let changed = Arc::new(std::sync::atomic::AtomicBool::new(true));
2683        let refresh_started = Arc::new(tokio::sync::Notify::new());
2684        let reader = BoxService::new(service_fn({
2685            let reads = reads.clone();
2686            let refresh_started = refresh_started.clone();
2687            move |()| {
2688                let call = reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2689                let refresh_started = refresh_started.clone();
2690                async move {
2691                    if call == 0 {
2692                        refresh_started.notify_one();
2693                        std::future::pending::<()>().await;
2694                    }
2695                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2696                        Protocol::HTTP,
2697                        "refreshed.proxy",
2698                        8080,
2699                    )))
2700                }
2701            }
2702        }));
2703        let trigger = service_fn({
2704            let changed = changed.clone();
2705            move |()| {
2706                let changed = changed.swap(false, std::sync::atomic::Ordering::AcqRel);
2707                async move { Ok::<_, Infallible>(changed) }
2708            }
2709        });
2710        let cached = SystemProxyConfig::default().with_http_proxy(proxy(
2711            Protocol::HTTP,
2712            "cached.proxy",
2713            8080,
2714        ));
2715        let layer =
2716            SystemProxyLayer::from_cached_with_reader(cached, Duration::from_mins(1), reader)
2717                .with_config_change_trigger(trigger);
2718
2719        let refresh_layer = layer.clone();
2720        let refresh = tokio::spawn(async move { refresh_layer.config().await });
2721        refresh_started.notified().await;
2722        assert_eq!(
2723            layer
2724                .config()
2725                .await
2726                .unwrap()
2727                .http_proxy()
2728                .unwrap()
2729                .address
2730                .host,
2731            "cached.proxy"
2732        );
2733        refresh.abort();
2734        refresh.await.unwrap_err();
2735
2736        let refreshed = layer.config().await.unwrap();
2737        assert_eq!(
2738            refreshed.http_proxy().unwrap().address.host,
2739            "refreshed.proxy"
2740        );
2741        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 2);
2742    }
2743
2744    #[tokio::test]
2745    async fn failed_triggered_refresh_is_acknowledged_until_the_ttl() {
2746        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2747        let changed = Arc::new(std::sync::atomic::AtomicBool::new(true));
2748        let reader = BoxService::new(service_fn({
2749            let reads = reads.clone();
2750            move |()| {
2751                reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2752                async {
2753                    Err::<SystemProxyConfig, BoxError>(std::io::Error::other("offline").into())
2754                }
2755            }
2756        }));
2757        let trigger = service_fn({
2758            let changed = changed.clone();
2759            move |()| {
2760                let changed = changed.swap(false, std::sync::atomic::Ordering::AcqRel);
2761                async move { Ok::<_, Infallible>(changed) }
2762            }
2763        });
2764        let layer = SystemProxyLayer::from_cached_with_reader(
2765            SystemProxyConfig::default(),
2766            Duration::from_mins(1),
2767            reader,
2768        )
2769        .with_config_change_trigger(trigger);
2770
2771        layer.config().await.unwrap();
2772        layer.config().await.unwrap();
2773        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 1);
2774    }
2775
2776    #[tokio::test]
2777    async fn disabled_change_trigger_retains_ttl_refresh() {
2778        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2779        let triggers = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2780        let reader = BoxService::new(service_fn({
2781            let reads = reads.clone();
2782            move |()| {
2783                reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2784                async { Ok::<_, BoxError>(SystemProxyConfig::default()) }
2785            }
2786        }));
2787        let trigger = service_fn({
2788            let triggers = triggers.clone();
2789            move |()| {
2790                triggers.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2791                async { Ok::<_, std::convert::Infallible>(true) }
2792            }
2793        });
2794        let layer = SystemProxyLayer::new_with_reader(Duration::ZERO, reader)
2795            .with_config_change_trigger(trigger)
2796            .without_config_change_trigger();
2797
2798        layer.config().await.unwrap();
2799        layer.config().await.unwrap();
2800
2801        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 2);
2802        assert_eq!(triggers.load(std::sync::atomic::Ordering::Relaxed), 0);
2803    }
2804
2805    #[tokio::test]
2806    async fn disabled_refresh_makes_a_cached_snapshot_immutable() {
2807        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2808        let reader = BoxService::new(service_fn({
2809            let reads = reads.clone();
2810            move |()| {
2811                reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2812                async { Ok::<_, BoxError>(SystemProxyConfig::default()) }
2813            }
2814        }));
2815        let cached = SystemProxyConfig::default().with_http_proxy(proxy(
2816            Protocol::HTTP,
2817            "cached.proxy",
2818            8080,
2819        ));
2820        let layer = SystemProxyLayer::from_cached_with_reader(cached, Duration::ZERO, reader)
2821            .with_config_refresh(false);
2822
2823        for _ in 0..2 {
2824            assert_eq!(
2825                layer
2826                    .config()
2827                    .await
2828                    .unwrap()
2829                    .http_proxy()
2830                    .unwrap()
2831                    .address
2832                    .host
2833                    .to_str(),
2834                "cached.proxy"
2835            );
2836        }
2837        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 0);
2838    }
2839
2840    #[tokio::test]
2841    async fn change_trigger_errors_fall_back_to_the_ttl() {
2842        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2843        let errors = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2844        let reader = BoxService::new(service_fn({
2845            let reads = reads.clone();
2846            move |()| {
2847                reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2848                async { Ok::<_, BoxError>(SystemProxyConfig::default()) }
2849            }
2850        }));
2851        let trigger = service_fn(|()| async {
2852            Err::<bool, _>(std::io::Error::other("configuration watcher failed"))
2853        });
2854        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader)
2855            .with_config_change_trigger(trigger)
2856            .with_config_change_trigger_error_sink({
2857                let errors = errors.clone();
2858                move |_error: BoxError| {
2859                    errors.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2860                }
2861            });
2862
2863        layer.config().await.unwrap();
2864        layer.config().await.unwrap();
2865
2866        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 1);
2867        assert_eq!(errors.load(std::sync::atomic::Ordering::Relaxed), 2);
2868    }
2869
2870    #[tokio::test]
2871    async fn first_request_lazily_loads_and_applies_system_proxy() {
2872        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2873        let reader = BoxService::new(service_fn({
2874            let calls = calls.clone();
2875            move |()| {
2876                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2877                async {
2878                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
2879                        Protocol::HTTP,
2880                        "lazy.proxy",
2881                        8080,
2882                    )))
2883                }
2884            }
2885        }));
2886        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
2887        let (inner, seen) = recorder();
2888        let service = layer.into_layer(inner);
2889
2890        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 0);
2891        service
2892            .serve(TestInput::new("http://example.com/"))
2893            .await
2894            .unwrap();
2895
2896        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
2897        assert_eq!(
2898            seen.lock()[0].as_ref().unwrap().as_slice()[0]
2899                .proxy_address()
2900                .unwrap()
2901                .address
2902                .host
2903                .to_str(),
2904            "lazy.proxy"
2905        );
2906    }
2907
2908    #[tokio::test]
2909    async fn concurrent_cold_loads_share_one_async_read() {
2910        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2911        let reader = BoxService::new(service_fn({
2912            let calls = calls.clone();
2913            move |()| {
2914                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2915                async {
2916                    tokio::time::sleep(Duration::from_millis(20)).await;
2917                    Ok::<_, BoxError>(SystemProxyConfig::default())
2918                }
2919            }
2920        }));
2921        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
2922
2923        let (first, second, third) = tokio::join!(layer.config(), layer.config(), layer.config());
2924
2925        first.unwrap();
2926        second.unwrap();
2927        third.unwrap();
2928        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
2929    }
2930
2931    #[tokio::test]
2932    async fn failed_cold_load_remains_retryable() {
2933        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2934        let reader = BoxService::new(service_fn({
2935            let calls = calls.clone();
2936            move |()| {
2937                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2938                async move {
2939                    if call == 0 {
2940                        Err(std::io::Error::other("temporary platform read failure").into())
2941                    } else {
2942                        Ok(SystemProxyConfig::default())
2943                    }
2944                }
2945            }
2946        }));
2947        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
2948
2949        layer.config().await.unwrap_err();
2950        assert!(layer.cached_config().is_none());
2951        layer.config().await.unwrap();
2952        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
2953    }
2954
2955    #[tokio::test]
2956    async fn handled_cold_load_error_is_sunk_and_cached_for_the_ttl() {
2957        let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2958        let errors = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2959        let reader = BoxService::new(service_fn({
2960            let reads = reads.clone();
2961            move |()| {
2962                reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2963                async {
2964                    Err::<SystemProxyConfig, BoxError>(
2965                        std::io::Error::other("platform read failed").into(),
2966                    )
2967                }
2968            }
2969        }));
2970        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader)
2971            .with_load_error_sink({
2972                let errors = errors.clone();
2973                move |error: BoxError| {
2974                    assert_eq!(error.to_string(), "platform read failed");
2975                    errors.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2976                }
2977            });
2978
2979        assert!(layer.config().await.unwrap().is_empty());
2980        assert!(layer.config().await.unwrap().is_empty());
2981        assert_eq!(reads.load(std::sync::atomic::Ordering::Relaxed), 1);
2982        assert_eq!(errors.load(std::sync::atomic::Ordering::Relaxed), 1);
2983    }
2984
2985    #[tokio::test]
2986    async fn native_change_trigger_is_initialized_only_when_polled() {
2987        let reader = || {
2988            BoxService::new(service_fn(|()| async {
2989                Ok::<_, BoxError>(SystemProxyConfig::default())
2990            }))
2991        };
2992        let trigger_initialized = |layer: &SystemProxyLayer| match &layer.refresh.trigger {
2993            Some(SystemProxyConfigChangeTrigger::Platform(trigger)) => trigger.is_initialized(),
2994            _ => panic!("expected the default platform change trigger"),
2995        };
2996
2997        let disabled = SystemProxyLayer::from_cached_with_reader(
2998            SystemProxyConfig::default(),
2999            Duration::ZERO,
3000            reader(),
3001        )
3002        .with_config_refresh(false);
3003        assert!(!trigger_initialized(&disabled));
3004        disabled.config().await.unwrap();
3005        assert!(!trigger_initialized(&disabled));
3006
3007        let enabled = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader());
3008        assert!(!trigger_initialized(&enabled));
3009        enabled.config().await.unwrap();
3010        assert!(trigger_initialized(&enabled));
3011    }
3012
3013    #[tokio::test]
3014    async fn concurrent_cold_failure_is_shared_without_a_retry_convoy() {
3015        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3016        let read_started = Arc::new(tokio::sync::Notify::new());
3017        let release_read = Arc::new(tokio::sync::Notify::new());
3018        let reader = BoxService::new(service_fn({
3019            let calls = calls.clone();
3020            let read_started = read_started.clone();
3021            let release_read = release_read.clone();
3022            move |()| {
3023                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3024                let read_started = read_started.clone();
3025                let release_read = release_read.clone();
3026                async move {
3027                    if call == 0 {
3028                        read_started.notify_one();
3029                        release_read.notified().await;
3030                        Err(std::io::Error::other("temporary platform read failure").into())
3031                    } else {
3032                        Ok(SystemProxyConfig::default())
3033                    }
3034                }
3035            }
3036        }));
3037        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
3038
3039        let release = async {
3040            read_started.notified().await;
3041            release_read.notify_one();
3042        };
3043        let (first, second, third, ()) = tokio::time::timeout(Duration::from_secs(5), async {
3044            tokio::join!(layer.config(), layer.config(), layer.config(), release,)
3045        })
3046        .await
3047        .expect("concurrent cold configuration load should complete");
3048
3049        first.unwrap_err();
3050        second.unwrap_err();
3051        third.unwrap_err();
3052        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
3053
3054        layer.config().await.unwrap();
3055        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
3056    }
3057
3058    #[tokio::test]
3059    async fn cancelled_cold_load_releases_single_flight_lock() {
3060        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3061        let reader = BoxService::new(service_fn({
3062            let calls = calls.clone();
3063            move |()| {
3064                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3065                async move {
3066                    if call == 0 {
3067                        std::future::pending::<()>().await;
3068                    }
3069                    Ok::<_, BoxError>(SystemProxyConfig::default())
3070                }
3071            }
3072        }));
3073        let layer = SystemProxyLayer::new_with_reader(Duration::from_mins(1), reader);
3074
3075        tokio::time::timeout(Duration::from_millis(10), layer.config())
3076            .await
3077            .unwrap_err();
3078        assert!(layer.cached_config().is_none());
3079        layer.config().await.unwrap();
3080        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
3081    }
3082
3083    #[tokio::test]
3084    async fn cancelled_stale_refresh_releases_single_flight_lock() {
3085        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3086        let reader = BoxService::new(service_fn({
3087            let calls = calls.clone();
3088            move |()| {
3089                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3090                async move {
3091                    if call == 1 {
3092                        std::future::pending::<()>().await;
3093                    }
3094                    let host = if call == 0 { "old.proxy" } else { "new.proxy" };
3095                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
3096                        Protocol::HTTP,
3097                        host,
3098                        8080,
3099                    )))
3100                }
3101            }
3102        }));
3103        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::ZERO, reader)
3104            .await
3105            .unwrap();
3106
3107        tokio::time::timeout(Duration::from_millis(10), layer.config())
3108            .await
3109            .unwrap_err();
3110        let fresh = layer.config().await.unwrap();
3111
3112        assert_eq!(
3113            fresh.http_proxy().unwrap().address.host.to_str(),
3114            "new.proxy"
3115        );
3116        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 3);
3117    }
3118
3119    #[tokio::test]
3120    async fn stale_refresh_is_single_flight_and_concurrent_calls_use_stale_config() {
3121        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3122        let refresh_started = Arc::new(tokio::sync::Notify::new());
3123        let allow_refresh = Arc::new(tokio::sync::Notify::new());
3124        let reader = BoxService::new(service_fn({
3125            let calls = calls.clone();
3126            let refresh_started = refresh_started.clone();
3127            let allow_refresh = allow_refresh.clone();
3128            move |()| {
3129                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3130                let refresh_started = refresh_started.clone();
3131                let allow_refresh = allow_refresh.clone();
3132                async move {
3133                    if call > 0 {
3134                        refresh_started.notify_one();
3135                        allow_refresh.notified().await;
3136                    }
3137                    let host = if call == 0 { "old.proxy" } else { "new.proxy" };
3138                    Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
3139                        Protocol::HTTP,
3140                        host,
3141                        8080,
3142                    )))
3143                }
3144            }
3145        }));
3146        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::ZERO, reader)
3147            .await
3148            .unwrap();
3149
3150        let refresh_layer = layer.clone();
3151        let refresh = tokio::spawn(async move { refresh_layer.config().await });
3152        tokio::time::timeout(Duration::from_secs(5), refresh_started.notified())
3153            .await
3154            .expect("stale configuration refresh should start");
3155
3156        let stale = tokio::time::timeout(Duration::from_millis(100), layer.config())
3157            .await
3158            .unwrap()
3159            .unwrap();
3160        assert_eq!(
3161            stale.http_proxy().unwrap().address.host.to_str(),
3162            "old.proxy"
3163        );
3164        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
3165
3166        allow_refresh.notify_one();
3167        let fresh = tokio::time::timeout(Duration::from_secs(5), refresh)
3168            .await
3169            .expect("stale configuration refresh should complete")
3170            .unwrap()
3171            .unwrap();
3172        assert_eq!(
3173            fresh.http_proxy().unwrap().address.host.to_str(),
3174            "new.proxy"
3175        );
3176    }
3177
3178    #[tokio::test]
3179    async fn failed_system_config_refresh_retains_snapshot_and_remains_retryable() {
3180        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3181        let reader = BoxService::new(service_fn({
3182            let calls = calls.clone();
3183            move |()| {
3184                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3185                async move {
3186                    match call {
3187                        0 => Ok(SystemProxyConfig::default().with_http_proxy(proxy(
3188                            Protocol::HTTP,
3189                            "old.proxy",
3190                            8080,
3191                        ))),
3192                        1 => Err(std::io::Error::other("temporary platform read failure").into()),
3193                        _ => Ok(SystemProxyConfig::default().with_http_proxy(proxy(
3194                            Protocol::HTTP,
3195                            "new.proxy",
3196                            8080,
3197                        ))),
3198                    }
3199                }
3200            }
3201        }));
3202        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::ZERO, reader)
3203            .await
3204            .unwrap();
3205
3206        let stale = layer.config().await.unwrap();
3207        assert_eq!(
3208            stale.http_proxy().unwrap().address.host.to_str(),
3209            "old.proxy"
3210        );
3211        let fresh = layer.config().await.unwrap();
3212        assert_eq!(
3213            fresh.http_proxy().unwrap().address.host.to_str(),
3214            "new.proxy"
3215        );
3216        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 3);
3217    }
3218
3219    #[tokio::test]
3220    async fn a_pac_uri_discovered_by_refresh_is_used_by_the_existing_service() {
3221        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3222        let reader = BoxService::new(service_fn({
3223            let calls = calls.clone();
3224            move |()| {
3225                let call = calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3226                async move {
3227                    if call == 0 {
3228                        Ok::<_, BoxError>(SystemProxyConfig::default().with_http_proxy(proxy(
3229                            Protocol::HTTP,
3230                            "fixed.proxy",
3231                            8080,
3232                        )))
3233                    } else {
3234                        Ok(SystemProxyConfig::default()
3235                            .with_pac_uri("https://config.example/proxy.pac".parse().unwrap()))
3236                    }
3237                }
3238            }
3239        }));
3240        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::from_mins(1), reader)
3241            .await
3242            .unwrap();
3243        let factory_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3244        let factory = service_fn({
3245            let factory_calls = factory_calls.clone();
3246            move |_uri: Uri| {
3247                factory_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3248                async move {
3249                    Ok::<_, Infallible>(service_fn(|_request| async move {
3250                        Ok::<_, Infallible>(Some(ProxyRoutes::from(proxy(
3251                            Protocol::HTTP,
3252                            "pac.proxy",
3253                            8080,
3254                        ))))
3255                    }))
3256                }
3257            }
3258        });
3259        let (inner, seen) = recorder();
3260        let service = layer.clone().with_pac_service(factory).into_layer(inner);
3261
3262        service
3263            .serve(TestInput::new("http://example.com/first"))
3264            .await
3265            .unwrap();
3266        layer.config.refresh_after_nanos.store(0, Ordering::Release);
3267        service
3268            .serve(TestInput::new("http://example.com/second"))
3269            .await
3270            .unwrap();
3271
3272        let seen = seen.lock();
3273        let hosts = seen
3274            .iter()
3275            .map(|routes| {
3276                routes.as_ref().unwrap().as_slice()[0]
3277                    .proxy_address()
3278                    .unwrap()
3279                    .address
3280                    .host
3281                    .to_str()
3282                    .into_owned()
3283            })
3284            .collect::<Vec<_>>();
3285        assert_eq!(hosts, ["fixed.proxy", "pac.proxy"]);
3286        assert_eq!(factory_calls.load(std::sync::atomic::Ordering::Relaxed), 1);
3287    }
3288
3289    #[tokio::test]
3290    async fn system_config_cache_honors_a_custom_ttl() {
3291        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3292        let reader = BoxService::new(service_fn({
3293            let calls = calls.clone();
3294            move |()| {
3295                calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3296                async { Ok::<_, BoxError>(SystemProxyConfig::default()) }
3297            }
3298        }));
3299        let layer = SystemProxyLayer::try_from_system_with_reader(Duration::from_mins(1), reader)
3300            .await
3301            .unwrap();
3302
3303        for _ in 0..10 {
3304            drop(layer.config().await.unwrap());
3305        }
3306        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
3307    }
3308}