codedefender_api/lib.rs
1//! High-level client interface for interacting with the CodeDefender SaaS API.
2//!
3//! This module provides functions to upload files, analyze binaries, initiate
4//! obfuscation, and poll for obfuscation results via blocking HTTP requests.
5//!
6//! All endpoints require a valid API key, passed via the `Authorization` header
7//! using the `ApiKey` scheme.
8use codedefender_config::{AnalysisResult, Config};
9use reqwest::{StatusCode, blocking::Client};
10use std::collections::HashMap;
11use std::error::Error;
12use std::io;
13use once_cell::sync::Lazy;
14
15pub use codedefender_config;
16pub use serde_json;
17
18/// Changing the BASE_URL env variable allows you to specify a different backend like staging or local.
19pub static BASE_URL: Lazy<String> = Lazy::new(|| {
20 std::env::var("BASE_URL").unwrap_or_else(|_| "https://app.codedefender.io".into())
21});
22
23pub static GET_UPLOAD_URL_EP: Lazy<String> =
24 Lazy::new(|| format!("{}/api/get-upload-url", *BASE_URL));
25
26pub static ANALYZE_EP: Lazy<String> =
27 Lazy::new(|| format!("{}/api/analyze", *BASE_URL));
28
29pub static ANALYZE_STATUS_EP: Lazy<String> =
30 Lazy::new(|| format!("{}/api/analyze-status", *BASE_URL));
31
32pub static DEFEND_EP: Lazy<String> =
33 Lazy::new(|| format!("{}/api/defend", *BASE_URL));
34
35pub static DOWNLOAD_EP: Lazy<String> =
36 Lazy::new(|| format!("{}/api/download", *BASE_URL));
37
38pub enum Status {
39 Ready(String),
40 Processing,
41 Failed(Box<dyn Error>),
42}
43
44/// Gets the presigned upload URL and file ID for uploading a file.
45///
46/// # Arguments
47///
48/// * `file_size` - The size of the file to upload in bytes.
49/// * `file_name` - Optional custom file name. If provided, it will be used; otherwise, a random UUID will be generated by the server.
50/// * `client` - A preconfigured `reqwest::blocking::Client`.
51/// * `api_key` - Your CodeDefender API key.
52///
53/// # Returns
54///
55/// A `Result<(String, String), Box<dyn Error>>` containing the file ID and presigned upload URL on success.
56///
57/// # Errors
58///
59/// Returns an error if the request fails or if the server responds with a non-success status code.
60pub fn get_upload_info(
61 file_size: usize,
62 file_name: Option<String>,
63 client: &Client,
64 api_key: &str,
65) -> Result<(String, String), Box<dyn Error>> {
66 let mut query_params = HashMap::new();
67 query_params.insert("fileSize".to_string(), file_size.to_string());
68 if let Some(name) = file_name {
69 query_params.insert("fileName".to_string(), name);
70 }
71
72 let response = client
73 .get(&*GET_UPLOAD_URL_EP)
74 .header("Authorization", format!("ApiKey {}", api_key))
75 .query(&query_params)
76 .send()?
77 .error_for_status()?;
78
79 let json: HashMap<String, String> = response.json()?;
80 let upload_url = json.get("uploadUrl").cloned().ok_or("Missing uploadUrl")?;
81 let file_id = json.get("fileId").cloned().ok_or("Missing fileId")?;
82 Ok((file_id, upload_url))
83}
84
85/// Uploads file bytes to the presigned S3 URL.
86///
87/// # Arguments
88///
89/// * `upload_url` - The presigned S3 upload URL.
90/// * `file_bytes` - The raw contents of the file to upload.
91/// * `client` - A preconfigured `reqwest::blocking::Client`.
92///
93/// # Returns
94///
95/// `Ok(())` on success.
96///
97/// # Errors
98///
99/// Returns an error if the upload fails or the server responds with a non-success status code.
100pub fn upload_to_s3(
101 upload_url: &str,
102 file_bytes: Vec<u8>,
103 client: &Client,
104) -> Result<(), Box<dyn Error>> {
105 client
106 .put(upload_url)
107 .header("Content-Type", "application/octet-stream")
108 .header("Content-Length", file_bytes.len().to_string())
109 .body(file_bytes)
110 .send()?
111 .error_for_status()?;
112 Ok(())
113}
114
115/// Uploads raw data bytes to CodeDefender with a specific filename and returns the file ID.
116///
117/// # Arguments
118///
119/// * `data` - The raw bytes to upload.
120/// * `filename` - The specific filename to use for the upload.
121/// * `client` - A preconfigured `reqwest::blocking::Client`.
122/// * `api_key` - Your CodeDefender API key.
123///
124/// # Returns
125///
126/// A `Result<String, Box<dyn Error>>` containing the file ID on success.
127///
128/// # Errors
129///
130/// Returns an error if the request fails or if the server responds with a non-success status code.
131pub fn upload_data(
132 data: Vec<u8>,
133 filename: String,
134 client: &Client,
135 api_key: &str,
136) -> Result<String, Box<dyn Error>> {
137 let file_size = data.len();
138 let (file_id, upload_url) = get_upload_info(file_size, Some(filename), client, api_key)?;
139 upload_to_s3(&upload_url, data, client)?;
140 Ok(file_id)
141}
142
143/// Uploads a binary file to CodeDefender and returns a UUID representing the uploaded file.
144///
145/// # Arguments
146///
147/// * `file_bytes` - The raw contents of the binary file to upload.
148/// * `client` - A preconfigured `reqwest::blocking::Client`.
149/// * `api_key` - Your CodeDefender API key.
150///
151/// # Returns
152///
153/// A `Result<String, Box<dyn Error>>` containing the UUID on success, or an error if the upload failed.
154///
155/// # Errors
156///
157/// Returns an error if the request fails or if the server responds with a non-success status code (not in 200..=299).
158pub fn upload_file(
159 file_bytes: Vec<u8>,
160 client: &Client,
161 api_key: &str,
162) -> Result<String, Box<dyn Error>> {
163 let file_size = file_bytes.len();
164 let (file_id, upload_url) = get_upload_info(file_size, None, client, api_key)?;
165 upload_to_s3(&upload_url, file_bytes, client)?;
166 Ok(file_id)
167}
168
169/// Starts analysis of a previously uploaded binary file and optionally its PDB file.
170///
171/// # Arguments
172///
173/// * `file_id` - UUID of the uploaded binary file.
174/// * `pdb_file_id` - Optional UUID of the associated PDB file.
175/// * `client` - A preconfigured `reqwest::blocking::Client`.
176/// * `api_key` - Your CodeDefender API key.
177///
178/// # Returns
179///
180/// A `Result<String, Box<dyn Error>>` containing the execution ID for polling.
181///
182/// # Errors
183///
184/// Returns an error if the request fails or the server responds with a non-success status.
185pub fn start_analyze(
186 file_id: String,
187 pdb_file_id: Option<String>,
188 client: &Client,
189 api_key: &str,
190) -> Result<String, Box<dyn Error>> {
191 let mut query_params = HashMap::new();
192 query_params.insert("fileId".to_string(), file_id);
193 if let Some(pdb_id) = pdb_file_id {
194 query_params.insert("pdbFileId".to_string(), pdb_id);
195 }
196 let response = client
197 .put(&*ANALYZE_EP)
198 .header("Authorization", format!("ApiKey {}", api_key))
199 .query(&query_params)
200 .send()?
201 .error_for_status()?;
202 let json: HashMap<String, String> = response.json()?;
203 let execution_id = json
204 .get("executionId")
205 .cloned()
206 .ok_or("Missing executionId")?;
207 Ok(execution_id)
208}
209
210/// Polls the analysis status.
211///
212/// This endpoint should be called periodically until the analysis is complete.
213///
214/// # Arguments
215///
216/// * `execution_id` - The execution ID returned by [`start_analyze`].
217/// * `client` - A preconfigured `reqwest::blocking::Client`.
218/// * `api_key` - Your CodeDefender API key.
219///
220/// # Returns
221///
222/// An [`Status`] enum indicating whether the analysis is ready (with presigned URL), still processing, or failed.
223pub fn get_analyze_status(execution_id: String, client: &Client, api_key: &str) -> Status {
224 let mut query_params = HashMap::new();
225 query_params.insert("executionId".to_string(), execution_id);
226 let result = client
227 .get(&*ANALYZE_STATUS_EP)
228 .header("Authorization", format!("ApiKey {}", api_key))
229 .query(&query_params)
230 .send();
231 let Ok(resp) = result else {
232 return Status::Failed(Box::new(result.unwrap_err()));
233 };
234 let status = resp.status();
235 if status == StatusCode::ACCEPTED {
236 Status::Processing
237 } else if status == StatusCode::OK {
238 let json: serde_json::Value = match resp.json() {
239 Ok(v) => v,
240 Err(e) => return Status::Failed(Box::new(e)),
241 };
242
243 match json["analysisUrl"]
244 .as_str()
245 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "missing analysisUrl"))
246 {
247 Ok(value) => Status::Ready(value.to_string()),
248 Err(e) => Status::Failed(Box::new(e)),
249 }
250 } else {
251 Status::Failed(Box::new(io::Error::new(
252 io::ErrorKind::Other,
253 format!("Internal error!"),
254 )))
255 }
256}
257
258/// Downloads and deserializes the analysis result from the presigned URL.
259///
260/// # Arguments
261///
262/// * `analysis_url` - The presigned URL returned by [`get_analyze_status`] when ready.
263/// * `client` - A preconfigured `reqwest::blocking::Client`.
264///
265/// # Returns
266///
267/// An `AnalysisResult` containing metadata about the uploaded binary.
268///
269/// # Errors
270///
271/// Returns an error if the download fails, the server responds with a non-success status, or deserialization fails.
272pub fn download_analysis_result(
273 analysis_url: &str,
274 client: &Client,
275) -> Result<AnalysisResult, Box<dyn Error>> {
276 let response = client.get(analysis_url).send()?.error_for_status()?;
277 let result_bytes = response.bytes()?;
278 let analysis_result: AnalysisResult = serde_json::from_slice(&result_bytes)?;
279 Ok(analysis_result)
280}
281
282/// Starts the obfuscation process for a given file using the provided configuration.
283///
284/// # Arguments
285///
286/// * `uuid` - UUID of the uploaded binary file (not the PDB).
287/// * `config` - Obfuscation configuration as a `CDConfig`.
288/// * `client` - A preconfigured `reqwest::blocking::Client`.
289/// * `api_key` - Your CodeDefender API key.
290///
291/// # Returns
292///
293/// A `Result<String, reqwest::Error>` containing the `execution_id` used for polling.
294///
295/// # Errors
296///
297/// Returns an error if the request fails or the server returns a non-success status.
298pub fn defend(
299 uuid: String,
300 config: Config,
301 client: &Client,
302 api_key: &str,
303) -> Result<String, reqwest::Error> {
304 let body = serde_json::to_string(&config).expect("Failed to serialize CDConfig");
305 let mut query_params = HashMap::new();
306 query_params.insert("fileId", uuid);
307
308 let response = client
309 .post(&*DEFEND_EP)
310 .header("Authorization", format!("ApiKey {}", api_key))
311 .header("Content-Type", "application/json")
312 .query(&query_params)
313 .body(body)
314 .send()?
315 .error_for_status()?;
316
317 response.text()
318}
319
320/// Polls the obfuscation status.
321///
322/// This endpoint should be called every 500 milliseconds until the obfuscation is complete.
323///
324/// ⚠️ Note: This endpoint is rate-limited to **200 requests per minute**.
325///
326/// # Arguments
327///
328/// * `execution_id` - The execution ID returned by [`defend`].
329/// * `client` - A preconfigured `reqwest::blocking::Client`.
330/// * `api_key` - Your CodeDefender API key.
331///
332/// # Returns
333///
334/// A [`Status`] enum indicating whether the file is ready (with presigned URL), still processing, or failed.
335pub fn download(execution_id: String, client: &Client, api_key: &str) -> Status {
336 let mut query_params = HashMap::new();
337 query_params.insert("executionId".to_string(), execution_id);
338 let result = client
339 .get(&*DOWNLOAD_EP)
340 .header("Authorization", format!("ApiKey {}", api_key))
341 .query(&query_params)
342 .send();
343 let Ok(resp) = result else {
344 return Status::Failed(Box::new(result.unwrap_err()));
345 };
346 let status = resp.status();
347 if status == StatusCode::ACCEPTED {
348 Status::Processing
349 } else if status == StatusCode::OK {
350 let json: serde_json::Value = match resp.json() {
351 Ok(v) => v,
352 Err(e) => return Status::Failed(Box::new(e)),
353 };
354
355 match json["downloadUrl"]
356 .as_str()
357 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "missing downloadUrl"))
358 {
359 Ok(value) => Status::Ready(value.to_string()),
360 Err(e) => Status::Failed(Box::new(e)),
361 }
362 } else {
363 Status::Failed(Box::new(io::Error::new(
364 io::ErrorKind::Other,
365 format!("Internal error!"),
366 )))
367 }
368}
369
370/// Downloads the obfuscated file from the presigned URL.
371///
372/// # Arguments
373///
374/// * `download_url` - The presigned URL returned by [`download`] when ready.
375/// * `client` - A preconfigured `reqwest::blocking::Client`.
376///
377/// # Returns
378///
379/// A `Result<Vec<u8>, Box<dyn Error>>` containing the obfuscated file bytes.
380///
381/// # Errors
382///
383/// Returns an error if the download fails or the server responds with a non-success status.
384pub fn download_obfuscated_file(
385 download_url: &str,
386 client: &Client,
387) -> Result<Vec<u8>, Box<dyn Error>> {
388 let response = client.get(download_url).send()?.error_for_status()?;
389 let bytes = response.bytes()?;
390 Ok(bytes.to_vec())
391}