1use std::collections::BTreeMap;
2use std::process::{Command, Stdio};
3
4use crate::error::Result;
5use crate::models::{
6 MSFHostCheckResult, MSFHostInfo, MSFHostsResult, MSFLoginResult, MSFWorkspaceInfo,
7 MSFWorkspacesResult,
8};
9use crate::msgpack::{decode, encode, MsgValue};
10
11#[derive(Debug, Clone)]
12pub struct MsfRpcClient {
13 host: String,
14 port: u16,
15 ssl: bool,
16 username: String,
17 password: String,
18 token: Option<String>,
19}
20
21impl MsfRpcClient {
22 pub fn new(host: &str, port: u16, username: &str, password: &str, ssl: bool) -> Self {
23 Self {
24 host: host.to_string(),
25 port,
26 ssl,
27 username: username.to_string(),
28 password: password.to_string(),
29 token: None,
30 }
31 }
32
33 pub fn login(&mut self) -> Result<MSFLoginResult> {
34 let url = build_url(&self.host, self.port, self.ssl);
35 let payload = encode(&msg_array(vec![
37 msg_str("auth.login"),
38 msg_str(&self.username),
39 msg_str(&self.password),
40 ]));
41
42 match send_request(&url, &payload, 10) {
43 Ok(val) => {
44 let success = val.get("result").and_then(MsgValue::as_str) == Some("success");
45 let token = val
46 .get("token")
47 .and_then(MsgValue::as_str)
48 .unwrap_or("")
49 .to_string();
50
51 if success && !token.is_empty() {
52 self.token = Some(token.clone());
53 Ok(MSFLoginResult {
54 success: true,
55 token,
56 error: String::new(),
57 })
58 } else {
59 let error_msg = val
60 .get("error_message")
61 .or_else(|| val.get("error_string"))
62 .and_then(MsgValue::as_str)
63 .unwrap_or("Authentication failed");
64 Ok(MSFLoginResult {
65 success: false,
66 token: String::new(),
67 error: error_msg.to_string(),
68 })
69 }
70 }
71 Err(error) => Ok(MSFLoginResult {
72 success: false,
73 token: String::new(),
74 error,
75 }),
76 }
77 }
78
79 pub fn logout(&mut self) {
80 if let Some(token) = self.token.clone() {
81 let url = build_url(&self.host, self.port, self.ssl);
82 let payload = encode(&msg_array(vec![msg_str("auth.logout"), msg_str(&token)]));
84 let _ = send_request(&url, &payload, 5);
85 }
86 self.token = None;
87 }
88
89 pub fn is_authenticated(&self) -> bool {
90 self.token.is_some()
91 }
92
93 pub fn get_workspaces(&self) -> Result<MSFWorkspacesResult> {
94 let token = match self.token.as_ref() {
95 Some(token) => token.clone(),
96 None => {
97 return Ok(MSFWorkspacesResult {
98 workspaces: vec![],
99 error: "Not authenticated".to_string(),
100 });
101 }
102 };
103
104 let url = build_url(&self.host, self.port, self.ssl);
105 let payload = encode(&msg_array(vec![msg_str("db.workspaces"), msg_str(&token)]));
107
108 match send_request(&url, &payload, 15) {
109 Ok(val) => Ok(MSFWorkspacesResult {
110 workspaces: extract_workspaces(&val),
111 error: String::new(),
112 }),
113 Err(error) => Ok(MSFWorkspacesResult {
114 workspaces: vec![],
115 error,
116 }),
117 }
118 }
119
120 pub fn get_hosts(&self, workspace: Option<&str>) -> Result<MSFHostsResult> {
121 let token = match self.token.as_ref() {
122 Some(token) => token.clone(),
123 None => {
124 return Ok(MSFHostsResult {
125 hosts: vec![],
126 error: "Not authenticated".to_string(),
127 });
128 }
129 };
130
131 let url = build_url(&self.host, self.port, self.ssl);
132 let mut params = vec![msg_str("db.hosts"), msg_str(&token)];
136 if let Some(workspace) = workspace {
137 params.push(MsgValue::Map(BTreeMap::from([(
138 "workspace".to_string(),
139 msg_str(workspace),
140 )])));
141 } else {
142 params.push(MsgValue::Map(BTreeMap::new()));
143 }
144 let payload = encode(&msg_array(params));
145
146 match send_request(&url, &payload, 15) {
147 Ok(val) => Ok(MSFHostsResult {
148 hosts: extract_hosts(&val),
149 error: String::new(),
150 }),
151 Err(error) => Ok(MSFHostsResult {
152 hosts: vec![],
153 error,
154 }),
155 }
156 }
157
158 pub fn check_host_exists(
159 &self,
160 address: &str,
161 workspace: Option<&str>,
162 ) -> Result<MSFHostCheckResult> {
163 let hosts = self.get_hosts(workspace)?;
164 Ok(MSFHostCheckResult {
165 exists: hosts.hosts.iter().any(|host| host.address == address),
166 error: hosts.error,
167 })
168 }
169
170 pub fn rpc_call(
176 &self,
177 method: &str,
178 params: &[MsgValue],
179 timeout_secs: u64,
180 ) -> Result<MsgValue> {
181 let token = self
182 .token
183 .clone()
184 .ok_or_else(|| crate::error::AppError::Message("not authenticated".to_string()))?;
185 let url = build_url(&self.host, self.port, self.ssl);
186 let mut args = vec![msg_str(method), msg_str(&token)];
187 args.extend_from_slice(params);
188 let payload = encode(&msg_array(args));
189 send_request(&url, &payload, timeout_secs).map_err(crate::error::AppError::Network)
190 }
191
192 pub fn module_exists(&self, module_type: &str, name: &str) -> Result<bool> {
195 let method = if module_type == "auxiliary" {
196 "module.auxiliary"
197 } else {
198 "module.exploit"
199 };
200 let val = self.rpc_call(method, &[msg_str(name)], 15)?;
201 let result = val.get("result").and_then(MsgValue::as_str).unwrap_or("");
202 Ok(result == "success")
203 }
204
205 pub fn module_info(&self, module_type: &str, name: &str) -> Result<MsgValue> {
207 self.rpc_call("module.info", &[msg_str(module_type), msg_str(name)], 15)
208 }
209
210 pub fn module_execute(
213 &self,
214 module_type: &str,
215 name: &str,
216 options: &BTreeMap<String, String>,
217 payload: Option<&str>,
218 ) -> Result<MsgValue> {
219 let mut opts = BTreeMap::new();
220 for (key, value) in options {
221 opts.insert(key.clone(), msg_str(value));
222 }
223 if let Some(payload) = payload {
224 opts.insert("PAYLOAD".to_string(), msg_str(payload));
225 }
226 self.rpc_call(
227 "module.execute",
228 &[msg_str(module_type), msg_str(name), MsgValue::Map(opts)],
229 30,
230 )
231 }
232
233 pub fn job_list(&self) -> Result<MsgValue> {
235 self.rpc_call("job.list", &[], 15)
236 }
237
238 pub fn job_info(&self, job_id: &str) -> Result<MsgValue> {
240 self.rpc_call("job.info", &[msg_str(job_id)], 15)
241 }
242
243 pub fn job_stop(&self, job_id: &str) -> Result<MsgValue> {
245 self.rpc_call("job.stop", &[msg_str(job_id)], 15)
246 }
247
248 pub fn session_list(&self) -> Result<MsgValue> {
250 self.rpc_call("session.list", &[], 15)
251 }
252
253 pub fn db_loots(&self, workspace: &str) -> Result<MsgValue> {
255 let mut opts = BTreeMap::new();
256 if !workspace.is_empty() {
257 opts.insert("workspace".to_string(), msg_str(workspace));
258 }
259 self.rpc_call("db.loots", &[MsgValue::Map(opts)], 15)
260 }
261}
262
263fn msg_str(value: &str) -> MsgValue {
265 MsgValue::String(value.to_string())
266}
267
268fn msg_array(values: Vec<MsgValue>) -> MsgValue {
269 MsgValue::Array(values)
270}
271
272fn build_url(host: &str, port: u16, ssl: bool) -> String {
273 let scheme = if ssl { "https" } else { "http" };
274 format!("{}://{}:{}/api/", scheme, host, port)
275}
276
277fn send_request(
278 url: &str,
279 payload: &[u8],
280 timeout_secs: u64,
281) -> std::result::Result<MsgValue, String> {
282 let mut child = Command::new("curl")
283 .arg("-sS")
284 .arg("--max-time")
285 .arg(timeout_secs.to_string())
286 .arg("-H")
287 .arg("Content-Type: binary/message-pack")
288 .arg("-H")
289 .arg("Accept: binary/message-pack")
290 .arg("-i")
291 .arg("--data-binary")
292 .arg("@-")
293 .arg(url)
294 .stdin(Stdio::piped())
295 .stdout(Stdio::piped())
296 .stderr(Stdio::piped())
297 .spawn()
298 .map_err(|e| format!("failed to spawn curl: {e}"))?;
299
300 if let Some(stdin) = child.stdin.as_mut() {
301 use std::io::Write;
302 stdin
303 .write_all(payload)
304 .map_err(|e| format!("failed to write request body: {e}"))?;
305 }
306
307 let output = child
308 .wait_with_output()
309 .map_err(|e| format!("curl wait failed: {e}"))?;
310
311 let stdout = &output.stdout;
312 if stdout.is_empty() {
313 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
314 if !stderr.is_empty() {
315 return Err(stderr);
316 }
317 return Err("empty response from server".to_string());
318 }
319
320 let body = if let Some(pos) = find_header_body_boundary(stdout) {
322 &stdout[pos..]
323 } else {
324 &stdout[..]
325 };
326
327 if body.is_empty() {
328 return Err("empty response body from server".to_string());
329 }
330
331 decode(body).map_err(|e| {
332 let dump_len = body.len().min(512);
333 let hex_dump: Vec<String> = body[..dump_len]
334 .iter()
335 .map(|b| format!("{:02x}", b))
336 .collect();
337 let utf8_dump = String::from_utf8_lossy(&body[..dump_len]);
338 format!(
339 "{}\n response body hex ({} bytes): {}\n response body utf8: {}",
340 e,
341 dump_len,
342 hex_dump.join(" "),
343 utf8_dump
344 )
345 })
346}
347
348fn find_header_body_boundary(data: &[u8]) -> Option<usize> {
349 data.windows(4)
350 .position(|w| w == b"\r\n\r\n")
351 .map(|p| p + 4)
352}
353
354fn extract_workspaces(val: &MsgValue) -> Vec<MSFWorkspaceInfo> {
355 let mut workspaces = Vec::new();
356 if let Some(map) = val.as_map() {
357 if let Some(MsgValue::Array(ws_arr)) = map.get("workspaces") {
358 for ws in ws_arr {
359 if let Some(ws_map) = ws.as_map() {
360 let name = ws_map
361 .get("name")
362 .and_then(MsgValue::as_str)
363 .unwrap_or("")
364 .to_string();
365 let scope = ws_map
366 .get("scope")
367 .and_then(MsgValue::as_str)
368 .unwrap_or("")
369 .to_string();
370 let host_count = ws_map
371 .get("hosts_count")
372 .or_else(|| ws_map.get("host_count"))
373 .and_then(MsgValue::as_u64)
374 .unwrap_or(0);
375
376 workspaces.push(MSFWorkspaceInfo {
377 name,
378 scope,
379 host_count,
380 });
381 }
382 }
383 }
384 }
385 workspaces
386}
387
388fn extract_hosts(val: &MsgValue) -> Vec<MSFHostInfo> {
389 let mut hosts = Vec::new();
390 if let Some(map) = val.as_map() {
391 if let Some(MsgValue::Array(host_arr)) = map.get("hosts") {
392 for host in host_arr {
393 if let Some(host_map) = host.as_map() {
394 let address = host_map
395 .get("address")
396 .and_then(MsgValue::as_str)
397 .unwrap_or("")
398 .to_string();
399 let os_name = host_map
400 .get("os_name")
401 .and_then(MsgValue::as_str)
402 .unwrap_or("")
403 .to_string();
404 let os_flavor = host_map
405 .get("os_flavor")
406 .and_then(MsgValue::as_str)
407 .unwrap_or("")
408 .to_string();
409 let state = host_map
410 .get("state")
411 .and_then(MsgValue::as_str)
412 .unwrap_or("")
413 .to_string();
414 let notes_count = host_map
415 .get("notes_count")
416 .and_then(MsgValue::as_u64)
417 .unwrap_or(0);
418
419 hosts.push(MSFHostInfo {
420 address,
421 os_name,
422 os_flavor,
423 state,
424 notes_count,
425 });
426 }
427 }
428 }
429 }
430 hosts
431}