use crate::error::SqlmapError;
use crate::types::{
BasicResponse, DataResponse, LogResponse, NewTaskResponse, SqlmapOptions, StatusResponse,
};
use reqwest::Client;
use std::process::Stdio;
use std::time::Duration;
use tokio::process::{Child, Command};
use tokio::time::sleep;
use tracing::{debug, warn};
fn map_json_error(err: reqwest::Error) -> SqlmapError {
if err.is_decode() {
SqlmapError::MalformedResponse
} else {
SqlmapError::RequestError(err)
}
}
pub struct SqlmapEngine {
api_url: String,
http: Client,
daemon_process: Option<Child>,
poll_interval: Duration,
}
impl SqlmapEngine {
pub async fn new(
port: u16,
spawn_local: bool,
binary_path: Option<&str>,
) -> Result<Self, SqlmapError> {
Self::with_config(
port,
spawn_local,
binary_path,
Duration::from_secs(10),
Duration::from_millis(1000),
)
.await
}
pub async fn with_config(
port: u16,
spawn_local: bool,
binary_path: Option<&str>,
request_timeout: Duration,
poll_interval: Duration,
) -> Result<Self, SqlmapError> {
if poll_interval.is_zero() {
return Err(SqlmapError::ApiError(
"poll_interval must be greater than zero".into(),
));
}
if request_timeout.is_zero() {
return Err(SqlmapError::ApiError(
"request_timeout must be greater than zero".into(),
));
}
if port == 0 {
return Err(SqlmapError::ApiError("port 0 is not supported".into()));
}
let mut daemon_process = None;
let api_url = format!("http://127.0.0.1:{port}");
let http = Client::builder().timeout(request_timeout).build()?;
if spawn_local {
if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() {
return Err(SqlmapError::PortConflict { port });
}
let binary = binary_path.unwrap_or("sqlmapapi");
let mut cmd = Command::new(binary);
cmd.arg("-s")
.arg("-H")
.arg("127.0.0.1")
.arg("-p")
.arg(port.to_string())
.kill_on_drop(true);
cmd.stdout(Stdio::null()).stderr(Stdio::null());
daemon_process = Some(cmd.spawn().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
SqlmapError::BinaryNotFound(binary.to_string())
} else {
SqlmapError::ProcessError(e)
}
})?);
let mut ready = false;
for attempt in 0..20 {
if Self::probe_task_new(&http, &api_url).await.is_ok() {
ready = true;
break;
}
debug!(attempt, "waiting for sqlmapapi daemon to become ready");
sleep(Duration::from_millis(250)).await;
}
if !ready {
return Err(SqlmapError::ApiError(
"sqlmapapi daemon failed to become responsive within 5 seconds".into(),
));
}
} else {
Self::probe_task_new(&http, &api_url).await?;
}
Ok(Self {
api_url,
http,
daemon_process,
poll_interval,
})
}
async fn probe_task_new(http: &Client, api_url: &str) -> Result<(), SqlmapError> {
let resp = http.get(format!("{api_url}/task/new")).send().await?;
if !resp.status().is_success() {
return Err(SqlmapError::ApiError(format!(
"health probe returned HTTP {}",
resp.status()
)));
}
let json = resp
.json::<NewTaskResponse>()
.await
.map_err(map_json_error)?;
if !json.success {
return Err(SqlmapError::ApiError(
json.message
.unwrap_or_else(|| "health probe returned success=false".into()),
));
}
let task_id = json.taskid.filter(|id| !id.is_empty()).ok_or_else(|| {
SqlmapError::ApiError("health probe: /task/new did not return a taskid".into())
})?;
let _ = http
.get(format!("{api_url}/task/{task_id}/delete"))
.send()
.await;
Ok(())
}
pub async fn create_task(
&self,
options: &SqlmapOptions,
) -> Result<SqlmapTask<'_>, SqlmapError> {
let uri = format!("{}/task/new", self.api_url);
let resp = self.http.get(uri).send().await?;
if !resp.status().is_success() {
return Err(SqlmapError::ApiError(format!(
"task creation returned HTTP {}",
resp.status()
)));
}
let resp = resp
.json::<NewTaskResponse>()
.await
.map_err(map_json_error)?;
if !resp.success {
return Err(SqlmapError::ApiError(
resp.message
.unwrap_or_else(|| "task creation returned success=false".into()),
));
}
let task_id = resp
.taskid
.filter(|id| !id.is_empty())
.ok_or_else(|| SqlmapError::InvalidTask(String::new()))?;
let task = SqlmapTask {
engine: self,
task_id,
};
let set_uri = format!("{}/option/{}/set", self.api_url, task.task_id);
let set_resp = self.http.post(&set_uri).json(options).send().await?;
if !set_resp.status().is_success() {
return Err(SqlmapError::ApiError(format!(
"option configuration returned HTTP {}",
set_resp.status()
)));
}
let set_resp = set_resp
.json::<BasicResponse>()
.await
.map_err(map_json_error)?;
if !set_resp.success {
return Err(SqlmapError::ApiError(
set_resp
.message
.unwrap_or_else(|| "option configuration failed".into()),
));
}
Ok(task)
}
pub fn is_available() -> bool {
std::process::Command::new("sqlmapapi")
.arg("-h")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn is_available_at(binary_path: &str) -> bool {
std::process::Command::new(binary_path)
.arg("-h")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn api_url(&self) -> &str {
&self.api_url
}
}
impl Drop for SqlmapEngine {
fn drop(&mut self) {
if let Some(mut proc) = self.daemon_process.take() {
let _ = proc.start_kill();
}
}
}
pub struct SqlmapTask<'a> {
engine: &'a SqlmapEngine,
task_id: String,
}
impl<'a> SqlmapTask<'a> {
pub fn task_id(&self) -> &str {
&self.task_id
}
pub async fn start(&self) -> Result<(), SqlmapError> {
let uri = format!("{}/scan/{}/start", self.engine.api_url, self.task_id);
let payload = serde_json::json!({});
let resp = self.engine.http.post(&uri).json(&payload).send().await?;
if resp.status().is_success() {
let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
if !body.success {
return Err(SqlmapError::ApiError(
body.message
.unwrap_or_else(|| "scan start returned success=false".into()),
));
}
Ok(())
} else {
Err(SqlmapError::ApiError(format!(
"scan start returned HTTP {}",
resp.status()
)))
}
}
pub async fn wait_for_completion(&self, timeout_secs: u64) -> Result<(), SqlmapError> {
let uri = format!("{}/scan/{}/status", self.engine.api_url, self.task_id);
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
loop {
if std::time::Instant::now() >= deadline {
return Err(SqlmapError::Timeout(timeout_secs));
}
let resp = self.engine.http.get(&uri).send().await?;
if !resp.status().is_success() {
return Err(SqlmapError::ApiError(format!(
"status check returned HTTP {}",
resp.status()
)));
}
let status = resp
.json::<StatusResponse>()
.await
.map_err(map_json_error)?;
if !status.success {
return Err(SqlmapError::ApiError(
status
.message
.unwrap_or_else(|| "status check returned success=false".into()),
));
}
match status.status.as_deref() {
Some("running") => {
debug!(task_id = %self.task_id, "scan running");
}
Some("terminated") => match status.returncode {
Some(0) => return Ok(()),
Some(code) => {
return Err(SqlmapError::ApiError(format!(
"scan terminated with non-zero exit code {code}"
)));
}
None => {
return Err(SqlmapError::ApiError(
"scan terminated without a process exit code".into(),
));
}
},
Some("not running") => {
debug!(task_id = %self.task_id, "scan not running yet");
}
Some(other) => {
warn!(task_id = %self.task_id, status = %other, "unknown sqlmap status");
}
None => {}
}
sleep(self.engine.poll_interval).await;
}
}
pub async fn fetch_data(&self) -> Result<DataResponse, SqlmapError> {
let uri = format!("{}/scan/{}/data", self.engine.api_url, self.task_id);
let resp = self.engine.http.get(uri).send().await?;
if resp.status().is_success() {
let data = resp.json::<DataResponse>().await.map_err(map_json_error)?;
if !data.success {
return Err(SqlmapError::ApiError(
data.message
.unwrap_or_else(|| "data fetch returned success=false".into()),
));
}
Ok(data)
} else {
Err(SqlmapError::ApiError(format!(
"data fetch returned HTTP {}",
resp.status()
)))
}
}
pub async fn fetch_log(&self) -> Result<LogResponse, SqlmapError> {
let uri = format!("{}/scan/{}/log", self.engine.api_url, self.task_id);
let resp = self.engine.http.get(uri).send().await?;
if resp.status().is_success() {
let log = resp.json::<LogResponse>().await.map_err(map_json_error)?;
if !log.success {
return Err(SqlmapError::ApiError(
log.message
.unwrap_or_else(|| "log fetch returned success=false".into()),
));
}
Ok(log)
} else {
Err(SqlmapError::ApiError(format!(
"log fetch returned HTTP {}",
resp.status()
)))
}
}
pub async fn stop(&self) -> Result<(), SqlmapError> {
let uri = format!("{}/scan/{}/stop", self.engine.api_url, self.task_id);
let resp = self.engine.http.get(uri).send().await?;
if resp.status().is_success() {
let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
if !body.success {
return Err(SqlmapError::ApiError(
body.message
.unwrap_or_else(|| "scan stop returned success=false".into()),
));
}
Ok(())
} else {
Err(SqlmapError::ApiError(format!(
"scan stop returned HTTP {}",
resp.status()
)))
}
}
pub async fn kill(&self) -> Result<(), SqlmapError> {
let uri = format!("{}/scan/{}/kill", self.engine.api_url, self.task_id);
let resp = self.engine.http.get(uri).send().await?;
if resp.status().is_success() {
let body = resp.json::<BasicResponse>().await.map_err(map_json_error)?;
if !body.success {
return Err(SqlmapError::ApiError(
body.message
.unwrap_or_else(|| "scan kill returned success=false".into()),
));
}
Ok(())
} else {
Err(SqlmapError::ApiError(format!(
"scan kill returned HTTP {}",
resp.status()
)))
}
}
pub async fn list_options(&self) -> Result<serde_json::Value, SqlmapError> {
let uri = format!("{}/option/{}/list", self.engine.api_url, self.task_id);
let resp = self.engine.http.get(uri).send().await?;
if resp.status().is_success() {
let value = resp
.json::<serde_json::Value>()
.await
.map_err(map_json_error)?;
if value
.get("success")
.and_then(|v| v.as_bool())
.is_some_and(|success| !success)
{
let message = value
.get("message")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| "option list returned success=false".into());
return Err(SqlmapError::ApiError(message));
}
Ok(value)
} else {
Err(SqlmapError::ApiError(format!(
"option list returned HTTP {}",
resp.status()
)))
}
}
}
impl<'a> Drop for SqlmapTask<'a> {
fn drop(&mut self) {
let uri = format!("{}/task/{}/delete", self.engine.api_url, self.task_id);
let client = self.engine.http.clone();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
let _ = client.get(&uri).send().await;
});
}
}
}