1use crate::error::SqlmapError;
4use crate::types::{BasicResponse, DataResponse, NewTaskResponse, SqlmapOptions, StatusResponse};
5use reqwest::Client;
6use std::process::Stdio;
7use std::time::Duration;
8use tokio::process::{Child, Command};
9use tokio::time::sleep;
10use tracing::{debug, warn};
11
12pub struct SqlmapEngine {
14 api_url: String,
15 http: Client,
16 _process: Option<Child>,
17}
18
19impl SqlmapEngine {
20 pub async fn new(
22 port: u16,
23 spawn_local: bool,
24 binary_path: Option<&str>,
25 ) -> Result<Self, SqlmapError> {
26 let mut _process = None;
27 let api_url = format!("http://127.0.0.1:{}", port);
28
29 let http = Client::builder()
30 .timeout(Duration::from_secs(10))
31 .build()?;
32
33 if spawn_local {
34 let binary = binary_path.unwrap_or("sqlmapapi");
35
36 let mut cmd = Command::new(binary);
37 cmd.arg("-s")
38 .arg("-H").arg("127.0.0.1")
39 .arg("-p").arg(port.to_string())
40 .kill_on_drop(true); cmd.stdout(Stdio::null()).stderr(Stdio::null());
43
44 _process = Some(cmd.spawn()?);
45
46 let mut ready = false;
48 for _ in 0..20 {
49 if let Ok(resp) = http.get(format!("{}/task/new", api_url)).send().await {
51 if let Ok(json) = resp.json::<NewTaskResponse>().await {
52 if let Some(task_id) = json.taskid {
53 let _ = http.get(format!("{}/task/{}/delete", api_url, task_id)).send().await;
54 ready = true;
55 break;
56 }
57 }
58 }
59 sleep(Duration::from_millis(250)).await;
60 }
61
62 if !ready {
63 return Err(SqlmapError::ApiError("Daemon failed to boot within 5 seconds".into()));
64 }
65 }
66
67 Ok(Self { api_url, http, _process })
68 }
69
70 pub async fn create_task(&self, options: &SqlmapOptions) -> Result<SqlmapTask<'_>, SqlmapError> {
72 let uri = format!("{}/task/new", self.api_url);
74 let resp = self.http.get(uri).send().await?.json::<NewTaskResponse>().await?;
75
76 let task_id = if resp.success {
77 resp.taskid.unwrap_or_default()
78 } else {
79 return Err(SqlmapError::ApiError(resp.message.unwrap_or_else(|| "Failed to create task".into())));
80 };
81
82 let task = SqlmapTask {
83 engine: self,
84 task_id,
85 };
86
87 let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
89 let set_resp = self.http.post(&set_uri).json(&options).send().await?.json::<BasicResponse>().await?;
91
92 if !set_resp.success {
93 return Err(SqlmapError::ApiError(set_resp.message.unwrap_or_else(|| "Option setup failed".into())));
94 }
95
96 Ok(task)
97 }
98}
99
100pub struct SqlmapTask<'a> {
102 engine: &'a SqlmapEngine,
103 task_id: String,
104}
105
106impl<'a> SqlmapTask<'a> {
107 pub async fn start(&self) -> Result<(), SqlmapError> {
109 let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
110
111 let payload = serde_json::json!({});
113 let resp = self.engine.http.post(&uri).json(&payload).send().await?.json::<BasicResponse>().await?;
114
115 if !resp.success {
116 return Err(SqlmapError::ApiError(resp.message.unwrap_or_else(|| "Failed to start engine".into())));
117 }
118 Ok(())
119 }
120
121 pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
123 let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
124 let start = std::time::Instant::now();
125
126 loop {
127 if start.elapsed().as_secs() > timeout_secs {
128 return Err(SqlmapError::Timeout(timeout_secs));
129 }
130
131 let resp = self.engine.http.get(&uri).send().await?.json::<StatusResponse>().await?;
132 if !resp.success {
133 return Err(SqlmapError::ApiError("Failed to fetch task status".into()));
134 }
135
136 match resp.status.as_deref() {
137 Some("running") => {
138 debug!("Task {} running...", self.task_id);
139 }
140 Some("terminated") => {
141 return Ok(());
142 }
143 Some(other) => {
144 warn!("Unknown sqlmap status string: {}", other);
145 }
146 None => {}
147 }
148
149 sleep(Duration::from_millis(3000)).await;
150 }
151 }
152
153 pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
155 let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
156 let resp = self.engine.http.get(uri).send().await?;
157
158 if resp.status().is_success() {
159 Ok(resp.json::<DataResponse>().await?)
160 } else {
161 Err(SqlmapError::ApiError(format!("Failed pulling data, status: {}", resp.status())))
162 }
163 }
164}
165
166impl<'a> Drop for SqlmapTask<'a> {
167 fn drop(&mut self) {
168 let uri = format!("{}/task/{}/delete", self.engine.api_url, self.task_id);
171 let client = self.engine.http.clone();
172
173 tokio::spawn(async move {
174 let _ = client.get(&uri).send().await;
175 });
176 }
177}
178
179impl SqlmapEngine {
180 pub fn is_available() -> bool {
182 std::process::Command::new("sqlmapapi")
183 .arg("-h")
184 .stdout(Stdio::null())
185 .stderr(Stdio::null())
186 .status()
187 .is_ok()
188 || std::process::Command::new("python3")
189 .args(["-c", "import sqlmap"])
190 .stdout(Stdio::null())
191 .stderr(Stdio::null())
192 .status()
193 .is_ok()
194 }
195}
196
197impl Drop for SqlmapEngine {
198 fn drop(&mut self) {
199 if let Some(mut proc) = self._process.take() {
200 let _ = proc.start_kill();
201 }
202 }
203}