Skip to main content

socks_hub_core/
config.rs

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