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 _ => 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
194fn 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
222fn 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}