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