Skip to main content

socks5_impl/
proxy_parameters.rs

1use crate::{Address, Error, Result, UserKey};
2
3#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
5#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
6pub struct ProxyParameters {
7    pub proxy_type: ProxyType,
8    pub addr: Option<Address>,
9    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
10    pub credentials: Option<UserKey>,
11}
12
13impl Default for ProxyParameters {
14    fn default() -> Self {
15        ProxyParameters {
16            proxy_type: ProxyType::Socks5,
17            addr: Some("127.0.0.1:1080".parse().unwrap()),
18            credentials: None,
19        }
20    }
21}
22
23impl std::fmt::Display for ProxyParameters {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        if self.proxy_type == ProxyType::None || self.addr.is_none() {
26            return write!(f, "none");
27        }
28
29        let auth = match &self.credentials {
30            Some(creds) => format!("{creds}"),
31            None => "".to_owned(),
32        };
33        let addr = self.addr.as_ref().unwrap();
34        if auth.is_empty() {
35            write!(f, "{}://{addr}", self.proxy_type)
36        } else {
37            write!(f, "{}://{}@{addr}", self.proxy_type, auth)
38        }
39    }
40}
41
42#[cfg(feature = "serde")]
43impl From<ProxyParameters> for String {
44    fn from(parameters: ProxyParameters) -> Self {
45        if parameters.proxy_type == ProxyType::None {
46            "none".to_owned()
47        } else {
48            parameters.to_string()
49        }
50    }
51}
52
53impl ProxyParameters {
54    pub fn new(proxy_type: ProxyType, addr: Option<Address>, credentials: Option<UserKey>) -> Self {
55        ProxyParameters {
56            proxy_type,
57            addr,
58            credentials,
59        }
60    }
61}
62
63impl TryFrom<String> for ProxyParameters {
64    type Error = Error;
65    fn try_from(s: String) -> Result<Self> {
66        Self::try_from(s.as_str())
67    }
68}
69
70impl TryFrom<&String> for ProxyParameters {
71    type Error = Error;
72    fn try_from(s: &String) -> Result<Self> {
73        Self::try_from(s.as_str())
74    }
75}
76
77impl TryFrom<&str> for ProxyParameters {
78    type Error = Error;
79    fn try_from(s: &str) -> Result<Self> {
80        if s == "none" {
81            return Ok(ProxyParameters {
82                proxy_type: ProxyType::None,
83                addr: None,
84                credentials: None,
85            });
86        }
87
88        let e = format!("`{s}` is not a valid proxy URL");
89        let url = url::Url::parse(s).map_err(|_| Error::from(&e))?;
90        let e = format!("`{s}` does not contain a host");
91        let host = url.host_str().ok_or(Error::from(e))?;
92
93        let e = format!("`{s}` does not contain a port");
94        let port = url.port_or_known_default().ok_or(Error::from(&e))?;
95
96        let addr = (host, port).into();
97
98        let credentials = if url.username() == "" && url.password().is_none() {
99            None
100        } else {
101            use percent_encoding::percent_decode;
102            let username = percent_decode(url.username().as_bytes()).decode_utf8()?;
103            let password = percent_decode(url.password().unwrap_or("").as_bytes()).decode_utf8()?;
104            Some(UserKey::new(username, password))
105        };
106
107        let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;
108
109        Ok(ProxyParameters {
110            proxy_type,
111            addr: Some(addr),
112            credentials,
113        })
114    }
115}
116
117impl std::str::FromStr for ProxyParameters {
118    type Err = Error;
119    fn from_str(s: &str) -> Result<Self> {
120        Self::try_from(s)
121    }
122}
123
124impl TryFrom<url::Url> for ProxyParameters {
125    type Error = Error;
126    fn try_from(url: url::Url) -> Result<Self> {
127        let e = format!("`{url}` does not contain a host");
128        let host = url.host_str().ok_or(Error::from(e))?;
129
130        let e = format!("`{url}` does not contain a port");
131        let port = url.port_or_known_default().ok_or(Error::from(&e))?;
132
133        let addr = (host, port).into();
134
135        let credentials = if url.username() == "" && url.password().is_none() {
136            None
137        } else {
138            use percent_encoding::percent_decode;
139            let username = percent_decode(url.username().as_bytes()).decode_utf8()?;
140            let password = percent_decode(url.password().unwrap_or("").as_bytes()).decode_utf8()?;
141            Some(UserKey::new(username, password))
142        };
143
144        let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;
145
146        Ok(ProxyParameters {
147            proxy_type,
148            addr: Some(addr),
149            credentials,
150        })
151    }
152}
153
154impl TryFrom<ProxyParameters> for url::Url {
155    type Error = Error;
156    fn try_from(parameters: ProxyParameters) -> Result<Self> {
157        let addr = parameters
158            .addr
159            .ok_or_else(|| Error::from("proxy parameters do not contain an address"))?;
160        let mut url = url::Url::parse(&format!("{}://{addr}", parameters.proxy_type))
161            .map_err(|e| Error::from(&format!("failed to parse proxy parameters into URL: {e}")))?;
162        if let Some(creds) = parameters.credentials {
163            url.set_username(&creds.username)
164                .map_err(|e| Error::from(&format!("failed to set username in proxy URL: {e:?}")))?;
165            url.set_password(Some(&creds.password))
166                .map_err(|e| Error::from(&format!("failed to set password in proxy URL: {e:?}")))?;
167        }
168        Ok(url)
169    }
170}
171
172#[repr(C)]
173#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
174#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
175pub enum ProxyType {
176    None = 0,
177    Http,
178    Socks4,
179    #[default]
180    Socks5,
181    Mixed,
182}
183
184impl TryFrom<&str> for ProxyType {
185    type Error = Error;
186    fn try_from(value: &str) -> Result<Self> {
187        match value.to_ascii_lowercase().as_str() {
188            "none" => Ok(ProxyType::None),
189            "http" => Ok(ProxyType::Http),
190            "socks4" => Ok(ProxyType::Socks4),
191            "socks5" => Ok(ProxyType::Socks5),
192            "mixed" => Ok(ProxyType::Mixed),
193            scheme => Err(Error::from(&format!("`{scheme}` is an invalid proxy type"))),
194        }
195    }
196}
197
198impl std::fmt::Display for ProxyType {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        match self {
201            ProxyType::None => write!(f, "none"),
202            ProxyType::Socks4 => write!(f, "socks4"),
203            ProxyType::Socks5 => write!(f, "socks5"),
204            ProxyType::Http => write!(f, "http"),
205            ProxyType::Mixed => write!(f, "mixed"),
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn default_proxy_parameters() {
216        let parameters = ProxyParameters::default();
217
218        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
219        assert_eq!(parameters.addr, Some("127.0.0.1:1080".parse().unwrap()));
220        assert_eq!(parameters.credentials, None);
221        assert_eq!(parameters.to_string(), "socks5://127.0.0.1:1080");
222    }
223
224    #[test]
225    fn parse_without_credentials() {
226        let parameters = "socks5://123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
227        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
228        assert_eq!(parameters.addr, Some(("123.45.67.89", 1080).into()));
229        assert_eq!(parameters.addr.as_ref().unwrap().get_type(), crate::AddressType::IPv4);
230        assert_eq!(parameters.credentials, None);
231        assert_eq!(parameters.to_string(), "socks5://123.45.67.89:1080");
232
233        let parameters = "socks5://proxy.example.com:1080".parse::<ProxyParameters>().unwrap();
234
235        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
236        assert_eq!(parameters.addr, Some(("proxy.example.com", 1080).into()));
237        assert_eq!(parameters.credentials, None);
238        assert_eq!(parameters.to_string(), "socks5://proxy.example.com:1080");
239
240        let parameters = "http://proxy.example.com:8080".parse::<ProxyParameters>().unwrap();
241        assert_eq!(parameters.proxy_type, ProxyType::Http);
242        assert_eq!(parameters.addr, Some(("proxy.example.com", 8080).into()));
243        assert_eq!(parameters.credentials, None);
244        assert_eq!(parameters.to_string(), "http://proxy.example.com:8080");
245
246        let parameters = "http://proxy.example.com".parse::<ProxyParameters>().unwrap();
247        assert_eq!(parameters.proxy_type, ProxyType::Http);
248        assert_eq!(parameters.addr, Some(("proxy.example.com", 80).into()));
249        assert_eq!(parameters.credentials, None);
250        assert_eq!(parameters.to_string(), "http://proxy.example.com:80");
251
252        assert!("socks5://proxy.example.com".parse::<ProxyParameters>().is_err());
253    }
254
255    #[test]
256    fn parse_with_credentials() {
257        let parameters = "socks5://user:password@proxy.example.com:1080".parse::<ProxyParameters>().unwrap();
258        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
259        assert_eq!(parameters.addr, Some(("proxy.example.com", 1080).into()));
260        assert_eq!(parameters.credentials, Some(UserKey::new("user", "password")));
261        assert_eq!(parameters.to_string(), "socks5://user:password@proxy.example.com:1080");
262
263        let parameters = "socks5://user@123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
264        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
265        assert_eq!(parameters.addr, Some(("123.45.67.89", 1080).into()));
266        assert_eq!(parameters.credentials, Some(UserKey::new("user", "")));
267        assert_eq!(parameters.to_string(), "socks5://user@123.45.67.89:1080");
268
269        let parameters = "socks5://:password@123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
270        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
271        assert_eq!(parameters.addr, Some(("123.45.67.89", 1080).into()));
272        assert_eq!(parameters.credentials, Some(UserKey::new("", "password")));
273        assert_eq!(parameters.to_string(), "socks5://:password@123.45.67.89:1080");
274    }
275
276    #[test]
277    fn parse_with_percent_encoded_credentials() {
278        let parameters = "socks5://user%40name:pa%24%24@proxy.example.com:1080"
279            .parse::<ProxyParameters>()
280            .unwrap();
281
282        assert_eq!(parameters.proxy_type, ProxyType::Socks5);
283        assert_eq!(parameters.addr, Some(("proxy.example.com", 1080).into()));
284        assert_eq!(parameters.credentials, Some(UserKey::new("user@name", "pa$$")));
285        assert_eq!(parameters.to_string(), "socks5://user%40name:pa%24%24@proxy.example.com:1080");
286    }
287
288    #[test]
289    fn parse_none_proxy() {
290        let parameters = "none".parse::<ProxyParameters>().unwrap();
291
292        assert_eq!(parameters.proxy_type, ProxyType::None);
293        assert_eq!(parameters.addr, None);
294        assert_eq!(parameters.credentials, None);
295        assert_eq!(parameters.to_string(), "none");
296        assert!(url::Url::try_from(parameters).is_err());
297    }
298
299    #[test]
300    fn parse_invalid_proxy_type() {
301        let err = "ftp://proxy.example.com:21".parse::<ProxyParameters>().unwrap_err();
302        assert!(format!("{err}").contains("invalid proxy type"));
303    }
304
305    #[test]
306    fn try_from_string_and_url_work() {
307        let s = "http://proxy.example.com:8080".to_string();
308        let parameters = ProxyParameters::try_from(s.clone()).unwrap();
309        assert_eq!(parameters.proxy_type, ProxyType::Http);
310        assert_eq!(parameters.addr, Some(("proxy.example.com", 8080).into()));
311        assert_eq!(parameters.to_string(), "http://proxy.example.com:8080");
312
313        let url = url::Url::parse(&s).unwrap();
314        let parameters = ProxyParameters::try_from(url).unwrap();
315        assert_eq!(parameters.proxy_type, ProxyType::Http);
316        assert_eq!(parameters.addr, Some(("proxy.example.com", 8080).into()));
317    }
318
319    #[test]
320    fn proxy_parameters_new_and_into_url() {
321        let parameters = ProxyParameters::new(
322            ProxyType::Socks5,
323            Some(("proxy.example.com", 1080).into()),
324            Some(UserKey::new("user", "password")),
325        );
326
327        let url = url::Url::try_from(parameters.clone()).unwrap();
328        assert_eq!(url.as_str(), "socks5://user:password@proxy.example.com:1080");
329        assert_eq!(parameters.to_string(), "socks5://user:password@proxy.example.com:1080");
330    }
331
332    #[cfg(feature = "serde")]
333    #[test]
334    fn serde_string_round_trip() {
335        let parameters: ProxyParameters = serde_json::from_str(r#""mixed://127.0.0.1:3080""#).unwrap();
336        assert_eq!(parameters.to_string(), "mixed://127.0.0.1:3080");
337        assert_eq!(serde_json::to_string(&parameters).unwrap(), r#""mixed://127.0.0.1:3080""#);
338
339        let no_proxy: ProxyParameters = serde_json::from_str(r#""none""#).unwrap();
340        assert_eq!(no_proxy.proxy_type, ProxyType::None);
341        assert_eq!(serde_json::to_string(&no_proxy).unwrap(), r#""none""#);
342    }
343}