1use 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
296fn 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
324fn 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}