11use super :: { EvaluationResult , RuleEngineTrait } ;
22use hyper:: Method ;
3- use std:: process :: Command ;
3+ use std:: time :: Duration ;
44use tracing:: { debug, info, warn} ;
55use url:: Url ;
66
@@ -32,51 +32,106 @@ impl ScriptRuleEngine {
3232 method, url, host, path
3333 ) ;
3434
35- let output = if self . script . contains ( ' ' ) {
36- let shell = std:: env:: var ( "SHELL" ) . unwrap_or_else ( |_| "/bin/sh" . to_string ( ) ) ;
37- Command :: new ( & shell)
38- . arg ( "-c" )
39- . arg ( & self . script )
40- . env ( "HTTPJAIL_URL" , url)
41- . env ( "HTTPJAIL_METHOD" , method. as_str ( ) )
42- . env ( "HTTPJAIL_SCHEME" , scheme)
43- . env ( "HTTPJAIL_HOST" , host)
44- . env ( "HTTPJAIL_PATH" , path)
45- . output ( )
46- } else {
47- Command :: new ( & self . script )
48- . env ( "HTTPJAIL_URL" , url)
49- . env ( "HTTPJAIL_METHOD" , method. as_str ( ) )
50- . env ( "HTTPJAIL_SCHEME" , scheme)
51- . env ( "HTTPJAIL_HOST" , host)
52- . env ( "HTTPJAIL_PATH" , path)
53- . output ( )
54- } ;
55-
56- match output {
57- Ok ( output) => {
58- let stdout = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
59- let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
60-
61- if !stderr. is_empty ( ) {
62- debug ! ( "Script stderr: {}" , stderr) ;
35+ // Use tokio runtime to execute async command with timeout
36+ let script_clone = self . script . clone ( ) ;
37+ let method_str = method. as_str ( ) . to_string ( ) ;
38+ let url_str = url. to_string ( ) ;
39+ let scheme_str = scheme. to_string ( ) ;
40+ let host_str = host. to_string ( ) ;
41+ let path_str = path. to_string ( ) ;
42+
43+ // Use spawn_blocking to avoid blocking the async runtime
44+ // This is safe since execute_script is called from a sync context in evaluate()
45+ let result = std:: thread:: spawn ( move || {
46+ use std:: process:: { Command , Stdio } ;
47+ use std:: time:: Instant ;
48+
49+ let start = Instant :: now ( ) ;
50+ let timeout = Duration :: from_secs ( 5 ) ;
51+
52+ let mut cmd = if script_clone. contains ( ' ' ) {
53+ let shell = std:: env:: var ( "SHELL" ) . unwrap_or_else ( |_| "/bin/sh" . to_string ( ) ) ;
54+ let mut cmd = Command :: new ( & shell) ;
55+ cmd. arg ( "-c" ) . arg ( & script_clone) ;
56+ cmd
57+ } else {
58+ Command :: new ( & script_clone)
59+ } ;
60+
61+ let mut child = match cmd
62+ . env ( "HTTPJAIL_URL" , & url_str)
63+ . env ( "HTTPJAIL_METHOD" , & method_str)
64+ . env ( "HTTPJAIL_SCHEME" , & scheme_str)
65+ . env ( "HTTPJAIL_HOST" , & host_str)
66+ . env ( "HTTPJAIL_PATH" , & path_str)
67+ . stdout ( Stdio :: piped ( ) )
68+ . stderr ( Stdio :: piped ( ) )
69+ . spawn ( )
70+ {
71+ Ok ( child) => child,
72+ Err ( e) => {
73+ warn ! ( "Failed to spawn script: {}" , e) ;
74+ return ( false , format ! ( "Script execution failed: {}" , e) ) ;
75+ }
76+ } ;
77+
78+ // Poll for completion with timeout
79+ loop {
80+ match child. try_wait ( ) {
81+ Ok ( Some ( status) ) => {
82+ // Process has exited
83+ let output = child. wait_with_output ( ) . unwrap_or_else ( |e| {
84+ warn ! ( "Failed to read script output: {}" , e) ;
85+ std:: process:: Output {
86+ status,
87+ stdout : Vec :: new ( ) ,
88+ stderr : Vec :: new ( ) ,
89+ }
90+ } ) ;
91+
92+ let stdout = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
93+ let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
94+
95+ if !stderr. is_empty ( ) {
96+ debug ! ( "Script stderr: {}" , stderr) ;
97+ }
98+
99+ let allowed = status. success ( ) ;
100+
101+ debug ! (
102+ "Script returned {} for {} {} (exit code: {:?})" ,
103+ if allowed { "ALLOW" } else { "DENY" } ,
104+ method_str,
105+ url_str,
106+ status. code( )
107+ ) ;
108+
109+ return ( allowed, stdout) ;
110+ }
111+ Ok ( None ) => {
112+ // Still running
113+ if start. elapsed ( ) > timeout {
114+ // Timeout - kill the process
115+ let _ = child. kill ( ) ;
116+ warn ! ( "Script execution timed out after {:?}" , timeout) ;
117+ return ( false , "Script execution timed out" . to_string ( ) ) ;
118+ }
119+ // Sleep briefly before checking again
120+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
121+ }
122+ Err ( e) => {
123+ warn ! ( "Error waiting for script: {}" , e) ;
124+ return ( false , format ! ( "Script execution error: {}" , e) ) ;
125+ }
63126 }
64-
65- let allowed = output. status . success ( ) ;
66-
67- debug ! (
68- "Script returned {} for {} {} (exit code: {:?})" ,
69- if allowed { "ALLOW" } else { "DENY" } ,
70- method,
71- url,
72- output. status. code( )
73- ) ;
74-
75- ( allowed, stdout)
76127 }
77- Err ( e) => {
78- warn ! ( "Failed to execute script: {}" , e) ;
79- ( false , format ! ( "Script execution failed: {}" , e) )
128+ } ) ;
129+
130+ match result. join ( ) {
131+ Ok ( res) => res,
132+ Err ( _) => {
133+ warn ! ( "Script execution thread panicked" ) ;
134+ ( false , "Script execution failed" . to_string ( ) )
80135 }
81136 }
82137 }
@@ -113,7 +168,6 @@ mod tests {
113168 use super :: * ;
114169 use crate :: rules:: Action ;
115170 use std:: fs;
116- use std:: os:: unix:: fs:: PermissionsExt ;
117171 use tempfile:: NamedTempFile ;
118172
119173 #[ test]
@@ -124,9 +178,13 @@ exit 0
124178"# ;
125179 fs:: write ( script_file. path ( ) , script) . unwrap ( ) ;
126180
127- let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
128- perms. set_mode ( 0o755 ) ;
129- fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
181+ #[ cfg( unix) ]
182+ {
183+ use std:: os:: unix:: fs:: PermissionsExt ;
184+ let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
185+ perms. set_mode ( 0o755 ) ;
186+ fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
187+ }
130188
131189 let engine = ScriptRuleEngine :: new ( script_file. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ) ;
132190 let result = engine. evaluate ( Method :: GET , "https://example.com/test" ) ;
@@ -142,9 +200,13 @@ exit 1
142200"# ;
143201 fs:: write ( script_file. path ( ) , script) . unwrap ( ) ;
144202
145- let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
146- perms. set_mode ( 0o755 ) ;
147- fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
203+ #[ cfg( unix) ]
204+ {
205+ use std:: os:: unix:: fs:: PermissionsExt ;
206+ let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
207+ perms. set_mode ( 0o755 ) ;
208+ fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
209+ }
148210
149211 let engine = ScriptRuleEngine :: new ( script_file. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ) ;
150212 let result = engine. evaluate ( Method :: GET , "https://example.com/test" ) ;
@@ -161,9 +223,13 @@ exit 1
161223"# ;
162224 fs:: write ( script_file. path ( ) , script) . unwrap ( ) ;
163225
164- let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
165- perms. set_mode ( 0o755 ) ;
166- fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
226+ #[ cfg( unix) ]
227+ {
228+ use std:: os:: unix:: fs:: PermissionsExt ;
229+ let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
230+ perms. set_mode ( 0o755 ) ;
231+ fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
232+ }
167233
168234 let engine = ScriptRuleEngine :: new ( script_file. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ) ;
169235 let result = engine. evaluate ( Method :: GET , "https://example.com/test" ) ;
185251"# ;
186252 fs:: write ( script_file. path ( ) , script) . unwrap ( ) ;
187253
188- let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
189- perms. set_mode ( 0o755 ) ;
190- fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
254+ #[ cfg( unix) ]
255+ {
256+ use std:: os:: unix:: fs:: PermissionsExt ;
257+ let mut perms = fs:: metadata ( script_file. path ( ) ) . unwrap ( ) . permissions ( ) ;
258+ perms. set_mode ( 0o755 ) ;
259+ fs:: set_permissions ( script_file. path ( ) , perms) . unwrap ( ) ;
260+ }
191261
192262 let engine = ScriptRuleEngine :: new ( script_file. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ) ;
193263
0 commit comments