use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::sync::Arc;
use ureq_proto::http::uri::{PathAndQuery, Scheme};
use http::Uri;
use crate::http;
use crate::util::{AuthorityExt, DebugUri};
use crate::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ProxyProtocol {
Http,
Https,
Socks4,
Socks4A,
Socks5,
}
impl ProxyProtocol {
pub(crate) fn default_port(&self) -> u16 {
match self {
ProxyProtocol::Http => 80,
ProxyProtocol::Https => 443,
ProxyProtocol::Socks4 | ProxyProtocol::Socks4A | ProxyProtocol::Socks5 => 1080,
}
}
pub(crate) fn is_socks(&self) -> bool {
matches!(self, Self::Socks4 | Self::Socks4A | Self::Socks5)
}
pub(crate) fn is_connect(&self) -> bool {
matches!(self, Self::Http | Self::Https)
}
fn default_resolve_target(&self) -> bool {
match self {
ProxyProtocol::Http => false,
ProxyProtocol::Https => false,
ProxyProtocol::Socks4 => true, ProxyProtocol::Socks4A => false,
ProxyProtocol::Socks5 => false,
}
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct Proxy {
inner: Arc<ProxyInner>,
}
#[derive(Eq, Hash, PartialEq)]
struct ProxyInner {
proto: ProxyProtocol,
uri: Uri,
from_env: bool,
resolve_target: bool,
no_proxy: Option<NoProxy>,
}
impl Proxy {
pub fn new(proxy: &str) -> Result<Self, Error> {
Self::new_with_flag(proxy, None, false, None)
}
pub fn builder(p: ProxyProtocol) -> ProxyBuilder {
ProxyBuilder {
protocol: p,
host: None,
port: None,
username: None,
password: None,
resolve_target: p.default_resolve_target(),
no_proxy: None,
}
}
fn new_with_flag(
proxy: &str,
no_proxy: Option<NoProxy>,
from_env: bool,
resolve_target: Option<bool>,
) -> Result<Self, Error> {
let mut uri = proxy.parse::<Uri>().or(Err(Error::InvalidProxyUrl))?;
let _ = uri.authority().ok_or(Error::InvalidProxyUrl)?;
let scheme = match uri.scheme_str() {
Some(v) => v,
None => {
uri = insert_default_scheme(uri);
"http"
}
};
let proto: ProxyProtocol = scheme.try_into()?;
let resolve_target = resolve_target.unwrap_or(proto.default_resolve_target());
let inner = ProxyInner {
proto,
uri,
from_env,
resolve_target,
no_proxy,
};
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn try_from_env() -> Option<Self> {
const TRY_ENV: &[&str] = &[
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
];
for attempt in TRY_ENV {
if let Ok(env) = std::env::var(attempt) {
let no_proxy = NoProxy::try_from_env();
if let Ok(proxy) = Self::new_with_flag(&env, no_proxy, true, None) {
return Some(proxy);
}
}
}
None
}
pub fn protocol(&self) -> ProxyProtocol {
self.inner.proto
}
pub fn uri(&self) -> &Uri {
&self.inner.uri
}
pub fn host(&self) -> &str {
self.inner
.uri
.authority()
.map(|a| a.host())
.expect("constructor to ensure there is an authority")
}
pub fn port(&self) -> u16 {
self.inner
.uri
.authority()
.and_then(|a| a.port_u16())
.unwrap_or_else(|| self.inner.proto.default_port())
}
pub fn username(&self) -> Option<&str> {
self.inner.uri.authority().and_then(|a| a.username())
}
pub fn password(&self) -> Option<&str> {
self.inner.uri.authority().and_then(|a| a.password())
}
pub fn is_from_env(&self) -> bool {
self.inner.from_env
}
pub fn resolve_target(&self) -> bool {
self.inner.resolve_target
}
pub fn is_no_proxy(&self, uri: &Uri) -> bool {
if let (Some(no_proxy), Some(host)) = (&self.inner.no_proxy, uri.host()) {
return no_proxy.is_no_proxy(host);
}
false
}
}
fn insert_default_scheme(uri: Uri) -> Uri {
let mut parts = uri.into_parts();
parts.scheme = Some(Scheme::HTTP);
parts.path_and_query = parts
.path_and_query
.or_else(|| Some(PathAndQuery::from_static("/")));
Uri::from_parts(parts).unwrap()
}
pub struct ProxyBuilder {
protocol: ProxyProtocol,
host: Option<String>,
port: Option<u16>,
username: Option<String>,
password: Option<String>,
resolve_target: bool,
no_proxy: Option<NoProxy>,
}
impl ProxyBuilder {
pub fn host(mut self, host: &str) -> Self {
self.host = Some(host.to_string());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn username(mut self, v: &str) -> Self {
self.username = Some(v.to_string());
self
}
pub fn password(mut self, v: &str) -> Self {
self.password = Some(v.to_string());
self
}
pub fn resolve_target(mut self, do_resolve: bool) -> Self {
self.resolve_target = do_resolve;
self
}
pub fn no_proxy(mut self, expr: &str) -> Self {
if let Some(entry) = NoProxyEntry::try_parse(expr) {
if self.no_proxy.is_none() {
self.no_proxy = Some(NoProxy::default());
}
self.no_proxy.as_mut().unwrap().inner.push(entry);
}
self
}
pub fn build(self) -> Result<Proxy, Error> {
let host = self.host.as_deref().unwrap_or("localhost");
let port = self.port.unwrap_or(self.protocol.default_port());
let mut userpass = String::new();
if let Some(username) = self.username {
userpass.push_str(&username);
if let Some(password) = self.password {
userpass.push(':');
userpass.push_str(&password);
}
userpass.push('@');
}
let proxy = format!("{}://{}{}:{}", self.protocol, userpass, host, port);
Proxy::new_with_flag(&proxy, self.no_proxy, false, Some(self.resolve_target))
}
}
impl TryFrom<&str> for ProxyProtocol {
type Error = Error;
fn try_from(scheme: &str) -> Result<Self, Self::Error> {
match scheme.to_ascii_lowercase().as_str() {
"http" => Ok(ProxyProtocol::Http),
"https" => Ok(ProxyProtocol::Https),
"socks4" => Ok(ProxyProtocol::Socks4),
"socks4a" => Ok(ProxyProtocol::Socks4A),
"socks" => Ok(ProxyProtocol::Socks5),
"socks5" => Ok(ProxyProtocol::Socks5),
_ => Err(Error::InvalidProxyUrl),
}
}
}
impl fmt::Debug for Proxy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Proxy")
.field("proto", &self.inner.proto)
.field("uri", &DebugUri(&self.inner.uri))
.field("from_env", &self.inner.from_env)
.finish()
}
}
impl fmt::Display for ProxyProtocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProxyProtocol::Http => write!(f, "HTTP"),
ProxyProtocol::Https => write!(f, "HTTPS"),
ProxyProtocol::Socks4 => write!(f, "SOCKS4"),
ProxyProtocol::Socks4A => write!(f, "SOCKS4a"),
ProxyProtocol::Socks5 => write!(f, "SOCKS5"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum NoProxyEntry {
ExactHost(String),
HostSuffix(String),
MatchAll,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
struct NoProxy {
inner: Vec<NoProxyEntry>,
}
impl NoProxy {
pub fn try_from_env() -> Option<Self> {
const TRY_ENV: &[&str] = &["NO_PROXY", "no_proxy"];
for attempt in TRY_ENV {
if let Ok(env) = std::env::var(attempt) {
let inner = env.split(',').filter_map(NoProxyEntry::try_parse).collect();
return Some(Self { inner });
}
}
None
}
pub fn is_no_proxy(&self, host: &str) -> bool {
self.inner.iter().any(|entry| entry.matches(host))
}
}
impl NoProxyEntry {
fn try_parse(u: &str) -> Option<Self> {
let entry = match u {
"*" => Self::MatchAll,
u if u.starts_with("*") => {
Self::HostSuffix(u.chars().skip(1).collect::<String>().to_ascii_lowercase())
}
u if u.starts_with(".") => Self::HostSuffix(u.to_ascii_lowercase()),
_ => Self::ExactHost(u.to_ascii_lowercase()),
};
Some(entry)
}
fn matches(&self, host: &str) -> bool {
match self {
NoProxyEntry::MatchAll => true,
NoProxyEntry::ExactHost(pattern) => {
if host.chars().all(|c| !c.is_ascii_uppercase()) {
pattern == host
} else {
pattern == &host.to_ascii_lowercase()
}
}
NoProxyEntry::HostSuffix(suffix) => {
if host.len() < suffix.len() {
return false;
}
let host_suffix = &host[host.len() - suffix.len()..];
if host_suffix.chars().all(|c| !c.is_ascii_uppercase()) {
suffix == host_suffix
} else {
suffix == &host_suffix.to_ascii_lowercase()
}
}
}
}
}
#[cfg(test)]
mod tests {
use assert_no_alloc::*;
use std::str::FromStr;
use super::*;
#[test]
fn parse_proxy_fakeproto() {
assert!(Proxy::new("fakeproto://localhost").is_err());
}
#[test]
fn parse_proxy_http_user_pass_server_port() {
let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
}
#[test]
fn parse_proxy_http_user_pass_server_port_trailing_slash() {
let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999/").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
}
#[test]
fn parse_proxy_socks4_user_pass_server_port() {
let proxy = Proxy::new("socks4://user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4);
}
#[test]
fn parse_proxy_socks4a_user_pass_server_port() {
let proxy = Proxy::new("socks4a://user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4A);
}
#[test]
fn parse_proxy_socks_user_pass_server_port() {
let proxy = Proxy::new("socks://user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
}
#[test]
fn parse_proxy_socks5_user_pass_server_port() {
let proxy = Proxy::new("socks5://user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
}
#[test]
fn parse_proxy_user_pass_server_port() {
let proxy = Proxy::new("user:p@ssw0rd@localhost:9999").unwrap();
assert_eq!(proxy.username(), Some("user"));
assert_eq!(proxy.password(), Some("p@ssw0rd"));
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
}
#[test]
fn parse_proxy_server_port() {
let proxy = Proxy::new("localhost:9999").unwrap();
assert_eq!(proxy.username(), None);
assert_eq!(proxy.password(), None);
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 9999);
assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
}
#[test]
fn parse_proxy_server() {
let proxy = Proxy::new("localhost").unwrap();
assert_eq!(proxy.username(), None);
assert_eq!(proxy.password(), None);
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 80);
assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
}
#[test]
fn no_proxy_exact_host_matching() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("localhost")
.no_proxy("127.0.0.1")
.no_proxy("api.internal.com")
.build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "localhost"));
assert!(is_no_proxy(&p, "127.0.0.1"));
assert!(is_no_proxy(&p, "api.internal.com"));
assert!(!is_no_proxy(&p, "mylocalhost"));
assert!(!is_no_proxy(&p, "localhost.example.com"));
assert!(!is_no_proxy(&p, "127.0.0.2"));
assert!(!is_no_proxy(&p, "api.internal.com.evil.com"));
assert!(!is_no_proxy(&p, "docs.rs"));
}
#[test]
fn no_proxy_wildcard_suffix_matching() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("*.internal.com")
.no_proxy("*.dev")
.build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "api.internal.com"));
assert!(is_no_proxy(&p, "auth.internal.com"));
assert!(is_no_proxy(&p, "db.internal.com"));
assert!(is_no_proxy(&p, "app.dev"));
assert!(is_no_proxy(&p, "test.dev"));
assert!(!is_no_proxy(&p, "internal.com"));
assert!(!is_no_proxy(&p, "dev"));
assert!(!is_no_proxy(&p, "api.external.com"));
assert!(!is_no_proxy(&p, "app.prod"));
assert!(!is_no_proxy(&p, "docs.rs"));
}
#[test]
fn no_proxy_dot_suffix_matching() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy(".internal.com")
.no_proxy(".staging")
.build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "api.internal.com"));
assert!(is_no_proxy(&p, "auth.internal.com"));
assert!(is_no_proxy(&p, "db.sub.internal.com"));
assert!(is_no_proxy(&p, "app.staging"));
assert!(is_no_proxy(&p, "test.staging"));
assert!(!is_no_proxy(&p, "internal.com"));
assert!(!is_no_proxy(&p, "staging"));
assert!(!is_no_proxy(&p, "api.external.com"));
assert!(!is_no_proxy(&p, "prod"));
assert!(!is_no_proxy(&p, "docs.rs"));
}
#[test]
fn no_proxy_match_all_wildcard() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("*")
.build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "localhost"));
assert!(is_no_proxy(&p, "127.0.0.1"));
assert!(is_no_proxy(&p, "api.example.com"));
assert!(is_no_proxy(&p, "docs.rs"));
assert!(is_no_proxy(&p, "github.com"));
assert!(is_no_proxy(&p, "any.random.domain"));
}
#[test]
fn no_proxy_mixed_patterns() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("localhost") .no_proxy("*.dev") .no_proxy(".staging") .no_proxy("127.0.0.1") .build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "localhost"));
assert!(is_no_proxy(&p, "127.0.0.1"));
assert!(is_no_proxy(&p, "api.dev"));
assert!(is_no_proxy(&p, "test.dev"));
assert!(is_no_proxy(&p, "app.staging"));
assert!(!is_no_proxy(&p, "staging"));
assert!(!is_no_proxy(&p, "dev")); assert!(!is_no_proxy(&p, "api.prod")); assert!(!is_no_proxy(&p, "docs.rs")); assert!(!is_no_proxy(&p, "127.0.0.2")); }
#[test]
fn no_proxy_case_insensitive_matching() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("localhost")
.no_proxy("*.Example.Com")
.no_proxy(".INTERNAL")
.build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "localhost")); assert!(is_no_proxy(&p, "LOCALHOST")); assert!(is_no_proxy(&p, "LocalHost"));
assert!(is_no_proxy(&p, "api.example.com")); assert!(is_no_proxy(&p, "api.EXAMPLE.COM")); assert!(is_no_proxy(&p, "API.example.COM")); assert!(is_no_proxy(&p, "api.Example.Com"));
assert!(is_no_proxy(&p, "app.internal")); assert!(is_no_proxy(&p, "app.INTERNAL")); assert!(is_no_proxy(&p, "APP.Internal")); assert!(!is_no_proxy(&p, "INTERNAL")); assert!(!is_no_proxy(&p, "internal")); }
#[test]
fn no_proxy_edge_cases() {
let p = Proxy::builder(ProxyProtocol::Http)
.host("proxy.example.com")
.port(8080)
.no_proxy("") .no_proxy("single") .no_proxy("*..") .no_proxy("..") .no_proxy("192.168.1.1") .no_proxy("*.local") .build()
.unwrap();
fn is_no_proxy(p: &Proxy, host: &str) -> bool {
let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
p.is_no_proxy(&uri)
}
assert!(is_no_proxy(&p, "single"));
assert!(is_no_proxy(&p, "192.168.1.1"));
assert!(!is_no_proxy(&p, "192.168.1.2"));
assert!(is_no_proxy(&p, "printer.local"));
assert!(is_no_proxy(&p, "router.local"));
assert!(!is_no_proxy(&p, "local"));
assert!(is_no_proxy(&p, "something..")); assert!(!is_no_proxy(&p, "something.else"));
}
#[test]
fn proxy_clone_does_not_allocate() {
let c = Proxy::new("socks://1.2.3.4").unwrap();
assert_no_alloc(|| c.clone());
}
#[test]
fn proxy_new_default_scheme() {
let c = Proxy::new("localhost:1234").unwrap();
assert_eq!(c.protocol(), ProxyProtocol::Http);
assert_eq!(c.uri(), "http://localhost:1234");
}
#[test]
fn proxy_empty_env_url() {
let result = Proxy::new_with_flag("", None, false, None);
assert!(result.is_err());
}
#[test]
fn proxy_invalid_env_url() {
let result = Proxy::new_with_flag("r32/?//52:**", None, false, None);
assert!(result.is_err());
}
#[test]
fn proxy_builder() {
let proxy = Proxy::builder(ProxyProtocol::Socks4)
.host("my-proxy.com")
.port(5551)
.resolve_target(false)
.build()
.unwrap();
assert_eq!(proxy.protocol(), ProxyProtocol::Socks4);
assert_eq!(proxy.uri(), "SOCKS4://my-proxy.com:5551/");
assert_eq!(proxy.host(), "my-proxy.com");
assert_eq!(proxy.port(), 5551);
assert_eq!(proxy.username(), None);
assert_eq!(proxy.password(), None);
assert_eq!(proxy.is_from_env(), false);
assert_eq!(proxy.resolve_target(), false);
}
#[test]
fn proxy_builder_username() {
let proxy = Proxy::builder(ProxyProtocol::Https)
.username("hemligearne")
.build()
.unwrap();
assert_eq!(proxy.protocol(), ProxyProtocol::Https);
assert_eq!(proxy.uri(), "https://hemligearne@localhost:443/");
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 443);
assert_eq!(proxy.username(), Some("hemligearne"));
assert_eq!(proxy.password(), None);
assert_eq!(proxy.is_from_env(), false);
assert_eq!(proxy.resolve_target(), false);
}
#[test]
fn proxy_builder_username_password() {
let proxy = Proxy::builder(ProxyProtocol::Https)
.username("hemligearne")
.password("kulgrej")
.build()
.unwrap();
assert_eq!(proxy.protocol(), ProxyProtocol::Https);
assert_eq!(proxy.uri(), "https://hemligearne:kulgrej@localhost:443/");
assert_eq!(proxy.host(), "localhost");
assert_eq!(proxy.port(), 443);
assert_eq!(proxy.username(), Some("hemligearne"));
assert_eq!(proxy.password(), Some("kulgrej"));
assert_eq!(proxy.is_from_env(), false);
assert_eq!(proxy.resolve_target(), false);
}
}