Skip to main content

rama_proxy/proxydb/
internal.rs

1use super::{ProxyContext, ProxyFilter, StringFilter};
2use rama_core::extensions::Extension;
3use rama_net::{address::ProxyAddress, asn::Asn, transport::TransportProtocol};
4use rama_utils::str::NonEmptyStr;
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "memory-db")]
8use venndb::VennDB;
9
10#[derive(Debug, Clone, Serialize, Deserialize, Extension)]
11#[extension(tags(proxy))]
12#[cfg_attr(feature = "memory-db", derive(VennDB))]
13#[cfg_attr(feature = "memory-db", venndb(validator = proxydb_insert_validator))]
14/// A proxy record returned by a [`ProxyDB`](super::ProxyDB).
15pub struct Proxy {
16    #[cfg_attr(feature = "memory-db", venndb(key))]
17    /// Unique identifier of the proxy.
18    pub id: NonEmptyStr,
19
20    /// The address to be used to connect to the proxy, including credentials if needed.
21    pub address: ProxyAddress,
22
23    /// True if the proxy supports TCP connections.
24    pub tcp: bool,
25
26    /// True if the proxy supports UDP connections.
27    pub udp: bool,
28
29    /// http-proxy enabled
30    pub http: bool,
31
32    /// https-proxy enabled
33    pub https: bool,
34
35    /// socks5-proxy enabled
36    pub socks5: bool,
37
38    /// socks5h-proxy enabled
39    pub socks5h: bool,
40
41    /// Proxy is located in a datacenter.
42    pub datacenter: bool,
43
44    /// Proxy's IP is labeled as residential.
45    pub residential: bool,
46
47    /// Proxy's IP originates from a mobile network.
48    pub mobile: bool,
49
50    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
51    /// Pool ID of the proxy.
52    pub pool_id: Option<StringFilter>,
53
54    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
55    /// Continent of the proxy.
56    pub continent: Option<StringFilter>,
57
58    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
59    /// Country of the proxy.
60    pub country: Option<StringFilter>,
61
62    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
63    /// State of the proxy.
64    pub state: Option<StringFilter>,
65
66    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
67    /// City of the proxy.
68    pub city: Option<StringFilter>,
69
70    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
71    /// Mobile carrier of the proxy.
72    pub carrier: Option<StringFilter>,
73
74    #[cfg_attr(feature = "memory-db", venndb(filter, any))]
75    ///  Autonomous System Number (ASN).
76    pub asn: Option<Asn>,
77}
78
79#[cfg(feature = "memory-db")]
80/// Validate the proxy is valid according to rules that are not enforced by the type system.
81fn proxydb_insert_validator(proxy: &Proxy) -> bool {
82    (proxy.datacenter || proxy.residential || proxy.mobile)
83        && (((proxy.http || proxy.https) && proxy.tcp)
84            || ((proxy.socks5 || proxy.socks5h) && (proxy.tcp || proxy.udp)))
85}
86
87impl Proxy {
88    /// Check if the proxy is a match for the given[`ProxyContext`] and [`ProxyFilter`].
89    #[must_use]
90    pub fn is_match(&self, ctx: &ProxyContext, filter: &ProxyFilter) -> bool {
91        if let Some(id) = &filter.id
92            && id != &self.id
93        {
94            return false;
95        }
96
97        match ctx.protocol {
98            TransportProtocol::Udp => {
99                if !(self.socks5 || self.socks5h) || !self.udp {
100                    return false;
101                }
102            }
103            TransportProtocol::Tcp => {
104                if !self.tcp || !(self.http || self.https || self.socks5 || self.socks5h) {
105                    return false;
106                }
107            }
108        }
109
110        filter
111            .continent
112            .as_ref()
113            .map(|c| {
114                let continent = self.continent.as_ref();
115                c.iter().any(|c| Some(c) == continent)
116            })
117            .unwrap_or(true)
118            && filter
119                .country
120                .as_ref()
121                .map(|c| {
122                    let country = self.country.as_ref();
123                    c.iter().any(|c| Some(c) == country)
124                })
125                .unwrap_or(true)
126            && filter
127                .state
128                .as_ref()
129                .map(|s| {
130                    let state = self.state.as_ref();
131                    s.iter().any(|s| Some(s) == state)
132                })
133                .unwrap_or(true)
134            && filter
135                .city
136                .as_ref()
137                .map(|c| {
138                    let city = self.city.as_ref();
139                    c.iter().any(|c| Some(c) == city)
140                })
141                .unwrap_or(true)
142            && filter
143                .pool_id
144                .as_ref()
145                .map(|p| {
146                    let pool_id = self.pool_id.as_ref();
147                    p.iter().any(|p| Some(p) == pool_id)
148                })
149                .unwrap_or(true)
150            && filter
151                .carrier
152                .as_ref()
153                .map(|c| {
154                    let carrier = self.carrier.as_ref();
155                    c.iter().any(|c| Some(c) == carrier)
156                })
157                .unwrap_or(true)
158            && filter
159                .asn
160                .as_ref()
161                .map(|a| {
162                    let asn = self.asn.as_ref();
163                    a.iter().any(|a| Some(a) == asn)
164                })
165                .unwrap_or(true)
166            && filter
167                .datacenter
168                .map(|d| d == self.datacenter)
169                .unwrap_or(true)
170            && filter
171                .residential
172                .map(|r| r == self.residential)
173                .unwrap_or(true)
174            && filter.mobile.map(|m| m == self.mobile).unwrap_or(true)
175    }
176}
177
178#[cfg(all(feature = "csv", feature = "memory-db"))]
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::proxydb::csv::{ProxyCsvRowReader, parse_csv_row};
183    use crate::proxydb::internal::{ProxyDB, ProxyDBErrorKind};
184    use itertools::Itertools;
185
186    #[test]
187    fn test_proxy_db_happy_path_basic() {
188        let mut db = ProxyDB::new();
189        let proxy = parse_csv_row("id,1,,1,,,,1,,,authority:80,,,,,,,,").unwrap();
190        db.append(proxy).unwrap();
191
192        let mut query = db.query();
193        query.tcp(true).http(true);
194
195        let proxy = query.execute().unwrap().any();
196        assert_eq!(proxy.id, "id");
197    }
198
199    #[tokio::test]
200    async fn test_proxy_db_happy_path_any_country() {
201        let mut db = ProxyDB::new();
202        let mut reader = ProxyCsvRowReader::raw(
203            "1,1,,1,,,,1,,,authority:80,,,US,,,,,\n2,1,,1,,,,1,,,authority:80,,,*,,,,,",
204        );
205        while let Some(proxy) = reader.next().await.unwrap() {
206            db.append(proxy).unwrap();
207        }
208
209        let mut query = db.query();
210        query.tcp(true).http(true).country("US");
211
212        let proxies: Vec<_> = query
213            .execute()
214            .unwrap()
215            .iter()
216            .sorted_by(|a, b| a.id.cmp(&b.id))
217            .collect();
218        assert_eq!(proxies.len(), 2);
219        assert_eq!(proxies[0].id, "1");
220        assert_eq!(proxies[1].id, "2");
221
222        query.reset().country("BE");
223        let proxies: Vec<_> = query
224            .execute()
225            .unwrap()
226            .iter()
227            .sorted_by(|a, b| a.id.cmp(&b.id))
228            .collect();
229        assert_eq!(proxies.len(), 1);
230        assert_eq!(proxies[0].id, "2");
231    }
232
233    #[tokio::test]
234    async fn test_proxy_db_happy_path_any_country_city() {
235        let mut db = ProxyDB::new();
236        let mut reader = ProxyCsvRowReader::raw(
237            "1,1,,1,,,,1,,,authority:80,,,US,,New York,,,\n2,1,,1,,,,1,,,authority:80,,,*,,*,,,",
238        );
239        while let Some(proxy) = reader.next().await.unwrap() {
240            db.append(proxy).unwrap();
241        }
242
243        let mut query = db.query();
244        query.tcp(true).http(true).country("US").city("new york");
245
246        let proxies: Vec<_> = query
247            .execute()
248            .unwrap()
249            .iter()
250            .sorted_by(|a, b| a.id.cmp(&b.id))
251            .collect();
252        assert_eq!(proxies.len(), 2);
253        assert_eq!(proxies[0].id, "1");
254        assert_eq!(proxies[1].id, "2");
255
256        query.reset().country("US").city("Los Angeles");
257        let proxies: Vec<_> = query
258            .execute()
259            .unwrap()
260            .iter()
261            .sorted_by(|a, b| a.id.cmp(&b.id))
262            .collect();
263        assert_eq!(proxies.len(), 1);
264        assert_eq!(proxies[0].id, "2");
265
266        query.reset().city("Ghent");
267        let proxies: Vec<_> = query
268            .execute()
269            .unwrap()
270            .iter()
271            .sorted_by(|a, b| a.id.cmp(&b.id))
272            .collect();
273        assert_eq!(proxies.len(), 1);
274        assert_eq!(proxies[0].id, "2");
275    }
276
277    #[tokio::test]
278    async fn test_proxy_db_happy_path_specific_asn_within_continents() {
279        let mut db = ProxyDB::new();
280        let mut reader = ProxyCsvRowReader::raw(
281            "1,1,,1,,,,1,,,authority:80,,europe,BE,,Brussels,,1348,\n2,1,,1,,,,1,,,authority:80,,asia,CN,,Shenzen,,1348,\n3,1,,1,,,,1,,,authority:80,,asia,CN,,Peking,,42,",
282        );
283        while let Some(proxy) = reader.next().await.unwrap() {
284            db.append(proxy).unwrap();
285        }
286
287        let mut query = db.query();
288        query
289            .tcp(true)
290            .http(true)
291            .continent("europe")
292            .continent("asia")
293            .asn(Asn::from_static(1348));
294
295        let proxies: Vec<_> = query
296            .execute()
297            .unwrap()
298            .iter()
299            .sorted_by(|a, b| a.id.cmp(&b.id))
300            .collect();
301        assert_eq!(proxies.len(), 2);
302        assert_eq!(proxies[0].id, "1");
303        assert_eq!(proxies[1].id, "2");
304
305        query.reset().asn(Asn::from_static(42));
306        let proxies: Vec<_> = query
307            .execute()
308            .unwrap()
309            .iter()
310            .sorted_by(|a, b| a.id.cmp(&b.id))
311            .collect();
312        assert_eq!(proxies.len(), 1);
313        assert_eq!(proxies[0].id, "3");
314    }
315
316    #[tokio::test]
317    async fn test_proxy_db_happy_path_states() {
318        let mut db = ProxyDB::new();
319        let mut reader = ProxyCsvRowReader::raw(
320            "1,1,,1,,,,1,,,authority:80,,,US,Texas,,,,\n2,1,,1,,,,1,,,authority:80,,,US,New York,,,,\n3,1,,1,,,,1,,,authority:80,,,US,California,,,,",
321        );
322        while let Some(proxy) = reader.next().await.unwrap() {
323            db.append(proxy).unwrap();
324        }
325
326        let mut query = db.query();
327        query.tcp(true).http(true).state("texas").state("new york");
328
329        let proxies: Vec<_> = query
330            .execute()
331            .unwrap()
332            .iter()
333            .sorted_by(|a, b| a.id.cmp(&b.id))
334            .collect();
335        assert_eq!(proxies.len(), 2);
336        assert_eq!(proxies[0].id, "1");
337        assert_eq!(proxies[1].id, "2");
338
339        query.reset().state("california");
340        let proxies: Vec<_> = query
341            .execute()
342            .unwrap()
343            .iter()
344            .sorted_by(|a, b| a.id.cmp(&b.id))
345            .collect();
346        assert_eq!(proxies.len(), 1);
347        assert_eq!(proxies[0].id, "3");
348    }
349
350    #[tokio::test]
351    async fn test_proxy_db_invalid_row_cases() {
352        let mut db = ProxyDB::new();
353        let mut reader = ProxyCsvRowReader::raw(
354            "id1,1,,,,,,,,,authority:80,,,,,,,\nid2,,1,,,,,,,,authority:80,,,,,,,\nid3,,1,1,,,,,,,authority:80,,,,,,,\nid4,,1,1,,,,,1,,authority:80,,,,,,,\nid5,,1,1,,,,,1,,authority:80,,,,,,,",
355        );
356        while let Some(proxy) = reader.next().await.unwrap() {
357            assert_eq!(
358                ProxyDBErrorKind::InvalidRow,
359                db.append(proxy).unwrap_err().kind
360            );
361        }
362    }
363}