Skip to main content

proxy_types/
lib.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3use std::sync::LazyLock;
4use percent_encoding::{utf8_percent_encode, AsciiSet};
5use url::{ParseError, Url};
6use crate::ParseErr::InvalidURL;
7
8/// authentication using username:password pairs
9pub type BasicAuthentication = (String, String);
10
11#[derive(Clone, Debug)]
12pub enum Proxy {
13    HTTP(HTTPProxy),
14    HTTPS(HTTPSProxy),
15    SOCKS4(SOCKS4Proxy),
16    SOCKS5(SOCKS5Proxy)
17}
18
19impl Proxy {
20    /// Returns auth credentials in an Option.
21    /// Some protocols (i.e. SOCKS4) don't support authentication at all,
22    /// it will always return [None] in that case.
23    pub fn get_auth(&self) -> Option<&BasicAuthentication> {
24        match self {
25            Proxy::SOCKS4(_) => None,
26            Proxy::SOCKS5(x) => x.auth.as_ref(),
27            Proxy::HTTP(x) => x.auth.as_ref(),
28            Proxy::HTTPS(x) => x.auth.as_ref(),
29        }
30    }
31    /// Gets the URL scheme of this Proxy in lowercase.
32    /// Usually it only matters what variant of Proxy (i.e. HTTP, HTTPS) you're using for the result,
33    /// but in the case of [Proxy::SOCKS5], `h` is appended to the end if `SOCKS5Proxy.h` is true.
34    /// And it's the same thing for [Proxy::SOCKS4], but `a` instead of `h`
35    /// (and if `SOCKS4Proxy.h` is true).
36    pub fn get_scheme(&self) -> &str {
37        match self {
38            Proxy::HTTP(_) => "http",
39            Proxy::HTTPS(_) => "https",
40            Proxy::SOCKS4(x) => if x.a { "socks4a" } else { "socks4" },
41            Proxy::SOCKS5(x) => if x.h { "socks5h" } else { "socks5" }
42        }
43    }
44    /// Gets the hostname / host of this Proxy.
45    pub fn get_host(&self) -> &String {
46        match self {
47            Proxy::HTTP(x) => &x.hostname,
48            Proxy::HTTPS(x) => &x.hostname,
49            Proxy::SOCKS4(x) => &x.hostname,
50            Proxy::SOCKS5(x) => &x.hostname,
51        }
52    }
53    /// Gets the port of this Proxy.
54    pub fn get_port(&self) -> u16 {
55        match self {
56            Proxy::HTTP(x) => x.port,
57            Proxy::HTTPS(x) => x.port,
58            Proxy::SOCKS4(x) => x.port,
59            Proxy::SOCKS5(x) => x.port,
60        }
61    }
62}
63
64#[cfg(feature = "proxy_parse")]
65#[derive(Debug, Clone)]
66pub enum ParseErr {
67    /// Couldn't parse a URL
68    InvalidURL(ParseError),
69    /// Unsupported scheme, these get normalized to lowercase.
70    InvalidScheme(String),
71    MissingHost,
72    /// The proxy protocol is missing a default port in [DEFAULT_PORTS] and you didn't provide one
73    MissingPort,
74    /// If you're using a proxy protocol (i.e. SOCKS4) that doesn't support authentication,
75    /// and you provided authentication credentials.
76    AuthUnsupported
77}
78
79impl From<ParseError> for ParseErr {
80    fn from(err: ParseError) -> ParseErr {
81        InvalidURL(err)
82    }
83}
84
85/// Maps a lowercase (proxy) protocol name to its default port
86static DEFAULT_PORTS: LazyLock<HashMap<&str, u16>> = LazyLock::new(|| {
87    [
88        ("http", 80),
89        ("https", 443),
90        ("socks4", 1080),
91        ("socks5", 1080)
92    ].into_iter().collect()
93});
94
95#[cfg(feature = "proxy_parse")]
96impl FromStr for Proxy {
97    type Err = ParseErr;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        let a = s.parse::<Url>()?;
101        let Some(host) = a.host() else { return Err(ParseErr::MissingHost) };
102        let lc = a.scheme().to_lowercase();
103        let Some(port) = a.port().or(DEFAULT_PORTS.get(lc.as_str()).cloned()) else { return Err(ParseErr::MissingPort) };
104        let auth: Option<BasicAuthentication> = if let Some(p) = a.password() {
105            Some((a.username().into(), p.into()))
106        } else { None };
107        match lc.as_str() {
108            "http" => Ok(Proxy::HTTP(HTTPProxy { hostname: host.to_string(), port, auth })),
109            "https" => Ok(Proxy::HTTPS(HTTPSProxy { hostname: host.to_string(), port, auth })),
110            "socks4" => {
111                if auth.is_some() {
112                    return Err(ParseErr::AuthUnsupported)
113                }
114                Ok(Proxy::SOCKS4(SOCKS4Proxy { hostname: host.to_string(), port, a: false }))
115            },
116            "socks5" => Ok(Proxy::SOCKS5(SOCKS5Proxy { hostname: host.to_string(), port, auth, h: false })),
117            "socks4h" => {
118                if auth.is_some() {
119                    return Err(ParseErr::AuthUnsupported)
120                }
121                Ok(Proxy::SOCKS4(SOCKS4Proxy { hostname: host.to_string(), port, a: true }))
122            },
123            "socks5h" => Ok(Proxy::SOCKS5(SOCKS5Proxy { hostname: host.to_string(), port, auth, h: true })),
124
125            scheme => Err(ParseErr::InvalidScheme(scheme.into())),
126        }
127    }
128}
129
130#[cfg(feature = "proxy_parse")]
131impl From<&Proxy> for Url {
132    fn from(px: &Proxy) -> Self {
133        let scheme = match px {
134            Proxy::HTTP(_) => "http",
135            Proxy::HTTPS(_) => "https",
136            Proxy::SOCKS4(_) => "socks4",
137            Proxy::SOCKS5(_) => "socks5"
138        };
139        let auth_stuff = px.get_auth().map(|a| {
140            let ascii_set = &AsciiSet::EMPTY;
141            let u = utf8_percent_encode(a.0.as_str(), ascii_set);
142            let p = utf8_percent_encode(a.1.as_str(), ascii_set);
143            format!("{u}:{p}@")
144        }).unwrap_or_else(|| "".into());
145        let host = px.get_host();
146        let port = px.get_port();
147        Url::parse(format!("{scheme}://{auth_stuff}{host}:{port}").as_str()).unwrap()
148    }
149}
150#[cfg(feature = "proxy_parse")]
151impl From<Proxy> for Url {
152    fn from(px: Proxy) -> Self {
153        Url::from(&px)
154    }
155}
156
157#[derive(Debug, Clone)]
158pub struct HTTPProxy {
159    pub hostname: String,
160    pub port: u16,
161    pub auth: Option<BasicAuthentication>
162}
163
164#[derive(Debug, Clone)]
165pub struct HTTPSProxy {
166    pub hostname: String,
167    pub port: u16,
168    pub auth: Option<BasicAuthentication>
169}
170
171#[derive(Debug, Clone)]
172pub struct SOCKS4Proxy {
173    pub hostname: String,
174    pub port: u16,
175    /// If the proxy server resolves the hostname
176    /// instead of you directly resolving it to an IP.
177    pub a: bool
178}
179
180#[derive(Debug, Clone)]
181pub struct SOCKS5Proxy {
182    pub hostname: String,
183    pub port: u16,
184    pub auth: Option<BasicAuthentication>,
185    /// If the proxy server resolves the hostname
186    /// instead of you directly resolving it to an IP.
187    pub h: bool
188}