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 .error_for_status()?;
57
58 response.text()
59}
60
61/// Analyzes a previously uploaded binary file and optionally its PDB file.
62///
63/// # Arguments
64///
65/// * `file_id` - UUID of the uploaded binary file.
66/// * `pdb_file_id` - Optional UUID of the associated PDB file.
67/// * `client` - A preconfigured `reqwest::blocking::Client`.
68/// * `api_key` - Your CodeDefender API key.
69///
70/// # Returns
71///
72/// An `AnalysisResult` containing metadata about the uploaded binary.
73///
74/// # Errors
75///
76/// Returns an error if the request fails or the server responds with a non-success status.
77/// Panics if JSON deserialization fails (future versions should return a custom error instead).
78pub fn analyze_program(
79 file_id: String,
80 pdb_file_id: Option<String>,
81 client: &Client,
82 api_key: &str,
83) -> Result<AnalysisResult, reqwest::Error> {
84 let mut query_params = HashMap::new();
85 query_params.insert("fileId", file_id);
86 if let Some(pdb_id) = pdb_file_id {
87 query_params.insert("pdbFileId", pdb_id);
88 }
89
90 let response = client
91 .put(ANALYZE_EP)
92 .header("Authorization", format!("ApiKey {}", api_key))
93 .query(&query_params)
94 .send()?
95 .error_for_status()?;
96
97 let result_bytes = response.bytes()?;
98 let analysis_result: AnalysisResult =
99 serde_json::from_slice(&result_bytes).expect("Failed to deserialize analysis result");
100
101 Ok(analysis_result)
102}
103
104/// Starts the obfuscation process for a given file using the provided configuration.
105///
106/// # Arguments
107///
108/// * `uuid` - UUID of the uploaded binary file (not the PDB).
109/// * `config` - Obfuscation configuration as a `CDConfig`.
110/// * `client` - A preconfigured `reqwest::blocking::Client`.
111/// * `api_key` - Your CodeDefender API key.
112///
113/// # Returns
114///
115/// A `Result<String, reqwest::Error>` containing the `execution_id` used for polling.
116///
117/// # Errors
118///
119/// Returns an error if the request fails or the server returns a non-success status.
120pub fn defend(
121 uuid: String,
122 config: CDConfig,
123 client: &Client,
124 api_key: &str,
125) -> Result<String, reqwest::Error> {
126 let body = serde_json::to_string(&config).expect("Failed to serialize CDConfig");
127 let mut query_params = HashMap::new();
128 query_params.insert("fileId", uuid);
129
130 let response = client
131 .post(DEFEND_EP)
132 .header("Authorization", format!("ApiKey {}", api_key))
133 .header("Content-Type", "application/json")
134 .query(&query_params)
135 .body(body)
136 .send()?
137 .error_for_status()?;
138
139 response.text()
140}
141
142/// Polls the obfuscation status or retrieves the obfuscated file.
143///
144/// This endpoint should be called every 500 milliseconds until the obfuscation is complete.
145///
146/// ⚠️ Note: This endpoint is rate-limited to **200 requests per minute**.
147///
148/// # Arguments
149///
150/// * `uuid` - The execution ID returned by [`defend`].
151/// * `client` - A preconfigured `reqwest::blocking::Client`.
152/// * `api_key` - Your CodeDefender API key.
153///
154/// # Returns
155///
156/// A [`DownloadStatus`] enum indicating whether the file is ready, still processing, or failed.
157pub fn download(uuid: String, client: &Client, api_key: &str) -> DownloadStatus {
158 let mut query_params = HashMap::new();
159 query_params.insert("executionId", uuid);
160
161 let response = client
162 .get(DOWNLOAD_EP)
163 .header("Authorization", format!("ApiKey {}", api_key))
164 .query(&query_params)
165 .send();
166
167 match response {
168 Ok(resp) => match resp.error_for_status() {
169 Ok(resp) => {
170 if resp.status() == StatusCode::ACCEPTED {
171 DownloadStatus::Processing
172 } else {
173 match resp.bytes() {
174 Ok(bytes) => DownloadStatus::Ready(bytes.to_vec()),
175 Err(e) => DownloadStatus::Failed(e),
176 }
177 }
178 }
179 Err(e) => DownloadStatus::Failed(e),
180 },
181 Err(e) => DownloadStatus::Failed(e),
182 }
183}