use std::fmt;
use std::net::Ipv6Addr;
use std::str::FromStr;
use url::{Host, Url};
use crate::auth::ProxyAuth;
use crate::error::Error;
use crate::util::{percent_decode, quote_if_not_credential_shaped, split_host_port};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Scheme {
Http,
Https,
Ftp,
Socks,
All,
}
impl Scheme {
pub const ALL: [Scheme; 5] = [
Scheme::Http,
Scheme::Https,
Scheme::Ftp,
Scheme::Socks,
Scheme::All,
];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Scheme::Http => "http",
Scheme::Https => "https",
Scheme::Ftp => "ftp",
Scheme::Socks => "socks",
Scheme::All => "all",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Scheme> {
match name.trim().to_ascii_lowercase().as_str() {
"http" => Some(Scheme::Http),
"https" => Some(Scheme::Https),
"ftp" => Some(Scheme::Ftp),
"socks" => Some(Scheme::Socks),
"all" => Some(Scheme::All),
_ => None,
}
}
}
impl fmt::Display for Scheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum ProxyScheme {
Http,
Https,
Socks4,
Socks4a,
Socks5,
Socks5h,
}
impl ProxyScheme {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
ProxyScheme::Http => "http",
ProxyScheme::Https => "https",
ProxyScheme::Socks4 => "socks4",
ProxyScheme::Socks4a => "socks4a",
ProxyScheme::Socks5 => "socks5",
ProxyScheme::Socks5h => "socks5h",
}
}
#[must_use]
pub fn default_port(self) -> u16 {
match self {
ProxyScheme::Http => 80,
ProxyScheme::Https => 443,
ProxyScheme::Socks4
| ProxyScheme::Socks4a
| ProxyScheme::Socks5
| ProxyScheme::Socks5h => 1080,
}
}
}
impl fmt::Display for ProxyScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ProxyScheme {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"http" => Ok(ProxyScheme::Http),
"https" => Ok(ProxyScheme::Https),
"socks4" => Ok(ProxyScheme::Socks4),
"socks" | "socks5" => Ok(ProxyScheme::Socks5),
"socks4a" => Ok(ProxyScheme::Socks4a),
"socks5h" => Ok(ProxyScheme::Socks5h),
other => Err(Error::UnsupportedProxyScheme(
crate::util::redact_offending_token(other),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ProxyEndpoint {
pub scheme_hint: Option<ProxyScheme>,
pub host: Host,
pub port: u16,
pub auth: Option<ProxyAuth>,
}
impl ProxyEndpoint {
#[must_use]
pub fn new(host: Host, port: u16) -> Self {
Self {
scheme_hint: None,
host,
port,
auth: None,
}
}
#[must_use]
pub fn with_scheme_hint(mut self, scheme: ProxyScheme) -> Self {
self.scheme_hint = Some(scheme);
self
}
#[must_use]
pub fn with_auth(mut self, auth: ProxyAuth) -> Self {
self.auth = Some(auth);
self
}
pub fn parse(input: &str, default_port: u16) -> Result<Self, Error> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(Error::proxy_server(input, "empty address"));
}
let (scheme_hint, rest) = match trimmed.split_once("://") {
Some((scheme, rest)) => (Some(scheme.parse::<ProxyScheme>()?), rest),
None => (None, trimmed),
};
let (rest, dropped) = match rest.find(['/', '?', '#']) {
Some(cut) => rest.split_at(cut),
None => (rest, ""),
};
if dropped.contains('@') {
return Err(Error::proxy_server(
input,
"an '@' follows a '/', '?' or '#', so the host before it is the start of a \
user name or password rather than the proxy (percent-encode the character \
in the credentials: %2F, %3F, %23)",
));
}
if rest.contains(char::REPLACEMENT_CHARACTER) {
return Err(Error::proxy_server(input, "value is not valid text"));
}
let (auth, host_port) = match rest.rsplit_once('@') {
Some((userinfo, host_port)) => (Some(parse_userinfo(userinfo)), host_port),
None => (None, rest),
};
if host_port.is_empty() {
return Err(Error::proxy_server(input, "missing host"));
}
let (host_text, port) =
split_host_port(host_port).map_err(|reason| Error::proxy_server(input, reason))?;
let host = parse_host(host_text).map_err(|reason| Error::proxy_server(input, reason))?;
if port.is_none() && host_port.len() > host_text.len() {
return Err(Error::proxy_server(input, "empty port after ':'"));
}
let port = port
.or_else(|| scheme_hint.map(ProxyScheme::default_port))
.unwrap_or(default_port);
Ok(Self {
scheme_hint,
host,
port,
auth,
})
}
#[must_use]
pub fn authority(&self) -> String {
match &self.host {
Host::Ipv6(ip) => format!("[{ip}]:{}", self.port),
other => format!("{other}:{}", self.port),
}
}
}
impl fmt::Display for ProxyEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(scheme) = self.scheme_hint {
write!(f, "{scheme}://")?;
}
f.write_str(&self.authority())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProxyEntry {
Use(ProxyEndpoint),
Disabled,
Unusable(crate::diagnostic::RejectedValue),
}
impl ProxyEntry {
#[must_use]
pub fn endpoint(&self) -> Option<&ProxyEndpoint> {
match self {
ProxyEntry::Use(endpoint) => Some(endpoint),
ProxyEntry::Disabled | ProxyEntry::Unusable(_) => None,
}
}
#[must_use]
pub fn is_disabled(&self) -> bool {
matches!(self, ProxyEntry::Disabled)
}
#[must_use]
pub fn rejected(&self) -> Option<&crate::diagnostic::RejectedValue> {
match self {
ProxyEntry::Unusable(rejected) => Some(rejected),
ProxyEntry::Use(_) | ProxyEntry::Disabled => None,
}
}
}
pub(crate) fn parse_host(text: &str) -> Result<Host, String> {
let text = text.trim();
if text.is_empty() {
return Err("missing host".to_owned());
}
if let Some(inner) = text.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
return inner.parse::<Ipv6Addr>().map(Host::Ipv6).map_err(|_| {
format!(
"invalid IPv6 literal {}",
quote_if_not_credential_shaped(inner)
)
});
}
if let Ok(ip) = text.parse::<Ipv6Addr>() {
return Ok(Host::Ipv6(ip));
}
Host::parse(text)
.map_err(|e| format!("invalid host {}: {e}", quote_if_not_credential_shaped(text)))
}
fn request_host_ref(url: &Url) -> Option<Host<&str>> {
match url.host()? {
Host::Domain("") => None,
other => Some(other),
}
}
#[cfg(feature = "resolve")]
pub(crate) fn has_request_host(url: &Url) -> bool {
request_host_ref(url).is_some()
}
pub(crate) fn request_host(url: &Url) -> Option<Host> {
match request_host_ref(url)? {
Host::Domain(domain) => Some(Host::Domain(domain.to_owned())),
Host::Ipv4(ip) => Some(Host::Ipv4(ip)),
Host::Ipv6(ip) => Some(Host::Ipv6(ip)),
}
}
fn parse_userinfo(userinfo: &str) -> ProxyAuth {
match userinfo.split_once(':') {
Some((user, password)) => {
ProxyAuth::new(percent_decode(user), Some(percent_decode(password)))
}
None => ProxyAuth::from_username(percent_decode(userinfo)),
}
}