Skip to content

Commit 42c46c9

Browse files
authored
Add console.log() support for JavaScript rules (#92)
1 parent 4700593 commit 42c46c9

6 files changed

Lines changed: 243 additions & 0 deletions

File tree

docs/guide/rule-engines/javascript.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,72 @@ const requestString = `${r.method} ${r.host}${r.path}`;
151151
patterns.some(pattern => pattern.test(requestString))
152152
```
153153

154+
## Debugging with Console API
155+
156+
JavaScript rules support the full console API for debugging. Each method maps to a corresponding tracing level:
157+
158+
| Console Method | Tracing Level | Use Case |
159+
|----------------|---------------|----------|
160+
| `console.debug()` | DEBUG | Detailed troubleshooting information |
161+
| `console.log()` | INFO | General informational messages |
162+
| `console.info()` | INFO | Informational messages (e.g., allowed requests) |
163+
| `console.warn()` | WARN | Warning messages (e.g., suspicious patterns) |
164+
| `console.error()` | ERROR | Error messages (e.g., blocked threats) |
165+
166+
### Example
167+
168+
```javascript
169+
// Debug: detailed information
170+
console.debug("Evaluating request:", r.method, r.url);
171+
console.debug("Full request:", r);
172+
173+
// Info: general messages
174+
console.info("Allowing trusted domain:", r.host);
175+
176+
// Warn: suspicious patterns
177+
console.warn("Suspicious path detected:", r.path);
178+
179+
// Error: security issues
180+
console.error("Blocked malicious request:", r.url);
181+
```
182+
183+
### Viewing Console Output
184+
185+
Set `RUST_LOG` to control which messages appear:
186+
187+
```bash
188+
# Show debug and above (debug, info, warn, error) - all console output
189+
RUST_LOG=debug httpjail --js-file rules.js -- command
190+
191+
# Show info and above (info, warn, error) - recommended for production
192+
# Includes console.log(), console.info(), console.warn(), console.error()
193+
RUST_LOG=info httpjail --js-file rules.js -- command
194+
195+
# Show only warnings and errors
196+
RUST_LOG=warn httpjail --js-file rules.js -- command
197+
```
198+
199+
Example output with color coding:
200+
201+
```
202+
DEBUG httpjail::rules::js: Evaluating request: GET https://api.github.com/users
203+
INFO httpjail::rules::js: Allowing trusted domain: api.github.com
204+
WARN httpjail::rules::js: Suspicious path detected: /admin
205+
ERROR httpjail::rules::js: Blocked malicious request: https://evil.com/exploit
206+
```
207+
208+
### Objects and Arrays
209+
210+
Objects and arrays are automatically JSON-stringified:
211+
212+
```javascript
213+
console.log("Request:", r);
214+
// Output: Request: {"url":"https://...","method":"GET",...}
215+
216+
console.log("Complex:", {hosts: ["a.com", "b.com"], count: 42});
217+
// Output: Complex: {"hosts":["a.com","b.com"],"count":42}
218+
```
219+
154220
## When to Use
155221

156222
Best for:

examples/console_log_demo.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Example JavaScript rule file demonstrating console API usage
2+
// This can be used with: httpjail --js-file examples/console_log_demo.js ...
3+
//
4+
// Console output is visible when running with appropriate log levels:
5+
// RUST_LOG=debug httpjail --js-file examples/console_log_demo.js ... # Shows debug/log
6+
// RUST_LOG=info httpjail --js-file examples/console_log_demo.js ... # Shows info/warn/error
7+
// RUST_LOG=warn httpjail --js-file examples/console_log_demo.js ... # Shows warn/error
8+
9+
// Different console methods map to tracing levels:
10+
// console.debug() -> DEBUG
11+
// console.log() -> INFO
12+
// console.info() -> INFO
13+
// console.warn() -> WARN
14+
// console.error() -> ERROR
15+
16+
// Debug: detailed information for troubleshooting
17+
console.debug("Evaluating request:", r.method, r.url);
18+
console.debug("Full request object:", r);
19+
20+
// Log: general informational messages
21+
console.log("Requester IP:", r.requester_ip);
22+
23+
// Example: Allow only GET requests to example.com
24+
if (r.method === "GET" && r.url.includes("example.com")) {
25+
console.info("Allowing request to example.com");
26+
true
27+
} else if (r.url.includes("suspicious-site.com")) {
28+
console.error("Blocked suspicious site:", r.url);
29+
({deny_message: "Blocked: suspicious site"})
30+
} else {
31+
console.warn("Denying request - not example.com or not GET");
32+
({deny_message: "Only GET requests to example.com are allowed"})
33+
}

src/rules.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod common;
2+
mod console_log;
23
pub mod proc;
34
pub mod shell;
45
pub mod v8_js;

src/rules/console_log.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use tracing::{debug, info, warn};
2+
3+
/// Convert a V8 value to a string, using JSON.stringify for objects
4+
fn v8_value_to_string(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> String {
5+
// For objects and arrays, try JSON.stringify
6+
if value.is_object() && !value.is_null() && !value.is_undefined() {
7+
if let Some(json_str) = try_json_stringify(scope, value) {
8+
return json_str;
9+
}
10+
}
11+
12+
// Fallback to toString() for all types
13+
value
14+
.to_string(scope)
15+
.map(|s| s.to_rust_string_lossy(scope))
16+
.unwrap_or_else(|| "[value]".to_string())
17+
}
18+
19+
/// Try to JSON.stringify a value, returning None if it fails
20+
fn try_json_stringify(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> Option<String> {
21+
let global = scope.get_current_context().global(scope);
22+
let json_key = v8::String::new(scope, "JSON")?;
23+
let stringify_key = v8::String::new(scope, "stringify")?;
24+
25+
let json_obj = global.get(scope, json_key.into())?.to_object(scope)?;
26+
let stringify_fn = json_obj.get(scope, stringify_key.into())?;
27+
let stringify_fn = v8::Local::<v8::Function>::try_from(stringify_fn).ok()?;
28+
29+
let result = stringify_fn.call(scope, json_obj.into(), &[value])?;
30+
result
31+
.to_string(scope)
32+
.map(|s| s.to_rust_string_lossy(scope))
33+
}
34+
35+
/// Format console arguments into a single string
36+
fn format_console_args(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments) -> String {
37+
let mut log_parts = Vec::new();
38+
for i in 0..args.length() {
39+
let arg = args.get(i);
40+
log_parts.push(v8_value_to_string(scope, arg));
41+
}
42+
log_parts.join(" ")
43+
}
44+
45+
/// Generic console callback that logs at a specific level
46+
fn console_callback(
47+
scope: &mut v8::HandleScope,
48+
args: v8::FunctionCallbackArguments,
49+
_retval: v8::ReturnValue,
50+
log_fn: fn(&str),
51+
) {
52+
let message = format_console_args(scope, args);
53+
log_fn(&message);
54+
}
55+
56+
/// Macro to generate console method callbacks for each log level
57+
macro_rules! console_method {
58+
($name:ident, $log_macro:path) => {
59+
fn $name(
60+
scope: &mut v8::HandleScope,
61+
args: v8::FunctionCallbackArguments,
62+
retval: v8::ReturnValue,
63+
) {
64+
console_callback(scope, args, retval, |msg| {
65+
$log_macro!(target: "httpjail::rules::js", "{}", msg)
66+
});
67+
}
68+
};
69+
}
70+
71+
// Generate console.debug, console.log, console.info, console.warn, console.error
72+
console_method!(console_debug, debug);
73+
console_method!(console_log, info);
74+
console_method!(console_info, info);
75+
console_method!(console_warn, warn);
76+
console_method!(console_error, tracing::error);
77+
78+
/// Set up console object with debug, log, info, warn, error methods
79+
pub fn setup_console(context_scope: &mut v8::ContextScope<v8::HandleScope>) {
80+
let global = context_scope.get_current_context().global(context_scope);
81+
let console_obj = v8::Object::new(context_scope);
82+
83+
// Register each console method
84+
macro_rules! add_console_method {
85+
($name:expr, $callback:expr) => {
86+
let key = v8::String::new(context_scope, $name).unwrap();
87+
let func = v8::Function::new(context_scope, $callback).unwrap();
88+
console_obj.set(context_scope, key.into(), func.into());
89+
};
90+
}
91+
92+
add_console_method!("debug", console_debug);
93+
add_console_method!("log", console_log);
94+
add_console_method!("info", console_info);
95+
add_console_method!("warn", console_warn);
96+
add_console_method!("error", console_error);
97+
98+
let console_key = v8::String::new(context_scope, "console").unwrap();
99+
global.set(context_scope, console_key.into(), console_obj.into());
100+
}

src/rules/v8_js.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::rules::common::{RequestInfo, RuleResponse};
2+
use crate::rules::console_log;
23
use crate::rules::{EvaluationResult, RuleEngineTrait};
34
use async_trait::async_trait;
45
use hyper::Method;
@@ -125,6 +126,9 @@ impl V8JsRuleEngine {
125126
let context = v8::Context::new(handle_scope, Default::default());
126127
let context_scope = &mut v8::ContextScope::new(handle_scope, context);
127128

129+
// Set up console object with debug, log, info, warn, error methods
130+
console_log::setup_console(context_scope);
131+
128132
let global = context.global(context_scope);
129133

130134
// Serialize RequestInfo to JSON - this is the exact same JSON sent to proc

tests/json_parity.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,42 @@ async fn test_response_parity() {
113113
}
114114
}
115115
}
116+
117+
#[tokio::test]
118+
async fn test_console_api() {
119+
// Test that console API methods work without throwing errors.
120+
// The console output is visible in test output when run with RUST_LOG=debug,
121+
// which provides visual confirmation that the console API is working correctly.
122+
// We don't attempt to capture/assert on logs because the global tracing subscriber
123+
// is already initialized by tests/common/logging.rs, making log capture unreliable.
124+
125+
let js_engine = V8JsRuleEngine::new(
126+
r#"
127+
// Test all console methods
128+
console.debug("Test debug");
129+
console.log("Test log");
130+
console.info("Test info");
131+
console.warn("Test warn");
132+
console.error("Test error");
133+
134+
// Test object/array formatting
135+
console.log("Object:", {foo: "bar"});
136+
console.log("Array:", [1, 2, 3]);
137+
138+
// Test multiple arguments
139+
console.log("Multiple", "arguments", 123);
140+
141+
true
142+
"#
143+
.to_string(),
144+
)
145+
.unwrap();
146+
147+
let result = js_engine
148+
.evaluate(Method::GET, "https://example.com", "127.0.0.1")
149+
.await;
150+
151+
// Should allow since the expression returns true
152+
// If console methods threw errors, the rule would fail
153+
assert!(matches!(result.action, Action::Allow));
154+
}

0 commit comments

Comments
 (0)