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