1use 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
274fn 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
302fn 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}