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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,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:**
Expand DownExpand Up@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
- `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:**

Expand All@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
> Script-based evaluation can also be used for custom logging! Your script can log requests to a database, send metrics to a monitoring service, or implement complex audit trails before returning the allow/deny decision.

## Advanced Options

```bash
# Verbose logging
httpjail -vvv --js "true" -- curl https://example.com
Expand Down
30 changes: 23 additions & 7 deletions src/proxy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -293,7 +293,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);
}
Expand DownExpand Up@@ -335,7 +336,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);
}
Expand DownExpand Up@@ -364,10 +366,16 @@ async fn handle_http_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -383,15 +391,17 @@ async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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<Incoming>,
rule_engine: Arc<RuleEngine>,
_cert_manager: Arc<CertificateManager>,
remote_addr: SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -412,10 +422,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);
Expand Down
61 changes: 39 additions & 22 deletions src/proxy_tls.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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];
Expand All@@ -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!(
Expand DownExpand Up@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
mut stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling transparent TLS connection");

Expand DownExpand Up@@ -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");
Expand All@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
debug!("Handling CONNECT tunnel");

Expand DownExpand Up@@ -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 => {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -372,6 +376,7 @@ async fn perform_tls_interception(
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
host: &str,
remote_addr: std::net::SocketAddr,
) -> Result<()> {
// Get certificate for the host
let (cert_chain, key) = cert_manager
Expand DownExpand Up@@ -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");
Expand All@@ -425,12 +431,18 @@ async fn handle_plain_http(
stream: TcpStream,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
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()
Expand All@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
req: Request<Incoming>,
rule_engine: Arc<RuleEngine>,
host: String,
remote_addr: std::net::SocketAddr,
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
Expand All@@ -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 => {
Expand DownExpand Up@@ -671,8 +688,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
Expand DownExpand Up@@ -706,8 +723,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
Expand DownExpand Up@@ -743,8 +760,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)
Expand DownExpand Up@@ -815,8 +832,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();
Expand DownExpand Up@@ -848,9 +865,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
Expand Down
26 changes: 21 additions & 5 deletions src/rules.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,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;
Comment on lines 44 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Update tests for new requester_ip parameter

The trait method RuleEngineTrait::evaluate now requires a requester_ip argument, but the unit tests still invoke engine.evaluate(method, url) with the old two-parameter signature (see src/rules/script.rs and src/rules.rs). Running cargo test will fail to compile until those call sites provide an IP value or the API offers a backwards-compatible wrapper. Consider updating the tests to pass a dummy IP so the suite builds again.

Useful? React with 👍 / 👎.


fn name(&self) -> &str;
}
Expand All@@ -65,8 +65,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()
Expand DownExpand Up@@ -110,11 +113,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
}
}

Expand Down
Loading
Loading