1use crate::error::SqlmapError;
7use crate::types::{
8 BasicResponse, DataResponse, LogResponse, NewTaskResponse, SqlmapOptions, StatusResponse,
9};
10use reqwest::Client;
11use std::process::Stdio;
12use std::time::Duration;
13use tokio::process::{Child, Command};
14use tokio::time::sleep;
15use tracing::{debug, warn};
16
17pub struct SqlmapEngine {
22 api_url: String,
23 http: Client,
24 daemon_process: Option<Child>,
25 poll_interval: Duration,
27}
28
29impl SqlmapEngine {
30 pub async fn new(
44 port: u16,
45 spawn_local: bool,
46 binary_path: Option<&str>,
47 ) -> Result<Self, SqlmapError> {
48 Self::with_config(port, spawn_local, binary_path, Duration::from_secs(10), Duration::from_millis(1000)).await
49 }
50
51 pub async fn with_config(
58 port: u16,
59 spawn_local: bool,
60 binary_path: Option<&str>,
61 request_timeout: Duration,
62 poll_interval: Duration,
63 ) -> Result<Self, SqlmapError> {
64 let mut daemon_process = None;
65 let api_url = format!("http://127.0.0.1:{port}");
66
67 let http = Client::builder()
68 .timeout(request_timeout)
69 .build()?;
70
71 if spawn_local {
72 if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() {
74 return Err(SqlmapError::PortConflict { port });
75 }
76
77 let binary = binary_path.unwrap_or("sqlmapapi");
78
79 let mut cmd = Command::new(binary);
80 cmd.arg("-s")
81 .arg("-H").arg("127.0.0.1")
82 .arg("-p").arg(port.to_string())
83 .kill_on_drop(true);
84
85 cmd.stdout(Stdio::null()).stderr(Stdio::null());
86
87 daemon_process = Some(cmd.spawn()?);
88
89 let mut ready = false;
91 for attempt in 0..20 {
92 if let Ok(resp) = http.get(format!("{api_url}/task/new")).send().await {
93 if let Ok(json) = resp.json::<NewTaskResponse>().await {
94 if json.success {
95 if let Some(task_id) = json.taskid {
96 let _ = http
98 .get(format!("{api_url}/task/{task_id}/delete"))
99 .send()
100 .await;
101 ready = true;
102 break;
103 }
104 }
105 }
106 }
107 debug!(attempt, "waiting for sqlmapapi daemon to become ready");
108 sleep(Duration::from_millis(250)).await;
109 }
110
111 if !ready {
112 return Err(SqlmapError::ApiError(
113 "sqlmapapi daemon failed to become responsive within 5 seconds".into(),
114 ));
115 }
116 }
117
118 Ok(Self {
119 api_url,
120 http,
121 daemon_process,
122 poll_interval,
123 })
124 }
125
126 pub async fn create_task(&self, options: &SqlmapOptions) -> Result<SqlmapTask<'_>, SqlmapError> {
130 let uri = format!("{}/task/new", self.api_url);
131 let resp = self
132 .http
133 .get(uri)
134 .send()
135 .await?
136 .json::<NewTaskResponse>()
137 .await?;
138
139 if !resp.success {
140 return Err(SqlmapError::ApiError(
141 resp.message
142 .unwrap_or_else(|| "task creation returned success=false".into()),
143 ));
144 }
145
146 let task_id = resp.taskid.ok_or_else(|| {
147 SqlmapError::ApiError("task creation succeeded but returned no task ID".into())
148 })?;
149
150 if task_id.is_empty() {
151 return Err(SqlmapError::ApiError(
152 "task creation succeeded but returned empty task ID".into(),
153 ));
154 }
155
156 let task = SqlmapTask {
157 engine: self,
158 task_id,
159 };
160
161 let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
163 let set_resp = self
164 .http
165 .post(&set_uri)
166 .json(options)
167 .send()
168 .await?
169 .json::<BasicResponse>()
170 .await?;
171
172 if !set_resp.success {
173 return Err(SqlmapError::ApiError(
174 set_resp
175 .message
176 .unwrap_or_else(|| "option configuration failed".into()),
177 ));
178 }
179
180 Ok(task)
181 }
182
183 pub fn is_available() -> bool {
189 std::process::Command::new("sqlmapapi")
190 .arg("-h")
191 .stdout(Stdio::null())
192 .stderr(Stdio::null())
193 .status()
194 .map(|s| s.success())
195 .unwrap_or(false)
196 }
197
198 pub fn is_available_at(binary_path: &str) -> bool {
200 std::process::Command::new(binary_path)
201 .arg("-h")
202 .stdout(Stdio::null())
203 .stderr(Stdio::null())
204 .status()
205 .map(|s| s.success())
206 .unwrap_or(false)
207 }
208
209 pub fn api_url(&self) -> &str {
211 &self.api_url
212 }
213}
214
215impl Drop for SqlmapEngine {
216 fn drop(&mut self) {
217 if let Some(mut proc) = self.daemon_process.take() {
218 let _ = proc.start_kill();
219 }
220 }
221}
222
223pub struct SqlmapTask<'a> {
230 engine: &'a SqlmapEngine,
231 task_id: String,
232}
233
234impl<'a> SqlmapTask<'a> {
235 pub fn task_id(&self) -> &str {
237 &self.task_id
238 }
239
240 pub async fn start(&self) -> Result<(), SqlmapError> {
244 let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
245 let payload = serde_json::json!({});
246 let resp = self
247 .engine
248 .http
249 .post(&uri)
250 .json(&payload)
251 .send()
252 .await?
253 .json::<BasicResponse>()
254 .await?;
255
256 if !resp.success {
257 return Err(SqlmapError::ApiError(
258 resp.message
259 .unwrap_or_else(|| "scan start returned success=false".into()),
260 ));
261 }
262 Ok(())
263 }
264
265 pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
269 let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
270 let start = std::time::Instant::now();
271
272 loop {
273 if start.elapsed().as_secs() > timeout_secs {
274 return Err(SqlmapError::Timeout(timeout_secs));
275 }
276
277 let resp = self
278 .engine
279 .http
280 .get(&uri)
281 .send()
282 .await?
283 .json::<StatusResponse>()
284 .await?;
285
286 if !resp.success {
287 return Err(SqlmapError::ApiError(
288 "status check returned success=false".into(),
289 ));
290 }
291
292 match resp.status.as_deref() {
293 Some("running") => {
294 debug!(task_id = %self.task_id, "scan running");
295 }
296 Some("terminated") => {
297 if let Some(code) = resp.returncode {
298 if code != 0 {
299 return Err(SqlmapError::ApiError(format!(
300 "scan terminated with non-zero exit code {code}"
301 )));
302 }
303 }
304 return Ok(());
305 }
306 Some("not running") => {
307 debug!(task_id = %self.task_id, "scan not running yet");
311 }
312 Some(other) => {
313 warn!(task_id = %self.task_id, status = %other, "unknown sqlmap status");
314 }
315 None => {}
316 }
317
318 sleep(self.engine.poll_interval).await;
319 }
320 }
321
322 pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
324 let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
325 let resp = self.engine.http.get(uri).send().await?;
326
327 if resp.status().is_success() {
328 Ok(resp.json::<DataResponse>().await?)
329 } else {
330 Err(SqlmapError::ApiError(format!(
331 "data fetch returned HTTP {}",
332 resp.status()
333 )))
334 }
335 }
336
337 pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
341 let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
342 let resp = self.engine.http.get(uri).send().await?;
343
344 if resp.status().is_success() {
345 Ok(resp.json::<LogResponse>().await?)
346 } else {
347 Err(SqlmapError::ApiError(format!(
348 "log fetch returned HTTP {}",
349 resp.status()
350 )))
351 }
352 }
353
354 pub async fn stop(&self) -> Result<(), SqlmapError> {
358 let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
359 let resp = self
360 .engine
361 .http
362 .get(uri)
363 .send()
364 .await?
365 .json::<BasicResponse>()
366 .await?;
367
368 if !resp.success {
369 return Err(SqlmapError::ApiError(
370 resp.message
371 .unwrap_or_else(|| "scan stop returned success=false".into()),
372 ));
373 }
374 Ok(())
375 }
376
377 pub async fn kill(&self) -> Result<(), SqlmapError> {
382 let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
383 let resp = self
384 .engine
385 .http
386 .get(uri)
387 .send()
388 .await?
389 .json::<BasicResponse>()
390 .await?;
391
392 if !resp.success {
393 return Err(SqlmapError::ApiError(
394 resp.message
395 .unwrap_or_else(|| "scan kill returned success=false".into()),
396 ));
397 }
398 Ok(())
399 }
400
401 pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
403 let uri = format!("{}/option/{}/list", self.engine.api_url, self.task_id);
404 let resp = self.engine.http.get(uri).send().await?;
405
406 if resp.status().is_success() {
407 Ok(resp.json::<serde_json::Value>().await?)
408 } else {
409 Err(SqlmapError::ApiError(format!(
410 "option list returned HTTP {}",
411 resp.status()
412 )))
413 }
414 }
415}
416
417impl<'a> Drop for SqlmapTask<'a> {
418 fn drop(&mut self) {
419 let uri = format!(
422 "{}/task/{}/delete",
423 self.engine.api_url, self.task_id
424 );
425 let client = self.engine.http.clone();
426
427 if let Ok(handle) = tokio::runtime::Handle::try_current() {
428 handle.spawn(async move {
429 let _ = client.get(&uri).send().await;
430 });
431 }
432 }
435}