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}