1pub mod download;
12
13use crate::{
14 common::types::{FieldValue, ShurikenState},
15 shuriken::{Shuriken, ShurikenConfig},
16};
17use anyhow::{Error, Result, anyhow};
18use flate2::{Compression, write::GzEncoder};
19use regex::Regex;
20use std::{
21 collections::HashMap,
22 fs,
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26use tar::Builder as TarBuilder;
27use tokio::{fs as async_fs, sync::Mutex};
28
29pub fn get_http_port() -> Result<u16> {
30 let apache_conf = "shurikens/Apache/conf/httpd.conf";
31 let nginx_conf = "shurikens/Nginx/nginx.conf";
32
33 if Path::new(apache_conf).exists() {
34 return parse_apache_port(apache_conf);
35 }
36
37 if Path::new(nginx_conf).exists() {
38 return parse_nginx_port(nginx_conf);
39 }
40
41 Err(anyhow!("No Apache or Nginx shuriken found in ./shurikens"))
42}
43
44fn parse_apache_port(path: &str) -> Result<u16> {
45 let file = fs::read_to_string(path)?;
46 let re = Regex::new(r#"(?mi)^\s*Listen\s+([0-9]+)"#)?;
47
48 if let Some(cap) = re.captures(&file) {
49 return Ok(cap[1].parse()?);
50 }
51
52 Err(anyhow!(
53 "Apache config exists but contains no Listen directive"
54 ))
55}
56
57fn parse_nginx_port(path: &str) -> Result<u16> {
58 let file = fs::read_to_string(path)?;
59 let re = Regex::new(r#"(?mi)^\s*listen\s+([0-9]+)"#)?;
60
61 if let Some(cap) = re.captures(&file) {
62 return Ok(cap[1].parse()?);
63 }
64
65 Err(anyhow!(
66 "Nginx config exists but contains no listen directive"
67 ))
68}
69
70pub fn resolve_path(virtual_cwd: &Path, path: &PathBuf) -> PathBuf {
71 let p = Path::new(path);
72
73 if p.is_absolute() {
74 p.to_path_buf()
75 } else {
76 virtual_cwd.join(p)
77 }
78}
79
80#[cfg(windows)]
81pub fn kill_process_by_pid(pid: u32) -> bool {
82 use windows::Win32::Foundation::CloseHandle;
83 use windows::Win32::System::Threading::{
84 OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, TerminateProcess,
85 };
86
87 unsafe {
88 let query_handle = match OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) {
89 Ok(h) => h,
90 Err(_) => return false,
91 };
92
93 if query_handle.is_invalid() {
94 return false;
95 }
96
97 let _ = CloseHandle(query_handle);
98
99 let terminate_handle = match OpenProcess(PROCESS_TERMINATE, false, pid) {
100 Ok(h) => h,
101 Err(_) => return false,
102 };
103
104 if terminate_handle.is_invalid() {
105 return false;
106 }
107
108 let result = TerminateProcess(terminate_handle, 1).is_ok();
109 let _ = CloseHandle(terminate_handle);
110 result
111 }
112}
113
114#[cfg(windows)]
115pub fn kill_process_by_name(name: &str) -> bool {
116 use std::ffi::OsString;
117 use std::os::windows::ffi::OsStringExt;
118 use windows::Win32::Foundation::{CloseHandle, HANDLE};
119 use windows::Win32::System::Diagnostics::ToolHelp::{
120 CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
121 TH32CS_SNAPPROCESS,
122 };
123
124 let target = name.to_ascii_lowercase();
125
126 unsafe {
127 let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) {
128 Ok(s) => s,
129 Err(_) => return false,
130 };
131
132 if snapshot.is_invalid() {
133 return false;
134 }
135
136 let mut entry = PROCESSENTRY32W::default();
137 let mut any_killed = false;
138
139 if Process32FirstW(snapshot, &mut entry).is_ok() {
140 loop {
141 let len = entry
142 .szExeFile
143 .iter()
144 .position(|&c| c == 0)
145 .unwrap_or(entry.szExeFile.len());
146
147 let exe_os: OsString = OsString::from_wide(&entry.szExeFile[..len]);
148 let exe = exe_os.to_string_lossy().to_string();
149
150 if exe.to_ascii_lowercase() == target {
151 if kill_process_by_pid(entry.th32ProcessID) {
152 any_killed = true;
153 }
154 }
155
156 if Process32NextW(snapshot, &mut entry).is_err() {
157 break;
158 }
159 }
160 }
161
162 let _ = CloseHandle(HANDLE(snapshot.0));
163 any_killed
164 }
165}
166
167#[cfg(unix)]
168pub fn kill_process_by_pid(pid: u32) -> bool {
169 use nix::errno::Errno;
170 use nix::sys::signal::{Signal, kill};
171 use nix::unistd::Pid;
172 use std::thread;
173 use std::time::Duration;
174
175 let pid = Pid::from_raw(pid as i32);
176
177 match kill(pid, None) {
178 Ok(_) => {
179 if kill(pid, Signal::SIGTERM).is_ok() {
180 for _ in 0..10 {
181 thread::sleep(Duration::from_millis(100));
182 if kill(pid, None).is_err() {
183 return true;
184 }
185 }
186 }
187
188 kill(pid, Signal::SIGKILL).is_ok()
189 }
190 Err(Errno::ESRCH) => false,
191 Err(Errno::EPERM) => kill(pid, Signal::SIGKILL).is_ok(),
192 Err(_) => false,
193 }
194}
195
196#[cfg(unix)]
197pub fn kill_process_by_name(name: &str) -> bool {
198 #[cfg(target_os = "linux")]
199 {
200 use std::fs;
201 use std::io::Read;
202 use std::path::Path;
203
204 let target = name.to_string();
205 let mut any_killed = false;
206
207 if let Ok(entries) = fs::read_dir("/proc") {
208 for entry in entries.flatten() {
209 let file_name = entry.file_name();
210 let file_name_str = match file_name.to_string_lossy().parse::<u32>() {
211 Ok(_) => file_name.to_string_lossy().to_string(),
212 Err(_) => continue, };
214
215 let pid: u32 = match file_name_str.parse() {
216 Ok(p) => p,
217 Err(_) => continue,
218 };
219
220 let comm_path = entry.path().join("comm"); if !Path::new(&comm_path).exists() {
223 continue;
224 }
225
226 let mut comm_file = match fs::File::open(&comm_path) {
227 Ok(f) => f,
228 Err(_) => continue,
229 };
230
231 let mut comm = String::new();
232 if comm_file.read_to_string(&mut comm).is_err() {
233 continue;
234 }
235
236 let comm = comm.trim(); if comm == target && kill_process_by_pid(pid) {
239 any_killed = true;
240 }
241 }
242 }
243
244 any_killed
245 }
246
247 #[cfg(not(target_os = "linux"))]
249 {
250 use std::process::Command;
251
252 let target = name.to_string();
253 let output = match Command::new("ps")
254 .args(["-axo", "pid,comm"]) .output()
256 {
257 Ok(o) => o,
258 Err(_) => return false,
259 };
260
261 let stdout = String::from_utf8_lossy(&output.stdout);
262 let mut any_killed = false;
263
264 for line in stdout.lines().skip(1) {
265 let mut parts = line.split_whitespace();
267 let pid_str = match parts.next() {
268 Some(p) => p,
269 None => continue,
270 };
271 let comm = parts.next().unwrap_or("");
272
273 let pid: u32 = match pid_str.parse() {
274 Ok(p) => p,
275 Err(_) => continue,
276 };
277
278 let base = comm.rsplit('/').next().unwrap_or(comm);
280 if base == target {
281 if kill_process_by_pid(pid) {
282 any_killed = true;
283 }
284 }
285 }
286
287 any_killed
288 }
289}
290
291pub fn normalize_shuriken_name(name: &str) -> String {
294 name.to_lowercase()
295}
296
297pub fn create_tar_gz_bytes(src_dir: &Path) -> Result<Vec<u8>> {
298 if !src_dir.is_dir() {
299 return Err(anyhow::Error::msg(format!(
300 "Source directory does not exist or is not a directory: {}",
301 src_dir.display()
302 )));
303 }
304
305 let mut buf = Vec::new();
306
307 {
308 let enc = GzEncoder::new(&mut buf, Compression::default());
310 let mut tar = TarBuilder::new(enc);
311
312 tar.append_dir_all(".", src_dir)?;
314
315 let enc = tar.into_inner()?; enc.finish()?; }
319
320 Ok(buf)
321}
322
323pub async fn load_shurikens(root_path: &Path) -> Result<HashMap<String, Shuriken>> {
325 let shurikens_dir = root_path.join("shurikens");
326 let mut shurikens = HashMap::new();
327
328 let mut dir = match async_fs::read_dir(&shurikens_dir).await {
330 Ok(d) => d,
331 Err(_) => return Ok(shurikens), };
333
334 while let Some(entry) = dir.next_entry().await? {
335 let shuriken_path = entry.path();
336 if !shuriken_path.is_dir() {
337 continue;
338 }
339
340 let name = match shuriken_path.file_name().and_then(|n| n.to_str()) {
341 Some(n) => n.to_owned(),
342 None => continue, };
344
345 let ninja_dir = shuriken_path.join(".ninja");
346
347 let manifest_path = ninja_dir.join("manifest.toml");
349 if !manifest_path.exists() {
350 continue; }
352
353 let content: String = async_fs::read_to_string(&manifest_path).await?;
354
355 let mut shuriken: Shuriken = toml::from_str(&content)
356 .map_err(|e| Error::msg(format!("TOML error in {}: {}", manifest_path.display(), e)))?;
357
358 let lock_path = ninja_dir.join("shuriken.lck");
360 let state = if lock_path.exists() {
361 ShurikenState::Running
362 } else {
363 ShurikenState::Idle
364 };
365
366 shuriken.state = Arc::new(Mutex::new(state.clone()));
367
368 let options_path = ninja_dir.join("options.toml");
370 if options_path.exists() {
371 let content: String = async_fs::read_to_string(&options_path).await?;
372 let options: HashMap<String, FieldValue> = toml::from_str(&content).map_err(|e| {
373 Error::msg(format!(
374 "Options error in {}: {}",
375 options_path.display(),
376 e
377 ))
378 })?;
379
380 if let Some(config) = &mut shuriken.config {
381 config.options = Some(options);
382 } else {
383 shuriken.config = Some(ShurikenConfig {
384 config_path: PathBuf::from("options.toml"),
385 options: Some(options),
386 });
387 }
388 }
389
390 let normalized_name = normalize_shuriken_name(&name);
393 shurikens.insert(normalized_name.clone(), shuriken);
394 }
395
396 Ok(shurikens)
397}
398
399pub struct PortOwner {
400 pub pid: u32,
401 pub name: Option<String>,
402}
403
404#[cfg(target_os = "linux")]
405pub fn get_port_owner(port: u16) -> Option<PortOwner> {
406 use std::fs;
407
408 let content = fs::read_to_string("/proc/net/tcp").ok()?;
409
410 let port_hex = format!("{:04X}", port);
411
412 for line in content.lines().skip(1) {
413 let cols: Vec<_> = line.split_whitespace().collect();
414 if cols.len() < 10 {
415 continue;
416 }
417
418 let local_address = cols[1]; if local_address.ends_with(&port_hex) {
420 let inode = cols[9];
421
422 if let Some(pid) = find_pid_by_inode(inode) {
424 return Some(PortOwner { pid, name: None });
425 }
426 }
427 }
428
429 None
430}
431
432#[cfg(target_os = "linux")]
433fn find_pid_by_inode(inode: &str) -> Option<u32> {
434 use std::fs;
435
436 for entry in fs::read_dir("/proc").ok()? {
437 let entry = entry.ok()?;
438 let pid_str = entry.file_name().to_string_lossy().to_string();
439
440 if !pid_str.chars().all(|c| c.is_ascii_digit()) {
441 continue;
442 }
443
444 let fd_path = format!("/proc/{pid_str}/fd");
445
446 if let Ok(fds) = fs::read_dir(fd_path) {
447 for fd in fds.flatten() {
448 if let Ok(link) = fs::read_link(fd.path()) {
449 if link.to_string_lossy().contains(inode) {
450 return pid_str.parse().ok();
451 }
452 }
453 }
454 }
455 }
456
457 None
458}
459
460#[cfg(target_os = "macos")]
461pub fn get_port_owner(port: u16) -> Option<PortOwner> {
462 use std::process::Command;
463
464 let output = Command::new("netstat")
465 .args(["-anv", "-p", "tcp"])
466 .output()
467 .ok()?;
468
469 let text = String::from_utf8_lossy(&output.stdout);
470
471 for line in text.lines() {
472 if line.contains(&format!(".{}", port)) {
473 return Some(PortOwner { pid: 0, name: None });
475 }
476 }
477
478 None
479}
480
481#[cfg(target_os = "windows")]
482pub fn get_port_owner(port: u16) -> Option<PortOwner> {
483 use windows::Win32::NetworkManagement::IpHelper::*;
484 use windows::Win32::Foundation::*;
485
486 unsafe {
487 let mut size = 0u32;
488
489 GetExtendedTcpTable(
490 std::ptr::null_mut(),
491 &mut size,
492 false.into(),
493 AF_INET.0 as u32,
494 TCP_TABLE_OWNER_PID_ALL,
495 0,
496 );
497
498 let mut buffer = vec![0u8; size as usize];
499
500 let res = GetExtendedTcpTable(
501 buffer.as_mut_ptr() as *mut _,
502 &mut size,
503 false.into(),
504 AF_INET.0 as u32,
505 TCP_TABLE_OWNER_PID_ALL,
506 0,
507 );
508
509 if res != NO_ERROR.0 as i32 {
510 return None;
511 }
512
513 let table = buffer.as_ptr() as *const MIB_TCPTABLE_OWNER_PID;
514 let table = &*table;
515
516 let rows = std::slice::from_raw_parts(
517 table.table.as_ptr(),
518 table.dwNumEntries as usize,
519 );
520
521 for row in rows {
522 let local_port = u16::from_be((*row).dwLocalPort as u16);
523
524 if local_port == port {
525 return Some(PortOwner {
526 pid: row.dwOwningPid,
527 name: None,
528 });
529 }
530 }
531 }
532
533 None
534}