Skip to content

Commit 340606d

Browse files
authored
feat(rules): expose requester IP to JS (r.requester_ip) and scripts (HTTPJAIL_REQUESTER_IP) (#25)
1 parent 477acd5 commit 340606d

File tree

7 files changed

+189
-74
lines changed

7 files changed

+189
-74
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ All request information is available via the `r` object:
175175
- `r.host` - Hostname from the URL
176176
- `r.scheme` - URL scheme (http or https)
177177
- `r.path` - Path portion of the URL
178+
- `r.requester_ip` - IP address of the client making the request
178179
- `r.block_message` - Optional message to set when denying (writable)
179180

180181
**JavaScript evaluation rules:**
@@ -224,6 +225,7 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
224225
- `HTTPJAIL_HOST` - Hostname from the URL
225226
- `HTTPJAIL_SCHEME` - URL scheme (http or https)
226227
- `HTTPJAIL_PATH` - Path component of the URL
228+
- `HTTPJAIL_REQUESTER_IP` - IP address of the client making the request
227229

228230
**Script requirements:**
229231

@@ -236,7 +238,6 @@ If `--sh` has spaces, it's run through `sh`; otherwise it's executed directly.
236238
> 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.
237239
238240
## Advanced Options
239-
240241
```bash
241242
# Verbose logging
242243
httpjail -vvv --js "true" -- curl https://example.com

src/proxy.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ impl ProxyServer {
293293

294294
tokio::spawn(async move {
295295
if let Err(e) =
296-
handle_http_connection(stream, rule_engine, cert_manager).await
296+
handle_http_connection(stream, rule_engine, cert_manager, addr)
297+
.await
297298
{
298299
error!("Error handling HTTP connection: {:?}", e);
299300
}
@@ -335,7 +336,8 @@ impl ProxyServer {
335336

336337
tokio::spawn(async move {
337338
if let Err(e) =
338-
handle_https_connection(stream, rule_engine, cert_manager).await
339+
handle_https_connection(stream, rule_engine, cert_manager, addr)
340+
.await
339341
{
340342
error!("Error handling HTTPS connection: {:?}", e);
341343
}
@@ -364,10 +366,16 @@ async fn handle_http_connection(
364366
stream: TcpStream,
365367
rule_engine: Arc<RuleEngine>,
366368
cert_manager: Arc<CertificateManager>,
369+
remote_addr: SocketAddr,
367370
) -> Result<()> {
368371
let io = TokioIo::new(stream);
369372
let service = service_fn(move |req| {
370-
handle_http_request(req, Arc::clone(&rule_engine), Arc::clone(&cert_manager))
373+
handle_http_request(
374+
req,
375+
Arc::clone(&rule_engine),
376+
Arc::clone(&cert_manager),
377+
remote_addr,
378+
)
371379
});
372380

373381
http1::Builder::new()
@@ -383,15 +391,17 @@ async fn handle_https_connection(
383391
stream: TcpStream,
384392
rule_engine: Arc<RuleEngine>,
385393
cert_manager: Arc<CertificateManager>,
394+
remote_addr: SocketAddr,
386395
) -> Result<()> {
387396
// Delegate to the TLS-specific module
388-
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager).await
397+
crate::proxy_tls::handle_https_connection(stream, rule_engine, cert_manager, remote_addr).await
389398
}
390399

391400
pub async fn handle_http_request(
392401
req: Request<Incoming>,
393402
rule_engine: Arc<RuleEngine>,
394403
_cert_manager: Arc<CertificateManager>,
404+
remote_addr: SocketAddr,
395405
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
396406
let method = req.method().clone();
397407
let uri = req.uri().clone();
@@ -412,10 +422,16 @@ pub async fn handle_http_request(
412422
format!("http://{}{}", host, path)
413423
};
414424

415-
debug!("Proxying HTTP request: {} {}", method, full_url);
425+
debug!(
426+
"Proxying HTTP request: {} {} from {}",
427+
method, full_url, remote_addr
428+
);
416429

417-
// Evaluate rules with method
418-
let evaluation = rule_engine.evaluate_with_context(method, &full_url).await;
430+
// Evaluate rules with method and requester IP
431+
let requester_ip = remote_addr.ip().to_string();
432+
let evaluation = rule_engine
433+
.evaluate_with_context_and_ip(method, &full_url, &requester_ip)
434+
.await;
419435
match evaluation.action {
420436
Action::Allow => {
421437
debug!("Request allowed: {}", full_url);

src/proxy_tls.rs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ pub async fn handle_https_connection(
3737
stream: TcpStream,
3838
rule_engine: Arc<RuleEngine>,
3939
cert_manager: Arc<CertificateManager>,
40+
remote_addr: std::net::SocketAddr,
4041
) -> Result<()> {
41-
debug!("Handling new HTTPS connection");
42+
debug!("Handling new HTTPS connection from {}", remote_addr);
4243

4344
// Peek at the first few bytes to determine if this is HTTP or TLS
4445
let mut peek_buf = [0; 6];
@@ -64,18 +65,18 @@ pub async fn handle_https_connection(
6465
if peek_buf[0] == 0x16 && n > 1 && (peek_buf[1] == 0x03 || peek_buf[1] == 0x02) {
6566
// This is a TLS ClientHello - we're in transparent proxy mode
6667
debug!("Detected TLS ClientHello - transparent proxy mode");
67-
handle_transparent_tls(stream, rule_engine, cert_manager).await
68+
handle_transparent_tls(stream, rule_engine, cert_manager, remote_addr).await
6869
} else if peek_buf[0] >= 0x41 && peek_buf[0] <= 0x5A {
6970
// This looks like HTTP (starts with uppercase ASCII letter)
7071
// Check if it's a CONNECT request
7172
let request_str = String::from_utf8_lossy(&peek_buf);
7273
if request_str.starts_with("CONNEC") {
7374
debug!("Detected CONNECT request - explicit proxy mode");
74-
handle_connect_tunnel(stream, rule_engine, cert_manager).await
75+
handle_connect_tunnel(stream, rule_engine, cert_manager, remote_addr).await
7576
} else {
7677
// Regular HTTP on HTTPS port
7778
debug!("Detected plain HTTP on HTTPS port");
78-
handle_plain_http(stream, rule_engine, cert_manager).await
79+
handle_plain_http(stream, rule_engine, cert_manager, remote_addr).await
7980
}
8081
} else {
8182
warn!(
@@ -159,6 +160,7 @@ async fn handle_transparent_tls(
159160
mut stream: TcpStream,
160161
rule_engine: Arc<RuleEngine>,
161162
cert_manager: Arc<CertificateManager>,
163+
remote_addr: std::net::SocketAddr,
162164
) -> Result<()> {
163165
debug!("Handling transparent TLS connection");
164166

@@ -212,7 +214,7 @@ async fn handle_transparent_tls(
212214
let io = TokioIo::new(tls_stream);
213215
let service = service_fn(move |req| {
214216
let host_clone = hostname.clone();
215-
handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone)
217+
handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone, remote_addr)
216218
});
217219

218220
debug!("Starting HTTP/1.1 server for decrypted requests");
@@ -230,6 +232,7 @@ async fn handle_connect_tunnel(
230232
stream: TcpStream,
231233
rule_engine: Arc<RuleEngine>,
232234
cert_manager: Arc<CertificateManager>,
235+
remote_addr: std::net::SocketAddr,
233236
) -> Result<()> {
234237
debug!("Handling CONNECT tunnel");
235238

@@ -305,8 +308,9 @@ async fn handle_connect_tunnel(
305308

306309
// Check if this host is allowed
307310
let full_url = format!("https://{}", target);
311+
let requester_ip = remote_addr.ip().to_string();
308312
let evaluation = rule_engine
309-
.evaluate_with_context(Method::GET, &full_url)
313+
.evaluate_with_context_and_ip(Method::GET, &full_url, &requester_ip)
310314
.await;
311315
match evaluation.action {
312316
Action::Allow => {
@@ -337,7 +341,7 @@ async fn handle_connect_tunnel(
337341
debug!("Sent 200 Connection Established, starting TLS handshake");
338342

339343
// Now perform TLS handshake with the client
340-
perform_tls_interception(stream, rule_engine, cert_manager, host).await
344+
perform_tls_interception(stream, rule_engine, cert_manager, host, remote_addr).await
341345
}
342346
Action::Deny => {
343347
warn!("CONNECT denied to: {}", host);
@@ -372,6 +376,7 @@ async fn perform_tls_interception(
372376
rule_engine: Arc<RuleEngine>,
373377
cert_manager: Arc<CertificateManager>,
374378
host: &str,
379+
remote_addr: std::net::SocketAddr,
375380
) -> Result<()> {
376381
// Get certificate for the host
377382
let (cert_chain, key) = cert_manager
@@ -405,9 +410,10 @@ async fn perform_tls_interception(
405410
// Now handle the decrypted HTTPS requests
406411
let io = TokioIo::new(tls_stream);
407412
let host_string = host.to_string();
413+
let remote_addr_copy = remote_addr; // Copy for the closure
408414
let service = service_fn(move |req| {
409415
let host_clone = host_string.clone();
410-
handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone)
416+
handle_decrypted_https_request(req, Arc::clone(&rule_engine), host_clone, remote_addr_copy)
411417
});
412418

413419
debug!("Starting HTTP/1.1 server for decrypted requests");
@@ -425,12 +431,18 @@ async fn handle_plain_http(
425431
stream: TcpStream,
426432
rule_engine: Arc<RuleEngine>,
427433
cert_manager: Arc<CertificateManager>,
434+
remote_addr: std::net::SocketAddr,
428435
) -> Result<()> {
429436
debug!("Handling plain HTTP on HTTPS port");
430437

431438
let io = TokioIo::new(stream);
432439
let service = service_fn(move |req| {
433-
crate::proxy::handle_http_request(req, Arc::clone(&rule_engine), Arc::clone(&cert_manager))
440+
crate::proxy::handle_http_request(
441+
req,
442+
Arc::clone(&rule_engine),
443+
Arc::clone(&cert_manager),
444+
remote_addr,
445+
)
434446
});
435447

436448
http1::Builder::new()
@@ -447,6 +459,7 @@ async fn handle_decrypted_https_request(
447459
req: Request<Incoming>,
448460
rule_engine: Arc<RuleEngine>,
449461
host: String,
462+
remote_addr: std::net::SocketAddr,
450463
) -> Result<Response<BoxBody<Bytes, HyperError>>, std::convert::Infallible> {
451464
let method = req.method().clone();
452465
let uri = req.uri().clone();
@@ -455,11 +468,15 @@ async fn handle_decrypted_https_request(
455468
let path = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
456469
let full_url = format!("https://{}{}", host, path);
457470

458-
debug!("Proxying HTTPS request: {} {}", method, full_url);
471+
debug!(
472+
"Proxying HTTPS request: {} {} from {}",
473+
method, full_url, remote_addr
474+
);
459475

460-
// Evaluate rules with method
476+
// Evaluate rules with method and requester IP
477+
let requester_ip = remote_addr.ip().to_string();
461478
let evaluation = rule_engine
462-
.evaluate_with_context(method.clone(), &full_url)
479+
.evaluate_with_context_and_ip(method.clone(), &full_url, &requester_ip)
463480
.await;
464481
match evaluation.action {
465482
Action::Allow => {
@@ -671,8 +688,8 @@ mod tests {
671688

672689
// Spawn proxy handler
673690
tokio::spawn(async move {
674-
let (stream, _) = listener.accept().await.unwrap();
675-
let _ = handle_connect_tunnel(stream, rule_engine, cert_manager).await;
691+
let (stream, addr) = listener.accept().await.unwrap();
692+
let _ = handle_connect_tunnel(stream, rule_engine, cert_manager, addr).await;
676693
});
677694

678695
// Connect to proxy
@@ -706,8 +723,8 @@ mod tests {
706723

707724
// Spawn proxy handler
708725
tokio::spawn(async move {
709-
let (stream, _) = listener.accept().await.unwrap();
710-
let _ = handle_connect_tunnel(stream, rule_engine, cert_manager).await;
726+
let (stream, addr) = listener.accept().await.unwrap();
727+
let _ = handle_connect_tunnel(stream, rule_engine, cert_manager, addr).await;
711728
});
712729

713730
// Connect to proxy
@@ -743,8 +760,8 @@ mod tests {
743760

744761
// Spawn proxy handler
745762
tokio::spawn(async move {
746-
let (stream, _) = listener.accept().await.unwrap();
747-
let _ = handle_transparent_tls(stream, rule_engine, cert_manager).await;
763+
let (stream, addr) = listener.accept().await.unwrap();
764+
let _ = handle_transparent_tls(stream, rule_engine, cert_manager, addr).await;
748765
});
749766

750767
// Connect to proxy with TLS directly (transparent mode)
@@ -815,8 +832,8 @@ mod tests {
815832
let cert_manager = cert_manager.clone();
816833
let rule_engine = rule_engine.clone();
817834
tokio::spawn(async move {
818-
let (stream, _) = listener.accept().await.unwrap();
819-
let _ = handle_https_connection(stream, rule_engine, cert_manager).await;
835+
let (stream, addr) = listener.accept().await.unwrap();
836+
let _ = handle_https_connection(stream, rule_engine, cert_manager, addr).await;
820837
});
821838

822839
let mut stream = TcpStream::connect(addr).await.unwrap();
@@ -848,9 +865,9 @@ mod tests {
848865

849866
// Start proxy handler
850867
tokio::spawn(async move {
851-
let (stream, _) = listener.accept().await.unwrap();
868+
let (stream, addr) = listener.accept().await.unwrap();
852869
// Use the actual transparent TLS handler (which will extract SNI, etc.)
853-
let _ = handle_transparent_tls(stream, rule_engine, cert_manager).await;
870+
let _ = handle_transparent_tls(stream, rule_engine, cert_manager, addr).await;
854871
});
855872

856873
// Give the server time to start

src/rules.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl EvaluationResult {
4444

4545
#[async_trait]
4646
pub trait RuleEngineTrait: Send + Sync {
47-
async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult;
47+
async fn evaluate(&self, method: Method, url: &str, requester_ip: &str) -> EvaluationResult;
4848

4949
fn name(&self) -> &str;
5050
}
@@ -65,8 +65,11 @@ impl LoggingRuleEngine {
6565

6666
#[async_trait]
6767
impl RuleEngineTrait for LoggingRuleEngine {
68-
async fn evaluate(&self, method: Method, url: &str) -> EvaluationResult {
69-
let result = self.engine.evaluate(method.clone(), url).await;
68+
async fn evaluate(&self, method: Method, url: &str, requester_ip: &str) -> EvaluationResult {
69+
let result = self
70+
.engine
71+
.evaluate(method.clone(), url, requester_ip)
72+
.await;
7073

7174
if let Some(log) = &self.request_log
7275
&& let Ok(mut file) = log.lock()
@@ -110,11 +113,24 @@ impl RuleEngine {
110113
}
111114

112115
pub async fn evaluate(&self, method: Method, url: &str) -> Action {
113-
self.inner.evaluate(method, url).await.action
116+
self.inner.evaluate(method, url, "127.0.0.1").await.action
114117
}
115118

116119
pub async fn evaluate_with_context(&self, method: Method, url: &str) -> EvaluationResult {
117-
self.inner.evaluate(method, url).await
120+
self.inner.evaluate(method, url, "127.0.0.1").await
121+
}
122+
123+
pub async fn evaluate_with_ip(&self, method: Method, url: &str, requester_ip: &str) -> Action {
124+
self.inner.evaluate(method, url, requester_ip).await.action
125+
}
126+
127+
pub async fn evaluate_with_context_and_ip(
128+
&self,
129+
method: Method,
130+
url: &str,
131+
requester_ip: &str,
132+
) -> EvaluationResult {
133+
self.inner.evaluate(method, url, requester_ip).await
118134
}
119135
}
120136

0 commit comments

Comments
 (0)