Skip to main content

rns_ctl/cmd/
hook.rs

1//! Hook management subcommands.
2//!
3//! Connects to a running rns-ctl HTTP server to list, load, and unload hooks.
4
5use crate::args::Args;
6
7const DEFAULT_HOOK_TYPE: &str = {
8    #[cfg(feature = "rns-hooks-native")]
9    {
10        "native"
11    }
12    #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
13    {
14        "wasm"
15    }
16    #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
17    {
18        "wasm"
19    }
20};
21
22pub fn run(args: Args) {
23    if args.has("help") {
24        print_usage();
25        return;
26    }
27
28    let base_url = args
29        .get("url")
30        .unwrap_or("http://127.0.0.1:8080")
31        .to_string();
32    let token = args
33        .get("token")
34        .or_else(|| args.get("t"))
35        .map(|s| s.to_string());
36
37    match args.positional.first().map(|s| s.as_str()) {
38        Some("list") => do_list(&base_url, token.as_deref()),
39        Some("load") => do_load(&args, &base_url, token.as_deref()),
40        Some("unload") => do_unload(&args, &base_url, token.as_deref()),
41        Some("reload") => do_reload(&args, &base_url, token.as_deref()),
42        Some("enable") => do_set_enabled(&args, &base_url, token.as_deref(), true),
43        Some("disable") => do_set_enabled(&args, &base_url, token.as_deref(), false),
44        Some("set-priority") => do_set_priority(&args, &base_url, token.as_deref()),
45        _ => print_usage(),
46    }
47}
48
49fn do_list(base_url: &str, token: Option<&str>) {
50    let url = format!("{}/api/hooks", base_url);
51    match simple_get(&url, token) {
52        Ok(body) => match serde_json::from_str::<serde_json::Value>(&body) {
53            Ok(val) => {
54                if let Some(hooks) = val["hooks"].as_array() {
55                    if hooks.is_empty() {
56                        println!("No hooks loaded");
57                        return;
58                    }
59                    println!(
60                        "{:<20} {:<8} {:<28} {:>8} {:>8} {:>6}",
61                        "Name", "Type", "Attach Point", "Priority", "Traps", "On"
62                    );
63                    println!("{}", "-".repeat(83));
64                    for h in hooks {
65                        println!(
66                            "{:<20} {:<8} {:<28} {:>8} {:>8} {:>6}",
67                            h["name"].as_str().unwrap_or(""),
68                            h["type"].as_str().unwrap_or(DEFAULT_HOOK_TYPE),
69                            h["attach_point"].as_str().unwrap_or(""),
70                            h["priority"].as_i64().unwrap_or(0),
71                            h["consecutive_traps"].as_u64().unwrap_or(0),
72                            if h["enabled"].as_bool().unwrap_or(false) {
73                                "yes"
74                            } else {
75                                "no"
76                            },
77                        );
78                    }
79                } else {
80                    println!("{}", body);
81                }
82            }
83            Err(_) => println!("{}", body),
84        },
85        Err(e) => {
86            eprintln!("Error: {}", e);
87            std::process::exit(1);
88        }
89    }
90}
91
92fn do_load(args: &Args, base_url: &str, token: Option<&str>) {
93    let path = match args.positional.get(1) {
94        Some(p) => p,
95        None => {
96            eprintln!("Missing hook file path");
97            print_usage();
98            std::process::exit(1);
99        }
100    };
101    let attach_point = match args.get("point") {
102        Some(p) => p.to_string(),
103        None => {
104            eprintln!("Missing --point <HookPoint>");
105            print_usage();
106            std::process::exit(1);
107        }
108    };
109    let priority: i32 = args
110        .get("priority")
111        .and_then(|s| s.parse().ok())
112        .unwrap_or(0);
113    let hook_type = args.get("type").unwrap_or(DEFAULT_HOOK_TYPE).to_string();
114    let name = args.get("name").map(|s| s.to_string()).unwrap_or_else(|| {
115        std::path::Path::new(path)
116            .file_stem()
117            .and_then(|s| s.to_str())
118            .unwrap_or("hook")
119            .to_string()
120    });
121
122    let body = serde_json::json!({
123        "name": name,
124        "path": path,
125        "type": hook_type,
126        "builtin_id": args.get("builtin").or_else(|| args.get("id")),
127        "attach_point": attach_point,
128        "priority": priority,
129    });
130
131    let url = format!("{}/api/hook/load", base_url);
132    match simple_post(&url, &body.to_string(), token) {
133        Ok(resp) => println!("{}", resp),
134        Err(e) => {
135            eprintln!("Error: {}", e);
136            std::process::exit(1);
137        }
138    }
139}
140
141fn do_unload(args: &Args, base_url: &str, token: Option<&str>) {
142    let name = match args.positional.get(1) {
143        Some(n) => n,
144        None => {
145            eprintln!("Missing hook name");
146            print_usage();
147            std::process::exit(1);
148        }
149    };
150    let attach_point = match args.get("point") {
151        Some(p) => p.to_string(),
152        None => {
153            eprintln!("Missing --point <HookPoint>");
154            print_usage();
155            std::process::exit(1);
156        }
157    };
158
159    let body = serde_json::json!({
160        "name": name,
161        "attach_point": attach_point,
162    });
163
164    let url = format!("{}/api/hook/unload", base_url);
165    match simple_post(&url, &body.to_string(), token) {
166        Ok(resp) => println!("{}", resp),
167        Err(e) => {
168            eprintln!("Error: {}", e);
169            std::process::exit(1);
170        }
171    }
172}
173
174fn do_reload(args: &Args, base_url: &str, token: Option<&str>) {
175    let name = match args.positional.get(1) {
176        Some(n) => n,
177        None => {
178            eprintln!("Missing hook name");
179            print_usage();
180            std::process::exit(1);
181        }
182    };
183    let attach_point = match args.get("point") {
184        Some(p) => p.to_string(),
185        None => {
186            eprintln!("Missing --point <HookPoint>");
187            print_usage();
188            std::process::exit(1);
189        }
190    };
191    let path = match args.get("path") {
192        Some(p) => p.to_string(),
193        None => {
194            eprintln!("Missing --path <hook_file>");
195            print_usage();
196            std::process::exit(1);
197        }
198    };
199    let hook_type = args.get("type").unwrap_or(DEFAULT_HOOK_TYPE).to_string();
200
201    let body = serde_json::json!({
202        "name": name,
203        "path": path,
204        "type": hook_type,
205        "builtin_id": args.get("builtin").or_else(|| args.get("id")),
206        "attach_point": attach_point,
207    });
208
209    let url = format!("{}/api/hook/reload", base_url);
210    match simple_post(&url, &body.to_string(), token) {
211        Ok(resp) => println!("{}", resp),
212        Err(e) => {
213            eprintln!("Error: {}", e);
214            std::process::exit(1);
215        }
216    }
217}
218
219fn do_set_enabled(args: &Args, base_url: &str, token: Option<&str>, enabled: bool) {
220    let name = match args.positional.get(1) {
221        Some(n) => n,
222        None => {
223            eprintln!("Missing hook name");
224            print_usage();
225            std::process::exit(1);
226        }
227    };
228    let attach_point = match args.get("point") {
229        Some(p) => p.to_string(),
230        None => {
231            eprintln!("Missing --point <HookPoint>");
232            print_usage();
233            std::process::exit(1);
234        }
235    };
236
237    let body = serde_json::json!({
238        "name": name,
239        "attach_point": attach_point,
240    });
241    let url = format!(
242        "{}/api/hook/{}",
243        base_url,
244        if enabled { "enable" } else { "disable" }
245    );
246    match simple_post(&url, &body.to_string(), token) {
247        Ok(resp) => println!("{}", resp),
248        Err(e) => {
249            eprintln!("Error: {}", e);
250            std::process::exit(1);
251        }
252    }
253}
254
255fn do_set_priority(args: &Args, base_url: &str, token: Option<&str>) {
256    let name = match args.positional.get(1) {
257        Some(n) => n,
258        None => {
259            eprintln!("Missing hook name");
260            print_usage();
261            std::process::exit(1);
262        }
263    };
264    let attach_point = match args.get("point") {
265        Some(p) => p.to_string(),
266        None => {
267            eprintln!("Missing --point <HookPoint>");
268            print_usage();
269            std::process::exit(1);
270        }
271    };
272    let priority: i32 = match args.get("priority").and_then(|s| s.parse().ok()) {
273        Some(priority) => priority,
274        None => {
275            eprintln!("Missing --priority <N>");
276            print_usage();
277            std::process::exit(1);
278        }
279    };
280
281    let body = serde_json::json!({
282        "name": name,
283        "attach_point": attach_point,
284        "priority": priority,
285    });
286    let url = format!("{}/api/hook/priority", base_url);
287    match simple_post(&url, &body.to_string(), token) {
288        Ok(resp) => println!("{}", resp),
289        Err(e) => {
290            eprintln!("Error: {}", e);
291            std::process::exit(1);
292        }
293    }
294}
295
296/// Simple HTTP GET using std::net::TcpStream (no external HTTP client dependency).
297fn simple_get(url: &str, token: Option<&str>) -> Result<String, String> {
298    let (host, port, path) = parse_url(url)?;
299    let addr = format!("{}:{}", host, port);
300    let mut stream =
301        std::net::TcpStream::connect(&addr).map_err(|e| format!("connect to {}: {}", addr, e))?;
302
303    use std::io::{Read, Write};
304    let auth = match token {
305        Some(t) => format!("Authorization: Bearer {}\r\n", t),
306        None => String::new(),
307    };
308    let request = format!(
309        "GET {} HTTP/1.1\r\nHost: {}\r\n{}Connection: close\r\n\r\n",
310        path, host, auth
311    );
312    stream
313        .write_all(request.as_bytes())
314        .map_err(|e| format!("write: {}", e))?;
315
316    let mut response = String::new();
317    stream
318        .read_to_string(&mut response)
319        .map_err(|e| format!("read: {}", e))?;
320
321    extract_body(&response)
322}
323
324/// Simple HTTP POST using std::net::TcpStream.
325fn simple_post(url: &str, body: &str, token: Option<&str>) -> Result<String, String> {
326    let (host, port, path) = parse_url(url)?;
327    let addr = format!("{}:{}", host, port);
328    let mut stream =
329        std::net::TcpStream::connect(&addr).map_err(|e| format!("connect to {}: {}", addr, e))?;
330
331    use std::io::{Read, Write};
332    let auth = match token {
333        Some(t) => format!("Authorization: Bearer {}\r\n", t),
334        None => String::new(),
335    };
336    let request = format!(
337        "POST {} HTTP/1.1\r\nHost: {}\r\n{}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
338        path, host, auth, body.len(), body
339    );
340    stream
341        .write_all(request.as_bytes())
342        .map_err(|e| format!("write: {}", e))?;
343
344    let mut response = String::new();
345    stream
346        .read_to_string(&mut response)
347        .map_err(|e| format!("read: {}", e))?;
348
349    extract_body(&response)
350}
351
352fn parse_url(url: &str) -> Result<(String, u16, String), String> {
353    let url = url.strip_prefix("http://").unwrap_or(url);
354    let (hostport, path) = match url.find('/') {
355        Some(i) => (&url[..i], &url[i..]),
356        None => (url, "/"),
357    };
358    let (host, port) = match hostport.rfind(':') {
359        Some(i) => (
360            &hostport[..i],
361            hostport[i + 1..]
362                .parse::<u16>()
363                .map_err(|_| "invalid port".to_string())?,
364        ),
365        None => (hostport, 80),
366    };
367    Ok((host.to_string(), port, path.to_string()))
368}
369
370fn extract_body(response: &str) -> Result<String, String> {
371    match response.find("\r\n\r\n") {
372        Some(i) => Ok(response[i + 4..].to_string()),
373        None => Ok(response.to_string()),
374    }
375}
376
377fn print_usage() {
378    println!("Usage: rns-ctl hook <COMMAND> [OPTIONS]");
379    println!();
380    println!("COMMANDS:");
381    println!("    list                               List loaded hooks");
382    println!("    load <path> --point <HookPoint>     Load a hook");
383    println!("         [--type wasm|native|builtin] [--priority N] [--name name]");
384    println!("         [--builtin ID]              Built-in hook ID (defaults to <path>)");
385    println!("    unload <name> --point <HookPoint>   Unload a hook");
386    println!("    reload <name> --point <HookPoint>   Reload a hook");
387    println!("         --path <hook_file_or_builtin_id> [--type wasm|native|builtin]");
388    println!("    enable <name> --point <HookPoint>   Enable a loaded hook");
389    println!("    disable <name> --point <HookPoint>  Disable a loaded hook");
390    println!("    set-priority <name> --point <HookPoint> --priority N");
391    println!();
392    println!("OPTIONS:");
393    println!("    --url URL          HTTP server URL (default: http://127.0.0.1:8080)");
394    println!("    --token TOKEN, -t  Bearer auth token (printed by rns-ctl http on start)");
395    println!();
396    println!("HOOK POINTS:");
397    println!("    PreIngress, PreDispatch, AnnounceReceived, PathUpdated,");
398    println!("    AnnounceRetransmit, LinkRequestReceived, LinkEstablished,");
399    println!("    LinkClosed, InterfaceUp, InterfaceDown, InterfaceConfigChanged,");
400    println!("    SendOnInterface, BroadcastOnAllInterfaces, DeliverLocal,");
401    println!("    TunnelSynthesize, Tick");
402}