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 return Ok(());
309 }
310 Some(other) => {
311 warn!(task_id = %self.task_id, status = %other, "unknown sqlmap status");
312 }
313 None => {}
314 }
315
316 sleep(self.engine.poll_interval).await;
317 }
318 }
319
320 pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
322 let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
323 let resp = self.engine.http.get(uri).send().await?;
324
325 if resp.status().is_success() {
326 Ok(resp.json::<DataResponse>().await?)
327 } else {
328 Err(SqlmapError::ApiError(format!(
329 "data fetch returned HTTP {}",
330 resp.status()
331 )))
332 }
333 }
334
335 pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
339 let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
340 let resp = self.engine.http.get(uri).send().await?;
341
342 if resp.status().is_success() {
343 Ok(resp.json::<LogResponse>().await?)
344 } else {
345 Err(SqlmapError::ApiError(format!(
346 "log fetch returned HTTP {}",
347 resp.status()
348 )))
349 }
350 }
351
352 pub async fn stop(&self) -> Result<(), SqlmapError> {
356 let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
357 let resp = self
358 .engine
359 .http
360 .get(uri)
361 .send()
362 .await?
363 .json::<BasicResponse>()
364 .await?;
365
366 if !resp.success {
367 return Err(SqlmapError::ApiError(
368 resp.message
369 .unwrap_or_else(|| "scan stop returned success=false".into()),
370 ));
371 }
372 Ok(())
373 }
374
375 pub async fn kill(&self) -> Result<(), SqlmapError> {
380 let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
381 let resp = self
382 .engine
383 .http
384 .get(uri)
385 .send()
386 .await?
387 .json::<BasicResponse>()
388 .await?;
389
390 if !resp.success {
391 return Err(SqlmapError::ApiError(
392 resp.message
393 .unwrap_or_else(|| "scan kill returned success=false".into()),
394 ));
395 }
396 Ok(())
397 }
398
399 pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
401 let uri = format!("{}/option/{}/list", 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 Ok(resp.json::<serde_json::Value>().await?)
406 } else {
407 Err(SqlmapError::ApiError(format!(
408 "option list returned HTTP {}",
409 resp.status()
410 )))
411 }
412 }
413}
414
415impl<'a> Drop for SqlmapTask<'a> {
416 fn drop(&mut self) {
417 let uri = format!(
420 "{}/task/{}/delete",
421 self.engine.api_url, self.task_id
422 );
423 let client = self.engine.http.clone();
424
425 if let Ok(handle) = tokio::runtime::Handle::try_current() {
426 handle.spawn(async move {
427 let _ = client.get(&uri).send().await;
428 });
429 }
430 }
433}