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.
8
9use codedefender_config::{AnalysisResult, CDConfig};
10use reqwest::{blocking::Client, StatusCode};
11use std::collections::HashMap;
12
13const UPLOAD_EP: &str = "https://app.codedefender.io/api/upload";
14const ANALYZE_EP: &str = "https://app.codedefender.io/api/analyze";
15const DEFEND_EP: &str = "https://app.codedefender.io/api/defend";
16const DOWNLOAD_EP: &str = "https://app.codedefender.io/api/download";
17
18/// Represents the result of a call to [`download`].
19pub enum DownloadStatus {
20 /// The obfuscated file is ready and contains the returned bytes.
21 Ready(Vec<u8>),
22
23 /// The obfuscation is still in progress.
24 Processing,
25
26 /// The download failed due to a network or server error.
27 Failed(reqwest::Error),
28}
29
30/// Uploads a binary file to CodeDefender and returns a UUID representing the uploaded file.
31///
32/// # Arguments
33///
34/// * `file_bytes` - The raw contents of the binary file to upload.
35/// * `client` - A preconfigured `reqwest::blocking::Client`.
36/// * `api_key` - Your CodeDefender API key.
37///
38/// # Returns
39///
40/// A `Result<String, reqwest::Error>` containing the UUID on success, or an error if the upload failed.
41///
42/// # Errors
43///
44/// Returns an error if the request fails or if the server responds with a non-success status code (not in 200..=299).
45pub fn upload_file(
46 file_bytes: Vec<u8>,
47 client: &Client,
48 api_key: &str,
49) -> Result<String, reqwest::Error> {
50 let response = client
51 .put(UPLOAD_EP)
52 .header("Authorization", format!("ApiKey {}", api_key))
53 .header("Content-Type", "application/octet-stream")
54 .body(file_bytes)
55 .send()?;
56
57 response.text()
58}
59
60/// Analyzes a previously uploaded binary file and optionally its PDB file.
61///
62/// # Arguments
63///
64/// * `file_id` - UUID of the uploaded binary file.
65/// * `pdb_file_id` - Optional UUID of the associated PDB file.
66/// * `client` - A preconfigured `reqwest::blocking::Client`.
67/// * `api_key` - Your CodeDefender API key.
68///
69/// # Returns
70///
71/// An `AnalysisResult` containing metadata about the uploaded binary.
72///
73/// # Errors
74///
75/// Returns an error if the request fails or the server responds with a non-success status.
76/// Panics if JSON deserialization fails (future versions should return a custom error instead).
77pub fn analyze_program(
78 file_id: String,
79 pdb_file_id: Option<String>,
80 client: &Client,
81 api_key: &str,
82) -> Result<AnalysisResult, reqwest::Error> {
83 let mut query_params = HashMap::new();
84 query_params.insert("fileId", file_id);
85 if let Some(pdb_id) = pdb_file_id {
86 query_params.insert("pdbFileId", pdb_id);
87 }
88
89 let response = client
90 .put(ANALYZE_EP)
91 .header("Authorization", format!("ApiKey {}", api_key))
92 .query(&query_params)
93 .send()?
94 .error_for_status()?;
95
96 let result_bytes = response.bytes()?;
97 let analysis_result: AnalysisResult =
98 serde_json::from_slice(&result_bytes).expect("Failed to deserialize analysis result");
99
100 Ok(analysis_result)
101}
102
103/// Starts the obfuscation process for a given file using the provided configuration.
104///
105/// # Arguments
106///
107/// * `uuid` - UUID of the uploaded binary file (not the PDB).
108/// * `config` - Obfuscation configuration as a `CDConfig`.
109/// * `client` - A preconfigured `reqwest::blocking::Client`.
110/// * `api_key` - Your CodeDefender API key.
111///
112/// # Returns
113///
114/// A `Result<String, reqwest::Error>` containing the `execution_id` used for polling.
115///
116/// # Errors
117///
118/// Returns an error if the request fails or the server returns a non-success status.
119pub fn defend(
120 uuid: String,
121 config: CDConfig,
122 client: &Client,
123 api_key: &str,
124) -> Result<String, reqwest::Error> {
125 let body = serde_json::to_string(&config).expect("Failed to serialize CDConfig");
126 let mut query_params = HashMap::new();
127 query_params.insert("fileId", uuid);
128
129 let response = client
130 .post(DEFEND_EP)
131 .header("Authorization", format!("ApiKey {}", api_key))
132 .header("Content-Type", "application/json")
133 .query(&query_params)
134 .body(body)
135 .send()?
136 .error_for_status()?;
137
138 response.text()
139}
140
141/// Polls the obfuscation status or retrieves the obfuscated file.
142///
143/// This endpoint should be called every 500 milliseconds until the obfuscation is complete.
144///
145/// ⚠️ Note: This endpoint is rate-limited to **200 requests per minute**.
146///
147/// # Arguments
148///
149/// * `uuid` - The execution ID returned by [`defend`].
150/// * `client` - A preconfigured `reqwest::blocking::Client`.
151/// * `api_key` - Your CodeDefender API key.
152///
153/// # Returns
154///
155/// A [`DownloadStatus`] enum indicating whether the file is ready, still processing, or failed.
156pub fn download(uuid: String, client: &Client, api_key: &str) -> DownloadStatus {
157 let mut query_params = HashMap::new();
158 query_params.insert("executionId", uuid);
159
160 let response = client
161 .get(DOWNLOAD_EP)
162 .header("Authorization", format!("ApiKey {}", api_key))
163 .query(&query_params)
164 .send();
165
166 match response {
167 Ok(resp) => match resp.error_for_status() {
168 Ok(resp) => {
169 if resp.status() == StatusCode::ACCEPTED {
170 DownloadStatus::Processing
171 } else {
172 match resp.bytes() {
173 Ok(bytes) => DownloadStatus::Ready(bytes.to_vec()),
174 Err(e) => DownloadStatus::Failed(e),
175 }
176 }
177 }
178 Err(e) => DownloadStatus::Failed(e),
179 },
180 Err(e) => DownloadStatus::Failed(e),
181 }
182}