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.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
196fn 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
224fn 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}