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