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