Skip to main content

socks5_impl/protocol/
proxy_parameters.rs

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