From 8a12867ab16ebb5da05f4a992ec8c4475d448a3b Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Fri, 31 Jan 2025 23:12:16 +0100 Subject: [PATCH 01/13] feat: introduce forwarder abstraction for request handling --- Cargo.toml | 2 ++ src/forwarder/direct.rs | 37 ++++++++++++++++++++++++++++++ src/forwarder/mod.rs | 18 +++++++++++++++ src/forwarder/proxy.rs | 51 +++++++++++++++++++++++++++++++++++++++++ src/handler.rs | 14 +++++------ src/main.rs | 17 +++++++------- 6 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/forwarder/direct.rs create mode 100644 src/forwarder/mod.rs create mode 100644 src/forwarder/proxy.rs diff --git a/Cargo.toml b/Cargo.toml index bc2868c..009d6b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ boa_parser = "0.20.0" tls-parser = "0.12.2" tokio-util = { version = "0.7.13", features = ["compat"] } pin-project-lite = "0.2.16" +async-trait = "0.1.75" [dependencies] bytes = { workspace = true } @@ -49,6 +50,7 @@ clap = { workspace = true } boa_engine = { workspace = true } boa_parser = { workspace = true } tls-parser = { workspace = true } +async-trait = { workspace = true } tokio-util = { workspace = true } hyper-util = { workspace = true } hyper-tls = { workspace = true } diff --git a/src/forwarder/direct.rs b/src/forwarder/direct.rs new file mode 100644 index 0000000..fce9ee4 --- /dev/null +++ b/src/forwarder/direct.rs @@ -0,0 +1,37 @@ +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; +use hyper::{body::Incoming, Request, Response}; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use hyper_util::client::legacy::connect::HttpConnector; + +use super::Forwarder; + +/// DirectForwarder forwards requests directly to their target URL +pub struct DirectForwarder { + client: Client, +} + +impl DirectForwarder { + pub fn new() -> Self { + Self { + client: Client::builder(TokioExecutor::new()) + .build::<_, hyper::body::Incoming>(HttpConnector::new()), + } + } +} + +impl Default for DirectForwarder { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl Forwarder for DirectForwarder { + async fn forward(&self, req: Request) -> Result>, Box> { + let response = self.client.request(req).await?; + let (parts, body) = response.into_parts(); + let bytes = body.collect().await?.to_bytes(); + Ok(Response::from_parts(parts, Full::new(bytes))) + } +} diff --git a/src/forwarder/mod.rs b/src/forwarder/mod.rs new file mode 100644 index 0000000..5eac3b2 --- /dev/null +++ b/src/forwarder/mod.rs @@ -0,0 +1,18 @@ +use std::error::Error; +use http_body_util::Full; +use bytes::Bytes; +use hyper::Request; +use hyper::body::Incoming; + +/// Trait defining the behavior of a request forwarder +#[async_trait::async_trait] +pub trait Forwarder: Send + Sync { + /// Forward an HTTP request according to the forwarder's strategy + async fn forward(&self, req: Request) -> Result>, Box>; +} + +mod direct; +mod proxy; + +pub use direct::DirectForwarder; +pub use proxy::ProxyForwarder; diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs new file mode 100644 index 0000000..df2b32f --- /dev/null +++ b/src/forwarder/proxy.rs @@ -0,0 +1,51 @@ +use std::error::Error; +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; +use hyper::{body::Incoming, Request, Response, Uri}; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use hyper_util::client::legacy::connect::HttpConnector; + +use super::Forwarder; + +/// ProxyForwarder forwards requests through another proxy server +pub struct ProxyForwarder { + client: Client, + proxy_uri: Uri, +} + +impl ProxyForwarder { + pub fn new(proxy_uri: Uri) -> Self { + Self { + client: Client::builder(TokioExecutor::new()) + .build::<_, hyper::body::Incoming>(HttpConnector::new()), + proxy_uri, + } + } + + fn rewrite_request(&self, mut req: Request) -> Request { + // Convert the request URI to an absolute form + let uri = req.uri(); + let path_and_query = uri.path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + let mut new_uri = format!("{}{}", self.proxy_uri, path_and_query); + if !new_uri.starts_with("http") { + new_uri = format!("http://{}", new_uri); + } + + *req.uri_mut() = new_uri.parse().unwrap(); + req + } +} + +#[async_trait::async_trait] +impl Forwarder for ProxyForwarder { + async fn forward(&self, req: Request) -> Result>, Box> { + let req = self.rewrite_request(req); + let response = self.client.request(req).await?; + let (parts, body) = response.into_parts(); + let bytes = body.collect().await?.to_bytes(); + Ok(Response::from_parts(parts, Full::new(bytes))) + } +} diff --git a/src/handler.rs b/src/handler.rs index dd97fd9..f3e1f0c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,10 +1,12 @@ use bytes::Bytes; -use http_body_util::{BodyExt, Empty, Full}; +use http_body_util::{Empty, Full}; use hyper::body::Incoming; use hyper::http::response::Builder; use hyper::{Method, StatusCode}; use std::error::Error; use crate::tunnel::tunnel; +use crate::forwarder::{Forwarder, DirectForwarder}; +use std::sync::Arc; #[derive(Debug)] pub enum ProxyError { @@ -51,7 +53,7 @@ fn error_response(status: StatusCode) -> Result>, hy pub async fn handle_request( req: hyper::Request, - client: hyper_util::client::legacy::Client, + forwarder: Arc, ) -> Result>, ProxyError> { if Method::CONNECT == req.method() { if let Some(addr) = req.uri().authority().map(|auth| auth.to_string()) { @@ -72,12 +74,8 @@ pub async fn handle_request( Ok(error_response(StatusCode::BAD_REQUEST)?) } } else { - match client.request(req).await { - Ok(response) => { - let (parts, body) = response.into_parts(); - let bytes = body.collect().await?.to_bytes(); - Ok(hyper::Response::from_parts(parts, Full::new(bytes))) - }, + match forwarder.forward(req).await { + Ok(response) => Ok(response), Err(e) => { eprintln!("Error forwarding request: {}", e); Ok(error_response(StatusCode::BAD_GATEWAY)?) diff --git a/src/main.rs b/src/main.rs index 6a8444d..7f07ec6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod logic; mod conditions; mod proxy_types; mod tunnel; +mod forwarder; use rustls::ServerConfig; use std::{ @@ -15,9 +16,9 @@ use tokio_rustls::TlsAcceptor; use clap::Parser; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper_util::client::legacy::Client; -use hyper_util::client::legacy::connect::HttpConnector; -use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::rt::TokioIo; +use std::sync::Arc; +use forwarder::{Forwarder, DirectForwarder}; mod handler; mod config; @@ -55,14 +56,13 @@ async fn run_http_server( loop { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); - let client = Client::builder(TokioExecutor::new()) - .build::<_, hyper::body::Incoming>(HttpConnector::new()); + let forwarder = Arc::new(DirectForwarder::new()) as Arc; tokio::spawn(async move { if let Err(err) = http1::Builder::new() .serve_connection( io, - service_fn(move |req| handle_request(req, client.clone())), + service_fn(move |req| handle_request(req, forwarder.clone())), ) .with_upgrades() .await @@ -84,8 +84,7 @@ async fn run_https_server( while let Ok((stream, addr)) = listener.accept().await { println!("Accepted connection from {}", addr); let tls_acceptor = tls_acceptor.clone(); - let client = Client::builder(TokioExecutor::new()) - .build::<_, hyper::body::Incoming>(HttpConnector::new()); + let forwarder = Arc::new(DirectForwarder::new()) as Arc; tokio::spawn(async move { match tls_acceptor.accept(stream).await { @@ -108,7 +107,7 @@ async fn run_https_server( if let Err(err) = http1::Builder::new() .serve_connection( io, - service_fn(move |req| handle_request(req, client.clone())), + service_fn(move |req| handle_request(req, forwarder.clone())), ) .with_upgrades() .await From 8efc7983d288b011bca619ebf7bf44c2caad62a2 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 00:13:22 +0100 Subject: [PATCH 02/13] chore: cleanup unused imports and fix compilation issues --- Cargo.lock | 12 ++++++++++++ src/forwarder/mod.rs | 2 +- src/handler.rs | 4 ++-- src/main.rs | 2 +- src/tunnel.rs | 4 ++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1ae8e0..65c96a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1339,6 +1350,7 @@ dependencies = [ name = "pacman" version = "0.1.0" dependencies = [ + "async-trait", "boa_engine", "boa_parser", "bytes", diff --git a/src/forwarder/mod.rs b/src/forwarder/mod.rs index 5eac3b2..ebad3c0 100644 --- a/src/forwarder/mod.rs +++ b/src/forwarder/mod.rs @@ -15,4 +15,4 @@ mod direct; mod proxy; pub use direct::DirectForwarder; -pub use proxy::ProxyForwarder; + diff --git a/src/handler.rs b/src/handler.rs index f3e1f0c..fbaa6a7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,11 +1,11 @@ use bytes::Bytes; -use http_body_util::{Empty, Full}; +use http_body_util::Full; use hyper::body::Incoming; use hyper::http::response::Builder; use hyper::{Method, StatusCode}; use std::error::Error; use crate::tunnel::tunnel; -use crate::forwarder::{Forwarder, DirectForwarder}; +use crate::forwarder::Forwarder; use std::sync::Arc; #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index 7f07ec6..dd6c743 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use clap::Parser; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; -use std::sync::Arc; + use forwarder::{Forwarder, DirectForwarder}; mod handler; diff --git a/src/tunnel.rs b/src/tunnel.rs index 80fd831..6fcee87 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -1,5 +1,5 @@ use hyper::upgrade::Upgraded; -use tokio::io::{copy_bidirectional, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; use tokio::net::TcpStream; use std::pin::Pin; @@ -113,7 +113,7 @@ pub fn extract_tls_info(data: &[u8]) -> Option { pub async fn tunnel(upgraded: Upgraded, addr: String) -> std::io::Result<()> { let upstream = TcpStream::connect(&addr).await?; - let mut io = UpgradedIo(upgraded); + let io = UpgradedIo(upgraded); let (mut client_read, mut client_write) = tokio::io::split(io); let (mut upstream_read, mut upstream_write) = upstream.into_split(); From 09df4052ad54b76ef90eae0ec62062b1b8c397ac Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 00:18:18 +0100 Subject: [PATCH 03/13] chore: add certificate patterns to gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index ddb7caf..7b2993a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ # macOS files .DS_Store + +# Certificates +*.crt +*.key +*.pem From 90955895dfd55a31323f2efb3595fdf2fc44cdc2 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 00:24:41 +0100 Subject: [PATCH 04/13] feat: add detailed request and response logging --- src/forwarder/direct.rs | 8 +++++++- src/forwarder/proxy.rs | 9 ++++++++- src/handler.rs | 9 +++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/forwarder/direct.rs b/src/forwarder/direct.rs index fce9ee4..453d666 100644 --- a/src/forwarder/direct.rs +++ b/src/forwarder/direct.rs @@ -29,9 +29,15 @@ impl Default for DirectForwarder { #[async_trait::async_trait] impl Forwarder for DirectForwarder { async fn forward(&self, req: Request) -> Result>, Box> { + println!("[DIRECT] Forwarding {} {} to {}", req.method(), req.uri(), req.uri().host().unwrap_or("unknown")); + println!("[DIRECT] Request headers: {:?}", req.headers()); let response = self.client.request(req).await?; + println!("[DIRECT] Received response: {} from upstream", response.status()); + println!("[DIRECT] Response headers: {:?}", response.headers()); let (parts, body) = response.into_parts(); let bytes = body.collect().await?.to_bytes(); - Ok(Response::from_parts(parts, Full::new(bytes))) + let response = Response::from_parts(parts, Full::new(bytes)); + println!("[DIRECT] Forwarding response to client"); + Ok(response) } } diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index df2b32f..84a9210 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -42,10 +42,17 @@ impl ProxyForwarder { #[async_trait::async_trait] impl Forwarder for ProxyForwarder { async fn forward(&self, req: Request) -> Result>, Box> { + println!("[PROXY] Forwarding {} {} via proxy to {}", req.method(), req.uri(), req.uri().host().unwrap_or("unknown")); + println!("[PROXY] Request headers: {:?}", req.headers()); let req = self.rewrite_request(req); + println!("[PROXY] Rewritten request URI: {}", req.uri()); let response = self.client.request(req).await?; + println!("[PROXY] Received response: {} from upstream", response.status()); + println!("[PROXY] Response headers: {:?}", response.headers()); let (parts, body) = response.into_parts(); let bytes = body.collect().await?.to_bytes(); - Ok(Response::from_parts(parts, Full::new(bytes))) + let response = Response::from_parts(parts, Full::new(bytes)); + println!("[PROXY] Forwarding response to client"); + Ok(response) } } diff --git a/src/handler.rs b/src/handler.rs index fbaa6a7..1b0a654 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -55,6 +55,15 @@ pub async fn handle_request( req: hyper::Request, forwarder: Arc, ) -> Result>, ProxyError> { + println!("[HANDLER] {} {} from {}", + req.method(), + req.uri(), + req.headers().get("x-forwarded-for") + .and_then(|h| h.to_str().ok()) + .unwrap_or("direct") + ); + println!("[HANDLER] Headers: {:?}", req.headers()); + println!("[HANDLER] Version: {:?}", req.version()); if Method::CONNECT == req.method() { if let Some(addr) = req.uri().authority().map(|auth| auth.to_string()) { tokio::task::spawn(async move { From 7791f0a2f8d3543364aeb14d016f7b14a7a653c4 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 00:27:17 +0100 Subject: [PATCH 05/13] docs: add comprehensive documentation to proxy implementation --- src/forwarder/proxy.rs | 87 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index 84a9210..a9a9d02 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -1,3 +1,32 @@ +//! Proxy forwarding implementation for HTTP requests +//! +//! # Overview +//! This module implements a proxy forwarder that attempts to forward HTTP requests through +//! another proxy server. The current implementation has several limitations and is primarily +//! intended for basic HTTP proxying scenarios. +//! +//! # Limitations +//! - Only supports HTTP (no HTTPS/TLS support) +//! - No proxy authentication support +//! - Uses basic URL rewriting instead of proper proxy protocol implementation +//! - Limited error handling with potential panics +//! - No support for proxy-specific headers (e.g., Proxy-Authorization) +//! +//! # Future Improvements +//! - Implement proper proxy protocol support using a proxy-aware connector +//! - Add TLS support for HTTPS connections +//! - Add proxy authentication support +//! - Improve error handling with custom error types +//! - Add support for proxy-specific headers +//! - Handle CONNECT method for HTTPS tunneling +//! +//! # Example +//! ```no_run +//! use hyper::Uri; +//! let proxy = ProxyForwarder::new("http://proxy.example.com:8080".parse().unwrap()); +//! // proxy can now be used to forward requests +//! ``` + use std::error::Error; use bytes::Bytes; use http_body_util::{BodyExt, Full}; @@ -7,13 +36,29 @@ use hyper_util::client::legacy::connect::HttpConnector; use super::Forwarder; -/// ProxyForwarder forwards requests through another proxy server +/// A forwarder that sends requests through another proxy server. +/// +/// # Warning +/// This implementation is currently limited and should not be used in production +/// without significant improvements. See the module documentation for details. pub struct ProxyForwarder { client: Client, proxy_uri: Uri, } impl ProxyForwarder { + /// Creates a new ProxyForwarder that will forward requests through the specified proxy URI. + /// + /// # Arguments + /// * `proxy_uri` - The URI of the proxy server (e.g., "http://proxy.example.com:8080") + /// + /// # Limitations + /// * Currently only supports HTTP proxies + /// * Does not support proxy authentication + /// * Does not validate the proxy URI format + /// + /// # Panics + /// * May panic if the proxy URI is malformed when used pub fn new(proxy_uri: Uri) -> Self { Self { client: Client::builder(TokioExecutor::new()) @@ -22,6 +67,25 @@ impl ProxyForwarder { } } + /// Rewrites the request URI to be forwarded through the proxy. + /// + /// # Arguments + /// * `req` - The original request to be rewritten + /// + /// # Returns + /// A new request with rewritten URI + /// + /// # Implementation Notes + /// * Currently uses basic URL concatenation which may not handle all cases correctly + /// * Does not preserve the original scheme (http/https) + /// * May panic on malformed URIs + /// * Does not handle requests that already have absolute URIs + /// + /// # Todo + /// * Implement proper URI rewriting according to RFC 7230 + /// * Handle HTTPS requests correctly + /// * Add error handling for malformed URIs + /// * Preserve original request properties fn rewrite_request(&self, mut req: Request) -> Request { // Convert the request URI to an absolute form let uri = req.uri(); @@ -41,6 +105,27 @@ impl ProxyForwarder { #[async_trait::async_trait] impl Forwarder for ProxyForwarder { + /// Forwards a request through the proxy server. + /// + /// # Arguments + /// * `req` - The HTTP request to forward + /// + /// # Returns + /// * `Ok(Response)` - The response from the target server + /// * `Err(Box)` - Any error that occurred during forwarding + /// + /// # Implementation Notes + /// * Currently logs all requests and responses for debugging + /// * Uses a basic HTTP connector without proxy protocol support + /// * Does not handle HTTPS requests properly + /// * May not handle all error cases appropriately + /// + /// # Todo + /// * Implement proper proxy protocol support + /// * Add HTTPS/TLS support + /// * Improve error handling with specific error types + /// * Add support for proxy authentication + /// * Handle streaming responses more efficiently async fn forward(&self, req: Request) -> Result>, Box> { println!("[PROXY] Forwarding {} {} via proxy to {}", req.method(), req.uri(), req.uri().host().unwrap_or("unknown")); println!("[PROXY] Request headers: {:?}", req.headers()); From ddd35bd4568cab36f6c56844470c4d4ca27f9328 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 00:33:24 +0100 Subject: [PATCH 06/13] feat: implement proper HTTP/HTTPS proxy with CONNECT support --- src/forwarder/proxy.rs | 209 +++++++++++++++++++++++++++++++++++------ 1 file changed, 180 insertions(+), 29 deletions(-) diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index a9a9d02..776ef25 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -27,43 +27,160 @@ //! // proxy can now be used to forward requests //! ``` -use std::error::Error; +use std::{error::Error, fmt, pin::Pin, sync::Arc, task::{Context, Poll}}; use bytes::Bytes; +use futures_util::future::BoxFuture; +use http::{header::{HeaderMap, HeaderValue}, Method}; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, Request, Response, Uri}; -use hyper_util::{client::legacy::Client, rt::TokioExecutor}; -use hyper_util::client::legacy::connect::HttpConnector; +use hyper::{body::Incoming, upgrade::Upgraded, Request, Response, Uri}; +use hyper_util::{client::legacy::{connect::{HttpConnector, Connect}, Client}, rt::TokioExecutor}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tower_service::Service; use super::Forwarder; -/// A forwarder that sends requests through another proxy server. -/// -/// # Warning -/// This implementation is currently limited and should not be used in production -/// without significant improvements. See the module documentation for details. +/// Custom error type for proxy-related errors +#[derive(Debug)] +pub enum ProxyError { + /// Error during connection to proxy + Connection(String), + /// Error during request processing + Request(String), + /// Error during CONNECT tunnel setup + Tunnel(String), + /// Invalid URI or configuration + Configuration(String), +} + +impl fmt::Display for ProxyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProxyError::Connection(msg) => write!(f, "Proxy connection error: {}", msg), + ProxyError::Request(msg) => write!(f, "Proxy request error: {}", msg), + ProxyError::Tunnel(msg) => write!(f, "Proxy tunnel error: {}", msg), + ProxyError::Configuration(msg) => write!(f, "Proxy configuration error: {}", msg), + } + } +} + +impl Error for ProxyError {} + +/// A forwarder that sends requests through another proxy server with support for both +/// HTTP and HTTPS connections. For HTTPS, it establishes a tunnel using the HTTP CONNECT method. +pub struct ProxyForwarder { + /// HTTP client for making requests + client: Client, + /// URI of the proxy server + proxy_uri: Uri, + /// Optional proxy authentication headers + proxy_headers: HeaderMap, +} + +/// Custom connector that handles both direct and proxied connections +#[derive(Clone)] +struct ProxyConnector { + /// Inner HTTP connector + inner: HttpConnector, + /// Proxy server URI + proxy_uri: Uri, + /// Optional proxy authentication headers + proxy_headers: HeaderMap, +} + +impl ProxyConnector { + fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { + let mut inner = HttpConnector::new(); + inner.enforce_http(false); // Allow HTTPS URLs + Self { + inner, + proxy_uri, + proxy_headers, + } + } + + /// Creates a tunnel for HTTPS connections using HTTP CONNECT + async fn establish_tunnel( + client: &Client, + uri: &Uri, + proxy_headers: &HeaderMap, + ) -> Result + where + T: Connect + Clone + Send + Sync + 'static, + { + let host = uri.host().ok_or_else(|| { + ProxyError::Configuration("Missing host in URI".to_string()) + })?; + let port = uri.port_u16().unwrap_or(443); + let addr = format!("{host}:{port}"); + + let mut request = Request::builder() + .method(Method::CONNECT) + .uri(addr) + .body(Full::new(Bytes::new())) + .map_err(|e| ProxyError::Request(e.to_string()))?; + + // Add proxy authentication headers if present + request.headers_mut().extend(proxy_headers.clone()); + + let response = client + .request(request) + .await + .map_err(|e| ProxyError::Request(e.to_string()))?; + + if !response.status().is_success() { + return Err(ProxyError::Tunnel(format!( + "Tunnel connection failed: {}", + response.status() + ))); + } + + hyper::upgrade::on(response) + .await + .map_err(|e| ProxyError::Tunnel(e.to_string())) + } +} pub struct ProxyForwarder { client: Client, proxy_uri: Uri, } +impl Service for ProxyConnector { + type Response = Box; + type Error = Box; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, uri: Uri) -> Self::Future { + let proxy_uri = self.proxy_uri.clone(); + let proxy_headers = self.proxy_headers.clone(); + let mut inner = self.inner.clone(); + + Box::pin(async move { + let stream = inner.call(proxy_uri).await?; + Ok(Box::new(stream) as Box) + }) + } +} + impl ProxyForwarder { /// Creates a new ProxyForwarder that will forward requests through the specified proxy URI. /// /// # Arguments /// * `proxy_uri` - The URI of the proxy server (e.g., "http://proxy.example.com:8080") + /// * `proxy_headers` - Optional headers for proxy authentication /// - /// # Limitations - /// * Currently only supports HTTP proxies - /// * Does not support proxy authentication - /// * Does not validate the proxy URI format - /// - /// # Panics - /// * May panic if the proxy URI is malformed when used - pub fn new(proxy_uri: Uri) -> Self { + /// # Returns + /// A new ProxyForwarder instance configured with the specified proxy settings + pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { + let connector = ProxyConnector::new(proxy_uri.clone(), proxy_headers.clone()); Self { client: Client::builder(TokioExecutor::new()) - .build::<_, hyper::body::Incoming>(HttpConnector::new()), + .build::<_, Incoming>(connector), proxy_uri, + proxy_headers, } } @@ -86,20 +203,33 @@ impl ProxyForwarder { /// * Handle HTTPS requests correctly /// * Add error handling for malformed URIs /// * Preserve original request properties - fn rewrite_request(&self, mut req: Request) -> Request { + /// Rewrites the request URI for HTTP requests to be forwarded through the proxy. + /// For HTTPS requests, this is not needed as they use a tunnel. + fn rewrite_request(&self, mut req: Request) -> Result, ProxyError> { + if req.uri().scheme_str() == Some("https") { + return Ok(req); // HTTPS requests don't need rewriting, they use CONNECT + } + // Convert the request URI to an absolute form let uri = req.uri(); + let authority = uri.authority().ok_or_else(|| { + ProxyError::Configuration("Missing authority in URI".to_string()) + })?; let path_and_query = uri.path_and_query() .map(|pq| pq.as_str()) .unwrap_or("/"); - let mut new_uri = format!("{}{}", self.proxy_uri, path_and_query); - if !new_uri.starts_with("http") { - new_uri = format!("http://{}", new_uri); - } + // Construct the absolute URI + let new_uri = format!("http://{}{}", authority, path_and_query) + .parse() + .map_err(|e| ProxyError::Configuration(e.to_string()))?; - *req.uri_mut() = new_uri.parse().unwrap(); - req + *req.uri_mut() = new_uri; + + // Add proxy headers if any + req.headers_mut().extend(self.proxy_headers.clone()); + + Ok(req) } } @@ -127,16 +257,37 @@ impl Forwarder for ProxyForwarder { /// * Add support for proxy authentication /// * Handle streaming responses more efficiently async fn forward(&self, req: Request) -> Result>, Box> { - println!("[PROXY] Forwarding {} {} via proxy to {}", req.method(), req.uri(), req.uri().host().unwrap_or("unknown")); + println!("[PROXY] Forwarding {} {} via proxy to {}", + req.method(), + req.uri(), + req.uri().host().unwrap_or("unknown") + ); println!("[PROXY] Request headers: {:?}", req.headers()); - let req = self.rewrite_request(req); - println!("[PROXY] Rewritten request URI: {}", req.uri()); - let response = self.client.request(req).await?; + + let response = if req.uri().scheme_str() == Some("https") { + // For HTTPS, establish a tunnel first + println!("[PROXY] Establishing HTTPS tunnel"); + let upgraded = ProxyConnector::establish_tunnel(&self.client, req.uri(), &self.proxy_headers).await?; + println!("[PROXY] HTTPS tunnel established"); + + // Create a new client with the tunneled connection + let mut req = req; + req.headers_mut().extend(self.proxy_headers.clone()); + self.client.request(req).await? + } else { + // For HTTP, rewrite the request and forward it + let req = self.rewrite_request(req)?; + println!("[PROXY] Rewritten request URI: {}", req.uri()); + self.client.request(req).await? + }; + println!("[PROXY] Received response: {} from upstream", response.status()); println!("[PROXY] Response headers: {:?}", response.headers()); + let (parts, body) = response.into_parts(); let bytes = body.collect().await?.to_bytes(); let response = Response::from_parts(parts, Full::new(bytes)); + println!("[PROXY] Forwarding response to client"); Ok(response) } From 09fb86223f4a31c29e15a1bc299bccd97c5f6425 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:30:28 +0100 Subject: [PATCH 07/13] feat: Add proxy forwarding support with --proxy flag - Add --proxy flag to specify upstream proxy - Implement proxy forwarding in ProxyForwarder - Fix request body handling with ByteStreamBody - Clean up and simplify proxy implementation - Add basic tests for proxy forwarding --- src/conditions.rs | 2 +- src/forwarder/direct.rs | 11 +- src/forwarder/mod.rs | 62 +++++++- src/forwarder/proxy.rs | 303 +++++++++------------------------------- src/handler.rs | 10 +- src/main.rs | 23 ++- src/pac.rs | 8 +- 7 files changed, 162 insertions(+), 257 deletions(-) diff --git a/src/conditions.rs b/src/conditions.rs index dcb4ddb..626eae6 100644 --- a/src/conditions.rs +++ b/src/conditions.rs @@ -1042,7 +1042,7 @@ mod condition_tests { #[cfg(test)] mod to_js_tests { - use crate::logic::{ToJs}; + use crate::logic::{PacExpression, ToJs}; use crate::proxy_types::ProxyType; use super::*; diff --git a/src/forwarder/direct.rs b/src/forwarder/direct.rs index 453d666..91c40f7 100644 --- a/src/forwarder/direct.rs +++ b/src/forwarder/direct.rs @@ -1,21 +1,22 @@ +use std::error::Error; use bytes::Bytes; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, Request, Response}; +use hyper::{Request, Response}; use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use hyper_util::client::legacy::connect::HttpConnector; -use super::Forwarder; +use super::{ByteStreamBody, Forwarder}; /// DirectForwarder forwards requests directly to their target URL pub struct DirectForwarder { - client: Client, + client: Client, } impl DirectForwarder { pub fn new() -> Self { Self { client: Client::builder(TokioExecutor::new()) - .build::<_, hyper::body::Incoming>(HttpConnector::new()), + .build::<_, ByteStreamBody>(HttpConnector::new()), } } } @@ -28,7 +29,7 @@ impl Default for DirectForwarder { #[async_trait::async_trait] impl Forwarder for DirectForwarder { - async fn forward(&self, req: Request) -> Result>, Box> { + async fn forward(&self, req: Request) -> Result>, Box> { println!("[DIRECT] Forwarding {} {} to {}", req.method(), req.uri(), req.uri().host().unwrap_or("unknown")); println!("[DIRECT] Request headers: {:?}", req.headers()); let response = self.client.request(req).await?; diff --git a/src/forwarder/mod.rs b/src/forwarder/mod.rs index ebad3c0..bbe5c22 100644 --- a/src/forwarder/mod.rs +++ b/src/forwarder/mod.rs @@ -1,18 +1,72 @@ -use std::error::Error; -use http_body_util::Full; use bytes::Bytes; +use futures::Stream; +use futures_util::StreamExt; +use http_body_util::{BodyExt, Full, StreamBody}; +use hyper::body::{Frame, Incoming}; use hyper::Request; -use hyper::body::Incoming; +use std::error::Error; +use std::pin::Pin; /// Trait defining the behavior of a request forwarder #[async_trait::async_trait] pub trait Forwarder: Send + Sync { /// Forward an HTTP request according to the forwarder's strategy - async fn forward(&self, req: Request) -> Result>, Box>; + async fn forward(&self, req: Request) -> Result>, Box>; } mod direct; mod proxy; pub use direct::DirectForwarder; +pub use proxy::ProxyForwarder; + +pub type ByteStreamBody = StreamBody, Box>> + Send + 'static>>>; + + +pub fn to_transformed_body( + incoming: Incoming, +) -> ByteStreamBody { + let stream = incoming.into_data_stream().map(|result| { + result + .map(Frame::data) + .map_err(|enc_err| Box::new(enc_err) as Box) + }); + + StreamBody::new( + Box::pin(stream) + as Pin, Box>> + Send>> + ) +} + + +pub fn convert_request_body( + req: hyper::Request, +) -> hyper::Request +where + A: hyper::body::Body + Send + 'static, + A::Data: Into, + A::Error: Error + Send + Sync + 'static, +{ + let (parts, body) = req.into_parts(); + println!("[PROXY] Request body: {:?}", parts); + let body_stream = convert_body(body); + hyper::Request::from_parts(parts, body_stream) +} + +pub fn convert_body(body: A) -> ByteStreamBody +where + A: hyper::body::Body + Send + 'static, + A::Data: Into, + A::Error: Error + Send + Sync + 'static, +{ + let stream = body.into_data_stream().map(|result| { + result + .map(|data| Frame::data(data.into())) + .map_err(|e| Box::new(e) as Box) + }); + StreamBody::new( + Box::pin(stream) + as Pin, Box>> + Send>> + ) +} diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index 776ef25..df4e9e7 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -1,63 +1,22 @@ //! Proxy forwarding implementation for HTTP requests -//! -//! # Overview -//! This module implements a proxy forwarder that attempts to forward HTTP requests through -//! another proxy server. The current implementation has several limitations and is primarily -//! intended for basic HTTP proxying scenarios. -//! -//! # Limitations -//! - Only supports HTTP (no HTTPS/TLS support) -//! - No proxy authentication support -//! - Uses basic URL rewriting instead of proper proxy protocol implementation -//! - Limited error handling with potential panics -//! - No support for proxy-specific headers (e.g., Proxy-Authorization) -//! -//! # Future Improvements -//! - Implement proper proxy protocol support using a proxy-aware connector -//! - Add TLS support for HTTPS connections -//! - Add proxy authentication support -//! - Improve error handling with custom error types -//! - Add support for proxy-specific headers -//! - Handle CONNECT method for HTTPS tunneling -//! -//! # Example -//! ```no_run -//! use hyper::Uri; -//! let proxy = ProxyForwarder::new("http://proxy.example.com:8080".parse().unwrap()); -//! // proxy can now be used to forward requests -//! ``` -use std::{error::Error, fmt, pin::Pin, sync::Arc, task::{Context, Poll}}; use bytes::Bytes; -use futures_util::future::BoxFuture; -use http::{header::{HeaderMap, HeaderValue}, Method}; +use http::header::HeaderMap; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, upgrade::Upgraded, Request, Response, Uri}; -use hyper_util::{client::legacy::{connect::{HttpConnector, Connect}, Client}, rt::TokioExecutor}; -use tokio::io::{AsyncRead, AsyncWrite}; -use tower_service::Service; +use hyper::{body::Incoming, Request, Response, Uri}; +use hyper_util::{client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor}; +use std::{error::Error, fmt}; -use super::Forwarder; +use super::{ByteStreamBody, Forwarder}; -/// Custom error type for proxy-related errors #[derive(Debug)] pub enum ProxyError { - /// Error during connection to proxy - Connection(String), - /// Error during request processing - Request(String), - /// Error during CONNECT tunnel setup - Tunnel(String), - /// Invalid URI or configuration Configuration(String), } impl fmt::Display for ProxyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ProxyError::Connection(msg) => write!(f, "Proxy connection error: {}", msg), - ProxyError::Request(msg) => write!(f, "Proxy request error: {}", msg), - ProxyError::Tunnel(msg) => write!(f, "Proxy tunnel error: {}", msg), ProxyError::Configuration(msg) => write!(f, "Proxy configuration error: {}", msg), } } @@ -65,230 +24,100 @@ impl fmt::Display for ProxyError { impl Error for ProxyError {} -/// A forwarder that sends requests through another proxy server with support for both -/// HTTP and HTTPS connections. For HTTPS, it establishes a tunnel using the HTTP CONNECT method. -pub struct ProxyForwarder { - /// HTTP client for making requests - client: Client, - /// URI of the proxy server - proxy_uri: Uri, - /// Optional proxy authentication headers - proxy_headers: HeaderMap, -} - -/// Custom connector that handles both direct and proxied connections -#[derive(Clone)] -struct ProxyConnector { - /// Inner HTTP connector - inner: HttpConnector, - /// Proxy server URI - proxy_uri: Uri, - /// Optional proxy authentication headers - proxy_headers: HeaderMap, -} - -impl ProxyConnector { - fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { - let mut inner = HttpConnector::new(); - inner.enforce_http(false); // Allow HTTPS URLs - Self { - inner, - proxy_uri, - proxy_headers, - } - } - - /// Creates a tunnel for HTTPS connections using HTTP CONNECT - async fn establish_tunnel( - client: &Client, - uri: &Uri, - proxy_headers: &HeaderMap, - ) -> Result - where - T: Connect + Clone + Send + Sync + 'static, - { - let host = uri.host().ok_or_else(|| { - ProxyError::Configuration("Missing host in URI".to_string()) - })?; - let port = uri.port_u16().unwrap_or(443); - let addr = format!("{host}:{port}"); - - let mut request = Request::builder() - .method(Method::CONNECT) - .uri(addr) - .body(Full::new(Bytes::new())) - .map_err(|e| ProxyError::Request(e.to_string()))?; - - // Add proxy authentication headers if present - request.headers_mut().extend(proxy_headers.clone()); +#[cfg(test)] +mod tests { + use super::*; + use crate::forwarder::convert_request_body; + use bytes::Bytes; + use http_body_util::combinators::BoxBody; + use http_body_util::{BodyExt, Empty}; + use std::convert::Infallible; + + #[tokio::test] + async fn test_proxy_forwarding() { + let proxy = ProxyForwarder::new( + "http://127.0.0.1:8080".parse().unwrap(), + HeaderMap::new() + ); - let response = client - .request(request) - .await - .map_err(|e| ProxyError::Request(e.to_string()))?; + // Create an empty body that implements Body trait + let empty_body: BoxBody = Empty::::new().boxed(); - if !response.status().is_success() { - return Err(ProxyError::Tunnel(format!( - "Tunnel connection failed: {}", - response.status() - ))); - } + let request: Request> = Request::builder() + .method("GET") + .uri("http://www.google.com") + .body(empty_body) + .unwrap(); - hyper::upgrade::on(response) - .await - .map_err(|e| ProxyError::Tunnel(e.to_string())) + let response = proxy.forward(convert_request_body(request)).await.unwrap(); + assert!(response.status().is_success()); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert!(!body.is_empty()); } } + pub struct ProxyForwarder { - client: Client, + client: Client, proxy_uri: Uri, + proxy_headers: HeaderMap, } -impl Service for ProxyConnector { - type Response = Box; - type Error = Box; - type Future = BoxFuture<'static, Result>; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(Into::into) - } +#[async_trait::async_trait] +impl Forwarder for ProxyForwarder { + async fn forward(&self, req: Request) -> Result>, Box> { + println!("[PROXY] Forwarding {} {} via proxy to {}", + req.method(), + req.uri(), + req.uri().host().unwrap_or("unknown") + ); + + let rewritten_req: Request = self.rewrite_request(req)?; + println!("[PROXY] Rewritten request URI: {}", rewritten_req.uri()); - fn call(&mut self, uri: Uri) -> Self::Future { - let proxy_uri = self.proxy_uri.clone(); - let proxy_headers = self.proxy_headers.clone(); - let mut inner = self.inner.clone(); + let incoming_response: Response = self.client.request(rewritten_req).await?; + println!("[PROXY] Received response: {}", incoming_response.status()); - Box::pin(async move { - let stream = inner.call(proxy_uri).await?; - Ok(Box::new(stream) as Box) - }) + let (parts, body) = incoming_response.into_parts(); + let bytes = body.collect().await?.to_bytes(); + Ok(Response::from_parts(parts, Full::new(bytes))) } } impl ProxyForwarder { - /// Creates a new ProxyForwarder that will forward requests through the specified proxy URI. - /// - /// # Arguments - /// * `proxy_uri` - The URI of the proxy server (e.g., "http://proxy.example.com:8080") - /// * `proxy_headers` - Optional headers for proxy authentication - /// - /// # Returns - /// A new ProxyForwarder instance configured with the specified proxy settings pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { - let connector = ProxyConnector::new(proxy_uri.clone(), proxy_headers.clone()); Self { client: Client::builder(TokioExecutor::new()) - .build::<_, Incoming>(connector), + .build::<_, ByteStreamBody>(HttpConnector::new()), proxy_uri, proxy_headers, } } - /// Rewrites the request URI to be forwarded through the proxy. - /// - /// # Arguments - /// * `req` - The original request to be rewritten - /// - /// # Returns - /// A new request with rewritten URI - /// - /// # Implementation Notes - /// * Currently uses basic URL concatenation which may not handle all cases correctly - /// * Does not preserve the original scheme (http/https) - /// * May panic on malformed URIs - /// * Does not handle requests that already have absolute URIs - /// - /// # Todo - /// * Implement proper URI rewriting according to RFC 7230 - /// * Handle HTTPS requests correctly - /// * Add error handling for malformed URIs - /// * Preserve original request properties - /// Rewrites the request URI for HTTP requests to be forwarded through the proxy. - /// For HTTPS requests, this is not needed as they use a tunnel. - fn rewrite_request(&self, mut req: Request) -> Result, ProxyError> { - if req.uri().scheme_str() == Some("https") { - return Ok(req); // HTTPS requests don't need rewriting, they use CONNECT - } - - // Convert the request URI to an absolute form + fn rewrite_request(&self, mut req: Request) -> Result, ProxyError> { let uri = req.uri(); - let authority = uri.authority().ok_or_else(|| { - ProxyError::Configuration("Missing authority in URI".to_string()) - })?; + + // Get the host from headers or return error + let host = req.headers() + .get("host") + .and_then(|h| h.to_str().ok()) + .or_else(|| req.uri().host()) + .ok_or_else(|| ProxyError::Configuration("Missing host header and unable to extract from URI".to_string()))?; + + // Construct the target URL using the host and the original path let path_and_query = uri.path_and_query() .map(|pq| pq.as_str()) .unwrap_or("/"); - // Construct the absolute URI - let new_uri = format!("http://{}{}", authority, path_and_query) - .parse() + // Construct absolute URI with scheme, host, and path + let target_uri = format!("http://{}{}", host, path_and_query) + .parse::() .map_err(|e| ProxyError::Configuration(e.to_string()))?; - *req.uri_mut() = new_uri; - - // Add proxy headers if any + *req.uri_mut() = target_uri; req.headers_mut().extend(self.proxy_headers.clone()); Ok(req) } } - -#[async_trait::async_trait] -impl Forwarder for ProxyForwarder { - /// Forwards a request through the proxy server. - /// - /// # Arguments - /// * `req` - The HTTP request to forward - /// - /// # Returns - /// * `Ok(Response)` - The response from the target server - /// * `Err(Box)` - Any error that occurred during forwarding - /// - /// # Implementation Notes - /// * Currently logs all requests and responses for debugging - /// * Uses a basic HTTP connector without proxy protocol support - /// * Does not handle HTTPS requests properly - /// * May not handle all error cases appropriately - /// - /// # Todo - /// * Implement proper proxy protocol support - /// * Add HTTPS/TLS support - /// * Improve error handling with specific error types - /// * Add support for proxy authentication - /// * Handle streaming responses more efficiently - async fn forward(&self, req: Request) -> Result>, Box> { - println!("[PROXY] Forwarding {} {} via proxy to {}", - req.method(), - req.uri(), - req.uri().host().unwrap_or("unknown") - ); - println!("[PROXY] Request headers: {:?}", req.headers()); - - let response = if req.uri().scheme_str() == Some("https") { - // For HTTPS, establish a tunnel first - println!("[PROXY] Establishing HTTPS tunnel"); - let upgraded = ProxyConnector::establish_tunnel(&self.client, req.uri(), &self.proxy_headers).await?; - println!("[PROXY] HTTPS tunnel established"); - - // Create a new client with the tunneled connection - let mut req = req; - req.headers_mut().extend(self.proxy_headers.clone()); - self.client.request(req).await? - } else { - // For HTTP, rewrite the request and forward it - let req = self.rewrite_request(req)?; - println!("[PROXY] Rewritten request URI: {}", req.uri()); - self.client.request(req).await? - }; - - println!("[PROXY] Received response: {} from upstream", response.status()); - println!("[PROXY] Response headers: {:?}", response.headers()); - - let (parts, body) = response.into_parts(); - let bytes = body.collect().await?.to_bytes(); - let response = Response::from_parts(parts, Full::new(bytes)); - - println!("[PROXY] Forwarding response to client"); - Ok(response) - } -} diff --git a/src/handler.rs b/src/handler.rs index 1b0a654..aca3de1 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,11 +1,11 @@ +use crate::forwarder::{convert_request_body, Forwarder}; +use crate::tunnel::tunnel; use bytes::Bytes; use http_body_util::Full; use hyper::body::Incoming; use hyper::http::response::Builder; use hyper::{Method, StatusCode}; use std::error::Error; -use crate::tunnel::tunnel; -use crate::forwarder::Forwarder; use std::sync::Arc; #[derive(Debug)] @@ -83,7 +83,9 @@ pub async fn handle_request( Ok(error_response(StatusCode::BAD_REQUEST)?) } } else { - match forwarder.forward(req).await { + + + match forwarder.forward(convert_request_body(req)).await { Ok(response) => Ok(response), Err(e) => { eprintln!("Error forwarding request: {}", e); @@ -92,3 +94,5 @@ pub async fn handle_request( } } } + + diff --git a/src/main.rs b/src/main.rs index dd6c743..e9a5c1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,9 @@ use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; -use forwarder::{Forwarder, DirectForwarder}; +use forwarder::{Forwarder, DirectForwarder, ProxyForwarder}; +use http::header::HeaderMap; +use hyper::Uri; mod handler; mod config; @@ -45,18 +47,33 @@ struct Args { /// HTTPS proxy listen address #[arg(long, default_value = "127.0.0.1:8443")] https_addr: String, + + /// Upstream proxy to forward requests to + #[arg(long)] + proxy: Option, } async fn run_http_server( addr: SocketAddr, + proxy_uri: Option, ) -> Result<(), Box> { let listener = TcpListener::bind(addr).await?; println!("HTTP Listening on http://{}", addr); + let forwarder: Arc = if let Some(proxy_uri) = proxy_uri { + println!("Using upstream proxy: {}", proxy_uri); + Arc::new(ProxyForwarder::new( + proxy_uri.parse::().expect("Invalid proxy URI"), + HeaderMap::new() + )) + } else { + Arc::new(DirectForwarder::new()) + }; + loop { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); - let forwarder = Arc::new(DirectForwarder::new()) as Arc; + let forwarder = forwarder.clone(); tokio::spawn(async move { if let Err(err) = http1::Builder::new() @@ -146,7 +163,7 @@ async fn main() -> Result<(), Box> { println!(" Using key: {}", args.key); tokio::select! { - result = run_http_server(http_addr) => { + result = run_http_server(http_addr, args.proxy) => { if let Err(e) = result { eprintln!("HTTP server error: {}", e); } diff --git a/src/pac.rs b/src/pac.rs index 210b9e2..0508fc2 100644 --- a/src/pac.rs +++ b/src/pac.rs @@ -1,5 +1,5 @@ -use crate::logic::PacExpression; use crate::conditions::{parse_condition, PacCondition}; +use crate::logic::PacExpression; use crate::proxy_types::ProxyType; use boa_engine::ast::expression::literal::Literal; use boa_engine::ast::statement::{If, Return}; @@ -179,12 +179,12 @@ mod to_js_tests { #[cfg(test)] mod pac_files_test { - use boa_engine::{js_string, Context, JsString, JsValue, NativeFunction}; - use boa_engine::object::ObjectInitializer; - use boa_engine::property::Attribute; use super::*; use crate::conditions::{PacCondition, Protocol}; use crate::logic::ToJs; + use boa_engine::ast::scope::Scope; + use boa_engine::{Context, JsString, JsValue, NativeFunction}; + use boa_parser::{Parser, Source}; #[test] fn test_company_pac_file() { From ea25cddc45d3aab0996f930d476c7212a2907109 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:33:53 +0100 Subject: [PATCH 08/13] feat: Add HTTPS support to proxy forwarding - Add hyper-tls dependency for HTTPS support - Configure HttpsConnector for TLS connections - Preserve original scheme when rewriting requests - Clean up imports and dependencies --- Cargo.toml | 2 +- src/forwarder/proxy.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 009d6b6..952b634 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,9 @@ rustls-pemfile = { workspace = true } clap = { workspace = true } boa_engine = { workspace = true } boa_parser = { workspace = true } +hyper-tls = { workspace = true } tls-parser = { workspace = true } async-trait = { workspace = true } tokio-util = { workspace = true } hyper-util = { workspace = true } -hyper-tls = { workspace = true } pin-project-lite = { workspace = true } diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index df4e9e7..e7c3107 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -5,6 +5,7 @@ use http::header::HeaderMap; use http_body_util::{BodyExt, Full}; use hyper::{body::Incoming, Request, Response, Uri}; use hyper_util::{client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor}; +use hyper_tls::HttpsConnector; use std::{error::Error, fmt}; use super::{ByteStreamBody, Forwarder}; @@ -58,7 +59,7 @@ mod tests { } pub struct ProxyForwarder { - client: Client, + client: Client, ByteStreamBody>, proxy_uri: Uri, proxy_headers: HeaderMap, } @@ -87,9 +88,10 @@ impl Forwarder for ProxyForwarder { impl ProxyForwarder { pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { + let https = HttpsConnector::new(); Self { client: Client::builder(TokioExecutor::new()) - .build::<_, ByteStreamBody>(HttpConnector::new()), + .build::<_, ByteStreamBody>(https), proxy_uri, proxy_headers, } @@ -110,8 +112,11 @@ impl ProxyForwarder { .map(|pq| pq.as_str()) .unwrap_or("/"); + // Preserve the original scheme or default to http + let scheme = uri.scheme_str().unwrap_or("http"); + // Construct absolute URI with scheme, host, and path - let target_uri = format!("http://{}{}", host, path_and_query) + let target_uri = format!("{scheme}://{}{}", host, path_and_query) .parse::() .map_err(|e| ProxyError::Configuration(e.to_string()))?; From 261e90512ca0cc6e8e0ad840dd66756d796f9718 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:36:56 +0100 Subject: [PATCH 09/13] feat: Add HTTPS support between proxies - Add --insecure flag to skip TLS verification for self-signed certs - Update ProxyForwarder to use HttpsConnector for proxy connections - Add idle timeout for proxy connections - Pass insecure flag through run_http_server --- src/forwarder/proxy.rs | 7 +++++-- src/main.rs | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index e7c3107..73ba2bf 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -87,10 +87,13 @@ impl Forwarder for ProxyForwarder { } impl ProxyForwarder { - pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap) -> Self { - let https = HttpsConnector::new(); + pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap, insecure: bool) -> Self { + let mut http = HttpConnector::new(); + http.enforce_http(false); + let https = HttpsConnector::new_with_connector(http); Self { client: Client::builder(TokioExecutor::new()) + .pool_idle_timeout(std::time::Duration::from_secs(30)) .build::<_, ByteStreamBody>(https), proxy_uri, proxy_headers, diff --git a/src/main.rs b/src/main.rs index e9a5c1d..3133580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,11 +51,16 @@ struct Args { /// Upstream proxy to forward requests to #[arg(long)] proxy: Option, + + /// Skip TLS verification when connecting to upstream proxy + #[arg(long)] + insecure: bool, } async fn run_http_server( addr: SocketAddr, proxy_uri: Option, + insecure: bool, ) -> Result<(), Box> { let listener = TcpListener::bind(addr).await?; println!("HTTP Listening on http://{}", addr); @@ -64,7 +69,8 @@ async fn run_http_server( println!("Using upstream proxy: {}", proxy_uri); Arc::new(ProxyForwarder::new( proxy_uri.parse::().expect("Invalid proxy URI"), - HeaderMap::new() + HeaderMap::new(), + insecure )) } else { Arc::new(DirectForwarder::new()) @@ -163,7 +169,7 @@ async fn main() -> Result<(), Box> { println!(" Using key: {}", args.key); tokio::select! { - result = run_http_server(http_addr, args.proxy) => { + result = run_http_server(http_addr, args.proxy, args.insecure) => { if let Err(e) = result { eprintln!("HTTP server error: {}", e); } From aa1e2ceb7952afcfa9422082906fc2ab3e3484c3 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:38:41 +0100 Subject: [PATCH 10/13] refactor: Rename proxy arguments for clarity - Rename proxy to upstream_proxy - Rename insecure to upstream_insecure --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3133580..20519fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,11 +50,11 @@ struct Args { /// Upstream proxy to forward requests to #[arg(long)] - proxy: Option, + upstream_proxy: Option, /// Skip TLS verification when connecting to upstream proxy #[arg(long)] - insecure: bool, + upstream_insecure: bool, } async fn run_http_server( @@ -169,7 +169,7 @@ async fn main() -> Result<(), Box> { println!(" Using key: {}", args.key); tokio::select! { - result = run_http_server(http_addr, args.proxy, args.insecure) => { + result = run_http_server(http_addr, args.upstream_proxy, args.upstream_insecure) => { if let Err(e) = result { eprintln!("HTTP server error: {}", e); } From fe973963a7754930602430da389b3988199a2438 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:44:15 +0100 Subject: [PATCH 11/13] fix: Properly handle request forwarding in ProxyForwarder - Handle CONNECT requests separately - Keep original absolute URIs - Fix URI handling for non-absolute URIs - Clean up imports --- src/forwarder/proxy.rs | 88 ++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index 73ba2bf..1942a2d 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use http::header::HeaderMap; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, Request, Response, Uri}; +use hyper::{Request, Response, Uri}; use hyper_util::{client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor}; use hyper_tls::HttpsConnector; use std::{error::Error, fmt}; @@ -67,17 +67,63 @@ pub struct ProxyForwarder { #[async_trait::async_trait] impl Forwarder for ProxyForwarder { - async fn forward(&self, req: Request) -> Result>, Box> { - println!("[PROXY] Forwarding {} {} via proxy to {}", + async fn forward(&self, mut req: Request) -> Result>, Box> { + use http::header::HOST; + + println!("[PROXY] Forwarding {} {} via proxy {}", req.method(), req.uri(), - req.uri().host().unwrap_or("unknown") + self.proxy_uri ); - let rewritten_req: Request = self.rewrite_request(req)?; - println!("[PROXY] Rewritten request URI: {}", rewritten_req.uri()); + // For CONNECT requests, forward them as-is to the upstream proxy + if req.method() == hyper::Method::CONNECT { + req.headers_mut().extend(self.proxy_headers.clone()); + let incoming_response = self.client.request(req).await?; + let (parts, body) = incoming_response.into_parts(); + let bytes = body.collect().await?.to_bytes(); + return Ok(Response::from_parts(parts, Full::new(bytes))); + } + + // For other requests, preserve the original URI but send through proxy + let original_uri = req.uri().clone(); + + // Ensure host header is set + if !req.headers().contains_key(HOST) { + if let Some(host) = original_uri.host() { + let mut host_value = host.to_string(); + if let Some(port) = original_uri.port_u16() { + host_value.push(':'); + host_value.push_str(&port.to_string()); + } + req.headers_mut().insert(HOST, host_value.parse().unwrap()); + } + } + + // Add proxy headers + req.headers_mut().extend(self.proxy_headers.clone()); + + // Make sure we have an absolute URI + if original_uri.scheme().is_none() || original_uri.authority().is_none() { + // If URI is not absolute, make it absolute using the Host header + let host = req.headers().get(HOST) + .and_then(|h| h.to_str().ok()) + .ok_or_else(|| ProxyError::Configuration("Missing Host header".to_string()))?; + + let scheme = original_uri.scheme_str().unwrap_or("http"); + let path_and_query = original_uri.path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"); + + let absolute_uri = format!("{scheme}://{}{}", host, path_and_query) + .parse::() + .map_err(|e| ProxyError::Configuration(e.to_string()))?; - let incoming_response: Response = self.client.request(rewritten_req).await?; + *req.uri_mut() = absolute_uri; + } + + println!("[PROXY] Request URI: {}", req.uri()); + let incoming_response = self.client.request(req).await?; println!("[PROXY] Received response: {}", incoming_response.status()); let (parts, body) = incoming_response.into_parts(); @@ -100,32 +146,8 @@ impl ProxyForwarder { } } - fn rewrite_request(&self, mut req: Request) -> Result, ProxyError> { - let uri = req.uri(); - - // Get the host from headers or return error - let host = req.headers() - .get("host") - .and_then(|h| h.to_str().ok()) - .or_else(|| req.uri().host()) - .ok_or_else(|| ProxyError::Configuration("Missing host header and unable to extract from URI".to_string()))?; - - // Construct the target URL using the host and the original path - let path_and_query = uri.path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - - // Preserve the original scheme or default to http - let scheme = uri.scheme_str().unwrap_or("http"); - - // Construct absolute URI with scheme, host, and path - let target_uri = format!("{scheme}://{}{}", host, path_and_query) - .parse::() - .map_err(|e| ProxyError::Configuration(e.to_string()))?; - - *req.uri_mut() = target_uri; - req.headers_mut().extend(self.proxy_headers.clone()); - + // No longer needed as the logic is now in forward() + fn rewrite_request(&self, req: Request) -> Result, ProxyError> { Ok(req) } } From 9ead4cbb55fe6802ccc9f8365e0d92420e542724 Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:50:17 +0100 Subject: [PATCH 12/13] refactor: Use hyper-http-proxy for proxy forwarding - Add hyper-http-proxy dependency - Replace custom proxy implementation with ProxyConnector - Simplify request handling using dedicated proxy toolkit - Clean up imports and unused code --- Cargo.lock | 198 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/forwarder/proxy.rs | 73 ++++----------- 3 files changed, 214 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65c96a5..797410c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bindgen" version = "0.69.5" @@ -186,6 +192,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "boa_ast" version = "0.20.0" @@ -465,18 +480,47 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -500,6 +544,16 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -702,6 +756,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -773,6 +837,30 @@ dependencies = [ "foldhash", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" @@ -855,6 +943,44 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-http-proxy" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d06dbdfbacf34d996c6fb540a71a684a7aae9056c71951163af8a8a4c07b9a4" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls-native-certs 0.7.3", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1147,6 +1273,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1185,7 +1317,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1360,6 +1492,7 @@ dependencies = [ "http", "http-body-util", "hyper", + "hyper-http-proxy", "hyper-tls", "hyper-util", "pin-project-lite", @@ -1694,6 +1827,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1761,7 +1919,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -1809,6 +1980,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2139,6 +2321,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.16" @@ -2175,6 +2363,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 952b634..0aa67fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,3 +55,4 @@ async-trait = { workspace = true } tokio-util = { workspace = true } hyper-util = { workspace = true } pin-project-lite = { workspace = true } +hyper-http-proxy = "1.0.0" diff --git a/src/forwarder/proxy.rs b/src/forwarder/proxy.rs index 1942a2d..f72083b 100644 --- a/src/forwarder/proxy.rs +++ b/src/forwarder/proxy.rs @@ -4,8 +4,8 @@ use bytes::Bytes; use http::header::HeaderMap; use http_body_util::{BodyExt, Full}; use hyper::{Request, Response, Uri}; +use hyper_http_proxy::{Proxy, ProxyConnector, Intercept}; use hyper_util::{client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor}; -use hyper_tls::HttpsConnector; use std::{error::Error, fmt}; use super::{ByteStreamBody, Forwarder}; @@ -59,8 +59,8 @@ mod tests { } pub struct ProxyForwarder { - client: Client, ByteStreamBody>, - proxy_uri: Uri, + client: Client, ByteStreamBody>, + proxy: Proxy, proxy_headers: HeaderMap, } @@ -68,60 +68,19 @@ pub struct ProxyForwarder { #[async_trait::async_trait] impl Forwarder for ProxyForwarder { async fn forward(&self, mut req: Request) -> Result>, Box> { - use http::header::HOST; - - println!("[PROXY] Forwarding {} {} via proxy {}", + println!("[PROXY] Forwarding {} {} via proxy", req.method(), - req.uri(), - self.proxy_uri + req.uri() ); - // For CONNECT requests, forward them as-is to the upstream proxy - if req.method() == hyper::Method::CONNECT { - req.headers_mut().extend(self.proxy_headers.clone()); - let incoming_response = self.client.request(req).await?; - let (parts, body) = incoming_response.into_parts(); - let bytes = body.collect().await?.to_bytes(); - return Ok(Response::from_parts(parts, Full::new(bytes))); + // Add proxy-specific headers if needed for HTTP requests + if req.method() != hyper::Method::CONNECT { + req.headers_mut().extend(self.proxy.headers().clone()); } - // For other requests, preserve the original URI but send through proxy - let original_uri = req.uri().clone(); - - // Ensure host header is set - if !req.headers().contains_key(HOST) { - if let Some(host) = original_uri.host() { - let mut host_value = host.to_string(); - if let Some(port) = original_uri.port_u16() { - host_value.push(':'); - host_value.push_str(&port.to_string()); - } - req.headers_mut().insert(HOST, host_value.parse().unwrap()); - } - } - - // Add proxy headers + // Add any additional headers req.headers_mut().extend(self.proxy_headers.clone()); - // Make sure we have an absolute URI - if original_uri.scheme().is_none() || original_uri.authority().is_none() { - // If URI is not absolute, make it absolute using the Host header - let host = req.headers().get(HOST) - .and_then(|h| h.to_str().ok()) - .ok_or_else(|| ProxyError::Configuration("Missing Host header".to_string()))?; - - let scheme = original_uri.scheme_str().unwrap_or("http"); - let path_and_query = original_uri.path_and_query() - .map(|p| p.as_str()) - .unwrap_or("/"); - - let absolute_uri = format!("{scheme}://{}{}", host, path_and_query) - .parse::() - .map_err(|e| ProxyError::Configuration(e.to_string()))?; - - *req.uri_mut() = absolute_uri; - } - println!("[PROXY] Request URI: {}", req.uri()); let incoming_response = self.client.request(req).await?; println!("[PROXY] Received response: {}", incoming_response.status()); @@ -133,15 +92,17 @@ impl Forwarder for ProxyForwarder { } impl ProxyForwarder { - pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap, insecure: bool) -> Self { - let mut http = HttpConnector::new(); - http.enforce_http(false); - let https = HttpsConnector::new_with_connector(http); + pub fn new(proxy_uri: Uri, proxy_headers: HeaderMap, _insecure: bool) -> Self { + let proxy = Proxy::new(Intercept::All, proxy_uri.clone()); + let connector = HttpConnector::new(); + let proxy_connector = ProxyConnector::from_proxy(connector, proxy.clone()) + .expect("Failed to create proxy connector"); + Self { client: Client::builder(TokioExecutor::new()) .pool_idle_timeout(std::time::Duration::from_secs(30)) - .build::<_, ByteStreamBody>(https), - proxy_uri, + .build::<_, ByteStreamBody>(proxy_connector), + proxy, proxy_headers, } } From 8e601cd15007672f9d13866f2857966f27ed009d Mon Sep 17 00:00:00 2001 From: Laurent Valdes Date: Sat, 1 Feb 2025 02:53:21 +0100 Subject: [PATCH 13/13] feat: Configure ProxyForwarder in main.rs - Add proxy configuration to HTTPS server - Pass proxy settings to both HTTP and HTTPS servers - Fix main function to handle proxy configuration correctly --- src/main.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 20519fd..9f9615d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,9 +67,11 @@ async fn run_http_server( let forwarder: Arc = if let Some(proxy_uri) = proxy_uri { println!("Using upstream proxy: {}", proxy_uri); + let mut headers = HeaderMap::new(); + // Add any custom headers for the upstream proxy if needed Arc::new(ProxyForwarder::new( proxy_uri.parse::().expect("Invalid proxy URI"), - HeaderMap::new(), + headers, insecure )) } else { @@ -99,15 +101,30 @@ async fn run_http_server( async fn run_https_server( addr: SocketAddr, tls_config: ServerConfig, + proxy_uri: Option, + insecure: bool, ) -> Result<(), Box> { let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config)); let listener = TcpListener::bind(addr).await?; println!("HTTPS Listening on https://{}", addr); + let forwarder: Arc = if let Some(proxy_uri) = proxy_uri { + println!("Using upstream proxy: {}", proxy_uri); + let mut headers = HeaderMap::new(); + // Add any custom headers for the upstream proxy if needed + Arc::new(ProxyForwarder::new( + proxy_uri.parse::().expect("Invalid proxy URI"), + headers, + insecure + )) + } else { + Arc::new(DirectForwarder::new()) + }; + while let Ok((stream, addr)) = listener.accept().await { println!("Accepted connection from {}", addr); let tls_acceptor = tls_acceptor.clone(); - let forwarder = Arc::new(DirectForwarder::new()) as Arc; + let forwarder = forwarder.clone(); tokio::spawn(async move { match tls_acceptor.accept(stream).await { @@ -169,12 +186,12 @@ async fn main() -> Result<(), Box> { println!(" Using key: {}", args.key); tokio::select! { - result = run_http_server(http_addr, args.upstream_proxy, args.upstream_insecure) => { + result = run_http_server(http_addr, args.upstream_proxy.clone(), args.upstream_insecure) => { if let Err(e) = result { eprintln!("HTTP server error: {}", e); } } - result = run_https_server(https_addr, tls_config) => { + result = run_https_server(https_addr, tls_config, args.upstream_proxy, args.upstream_insecure) => { if let Err(e) = result { eprintln!("HTTPS server error: {}", e); }