Skip to main content

rama_proxy/
lib.rs

1//! rama proxy types and utilities
2//!
3//! Proxy protocols are implemented in their relevant crates:
4//!
5//! - HaProxy: `rama-haproxy`
6//! - HttpProxy: `rama-http-backend`
7//!
8//! See the [`ProxyFilter`] for more information on how to select a proxy,
9//! and the [`ProxyDB`] trait for how to implement a proxy database.
10//!
11//! If you wish to support proxy filters directly from the username,
12//! you can use the [`ProxyFilterUsernameParser`] to extract the proxy filter
13//! so it will be added to the input [`Extensions`].
14//!
15//! The [`ProxyDB`] is used by Connection Pools to connect via a proxy,
16//! in case a [`ProxyFilter`] is present in the input [`Extensions`].
17//!
18//! # DB Live Reloads
19//!
20//! [`ProxyDB`] implementations like the [`MemoryProxyDB`] feel static in nature, and they are.
21//! The goal is really to load it once and read it often as fast as possible.
22//!
23//! In fact, given that we access everything through shared references,
24//! there is also no cheap way to mutate it all the time.
25//!
26//! As such the normal way to update data such as your proxy list
27//! is by performing a rolling update of your actual rama-driven proxy workloads.
28//!
29//! That said. By using crates such as [left-right](https://crates.io/crates/left-right)
30//! you can relatively affordable perform live reloads by having the writer on its own tokio
31//! task and wrap the reader in a [`ProxyDB`] implementation. This way you can live reload based upon
32//! a signal, or more realistically, every x minutes.
33//!
34//! [`Extensions`]: rama_core::extensions::Extensions
35//!
36//! ## ProxyDB layer
37//!
38//! [`ProxyDB`] layer support to select a proxy based on the given input [`Extensions`].
39//!
40//! This layer expects a [`ProxyFilter`] to be available in the input [`Extensions`],
41//! which can be added by using the `HeaderConfigLayer` (`rama-http`)
42//! when operating on the HTTP layer and/or by parsing it via the TCP proxy username labels (e.g. `john-country-us-residential`),
43//! in case you support that as part of your transport-layer authentication. And of course you can
44//! combine the two approaches.
45//!
46//! You can also give a single [`Proxy`] as "proxy db".
47//!
48//! By default the layer publishes at most five matching proxies as an ordered
49//! [`ProxyRoutes`] plan. The limit is configurable, including an unbounded mode.
50//! Each route carries its [`Proxy`] and [`ProxyID`] as route-specific
51//! extensions. [`ProxyRoutesConnector`] installs only the selected route's
52//! metadata on its isolated connection attempt and returned input.
53//!
54//! [`ProxyDBLayer::with_single_proxy`] opts into the legacy behaviour of
55//! using the database's singular selection and inserting one route.
56//! `MemoryProxyDB` keeps its random selection for this mode. A database miss
57//! remains an error in either mode; no direct route is appended implicitly.
58//! Existing singular routes are preserved by default in both modes.
59//! [`ProxyDBLayer::with_overwrite_proxy`] explicitly lets the database selection
60//! replace them by publishing the newer decision. [`ProxyRoutesLayer`] resolves
61//! singular and plural decisions by insertion order before route-aware
62//! middleware runs.
63//!
64//! [`ProxyRoute`]: rama_net::client::ProxyRoute
65//! [`ProxyRoutes`]: rama_net::client::ProxyRoutes
66//! [`ProxyRoutesLayer`]: rama_net::client::ProxyRoutesLayer
67//! [`ProxyRoutesConnector`]: rama_net::client::ProxyRoutesConnector
68//! [`ProxyDB`]: ProxyDB
69//!
70//! # Example
71//!
72//! ```rust
73//! use rama_http_types::{Body, Version, Request};
74//! use rama_proxy::{
75//!      Proxy,
76//!      ProxyDBLayer, ProxyFilterMode,
77//!      ProxyFilter,
78//! };
79//! # #[cfg(feature = "memory-db")]
80//! # use rama_proxy::MemoryProxyDB;
81//! use rama_core::{
82//!    service::service_fn,
83//!    extensions::{ExtensionsRef},
84//!    Service, Layer,
85//! };
86//! use rama_net::client::ProxyRoute;
87//! use rama_utils::str::non_empty_str;
88//! use std::{convert::Infallible, sync::Arc};
89//!
90//! # #[cfg(feature = "memory-db")]
91//! #[tokio::main]
92//! async fn main() {
93//!     let db = MemoryProxyDB::try_from_iter([
94//!         Proxy {
95//!             id: non_empty_str!("42"),
96//!             address: "12.34.12.34:8080".try_into().unwrap(),
97//!             tcp: true,
98//!             udp: true,
99//!             http: true,
100//!             https: false,
101//!             socks5: true,
102//!             socks5h: false,
103//!             datacenter: false,
104//!             residential: true,
105//!             mobile: true,
106//!             pool_id: None,
107//!             continent: Some("*".into()),
108//!             country: Some("*".into()),
109//!             state: Some("*".into()),
110//!             city: Some("*".into()),
111//!             carrier: Some("*".into()),
112//!             asn: None,
113//!         },
114//!         Proxy {
115//!             id: non_empty_str!("100"),
116//!             address: "123.123.123.123:8080".try_into().unwrap(),
117//!             tcp: true,
118//!             udp: false,
119//!             http: true,
120//!             https: false,
121//!             socks5: false,
122//!             socks5h: false,
123//!             datacenter: true,
124//!             residential: false,
125//!             mobile: false,
126//!             pool_id: None,
127//!             continent: None,
128//!             country: Some("US".into()),
129//!             state: None,
130//!             city: None,
131//!             carrier: None,
132//!             asn: None,
133//!         },
134//!     ])
135//!     .unwrap();
136//!
137//!     // Singular mode keeps this small example independent of a connector.
138//!     // The normal client stack uses the default plural mode together with
139//!     // ProxyRoutesConnector.
140//!     let service = ProxyDBLayer::new(Arc::new(db))
141//!         .with_filter_mode(ProxyFilterMode::Default)
142//!         .with_single_proxy(true)
143//!         .into_layer(service_fn(async |req: Request| {
144//!             Ok::<_, Infallible>(req)
145//!         }));
146//!
147//!     let req = Request::builder()
148//!         .version(Version::HTTP_3)
149//!         .method("GET")
150//!         .uri("https://example.com")
151//!         .body(Body::empty())
152//!         .unwrap();
153//!
154//!     req.extensions().insert(ProxyFilter {
155//!         country: Some(vec!["BE".into()]),
156//!         mobile: Some(true),
157//!         residential: Some(true),
158//!         ..Default::default()
159//!     });
160//!
161//!     let output = service.serve(req).await.unwrap();
162//!     let proxy_address = output
163//!         .extensions()
164//!         .get_ref::<ProxyRoute>()
165//!         .and_then(ProxyRoute::proxy_address)
166//!         .unwrap();
167//!     assert_eq!(proxy_address.address.to_string(), "12.34.12.34:8080");
168//!     assert_eq!(output.extensions().get_ref::<Proxy>().unwrap().id, "42");
169//! }
170//! # #[cfg(not(feature = "memory-db"))]
171//! # fn main() {}
172//! ```
173//!
174//! ## Single Proxy Router
175//!
176//! Another example is a single proxy through which
177//! one can connect with config for further downstream proxies
178//! passed by username labels.
179//!
180//! Note that the username formatter is available for any proxy db,
181//! it is not specific to the usage of a single proxy.
182//!
183//! ```rust
184//! use rama_http_types::{Body, Version, Request};
185//! use rama_proxy::{
186//!    Proxy,
187//!    ProxyDBLayer, ProxyFilterMode,
188//!    ProxyFilter,
189//! };
190//! use rama_core::{
191//!    service::service_fn,
192//!    extensions::{ExtensionsRef},
193//!    Service, Layer,
194//! };
195//! use rama_net::client::ProxyRoute;
196//! use rama_utils::str::non_empty_str;
197//! use std::{convert::Infallible, sync::Arc};
198//!
199//! #[tokio::main]
200//! async fn main() {
201//!     let proxy = Proxy {
202//!         id: non_empty_str!("1"),
203//!         address: "john:secret@proxy.example.com:60000".try_into().unwrap(),
204//!         tcp: true,
205//!         udp: true,
206//!         http: true,
207//!         https: false,
208//!         socks5: true,
209//!         socks5h: false,
210//!         datacenter: false,
211//!         residential: true,
212//!         mobile: false,
213//!         pool_id: None,
214//!         continent: Some("*".into()),
215//!         country: Some("*".into()),
216//!         state: Some("*".into()),
217//!         city: Some("*".into()),
218//!         carrier: Some("*".into()),
219//!         asn: None,
220//!     };
221//!
222//!     let service = ProxyDBLayer::new(Arc::new(proxy))
223//!         .with_filter_mode(ProxyFilterMode::Default)
224//!         .with_single_proxy(true)
225//!         .with_username_formatter(|_proxy: &Proxy, filter: &ProxyFilter, username: &str| {
226//!             use std::fmt::Write;
227//!
228//!             let mut output = String::new();
229//!
230//!             if let Some(countries) =
231//!                 filter.country.as_ref().filter(|t| !t.is_empty())
232//!             {
233//!                 _ = write!(output, "country-{}", countries[0]);
234//!             }
235//!             if let Some(states) =
236//!                 filter.state.as_ref().filter(|t| !t.is_empty())
237//!             {
238//!                 _ = write!(output, "state-{}", states[0]);
239//!             }
240//!
241//!             (!output.is_empty()).then(|| format!("{username}-{output}"))
242//!         })
243//!         .into_layer(service_fn(async |req: Request| {
244//!             Ok::<_, Infallible>(req)
245//!         }));
246//!
247//!     let req = Request::builder()
248//!         .version(Version::HTTP_3)
249//!         .method("GET")
250//!         .uri("https://example.com")
251//!         .body(Body::empty())
252//!         .unwrap();
253//!     req.extensions().insert(ProxyFilter {
254//!         country: Some(vec!["BE".into()]),
255//!         residential: Some(true),
256//!         ..Default::default()
257//!     });
258//!     let output = service.serve(req).await.unwrap();
259//!     let proxy_address = output
260//!         .extensions()
261//!         .get_ref::<ProxyRoute>()
262//!         .and_then(ProxyRoute::proxy_address)
263//!         .unwrap();
264//!     assert_eq!(
265//!         "socks5://john-country-be:secret@proxy.example.com:60000",
266//!         proxy_address.to_string()
267//!     );
268//! }
269//! ```
270
271#![doc(
272    html_favicon_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/rama_logo.svg"
273)]
274#![doc(
275    html_logo_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/rama_logo.svg"
276)]
277#![cfg_attr(docsrs, feature(doc_cfg))]
278#![cfg_attr(test, allow(clippy::float_cmp))]
279
280mod username;
281#[doc(inline)]
282pub use username::ProxyFilterUsernameParser;
283
284mod proxydb;
285
286#[doc(inline)]
287pub use proxydb::{
288    Proxy, ProxyContext, ProxyDB, ProxyFilter, ProxyID, ProxyQueryPredicate, StringFilter,
289};
290
291#[doc(inline)]
292pub use proxydb::layer::{
293    DEFAULT_PROXY_DB_MAX_PROXIES, ProxyDBLayer, ProxyDBService, ProxyFilterMode, UsernameFormatter,
294};
295
296#[cfg(feature = "live-update")]
297#[cfg_attr(docsrs, doc(cfg(feature = "live-update")))]
298#[doc(inline)]
299pub use proxydb::{LiveUpdateProxyDB, LiveUpdateProxyDBSetter, proxy_db_updater};
300
301#[cfg(feature = "memory-db")]
302#[cfg_attr(docsrs, doc(cfg(feature = "memory-db")))]
303#[doc(inline)]
304pub use proxydb::{
305    MemoryProxyDB, MemoryProxyDBInsertError, MemoryProxyDBInsertErrorKind, MemoryProxyDBQueryError,
306    MemoryProxyDBQueryErrorKind,
307};
308
309#[cfg(feature = "csv")]
310#[cfg_attr(docsrs, doc(cfg(feature = "csv")))]
311#[doc(inline)]
312pub use proxydb::{ProxyCsvRowReader, ProxyCsvRowReaderError, ProxyCsvRowReaderErrorKind};