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