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