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
|
## 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]
|
- 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 `hickory-resolver` dependency to `0.26.1`.
|
||||||
- Update `rand` dependency to `0.10`.
|
- Update `rand` dependency to `0.10`.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
connect::DefaultConnector,
|
connect::DefaultConnector,
|
||||||
error::SendRequestError,
|
error::SendRequestError,
|
||||||
middleware::{NestTransform, Redirect, Transform},
|
middleware::{NestTransform, Redirect, Transform},
|
||||||
|
proxy::Proxy,
|
||||||
Client, ConnectRequest, ConnectResponse,
|
Client, ConnectRequest, ConnectResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,6 +35,10 @@ pub struct ClientBuilder<S = (), M = ()> {
|
||||||
middleware: M,
|
middleware: M,
|
||||||
local_address: Option<IpAddr>,
|
local_address: Option<IpAddr>,
|
||||||
max_redirects: u8,
|
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 {
|
impl ClientBuilder {
|
||||||
|
|
@ -63,6 +68,8 @@ impl ClientBuilder {
|
||||||
middleware: (),
|
middleware: (),
|
||||||
local_address: None,
|
local_address: None,
|
||||||
max_redirects: 10,
|
max_redirects: 10,
|
||||||
|
proxy: None,
|
||||||
|
disable_proxy: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +100,8 @@ where
|
||||||
stream_window_size: self.stream_window_size,
|
stream_window_size: self.stream_window_size,
|
||||||
conn_window_size: self.conn_window_size,
|
conn_window_size: self.conn_window_size,
|
||||||
max_redirects: self.max_redirects,
|
max_redirects: self.max_redirects,
|
||||||
|
proxy: self.proxy,
|
||||||
|
disable_proxy: self.disable_proxy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +150,42 @@ where
|
||||||
self
|
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
|
/// Indicates the initial window size (in octets) for
|
||||||
/// HTTP2 stream-level flow control for received data.
|
/// HTTP2 stream-level flow control for received data.
|
||||||
///
|
///
|
||||||
|
|
@ -245,6 +290,8 @@ where
|
||||||
connector: self.connector,
|
connector: self.connector,
|
||||||
local_address: self.local_address,
|
local_address: self.local_address,
|
||||||
max_redirects: self.max_redirects,
|
max_redirects: self.max_redirects,
|
||||||
|
proxy: self.proxy,
|
||||||
|
disable_proxy: self.disable_proxy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,7 +331,14 @@ where
|
||||||
connector = connector.local_address(val);
|
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));
|
let connector = boxed::rc_service(self.middleware.new_transform(connector));
|
||||||
|
|
||||||
Client(ClientConfig {
|
Client(ClientConfig {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
net,
|
io, net,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
|
@ -9,11 +9,15 @@ use std::{
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead};
|
use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead};
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
|
use bytes::BytesMut;
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
|
use http::header::HeaderValue;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any_body::AnyBody,
|
any_body::AnyBody,
|
||||||
client::{Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError},
|
client::{Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError},
|
||||||
|
proxy::Proxy,
|
||||||
ClientResponse,
|
ClientResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,11 +86,12 @@ impl ConnectResponse {
|
||||||
|
|
||||||
pub struct DefaultConnector<S> {
|
pub struct DefaultConnector<S> {
|
||||||
connector: S,
|
connector: S,
|
||||||
|
proxy: Option<Proxy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> DefaultConnector<S> {
|
impl<S> DefaultConnector<S> {
|
||||||
pub(crate) fn new(connector: S) -> Self {
|
pub(crate) fn new(connector: S, proxy: Option<Proxy>) -> Self {
|
||||||
Self { connector }
|
Self { connector, proxy }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,21 +107,63 @@ where
|
||||||
actix_service::forward_ready!(connector);
|
actix_service::forward_ready!(connector);
|
||||||
|
|
||||||
fn call(&self, req: ConnectRequest) -> Self::Future {
|
fn call(&self, req: ConnectRequest) -> Self::Future {
|
||||||
// connect to the host
|
// Determine the target URI and whether we need a CONNECT tunnel.
|
||||||
let fut = match req {
|
let (target_uri, need_tunnel, tunnel_host) = match &req {
|
||||||
ConnectRequest::Client(ref head, .., addr) => self.connector.call(ClientConnect {
|
ConnectRequest::Client(head, ..) => {
|
||||||
uri: head.as_ref().uri.clone(),
|
let uri = &head.as_ref().uri;
|
||||||
addr,
|
let is_https = uri.scheme_str() == Some("https");
|
||||||
}),
|
let host_port = format!(
|
||||||
ConnectRequest::Tunnel(ref head, addr) => self.connector.call(ClientConnect {
|
"{}:{}",
|
||||||
uri: head.uri.clone(),
|
uri.host().unwrap_or(""),
|
||||||
addr,
|
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 {
|
ConnectRequestFuture::Connection {
|
||||||
fut,
|
fut,
|
||||||
req: Some(req),
|
req: Some(req),
|
||||||
|
proxy_auth,
|
||||||
|
proxy_tunnel_host,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,17 +177,20 @@ pin_project_lite::pin_project! {
|
||||||
Connection {
|
Connection {
|
||||||
#[pin]
|
#[pin]
|
||||||
fut: Fut,
|
fut: Fut,
|
||||||
req: Option<ConnectRequest>
|
req: Option<ConnectRequest>,
|
||||||
},
|
proxy_auth: Option<HeaderValue>,
|
||||||
Client {
|
proxy_tunnel_host: Option<String>,
|
||||||
fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
|
|
||||||
},
|
},
|
||||||
Tunnel {
|
Tunnel {
|
||||||
fut: LocalBoxFuture<
|
fut: LocalBoxFuture<'static, Result<(ResponseHead, Framed<Connection<Io>, ClientCodec>), SendRequestError>>,
|
||||||
'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>;
|
type Output = Result<ConnectResponse, SendRequestError>;
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
match self.as_mut().project() {
|
loop {
|
||||||
ConnectRequestProj::Connection { fut, req } => {
|
match self.as_mut().project() {
|
||||||
let connection = ready!(fut.poll(cx))?;
|
ConnectRequestProj::Connection {
|
||||||
let req = req.take().unwrap();
|
fut,
|
||||||
|
req,
|
||||||
|
proxy_auth,
|
||||||
|
proxy_tunnel_host,
|
||||||
|
} => {
|
||||||
|
let connection = ready!(fut.poll(cx))?;
|
||||||
|
let req = req.take().unwrap();
|
||||||
|
|
||||||
match req {
|
if let Some(host) = proxy_tunnel_host.take() {
|
||||||
ConnectRequest::Client(head, body, ..) => {
|
// We connected to the proxy over TCP. Now negotiate a CONNECT
|
||||||
// send request
|
// tunnel so subsequent TLS/HTTP2 traffic goes to the real target.
|
||||||
let fut = ConnectRequestFuture::Client {
|
let auth_hdr = proxy_auth.take();
|
||||||
fut: connection.send_request(head, body),
|
let fut = Box::pin(proxy_connect_tunnel(connection, host, auth_hdr));
|
||||||
};
|
self.set(ConnectRequestFuture::ProxyConnect {
|
||||||
|
fut,
|
||||||
self.set(fut);
|
req: Some(req),
|
||||||
|
});
|
||||||
|
// Loop back to poll the new state immediately.
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectRequest::Tunnel(head, ..) => {
|
match req {
|
||||||
// send request
|
ConnectRequest::Client(head, body, ..) => {
|
||||||
let fut = ConnectRequestFuture::Tunnel {
|
let fut = ConnectRequestFuture::Client {
|
||||||
fut: connection.open_tunnel(RequestHeadType::from(head)),
|
fut: connection.send_request(head, body),
|
||||||
};
|
};
|
||||||
|
self.set(fut);
|
||||||
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 } => {
|
match req {
|
||||||
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
ConnectRequest::Client(head, body, ..) => {
|
||||||
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
let inner_fut = ConnectRequestFuture::Client {
|
||||||
head, payload,
|
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 } => {
|
ConnectRequestProj::Client { fut } => {
|
||||||
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
|
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
|
||||||
let framed = framed.into_map_io(|io| Box::new(io) as _);
|
return Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
|
||||||
Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed)))
|
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;
|
pub mod error;
|
||||||
mod frozen;
|
mod frozen;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
pub mod proxy;
|
||||||
mod request;
|
mod request;
|
||||||
mod responses;
|
mod responses;
|
||||||
mod sender;
|
mod sender;
|
||||||
|
|
@ -141,6 +142,7 @@ pub use self::{
|
||||||
client::{Client, Connect, Connector},
|
client::{Client, Connect, Connector},
|
||||||
connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse},
|
connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse},
|
||||||
frozen::{FrozenClientRequest, FrozenSendBuilder},
|
frozen::{FrozenClientRequest, FrozenSendBuilder},
|
||||||
|
proxy::Proxy,
|
||||||
request::ClientRequest,
|
request::ClientRequest,
|
||||||
sender::SendClientRequest,
|
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