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