socks5_impl/protocol/
proxy_parameters.rs1use 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 TryFrom<&str> for ProxyParameters {
38 type Error = Error;
39 fn try_from(s: &str) -> Result<Self> {
40 if s == "none" {
41 return Ok(ProxyParameters {
42 proxy_type: ProxyType::None,
43 addr: "0.0.0.0:0".parse().unwrap(),
44 credentials: None,
45 });
46 }
47
48 let e = format!("`{s}` is not a valid proxy URL");
49 let url = url::Url::parse(s).map_err(|_| Error::from(&e))?;
50 let e = format!("`{s}` does not contain a host");
51 let host = url.host_str().ok_or(Error::from(e))?;
52
53 let e = format!("`{s}` does not contain a port");
54 let port = url.port_or_known_default().ok_or(Error::from(&e))?;
55
56 let addr = (host, port).into();
57
58 let credentials = if url.username() == "" && url.password().is_none() {
59 None
60 } else {
61 use percent_encoding::percent_decode;
62 let username = percent_decode(url.username().as_bytes()).decode_utf8()?;
63 let password = percent_decode(url.password().unwrap_or("").as_bytes()).decode_utf8()?;
64 Some(UserKey::new(username, password))
65 };
66
67 let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;
68
69 Ok(ProxyParameters {
70 proxy_type,
71 addr,
72 credentials,
73 })
74 }
75}
76
77impl std::str::FromStr for ProxyParameters {
78 type Err = Error;
79 fn from_str(s: &str) -> Result<Self> {
80 Self::try_from(s)
81 }
82}
83
84#[repr(C)]
85#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
86#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
87pub enum ProxyType {
88 Http = 0,
89 Socks4,
90 #[default]
91 Socks5,
92 None,
93}
94
95impl TryFrom<&str> for ProxyType {
96 type Error = Error;
97 fn try_from(value: &str) -> Result<Self> {
98 match value.to_ascii_lowercase().as_str() {
99 "http" => Ok(ProxyType::Http),
100 "socks4" => Ok(ProxyType::Socks4),
101 "socks5" => Ok(ProxyType::Socks5),
102 "none" => Ok(ProxyType::None),
103 scheme => Err(Error::from(&format!("`{scheme}` is an invalid proxy type"))),
104 }
105 }
106}
107
108impl std::fmt::Display for ProxyType {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 ProxyType::Socks4 => write!(f, "socks4"),
112 ProxyType::Socks5 => write!(f, "socks5"),
113 ProxyType::Http => write!(f, "http"),
114 ProxyType::None => write!(f, "none"),
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn default_proxy_parameters() {
125 let parameters = ProxyParameters::default();
126
127 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
128 assert_eq!(parameters.addr, "127.0.0.1:1080".parse().unwrap());
129 assert_eq!(parameters.credentials, None);
130 assert_eq!(parameters.to_string(), "socks5://127.0.0.1:1080");
131 }
132
133 #[test]
134 fn parse_without_credentials() {
135 let parameters = "socks5://proxy.example.com:1080".parse::<ProxyParameters>().unwrap();
136
137 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
138 assert_eq!(parameters.addr, ("proxy.example.com", 1080).into());
139 assert_eq!(parameters.credentials, None);
140 assert_eq!(parameters.to_string(), "socks5://proxy.example.com:1080");
141 }
142
143 #[test]
144 fn parse_with_percent_encoded_credentials() {
145 let parameters = "socks5://user%40name:pa%24%24@proxy.example.com:1080"
146 .parse::<ProxyParameters>()
147 .unwrap();
148
149 assert_eq!(parameters.proxy_type, ProxyType::Socks5);
150 assert_eq!(parameters.addr, ("proxy.example.com", 1080).into());
151 assert_eq!(parameters.credentials, Some(UserKey::new("user@name", "pa$$")));
152 assert_eq!(parameters.to_string(), "socks5://user%40name:pa%24%24@proxy.example.com:1080");
153 }
154
155 #[test]
156 fn parse_none_proxy() {
157 let parameters = "none".parse::<ProxyParameters>().unwrap();
158
159 assert_eq!(parameters.proxy_type, ProxyType::None);
160 assert_eq!(parameters.addr, "0.0.0.0:0".parse().unwrap());
161 assert_eq!(parameters.credentials, None);
162 assert_eq!(parameters.to_string(), "none://0.0.0.0:0");
163 }
164
165 #[test]
166 fn parse_invalid_proxy_type() {
167 let err = "ftp://proxy.example.com:21".parse::<ProxyParameters>().unwrap_err();
168 assert!(format!("{err}").contains("invalid proxy type"));
169 }
170}