Skip to main content

sqlmap_rs/
client.rs

1//! Orchestrator for the `sqlmapapi.py` subprocess and its RESTful interface.
2
3use crate::error::SqlmapError;
4use crate::types::{BasicResponse, DataResponse, NewTaskResponse, SqlmapOptions, StatusResponse};
5use reqwest::Client;
6use std::process::Stdio;
7use std::time::Duration;
8use tokio::process::{Child, Command};
9use tokio::time::sleep;
10use tracing::{debug, warn};
11
12/// Manages the `sqlmapapi` lifecycle and provides access to its REST API.
13pub struct SqlmapEngine {
14    api_url: String,
15    http: Client,
16    _process: Option<Child>,
17}
18
19impl SqlmapEngine {
20    /// Launches a local `sqlmapapi` daemon on a specific port, or connects to an existing remote one.
21    pub async fn new(
22        port: u16,
23        spawn_local: bool,
24        binary_path: Option<&str>,
25    ) -> Result<Self, SqlmapError> {
26        let mut _process = None;
27        let api_url = format!("http://127.0.0.1:{}", port);
28        
29        let http = Client::builder()
30            .timeout(Duration::from_secs(10))
31            .build()?;
32
33        if spawn_local {
34            let binary = binary_path.unwrap_or("sqlmapapi");
35            
36            let mut cmd = Command::new(binary);
37            cmd.arg("-s")
38               .arg("-H").arg("127.0.0.1")
39               .arg("-p").arg(port.to_string())
40               .kill_on_drop(true); // Law 4: Absolute deterministic teardown
41               
42            cmd.stdout(Stdio::null()).stderr(Stdio::null());
43
44            _process = Some(cmd.spawn()?);
45
46            // Wait for daemon to become responsive instead of blind sleeping.
47            let mut ready = false;
48            for _ in 0..20 {
49                // If we can create and delete a task, the daemon is fully online.
50                if let Ok(resp) = http.get(format!("{}/task/new", api_url)).send().await {
51                    if let Ok(json) = resp.json::<NewTaskResponse>().await {
52                        if let Some(task_id) = json.taskid {
53                            let _ = http.get(format!("{}/task/{}/delete", api_url, task_id)).send().await;
54                            ready = true;
55                            break;
56                        }
57                    }
58                }
59                sleep(Duration::from_millis(250)).await;
60            }
61
62            if !ready {
63                return Err(SqlmapError::ApiError("Daemon failed to boot within 5 seconds".into()));
64            }
65        }
66
67        Ok(Self { api_url, http, _process })
68    }
69
70    /// Creates and configures a new scanning task, returning an RAII wrapper.
71    pub async fn create_task(&self, options: &SqlmapOptions) -> Result<SqlmapTask<'_>, SqlmapError> {
72        // 1. Create the task ID
73        let uri = format!("{}/task/new", self.api_url);
74        let resp = self.http.get(uri).send().await?.json::<NewTaskResponse>().await?;
75        
76        let task_id = if resp.success {
77            resp.taskid.unwrap_or_default()
78        } else {
79            return Err(SqlmapError::ApiError(resp.message.unwrap_or_else(|| "Failed to create task".into())));
80        };
81
82        let task = SqlmapTask {
83            engine: self,
84            task_id,
85        };
86
87        // 2. Set the configuration options
88        let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
89        // Sqlmap expects the dictionary directly as JSON body, NOT wrapped in {"options": ...}
90        let set_resp = self.http.post(&set_uri).json(&options).send().await?.json::<BasicResponse>().await?;
91        
92        if !set_resp.success {
93            return Err(SqlmapError::ApiError(set_resp.message.unwrap_or_else(|| "Option setup failed".into())));
94        }
95
96        Ok(task)
97    }
98}
99
100/// An RAII tracked execution task. Ensures the Daemon memory is purged cleanly on Drop.
101pub struct SqlmapTask<'a> {
102    engine: &'a SqlmapEngine,
103    task_id: String,
104}
105
106impl<'a> SqlmapTask<'a> {
107    /// Starts the SQLMap fuzzing on this specific task.
108    pub async fn start(&self) -> Result<(), SqlmapError> {
109        let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
110        
111        // Blank body since the URL is passed via `options`.
112        let payload = serde_json::json!({});
113        let resp = self.engine.http.post(&uri).json(&payload).send().await?.json::<BasicResponse>().await?;
114        
115        if !resp.success {
116            return Err(SqlmapError::ApiError(resp.message.unwrap_or_else(|| "Failed to start engine".into())));
117        }
118        Ok(())
119    }
120
121    /// Polls the task status until completion. Returns an error on timeout.
122    pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
123        let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
124        let start = std::time::Instant::now();
125        
126        loop {
127            if start.elapsed().as_secs() > timeout_secs {
128                return Err(SqlmapError::Timeout(timeout_secs));
129            }
130
131            let resp = self.engine.http.get(&uri).send().await?.json::<StatusResponse>().await?;
132            if !resp.success {
133                return Err(SqlmapError::ApiError("Failed to fetch task status".into()));
134            }
135
136            match resp.status.as_deref() {
137                Some("running") => {
138                    debug!("Task {} running...", self.task_id);
139                }
140                Some("terminated") => {
141                     return Ok(());
142                }
143                Some(other) => {
144                    warn!("Unknown sqlmap status string: {}", other);
145                }
146                None => {}
147            }
148
149            sleep(Duration::from_millis(3000)).await;
150        }
151    }
152
153    /// Fetches the compiled data results from the engine.
154    pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
155        let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
156        let resp = self.engine.http.get(uri).send().await?;
157        
158        if resp.status().is_success() {
159            Ok(resp.json::<DataResponse>().await?)
160        } else {
161            Err(SqlmapError::ApiError(format!("Failed pulling data, status: {}", resp.status())))
162        }
163    }
164}
165
166impl<'a> Drop for SqlmapTask<'a> {
167    fn drop(&mut self) {
168        // Guarantee the server reclaims task memory when this struct goes out of scope.
169        // We spawn it as a detached future since Drop cannot be asynchronous.
170        let uri = format!("{}/task/{}/delete", self.engine.api_url, self.task_id);
171        let client = self.engine.http.clone();
172        
173        tokio::spawn(async move {
174            let _ = client.get(&uri).send().await;
175        });
176    }
177}
178
179impl SqlmapEngine {
180    /// Check if sqlmapapi is available on this system.
181    pub fn is_available() -> bool {
182        std::process::Command::new("sqlmapapi")
183            .arg("-h")
184            .stdout(Stdio::null())
185            .stderr(Stdio::null())
186            .status()
187            .is_ok()
188            || std::process::Command::new("python3")
189                .args(["-c", "import sqlmap"])
190                .stdout(Stdio::null())
191                .stderr(Stdio::null())
192                .status()
193                .is_ok()
194    }
195}
196
197impl Drop for SqlmapEngine {
198    fn drop(&mut self) {
199        if let Some(mut proc) = self._process.take() {
200            let _ = proc.start_kill();
201        }
202    }
203}