rama_net/address/
proxy.rs

1use super::{Authority, Host};
2use crate::{Protocol, proto::try_to_extract_protocol_from_uri_scheme, user::ProxyCredential};
3use rama_core::error::{ErrorContext, OpaqueError};
4use std::{fmt::Display, str::FromStr};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7/// Address of a proxy that can be connected to.
8pub struct ProxyAddress {
9    /// [`Protocol`] used by the proxy.
10    pub protocol: Option<Protocol>,
11
12    /// [`Authority`] of the proxy.
13    pub authority: Authority,
14
15    /// [`ProxyCredential`] of the proxy.
16    pub credential: Option<ProxyCredential>,
17}
18
19impl TryFrom<&str> for ProxyAddress {
20    type Error = OpaqueError;
21
22    fn try_from(value: &str) -> Result<Self, Self::Error> {
23        let slice = value.as_bytes();
24
25        let (protocol, size) = try_to_extract_protocol_from_uri_scheme(slice)
26            .context("extract protocol from proxy address scheme")?;
27        let slice = &slice[size..];
28
29        for i in 0..slice.len() {
30            if slice[i] == b'@' {
31                let credential = ProxyCredential::try_from_clear_str(
32                    std::str::from_utf8(&slice[..i])
33                        .context("parse proxy address: view credential as utf-8")?
34                        .to_owned(),
35                )
36                .context("parse proxy credential from address")?;
37
38                let authority: Authority = slice[i + 1..]
39                    .try_into()
40                    .or_else(|_| {
41                        Host::try_from(&slice[i + 1..]).map(|h| {
42                            (
43                                h,
44                                protocol
45                                    .as_ref()
46                                    .and_then(|proto| proto.default_port())
47                                    .unwrap_or(80),
48                            )
49                                .into()
50                        })
51                    })
52                    .context("parse proxy authority from address")?;
53
54                return Ok(ProxyAddress {
55                    protocol,
56                    authority,
57                    credential: Some(credential),
58                });
59            }
60        }
61
62        let authority: Authority = slice
63            .try_into()
64            .or_else(|_| {
65                Host::try_from(slice).map(|h| {
66                    (
67                        h,
68                        protocol
69                            .as_ref()
70                            .and_then(|proto| proto.default_port())
71                            .unwrap_or(80),
72                    )
73                        .into()
74                })
75            })
76            .context("parse proxy authority from address")?;
77        Ok(ProxyAddress {
78            protocol,
79            authority,
80            credential: None,
81        })
82    }
83}
84
85impl TryFrom<String> for ProxyAddress {
86    type Error = OpaqueError;
87
88    fn try_from(value: String) -> Result<Self, Self::Error> {
89        value.as_str().try_into()
90    }
91}
92
93impl FromStr for ProxyAddress {
94    type Err = OpaqueError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        s.try_into()
98    }
99}
100
101impl Display for ProxyAddress {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        if let Some(protocol) = &self.protocol {
104            write!(f, "{}://", protocol.as_str())?;
105        }
106        if let Some(credential) = &self.credential {
107            write!(f, "{}@", credential.as_clear_string())?;
108        }
109        self.authority.fmt(f)
110    }
111}
112
113impl serde::Serialize for ProxyAddress {
114    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: serde::Serializer,
117    {
118        let addr = self.to_string();
119        addr.serialize(serializer)
120    }
121}
122
123impl<'de> serde::Deserialize<'de> for ProxyAddress {
124    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125    where
126        D: serde::Deserializer<'de>,
127    {
128        let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
129        s.parse().map_err(serde::de::Error::custom)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::{
137        address::Host,
138        user::{Basic, Bearer},
139    };
140
141    #[test]
142    fn test_valid_proxy() {
143        let addr: ProxyAddress = "127.0.0.1:8080".try_into().unwrap();
144        assert_eq!(
145            addr,
146            ProxyAddress {
147                protocol: None,
148                authority: Authority::new(Host::Address("127.0.0.1".parse().unwrap()), 8080),
149                credential: None,
150            }
151        );
152    }
153
154    #[test]
155    fn test_valid_domain_proxy() {
156        let addr: ProxyAddress = "proxy.example.com".try_into().unwrap();
157        assert_eq!(
158            addr,
159            ProxyAddress {
160                protocol: None,
161                authority: Authority::new(Host::Name("proxy.example.com".parse().unwrap()), 80),
162                credential: None,
163            }
164        );
165    }
166
167    #[test]
168    fn test_valid_proxy_with_credential() {
169        let addr: ProxyAddress = "foo:bar@127.0.0.1:8080".try_into().unwrap();
170        assert_eq!(
171            addr,
172            ProxyAddress {
173                protocol: None,
174                authority: Authority::new(Host::Address("127.0.0.1".parse().unwrap()), 8080),
175                credential: Some(Basic::new("foo", "bar").into()),
176            }
177        );
178    }
179
180    #[test]
181    fn test_valid_http_proxy() {
182        let addr: ProxyAddress = "http://127.0.0.1:8080".try_into().unwrap();
183        assert_eq!(
184            addr,
185            ProxyAddress {
186                protocol: Some(Protocol::HTTP),
187                authority: Authority::new(Host::Address("127.0.0.1".parse().unwrap()), 8080),
188                credential: None,
189            }
190        );
191    }
192
193    #[test]
194    fn test_valid_http_proxy_with_credential() {
195        let addr: ProxyAddress = "http://foo:bar@127.0.0.1:8080".try_into().unwrap();
196        assert_eq!(
197            addr,
198            ProxyAddress {
199                protocol: Some(Protocol::HTTP),
200                authority: Authority::new(Host::Address("127.0.0.1".parse().unwrap()), 8080),
201                credential: Some(Basic::new("foo", "bar").into()),
202            }
203        );
204    }
205
206    #[test]
207    fn test_valid_https_proxy() {
208        let addr: ProxyAddress = "https://foo-cc-be:baz@my.proxy.io.:9999"
209            .try_into()
210            .unwrap();
211        assert_eq!(
212            addr,
213            ProxyAddress {
214                protocol: Some(Protocol::HTTPS),
215                authority: Authority::new(Host::Name("my.proxy.io.".parse().unwrap()), 9999),
216                credential: Some(Basic::new("foo-cc-be", "baz").into()),
217            }
218        );
219    }
220
221    #[test]
222    fn test_valid_socks5h_proxy() {
223        let addr: ProxyAddress = "socks5h://foo@[::1]:60000".try_into().unwrap();
224        assert_eq!(
225            addr,
226            ProxyAddress {
227                protocol: Some(Protocol::SOCKS5H),
228                authority: Authority::new(Host::Address("::1".parse().unwrap()), 60000),
229                credential: Some(Bearer::try_from_clear_str("foo").unwrap().into()),
230            }
231        );
232    }
233
234    #[test]
235    fn test_valid_proxy_address_symmetric() {
236        for s in [
237            "proxy.io",
238            "proxy.io:8080",
239            "127.0.0.1",
240            "127.0.0.1:8080",
241            "::1",
242            "[::1]:8080",
243            "socks5://proxy.io",
244            "socks5://proxy.io:8080",
245            "socks5://127.0.0.1",
246            "socks5://127.0.0.1:8080",
247            "socks5://::1",
248            "socks5://[::1]:8080",
249            "socks5://foo@proxy.io",
250            "socks5://foo@proxy.io:8080",
251            "socks5://foo@127.0.0.1",
252            "socks5://foo@127.0.0.1:8080",
253            "socks5://foo@::1",
254            "socks5://foo@[::1]:8080",
255            "socks5://foo:@proxy.io",
256            "socks5://foo:@proxy.io:8080",
257            "socks5://foo:@127.0.0.1",
258            "socks5://foo:@127.0.0.1:8080",
259            "socks5://foo:@::1",
260            "socks5://foo:@[::1]:8080",
261            "socks5://foo:bar@proxy.io",
262            "socks5://foo:bar@proxy.io:8080",
263            "socks5://foo:bar@127.0.0.1",
264            "socks5://foo:bar@127.0.0.1:8080",
265            "socks5://foo:bar@::1",
266            "socks5://foo:bar@[::1]:8080",
267        ] {
268            let addr: ProxyAddress = match s.try_into() {
269                Ok(addr) => addr,
270                Err(err) => panic!("invalid addr '{s}': {err}"),
271            };
272            let out = addr.to_string();
273            let mut s = s.to_owned();
274            if !s.ends_with(":8080") {
275                if s.contains("::1") {
276                    let mut it = s.split("://");
277                    let mut scheme = Some(it.next().unwrap());
278                    let host = it.next().unwrap_or_else(|| scheme.take().unwrap());
279                    if host.contains('@') {
280                        let mut it = host.split('@');
281                        let credential = it.next().unwrap();
282                        let host = it.next().unwrap();
283                        s = match scheme {
284                            Some(scheme) => format!("{scheme}://{credential}@[{host}]:1080"),
285                            None => format!("{credential}@[{host}]:80"),
286                        };
287                    } else {
288                        s = match scheme {
289                            Some(scheme) => format!("{scheme}://[{host}]:1080"),
290                            None => format!("[{host}]:80"),
291                        };
292                    }
293                } else {
294                    s = if s.contains("://") {
295                        format!("{s}:1080")
296                    } else {
297                        format!("{s}:80")
298                    };
299                }
300            }
301            assert_eq!(s, out, "addr: {addr}");
302        }
303    }
304}