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 let data = resp.json::<DataResponse>().await?;
329 if !data.success {
330 return Err(SqlmapError::ApiError(
331 data.message
332 .unwrap_or_else(|| "data fetch returned success=false".into()),
333 ));
334 }
335 Ok(data)
336 } else {
337 Err(SqlmapError::ApiError(format!(
338 "data fetch returned HTTP {}",
339 resp.status()
340 )))
341 }
342 }
343
344 pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
348 let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
349 let resp = self.engine.http.get(uri).send().await?;
350
351 if resp.status().is_success() {
352 let log = resp.json::<LogResponse>().await?;
353 if !log.success {
354 return Err(SqlmapError::ApiError(
355 log.message
356 .unwrap_or_else(|| "log fetch returned success=false".into()),
357 ));
358 }
359 Ok(log)
360 } else {
361 Err(SqlmapError::ApiError(format!(
362 "log fetch returned HTTP {}",
363 resp.status()
364 )))
365 }
366 }
367
368 pub async fn stop(&self) -> Result<(), SqlmapError> {
372 let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
373 let resp = self
374 .engine
375 .http
376 .get(uri)
377 .send()
378 .await?
379 .json::<BasicResponse>()
380 .await?;
381
382 if !resp.success {
383 return Err(SqlmapError::ApiError(
384 resp.message
385 .unwrap_or_else(|| "scan stop returned success=false".into()),
386 ));
387 }
388 Ok(())
389 }
390
391 pub async fn kill(&self) -> Result<(), SqlmapError> {
396 let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
397 let resp = self
398 .engine
399 .http
400 .get(uri)
401 .send()
402 .await?
403 .json::<BasicResponse>()
404 .await?;
405
406 if !resp.success {
407 return Err(SqlmapError::ApiError(
408 resp.message
409 .unwrap_or_else(|| "scan kill returned success=false".into()),
410 ));
411 }
412 Ok(())
413 }
414
415 pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
417 let uri = format!("{}/option/{}/list", self.engine.api_url, self.task_id);
418 let resp = self.engine.http.get(uri).send().await?;
419
420 if resp.status().is_success() {
421 let value = resp.json::<serde_json::Value>().await?;
422 if value
423 .get("success")
424 .and_then(|v| v.as_bool())
425 .is_some_and(|success| !success)
426 {
427 let message = value
428 .get("message")
429 .and_then(|v| v.as_str())
430 .map(str::to_string)
431 .unwrap_or_else(|| "option list returned success=false".into());
432 return Err(SqlmapError::ApiError(message));
433 }
434 Ok(value)
435 } else {
436 Err(SqlmapError::ApiError(format!(
437 "option list returned HTTP {}",
438 resp.status()
439 )))
440 }
441 }
442}
443
444impl<'a> Drop for SqlmapTask<'a> {
445 fn drop(&mut self) {
446 let uri = format!(
449 "{}/task/{}/delete",
450 self.engine.api_url, self.task_id
451 );
452 let client = self.engine.http.clone();
453
454 if let Ok(handle) = tokio::runtime::Handle::try_current() {
455 handle.spawn(async move {
456 let _ = client.get(&uri).send().await;
457 });
458 }
459 }
462}