1+ //! V8 JavaScript rule engine implementation.
2+ //!
3+ //! This module provides a rule engine that evaluates HTTP requests using JavaScript
4+ //! code executed via the V8 engine. It supports automatic file reloading when rules
5+ //! are loaded from a file path.
6+
17use crate :: rules:: common:: { RequestInfo , RuleResponse } ;
28use crate :: rules:: console_log;
39use crate :: rules:: { EvaluationResult , RuleEngineTrait } ;
10+ use arc_swap:: ArcSwap ;
411use async_trait:: async_trait;
512use hyper:: Method ;
13+ use std:: path:: PathBuf ;
614use std:: sync:: Arc ;
15+ use std:: time:: SystemTime ;
716use tokio:: sync:: Mutex ;
8- use tracing:: { debug, info, warn} ;
17+ use tracing:: { debug, error , info, warn} ;
918
19+ /// V8-based JavaScript rule engine with automatic file reloading support.
20+ ///
21+ /// The engine uses a lock-free ArcSwap for reading JavaScript code on every request,
22+ /// and employs a singleflight pattern (via Mutex) to prevent concurrent file reloads.
1023pub struct V8JsRuleEngine {
11- js_code : String ,
12- #[ allow( dead_code) ]
13- runtime : Arc < Mutex < ( ) > > , // Placeholder for V8 runtime management
24+ /// JavaScript code and its last modified time (lock-free atomic updates)
25+ js_code : ArcSwap < ( String , Option < SystemTime > ) > ,
26+ /// Optional file path for automatic reloading
27+ js_file_path : Option < PathBuf > ,
28+ /// Lock to prevent concurrent file reloads (singleflight pattern)
29+ reload_lock : Arc < Mutex < ( ) > > ,
1430}
1531
1632impl V8JsRuleEngine {
1733 pub fn new ( js_code : String ) -> Result < Self , Box < dyn std:: error:: Error > > {
34+ Self :: new_with_file ( js_code, None )
35+ }
36+
37+ pub fn new_with_file (
38+ js_code : String ,
39+ js_file_path : Option < PathBuf > ,
40+ ) -> Result < Self , Box < dyn std:: error:: Error > > {
1841 // Initialize V8 platform once and keep it alive for the lifetime of the program
1942 use std:: sync:: OnceLock ;
2043 static V8_PLATFORM : OnceLock < v8:: SharedRef < v8:: Platform > > = OnceLock :: new ( ) ;
@@ -27,27 +50,45 @@ impl V8JsRuleEngine {
2750 } ) ;
2851
2952 // Compile the JavaScript to check for syntax errors
30- {
31- let mut isolate = v8:: Isolate :: new ( v8:: CreateParams :: default ( ) ) ;
32- let handle_scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
33- let context = v8:: Context :: new ( handle_scope, Default :: default ( ) ) ;
34- let context_scope = & mut v8:: ContextScope :: new ( handle_scope, context) ;
53+ Self :: validate_js_code ( & js_code) ?;
54+
55+ // Get initial mtime if file path is provided
56+ let initial_mtime = js_file_path
57+ . as_ref ( )
58+ . and_then ( |path| std:: fs:: metadata ( path) . ok ( ) . and_then ( |m| m. modified ( ) . ok ( ) ) ) ;
3559
36- let source =
37- v8:: String :: new ( context_scope, & js_code) . ok_or ( "Failed to create V8 string" ) ?;
60+ let js_code_swap = ArcSwap :: from ( Arc :: new ( ( js_code, initial_mtime) ) ) ;
3861
39- v8 :: Script :: compile ( context_scope , source , None )
40- . ok_or ( "Failed to compile JavaScript expression" ) ? ;
62+ if js_file_path . is_some ( ) {
63+ info ! ( "File watching enabled for JS rules - will check for changes on each request" ) ;
4164 }
4265
4366 info ! ( "V8 JavaScript rule engine initialized" ) ;
4467 Ok ( Self {
45- js_code,
46- runtime : Arc :: new ( Mutex :: new ( ( ) ) ) ,
68+ js_code : js_code_swap,
69+ js_file_path,
70+ reload_lock : Arc :: new ( Mutex :: new ( ( ) ) ) ,
4771 } )
4872 }
4973
50- pub fn execute (
74+ /// Validate JavaScript code by compiling it with V8
75+ fn validate_js_code ( js_code : & str ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
76+ let mut isolate = v8:: Isolate :: new ( v8:: CreateParams :: default ( ) ) ;
77+ let handle_scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
78+ let context = v8:: Context :: new ( handle_scope, Default :: default ( ) ) ;
79+ let context_scope = & mut v8:: ContextScope :: new ( handle_scope, context) ;
80+
81+ let source = v8:: String :: new ( context_scope, js_code) . ok_or ( "Failed to create V8 string" ) ?;
82+
83+ v8:: Script :: compile ( context_scope, source, None )
84+ . ok_or ( "Failed to compile JavaScript expression" ) ?;
85+
86+ Ok ( ( ) )
87+ }
88+
89+ /// Execute JavaScript rules against a request (public API).
90+ /// For internal use, prefer calling `evaluate()` via the RuleEngineTrait.
91+ pub async fn execute (
5192 & self ,
5293 method : & Method ,
5394 url : & str ,
@@ -61,7 +102,11 @@ impl V8JsRuleEngine {
61102 }
62103 } ;
63104
64- match self . create_and_execute ( & request_info) {
105+ // Load the current JS code (lock-free)
106+ let code_and_mtime = self . js_code . load ( ) ;
107+ let ( js_code, _) = & * * code_and_mtime;
108+
109+ match Self :: execute_with_code ( js_code, & request_info) {
65110 Ok ( result) => result,
66111 Err ( e) => {
67112 warn ! ( "JavaScript execution failed: {}" , e) ;
@@ -196,57 +241,152 @@ impl V8JsRuleEngine {
196241 Ok ( ( allowed, message, max_tx_bytes) )
197242 }
198243
244+ /// Execute JavaScript code with a given code string (can be called from blocking context)
199245 #[ allow( clippy:: type_complexity) ]
200- fn create_and_execute (
201- & self ,
246+ fn execute_with_code (
247+ js_code : & str ,
202248 request_info : & RequestInfo ,
203249 ) -> Result < ( bool , Option < String > , Option < u64 > ) , Box < dyn std:: error:: Error > > {
204250 // Create a new isolate for each execution (simpler approach)
205251 let mut isolate = v8:: Isolate :: new ( v8:: CreateParams :: default ( ) ) ;
206- Self :: execute_with_isolate ( & mut isolate, & self . js_code , request_info)
252+ Self :: execute_with_isolate ( & mut isolate, js_code, request_info)
207253 }
208- }
209254
210- #[ async_trait]
211- impl RuleEngineTrait for V8JsRuleEngine {
212- async fn evaluate ( & self , method : Method , url : & str , requester_ip : & str ) -> EvaluationResult {
213- // Run the JavaScript evaluation in a blocking task to avoid
214- // issues with V8's single-threaded nature
255+ /// Check if the JS file has changed and reload if necessary.
256+ /// Uses double-check locking pattern to prevent concurrent reloads.
257+ async fn check_and_reload_file ( & self ) {
258+ let Some ( ref path) = self . js_file_path else {
259+ return ;
260+ } ;
261+
262+ let current_mtime = std:: fs:: metadata ( path) . ok ( ) . and_then ( |m| m. modified ( ) . ok ( ) ) ;
263+
264+ // Fast path: check if reload needed (no lock)
265+ let code_and_mtime = self . js_code . load ( ) ;
266+ let ( _, last_mtime) = & * * code_and_mtime;
267+
268+ if current_mtime != * last_mtime && current_mtime. is_some ( ) {
269+ // Slow path: acquire lock to prevent concurrent reloads (singleflight)
270+ let _guard = self . reload_lock . lock ( ) . await ;
271+
272+ // Double-check: file might have been reloaded while waiting for lock
273+ let code_and_mtime = self . js_code . load ( ) ;
274+ let ( _, last_mtime) = & * * code_and_mtime;
275+
276+ if current_mtime != * last_mtime && current_mtime. is_some ( ) {
277+ info ! ( "Detected change in JS rules file: {:?}" , path) ;
278+
279+ // Re-read and validate the file
280+ match std:: fs:: read_to_string ( path) {
281+ Ok ( new_code) => {
282+ // Validate the new code before reloading
283+ if let Err ( e) = Self :: validate_js_code ( & new_code) {
284+ error ! (
285+ "Failed to validate updated JS code: {}. Keeping existing rules." ,
286+ e
287+ ) ;
288+ } else {
289+ // Update the code and mtime atomically (lock-free swap)
290+ self . js_code . store ( Arc :: new ( ( new_code, current_mtime) ) ) ;
291+ info ! ( "Successfully reloaded JS rules from file" ) ;
292+ }
293+ }
294+ Err ( e) => {
295+ error ! (
296+ "Failed to read updated JS file: {}. Keeping existing rules." ,
297+ e
298+ ) ;
299+ }
300+ }
301+ }
302+ }
303+ }
304+
305+ /// Load the current JS code from the ArcSwap (lock-free operation).
306+ fn load_js_code ( & self ) -> String {
307+ let code_and_mtime = self . js_code . load ( ) ;
308+ let ( js_code, _) = & * * code_and_mtime;
309+ js_code. clone ( )
310+ }
311+
312+ /// Execute JavaScript in a blocking task to handle V8's single-threaded nature.
313+ /// Returns (allowed, context, max_tx_bytes).
314+ async fn execute_js_blocking (
315+ js_code : String ,
316+ method : Method ,
317+ url : & str ,
318+ requester_ip : & str ,
319+ ) -> ( bool , Option < String > , Option < u64 > ) {
215320 let method_clone = method. clone ( ) ;
216321 let url_clone = url. to_string ( ) ;
217322 let ip_clone = requester_ip. to_string ( ) ;
218323
219- // Clone self to move into the closure
220- let self_clone = Self {
221- js_code : self . js_code . clone ( ) ,
222- runtime : self . runtime . clone ( ) ,
223- } ;
324+ tokio:: task:: spawn_blocking ( move || {
325+ let request_info = match RequestInfo :: from_request ( & method_clone, & url_clone, & ip_clone)
326+ {
327+ Ok ( info) => info,
328+ Err ( e) => {
329+ warn ! ( "Failed to parse request info: {}" , e) ;
330+ return ( false , Some ( "Invalid request format" . to_string ( ) ) , None ) ;
331+ }
332+ } ;
224333
225- let ( allowed, context, max_tx_bytes) = tokio:: task:: spawn_blocking ( move || {
226- self_clone. execute ( & method_clone, & url_clone, & ip_clone)
334+ match Self :: execute_with_code ( & js_code, & request_info) {
335+ Ok ( result) => result,
336+ Err ( e) => {
337+ warn ! ( "JavaScript execution failed: {}" , e) ;
338+ ( false , Some ( "JavaScript execution failed" . to_string ( ) ) , None )
339+ }
340+ }
227341 } )
228342 . await
229343 . unwrap_or_else ( |e| {
230344 warn ! ( "Failed to spawn V8 evaluation task: {}" , e) ;
231345 ( false , Some ( "Evaluation failed" . to_string ( ) ) , None )
232- } ) ;
346+ } )
347+ }
348+
349+ /// Build an EvaluationResult from the execution outcome.
350+ fn build_evaluation_result (
351+ allowed : bool ,
352+ context : Option < String > ,
353+ max_tx_bytes : Option < u64 > ,
354+ ) -> EvaluationResult {
355+ let mut result = if allowed {
356+ EvaluationResult :: allow ( )
357+ } else {
358+ EvaluationResult :: deny ( )
359+ } ;
360+
361+ if let Some ( ctx) = context {
362+ result = result. with_context ( ctx) ;
363+ }
233364
234365 if allowed {
235- let mut result = EvaluationResult :: allow ( ) ;
236- if let Some ( ctx) = context {
237- result = result. with_context ( ctx) ;
238- }
239366 if let Some ( bytes) = max_tx_bytes {
240367 result = result. with_max_tx_bytes ( bytes) ;
241368 }
242- result
243- } else {
244- let mut result = EvaluationResult :: deny ( ) ;
245- if let Some ( ctx) = context {
246- result = result. with_context ( ctx) ;
247- }
248- result
249369 }
370+
371+ result
372+ }
373+ }
374+
375+ #[ async_trait]
376+ impl RuleEngineTrait for V8JsRuleEngine {
377+ async fn evaluate ( & self , method : Method , url : & str , requester_ip : & str ) -> EvaluationResult {
378+ // Check if file has changed and reload if necessary
379+ self . check_and_reload_file ( ) . await ;
380+
381+ // Load the current JS code (lock-free operation)
382+ let js_code = self . load_js_code ( ) ;
383+
384+ // Execute JavaScript in blocking task
385+ let ( allowed, context, max_tx_bytes) =
386+ Self :: execute_js_blocking ( js_code, method, url, requester_ip) . await ;
387+
388+ // Build and return the result
389+ Self :: build_evaluation_result ( allowed, context, max_tx_bytes)
250390 }
251391
252392 fn name ( & self ) -> & str {
0 commit comments