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(
52 port: u16,
53 spawn_local: bool,
54 binary_path: Option<&str>,
55 ) -> Result<Self, SqlmapError> {
56 Self::with_config(
57 port,
58 spawn_local,
59 binary_path,
60 Duration::from_secs(10),
61 Duration::from_millis(1000),
62 )
63 .await
64 }
65
66 pub async fn with_config(
73 port: u16,
74 spawn_local: bool,
75 binary_path: Option<&str>,
76 request_timeout: Duration,
77 poll_interval: Duration,
78 ) -> Result<Self, SqlmapError> {
79 if poll_interval.is_zero() {
80 return Err(SqlmapError::ApiError(
81 "poll_interval must be greater than zero".into(),
82 ));
83 }
84
85 if spawn_local && port == 0 {
86 return Err(SqlmapError::ApiError(
87 "port 0 is not supported when spawning local sqlmapapi".into(),
88 ));
89 }
90
91 let mut daemon_process = None;
92 let api_url = format!("http://127.0.0.1:{port}");
93
94 let http = Client::builder().timeout(request_timeout).build()?;
95
96 if spawn_local {
97 if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() {
99 return Err(SqlmapError::PortConflict { port });
100 }
101
102 let binary = binary_path.unwrap_or("sqlmapapi");
103
104 let mut cmd = Command::new(binary);
105 cmd.arg("-s")
106 .arg("-H")
107 .arg("127.0.0.1")
108 .arg("-p")
109 .arg(port.to_string())
110 .kill_on_drop(true);
111
112 cmd.stdout(Stdio::null()).stderr(Stdio::null());
113
114 daemon_process = Some(cmd.spawn().map_err(|e| {
115 if e.kind() == std::io::ErrorKind::NotFound {
116 SqlmapError::BinaryNotFound(binary.to_string())
117 } else {
118 SqlmapError::ProcessError(e)
119 }
120 })?);
121
122 let mut ready = false;
124 for attempt in 0..20 {
125 if let Ok(resp) = http.get(format!("{api_url}/task/new")).send().await {
126 if let Ok(json) = resp.json::<NewTaskResponse>().await.map_err(map_json_error) {
127 if json.success {
128 if let Some(task_id) = json.taskid {
129 let _ = http
131 .get(format!("{api_url}/task/{task_id}/delete"))
132 .send()
133 .await;
134 ready = true;
135 break;
136 }
137 }
138 }
139 }
140 debug!(attempt, "waiting for sqlmapapi daemon to become ready");
141 sleep(Duration::from_millis(250)).await;
142 }
143
144 if !ready {
145 return Err(SqlmapError::ApiError(
146 "sqlmapapi daemon failed to become responsive within 5 seconds".into(),
147 ));
148 }
149 }
150
151 Ok(Self {
152 api_url,
153 http,
154 daemon_process,
155 poll_interval,
156 })
157 }
158
159 pub async fn create_task(
163 &self,
164 options: &SqlmapOptions,
165 ) -> Result<SqlmapTask<'_>, SqlmapError> {
166 let uri = format!("{}/task/new", self.api_url);
167 let resp = self
168 .http
169 .get(uri)
170 .send()
171 .await?
172 .json::<NewTaskResponse>()
173 .await
174 .map_err(map_json_error)?;
175
176 if !resp.success {
177 return Err(SqlmapError::ApiError(
178 resp.message
179 .unwrap_or_else(|| "task creation returned success=false".into()),
180 ));
181 }
182
183 let task_id = resp
184 .taskid
185 .filter(|id| !id.is_empty())
186 .ok_or_else(|| SqlmapError::InvalidTask(String::new()))?;
187
188 let task = SqlmapTask {
189 engine: self,
190 task_id,
191 };
192
193 let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
195 let set_resp = self
196 .http
197 .post(&set_uri)
198 .json(options)
199 .send()
200 .await?
201 .json::<BasicResponse>()
202 .await
203 .map_err(map_json_error)?;
204
205 if !set_resp.success {
206 return Err(SqlmapError::ApiError(
207 set_resp
208 .message
209 .unwrap_or_else(|| "option configuration failed".into()),
210 ));
211 }
212
213 Ok(task)
214 }
215
216 pub fn is_available() -> bool {
222 std::process::Command::new("sqlmapapi")
223 .arg("-h")
224 .stdout(Stdio::null())
225 .stderr(Stdio::null())
226 .status()
227 .map(|s| s.success())
228 .unwrap_or(false)
229 }
230
231 pub fn is_available_at(binary_path: &str) -> bool {
233 std::process::Command::new(binary_path)
234 .arg("-h")
235 .stdout(Stdio::null())
236 .stderr(Stdio::null())
237 .status()
238 .map(|s| s.success())
239 .unwrap_or(false)
240 }
241
242 pub fn api_url(&self) -> &str {
244 &self.api_url
245 }
246}
247
248impl Drop for SqlmapEngine {
249 fn drop(&mut self) {
250 if let Some(mut proc) = self.daemon_process.take() {
251 let _ = proc.start_kill();
252 }
253 }
254}
255
256pub struct SqlmapTask<'a> {
263 engine: &'a SqlmapEngine,
264 task_id: String,
265}
266
267impl<'a> SqlmapTask<'a> {
268 pub fn task_id(&self) -> &str {
270 &self.task_id
271 }
272
273 pub async fn start(&self) -> Result<(), SqlmapError> {
277 let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
278 let payload = serde_json::json!({});
279 let resp = self.engine.http.post(&uri).json(&payload).send().await?;
280
281 if resp.status().is_success() {
282 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
283 if !body.success {
284 return Err(SqlmapError::ApiError(
285 body.message
286 .unwrap_or_else(|| "scan start returned success=false".into()),
287 ));
288 }
289 Ok(())
290 } else {
291 Err(SqlmapError::ApiError(format!(
292 "scan start returned HTTP {}",
293 resp.status()
294 )))
295 }
296 }
297
298 pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
302 let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
303 let start = std::time::Instant::now();
304
305 loop {
306 if start.elapsed().as_secs() > timeout_secs {
307 return Err(SqlmapError::Timeout(timeout_secs));
308 }
309
310 let resp = self.engine.http.get(&uri).send().await?;
311
312 if !resp.status().is_success() {
313 return Err(SqlmapError::ApiError(format!(
314 "status check returned HTTP {}",
315 resp.status()
316 )));
317 }
318
319 let status = resp
320 .json::<StatusResponse>()
321 .await
322 .map_err(map_json_error)?;
323
324 if !status.success {
325 return Err(SqlmapError::ApiError(
326 status
327 .message
328 .unwrap_or_else(|| "status check returned success=false".into()),
329 ));
330 }
331
332 match status.status.as_deref() {
333 Some("running") => {
334 debug!(task_id = %self.task_id, "scan running");
335 }
336 Some("terminated") => {
337 if let Some(code) = status.returncode {
338 if code != 0 {
339 return Err(SqlmapError::ApiError(format!(
340 "scan terminated with non-zero exit code {code}"
341 )));
342 }
343 }
344 return Ok(());
345 }
346 Some("not running") => {
347 debug!(task_id = %self.task_id, "scan not running yet");
351 }
352 Some(other) => {
353 warn!(task_id = %self.task_id, status = %other, "unknown sqlmap status");
354 }
355 None => {}
356 }
357
358 sleep(self.engine.poll_interval).await;
359 }
360 }
361
362 pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
364 let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
365 let resp = self.engine.http.get(uri).send().await?;
366
367 if resp.status().is_success() {
368 let data = resp.json::<DataResponse>().await.map_err(map_json_error)?;
369 if !data.success {
370 return Err(SqlmapError::ApiError(
371 data.message
372 .unwrap_or_else(|| "data fetch returned success=false".into()),
373 ));
374 }
375 Ok(data)
376 } else {
377 Err(SqlmapError::ApiError(format!(
378 "data fetch returned HTTP {}",
379 resp.status()
380 )))
381 }
382 }
383
384 pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
388 let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
389 let resp = self.engine.http.get(uri).send().await?;
390
391 if resp.status().is_success() {
392 let log = resp.json::<LogResponse>().await.map_err(map_json_error)?;
393 if !log.success {
394 return Err(SqlmapError::ApiError(
395 log.message
396 .unwrap_or_else(|| "log fetch returned success=false".into()),
397 ));
398 }
399 Ok(log)
400 } else {
401 Err(SqlmapError::ApiError(format!(
402 "log fetch returned HTTP {}",
403 resp.status()
404 )))
405 }
406 }
407
408 pub async fn stop(&self) -> Result<(), SqlmapError> {
412 let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
413 let resp = self.engine.http.get(uri).send().await?;
414
415 if resp.status().is_success() {
416 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
417 if !body.success {
418 return Err(SqlmapError::ApiError(
419 body.message
420 .unwrap_or_else(|| "scan stop returned success=false".into()),
421 ));
422 }
423 Ok(())
424 } else {
425 Err(SqlmapError::ApiError(format!(
426 "scan stop returned HTTP {}",
427 resp.status()
428 )))
429 }
430 }
431
432 pub async fn kill(&self) -> Result<(), SqlmapError> {
437 let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
438 let resp = self.engine.http.get(uri).send().await?;
439
440 if resp.status().is_success() {
441 let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
442 if !body.success {
443 return Err(SqlmapError::ApiError(
444 body.message
445 .unwrap_or_else(|| "scan kill returned success=false".into()),
446 ));
447 }
448 Ok(())
449 } else {
450 Err(SqlmapError::ApiError(format!(
451 "scan kill returned HTTP {}",
452 resp.status()
453 )))
454 }
455 }
456
457 pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
459 let uri = format!("{}/option/{}/list", self.engine.api_url, self.task_id);
460 let resp = self.engine.http.get(uri).send().await?;
461
462 if resp.status().is_success() {
463 let value = resp
464 .json::<serde_json::Value>()
465 .await
466 .map_err(map_json_error)?;
467 if value
468 .get("success")
469 .and_then(|v| v.as_bool())
470 .is_some_and(|success| !success)
471 {
472 let message = value
473 .get("message")
474 .and_then(|v| v.as_str())
475 .map(str::to_string)
476 .unwrap_or_else(|| "option list returned success=false".into());
477 return Err(SqlmapError::ApiError(message));
478 }
479 Ok(value)
480 } else {
481 Err(SqlmapError::ApiError(format!(
482 "option list returned HTTP {}",
483 resp.status()
484 )))
485 }
486 }
487}
488
489impl<'a> Drop for SqlmapTask<'a> {
490 fn drop(&mut self) {
491 let uri = format!("{}/task/{}/delete", self.engine.api_url, self.task_id);
494 let client = self.engine.http.clone();
495
496 if let Ok(handle) = tokio::runtime::Handle::try_current() {
497 handle.spawn(async move {
498 let _ = client.get(&uri).send().await;
499 });
500 }
501 }
504}