Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix: prevent infinite proxy loop with nonce-based detection by ammar-agent · Pull Request #85 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 116 additions & 79 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,11 @@ pub const HTTPJAIL_HEADER: &str = "HTTPJAIL";
pub const HTTPJAIL_HEADER_VALUE: &str = "true";
pub const BLOCKED_MESSAGE: &str = "Request blocked by httpjail";

/// Header added to outgoing requests to detect loops (Issue #84)
/// Contains comma-separated nonces of all httpjail instances in the proxy chain.
/// If we see our own nonce in an incoming request, we're in a loop.
pub const HTTPJAIL_LOOP_DETECTION_HEADER: &str = "Httpjail-Loop-Prevention";

/// Create a raw HTTP/1.1 403 Forbidden response for CONNECT tunnels
pub fn create_connect_403_response() -> &'static [u8] {
b"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 27\r\n\r\nRequest blocked by httpjail"
Expand DownExpand Up@@ -166,6 +171,7 @@ static HTTPS_CLIENT: OnceLock<
pub fn prepare_upstream_request(
req: Request<Incoming>,
target_uri: Uri,
loop_nonce: &str,
) -> Request<BoxBody<Bytes, HyperError>> {
let (mut parts, incoming_body) = req.into_parts();

Expand All@@ -178,6 +184,16 @@ pub fn prepare_upstream_request(
parts.headers.remove("proxy-authorization");
parts.headers.remove("proxy-authenticate");

// SECURITY: Add our nonce to the loop detection header (Issue #84)
// HTTP natively supports multiple values for the same header name (via append).
// This allows chaining multiple httpjail instances while still detecting self-loops.
// Each instance appends its nonce; if we see our own nonce in an incoming request, it's a loop.
parts.headers.append(
HTTPJAIL_LOOP_DETECTION_HEADER,
hyper::header::HeaderValue::from_str(loop_nonce)
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("invalid")),
);

// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
// This prevents attacks where an attacker sends a request to one domain but sets
// the Host header to another domain, potentially bypassing security controls in
Expand DownExpand Up@@ -364,11 +380,19 @@ async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
TcpListener::bind(addr).await.map_err(Into::into)
}

/// Context passed to all proxy handlers - reduces argument duplication
#[derive(Clone)]
pub struct ProxyContext {
pub rule_engine: Arc<RuleEngine>,
pub cert_manager: Arc<CertificateManager>,
/// Unique nonce for this proxy instance, used for loop detection (Issue #84)
pub loop_nonce: Arc<String>,
}

pub struct ProxyServer {
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
}

impl ProxyServer {
Expand All@@ -383,11 +407,23 @@ impl ProxyServer {
let ca_cert_der = cert_manager.get_ca_cert_der();
init_client_with_ca(ca_cert_der);

// Generate a unique nonce for loop detection (Issue #84)
// Use 16 random hex characters for a reasonably short but collision-resistant ID
let loop_nonce = {
let random_u64: u64 = rand::random();
format!("{:x}", random_u64)
};

let context = ProxyContext {
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
loop_nonce: Arc::new(loop_nonce),
};

ProxyServer {
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
context,
}
}

Expand All@@ -403,35 +439,13 @@ impl ProxyServer {
let http_port = http_listener.local_addr()?.port();
info!("Starting HTTP proxy on port {}", http_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTP proxy task
tokio::spawn(async move {
loop {
match http_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTP connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_http_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTP connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTP connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
http_listener,
self.context.clone(),
"HTTP",
handle_http_connection,
);

// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
Expand All@@ -444,61 +458,64 @@ impl ProxyServer {
let https_port = https_listener.local_addr()?.port();
info!("Starting HTTPS proxy on port {}", https_port);

let rule_engine = Arc::clone(&self.rule_engine);
let cert_manager = Arc::clone(&self.cert_manager);

// Start HTTPS proxy task
tokio::spawn(async move {
loop {
match https_listener.accept().await {
Ok((stream, addr)) => {
debug!("New HTTPS connection from {}", addr);
let rule_engine = Arc::clone(&rule_engine);
let cert_manager = Arc::clone(&cert_manager);

tokio::spawn(async move {
if let Err(e) =
handle_https_connection(stream, rule_engine, cert_manager, addr)
.await
{
error!("Error handling HTTPS connection: {:?}", e);
}
});
}
Err(e) => {
error!("Failed to accept HTTPS connection: {}", e);
}
}
}
});

// IPv6-specific listener not required; IPv4 listener suffices for jail routing
spawn_listener_task(
https_listener,
self.context.clone(),
"HTTPS",
handle_https_connection,
);

Ok((http_port, https_port))
}

/// Get the CA certificate for client trust
#[allow(dead_code)]
pub fn get_ca_cert_pem(&self) -> String {
self.cert_manager.get_ca_cert_pem()
self.context.cert_manager.get_ca_cert_pem()
}
}

/// Generic listener task spawner to avoid code duplication between HTTP and HTTPS
fn spawn_listener_task<F, Fut>(
listener: TcpListener,
context: ProxyContext,
protocol: &'static str,
handler: F,
) where
F: Fn(TcpStream, ProxyContext, SocketAddr) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
debug!("New {} connection from {}", protocol, addr);
let context = context.clone();
let handler = Arc::clone(&handler);

tokio::spawn(async move {
if let Err(e) = handler(stream, context, addr).await {
error!("Error handling {} connection: {:?}", protocol, e);
}
});
}
Err(e) => {
error!("Failed to accept {} connection: {}", protocol, e);
}
}
}
});
}

async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
let io = TokioIo::new(stream);
let service = service_fn(move |req| {
handle_http_request(
req,
Arc::clone(&rule_engine),
Arc::clone(&cert_manager),
remote_addr,
)
});
let service = service_fn(move |req| handle_http_request(req, context.clone(), remote_addr));

http1::Builder::new()
.preserve_header_case(true)
Expand All@@ -511,24 +528,41 @@ async fn handle_http_connection(

async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<()> {
// Delegate to the TLS-specific module
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
crate::proxy_tls::handle_https_connection(stream, context, remote_addr).await
}

pub async fn handle_http_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
context: ProxyContext,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let headers = req.headers().clone();

// SECURITY: Check for loop detection header (Issue #84)
// HTTP supports multiple values for the same header name.
// Each httpjail instance adds its nonce; if we see our own, it's a loop.
let our_nonce = context.loop_nonce.as_str();
for value in headers.get_all(HTTPJAIL_LOOP_DETECTION_HEADER).iter() {
if let Ok(nonce) = value.to_str() {
if nonce == our_nonce {
debug!(
"Loop detected: our nonce '{}' found in request to {}",
nonce, uri
);
return create_forbidden_response(Some(
"Loop detected: request already processed by this httpjail instance"
.to_string(),
));
}
}
}

// Check if the URI already contains the full URL (proxy request)
let full_url = if uri.scheme().is_some() && uri.authority().is_some() {
// This is a proxy request with absolute URL (e.g., GET http://example.com/ HTTP/1.1)
Expand All@@ -551,7 +585,8 @@ pub async fn handle_http_request(

// Evaluate rules with method and requester IP
let requester_ip = remote_addr.ip().to_string();
let evaluation = rule_engine
let evaluation = context
.rule_engine
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
.await;
match evaluation.action {
Expand All@@ -560,7 +595,8 @@ pub async fn handle_http_request(
"Request allowed: {} (max_tx_bytes: {:?})",
full_url, evaluation.max_tx_bytes
);
match proxy_request(req, &full_url, evaluation.max_tx_bytes).await {
match proxy_request(req, &full_url, evaluation.max_tx_bytes, &context.loop_nonce).await
{
Ok(resp) => Ok(resp),
Err(e) => {
error!("Proxy error: {}", e);
Expand All@@ -579,12 +615,13 @@ async fn proxy_request(
req: Request<Incoming>,
full_url: &str,
max_tx_bytes: Option<u64>,
loop_nonce: &str,
) -> Result<Response<BoxBody<Bytes, HyperError>>> {
// Parse the target URL
let target_uri = full_url.parse::<Uri>()?;

// Prepare request for upstream
let prepared_req = prepare_upstream_request(req, target_uri.clone());
let prepared_req = prepare_upstream_request(req, target_uri.clone(), loop_nonce);

// Apply byte limit to outgoing request if specified, converting to BoxBody
let new_req = if let Some(max_bytes) = max_tx_bytes {
Expand Down
Loading