Skip to main content

rama_proxy/proxydb/
layer.rs

1use super::{Proxy, ProxyContext, ProxyDB, ProxyFilter, ProxyQueryPredicate};
2use rama_core::error::BoxErrorExt as _;
3use rama_core::{
4    Layer, Service,
5    error::{BoxError, ErrorContext, ErrorExt},
6    extensions::{Extensions, ExtensionsRef},
7};
8use rama_net::{
9    Protocol, TransportProtocolInputExt,
10    client::{ProxyRoute, ProxyRoutes},
11    transport::TransportProtocol,
12    user::ProxyCredential,
13};
14use rama_utils::collections::NonEmptyVec;
15use rama_utils::macros::define_inner_service_accessors;
16use std::{fmt, num::NonZeroUsize};
17
18/// Default maximum number of proxy candidates published for one query.
19pub const DEFAULT_PROXY_DB_MAX_PROXIES: NonZeroUsize = NonZeroUsize::new(5).unwrap();
20
21/// A [`Service`] which resolves proxy candidates from the given input `Extensions`.
22///
23/// Depending on the [`ProxyFilterMode`] the selection proxies might be optional,
24/// or use the default [`ProxyFilter`] in case none is defined.
25///
26/// A predicate can be used to provide additional filtering on the found proxies,
27/// that otherwise did match the used [`ProxyFilter`].
28///
29/// By default up to [`DEFAULT_PROXY_DB_MAX_PROXIES`] matches are published as
30/// [`ProxyRoutes`]. Each route carries its corresponding [`Proxy`] and proxy ID
31/// as route-specific extensions, so only the successful connection attempt
32/// inherits them. Legacy single-proxy mode inserts one singular [`ProxyRoute`]
33/// and publishes its proxy metadata after the inner service succeeds.
34///
35/// See [the crate docs](crate) for examples and more info on the usage of this service.
36///
37/// [`Proxy`]: crate::Proxy
38#[derive(Debug, Clone)]
39pub struct ProxyDBService<S, D, P, F> {
40    inner: S,
41    db: D,
42    mode: ProxyFilterMode,
43    predicate: P,
44    username_formatter: F,
45    max_proxies: Option<NonZeroUsize>,
46    overwrite_proxy: bool,
47    single_proxy: bool,
48}
49
50#[derive(Debug, Clone, Default)]
51/// The modus operandi to decide how to deal with a missing [`ProxyFilter`] in the input `Extensions`
52/// when selecting a [`Proxy`] from the [`ProxyDB`].
53///
54/// More advanced behaviour can be achieved by combining one of these modi
55/// with another (custom) layer prepending the parent.
56pub enum ProxyFilterMode {
57    #[default]
58    /// The [`ProxyFilter`] is optional, and if not present, no proxy is selected.
59    Optional,
60    /// The [`ProxyFilter`] is optional, and if not present, the default [`ProxyFilter`] is used.
61    Default,
62    /// The [`ProxyFilter`] is required, and if not present, an error is returned.
63    Required,
64    /// The [`ProxyFilter`] is optional, and if not present, the provided fallback [`ProxyFilter`] is used.
65    Fallback(ProxyFilter),
66}
67
68impl<S, D> ProxyDBService<S, D, bool, ()> {
69    /// Create a new [`ProxyDBService`] with the given inner [`Service`] and [`ProxyDB`].
70    pub const fn new(inner: S, db: D) -> Self {
71        Self {
72            inner,
73            db,
74            mode: ProxyFilterMode::Optional,
75            predicate: true,
76            username_formatter: (),
77            max_proxies: Some(DEFAULT_PROXY_DB_MAX_PROXIES),
78            overwrite_proxy: false,
79            single_proxy: false,
80        }
81    }
82}
83
84impl<S, D, P, F> ProxyDBService<S, D, P, F> {
85    rama_utils::macros::generate_set_and_with! {
86        /// Limit the number of proxy candidates published for one query.
87        ///
88        /// The default is [`DEFAULT_PROXY_DB_MAX_PROXIES`]. Use
89        /// [`Self::without_max_proxies`] for an unbounded query.
90        pub fn max_proxies(mut self, max_proxies: Option<NonZeroUsize>) -> Self {
91            self.max_proxies = max_proxies;
92            self
93        }
94    }
95
96    rama_utils::macros::generate_set_and_with! {
97        /// Set a [`ProxyFilterMode`] to define the behaviour surrounding
98        /// [`ProxyFilter`] usage, e.g. if a proxy filter is required to be available or not,
99        /// or what to do if it is optional and not available.
100        pub fn filter_mode(mut self, mode: ProxyFilterMode) -> Self {
101            self.mode = mode;
102            self
103        }
104    }
105
106    rama_utils::macros::generate_set_and_with! {
107        /// Select and insert only one proxy instead of publishing every match as
108        /// an ordered route plan. This uses the database's singular-selection
109        /// semantics, preserves legacy pre-fallback behaviour, and is disabled
110        /// by default.
111        pub fn single_proxy(mut self, single_proxy: bool) -> Self {
112            self.single_proxy = single_proxy;
113            self
114        }
115    }
116
117    rama_utils::macros::generate_set_and_with! {
118        /// Overwrite an existing singular [`ProxyRoute`] with the selected
119        /// proxy route or route plan. This is disabled by default in both
120        /// plural and singular selection modes.
121        pub fn overwrite_proxy(mut self, overwrite_proxy: bool) -> Self {
122            self.overwrite_proxy = overwrite_proxy;
123            self
124        }
125    }
126
127    /// Set a [`ProxyQueryPredicate`] that will be used
128    /// to possibly filter out proxies that according to the filters are correct,
129    /// but not according to the predicate.
130    pub fn with_select_predicate<Predicate>(
131        self,
132        p: Predicate,
133    ) -> ProxyDBService<S, D, Predicate, F> {
134        ProxyDBService {
135            inner: self.inner,
136            db: self.db,
137            mode: self.mode,
138            predicate: p,
139            username_formatter: self.username_formatter,
140            max_proxies: self.max_proxies,
141            overwrite_proxy: self.overwrite_proxy,
142            single_proxy: self.single_proxy,
143        }
144    }
145
146    /// Set an optional [`UsernameFormatter`][crate::UsernameFormatter] for
147    /// Basic-auth proxy usernames. It is called separately for each published
148    /// candidate, so routing labels can depend on that proxy's metadata. Bearer
149    /// credentials and proxies without credentials are left unchanged.
150    pub fn with_username_formatter<Formatter>(
151        self,
152        f: Formatter,
153    ) -> ProxyDBService<S, D, P, Formatter> {
154        ProxyDBService {
155            inner: self.inner,
156            db: self.db,
157            mode: self.mode,
158            predicate: self.predicate,
159            username_formatter: f,
160            max_proxies: self.max_proxies,
161            overwrite_proxy: self.overwrite_proxy,
162            single_proxy: self.single_proxy,
163        }
164    }
165
166    define_inner_service_accessors!();
167}
168
169#[derive(Debug)]
170struct PreparedProxy {
171    proxy: Proxy,
172    route: ProxyRoute,
173}
174
175fn prepare_proxy<F: UsernameFormatter>(
176    formatter: &F,
177    proxy: Proxy,
178    filter: &ProxyFilter,
179    transport_protocol: TransportProtocol,
180    extensions: &Extensions,
181) -> Result<PreparedProxy, BoxError> {
182    let mut proxy_address = proxy.address.clone();
183
184    proxy_address.credential = proxy_address
185        .credential
186        .take()
187        .map(|credential| {
188            Ok::<_, BoxError>(match credential {
189                ProxyCredential::Basic(ref basic) => {
190                    match formatter.fmt_username(&proxy, filter, basic.username(), extensions) {
191                        Some(username) => ProxyCredential::Basic(
192                            basic.clone_with_new_username(
193                                username
194                                    .try_into()
195                                    .context("returned formatted username is invalid")?,
196                            ),
197                        ),
198                        None => credential,
199                    }
200                }
201                ProxyCredential::Bearer(_) => credential,
202            })
203        })
204        .transpose()?;
205
206    if proxy_address.protocol.is_none() {
207        proxy_address.protocol = match transport_protocol {
208            TransportProtocol::Udp => {
209                if proxy.socks5 {
210                    Some(Protocol::SOCKS5)
211                } else if proxy.socks5h {
212                    Some(Protocol::SOCKS5H)
213                } else {
214                    return Err(BoxError::from_static_str(
215                        "selected udp proxy does not have a valid protocol available (db bug?!)",
216                    ));
217                }
218            }
219            TransportProtocol::Tcp => match proxy_address.address.port {
220                Protocol::HTTP_DEFAULT_PORT | Protocol::HTTP_ALT_PORT if proxy.http => {
221                    Some(Protocol::HTTP)
222                }
223                Protocol::HTTPS_DEFAULT_PORT | Protocol::HTTPS_ALT_PORT if proxy.https => {
224                    Some(Protocol::HTTPS)
225                }
226                _ => {
227                    if proxy.socks5 {
228                        Some(Protocol::SOCKS5)
229                    } else if proxy.socks5h {
230                        Some(Protocol::SOCKS5H)
231                    } else if proxy.http {
232                        Some(Protocol::HTTP)
233                    } else if proxy.https {
234                        Some(Protocol::HTTPS)
235                    } else {
236                        return Err(BoxError::from_static_str(
237                            "selected tcp proxy does not have a valid protocol available (db bug?!)",
238                        ));
239                    }
240                }
241            },
242        };
243    }
244
245    Ok(PreparedProxy {
246        proxy,
247        route: ProxyRoute::Proxy(proxy_address),
248    })
249}
250
251impl<S, D, P, F, Input> Service<Input> for ProxyDBService<S, D, P, F>
252where
253    S: Service<Input, Error: Into<BoxError> + Send + Sync + 'static>,
254    D: ProxyDB<Error: Into<BoxError> + Send + Sync + 'static>,
255    P: ProxyQueryPredicate,
256    F: UsernameFormatter,
257    Input: TransportProtocolInputExt + ExtensionsRef + Send + 'static,
258{
259    type Output = S::Output;
260    type Error = BoxError;
261
262    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
263        if !self.overwrite_proxy
264            && (input.extensions().contains::<ProxyRoute>()
265                || input.extensions().contains::<ProxyRoutes>())
266        {
267            return self.inner.serve(input).await.into_box_error();
268        }
269
270        let maybe_filter = match self.mode {
271            ProxyFilterMode::Optional => input.extensions().get_ref::<ProxyFilter>().cloned(),
272            ProxyFilterMode::Default => Some(
273                if let Some(stored) = input.extensions().get_ref::<ProxyFilter>() {
274                    stored.clone()
275                } else {
276                    input.extensions().insert(ProxyFilter::default());
277                    ProxyFilter::default()
278                },
279            ),
280            ProxyFilterMode::Required => Some(
281                input
282                    .extensions()
283                    .get_ref::<ProxyFilter>()
284                    .cloned()
285                    .context("missing proxy filter")?,
286            ),
287            ProxyFilterMode::Fallback(ref filter) => Some(
288                if let Some(stored) = input.extensions().get_ref::<ProxyFilter>() {
289                    stored.clone()
290                } else {
291                    input.extensions().insert(filter.clone());
292                    filter.clone()
293                },
294            ),
295        };
296
297        let Some(filter) = maybe_filter else {
298            return self.inner.serve(input).await.into_box_error();
299        };
300
301        let transport_protocol = input.transport_protocol().unwrap_or(TransportProtocol::Tcp);
302        let proxy_ctx = ProxyContext {
303            protocol: transport_protocol,
304        };
305        let mut proxies = if self.single_proxy {
306            self.db
307                .get_proxy_if(proxy_ctx, filter.clone(), self.predicate.clone())
308                .await
309                .map(NonEmptyVec::new)
310        } else {
311            self.db
312                .get_proxies_if(
313                    proxy_ctx,
314                    filter.clone(),
315                    self.predicate.clone(),
316                    self.max_proxies,
317                )
318                .await
319        }
320        .map_err(|err| {
321            ProxySelectError {
322                inner: err.into(),
323                filter: filter.clone(),
324            }
325            .into_box_error()
326        })?;
327        if let Some(max_proxies) = self.max_proxies {
328            proxies.truncate(max_proxies);
329        }
330
331        let candidates = proxies.try_map(|proxy| {
332            prepare_proxy(
333                &self.username_formatter,
334                proxy,
335                &filter,
336                transport_protocol,
337                input.extensions(),
338            )
339        })?;
340
341        let selected_proxy = if self.single_proxy {
342            input.extensions().insert(candidates.head.route.clone());
343            Some((input.extensions().clone(), candidates.head.proxy.clone()))
344        } else {
345            let routes = (&candidates)
346                .into_iter()
347                .map(|candidate| {
348                    let extensions = Extensions::new();
349                    extensions.insert(super::ProxyID::from(candidate.proxy.id.clone()));
350                    extensions.insert(candidate.proxy.clone());
351                    (candidate.route.clone(), extensions)
352                })
353                .collect::<ProxyRoutes>();
354            input.extensions().insert(routes);
355            None
356        };
357
358        let output = self.inner.serve(input).await.into_box_error()?;
359        if let Some((extensions, proxy)) = selected_proxy {
360            extensions.insert(super::ProxyID::from(proxy.id.clone()));
361            extensions.insert(proxy);
362        }
363        Ok(output)
364    }
365}
366
367#[derive(Debug)]
368struct ProxySelectError {
369    inner: BoxError,
370    filter: ProxyFilter,
371}
372
373impl fmt::Display for ProxySelectError {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375        write!(
376            f,
377            "proxy select error ({}) for filter: {:?}",
378            self.inner, self.filter
379        )
380    }
381}
382
383impl std::error::Error for ProxySelectError {
384    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
385        Some(self.inner.source().unwrap_or_else(|| self.inner.as_ref()))
386    }
387}
388
389/// A [`Layer`] which wraps an inner [`Service`] to resolve proxy candidates
390/// based on the input extensions and publish route-specific proxy metadata.
391///
392/// See [the crate docs](crate) for examples and more info on the usage of this service.
393#[derive(Debug, Clone)]
394pub struct ProxyDBLayer<D, P, F> {
395    db: D,
396    mode: ProxyFilterMode,
397    predicate: P,
398    username_formatter: F,
399    max_proxies: Option<NonZeroUsize>,
400    overwrite_proxy: bool,
401    single_proxy: bool,
402}
403
404impl<D> ProxyDBLayer<D, bool, ()> {
405    /// Create a new [`ProxyDBLayer`] with the given [`ProxyDB`].
406    pub const fn new(db: D) -> Self {
407        Self {
408            db,
409            mode: ProxyFilterMode::Optional,
410            predicate: true,
411            username_formatter: (),
412            max_proxies: Some(DEFAULT_PROXY_DB_MAX_PROXIES),
413            overwrite_proxy: false,
414            single_proxy: false,
415        }
416    }
417}
418
419impl<D, P, F> ProxyDBLayer<D, P, F> {
420    rama_utils::macros::generate_set_and_with! {
421        /// Limit the number of proxy candidates published for one query.
422        ///
423        /// The default is [`DEFAULT_PROXY_DB_MAX_PROXIES`]. Use
424        /// [`Self::without_max_proxies`] for an unbounded query.
425        pub fn max_proxies(mut self, max_proxies: Option<NonZeroUsize>) -> Self {
426            self.max_proxies = max_proxies;
427            self
428        }
429    }
430
431    rama_utils::macros::generate_set_and_with! {
432        /// Set a [`ProxyFilterMode`] to define the behaviour surrounding
433        /// [`ProxyFilter`] usage, e.g. if a proxy filter is required to be available or not,
434        /// or what to do if it is optional and not available.
435        pub fn filter_mode(mut self, mode: ProxyFilterMode) -> Self {
436            self.mode = mode;
437            self
438        }
439    }
440
441    rama_utils::macros::generate_set_and_with! {
442        /// Select and insert only one proxy instead of publishing every match as
443        /// an ordered route plan. This uses the database's singular-selection
444        /// semantics, preserves legacy pre-fallback behaviour, and is disabled
445        /// by default.
446        pub fn single_proxy(mut self, single_proxy: bool) -> Self {
447            self.single_proxy = single_proxy;
448            self
449        }
450    }
451
452    rama_utils::macros::generate_set_and_with! {
453        /// Overwrite an existing singular [`ProxyRoute`] with the selected
454        /// proxy route or route plan. This is disabled by default in both
455        /// plural and singular selection modes.
456        pub fn overwrite_proxy(mut self, overwrite_proxy: bool) -> Self {
457            self.overwrite_proxy = overwrite_proxy;
458            self
459        }
460    }
461
462    /// Set a [`ProxyQueryPredicate`] that will be used
463    /// to possibly filter out proxies that according to the filters are correct,
464    /// but not according to the predicate.
465    #[must_use]
466    pub fn with_select_predicate<Predicate>(self, p: Predicate) -> ProxyDBLayer<D, Predicate, F> {
467        ProxyDBLayer {
468            db: self.db,
469            mode: self.mode,
470            predicate: p,
471            username_formatter: self.username_formatter,
472            max_proxies: self.max_proxies,
473            overwrite_proxy: self.overwrite_proxy,
474            single_proxy: self.single_proxy,
475        }
476    }
477
478    /// Set an optional [`UsernameFormatter`][crate::UsernameFormatter] for
479    /// Basic-auth proxy usernames. It is called separately for each published
480    /// candidate, so routing labels can depend on that proxy's metadata. Bearer
481    /// credentials and proxies without credentials are left unchanged.
482    #[must_use]
483    pub fn with_username_formatter<Formatter>(self, f: Formatter) -> ProxyDBLayer<D, P, Formatter> {
484        ProxyDBLayer {
485            db: self.db,
486            mode: self.mode,
487            predicate: self.predicate,
488            username_formatter: f,
489            max_proxies: self.max_proxies,
490            overwrite_proxy: self.overwrite_proxy,
491            single_proxy: self.single_proxy,
492        }
493    }
494}
495
496impl<S, D, P, F> Layer<S> for ProxyDBLayer<D, P, F>
497where
498    D: Clone,
499    P: Clone,
500    F: Clone,
501{
502    type Service = ProxyDBService<S, D, P, F>;
503
504    fn layer(&self, inner: S) -> Self::Service {
505        ProxyDBService {
506            inner,
507            db: self.db.clone(),
508            mode: self.mode.clone(),
509            predicate: self.predicate.clone(),
510            username_formatter: self.username_formatter.clone(),
511            max_proxies: self.max_proxies,
512            overwrite_proxy: self.overwrite_proxy,
513            single_proxy: self.single_proxy,
514        }
515    }
516
517    fn into_layer(self, inner: S) -> Self::Service {
518        ProxyDBService {
519            inner,
520            db: self.db,
521            mode: self.mode,
522            predicate: self.predicate,
523            username_formatter: self.username_formatter,
524            max_proxies: self.max_proxies,
525            overwrite_proxy: self.overwrite_proxy,
526            single_proxy: self.single_proxy,
527        }
528    }
529}
530
531/// Formats Basic-auth proxy usernames, for example to add routing labels used
532/// by an upstream proxy router.
533///
534/// The formatter is invoked independently for every proxy candidate. It is not
535/// invoked for bearer credentials or proxies without credentials.
536pub trait UsernameFormatter: Send + Sync + 'static {
537    /// Format the username based on the root properties of the given proxy.
538    fn fmt_username(
539        &self,
540        proxy: &Proxy,
541        filter: &ProxyFilter,
542        username: &str,
543        extensions: &Extensions,
544    ) -> Option<String>;
545}
546
547impl UsernameFormatter for () {
548    fn fmt_username(
549        &self,
550        _proxy: &Proxy,
551        _filter: &ProxyFilter,
552        _username: &str,
553        _extensions: &Extensions,
554    ) -> Option<String> {
555        None
556    }
557}
558
559impl<F> UsernameFormatter for F
560where
561    F: Fn(&Proxy, &ProxyFilter, &str) -> Option<String> + Send + Sync + 'static,
562{
563    fn fmt_username(
564        &self,
565        proxy: &Proxy,
566        filter: &ProxyFilter,
567        username: &str,
568        _extensions: &Extensions,
569    ) -> Option<String> {
570        (self)(proxy, filter, username)
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    #[cfg(feature = "memory-db")]
578    use crate::MemoryProxyDB;
579    use crate::Proxy;
580    #[cfg(all(feature = "memory-db", feature = "csv"))]
581    use crate::{ProxyCsvRowReader, StringFilter};
582    #[cfg(all(feature = "memory-db", feature = "csv"))]
583    use itertools::Itertools;
584    use rama_core::{ServiceInput, extensions::ExtensionsRef, service::service_fn};
585    use rama_http_types::{Body, Request, Version};
586    use rama_net::{
587        Protocol,
588        address::{HostWithPort, ProxyAddress},
589        asn::Asn,
590        client::{
591            ConnectRequest, ConnectionError, ConnectionErrorKind, EstablishedClientConnection,
592            ProxyRoute, ProxyRouteIndex, ProxyRoutesConnector,
593        },
594    };
595    use rama_utils::str::non_empty_str;
596    use std::{
597        convert::Infallible,
598        str::FromStr,
599        sync::{
600            Arc,
601            atomic::{AtomicUsize, Ordering},
602        },
603    };
604
605    fn selected_proxy_address(input: &impl ExtensionsRef) -> Option<&ProxyAddress> {
606        input
607            .extensions()
608            .get_ref::<ProxyRoute>()
609            .and_then(ProxyRoute::proxy_address)
610    }
611
612    fn test_proxy(id: &str, address: &str) -> Proxy {
613        Proxy {
614            id: id.try_into().unwrap(),
615            address: address.parse().unwrap(),
616            tcp: true,
617            udp: false,
618            http: true,
619            https: false,
620            socks5: false,
621            socks5h: false,
622            datacenter: true,
623            residential: false,
624            mobile: false,
625            pool_id: None,
626            continent: None,
627            country: None,
628            state: None,
629            city: None,
630            carrier: None,
631            asn: None,
632        }
633    }
634
635    fn inferred_protocol(port: u16, capabilities: (bool, bool, bool, bool)) -> Protocol {
636        let (http, https, socks5, socks5h) = capabilities;
637        let mut proxy = test_proxy("inferred", &format!("proxy.example:{port}"));
638        proxy.http = http;
639        proxy.https = https;
640        proxy.socks5 = socks5;
641        proxy.socks5h = socks5h;
642
643        prepare_proxy(
644            &(),
645            proxy,
646            &ProxyFilter::default(),
647            TransportProtocol::Tcp,
648            &Extensions::new(),
649        )
650        .unwrap()
651        .route
652        .proxy_address()
653        .unwrap()
654        .protocol
655        .clone()
656        .unwrap()
657    }
658
659    #[test]
660    fn known_ports_prefer_only_supported_protocols() {
661        for (port, capabilities, expected) in [
662            (
663                Protocol::HTTP_DEFAULT_PORT,
664                (true, false, true, false),
665                Protocol::HTTP,
666            ),
667            (
668                Protocol::HTTP_ALT_PORT,
669                (false, false, true, false),
670                Protocol::SOCKS5,
671            ),
672            (
673                Protocol::HTTPS_DEFAULT_PORT,
674                (false, true, true, false),
675                Protocol::HTTPS,
676            ),
677            (
678                Protocol::HTTPS_ALT_PORT,
679                (false, false, true, false),
680                Protocol::SOCKS5,
681            ),
682            (
683                Protocol::SOCKS5_DEFAULT_PORT,
684                (false, false, true, true),
685                Protocol::SOCKS5,
686            ),
687            (
688                Protocol::SOCKS5_DEFAULT_PORT,
689                (false, false, false, true),
690                Protocol::SOCKS5H,
691            ),
692        ] {
693            assert_eq!(
694                inferred_protocol(port, capabilities),
695                expected,
696                "port {port}"
697            );
698        }
699    }
700
701    #[derive(Debug, Clone)]
702    struct OrderedProxyDB(NonEmptyVec<Proxy>);
703
704    impl ProxyDB for OrderedProxyDB {
705        type Error = BoxError;
706
707        async fn get_proxies_if(
708            &self,
709            _ctx: ProxyContext,
710            _filter: ProxyFilter,
711            predicate: impl ProxyQueryPredicate,
712            limit: Option<NonZeroUsize>,
713        ) -> Result<NonEmptyVec<Proxy>, Self::Error> {
714            NonEmptyVec::collect(
715                (&self.0)
716                    .into_iter()
717                    .filter(|proxy| predicate.execute(proxy))
718                    .take(limit.map(NonZeroUsize::get).unwrap_or(usize::MAX))
719                    .cloned(),
720            )
721            .context("ordered test proxy db has no matching proxies")
722        }
723    }
724
725    #[tokio::test]
726    async fn default_mode_retries_candidates_and_records_selected_proxy() {
727        let first = test_proxy("first", "first:secret@a.example:8080");
728        let mut second = test_proxy("second", "second:secret@b.example:1080");
729        second.http = false;
730        second.socks5 = true;
731
732        let db = OrderedProxyDB(NonEmptyVec::from((first, vec![second.clone()])));
733        let attempts = Arc::new(AtomicUsize::new(0));
734        let inner = service_fn({
735            let attempts = attempts.clone();
736            move |input: ConnectRequest| {
737                let attempts = attempts.clone();
738                async move {
739                    let route = input.extensions().get_ref::<ProxyRoute>().unwrap();
740                    let proxy_address = route.proxy_address().unwrap();
741                    let selected = input.extensions().get_ref::<Proxy>().unwrap();
742                    let selected_id = input.extensions().get_ref::<crate::ProxyID>().unwrap();
743                    let attempt = attempts.fetch_add(1, Ordering::SeqCst);
744
745                    if attempt == 0 {
746                        assert_eq!(selected.id, "first");
747                        assert_eq!(selected_id.as_str(), "first");
748                        assert_eq!(
749                            proxy_address.to_string(),
750                            "http://first-first:secret@a.example:8080"
751                        );
752                        Err(ConnectionError::transport(
753                            BoxError::from_static_str("first proxy unavailable"),
754                            ConnectionErrorKind::Unavailable,
755                        ))
756                    } else {
757                        assert_eq!(attempt, 1);
758                        assert_eq!(selected.id, "second");
759                        assert_eq!(selected_id.as_str(), "second");
760                        assert_eq!(
761                            proxy_address.to_string(),
762                            "socks5://second-second:secret@b.example:1080"
763                        );
764                        Ok(EstablishedClientConnection {
765                            input,
766                            conn: ServiceInput::new(()),
767                        })
768                    }
769                }
770            }
771        });
772        let service = ProxyDBLayer::new(db)
773            .with_filter_mode(ProxyFilterMode::Default)
774            .with_username_formatter(|proxy: &Proxy, _filter: &ProxyFilter, username: &str| {
775                Some(format!("{username}-{}", proxy.id))
776            })
777            .into_layer(ProxyRoutesConnector::new(inner));
778        let input = ConnectRequest::new("www.example.com:443".parse().unwrap());
779
780        let established = service.serve(input).await.unwrap();
781
782        assert_eq!(attempts.load(Ordering::SeqCst), 2);
783        assert_eq!(
784            established
785                .input
786                .extensions()
787                .get_ref::<ProxyRouteIndex>()
788                .copied()
789                .map(ProxyRouteIndex::get),
790            Some(1)
791        );
792        let selected = established.input.extensions().get_ref::<Proxy>().unwrap();
793        assert_eq!(selected.id, second.id);
794        assert_eq!(selected.address, second.address);
795        assert_eq!(selected.socks5, second.socks5);
796        assert_eq!(
797            established
798                .input
799                .extensions()
800                .get_ref::<crate::ProxyID>()
801                .map(crate::ProxyID::as_str),
802            Some("second")
803        );
804        assert!(established.conn.extensions().get_ref::<Proxy>().is_none());
805        assert!(
806            established
807                .conn
808                .extensions()
809                .get_ref::<crate::ProxyID>()
810                .is_none()
811        );
812    }
813
814    #[tokio::test]
815    async fn default_mode_accepts_output_without_extensions() {
816        let first = test_proxy("first", "a.example:8080");
817        let second = test_proxy("second", "b.example:8080");
818        let db = OrderedProxyDB(NonEmptyVec::from((first, vec![second])));
819        let inner = service_fn(|input: ConnectRequest| async move {
820            let routes = input.extensions().get_ref::<ProxyRoutes>().unwrap();
821            assert_eq!(routes.as_slice().len(), 2);
822            assert_eq!(
823                routes
824                    .route_extensions(1)
825                    .and_then(|extensions| extensions.get_ref::<crate::ProxyID>())
826                    .map(crate::ProxyID::as_str),
827                Some("second")
828            );
829            Ok::<_, Infallible>(42_u8)
830        });
831        let service = ProxyDBLayer::new(db)
832            .with_filter_mode(ProxyFilterMode::Default)
833            .into_layer(inner);
834        let input = ConnectRequest::new("www.example.com:443".parse().unwrap());
835
836        assert_eq!(service.serve(input).await.unwrap(), 42);
837    }
838
839    enum TestProxyLimit {
840        Default,
841        Bounded(NonZeroUsize),
842        Unbounded,
843    }
844
845    async fn attempted_proxy_count(configured_limit: TestProxyLimit) -> usize {
846        let proxies = (0..7)
847            .map(|index| {
848                test_proxy(
849                    &format!("proxy-{index}"),
850                    &format!("proxy-{index}.example:8080"),
851                )
852            })
853            .collect::<Vec<_>>();
854        let db = OrderedProxyDB(NonEmptyVec::try_from(proxies).unwrap());
855        let attempts = Arc::new(AtomicUsize::new(0));
856        let inner = service_fn({
857            let attempts = attempts.clone();
858            move |_input: ConnectRequest| {
859                attempts.fetch_add(1, Ordering::SeqCst);
860                async {
861                    Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
862                        ConnectionError::transport(
863                            BoxError::from_static_str("proxy unavailable"),
864                            ConnectionErrorKind::Unavailable,
865                        ),
866                    )
867                }
868            }
869        });
870        let layer = ProxyDBLayer::new(db).with_filter_mode(ProxyFilterMode::Default);
871        let layer = match configured_limit {
872            TestProxyLimit::Default => layer,
873            TestProxyLimit::Bounded(limit) => layer.with_max_proxies(limit),
874            TestProxyLimit::Unbounded => layer.without_max_proxies(),
875        };
876        let service = layer.into_layer(ProxyRoutesConnector::new(inner));
877
878        let _error = service
879            .serve(ConnectRequest::new("www.example.com:443".parse().unwrap()))
880            .await
881            .unwrap_err();
882
883        attempts.load(Ordering::SeqCst)
884    }
885
886    #[tokio::test]
887    async fn plural_mode_defaults_to_five_candidates() {
888        assert_eq!(attempted_proxy_count(TestProxyLimit::Default).await, 5);
889    }
890
891    #[tokio::test]
892    async fn plural_mode_candidate_limit_is_configurable_or_unbounded() {
893        assert_eq!(
894            attempted_proxy_count(TestProxyLimit::Bounded(NonZeroUsize::new(2).unwrap())).await,
895            2
896        );
897        assert_eq!(attempted_proxy_count(TestProxyLimit::Unbounded).await, 7);
898    }
899
900    #[tokio::test]
901    async fn route_index_correlates_duplicate_proxy_addresses() {
902        let first = test_proxy("first", "duplicate.example:8080");
903        let second = test_proxy("second", "duplicate.example:8080");
904        let db = OrderedProxyDB(NonEmptyVec::from((first, vec![second])));
905        let inner = service_fn(async |input: ConnectRequest| {
906            if input
907                .extensions()
908                .get_ref::<ProxyRouteIndex>()
909                .copied()
910                .map(ProxyRouteIndex::get)
911                == Some(0)
912            {
913                Err(ConnectionError::transport(
914                    BoxError::from_static_str("first candidate unavailable"),
915                    ConnectionErrorKind::Unavailable,
916                ))
917            } else {
918                Ok(EstablishedClientConnection {
919                    input,
920                    conn: ServiceInput::new(()),
921                })
922            }
923        });
924        let service = ProxyDBLayer::new(db)
925            .with_filter_mode(ProxyFilterMode::Default)
926            .into_layer(ProxyRoutesConnector::new(inner));
927
928        let established = service
929            .serve(ConnectRequest::new("www.example.com:443".parse().unwrap()))
930            .await
931            .unwrap();
932
933        assert_eq!(
934            established
935                .input
936                .extensions()
937                .get_ref::<crate::ProxyID>()
938                .map(crate::ProxyID::as_str),
939            Some("second")
940        );
941        assert_eq!(
942            established
943                .input
944                .extensions()
945                .get_ref::<Proxy>()
946                .unwrap()
947                .id,
948            "second"
949        );
950    }
951
952    #[tokio::test]
953    async fn existing_singular_route_is_preserved_by_default_in_multi_mode() {
954        let db = OrderedProxyDB(NonEmptyVec::new(test_proxy(
955            "database",
956            "database.example:8080",
957        )));
958        let service = ProxyDBLayer::new(db)
959            .with_filter_mode(ProxyFilterMode::Default)
960            .into_layer(service_fn(async |input: Request| {
961                Ok::<_, Infallible>(input)
962            }));
963        let input = Request::builder()
964            .uri("https://example.com")
965            .body(Body::empty())
966            .unwrap();
967        input
968            .extensions()
969            .insert(ProxyRoute::Proxy("existing.example:8080".parse().unwrap()));
970
971        let output = service.serve(input).await.unwrap();
972
973        assert_eq!(
974            selected_proxy_address(&output).unwrap().address.to_string(),
975            "existing.example:8080"
976        );
977        assert!(output.extensions().get_ref::<ProxyRoutes>().is_none());
978        assert!(output.extensions().get_ref::<Proxy>().is_none());
979        assert!(output.extensions().get_ref::<crate::ProxyID>().is_none());
980    }
981
982    #[tokio::test]
983    async fn single_mode_can_opt_into_overwriting_existing_route() {
984        let db = OrderedProxyDB(NonEmptyVec::new(test_proxy(
985            "database",
986            "database.example:8080",
987        )));
988        let service = ProxyDBLayer::new(db)
989            .with_filter_mode(ProxyFilterMode::Default)
990            .with_single_proxy(true)
991            .with_overwrite_proxy(true)
992            .into_layer(service_fn(async |input: Request| {
993                Ok::<_, Infallible>(input)
994            }));
995        let input = Request::builder()
996            .uri("https://example.com")
997            .body(Body::empty())
998            .unwrap();
999        input
1000            .extensions()
1001            .insert(ProxyRoute::Proxy("existing.example:8080".parse().unwrap()));
1002
1003        let output = service.serve(input).await.unwrap();
1004
1005        assert_eq!(
1006            selected_proxy_address(&output).unwrap().address.to_string(),
1007            "database.example:8080"
1008        );
1009        assert_eq!(
1010            output
1011                .extensions()
1012                .get_ref::<crate::ProxyID>()
1013                .map(crate::ProxyID::as_str),
1014            Some("database")
1015        );
1016    }
1017
1018    #[tokio::test]
1019    async fn multi_mode_can_opt_into_overwriting_existing_route() {
1020        let db = OrderedProxyDB(NonEmptyVec::new(test_proxy(
1021            "database",
1022            "database.example:8080",
1023        )));
1024        let inner = service_fn(async |input: ConnectRequest| {
1025            Ok::<_, ConnectionError>(EstablishedClientConnection {
1026                input,
1027                conn: ServiceInput::new(()),
1028            })
1029        });
1030        let service = ProxyDBLayer::new(db)
1031            .with_filter_mode(ProxyFilterMode::Default)
1032            .with_overwrite_proxy(true)
1033            .into_layer(ProxyRoutesConnector::new(inner));
1034        let input = ConnectRequest::new("www.example.com:443".parse().unwrap());
1035        input
1036            .extensions()
1037            .insert(ProxyRoute::Proxy("existing.example:8080".parse().unwrap()));
1038
1039        let output = service.serve(input).await.unwrap();
1040
1041        assert_eq!(
1042            selected_proxy_address(&output.input)
1043                .unwrap()
1044                .address
1045                .to_string(),
1046            "database.example:8080"
1047        );
1048        assert_eq!(
1049            output
1050                .input
1051                .extensions()
1052                .get_ref::<crate::ProxyID>()
1053                .map(crate::ProxyID::as_str),
1054            Some("database")
1055        );
1056    }
1057
1058    #[tokio::test]
1059    async fn optional_mode_without_filter_adds_no_proxy_state() {
1060        let db = OrderedProxyDB(NonEmptyVec::new(test_proxy(
1061            "database",
1062            "database.example:8080",
1063        )));
1064        let service = ProxyDBLayer::new(db).into_layer(service_fn(async |input: Request| {
1065            Ok::<_, Infallible>(input)
1066        }));
1067        let input = Request::builder()
1068            .uri("https://example.com")
1069            .body(Body::empty())
1070            .unwrap();
1071
1072        let output = service.serve(input).await.unwrap();
1073
1074        assert!(output.extensions().get_ref::<ProxyRoute>().is_none());
1075        assert!(output.extensions().get_ref::<ProxyRoutes>().is_none());
1076        assert!(output.extensions().get_ref::<Proxy>().is_none());
1077        assert!(output.extensions().get_ref::<crate::ProxyID>().is_none());
1078    }
1079
1080    #[tokio::test]
1081    async fn plural_db_miss_does_not_call_inner_or_fall_back_direct() {
1082        let inner_calls = Arc::new(AtomicUsize::new(0));
1083        let service = ProxyDBLayer::new(())
1084            .with_filter_mode(ProxyFilterMode::Default)
1085            .into_layer(service_fn({
1086                let inner_calls = inner_calls.clone();
1087                move |input: Request| {
1088                    inner_calls.fetch_add(1, Ordering::SeqCst);
1089                    async move { Ok::<_, Infallible>(input) }
1090                }
1091            }));
1092        let input = Request::builder()
1093            .uri("https://example.com")
1094            .body(Body::empty())
1095            .unwrap();
1096
1097        service.serve(input).await.unwrap_err();
1098
1099        assert_eq!(inner_calls.load(Ordering::SeqCst), 0);
1100    }
1101
1102    #[tokio::test]
1103    #[cfg(feature = "memory-db")]
1104    async fn test_proxy_db_default_happy_path_example() {
1105        let db = MemoryProxyDB::try_from_iter([
1106            Proxy {
1107                id: non_empty_str!("42"),
1108                address: ProxyAddress::from_str("12.34.12.34:8080").unwrap(),
1109                tcp: true,
1110                udp: true,
1111                http: true,
1112                https: true,
1113                socks5: true,
1114                socks5h: true,
1115                datacenter: false,
1116                residential: true,
1117                mobile: true,
1118                pool_id: None,
1119                continent: Some("*".into()),
1120                country: Some("*".into()),
1121                state: Some("*".into()),
1122                city: Some("*".into()),
1123                carrier: Some("*".into()),
1124                asn: Some(Asn::unspecified()),
1125            },
1126            Proxy {
1127                id: non_empty_str!("100"),
1128                address: ProxyAddress::from_str("12.34.12.35:8080").unwrap(),
1129                tcp: true,
1130                udp: false,
1131                http: true,
1132                https: true,
1133                socks5: false,
1134                socks5h: false,
1135                datacenter: true,
1136                residential: false,
1137                mobile: false,
1138                pool_id: None,
1139                continent: Some("americas".into()),
1140                country: Some("US".into()),
1141                state: None,
1142                city: None,
1143                carrier: None,
1144                asn: Some(Asn::unspecified()),
1145            },
1146        ])
1147        .unwrap();
1148
1149        let service = ProxyDBLayer::new(Arc::new(db))
1150            .with_filter_mode(ProxyFilterMode::Default)
1151            .into_layer(ProxyRoutesConnector::new(service_fn(
1152                async |req: ConnectRequest| {
1153                    Ok::<_, ConnectionError>(EstablishedClientConnection {
1154                        input: req,
1155                        conn: ServiceInput::new(()),
1156                    })
1157                },
1158            )));
1159
1160        let req = ConnectRequest::new("www.example.com:443".parse().unwrap())
1161            .with_application_protocol(Protocol::HTTPS);
1162
1163        req.extensions().insert(ProxyFilter {
1164            country: Some(vec!["BE".into()]),
1165            mobile: Some(true),
1166            residential: Some(true),
1167            ..Default::default()
1168        });
1169
1170        let established = service.serve(req).await.unwrap();
1171        let proxy_address = selected_proxy_address(&established.input).unwrap();
1172        assert_eq!(
1173            proxy_address.address,
1174            HostWithPort::from(([12, 34, 12, 34], 8080))
1175        );
1176        assert_eq!(
1177            established
1178                .input
1179                .extensions()
1180                .get_ref::<Proxy>()
1181                .map(|p| p.id.as_ref()),
1182            Some("42")
1183        );
1184        assert_eq!(
1185            established
1186                .input
1187                .extensions()
1188                .get_ref::<crate::ProxyID>()
1189                .map(crate::ProxyID::as_str),
1190            Some("42")
1191        );
1192    }
1193
1194    #[tokio::test]
1195    async fn test_proxy_db_single_proxy_example() {
1196        let proxy = Proxy {
1197            id: non_empty_str!("42"),
1198            address: ProxyAddress::from_str("12.34.12.34:8080").unwrap(),
1199            tcp: true,
1200            udp: true,
1201            http: true,
1202            https: true,
1203            socks5: true,
1204            socks5h: true,
1205            datacenter: false,
1206            residential: true,
1207            mobile: true,
1208            pool_id: None,
1209            continent: Some("*".into()),
1210            country: Some("*".into()),
1211            state: Some("*".into()),
1212            city: Some("*".into()),
1213            carrier: Some("*".into()),
1214            asn: Some(Asn::unspecified()),
1215        };
1216
1217        let service = ProxyDBLayer::new(Arc::new(proxy))
1218            .with_filter_mode(ProxyFilterMode::Default)
1219            .with_single_proxy(true)
1220            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1221
1222        let req = Request::builder()
1223            .version(Version::HTTP_3)
1224            .method("GET")
1225            .uri("https://example.com")
1226            .body(Body::empty())
1227            .unwrap();
1228
1229        req.extensions().insert(ProxyFilter {
1230            country: Some(vec!["BE".into()]),
1231            mobile: Some(true),
1232            residential: Some(true),
1233            ..Default::default()
1234        });
1235
1236        let output = service.serve(req).await.unwrap();
1237        let proxy_address = selected_proxy_address(&output).unwrap();
1238        assert_eq!(
1239            proxy_address.address,
1240            HostWithPort::from(([12, 34, 12, 34], 8080))
1241        );
1242        assert!(output.extensions().get_ref::<ProxyRoutes>().is_none());
1243        assert_eq!(
1244            output
1245                .extensions()
1246                .get_ref::<Proxy>()
1247                .map(|p| p.id.as_ref()),
1248            Some("42")
1249        );
1250    }
1251
1252    #[tokio::test]
1253    async fn test_proxy_db_single_proxy_with_username_formatter() {
1254        let proxy = Proxy {
1255            id: non_empty_str!("42"),
1256            address: ProxyAddress::from_str("john:secret@12.34.12.34:8080").unwrap(),
1257            tcp: true,
1258            udp: true,
1259            http: true,
1260            https: true,
1261            socks5: true,
1262            socks5h: true,
1263            datacenter: false,
1264            residential: true,
1265            mobile: true,
1266            pool_id: Some("routers".into()),
1267            continent: Some("*".into()),
1268            country: Some("*".into()),
1269            state: Some("*".into()),
1270            city: Some("*".into()),
1271            carrier: Some("*".into()),
1272            asn: Some(Asn::unspecified()),
1273        };
1274
1275        let service = ProxyDBLayer::new(Arc::new(proxy))
1276            .with_filter_mode(ProxyFilterMode::Default)
1277            .with_single_proxy(true)
1278            .with_username_formatter(|proxy: &Proxy, filter: &ProxyFilter, username: &str| {
1279                if proxy
1280                    .pool_id
1281                    .as_ref()
1282                    .map(|id| id.as_ref() == "routers")
1283                    .unwrap_or_default()
1284                {
1285                    use std::fmt::Write;
1286
1287                    let mut output = String::new();
1288
1289                    if let Some(countries) = filter.country.as_ref().filter(|t| !t.is_empty()) {
1290                        _ = write!(output, "country-{}", countries[0]);
1291                    }
1292                    if let Some(states) = filter.state.as_ref().filter(|t| !t.is_empty()) {
1293                        _ = write!(output, "state-{}", states[0]);
1294                    }
1295
1296                    return (!output.is_empty()).then(|| format!("{username}-{output}"));
1297                }
1298
1299                None
1300            })
1301            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1302
1303        let req = Request::builder()
1304            .version(Version::HTTP_3)
1305            .method("GET")
1306            .uri("https://example.com")
1307            .body(Body::empty())
1308            .unwrap();
1309
1310        req.extensions().insert(ProxyFilter {
1311            country: Some(vec!["BE".into()]),
1312            mobile: Some(true),
1313            residential: Some(true),
1314            ..Default::default()
1315        });
1316
1317        let output = service.serve(req).await.unwrap();
1318        let proxy_address = selected_proxy_address(&output).unwrap();
1319        assert_eq!(
1320            "socks5://john-country-be:secret@12.34.12.34:8080",
1321            proxy_address.to_string()
1322        );
1323    }
1324
1325    #[tokio::test]
1326    #[cfg(feature = "memory-db")]
1327    async fn test_proxy_db_legacy_single_proxy_transport_layer() {
1328        let db = MemoryProxyDB::try_from_iter([
1329            Proxy {
1330                id: non_empty_str!("42"),
1331                address: ProxyAddress::from_str("12.34.12.34:8080").unwrap(),
1332                tcp: true,
1333                udp: true,
1334                http: true,
1335                https: true,
1336                socks5: true,
1337                socks5h: true,
1338                datacenter: false,
1339                residential: true,
1340                mobile: true,
1341                pool_id: None,
1342                continent: Some("*".into()),
1343                country: Some("*".into()),
1344                state: Some("*".into()),
1345                city: Some("*".into()),
1346                carrier: Some("*".into()),
1347                asn: Some(Asn::unspecified()),
1348            },
1349            Proxy {
1350                id: non_empty_str!("100"),
1351                address: ProxyAddress::from_str("12.34.12.35:8080").unwrap(),
1352                tcp: true,
1353                udp: false,
1354                http: true,
1355                https: true,
1356                socks5: false,
1357                socks5h: false,
1358                datacenter: true,
1359                residential: false,
1360                mobile: false,
1361                pool_id: None,
1362                continent: Some("americas".into()),
1363                country: Some("US".into()),
1364                state: None,
1365                city: None,
1366                carrier: None,
1367                asn: Some(Asn::unspecified()),
1368            },
1369        ])
1370        .unwrap();
1371
1372        let service = ProxyDBLayer::new(Arc::new(db))
1373            .with_filter_mode(ProxyFilterMode::Default)
1374            .with_single_proxy(true)
1375            .into_layer(service_fn(async |req: ConnectRequest| {
1376                Ok::<_, Infallible>(req)
1377            }));
1378
1379        let req = ConnectRequest::new("www.example.com:443".parse().unwrap())
1380            .with_application_protocol(Protocol::HTTPS);
1381
1382        req.extensions().insert(ProxyFilter {
1383            country: Some(vec!["BE".into()]),
1384            mobile: Some(true),
1385            residential: Some(true),
1386            ..Default::default()
1387        });
1388
1389        let output = service.serve(req).await.unwrap();
1390        let proxy_address = selected_proxy_address(&output).unwrap();
1391        assert_eq!(
1392            proxy_address.address,
1393            HostWithPort::from(([12, 34, 12, 34], 8080))
1394        );
1395    }
1396
1397    #[cfg(all(feature = "memory-db", feature = "csv"))]
1398    const RAW_CSV_DATA: &str = include_str!("./test_proxydb_rows.csv");
1399
1400    #[cfg(all(feature = "memory-db", feature = "csv"))]
1401    async fn memproxydb() -> MemoryProxyDB {
1402        let mut reader = ProxyCsvRowReader::raw(RAW_CSV_DATA);
1403        let mut rows = Vec::new();
1404        while let Some(proxy) = reader.next().await.unwrap() {
1405            rows.push(proxy);
1406        }
1407        MemoryProxyDB::try_from_rows(rows).unwrap()
1408    }
1409
1410    #[tokio::test]
1411    #[cfg(all(feature = "memory-db", feature = "csv"))]
1412    async fn single_mode_preserves_existing_proxy_address_by_default() {
1413        let db = memproxydb().await;
1414
1415        let service = ProxyDBLayer::new(Arc::new(db))
1416            .with_filter_mode(ProxyFilterMode::Default)
1417            .with_single_proxy(true)
1418            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1419
1420        let req = Request::builder()
1421            .version(Version::HTTP_11)
1422            .method("GET")
1423            .uri("http://example.com")
1424            .body(Body::empty())
1425            .unwrap();
1426
1427        req.extensions().insert(ProxyRoute::Proxy(
1428            ProxyAddress::try_from("http://john:secret@1.2.3.4:1234").unwrap(),
1429        ));
1430
1431        let output = service.serve(req).await.unwrap();
1432        let proxy_address = selected_proxy_address(&output).unwrap();
1433
1434        assert_eq!(proxy_address.address.to_string(), "1.2.3.4:1234");
1435        assert!(output.extensions().get_ref::<Proxy>().is_none());
1436        assert!(output.extensions().get_ref::<crate::ProxyID>().is_none());
1437    }
1438
1439    #[tokio::test]
1440    #[cfg(all(feature = "memory-db", feature = "csv"))]
1441    async fn test_proxy_db_service_optional() {
1442        let db = memproxydb().await;
1443
1444        let service = ProxyDBLayer::new(Arc::new(db))
1445            .with_single_proxy(true)
1446            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1447
1448        for (filter, expected_authority, req) in [
1449            (
1450                None,
1451                None,
1452                Request::builder()
1453                    .version(Version::HTTP_11)
1454                    .method("GET")
1455                    .uri("http://example.com")
1456                    .body(Body::empty())
1457                    .unwrap(),
1458            ),
1459            (
1460                Some(ProxyFilter {
1461                    id: Some(non_empty_str!("3031533634")),
1462                    ..Default::default()
1463                }),
1464                Some("105.150.55.60:4898"),
1465                Request::builder()
1466                    .version(Version::HTTP_11)
1467                    .method("GET")
1468                    .uri("http://example.com")
1469                    .body(Body::empty())
1470                    .unwrap(),
1471            ),
1472            (
1473                Some(ProxyFilter {
1474                    country: Some(vec![StringFilter::new("BE")]),
1475                    mobile: Some(true),
1476                    residential: Some(true),
1477                    ..Default::default()
1478                }),
1479                Some("140.249.154.18:5800"),
1480                Request::builder()
1481                    .version(Version::HTTP_3)
1482                    .method("GET")
1483                    .uri("https://example.com")
1484                    .body(Body::empty())
1485                    .unwrap(),
1486            ),
1487        ] {
1488            if let Some(filter) = filter {
1489                req.extensions().insert(filter);
1490            }
1491
1492            let output = service.serve(req).await.unwrap();
1493            let maybe_proxy_address = selected_proxy_address(&output);
1494
1495            assert_eq!(
1496                maybe_proxy_address.map(|p| p.address.clone()),
1497                expected_authority.map(|s| HostWithPort::try_from(s).unwrap())
1498            );
1499        }
1500    }
1501
1502    #[tokio::test]
1503    #[cfg(all(feature = "memory-db", feature = "csv"))]
1504    async fn test_proxy_db_legacy_single_proxy_default_filter() {
1505        let db = memproxydb().await;
1506
1507        let service = ProxyDBLayer::new(Arc::new(db))
1508            .with_filter_mode(ProxyFilterMode::Default)
1509            .with_single_proxy(true)
1510            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1511
1512        for (filter, expected_addresses, req_info) in [
1513            (
1514                None,
1515                "0.20.204.227:8373,104.207.92.167:9387,105.150.55.60:4898,106.213.197.28:9110,113.6.21.212:4525,115.29.251.35:5712,119.146.94.132:7851,129.204.152.130:6524,134.190.189.202:5772,136.186.95.10:7095,137.220.180.169:4929,140.249.154.18:5800,145.57.31.149:6304,151.254.135.9:6961,153.206.209.221:8696,162.97.174.152:1673,169.179.161.206:6843,171.174.56.89:5744,178.189.117.217:6496,182.34.76.182:2374,184.209.230.177:1358,193.188.239.29:3541,193.26.37.125:3780,204.168.216.113:1096,208.224.120.97:7118,209.176.177.182:4311,215.49.63.89:9458,223.234.242.63:7211,230.159.143.41:7296,233.22.59.115:1653,24.155.249.112:2645,247.118.71.100:1033,249.221.15.121:7434,252.69.242.136:4791,253.138.153.41:2640,28.139.151.127:2809,4.20.243.186:9155,42.54.35.118:6846,45.59.69.12:5934,46.247.45.238:3522,54.226.47.54:7442,61.112.212.160:3842,66.142.40.209:4251,66.171.139.181:4449,69.246.162.84:8964,75.43.123.181:7719,76.128.58.167:4797,85.14.163.105:8362,92.227.104.237:6161,97.192.206.72:6067",
1516                (Version::HTTP_11, "GET", "http://example.com"),
1517            ),
1518            (
1519                Some(ProxyFilter {
1520                    country: Some(vec![StringFilter::new("BE")]),
1521                    mobile: Some(true),
1522                    residential: Some(true),
1523                    ..Default::default()
1524                }),
1525                "140.249.154.18:5800",
1526                (Version::HTTP_3, "GET", "https://example.com"),
1527            ),
1528        ] {
1529            let mut seen_addresses = Vec::new();
1530            for _ in 0..5000 {
1531                let req = Request::builder()
1532                    .version(req_info.0)
1533                    .method(req_info.1)
1534                    .uri(req_info.2)
1535                    .body(Body::empty())
1536                    .unwrap();
1537
1538                if let Some(filter) = filter.clone() {
1539                    req.extensions().insert(filter);
1540                }
1541
1542                let output = service.serve(req).await.unwrap();
1543                let proxy_address = selected_proxy_address(&output).unwrap().address.to_string();
1544
1545                if !seen_addresses.contains(&proxy_address) {
1546                    seen_addresses.push(proxy_address);
1547                }
1548            }
1549
1550            let seen_addresses = seen_addresses.into_iter().sorted().join(",");
1551            assert_eq!(seen_addresses, expected_addresses);
1552        }
1553    }
1554
1555    #[tokio::test]
1556    #[cfg(all(feature = "memory-db", feature = "csv"))]
1557    async fn test_proxy_db_legacy_single_proxy_fallback_filter() {
1558        let db = memproxydb().await;
1559
1560        let service = ProxyDBLayer::new(Arc::new(db))
1561            .with_filter_mode(ProxyFilterMode::Fallback(ProxyFilter {
1562                datacenter: Some(true),
1563                residential: Some(false),
1564                mobile: Some(false),
1565                ..Default::default()
1566            }))
1567            .with_single_proxy(true)
1568            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1569
1570        for (filter, expected_addresses, req_info) in [
1571            (
1572                None,
1573                "113.6.21.212:4525,119.146.94.132:7851,136.186.95.10:7095,137.220.180.169:4929,247.118.71.100:1033,249.221.15.121:7434,92.227.104.237:6161",
1574                (Version::HTTP_11, "GET", "http://example.com"),
1575            ),
1576            (
1577                Some(ProxyFilter {
1578                    country: Some(vec![StringFilter::new("BE")]),
1579                    mobile: Some(true),
1580                    residential: Some(true),
1581                    ..Default::default()
1582                }),
1583                "140.249.154.18:5800",
1584                (Version::HTTP_3, "GET", "https://example.com"),
1585            ),
1586        ] {
1587            let mut seen_addresses = Vec::new();
1588            for _ in 0..5000 {
1589                let req = Request::builder()
1590                    .version(req_info.0)
1591                    .method(req_info.1)
1592                    .uri(req_info.2)
1593                    .body(Body::empty())
1594                    .unwrap();
1595
1596                if let Some(filter) = filter.clone() {
1597                    req.extensions().insert(filter);
1598                }
1599
1600                let output = service.serve(req).await.unwrap();
1601                let proxy_address = selected_proxy_address(&output).unwrap().address.to_string();
1602
1603                if !seen_addresses.contains(&proxy_address) {
1604                    seen_addresses.push(proxy_address);
1605                }
1606            }
1607
1608            let seen_addresses = seen_addresses.into_iter().sorted().join(",");
1609            assert_eq!(seen_addresses, expected_addresses);
1610        }
1611    }
1612
1613    #[tokio::test]
1614    #[cfg(all(feature = "memory-db", feature = "csv"))]
1615    async fn test_proxy_db_service_required() {
1616        let db = memproxydb().await;
1617
1618        let service = ProxyDBLayer::new(Arc::new(db))
1619            .with_filter_mode(ProxyFilterMode::Required)
1620            .with_single_proxy(true)
1621            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1622
1623        for (filter, expected_address, req) in [
1624            (
1625                None,
1626                None,
1627                Request::builder()
1628                    .version(Version::HTTP_11)
1629                    .method("GET")
1630                    .uri("http://example.com")
1631                    .body(Body::empty())
1632                    .unwrap(),
1633            ),
1634            (
1635                Some(ProxyFilter {
1636                    country: Some(vec![StringFilter::new("BE")]),
1637                    mobile: Some(true),
1638                    residential: Some(true),
1639                    ..Default::default()
1640                }),
1641                Some("140.249.154.18:5800"),
1642                Request::builder()
1643                    .version(Version::HTTP_3)
1644                    .method("GET")
1645                    .uri("https://example.com")
1646                    .body(Body::empty())
1647                    .unwrap(),
1648            ),
1649            (
1650                Some(ProxyFilter {
1651                    id: Some(non_empty_str!("FooBar")),
1652                    ..Default::default()
1653                }),
1654                None,
1655                Request::builder()
1656                    .version(Version::HTTP_3)
1657                    .method("GET")
1658                    .uri("https://example.com")
1659                    .body(Body::empty())
1660                    .unwrap(),
1661            ),
1662            (
1663                Some(ProxyFilter {
1664                    id: Some(non_empty_str!("1316455915")),
1665                    country: Some(vec![StringFilter::new("BE")]),
1666                    mobile: Some(true),
1667                    residential: Some(true),
1668                    ..Default::default()
1669                }),
1670                None,
1671                Request::builder()
1672                    .version(Version::HTTP_3)
1673                    .method("GET")
1674                    .uri("https://example.com")
1675                    .body(Body::empty())
1676                    .unwrap(),
1677            ),
1678        ] {
1679            if let Some(filter) = filter.clone() {
1680                req.extensions().insert(filter);
1681            }
1682
1683            let proxy_address_result = service.serve(req).await;
1684            match expected_address {
1685                Some(expected_address) => {
1686                    assert_eq!(
1687                        selected_proxy_address(&proxy_address_result.unwrap())
1688                            .unwrap()
1689                            .address,
1690                        HostWithPort::try_from(expected_address).unwrap()
1691                    );
1692                }
1693                None => {
1694                    proxy_address_result.unwrap_err();
1695                }
1696            }
1697        }
1698    }
1699
1700    #[tokio::test]
1701    #[cfg(all(feature = "memory-db", feature = "csv"))]
1702    async fn test_proxy_db_service_required_with_predicate() {
1703        let db = memproxydb().await;
1704
1705        let service = ProxyDBLayer::new(Arc::new(db))
1706            .with_filter_mode(ProxyFilterMode::Required)
1707            .with_single_proxy(true)
1708            .with_select_predicate(|proxy: &Proxy| proxy.mobile)
1709            .into_layer(service_fn(async |req: Request| Ok::<_, Infallible>(req)));
1710
1711        for (filter, expected, req) in [
1712            (
1713                None,
1714                None,
1715                Request::builder()
1716                    .version(Version::HTTP_11)
1717                    .method("GET")
1718                    .uri("http://example.com")
1719                    .body(Body::empty())
1720                    .unwrap(),
1721            ),
1722            (
1723                Some(ProxyFilter {
1724                    country: Some(vec![StringFilter::new("BE")]),
1725                    mobile: Some(true),
1726                    residential: Some(true),
1727                    ..Default::default()
1728                }),
1729                Some("140.249.154.18:5800"),
1730                Request::builder()
1731                    .version(Version::HTTP_3)
1732                    .method("GET")
1733                    .uri("https://example.com")
1734                    .body(Body::empty())
1735                    .unwrap(),
1736            ),
1737            (
1738                Some(ProxyFilter {
1739                    id: Some(non_empty_str!("FooBar")),
1740                    ..Default::default()
1741                }),
1742                None,
1743                Request::builder()
1744                    .version(Version::HTTP_3)
1745                    .method("GET")
1746                    .uri("https://example.com")
1747                    .body(Body::empty())
1748                    .unwrap(),
1749            ),
1750            (
1751                Some(ProxyFilter {
1752                    id: Some(non_empty_str!("1316455915")),
1753                    country: Some(vec![StringFilter::new("BE")]),
1754                    mobile: Some(true),
1755                    residential: Some(true),
1756                    ..Default::default()
1757                }),
1758                None,
1759                Request::builder()
1760                    .version(Version::HTTP_3)
1761                    .method("GET")
1762                    .uri("https://example.com")
1763                    .body(Body::empty())
1764                    .unwrap(),
1765            ),
1766            // match found, but due to custom predicate it won't check, given it is not mobile
1767            (
1768                Some(ProxyFilter {
1769                    id: Some(non_empty_str!("1316455915")),
1770                    ..Default::default()
1771                }),
1772                None,
1773                Request::builder()
1774                    .version(Version::HTTP_3)
1775                    .method("GET")
1776                    .uri("https://example.com")
1777                    .body(Body::empty())
1778                    .unwrap(),
1779            ),
1780        ] {
1781            if let Some(filter) = filter {
1782                req.extensions().insert(filter);
1783            }
1784
1785            let proxy_result = service.serve(req).await;
1786            match expected {
1787                Some(expected_address) => {
1788                    assert_eq!(
1789                        selected_proxy_address(&proxy_result.unwrap())
1790                            .unwrap()
1791                            .address,
1792                        HostWithPort::try_from(expected_address).unwrap()
1793                    );
1794                }
1795                None => {
1796                    proxy_result.unwrap_err();
1797                }
1798            }
1799        }
1800    }
1801}