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 Http = 0,
158 Socks4,
159 #[default]
160 Socks5,
161 None,
162}
163
164impl TryFrom<&str> for ProxyType {
165 type Error = Error;
166 fn try_from(value: &str) -> Result<Self> {
167 match value.to_ascii_lowercase().as_str() {
168 "http" => Ok(ProxyType::Http),
169 "socks4" => Ok(ProxyType::Socks4),
170 "socks5" => Ok(ProxyType::Socks5),
171 "none" => Ok(ProxyType::None),
172 scheme => Err(Error::from(&format!("`{scheme}` is an invalid proxy type"))),
173 }
174 }
175}
176
177impl std::fmt::Display for ProxyType {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 ProxyType::Socks4 => write!(f, "socks4"),
181 ProxyType::Socks5 => write!(f, "socks5"),
182 ProxyType::Http => write!(f, "http"),
183 ProxyType::None => write!(f, "none"),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn default_proxy_parameters() {
194 let parameters = ProxyParameters::default();
195
196 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
197 assert_eq!(parameters.addr, "127.0.0.1:1080".parse().unwrap());
198 assert_eq!(parameters.credentials, None);
199 assert_eq!(parameters.to_string(), "socks5://127.0.0.1:1080");
200 }
201
202 #[test]
203 fn parse_without_credentials() {
204 let parameters = "socks5://123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
205 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
206 assert_eq!(parameters.addr, ("123.45.67.89", 1080).into());
207 assert_eq!(parameters.addr.get_type(), crate::protocol::AddressType::IPv4);
208 assert_eq!(parameters.credentials, None);
209 assert_eq!(parameters.to_string(), "socks5://123.45.67.89:1080");
210
211 let parameters = "socks5://proxy.example.com:1080".parse::<ProxyParameters>().unwrap();
212
213 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
214 assert_eq!(parameters.addr, ("proxy.example.com", 1080).into());
215 assert_eq!(parameters.credentials, None);
216 assert_eq!(parameters.to_string(), "socks5://proxy.example.com:1080");
217
218 let parameters = "http://proxy.example.com:8080".parse::<ProxyParameters>().unwrap();
219 assert_eq!(parameters.proxy_type, ProxyType::Http);
220 assert_eq!(parameters.addr, ("proxy.example.com", 8080).into());
221 assert_eq!(parameters.credentials, None);
222 assert_eq!(parameters.to_string(), "http://proxy.example.com:8080");
223
224 let parameters = "http://proxy.example.com".parse::<ProxyParameters>().unwrap();
225 assert_eq!(parameters.proxy_type, ProxyType::Http);
226 assert_eq!(parameters.addr, ("proxy.example.com", 80).into());
227 assert_eq!(parameters.credentials, None);
228 assert_eq!(parameters.to_string(), "http://proxy.example.com:80");
229
230 assert!("socks5://proxy.example.com".parse::<ProxyParameters>().is_err());
231 }
232
233 #[test]
234 fn parse_with_credentials() {
235 let parameters = "socks5://user:password@proxy.example.com:1080".parse::<ProxyParameters>().unwrap();
236 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
237 assert_eq!(parameters.addr, ("proxy.example.com", 1080).into());
238 assert_eq!(parameters.credentials, Some(UserKey::new("user", "password")));
239 assert_eq!(parameters.to_string(), "socks5://user:password@proxy.example.com:1080");
240
241 let parameters = "socks5://user@123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
242 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
243 assert_eq!(parameters.addr, ("123.45.67.89", 1080).into());
244 assert_eq!(parameters.credentials, Some(UserKey::new("user", "")));
245 assert_eq!(parameters.to_string(), "socks5://user@123.45.67.89:1080");
246
247 let parameters = "socks5://:password@123.45.67.89:1080".parse::<ProxyParameters>().unwrap();
248 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
249 assert_eq!(parameters.addr, ("123.45.67.89", 1080).into());
250 assert_eq!(parameters.credentials, Some(UserKey::new("", "password")));
251 assert_eq!(parameters.to_string(), "socks5://:password@123.45.67.89:1080");
252 }
253
254 #[test]
255 fn parse_with_percent_encoded_credentials() {
256 let parameters = "socks5://user%40name:pa%24%24@proxy.example.com:1080"
257 .parse::<ProxyParameters>()
258 .unwrap();
259
260 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
261 assert_eq!(parameters.addr, ("proxy.example.com", 1080).into());
262 assert_eq!(parameters.credentials, Some(UserKey::new("user@name", "pa$$")));
263 assert_eq!(parameters.to_string(), "socks5://user%40name:pa%24%24@proxy.example.com:1080");
264 }
265
266 #[test]
267 fn parse_none_proxy() {
268 let parameters = "none".parse::<ProxyParameters>().unwrap();
269
270 assert_eq!(parameters.proxy_type, ProxyType::None);
271 assert_eq!(parameters.addr, "0.0.0.0:0".parse().unwrap());
272 assert_eq!(parameters.credentials, None);
273 assert_eq!(parameters.to_string(), "none://0.0.0.0:0");
274 }
275
276 #[test]
277 fn parse_invalid_proxy_type() {
278 let err = "ftp://proxy.example.com:21".parse::<ProxyParameters>().unwrap_err();
279 assert!(format!("{err}").contains("invalid proxy type"));
280 }
281
282 #[test]
283 fn try_from_string_and_url_work() {
284 let s = "http://proxy.example.com:8080".to_string();
285 let parameters = ProxyParameters::try_from(s.clone()).unwrap();
286 assert_eq!(parameters.proxy_type, ProxyType::Http);
287 assert_eq!(parameters.addr, ("proxy.example.com", 8080).into());
288 assert_eq!(parameters.to_string(), "http://proxy.example.com:8080");
289
290 let url = url::Url::parse(&s).unwrap();
291 let parameters = ProxyParameters::try_from(url).unwrap();
292 assert_eq!(parameters.proxy_type, ProxyType::Http);
293 assert_eq!(parameters.addr, ("proxy.example.com", 8080).into());
294 }
295
296 #[test]
297 fn proxy_parameters_new_and_into_url() {
298 let parameters = ProxyParameters::new(
299 ProxyType::Socks5,
300 ("proxy.example.com", 1080).into(),
301 Some(UserKey::new("user", "password")),
302 );
303
304 let url = url::Url::try_from(parameters.clone()).unwrap();
305 assert_eq!(url.as_str(), "socks5://user:password@proxy.example.com:1080");
306 assert_eq!(parameters.to_string(), "socks5://user:password@proxy.example.com:1080");
307 }
308}