mirror of https://github.com/fafhrd91/actix-web
Feat: Add HTTP/HTTPS Proxy support to AWC (Fix #4008)
This commit is contained in:
parent
7ded7ee478
commit
5b15f5fadf
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add support for HTTP and HTTPS proxies in `awc::Client`. Proxies can be configured explicitly via `ClientBuilder::proxy()` or automatically detected from `HTTP_PROXY` and `HTTPS_PROXY` environment variables.
|
||||
- Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953]
|
||||
- Update `hickory-resolver` dependency to `0.26.1`.
|
||||
- Update `rand` dependency to `0.10`.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use crate::{
|
|||
connect::DefaultConnector,
|
||||
error::SendRequestError,
|
||||
middleware::{NestTransform, Redirect, Transform},
|
||||
proxy::Proxy,
|
||||
Client, ConnectRequest, ConnectResponse,
|
||||
};
|
||||
|
||||
|
|
@ -34,6 +35,10 @@ pub struct ClientBuilder<S = (), M = ()> {
|
|||
middleware: M,
|
||||
local_address: Option<IpAddr>,
|
||||
max_redirects: u8,
|
||||
/// Explicit proxy configuration (overrides env-var detection when set).
|
||||
proxy: Option<Proxy>,
|
||||
/// When `true`, env-var proxy detection is disabled entirely.
|
||||
disable_proxy: bool,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
|
|
@ -63,6 +68,8 @@ impl ClientBuilder {
|
|||
middleware: (),
|
||||
local_address: None,
|
||||
max_redirects: 10,
|
||||
proxy: None,
|
||||
disable_proxy: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +100,8 @@ where
|
|||
stream_window_size: self.stream_window_size,
|
||||
conn_window_size: self.conn_window_size,
|
||||
max_redirects: self.max_redirects,
|
||||
proxy: self.proxy,
|
||||
disable_proxy: self.disable_proxy,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +150,42 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Route all outgoing connections through `proxy`.
|
||||
///
|
||||
/// Calling this method disables automatic environment-variable detection
|
||||
/// for the lifetime of the client. To opt out of both explicit and
|
||||
/// automatic proxies, call [`ClientBuilder::no_proxy`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use awc::{Client, proxy::Proxy};
|
||||
///
|
||||
/// let client = Client::builder()
|
||||
/// .proxy(Proxy::new("http://proxy.corp.example:3128"))
|
||||
/// .finish();
|
||||
/// ```
|
||||
pub fn proxy(mut self, proxy: Proxy) -> Self {
|
||||
self.proxy = Some(proxy);
|
||||
// Explicit proxy overrides env detection.
|
||||
self.disable_proxy = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable proxy support entirely, including automatic detection from the
|
||||
/// `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use awc::Client;
|
||||
///
|
||||
/// let client = Client::builder().no_proxy().finish();
|
||||
/// ```
|
||||
pub fn no_proxy(mut self) -> Self {
|
||||
self.proxy = None;
|
||||
self.disable_proxy = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indicates the initial window size (in octets) for
|
||||
/// HTTP2 stream-level flow control for received data.
|
||||
///
|
||||
|
|
@ -245,6 +290,8 @@ where
|
|||
connector: self.connector,
|
||||
local_address: self.local_address,
|
||||
max_redirects: self.max_redirects,
|
||||
proxy: self.proxy,
|
||||
disable_proxy: self.disable_proxy,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +331,14 @@ where
|
|||
connector = connector.local_address(val);
|
||||
}
|
||||
|
||||
let connector = DefaultConnector::new(connector.finish());
|
||||
// Resolve the proxy: explicit takes priority, then env-var auto-detection.
|
||||
let proxy = if self.disable_proxy {
|
||||
self.proxy
|
||||
} else {
|
||||
self.proxy.or_else(Proxy::from_env)
|
||||
};
|
||||
|
||||
let connector = DefaultConnector::new(connector.finish(), proxy);
|
||||
let connector = boxed::rc_service(self.middleware.new_transform(connector));
|
||||
|
||||
Client(ClientConfig {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
future::Future,
|
||||
net,
|
||||
io, net,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
|
|
@ -9,11 +9,15 @@ use std::{
|
|||
use actix_codec::Framed;
|
||||
use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead};
|
||||
use actix_service::Service;
|
||||
use bytes::BytesMut;
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use http::header::HeaderValue;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
any_body::AnyBody,
|
||||
client::{Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError},
|
||||
proxy::Proxy,
|
||||
ClientResponse,
|
||||
};
|
||||
|
||||
|
|
@ -82,11 +86,12 @@ impl ConnectResponse {
|
|||
|
||||
pub struct DefaultConnector<S> {
|
||||
connector: S,
|
||||
proxy: Option<Proxy>,
|
||||
}
|
||||
|
||||
impl<S> DefaultConnector<S> {
|
||||
pub(crate) fn new(connector: S) -> Self {
|
||||
Self { connector }
|
||||
pub(crate) fn new(connector: S, proxy: Option<Proxy>) -> Self {
|
||||
Self { connector, proxy }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,21 +107,63 @@ where
|
|||
actix_service::forward_ready!(connector);
|
||||
|
||||
fn call(&self, req: ConnectRequest) -> Self::Future {
|
||||
// connect to the host
|
||||
let fut = match req {
|
||||
ConnectRequest::Client(ref head, .., addr) => self.connector.call(ClientConnect {
|
||||
uri: head.as_ref().uri.clone(),
|
||||
addr,
|
||||
}),
|
||||
ConnectRequest::Tunnel(ref head, addr) => self.connector.call(ClientConnect {
|
||||
uri: head.uri.clone(),
|
||||
addr,
|
||||
}),
|
||||
// Determine the target URI and whether we need a CONNECT tunnel.
|
||||
let (target_uri, need_tunnel, tunnel_host) = match &req {
|
||||
ConnectRequest::Client(head, ..) => {
|
||||
let uri = &head.as_ref().uri;
|
||||
let is_https = uri.scheme_str() == Some("https");
|
||||
let host_port = format!(
|
||||
"{}:{}",
|
||||
uri.host().unwrap_or(""),
|
||||
uri.port_u16().unwrap_or(if is_https { 443 } else { 80 })
|
||||
);
|
||||
(uri.clone(), is_https, host_port)
|
||||
}
|
||||
ConnectRequest::Tunnel(head, _) => {
|
||||
let uri = &head.uri;
|
||||
let is_https = uri.scheme_str() == Some("https");
|
||||
let host_port = format!(
|
||||
"{}:{}",
|
||||
uri.host().unwrap_or(""),
|
||||
uri.port_u16().unwrap_or(if is_https { 443 } else { 80 })
|
||||
);
|
||||
(uri.clone(), is_https, host_port)
|
||||
}
|
||||
};
|
||||
|
||||
// When a proxy is configured:
|
||||
// - HTTP -> connect to proxy, send the full request URI as-is (proxy forwards)
|
||||
// - HTTPS -> connect to proxy, issue a CONNECT tunnel, then send request normally
|
||||
let (connect_uri, proxy_auth, proxy_tunnel_host) = match &self.proxy {
|
||||
Some(proxy) => {
|
||||
let auth = proxy.auth_header.clone();
|
||||
if need_tunnel {
|
||||
// HTTPS: connect to proxy first, then CONNECT-tunnel to target
|
||||
(proxy.uri.clone(), auth, Some(tunnel_host))
|
||||
} else {
|
||||
// HTTP: connect to proxy directly; the connector keeps the original
|
||||
// request unchanged so the full URI goes in the request line.
|
||||
(proxy.uri.clone(), auth, None)
|
||||
}
|
||||
}
|
||||
None => (target_uri, None, None),
|
||||
};
|
||||
|
||||
let addr = match &req {
|
||||
ConnectRequest::Client(_, _, a) => *a,
|
||||
ConnectRequest::Tunnel(_, a) => *a,
|
||||
};
|
||||
|
||||
let fut = self.connector.call(ClientConnect {
|
||||
uri: connect_uri,
|
||||
addr,
|
||||
});
|
||||
|
||||
ConnectRequestFuture::Connection {
|
||||
fut,
|
||||
req: Some(req),
|
||||
proxy_auth,
|
||||
proxy_tunnel_host,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,17 +177,20 @@ pin_project_lite::pin_project! {
|
|||
Connection {
|
||||
#[pin]
|
||||
fut: Fut,
|
||||
req: Option<ConnectRequest>
|
||||
},
|
||||
Client {
|
||||
fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
|
||||
req: Option<ConnectRequest>,
|
||||
proxy_auth: Option<HeaderValue>,
|
||||
proxy_tunnel_host: Option<String>,
|
||||
},
|
||||
Tunnel {
|
||||
fut: LocalBoxFuture<
|
||||
'static,
|
||||
Result<(ResponseHead, Framed<Connection<Io>, ClientCodec>), SendRequestError>,
|
||||
>,
|
||||
}
|
||||
fut: LocalBoxFuture<'static, Result<(ResponseHead, Framed<Connection<Io>, ClientCodec>), SendRequestError>>,
|
||||
},
|
||||
Client {
|
||||
fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>,
|
||||
},
|
||||
ProxyConnect {
|
||||
fut: LocalBoxFuture<'static, Result<Connection<Io>, SendRequestError>>,
|
||||
req: Option<ConnectRequest>,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,46 +202,155 @@ where
|
|||
type Output = Result<ConnectResponse, SendRequestError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project() {
|
||||
ConnectRequestProj::Connection { fut, req } => {
|
||||
let connection = ready!(fut.poll(cx))?;
|
||||
let req = req.take().unwrap();
|
||||
loop {
|
||||
match self.as_mut().project() {
|
||||
ConnectRequestProj::Connection {
|
||||
fut,
|
||||
req,
|
||||
proxy_auth,
|
||||
proxy_tunnel_host,
|
||||
} => {
|
||||
let connection = ready!(fut.poll(cx))?;
|
||||
let req = req.take().unwrap();
|
||||
|
||||
match req {
|
||||
ConnectRequest::Client(head, body, ..) => {
|
||||
// send request
|
||||
let fut = ConnectRequestFuture::Client {
|
||||
fut: connection.send_request(head, body),
|
||||
};
|
||||
|
||||
self.set(fut);
|
||||
if let Some(host) = proxy_tunnel_host.take() {
|
||||
// We connected to the proxy over TCP. Now negotiate a CONNECT
|
||||
// tunnel so subsequent TLS/HTTP2 traffic goes to the real target.
|
||||
let auth_hdr = proxy_auth.take();
|
||||
let fut = Box::pin(proxy_connect_tunnel(connection, host, auth_hdr));
|
||||
self.set(ConnectRequestFuture::ProxyConnect {
|
||||
fut,
|
||||
req: Some(req),
|
||||
});
|
||||
// Loop back to poll the new state immediately.
|
||||
continue;
|
||||
}
|
||||
|
||||
ConnectRequest::Tunnel(head, ..) => {
|
||||
// send request
|
||||
let fut = ConnectRequestFuture::Tunnel {
|
||||
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
||||
};
|
||||
|
||||
self.set(fut);
|
||||
match req {
|
||||
ConnectRequest::Client(head, body, ..) => {
|
||||
let fut = ConnectRequestFuture::Client {
|
||||
fut: connection.send_request(head, body),
|
||||
};
|
||||
self.set(fut);
|
||||
}
|
||||
ConnectRequest::Tunnel(head, ..) => {
|
||||
let fut = ConnectRequestFuture::Tunnel {
|
||||
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
||||
};
|
||||
self.set(fut);
|
||||
}
|
||||
}
|
||||
// Loop back to poll the newly set state.
|
||||
continue;
|
||||
}
|
||||
|
||||
self.poll(cx)
|
||||
}
|
||||
ConnectRequestProj::ProxyConnect { fut, req } => {
|
||||
let connection = ready!(fut.as_mut().poll(cx))?;
|
||||
let req = req.take().unwrap();
|
||||
|
||||
ConnectRequestProj::Client { fut } => {
|
||||
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
||||
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
||||
head, payload,
|
||||
))))
|
||||
}
|
||||
match req {
|
||||
ConnectRequest::Client(head, body, ..) => {
|
||||
let inner_fut = ConnectRequestFuture::Client {
|
||||
fut: connection.send_request(head, body),
|
||||
};
|
||||
self.set(inner_fut);
|
||||
}
|
||||
ConnectRequest::Tunnel(head, ..) => {
|
||||
let inner_fut = ConnectRequestFuture::Tunnel {
|
||||
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
||||
};
|
||||
self.set(inner_fut);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ConnectRequestProj::Tunnel { fut } => {
|
||||
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
|
||||
let framed = framed.into_map_io(|io| Box::new(io) as _);
|
||||
Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed)))
|
||||
ConnectRequestProj::Client { fut } => {
|
||||
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
||||
return Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
||||
head, payload,
|
||||
))));
|
||||
}
|
||||
|
||||
ConnectRequestProj::Tunnel { fut } => {
|
||||
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
|
||||
let framed = framed.into_map_io(|io| Box::new(io) as _);
|
||||
return Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Negotiate an HTTP `CONNECT` tunnel with the proxy.
|
||||
///
|
||||
/// Writes the `CONNECT host:port HTTP/1.1` preamble to `connection`, reads
|
||||
/// the proxy's `200 Connection established` response, and returns the
|
||||
/// connection (now a raw tunnel) or an error.
|
||||
async fn proxy_connect_tunnel<Io: ConnectionIo>(
|
||||
mut connection: Connection<Io>,
|
||||
host: String,
|
||||
auth: Option<HeaderValue>,
|
||||
) -> Result<Connection<Io>, SendRequestError> {
|
||||
// Build the CONNECT request bytes manually to avoid going through
|
||||
// the full HTTP codec machinery (CONNECT is a very simple exchange).
|
||||
let mut request = format!("CONNECT {host} HTTP/1.1\r\nHost: {host}\r\n");
|
||||
|
||||
if let Some(auth_hdr) = auth {
|
||||
if let Ok(val) = auth_hdr.to_str() {
|
||||
request.push_str(&format!("Proxy-Authorization: {val}\r\n"));
|
||||
}
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
|
||||
// `Connection<Io>` derefs to its inner `Io` type which implements
|
||||
// `AsyncRead + AsyncWrite` via `ActixStream`/`ConnectionIo`.
|
||||
connection
|
||||
.write_all(request.as_bytes())
|
||||
.await
|
||||
.map_err(|err| SendRequestError::Connect(ConnectError::Io(err)))?;
|
||||
|
||||
// Read the proxy response line-by-line until the blank line.
|
||||
let mut buf = BytesMut::with_capacity(256);
|
||||
let mut raw = [0u8; 256];
|
||||
loop {
|
||||
let n = connection
|
||||
.read(&mut raw)
|
||||
.await
|
||||
.map_err(|err| SendRequestError::Connect(ConnectError::Io(err)))?;
|
||||
if n == 0 {
|
||||
return Err(SendRequestError::Connect(ConnectError::Io(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"proxy closed connection",
|
||||
))));
|
||||
}
|
||||
buf.extend_from_slice(&raw[..n]);
|
||||
|
||||
// The proxy response ends with "\r\n\r\n".
|
||||
if buf.windows(4).any(|w| w == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the first line is "HTTP/1.x 200 ..."
|
||||
let status_line = std::str::from_utf8(&buf)
|
||||
.unwrap_or("")
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("");
|
||||
|
||||
if !status_line.contains("200") {
|
||||
return Err(SendRequestError::Connect(ConnectError::Io(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
format!("proxy CONNECT failed: {status_line}"),
|
||||
))));
|
||||
}
|
||||
|
||||
// Consume the response bytes from the connection's read buffer so they
|
||||
// do not leak into the TLS handshake / HTTP request that follows.
|
||||
// The bytes are already in `buf`; the connection's stream read them from
|
||||
// the socket and they are now consumed.
|
||||
drop(buf);
|
||||
|
||||
Ok(connection)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ mod connect;
|
|||
pub mod error;
|
||||
mod frozen;
|
||||
pub mod middleware;
|
||||
pub mod proxy;
|
||||
mod request;
|
||||
mod responses;
|
||||
mod sender;
|
||||
|
|
@ -141,6 +142,7 @@ pub use self::{
|
|||
client::{Client, Connect, Connector},
|
||||
connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse},
|
||||
frozen::{FrozenClientRequest, FrozenSendBuilder},
|
||||
proxy::Proxy,
|
||||
request::ClientRequest,
|
||||
sender::SendClientRequest,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
//! HTTP proxy support for [`crate::Client`].
|
||||
//!
|
||||
//! # Configuring a Proxy Explicitly
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use awc::{Client, proxy::Proxy};
|
||||
//!
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() {
|
||||
//! let proxy = Proxy::new("http://proxy.example.com:8080");
|
||||
//! let client = Client::builder().proxy(proxy).finish();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Automatic Environment-Variable Detection
|
||||
//!
|
||||
//! When no proxy is set explicitly and [`crate::ClientBuilder::no_proxy`] has not been
|
||||
//! called, [`Client`](crate::Client) will read `HTTPS_PROXY` (or `https_proxy`) first, then
|
||||
//! `HTTP_PROXY` (or `http_proxy`) from the process environment and use the first value it
|
||||
//! finds.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! // HTTP_PROXY=http://proxy.example.com:8080 cargo run
|
||||
//! use awc::Client;
|
||||
//!
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() {
|
||||
//! // Automatically picks up the env var:
|
||||
//! let client = Client::new();
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use actix_http::Uri;
|
||||
use base64::prelude::*;
|
||||
use http::header::HeaderValue;
|
||||
|
||||
/// An HTTP proxy configuration.
|
||||
///
|
||||
/// Can be constructed manually or read from the `HTTP_PROXY` / `HTTPS_PROXY`
|
||||
/// environment variables via [`Proxy::from_env`].
|
||||
#[derive(Clone)]
|
||||
pub struct Proxy {
|
||||
/// Full URI of the proxy server (e.g. `http://proxy.corp:3128`).
|
||||
pub(crate) uri: Uri,
|
||||
|
||||
/// Optional `Proxy-Authorization` header value (pre-formatted, ready to send).
|
||||
pub(crate) auth_header: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Proxy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Proxy")
|
||||
.field("uri", &self.uri)
|
||||
.field(
|
||||
"auth_header",
|
||||
&self.auth_header.as_ref().map(|_| "<redacted>"),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
/// Construct a proxy that connects through `uri`.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `uri` cannot be parsed as a valid [`Uri`].
|
||||
pub fn new(uri: impl TryInto<Uri, Error = impl fmt::Debug>) -> Self {
|
||||
let uri = uri.try_into().expect("proxy URI must be valid");
|
||||
Self {
|
||||
uri,
|
||||
auth_header: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a proxy with HTTP Basic authentication credentials.
|
||||
///
|
||||
/// The credentials are encoded once during construction so that no heap
|
||||
/// allocation occurs per-request.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `uri` cannot be parsed as a valid [`Uri`].
|
||||
pub fn new_with_creds(
|
||||
uri: impl TryInto<Uri, Error = impl fmt::Debug>,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Self {
|
||||
let raw = format!("{username}:{password}");
|
||||
let encoded = BASE64_STANDARD.encode(raw);
|
||||
let header_value = HeaderValue::from_str(&format!("Basic {encoded}"))
|
||||
.expect("base64-encoded credentials are always valid header values");
|
||||
|
||||
Self {
|
||||
uri: uri.try_into().expect("proxy URI must be valid"),
|
||||
auth_header: Some(header_value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an HTTP proxy from environment variables.
|
||||
///
|
||||
/// Checked in order (first match wins):
|
||||
/// 1. `HTTPS_PROXY`
|
||||
/// 2. `https_proxy`
|
||||
/// 3. `HTTP_PROXY`
|
||||
/// 4. `http_proxy`
|
||||
///
|
||||
/// Returns `None` when none of the variables are set or when the value
|
||||
/// cannot be parsed as a [`Uri`].
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let candidates = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
||||
|
||||
for var in candidates {
|
||||
if let Ok(val) = std::env::var(var) {
|
||||
if let Ok(uri) = Uri::from_str(&val) {
|
||||
log::debug!("awc: using proxy from env var {var}={val}");
|
||||
return Some(Self {
|
||||
uri,
|
||||
auth_header: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the proxy's URI.
|
||||
pub fn uri(&self) -> &Uri {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns the proxy hostname (and port if non-standard) as a `String`,
|
||||
/// suitable for the `Host` header of a `CONNECT` request.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn host_port(&self) -> Option<String> {
|
||||
let host = self.uri.host()?;
|
||||
match self.uri.port_u16() {
|
||||
Some(port) => Some(format!("{host}:{port}")),
|
||||
None => Some(host.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn proxy_new_parses_uri() {
|
||||
let p = Proxy::new("http://proxy.example.com:8080");
|
||||
assert_eq!(p.uri.host(), Some("proxy.example.com"));
|
||||
assert_eq!(p.uri.port_u16(), Some(8080));
|
||||
assert!(p.auth_header.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_with_credentials_encodes_header() {
|
||||
let p = Proxy::new_with_creds("http://proxy.example.com:3128", "alice", "s3cr3t");
|
||||
let hdr = p.auth_header.unwrap();
|
||||
// "alice:s3cr3t" => YWxpY2U6czNjcjN0
|
||||
assert_eq!(hdr.to_str().unwrap(), "Basic YWxpY2U6czNjcjN0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_from_env_none_when_absent() {
|
||||
// make sure the variable is not set in the test environment
|
||||
std::env::remove_var("HTTP_PROXY");
|
||||
std::env::remove_var("http_proxy");
|
||||
std::env::remove_var("HTTPS_PROXY");
|
||||
std::env::remove_var("https_proxy");
|
||||
assert!(Proxy::from_env().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_from_env_reads_http_proxy() {
|
||||
std::env::remove_var("HTTPS_PROXY");
|
||||
std::env::remove_var("https_proxy");
|
||||
std::env::set_var("HTTP_PROXY", "http://myproxy:3128");
|
||||
let p = Proxy::from_env().expect("should find HTTP_PROXY");
|
||||
assert_eq!(p.uri.host(), Some("myproxy"));
|
||||
std::env::remove_var("HTTP_PROXY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_from_env_https_takes_priority() {
|
||||
std::env::set_var("HTTP_PROXY", "http://http-proxy:3128");
|
||||
std::env::set_var("HTTPS_PROXY", "http://https-proxy:8080");
|
||||
let p = Proxy::from_env().expect("should find HTTPS_PROXY");
|
||||
assert_eq!(p.uri.host(), Some("https-proxy"));
|
||||
std::env::remove_var("HTTP_PROXY");
|
||||
std::env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_host_port_formats_correctly() {
|
||||
let p = Proxy::new("http://proxy.local:9090");
|
||||
assert_eq!(p.host_port(), Some("proxy.local:9090".to_owned()));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue