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(¶meters).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}