1use serde_derive::{Deserialize, Serialize};
2use socks5_impl::protocol::UserKey;
3use std::net::SocketAddr;
4
5#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize)]
7#[command(author, version, about = "SOCKS5 hub for downstreams proxy of HTTP or SOCKS5.", long_about = None)]
8pub struct Config {
9 #[arg(short, long, value_parser = |s: &str| ArgProxy::try_from(s), value_name = "URL")]
14 pub listen_proxy_role: ArgProxy,
15
16 #[arg(short, long, value_parser = |s: &str| ArgProxy::try_from(s), value_name = "URL")]
18 pub remote_server: ArgProxy,
19
20 #[arg(short, long, value_name = "path")]
22 pub acl_file: Option<std::path::PathBuf>,
23
24 #[arg(short, long, value_name = "level", default_value = "info")]
26 pub verbosity: ArgVerbosity,
27}
28
29impl Default for Config {
30 fn default() -> Self {
31 let remote_server: ArgProxy = "socks5://127.0.0.1:1080".try_into().unwrap();
32 Config {
33 listen_proxy_role: ArgProxy::default(),
34 remote_server,
35 acl_file: None,
36 verbosity: ArgVerbosity::Info,
37 }
38 }
39}
40
41impl Config {
42 pub fn parse_args() -> Self {
43 <Self as clap::Parser>::parse()
44 }
45
46 pub fn new(listen_proxy_role: &str, remote_server: &str) -> Self {
47 Config {
48 listen_proxy_role: listen_proxy_role.try_into().unwrap(),
49 remote_server: remote_server.try_into().unwrap(),
50 ..Config::default()
51 }
52 }
53
54 pub fn listen_proxy_role(&mut self, listen_proxy_role: &str) -> &mut Self {
55 self.listen_proxy_role = listen_proxy_role.try_into().unwrap();
56 self
57 }
58
59 pub fn remote_server(&mut self, remote_server: &str) -> &mut Self {
60 self.remote_server = remote_server.try_into().unwrap();
61 self
62 }
63
64 pub fn acl_file<P: Into<std::path::PathBuf>>(&mut self, acl_file: P) -> &mut Self {
65 self.acl_file = Some(acl_file.into());
66 self
67 }
68
69 pub fn verbosity(&mut self, verbosity: ArgVerbosity) -> &mut Self {
70 self.verbosity = verbosity;
71 self
72 }
73
74 pub fn get_credentials(&self) -> Credentials {
75 self.listen_proxy_role.credentials.clone().unwrap_or_default()
76 }
77
78 pub fn get_s5_credentials(&self) -> Credentials {
79 self.remote_server.credentials.clone().unwrap_or_default()
80 }
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct ArgProxy {
85 pub proxy_type: ProxyType,
86 pub addr: SocketAddr,
87 pub credentials: Option<Credentials>,
88}
89
90impl Default for ArgProxy {
91 fn default() -> Self {
92 ArgProxy {
93 proxy_type: ProxyType::Http,
94 addr: "127.0.0.1:8080".parse().unwrap(),
95 credentials: None,
96 }
97 }
98}
99
100impl std::fmt::Display for ArgProxy {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 let auth = match &self.credentials {
103 Some(creds) => format!("{creds}"),
104 None => "".to_owned(),
105 };
106 if auth.is_empty() {
107 write!(f, "{}://{}", &self.proxy_type, &self.addr)
108 } else {
109 write!(f, "{}://{}@{}", &self.proxy_type, auth, &self.addr)
110 }
111 }
112}
113
114impl TryFrom<&str> for ArgProxy {
115 type Error = std::io::Error;
116 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
117 use std::io::{Error, ErrorKind::InvalidInput};
118 let e = format!("`{s}` is not a valid proxy URL");
119 let url = url::Url::parse(s).map_err(|_| Error::new(InvalidInput, e.clone()))?;
120 let e = format!("`{s}` does not contain a host");
121 let host = url.host_str().ok_or(Error::new(InvalidInput, e))?;
122
123 let e = format!("`{s}` does not contain a port");
124 let port = url.port_or_known_default().ok_or(Error::new(InvalidInput, e))?;
125
126 let e2 = format!("`{host}` does not resolve to a usable IP address");
127 use std::net::ToSocketAddrs;
128 let addr = (host, port).to_socket_addrs()?.next().ok_or(Error::new(InvalidInput, e2))?;
129
130 let credentials = if url.username() == "" && url.password().is_none() {
131 None
132 } else {
133 use percent_encoding::percent_decode;
134 let username = percent_decode(url.username().as_bytes())
135 .decode_utf8()
136 .map_err(|e| Error::new(InvalidInput, e))?;
137 let password = percent_decode(url.password().unwrap_or("").as_bytes())
138 .decode_utf8()
139 .map_err(|e| Error::new(InvalidInput, e))?;
140 Some(Credentials::new(&username, &password))
141 };
142
143 let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;
144
145 Ok(ArgProxy {
146 proxy_type,
147 addr,
148 credentials,
149 })
150 }
151}
152
153#[repr(C)]
154#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)]
155pub enum ProxyType {
156 #[default]
157 Http = 0,
158 Socks5,
159}
160
161impl std::fmt::Display for ProxyType {
162 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
163 match self {
164 ProxyType::Http => write!(f, "http"),
165 ProxyType::Socks5 => write!(f, "socks5"),
166 }
167 }
168}
169
170impl TryFrom<&str> for ProxyType {
171 type Error = std::io::Error;
172 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
173 use std::io::{Error, ErrorKind::InvalidInput};
174 match value {
175 "http" => Ok(ProxyType::Http),
176 "socks5" => Ok(ProxyType::Socks5),
177 scheme => Err(Error::new(InvalidInput, format!("`{scheme}` is an invalid proxy type"))),
178 }
179 }
180}
181
182#[repr(C)]
183#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)]
184pub enum ArgVerbosity {
185 Off = 0,
186 Error,
187 Warn,
188 #[default]
189 Info,
190 Debug,
191 Trace,
192}
193
194impl From<ArgVerbosity> for log::LevelFilter {
195 fn from(verbosity: ArgVerbosity) -> Self {
196 match verbosity {
197 ArgVerbosity::Off => log::LevelFilter::Off,
198 ArgVerbosity::Error => log::LevelFilter::Error,
199 ArgVerbosity::Warn => log::LevelFilter::Warn,
200 ArgVerbosity::Info => log::LevelFilter::Info,
201 ArgVerbosity::Debug => log::LevelFilter::Debug,
202 ArgVerbosity::Trace => log::LevelFilter::Trace,
203 }
204 }
205}
206
207impl From<log::Level> for ArgVerbosity {
208 fn from(level: log::Level) -> Self {
209 match level {
210 log::Level::Error => ArgVerbosity::Error,
211 log::Level::Warn => ArgVerbosity::Warn,
212 log::Level::Info => ArgVerbosity::Info,
213 log::Level::Debug => ArgVerbosity::Debug,
214 log::Level::Trace => ArgVerbosity::Trace,
215 }
216 }
217}
218
219impl std::fmt::Display for ArgVerbosity {
220 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
221 match self {
222 ArgVerbosity::Off => write!(f, "off"),
223 ArgVerbosity::Error => write!(f, "error"),
224 ArgVerbosity::Warn => write!(f, "warn"),
225 ArgVerbosity::Info => write!(f, "info"),
226 ArgVerbosity::Debug => write!(f, "debug"),
227 ArgVerbosity::Trace => write!(f, "trace"),
228 }
229 }
230}
231
232#[derive(Debug, Default, Clone, Serialize, Deserialize)]
233pub struct Credentials {
234 pub username: Option<String>,
235 pub password: Option<String>,
236}
237
238impl Credentials {
239 pub fn new(username: &str, password: &str) -> Self {
240 Credentials {
241 username: Some(username.to_string()),
242 password: Some(password.to_string()),
243 }
244 }
245
246 pub fn to_vec(&self) -> Vec<u8> {
247 self.to_string().as_bytes().to_vec()
248 }
249
250 pub fn is_empty(&self) -> bool {
251 self.to_vec().is_empty()
252 }
253}
254
255impl TryFrom<Credentials> for UserKey {
256 type Error = std::io::Error;
257 fn try_from(creds: Credentials) -> Result<Self, Self::Error> {
258 match (creds.username, creds.password) {
259 (Some(u), Some(p)) => Ok(UserKey::new(u, p)),
260 _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "username and password")),
261 }
262 }
263}
264
265impl std::fmt::Display for Credentials {
266 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
267 use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
268 let empty = "".to_owned();
269 let u = percent_encode(self.username.as_ref().unwrap_or(&empty).as_bytes(), NON_ALPHANUMERIC).to_string();
270 let p = percent_encode(self.password.as_ref().unwrap_or(&empty).as_bytes(), NON_ALPHANUMERIC).to_string();
271 match (u.is_empty(), p.is_empty()) {
272 (true, true) => write!(f, ""),
273 (true, false) => write!(f, ":{p}"),
274 (false, true) => write!(f, "{u}:"),
275 (false, false) => write!(f, "{u}:{p}"),
276 }
277 }
278}