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
17fn map_json_error(err: reqwest::Error) -> SqlmapError {
18 if err.is_decode() {
19 SqlmapError::MalformedResponse
20 } else {
21 SqlmapError::RequestError(err)
22 }
23}
24
25pub struct SqlmapEngine {
30 api_url: String,
31 http: Client,
32 daemon_process: Option<Child>,
33 poll_interval: Duration,
35}
36
37impl SqlmapEngine {
38 pub async fn new(
51 port: u16,
52 spawn_local: bool,
53 binary_path: Option<&str>,
54 ) -> Result<Self, SqlmapError> {
55 Self::with_config(
56 port,
57 spawn_local,
58 binary_path,
59 Duration::from_secs(10),
60 Duration::from_millis(1000),
61 )
62 .await
63 }
64
65 pub async fn with_config(
72 port: u16,
73 spawn_local: bool,
74 binary_path: Option<&str>,
75 request_timeout: Duration,
76 poll_interval: Duration,
77 ) -> Result<Self, SqlmapError> {
78 if poll_interval.is_zero() {
79 return Err(SqlmapError::ApiError(
80 "poll_interval must be greater than zero".into(),
81 ));
82 }
83
84 if request_timeout.is_zero() {
85 return Err(SqlmapError::ApiError(
86 "request_timeout must be greater than zero".into(),
87 ));
88 }
89
90 if port == 0 {
91 return Err(SqlmapError::ApiError("port 0 is not supported".into()));
92 }
93
94 let mut daemon_process = None;
95 let api_url = format!("http://127.0.0.1:{port}");
96
97 let http = Client::builder().timeout(request_timeout).build()?;
98
99 if spawn_local {
100 if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() {
102 return Err(SqlmapError::PortConflict { port });
103 }
104
105 let binary = binary_path.unwrap_or("sqlmapapi");
106
107 let mut cmd = Command::new(binary);
108 cmd.arg("-s")
109 .arg("-H")
110 .arg("127.0.0.1")
111 .arg("-p")
112 .arg(port.to_string())
113 .kill_on_drop(true);
114
115 cmd.stdout(Stdio::null()).stderr(Stdio::null());
116
117 daemon_process = Some(cmd.spawn().map_err(|e| {
118 if e.kind() == std::io::ErrorKind::NotFound {
119 SqlmapError::BinaryNotFound(binary.to_string())
120 } else {
121 SqlmapError::ProcessError(e)
122 }
123 })?);
124
125 let mut ready = false;
127 for attempt in 0..20 {
128 if Self::probe_task_new(&http, &api_url).await.is_ok() {
129 ready = true;
130 break;
131 }
132 debug!(attempt, "waiting for sqlmapapi daemon to become ready");
133 sleep(Duration::from_millis(250)).await;
134 }
135
136 if !ready {
137 return Err(SqlmapError::ApiError(
138 "sqlmapapi daemon failed to become responsive within 5 seconds".into(),
139 ));
140 }
141 } else {
142 Self::probe_task_new(&http, &api_url).await?;
143 }
144
145 Ok(Self {
146 api_url,
147 http,
148 daemon_process,
149 poll_interval,
150 })
151 }
152
153 async fn probe_task_new(http: &Client, api_url: &str) -> Result<(), SqlmapError> {
155 let resp = http.get(format!("{api_url}/task/new")).send().await?;
156
157 if !resp.status().is_success() {
158 return Err(SqlmapError::ApiError(format!(
159 "health probe returned HTTP {}",
160 resp.status()
161 )));
162 }
163
164 let json = resp
165 .json::<NewTaskResponse>()
166 .await
167 .map_err(map_json_error)?;
168
169 if !json.success {
170 return Err(SqlmapError::ApiError(
171 json.message
172 .unwrap_or_else(|| "health probe returned success=false".into()),
173 ));
174 }
175
176 let task_id = json.taskid.filter(|id| !id.is_empty()).ok_or_else(|| {
177 SqlmapError::ApiError("health probe: /task/new did not return a taskid".into())
178 })?;
179
180 let _ = http
181 .get(format!("{api_url}/task/{task_id}/delete"))
182 .send()
183 .await;
184
185 Ok(())
186 }
187
188 pub async fn create_task(
192 &self,
193 options: &SqlmapOptions,
194 ) -> Result<SqlmapTask<'_>, SqlmapError> {
195 let uri = format!("{}/task/new", self.api_url);
196 let resp = self.http.get(uri).send().await?;
197
198 if !resp.status().is_success() {
199 return Err(SqlmapError::ApiError(format!(
200 "task creation returned HTTP {}",
201 resp.status()
202 )));
203 }
204
205 let resp = resp
206 .json::<NewTaskResponse>()
207 .await
208 .map_err(map_json_error)?;
209
210 if !resp.success {
211 return Err(SqlmapError::ApiError(
212 resp.message
213 .unwrap_or_else(|| "task creation returned success=false".into()),
214 ));
215 }
216
217 let task_id = resp
218 .taskid
219 .filter(|id| !id.is_empty())
220 .ok_or_else(|| SqlmapError::InvalidTask(String::new()))?;
221
222 let task = SqlmapTask {
223 engine: self,
224 task_id,
225 };
226
227 let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
229 let set_resp = self.http.post(&set_uri).json(options).send().await?;
230
231 if !set_resp.status().is_success() {
232 return Err(SqlmapError::ApiError(format!(
233 "option configuration returned HTTP {}",
234 set_resp.status()
235 )));
236 }
237
238 let set_resp = set_resp
239 .json::<BasicResponse>()
240 .await
241 .map_err(map_json_error)?;
242
243 if !set_resp.success {
244 return Err(SqlmapError::ApiError(
245 set_resp
246 .message
247 .unwrap_or_else(|| "option configuration failed".into()),
248 ));
249 }
250
251 Ok(task)
252 }
253
254 pub fn is_available() -> bool {
260 std::process::Command::new("sqlmapapi")
261 .arg("-h")
262 .stdout(Stdio::null())
263 .stderr(Stdio::null())
264 .status()
265 .map(|s| s.success())
266 .unwrap_or(false)
267 }
268
269 pub fn is_available_at(binary_path: &str) -> bool {
271 std::process::Command::new(binary_path)
272 .arg("-h")
273 .stdout(Stdio::null())
274 .stderr(Stdio::null())
275 .status()
276 .map(|s| s.success())
277 .unwrap_or(false)
278 }
279
280 pub fn api_url(&self) -> &str {
282 &self.api_url
283 }
284}
285
286impl Drop for SqlmapEngine {
287 fn drop(&mut self) {
288 if let Some(mut proc) = self.daemon_process.take() {
289 let _ = proc.start_kill();
290 }
291 }
292}
293
294pub struct SqlmapTask<'a> {
301 engine: &'a SqlmapEngine,
302 task_id: String,
303}
304
305impl<'a> SqlmapTask<'a> {
306 pub fn task_id(&self) -> &str {
308 &self.task_id
309 }
310
311 pub async fn start(&self) -> Result<(), SqlmapError> {
315 let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
316 let payload = serde_json::json!({});
317 let resp = self.engine.http.post(&uri).json(&payload).send().await?;
318
319 if resp.status().is_success() {
320 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
321 if !body.success {
322 return Err(SqlmapError::ApiError(
323 body.message
324 .unwrap_or_else(|| "scan start returned success=false".into()),
325 ));
326 }
327 Ok(())
328 } else {
329 Err(SqlmapError::ApiError(format!(
330 "scan start returned HTTP {}",
331 resp.status()
332 )))
333 }
334 }
335
336 pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
340 let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
341 let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
342
343 loop {
344 if std::time::Instant::now() >= deadline {
345 return Err(SqlmapError::Timeout(timeout_secs));
346 }
347
348 let resp = self.engine.http.get(&uri).send().await?;
349
350 if !resp.status().is_success() {
351 return Err(SqlmapError::ApiError(format!(
352 "status check returned HTTP {}",
353 resp.status()
354 )));
355 }
356
357 let status = resp
358 .json::<StatusResponse>()
359 .await
360 .map_err(map_json_error)?;
361
362 if !status.success {
363 return Err(SqlmapError::ApiError(
364 status
365 .message
366 .unwrap_or_else(|| "status check returned success=false".into()),
367 ));
368 }
369
370 match status.status.as_deref() {
371 Some("running") => {
372 debug!(task_id = %self.task_id, "scan running");
373 }
374 Some("terminated") => match status.returncode {
375 Some(0) => return Ok(()),
376 Some(code) => {
377 return Err(SqlmapError::ApiError(format!(
378 "scan terminated with non-zero exit code {code}"
379 )));
380 }
381 None => {
382 return Err(SqlmapError::ApiError(
383 "scan terminated without a process exit code".into(),
384 ));
385 }
386 },
387 Some("not running") => {
388 debug!(task_id = %self.task_id, "scan not running yet");
392 }
393 Some(other) => {
394 warn!(task_id = %self.task_id, status = %other, "unknown sqlmap status");
395 }
396 None => {}
397 }
398
399 sleep(self.engine.poll_interval).await;
400 }
401 }
402
403 pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
405 let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
406 let resp = self.engine.http.get(uri).send().await?;
407
408 if resp.status().is_success() {
409 let data = resp.json::<DataResponse>().await.map_err(map_json_error)?;
410 if !data.success {
411 return Err(SqlmapError::ApiError(
412 data.message
413 .unwrap_or_else(|| "data fetch returned success=false".into()),
414 ));
415 }
416 Ok(data)
417 } else {
418 Err(SqlmapError::ApiError(format!(
419 "data fetch returned HTTP {}",
420 resp.status()
421 )))
422 }
423 }
424
425 pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
429 let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
430 let resp = self.engine.http.get(uri).send().await?;
431
432 if resp.status().is_success() {
433 let log = resp.json::<LogResponse>().await.map_err(map_json_error)?;
434 if !log.success {
435 return Err(SqlmapError::ApiError(
436 log.message
437 .unwrap_or_else(|| "log fetch returned success=false".into()),
438 ));
439 }
440 Ok(log)
441 } else {
442 Err(SqlmapError::ApiError(format!(
443 "log fetch returned HTTP {}",
444 resp.status()
445 )))
446 }
447 }
448
449 pub async fn stop(&self) -> Result<(), SqlmapError> {
453 let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
454 let resp = self.engine.http.get(uri).send().await?;
455
456 if resp.status().is_success() {
457 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
458 if !body.success {
459 return Err(SqlmapError::ApiError(
460 body.message
461 .unwrap_or_else(|| "scan stop returned success=false".into()),
462 ));
463 }
464 Ok(())
465 } else {
466 Err(SqlmapError::ApiError(format!(
467 "scan stop returned HTTP {}",
468 resp.status()
469 )))
470 }
471 }
472
473 pub async fn kill(&self) -> Result<(), SqlmapError> {
478 let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
479 let resp = self.engine.http.get(uri).send().await?;
480
481 if resp.status().is_success() {
482 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
483 if !body.success {
484 return Err(SqlmapError::ApiError(
485 body.message
486 .unwrap_or_else(|| "scan kill returned success=false".into()),
487 ));
488 }
489 Ok(())
490 } else {
491 Err(SqlmapError::ApiError(format!(
492 "scan kill returned HTTP {}",
493 resp.status()
494 )))
495 }
496 }
497
498 pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
500 let uri = format!("{}/option/{}/list", self.engine.api_url, self.task_id);
501 let resp = self.engine.http.get(uri).send().await?;
502
503 if resp.status().is_success() {
504 let value = resp
505 .json::<serde_json::Value>()
506 .await
507 .map_err(map_json_error)?;
508 if value
509 .get("success")
510 .and_then(|v| v.as_bool())
511 .is_some_and(|success| !success)
512 {
513 let message = value
514 .get("message")
515 .and_then(|v| v.as_str())
516 .map(str::to_string)
517 .unwrap_or_else(|| "option list returned success=false".into());
518 return Err(SqlmapError::ApiError(message));
519 }
520 Ok(value)
521 } else {
522 Err(SqlmapError::ApiError(format!(
523 "option list returned HTTP {}",
524 resp.status()
525 )))
526 }
527 }
528}
529
530impl<'a> Drop for SqlmapTask<'a> {
531 fn drop(&mut self) {
532 let uri = format!("{}/task/{}/delete", self.engine.api_url, self.task_id);
535 let client = self.engine.http.clone();
536
537 if let Ok(handle) = tokio::runtime::Handle::try_current() {
538 handle.spawn(async move {
539 let _ = client.get(&uri).send().await;
540 });
541 }
542 }
545}