proxy-types 0.1.0

Basic proxy types for my own uses
Documentation
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::LazyLock;
use percent_encoding::{utf8_percent_encode, AsciiSet};
use url::{ParseError, Url};
use crate::ParseErr::InvalidURL;

/// authentication using username:password pairs
pub type BasicAuthentication = (String, String);

#[derive(Clone, Debug)]
pub enum Proxy {
    HTTP(HTTPProxy),
    HTTPS(HTTPSProxy),
    SOCKS4(SOCKS4Proxy),
    SOCKS5(SOCKS5Proxy)
}

impl Proxy {
    /// Returns auth credentials in an Option.
    /// Some protocols (i.e. SOCKS4) don't support authentication at all,
    /// it will always return [None] in that case.
    pub fn get_auth(&self) -> Option<&BasicAuthentication> {
        match self {
            Proxy::SOCKS4(_) => None,
            Proxy::SOCKS5(x) => x.auth.as_ref(),
            Proxy::HTTP(x) => x.auth.as_ref(),
            Proxy::HTTPS(x) => x.auth.as_ref(),
        }
    }
    /// Gets the URL scheme of this Proxy in lowercase.
    /// Usually it only matters what variant of Proxy (i.e. HTTP, HTTPS) you're using for the result,
    /// but in the case of [Proxy::SOCKS5], `h` is appended to the end if `SOCKS5Proxy.h` is true.
    /// And it's the same thing for [Proxy::SOCKS4], but `a` instead of `h`
    /// (and if `SOCKS4Proxy.h` is true).
    pub fn get_scheme(&self) -> &str {
        match self {
            Proxy::HTTP(_) => "http",
            Proxy::HTTPS(_) => "https",
            Proxy::SOCKS4(x) => if x.a { "socks4a" } else { "socks4" },
            Proxy::SOCKS5(x) => if x.h { "socks5h" } else { "socks5" }
        }
    }
    /// Gets the hostname / host of this Proxy.
    pub fn get_host(&self) -> &String {
        match self {
            Proxy::HTTP(x) => &x.hostname,
            Proxy::HTTPS(x) => &x.hostname,
            Proxy::SOCKS4(x) => &x.hostname,
            Proxy::SOCKS5(x) => &x.hostname,
        }
    }
    /// Gets the port of this Proxy.
    pub fn get_port(&self) -> u16 {
        match self {
            Proxy::HTTP(x) => x.port,
            Proxy::HTTPS(x) => x.port,
            Proxy::SOCKS4(x) => x.port,
            Proxy::SOCKS5(x) => x.port,
        }
    }
}

#[cfg(feature = "proxy_parse")]
#[derive(Debug, Clone)]
pub enum ParseErr {
    /// Couldn't parse a URL
    InvalidURL(ParseError),
    /// Unsupported scheme, these get normalized to lowercase.
    InvalidScheme(String),
    MissingHost,
    /// The proxy protocol is missing a default port in [DEFAULT_PORTS] and you didn't provide one
    MissingPort,
    /// If you're using a proxy protocol (i.e. SOCKS4) that doesn't support authentication,
    /// and you provided authentication credentials.
    AuthUnsupported
}

impl From<ParseError> for ParseErr {
    fn from(err: ParseError) -> ParseErr {
        InvalidURL(err)
    }
}

/// Maps a lowercase (proxy) protocol name to its default port
static DEFAULT_PORTS: LazyLock<HashMap<&str, u16>> = LazyLock::new(|| {
    [
        ("http", 80),
        ("https", 443),
        ("socks4", 1080),
        ("socks5", 1080)
    ].into_iter().collect()
});

#[cfg(feature = "proxy_parse")]
impl FromStr for Proxy {
    type Err = ParseErr;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let a = s.parse::<Url>()?;
        let Some(host) = a.host() else { return Err(ParseErr::MissingHost) };
        let lc = a.scheme().to_lowercase();
        let Some(port) = a.port().or(DEFAULT_PORTS.get(lc.as_str()).cloned()) else { return Err(ParseErr::MissingPort) };
        let auth: Option<BasicAuthentication> = if let Some(p) = a.password() {
            Some((a.username().into(), p.into()))
        } else { None };
        match lc.as_str() {
            "http" => Ok(Proxy::HTTP(HTTPProxy { hostname: host.to_string(), port, auth })),
            "https" => Ok(Proxy::HTTPS(HTTPSProxy { hostname: host.to_string(), port, auth })),
            "socks4" => {
                if auth.is_some() {
                    return Err(ParseErr::AuthUnsupported)
                }
                Ok(Proxy::SOCKS4(SOCKS4Proxy { hostname: host.to_string(), port, a: false }))
            },
            "socks5" => Ok(Proxy::SOCKS5(SOCKS5Proxy { hostname: host.to_string(), port, auth, h: false })),
            "socks4h" => {
                if auth.is_some() {
                    return Err(ParseErr::AuthUnsupported)
                }
                Ok(Proxy::SOCKS4(SOCKS4Proxy { hostname: host.to_string(), port, a: true }))
            },
            "socks5h" => Ok(Proxy::SOCKS5(SOCKS5Proxy { hostname: host.to_string(), port, auth, h: true })),

            scheme => Err(ParseErr::InvalidScheme(scheme.into())),
        }
    }
}

#[cfg(feature = "proxy_parse")]
impl From<&Proxy> for Url {
    fn from(px: &Proxy) -> Self {
        let scheme = match px {
            Proxy::HTTP(_) => "http",
            Proxy::HTTPS(_) => "https",
            Proxy::SOCKS4(_) => "socks4",
            Proxy::SOCKS5(_) => "socks5"
        };
        let auth_stuff = px.get_auth().map(|a| {
            let ascii_set = &AsciiSet::EMPTY;
            let u = utf8_percent_encode(a.0.as_str(), ascii_set);
            let p = utf8_percent_encode(a.1.as_str(), ascii_set);
            format!("{u}:{p}@")
        }).unwrap_or_else(|| "".into());
        let host = px.get_host();
        let port = px.get_port();
        Url::parse(format!("{scheme}://{auth_stuff}{host}:{port}").as_str()).unwrap()
    }
}
#[cfg(feature = "proxy_parse")]
impl From<Proxy> for Url {
    fn from(px: Proxy) -> Self {
        Url::from(&px)
    }
}

#[derive(Debug, Clone)]
pub struct HTTPProxy {
    pub hostname: String,
    pub port: u16,
    pub auth: Option<BasicAuthentication>
}

#[derive(Debug, Clone)]
pub struct HTTPSProxy {
    pub hostname: String,
    pub port: u16,
    pub auth: Option<BasicAuthentication>
}

#[derive(Debug, Clone)]
pub struct SOCKS4Proxy {
    pub hostname: String,
    pub port: u16,
    /// If the proxy server resolves the hostname
    /// instead of you directly resolving it to an IP.
    pub a: bool
}

#[derive(Debug, Clone)]
pub struct SOCKS5Proxy {
    pub hostname: String,
    pub port: u16,
    pub auth: Option<BasicAuthentication>,
    /// If the proxy server resolves the hostname
    /// instead of you directly resolving it to an IP.
    pub h: bool
}