From e860aeb1562b3c75c85f3a5a63030b76304ef40c Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 10 Sep 2025 22:31:12 -0500 Subject: [PATCH 1/6] feat: add HTTPJAIL_REQUESTER_IP environment variable for scripts - Pass requester IP address through all proxy handlers - Add HTTPJAIL_REQUESTER_IP environment variable to script execution - Update RuleEngineTrait to accept requester IP as required parameter - Add test to verify requester IP is correctly passed to scripts - Update README documentation with new environment variable This allows scripts to make decisions based on the client's IP address, enabling IP-based filtering, rate limiting, or custom logging scenarios. --- README.md | 1 + src/proxy.rs | 30 +++++++++++++---- src/proxy_tls.rs | 61 +++++++++++++++++++++------------ src/rules.rs | 26 +++++++++++--- src/rules/pattern.rs | 2 +- src/rules/script.rs | 16 ++++++--- tests/script_integration.rs | 67 +++++++++++++++++++++++++++++++------ 7 files changed, 153 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 8563ea5..18bef7e 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ httpjail --script '[ "$HTTPJAIL_HOST" = "github.com" ] && exit 0 || exit 1' -- g - `HTTPJAIL_HOST` - Hostname from the URL - `HTTPJAIL_SCHEME` - URL scheme (http or https) - `HTTPJAIL_PATH` - Path component of the URL +- `HTTPJAIL_REQUESTER_IP` - IP address of the client making the request **Script requirements:** diff --git a/src/proxy.rs b/src/proxy.rs index 06cc253..23f0479 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -246,7 +246,8 @@ impl ProxyServer { tokio::spawn(async move { if let Err(e) = - handle_http_connection(stream, rule_engine, cert_manager).await + handle_http_connection(stream, rule_engine, cert_manager, addr) + .await { error!("Error handling HTTP connection: {:?}", e); } @@ -290,7 +291,8 @@ impl ProxyServer { tokio::spawn(async move { if let Err(e) = - handle_https_connection(stream, rule_engine, cert_manager).await + handle_https_connection(stream, rule_engine, cert_manager, addr) + .await { error!("Error handling HTTPS connection: {:?}", e); } @@ -319,10 +321,16 @@ async fn handle_http_connection( stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + 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)) + handle_http_request( + req, + Arc::clone(&rule_engine), + Arc::clone(&cert_manager), + remote_addr, + ) }); http1::Builder::new() @@ -338,15 +346,17 @@ async fn handle_https_connection( stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + remote_addr: SocketAddr, ) -> Result<()> { // Delegate to the TLS-specific module - crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager).await + crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await } pub async fn handle_http_request( req: Request, rule_engine: Arc, _cert_manager: Arc, + remote_addr: SocketAddr, ) -> Result>, std::convert::Infallible> { let method = req.method().clone(); let uri = req.uri().clone(); @@ -367,10 +377,16 @@ pub async fn handle_http_request( format!("http://{}{}", host, path) }; - debug!("Proxying HTTP request: {} {}", method, full_url); + debug!( + "Proxying HTTP request: {} {} from {}", + method, full_url, remote_addr + ); - // Evaluate rules with method - let evaluation = rule_engine.evaluate_with_context(method, &full_url).await; + // Evaluate rules with method and requester IP + let requester_ip = remote_addr.ip().to_string(); + let evaluation = rule_engine + .evaluate_with_context_and_ip(method, &full_url, &requester_ip) + .await; match evaluation.action { Action::Allow => { debug!("Request allowed: {}", full_url); diff --git a/src/proxy_tls.rs b/src/proxy_tls.rs index 45ab1ac..0e92296 100644 --- a/src/proxy_tls.rs +++ b/src/proxy_tls.rs @@ -37,8 +37,9 @@ pub async fn handle_https_connection( stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + remote_addr: std::net::SocketAddr, ) -> Result<()> { - debug!("Handling new HTTPS connection"); + debug!("Handling new HTTPS connection from {}", remote_addr); // Peek at the first few bytes to determine if this is HTTP or TLS let mut peek_buf = [0; 6]; @@ -64,18 +65,18 @@ pub async fn handle_https_connection( if peek_buf[0] == 0x16 && n > 1 && (peek_buf[1] == 0x03 || peek_buf[1] == 0x02) { // This is a TLS ClientHello - we're in transparent proxy mode debug!("Detected TLS ClientHello - transparent proxy mode"); - handle_transparent_tls(stream, rule_engine, cert_manager).await + handle_transparent_tls(stream, rule_engine, cert_manager, remote_addr).await } else if peek_buf[0] >= 0x41 && peek_buf[0] <= 0x5A { // This looks like HTTP (starts with uppercase ASCII letter) // Check if it's a CONNECT request let request_str = String::from_utf8_lossy(&peek_buf); if request_str.starts_with("CONNEC") { debug!("Detected CONNECT request - explicit proxy mode"); - handle_connect_tunnel(stream, rule_engine, cert_manager).await + handle_connect_tunnel(stream, rule_engine, cert_manager, remote_addr).await } else { // Regular HTTP on HTTPS port debug!("Detected plain HTTP on HTTPS port"); - handle_plain_http(stream, rule_engine, cert_manager).await + handle_plain_http(stream, rule_engine, cert_manager, remote_addr).await } } else { warn!( @@ -159,6 +160,7 @@ async fn handle_transparent_tls( mut stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + remote_addr: std::net::SocketAddr, ) -> Result<()> { debug!("Handling transparent TLS connection"); @@ -212,7 +214,7 @@ async fn handle_transparent_tls( let io = TokioIo::new(tls_stream); let service = service_fn(move |req| { let host_clone = hostname.clone(); - handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone) + handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone, remote_addr) }); debug!("Starting HTTP/1.1 server for decrypted requests"); @@ -230,6 +232,7 @@ async fn handle_connect_tunnel( stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + remote_addr: std::net::SocketAddr, ) -> Result<()> { debug!("Handling CONNECT tunnel"); @@ -305,8 +308,9 @@ async fn handle_connect_tunnel( // Check if this host is allowed let full_url = format!("https://{}", target); + let requester_ip = remote_addr.ip().to_string(); let evaluation = rule_engine - .evaluate_with_context(Method::GET, &full_url) + .evaluate_with_context_and_ip(Method::GET, &full_url, &requester_ip) .await; match evaluation.action { Action::Allow => { @@ -337,7 +341,7 @@ async fn handle_connect_tunnel( debug!("Sent 200 Connection Established, starting TLS handshake"); // Now perform TLS handshake with the client - perform_tls_interception(stream, rule_engine, cert_manager, host).await + perform_tls_interception(stream, rule_engine, cert_manager, host, remote_addr).await } Action::Deny => { warn!("CONNECT denied to: {}", host); @@ -372,6 +376,7 @@ async fn perform_tls_interception( rule_engine: Arc, cert_manager: Arc, host: &str, + remote_addr: std::net::SocketAddr, ) -> Result<()> { // Get certificate for the host let (cert_chain, key) = cert_manager @@ -405,9 +410,10 @@ async fn perform_tls_interception( // Now handle the decrypted HTTPS requests let io = TokioIo::new(tls_stream); let host_string = host.to_string(); + let remote_addr_copy = remote_addr; // Copy for the closure let service = service_fn(move |req| { let host_clone = host_string.clone(); - handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone) + handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone, remote_addr_copy) }); debug!("Starting HTTP/1.1 server for decrypted requests"); @@ -425,12 +431,18 @@ async fn handle_plain_http( stream: TcpStream, rule_engine: Arc, cert_manager: Arc, + remote_addr: std::net::SocketAddr, ) -> Result<()> { debug!("Handling plain HTTP on HTTPS port"); let io = TokioIo::new(stream); let service = service_fn(move |req| { - crate::proxy::handle_http_request(req, Arc::clone(&rule_engine), Arc::clone(&cert_manager)) + crate::proxy::handle_http_request( + req, + Arc::clone(&rule_engine), + Arc::clone(&cert_manager), + remote_addr, + ) }); http1::Builder::new() @@ -447,6 +459,7 @@ async fn handle_decrypted_https_request( req: Request, rule_engine: Arc, host: String, + remote_addr: std::net::SocketAddr, ) -> Result>, std::convert::Infallible> { let method = req.method().clone(); let uri = req.uri().clone(); @@ -455,11 +468,15 @@ async fn handle_decrypted_https_request( let path = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/"); let full_url = format!("https://{}{}", host, path); - debug!("Proxying HTTPS request: {} {}", method, full_url); + debug!( + "Proxying HTTPS request: {} {} from {}", + method, full_url, remote_addr + ); - // Evaluate rules with method + // Evaluate rules with method and requester IP + let requester_ip = remote_addr.ip().to_string(); let evaluation = rule_engine - .evaluate_with_context(method.clone(), &full_url) + .evaluate_with_context_and_ip(method.clone(), &full_url, &requester_ip) .await; match evaluation.action { Action::Allow => { @@ -674,8 +691,8 @@ mod tests { // Spawn proxy handler tokio::spawn(async move { - let (stream, _) = listener.accept().await.unwrap(); - let _ = handle_connect_tunnel(stream, rule_engine, cert_manager).await; + let (stream, addr) = listener.accept().await.unwrap(); + let _ = handle_connect_tunnel(stream, rule_engine, cert_manager, addr).await; }); // Connect to proxy @@ -709,8 +726,8 @@ mod tests { // Spawn proxy handler tokio::spawn(async move { - let (stream, _) = listener.accept().await.unwrap(); - let _ = handle_connect_tunnel(stream, rule_engine, cert_manager).await; + let (stream, addr) = listener.accept().await.unwrap(); + let _ = handle_connect_tunnel(stream, rule_engine, cert_manager, addr).await; }); // Connect to proxy @@ -746,8 +763,8 @@ mod tests { // Spawn proxy handler tokio::spawn(async move { - let (stream, _) = listener.accept().await.unwrap(); - let _ = handle_transparent_tls(stream, rule_engine, cert_manager).await; + let (stream, addr) = listener.accept().await.unwrap(); + let _ = handle_transparent_tls(stream, rule_engine, cert_manager, addr).await; }); // Connect to proxy with TLS directly (transparent mode) @@ -818,8 +835,8 @@ mod tests { let cert_manager = cert_manager.clone(); let rule_engine = rule_engine.clone(); tokio::spawn(async move { - let (stream, _) = listener.accept().await.unwrap(); - let _ = handle_https_connection(stream, rule_engine, cert_manager).await; + let (stream, addr) = listener.accept().await.unwrap(); + let _ = handle_https_connection(stream, rule_engine, cert_manager, addr).await; }); let mut stream = TcpStream::connect(addr).await.unwrap(); @@ -851,9 +868,9 @@ mod tests { // Start proxy handler tokio::spawn(async move { - let (stream, _) = listener.accept().await.unwrap(); + let (stream, addr) = listener.accept().await.unwrap(); // Use the actual transparent TLS handler (which will extract SNI, etc.) - let _ = handle_transparent_tls(stream, rule_engine, cert_manager).await; + let _ = handle_transparent_tls(stream, rule_engine, cert_manager, addr).await; }); // Give the server time to start diff --git a/src/rules.rs b/src/rules.rs index dbea7f0..a088192 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -45,7 +45,7 @@ impl EvaluationResult { #[async_trait] pub trait RuleEngineTrait: Send + Sync { - async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult; + async fn evaluate(&self, method: Method, url: &str, requester_ip: &str) -> EvaluationResult; fn name(&self) -> &str; } @@ -66,8 +66,11 @@ impl LoggingRuleEngine { #[async_trait] impl RuleEngineTrait for LoggingRuleEngine { - async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult { - let result = self.engine.evaluate(method.clone(), url).await; + async fn evaluate(&self, method: Method, url: &str, requester_ip: &str) -> EvaluationResult { + let result = self + .engine + .evaluate(method.clone(), url, requester_ip) + .await; if let Some(log) = &self.request_log && let Ok(mut file) = log.lock() @@ -125,11 +128,24 @@ impl RuleEngine { } pub async fn evaluate(&self, method: Method, url: &str) -> Action { - self.inner.evaluate(method, url).await.action + self.inner.evaluate(method, url, "127.0.0.1").await.action } pub async fn evaluate_with_context(&self, method: Method, url: &str) -> EvaluationResult { - self.inner.evaluate(method, url).await + self.inner.evaluate(method, url, "127.0.0.1").await + } + + pub async fn evaluate_with_ip(&self, method: Method, url: &str, requester_ip: &str) -> Action { + self.inner.evaluate(method, url, requester_ip).await.action + } + + pub async fn evaluate_with_context_and_ip( + &self, + method: Method, + url: &str, + requester_ip: &str, + ) -> EvaluationResult { + self.inner.evaluate(method, url, requester_ip).await } } diff --git a/src/rules/pattern.rs b/src/rules/pattern.rs index e908fba..08295dc 100644 --- a/src/rules/pattern.rs +++ b/src/rules/pattern.rs @@ -52,7 +52,7 @@ impl PatternRuleEngine { #[async_trait] impl RuleEngineTrait for PatternRuleEngine { - async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult { + async fn evaluate(&self, method: Method, url: &str, _requester_ip: &str) -> EvaluationResult { for rule in &self.rules { if rule.matches(method.clone(), url) { match &rule.action { diff --git a/src/rules/script.rs b/src/rules/script.rs index c2dea76..078b91e 100644 --- a/src/rules/script.rs +++ b/src/rules/script.rs @@ -15,7 +15,12 @@ impl ScriptRuleEngine { ScriptRuleEngine { script } } - async fn execute_script(&self, method: Method, url: &str) -> (bool, String) { + async fn execute_script( + &self, + method: Method, + url: &str, + requester_ip: &str, + ) -> (bool, String) { let parsed_url = match Url::parse(url) { Ok(u) => u, Err(e) => { @@ -29,8 +34,8 @@ impl ScriptRuleEngine { let path = parsed_url.path(); debug!( - "Executing script for {} {} (host: {}, path: {})", - method, url, host, path + "Executing script for {} {} from {} (host: {}, path: {})", + method, url, requester_ip, host, path ); // Build the command @@ -48,6 +53,7 @@ impl ScriptRuleEngine { .env("HTTPJAIL_SCHEME", scheme) .env("HTTPJAIL_HOST", host) .env("HTTPJAIL_PATH", path) + .env("HTTPJAIL_REQUESTER_IP", requester_ip) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); // Ensure child is killed if dropped @@ -99,8 +105,8 @@ impl ScriptRuleEngine { #[async_trait] impl RuleEngineTrait for ScriptRuleEngine { - async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult { - let (allowed, context) = self.execute_script(method.clone(), url).await; + async fn evaluate(&self, method: Method, url: &str, requester_ip: &str) -> EvaluationResult { + let (allowed, context) = self.execute_script(method.clone(), url, requester_ip).await; if allowed { debug!("ALLOW: {} {} (script allowed)", method, url); diff --git a/tests/script_integration.rs b/tests/script_integration.rs index ac21e73..09e5e91 100644 --- a/tests/script_integration.rs +++ b/tests/script_integration.rs @@ -2,6 +2,7 @@ use httpjail::rules::script::ScriptRuleEngine; use httpjail::rules::{Action, RuleEngineTrait}; use hyper::Method; use std::fs; +use std::io::Write; use tempfile::NamedTempFile; #[tokio::test] @@ -34,13 +35,13 @@ fi // Test allowed request let result = engine - .evaluate(Method::GET, "https://github.com/user/repo") + .evaluate(Method::GET, "https://github.com/user/repo", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); // Test denied request with context let result = engine - .evaluate(Method::POST, "https://example.com/api") + .evaluate(Method::POST, "https://example.com/api", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); assert_eq!( @@ -82,18 +83,18 @@ fi // Test allowed methods let result = engine - .evaluate(Method::GET, "https://example.com/api") + .evaluate(Method::GET, "https://example.com/api", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); let result = engine - .evaluate(Method::HEAD, "https://example.com/api") + .evaluate(Method::HEAD, "https://example.com/api", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); // Test denied method with context let result = engine - .evaluate(Method::POST, "https://example.com/api") + .evaluate(Method::POST, "https://example.com/api", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); assert_eq!(result.context, Some("Method POST not allowed".to_string())); @@ -110,12 +111,16 @@ async fn test_inline_script_evaluation() { ); let result = engine - .evaluate(Method::GET, "https://example.com/api/v1/health") + .evaluate( + Method::GET, + "https://example.com/api/v1/health", + "127.0.0.1", + ) .await; assert!(matches!(result.action, Action::Allow)); let result = engine - .evaluate(Method::GET, "https://example.com/api/v2/users") + .evaluate(Method::GET, "https://example.com/api/v2/users", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); } @@ -156,7 +161,7 @@ fi // Test allowed GitHub GET let result = engine - .evaluate(Method::GET, "https://github.com/user/repo") + .evaluate(Method::GET, "https://github.com/user/repo", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); assert_eq!( @@ -166,14 +171,14 @@ fi // Test allowed API POST let result = engine - .evaluate(Method::POST, "https://api.example.com/users") + .evaluate(Method::POST, "https://api.example.com/users", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); assert_eq!(result.context, Some("API write access allowed".to_string())); // Test denied request let result = engine - .evaluate(Method::POST, "https://github.com/user/repo") + .evaluate(Method::POST, "https://github.com/user/repo", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); assert!( @@ -186,3 +191,45 @@ fi // TempPath will be automatically deleted when it goes out of scope drop(script_path); } + +#[tokio::test] +async fn test_script_receives_requester_ip() { + // Create a script that logs the requester IP + let mut script = NamedTempFile::new().unwrap(); + let script_content = r#"#!/bin/bash +if [ -n "$HTTPJAIL_REQUESTER_IP" ]; then + echo "Request from IP: $HTTPJAIL_REQUESTER_IP" + exit 0 +else + echo "No requester IP provided" + exit 1 +fi +"#; + script.write_all(script_content.as_bytes()).unwrap(); + script.flush().unwrap(); + + let script_path = script.into_temp_path(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + + let engine = ScriptRuleEngine::new(script_path.to_str().unwrap().to_string()); + + // Test with a specific IP + let result = engine + .evaluate(Method::GET, "https://example.com", "192.168.1.100") + .await; + + assert!(matches!(result.action, Action::Allow)); + assert!(result.context.is_some()); + assert!( + result + .context + .unwrap() + .contains("Request from IP: 192.168.1.100") + ); + + drop(script_path); +} From 5214786c077573e69efb7e80b32feb70f8fd344b Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:26:46 +0000 Subject: [PATCH 2/6] test: update RuleEngineTrait evaluate call sites to include requester_ip Fix tests to pass new parameter after API change. Co-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/rules/pattern.rs | 18 ++++++++++++------ src/rules/script.rs | 14 +++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/rules/pattern.rs b/src/rules/pattern.rs index 08295dc..f27286b 100644 --- a/src/rules/pattern.rs +++ b/src/rules/pattern.rs @@ -125,7 +125,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::GET, "https://github.com/api") +- .evaluate(Method::GET, "https://github.com/api") ++ .evaluate(Method::GET, "https://github.com/api", "127.0.0.1") .await .action, Action::Allow @@ -133,7 +134,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::POST, "https://telemetry.example.com") +- .evaluate(Method::POST, "https://telemetry.example.com") ++ .evaluate(Method::POST, "https://telemetry.example.com", "127.0.0.1") .await .action, Action::Deny @@ -141,7 +143,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::GET, "https://example.com") +- .evaluate(Method::GET, "https://example.com") ++ .evaluate(Method::GET, "https://example.com", "127.0.0.1") .await .action, Action::Deny @@ -161,7 +164,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::GET, "https://api.example.com/data") +- .evaluate(Method::GET, "https://api.example.com/data") ++ .evaluate(Method::GET, "https://api.example.com/data", "127.0.0.1") .await .action, Action::Allow @@ -169,7 +173,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::POST, "https://api.example.com/data") +- .evaluate(Method::POST, "https://api.example.com/data") ++ .evaluate(Method::POST, "https://api.example.com/data", "127.0.0.1") .await .action, Action::Deny @@ -182,7 +187,8 @@ mod tests { assert!(matches!( engine - .evaluate(Method::GET, "https://example.com") +- .evaluate(Method::GET, "https://example.com") ++ .evaluate(Method::GET, "https://example.com", "127.0.0.1") .await .action, Action::Deny diff --git a/src/rules/script.rs b/src/rules/script.rs index 078b91e..f0cfc36 100644 --- a/src/rules/script.rs +++ b/src/rules/script.rs @@ -159,7 +159,7 @@ exit 0 let engine = ScriptRuleEngine::new(script_path.to_str().unwrap().to_string()); let result = engine - .evaluate(Method::GET, "https://example.com/test") + .evaluate(Method::GET, "https://example.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); @@ -188,7 +188,7 @@ exit 1 let engine = ScriptRuleEngine::new(script_path.to_str().unwrap().to_string()); let result = engine - .evaluate(Method::GET, "https://example.com/test") + .evaluate(Method::GET, "https://example.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); @@ -218,7 +218,7 @@ exit 1 let engine = ScriptRuleEngine::new(script_path.to_str().unwrap().to_string()); let result = engine - .evaluate(Method::GET, "https://example.com/test") + .evaluate(Method::GET, "https://example.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); @@ -254,12 +254,12 @@ fi let engine = ScriptRuleEngine::new(script_path.to_str().unwrap().to_string()); let result = engine - .evaluate(Method::GET, "https://allowed.com/test") + .evaluate(Method::GET, "https://allowed.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); let result = engine - .evaluate(Method::GET, "https://blocked.com/test") + .evaluate(Method::GET, "https://blocked.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); assert_eq!( @@ -274,12 +274,12 @@ fi let engine = ScriptRuleEngine::new("test \"$HTTPJAIL_HOST\" = \"github.com\"".to_string()); let result = engine - .evaluate(Method::GET, "https://github.com/test") + .evaluate(Method::GET, "https://github.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Allow)); let result = engine - .evaluate(Method::GET, "https://example.com/test") + .evaluate(Method::GET, "https://example.com/test", "127.0.0.1") .await; assert!(matches!(result.action, Action::Deny)); } From ea45d44c9abe4f6e8bd15d11eaaa950066788338 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:28:39 +0000 Subject: [PATCH 3/6] chore(fmt): rustfmt after test updates Co-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/rules/pattern.rs | 84 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/rules/pattern.rs b/src/rules/pattern.rs index f27286b..3c07105 100644 --- a/src/rules/pattern.rs +++ b/src/rules/pattern.rs @@ -124,31 +124,31 @@ mod tests { let engine = PatternRuleEngine::new(rules); assert!(matches!( - engine -- .evaluate(Method::GET, "https://github.com/api") -+ .evaluate(Method::GET, "https://github.com/api", "127.0.0.1") - .await - .action, - Action::Allow - )); + engine + - .evaluate(Method::GET, "https://github.com/api") + + .evaluate(Method::GET, "https://github.com/api", "127.0.0.1") + .await + .action, + Action::Allow + )); assert!(matches!( - engine -- .evaluate(Method::POST, "https://telemetry.example.com") -+ .evaluate(Method::POST, "https://telemetry.example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + - .evaluate(Method::POST, "https://telemetry.example.com") + + .evaluate(Method::POST, "https://telemetry.example.com", "127.0.0.1") + .await + .action, + Action::Deny + )); assert!(matches!( - engine -- .evaluate(Method::GET, "https://example.com") -+ .evaluate(Method::GET, "https://example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + - .evaluate(Method::GET, "https://example.com") + + .evaluate(Method::GET, "https://example.com", "127.0.0.1") + .await + .action, + Action::Deny + )); } #[tokio::test] @@ -163,22 +163,22 @@ mod tests { let engine = PatternRuleEngine::new(rules); assert!(matches!( - engine -- .evaluate(Method::GET, "https://api.example.com/data") -+ .evaluate(Method::GET, "https://api.example.com/data", "127.0.0.1") - .await - .action, - Action::Allow - )); + engine + - .evaluate(Method::GET, "https://api.example.com/data") + + .evaluate(Method::GET, "https://api.example.com/data", "127.0.0.1") + .await + .action, + Action::Allow + )); assert!(matches!( - engine -- .evaluate(Method::POST, "https://api.example.com/data") -+ .evaluate(Method::POST, "https://api.example.com/data", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + - .evaluate(Method::POST, "https://api.example.com/data") + + .evaluate(Method::POST, "https://api.example.com/data", "127.0.0.1") + .await + .action, + Action::Deny + )); } #[tokio::test] @@ -186,12 +186,12 @@ mod tests { let engine = PatternRuleEngine::new(vec![]); assert!(matches!( - engine -- .evaluate(Method::GET, "https://example.com") -+ .evaluate(Method::GET, "https://example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + - .evaluate(Method::GET, "https://example.com") + + .evaluate(Method::GET, "https://example.com", "127.0.0.1") + .await + .action, + Action::Deny + )); } } From f067d26abf9a6bd7d4b737674408e7c9e60bd448 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:30:47 +0000 Subject: [PATCH 4/6] fix(tests): remove diff artifacts in pattern.rs and add requester_ip Co-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/rules/pattern.rs | 78 ++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/rules/pattern.rs b/src/rules/pattern.rs index 3c07105..beb822c 100644 --- a/src/rules/pattern.rs +++ b/src/rules/pattern.rs @@ -124,31 +124,28 @@ mod tests { let engine = PatternRuleEngine::new(rules); assert!(matches!( - engine - - .evaluate(Method::GET, "https://github.com/api") - + .evaluate(Method::GET, "https://github.com/api", "127.0.0.1") - .await - .action, - Action::Allow - )); + engine + .evaluate(Method::GET, "https://github.com/api", "127.0.0.1") + .await + .action, + Action::Allow + )); assert!(matches!( - engine - - .evaluate(Method::POST, "https://telemetry.example.com") - + .evaluate(Method::POST, "https://telemetry.example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + .evaluate(Method::POST, "https://telemetry.example.com", "127.0.0.1",) + .await + .action, + Action::Deny + )); assert!(matches!( - engine - - .evaluate(Method::GET, "https://example.com") - + .evaluate(Method::GET, "https://example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + .evaluate(Method::GET, "https://example.com", "127.0.0.1") + .await + .action, + Action::Deny + )); } #[tokio::test] @@ -163,22 +160,20 @@ mod tests { let engine = PatternRuleEngine::new(rules); assert!(matches!( - engine - - .evaluate(Method::GET, "https://api.example.com/data") - + .evaluate(Method::GET, "https://api.example.com/data", "127.0.0.1") - .await - .action, - Action::Allow - )); + engine + .evaluate(Method::GET, "https://api.example.com/data", "127.0.0.1",) + .await + .action, + Action::Allow + )); assert!(matches!( - engine - - .evaluate(Method::POST, "https://api.example.com/data") - + .evaluate(Method::POST, "https://api.example.com/data", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + .evaluate(Method::POST, "https://api.example.com/data", "127.0.0.1",) + .await + .action, + Action::Deny + )); } #[tokio::test] @@ -186,12 +181,11 @@ mod tests { let engine = PatternRuleEngine::new(vec![]); assert!(matches!( - engine - - .evaluate(Method::GET, "https://example.com") - + .evaluate(Method::GET, "https://example.com", "127.0.0.1") - .await - .action, - Action::Deny - )); + engine + .evaluate(Method::GET, "https://example.com", "127.0.0.1") + .await + .action, + Action::Deny + )); } } From 6b6c7fda3d1c6b1f0b4ec3e94895dbbb0b0e85c5 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:23:36 +0000 Subject: [PATCH 5/6] docs(js): document r.requester_ip in JS API; impl requester_ip in V8 engine\n\nCo-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f4ad917..4371fb2 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,7 @@ All request information is available via the `r` object: - `r.host` - Hostname from the URL - `r.scheme` - URL scheme (http or https) - `r.path` - Path portion of the URL +- `r.requester_ip` - IP address of the client making the request - `r.block_message` - Optional message to set when denying (writable) **JavaScript evaluation rules:** From dcaa5023746e0612f5ad1147bd54cdb254227f19 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:41:54 +0000 Subject: [PATCH 6/6] style: rustfmt after JS requester_ip changes Co-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/rules/v8_js.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rules/v8_js.rs b/src/rules/v8_js.rs index 126dacf..7476e4f 100644 --- a/src/rules/v8_js.rs +++ b/src/rules/v8_js.rs @@ -63,7 +63,12 @@ impl V8JsRuleEngine { } /// Evaluate the JavaScript rule against the given request - fn execute_js_rule(&self, method: &Method, url: &str, requester_ip: &str) -> (bool, Option) { + fn execute_js_rule( + &self, + method: &Method, + url: &str, + requester_ip: &str, + ) -> (bool, Option) { let parsed_url = match Url::parse(url) { Ok(u) => u, Err(e) => { @@ -374,4 +379,4 @@ mod tests { assert!(matches!(result.action, Action::Allow)); assert_eq!(result.context, None); } -} \ No newline at end of file +}