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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}
, '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
66 changes: 66 additions & 0 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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));
}