use alloc::{format, string::String};
use core::fmt;
use url::Url;
#[cfg(feature = "system-proxy")]
mod system;
#[cfg(feature = "system-proxy")]
pub use system::OriginProxy;
#[cfg(feature = "hyper")]
mod hyper;
#[cfg(feature = "reqwest")]
mod reqwest;
#[cfg(all(test, feature = "reqwest"))]
mod test_support;
#[derive(Clone, Default, Eq, PartialEq)]
pub enum ProxyMode {
#[default]
Automatic,
Direct,
Explicit(String),
}
impl fmt::Debug for ProxyMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Automatic => f.write_str("Automatic"),
Self::Direct => f.write_str("Direct"),
Self::Explicit(_) => f.write_str("Explicit([REDACTED])"),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ProxyOptions {
pub mode: ProxyMode,
pub no_proxy: Option<String>,
}
#[cfg(feature = "system-proxy")]
fn bypasses_all(list: &str) -> bool {
list.split(',').any(|rule| rule.trim() == "*")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum Error {
#[error("invalid native proxy setting: {0}")]
InvalidProxy(&'static str),
#[error("invalid explicit proxy URL: {0}")]
InvalidProxyUrl(&'static str),
#[error("unsupported automatic proxy source: {0}; configure an explicit proxy")]
UnsupportedProxy(&'static str),
#[error("invalid proxy target origin: {0}")]
InvalidOrigin(&'static str),
}
pub fn parse_endpoint(input: &str) -> Result<Url, Error> {
if input.is_empty() || input.chars().any(|c| c.is_whitespace() || c.is_control()) {
return Err(Error::InvalidProxyUrl(
"expected a nonempty URL without whitespace",
));
}
let url = environment_proxy(input).ok_or(Error::InvalidProxyUrl(
"expected an HTTP(S) or SOCKS4/4a/5/5h endpoint with a host",
))?;
if url.port() == Some(0) {
return Err(Error::InvalidProxyUrl("port must be nonzero"));
}
if !matches!(url.path(), "" | "/") || url.query().is_some() || url.fragment().is_some() {
return Err(Error::InvalidProxyUrl(
"paths, queries, and fragments are not supported",
));
}
if !url
.host_str()
.is_some_and(|host| host.is_ascii() && !host.contains('%'))
{
return Err(Error::InvalidProxyUrl("host must be ASCII or punycode"));
}
Ok(url)
}
fn environment_proxy(input: &str) -> Option<Url> {
if input.is_empty() {
return None;
}
let url = if input.contains("://") {
Url::parse(input)
} else {
Url::parse(&format!("http://{input}"))
}
.ok()?;
(url.host_str().is_some()
&& matches!(
url.scheme(),
"http" | "https" | "socks4" | "socks4a" | "socks5" | "socks5h"
))
.then_some(url)
}
#[cfg(test)]
mod tests;