use codedefender_config::{AnalysisResult, Config};
use reqwest::{StatusCode, blocking::Client};
use std::collections::HashMap;
use std::error::Error;
use std::io;
use once_cell::sync::Lazy;
pub use codedefender_config;
pub use serde_json;
pub static BASE_URL: Lazy<String> = Lazy::new(|| {
std::env::var("BASE_URL").unwrap_or_else(|_| "https://app.codedefender.io".into())
});
pub static GET_UPLOAD_URL_EP: Lazy<String> =
Lazy::new(|| format!("{}/api/get-upload-url", *BASE_URL));
pub static ANALYZE_EP: Lazy<String> =
Lazy::new(|| format!("{}/api/analyze", *BASE_URL));
pub static ANALYZE_STATUS_EP: Lazy<String> =
Lazy::new(|| format!("{}/api/analyze-status", *BASE_URL));
pub static DEFEND_EP: Lazy<String> =
Lazy::new(|| format!("{}/api/defend", *BASE_URL));
pub static DOWNLOAD_EP: Lazy<String> =
Lazy::new(|| format!("{}/api/download", *BASE_URL));
pub enum Status {
Ready(String),
Processing,
Failed(Box<dyn Error>),
}
pub fn get_upload_info(
file_size: usize,
file_name: Option<String>,
client: &Client,
api_key: &str,
) -> Result<(String, String), Box<dyn Error>> {
let mut query_params = HashMap::new();
query_params.insert("fileSize".to_string(), file_size.to_string());
if let Some(name) = file_name {
query_params.insert("fileName".to_string(), name);
}
let response = client
.get(&*GET_UPLOAD_URL_EP)
.header("Authorization", format!("ApiKey {}", api_key))
.query(&query_params)
.send()?
.error_for_status()?;
let json: HashMap<String, String> = response.json()?;
let upload_url = json.get("uploadUrl").cloned().ok_or("Missing uploadUrl")?;
let file_id = json.get("fileId").cloned().ok_or("Missing fileId")?;
Ok((file_id, upload_url))
}
pub fn upload_to_s3(
upload_url: &str,
file_bytes: Vec<u8>,
client: &Client,
) -> Result<(), Box<dyn Error>> {
client
.put(upload_url)
.header("Content-Type", "application/octet-stream")
.header("Content-Length", file_bytes.len().to_string())
.body(file_bytes)
.send()?
.error_for_status()?;
Ok(())
}
pub fn upload_data(
data: Vec<u8>,
filename: String,
client: &Client,
api_key: &str,
) -> Result<String, Box<dyn Error>> {
let file_size = data.len();
let (file_id, upload_url) = get_upload_info(file_size, Some(filename), client, api_key)?;
upload_to_s3(&upload_url, data, client)?;
Ok(file_id)
}
pub fn upload_file(
file_bytes: Vec<u8>,
client: &Client,
api_key: &str,
) -> Result<String, Box<dyn Error>> {
let file_size = file_bytes.len();
let (file_id, upload_url) = get_upload_info(file_size, None, client, api_key)?;
upload_to_s3(&upload_url, file_bytes, client)?;
Ok(file_id)
}
pub fn start_analyze(
file_id: String,
pdb_file_id: Option<String>,
client: &Client,
api_key: &str,
) -> Result<String, Box<dyn Error>> {
let mut query_params = HashMap::new();
query_params.insert("fileId".to_string(), file_id);
if let Some(pdb_id) = pdb_file_id {
query_params.insert("pdbFileId".to_string(), pdb_id);
}
let response = client
.put(&*ANALYZE_EP)
.header("Authorization", format!("ApiKey {}", api_key))
.query(&query_params)
.send()?
.error_for_status()?;
let json: HashMap<String, String> = response.json()?;
let execution_id = json
.get("executionId")
.cloned()
.ok_or("Missing executionId")?;
Ok(execution_id)
}
pub fn get_analyze_status(execution_id: String, client: &Client, api_key: &str) -> Status {
let mut query_params = HashMap::new();
query_params.insert("executionId".to_string(), execution_id);
let result = client
.get(&*ANALYZE_STATUS_EP)
.header("Authorization", format!("ApiKey {}", api_key))
.query(&query_params)
.send();
let Ok(resp) = result else {
return Status::Failed(Box::new(result.unwrap_err()));
};
let status = resp.status();
if status == StatusCode::ACCEPTED {
Status::Processing
} else if status == StatusCode::OK {
let json: serde_json::Value = match resp.json() {
Ok(v) => v,
Err(e) => return Status::Failed(Box::new(e)),
};
match json["analysisUrl"]
.as_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "missing analysisUrl"))
{
Ok(value) => Status::Ready(value.to_string()),
Err(e) => Status::Failed(Box::new(e)),
}
} else {
Status::Failed(Box::new(io::Error::new(
io::ErrorKind::Other,
format!("Internal error!"),
)))
}
}
pub fn download_analysis_result(
analysis_url: &str,
client: &Client,
) -> Result<AnalysisResult, Box<dyn Error>> {
let response = client.get(analysis_url).send()?.error_for_status()?;
let result_bytes = response.bytes()?;
let analysis_result: AnalysisResult = serde_json::from_slice(&result_bytes)?;
Ok(analysis_result)
}
pub fn defend(
uuid: String,
config: Config,
client: &Client,
api_key: &str,
) -> Result<String, reqwest::Error> {
let body = serde_json::to_string(&config).expect("Failed to serialize CDConfig");
let mut query_params = HashMap::new();
query_params.insert("fileId", uuid);
let response = client
.post(&*DEFEND_EP)
.header("Authorization", format!("ApiKey {}", api_key))
.header("Content-Type", "application/json")
.query(&query_params)
.body(body)
.send()?
.error_for_status()?;
response.text()
}
pub fn download(execution_id: String, client: &Client, api_key: &str) -> Status {
let mut query_params = HashMap::new();
query_params.insert("executionId".to_string(), execution_id);
let result = client
.get(&*DOWNLOAD_EP)
.header("Authorization", format!("ApiKey {}", api_key))
.query(&query_params)
.send();
let Ok(resp) = result else {
return Status::Failed(Box::new(result.unwrap_err()));
};
let status = resp.status();
if status == StatusCode::ACCEPTED {
Status::Processing
} else if status == StatusCode::OK {
let json: serde_json::Value = match resp.json() {
Ok(v) => v,
Err(e) => return Status::Failed(Box::new(e)),
};
match json["downloadUrl"]
.as_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "missing downloadUrl"))
{
Ok(value) => Status::Ready(value.to_string()),
Err(e) => Status::Failed(Box::new(e)),
}
} else {
Status::Failed(Box::new(io::Error::new(
io::ErrorKind::Other,
format!("Internal error!"),
)))
}
}
pub fn download_obfuscated_file(
download_url: &str,
client: &Client,
) -> Result<Vec<u8>, Box<dyn Error>> {
let response = client.get(download_url).send()?.error_for_status()?;
let bytes = response.bytes()?;
Ok(bytes.to_vec())
}