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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,72 @@ const requestString = `${r.method} ${r.host}${r.path}`;
patterns.some(pattern => pattern.test(requestString))
```

## Debugging with Console API

JavaScript rules support the full console API for debugging. Each method maps to a corresponding tracing level:

| Console Method | Tracing Level | Use Case |
|----------------|---------------|----------|
| `console.debug()` | DEBUG | Detailed troubleshooting information |
| `console.log()` | INFO | General informational messages |
| `console.info()` | INFO | Informational messages (e.g., allowed requests) |
| `console.warn()` | WARN | Warning messages (e.g., suspicious patterns) |
| `console.error()` | ERROR | Error messages (e.g., blocked threats) |

### Example

```javascript
// Debug: detailed information
console.debug("Evaluating request:", r.method, r.url);
console.debug("Full request:", r);

// Info: general messages
console.info("Allowing trusted domain:", r.host);

// Warn: suspicious patterns
console.warn("Suspicious path detected:", r.path);

// Error: security issues
console.error("Blocked malicious request:", r.url);
```

### Viewing Console Output

Set `RUST_LOG` to control which messages appear:

```bash
# Show debug and above (debug, info, warn, error) - all console output
RUST_LOG=debug httpjail --js-file rules.js -- command

# Show info and above (info, warn, error) - recommended for production
# Includes console.log(), console.info(), console.warn(), console.error()
RUST_LOG=info httpjail --js-file rules.js -- command

# Show only warnings and errors
RUST_LOG=warn httpjail --js-file rules.js -- command
```

Example output with color coding:

```
DEBUG httpjail::rules::js: Evaluating request: GET https://api.github.com/users
INFO httpjail::rules::js: Allowing trusted domain: api.github.com
WARN httpjail::rules::js: Suspicious path detected: /admin
ERROR httpjail::rules::js: Blocked malicious request: https://evil.com/exploit
```

### Objects and Arrays

Objects and arrays are automatically JSON-stringified:

```javascript
console.log("Request:", r);
// Output: Request: {"url":"https://...","method":"GET",...}

console.log("Complex:", {hosts: ["a.com", "b.com"], count: 42});
// Output: Complex: {"hosts":["a.com","b.com"],"count":42}
```

## When to Use

Best for:
Expand Down
33 changes: 33 additions & 0 deletions examples/console_log_demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Example JavaScript rule file demonstrating console API usage
// This can be used with: httpjail --js-file examples/console_log_demo.js ...
//
// Console output is visible when running with appropriate log levels:
// RUST_LOG=debug httpjail --js-file examples/console_log_demo.js ... # Shows debug/log
// RUST_LOG=info httpjail --js-file examples/console_log_demo.js ... # Shows info/warn/error
// RUST_LOG=warn httpjail --js-file examples/console_log_demo.js ... # Shows warn/error

// Different console methods map to tracing levels:
// console.debug() -> DEBUG
// console.log() -> INFO
// console.info() -> INFO
// console.warn() -> WARN
// console.error() -> ERROR

// Debug: detailed information for troubleshooting
console.debug("Evaluating request:", r.method, r.url);
console.debug("Full request object:", r);

// Log: general informational messages
console.log("Requester IP:", r.requester_ip);

// Example: Allow only GET requests to example.com
if (r.method === "GET" && r.url.includes("example.com")) {
console.info("Allowing request to example.com");
true
} else if (r.url.includes("suspicious-site.com")) {
console.error("Blocked suspicious site:", r.url);
({deny_message: "Blocked: suspicious site"})
} else {
console.warn("Denying request - not example.com or not GET");
({deny_message: "Only GET requests to example.com are allowed"})
}
1 change: 1 addition & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod common;
mod console_log;
pub mod proc;
pub mod shell;
pub mod v8_js;
Expand Down
100 changes: 100 additions & 0 deletions src/rules/console_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use tracing::{debug, info, warn};

/// Convert a V8 value to a string, using JSON.stringify for objects
fn v8_value_to_string(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> String {
// For objects and arrays, try JSON.stringify
if value.is_object() && !value.is_null() && !value.is_undefined() {
if let Some(json_str) = try_json_stringify(scope, value) {
return json_str;
}
}

// Fallback to toString() for all types
value
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.unwrap_or_else(|| "[value]".to_string())
}

/// Try to JSON.stringify a value, returning None if it fails
fn try_json_stringify(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> Option<String> {
let global = scope.get_current_context().global(scope);
let json_key = v8::String::new(scope, "JSON")?;
let stringify_key = v8::String::new(scope, "stringify")?;

let json_obj = global.get(scope, json_key.into())?.to_object(scope)?;
let stringify_fn = json_obj.get(scope, stringify_key.into())?;
let stringify_fn = v8::Local::<v8::Function>::try_from(stringify_fn).ok()?;

let result = stringify_fn.call(scope, json_obj.into(), &[value])?;
result
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
}

/// Format console arguments into a single string
fn format_console_args(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments) -> String {
let mut log_parts = Vec::new();
for i in 0..args.length() {
let arg = args.get(i);
log_parts.push(v8_value_to_string(scope, arg));
}
log_parts.join(" ")
}

/// Generic console callback that logs at a specific level
fn console_callback(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue,
log_fn: fn(&str),
) {
let message = format_console_args(scope, args);
log_fn(&message);
}

/// Macro to generate console method callbacks for each log level
macro_rules! console_method {
($name:ident, $log_macro:path) => {
fn $name(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
retval: v8::ReturnValue,
) {
console_callback(scope, args, retval, |msg| {
$log_macro!(target: "httpjail::rules::js", "{}", msg)
});
}
};
}

// Generate console.debug, console.log, console.info, console.warn, console.error
console_method!(console_debug, debug);
console_method!(console_log, info);
console_method!(console_info, info);
console_method!(console_warn, warn);
console_method!(console_error, tracing::error);

/// Set up console object with debug, log, info, warn, error methods
pub fn setup_console(context_scope: &mut v8::ContextScope<v8::HandleScope>) {
let global = context_scope.get_current_context().global(context_scope);
let console_obj = v8::Object::new(context_scope);

// Register each console method
macro_rules! add_console_method {
($name:expr, $callback:expr) => {
let key = v8::String::new(context_scope, $name).unwrap();
let func = v8::Function::new(context_scope, $callback).unwrap();
console_obj.set(context_scope, key.into(), func.into());
};
}

add_console_method!("debug", console_debug);
add_console_method!("log", console_log);
add_console_method!("info", console_info);
add_console_method!("warn", console_warn);
add_console_method!("error", console_error);

let console_key = v8::String::new(context_scope, "console").unwrap();
global.set(context_scope, console_key.into(), console_obj.into());
}
4 changes: 4 additions & 0 deletions src/rules/v8_js.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::rules::common::{RequestInfo, RuleResponse};
use crate::rules::console_log;
use crate::rules::{EvaluationResult, RuleEngineTrait};
use async_trait::async_trait;
use hyper::Method;
Expand Down Expand Up @@ -125,6 +126,9 @@ impl V8JsRuleEngine {
let context = v8::Context::new(handle_scope, Default::default());
let context_scope = &mut v8::ContextScope::new(handle_scope, context);

// Set up console object with debug, log, info, warn, error methods
console_log::setup_console(context_scope);

let global = context.global(context_scope);

// Serialize RequestInfo to JSON - this is the exact same JSON sent to proc
Expand Down
39 changes: 39 additions & 0 deletions tests/json_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,42 @@ async fn test_response_parity() {
}
}
}

#[tokio::test]
async fn test_console_api() {
// Test that console API methods work without throwing errors.
// The console output is visible in test output when run with RUST_LOG=debug,
// which provides visual confirmation that the console API is working correctly.
// We don't attempt to capture/assert on logs because the global tracing subscriber
// is already initialized by tests/common/logging.rs, making log capture unreliable.

let js_engine = V8JsRuleEngine::new(
r#"
// Test all console methods
console.debug("Test debug");
console.log("Test log");
console.info("Test info");
console.warn("Test warn");
console.error("Test error");

// Test object/array formatting
console.log("Object:", {foo: "bar"});
console.log("Array:", [1, 2, 3]);

// Test multiple arguments
console.log("Multiple", "arguments", 123);

true
"#
.to_string(),
)
.unwrap();

let result = js_engine
.evaluate(Method::GET, "https://example.com", "127.0.0.1")
.await;

// Should allow since the expression returns true
// If console methods threw errors, the rule would fail
assert!(matches!(result.action, Action::Allow));
}