1use std::time::{SystemTime, UNIX_EPOCH};
7use std::collections::HashMap;
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10use hex;
11use reqwest::{Client, multipart};
12use url::Url;
13use serde::Serialize;
14
15use crate::{VeracodeConfig, VeracodeError};
16
17type HmacSha256 = Hmac<Sha256>;
19
20#[derive(Clone)]
25pub struct VeracodeClient {
26 config: VeracodeConfig,
27 client: Client,
28}
29
30impl VeracodeClient {
31 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
41 let mut client_builder = Client::builder();
42
43 if !config.validate_certificates {
45 client_builder = client_builder
46 .danger_accept_invalid_certs(true)
47 .danger_accept_invalid_hostnames(true);
48 }
49
50 let client = client_builder.build().map_err(VeracodeError::Http)?;
51 Ok(Self { config, client })
52 }
53
54 pub fn base_url(&self) -> &str {
56 &self.config.base_url
57 }
58
59 pub fn config(&self) -> &VeracodeConfig {
61 &self.config
62 }
63
64 pub fn client(&self) -> &Client {
66 &self.client
67 }
68
69 fn generate_hmac_signature(&self, method: &str, url: &str, timestamp: u64, nonce: &str) -> Result<String, VeracodeError> {
71 let url_parsed = Url::parse(url)
72 .map_err(|_| VeracodeError::Authentication("Invalid URL".to_string()))?;
73
74 let path_and_query = match url_parsed.query() {
75 Some(query) => format!("{}?{}", url_parsed.path(), query),
76 None => url_parsed.path().to_string(),
77 };
78
79 let host = url_parsed.host_str().unwrap_or("");
80
81 let data = format!("id={}&host={}&url={}&method={}",
84 self.config.api_id,
85 host,
86 path_and_query,
87 method);
88
89 let timestamp_str = timestamp.to_string();
90 let ver_str = "vcode_request_version_1";
91
92 let key_bytes = hex::decode(&self.config.api_key)
94 .map_err(|_| VeracodeError::Authentication("Invalid API key format - must be hex string".to_string()))?;
95
96 let nonce_bytes = hex::decode(nonce)
97 .map_err(|_| VeracodeError::Authentication("Invalid nonce format".to_string()))?;
98
99 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
101 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
102 mac1.update(&nonce_bytes);
103 let hashed_nonce = mac1.finalize().into_bytes();
104
105 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
107 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
108 mac2.update(timestamp_str.as_bytes());
109 let hashed_timestamp = mac2.finalize().into_bytes();
110
111 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
113 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
114 mac3.update(ver_str.as_bytes());
115 let hashed_ver_str = mac3.finalize().into_bytes();
116
117 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
119 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
120 mac4.update(data.as_bytes());
121 let signature = mac4.finalize().into_bytes();
122
123 Ok(hex::encode(signature).to_lowercase())
125 }
126
127 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
129 let timestamp = SystemTime::now()
130 .duration_since(UNIX_EPOCH)
131 .unwrap()
132 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
136 let nonce = hex::encode(nonce_bytes);
137
138 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
139
140 Ok(format!("VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
141 self.config.api_id, timestamp, nonce, signature))
142 }
143
144 pub async fn get(&self, endpoint: &str, query_params: Option<&[(String, String)]>) -> Result<reqwest::Response, VeracodeError> {
155 let mut url = format!("{}{}", self.config.base_url, endpoint);
156
157 if let Some(params) = query_params {
158 if !params.is_empty() {
159 url.push('?');
160 url.push_str(¶ms.iter()
161 .map(|(k, v)| format!("{k}={v}"))
162 .collect::<Vec<_>>()
163 .join("&"));
164 }
165 }
166
167 let auth_header = self.generate_auth_header("GET", &url)?;
168
169 let response = self.client
170 .get(&url)
171 .header("Authorization", auth_header)
172 .header("Content-Type", "application/json")
173 .send()
174 .await?;
175
176 Ok(response)
177 }
178
179 pub async fn post<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
190 let url = format!("{}{}", self.config.base_url, endpoint);
191 let auth_header = self.generate_auth_header("POST", &url)?;
192
193 let mut request = self.client
194 .post(&url)
195 .header("Authorization", auth_header)
196 .header("Content-Type", "application/json");
197
198 if let Some(body) = body {
199 request = request.json(body);
200 }
201
202 let response = request.send().await?;
203 Ok(response)
204 }
205
206 pub async fn put<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
217 let url = format!("{}{}", self.config.base_url, endpoint);
218 let auth_header = self.generate_auth_header("PUT", &url)?;
219
220 let mut request = self.client
221 .put(&url)
222 .header("Authorization", auth_header)
223 .header("Content-Type", "application/json");
224
225 if let Some(body) = body {
226 request = request.json(body);
227 }
228
229 let response = request.send().await?;
230 Ok(response)
231 }
232
233 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
243 let url = format!("{}{}", self.config.base_url, endpoint);
244 let auth_header = self.generate_auth_header("DELETE", &url)?;
245
246 let response = self.client
247 .delete(&url)
248 .header("Authorization", auth_header)
249 .header("Content-Type", "application/json")
250 .send()
251 .await?;
252
253 Ok(response)
254 }
255
256 pub async fn handle_response(response: reqwest::Response) -> Result<reqwest::Response, VeracodeError> {
268 if !response.status().is_success() {
269 let status = response.status();
270 let error_text = response.text().await?;
271 return Err(VeracodeError::InvalidResponse(
272 format!("HTTP {status}: {error_text}")
273 ));
274 }
275 Ok(response)
276 }
277
278 pub async fn get_with_query(&self, endpoint: &str, query_params: Option<Vec<(String, String)>>) -> Result<reqwest::Response, VeracodeError> {
291 let query_slice = query_params.as_deref();
292 let response = self.get(endpoint, query_slice).await?;
293 Self::handle_response(response).await
294 }
295
296 pub async fn post_with_response<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
307 let response = self.post(endpoint, body).await?;
308 Self::handle_response(response).await
309 }
310
311 pub async fn put_with_response<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
322 let response = self.put(endpoint, body).await?;
323 Self::handle_response(response).await
324 }
325
326 pub async fn delete_with_response(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
336 let response = self.delete(endpoint).await?;
337 Self::handle_response(response).await
338 }
339
340 pub async fn get_paginated(&self, endpoint: &str, base_query_params: Option<Vec<(String, String)>>, page_size: Option<u32>) -> Result<String, VeracodeError> {
355 let size = page_size.unwrap_or(500);
356 let mut page = 0;
357 let mut all_items = Vec::new();
358 let mut page_info = None;
359
360 loop {
361 let mut query_params = base_query_params.clone().unwrap_or_default();
362 query_params.push(("page".to_string(), page.to_string()));
363 query_params.push(("size".to_string(), size.to_string()));
364
365 let response = self.get_with_query(endpoint, Some(query_params)).await?;
366 let response_text = response.text().await?;
367
368 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
370 if let Some(embedded) = json_value.get("_embedded") {
372 if let Some(items_array) = embedded.as_object().and_then(|obj| obj.values().next()) {
373 if let Some(items) = items_array.as_array() {
374 if items.is_empty() {
375 break; }
377 all_items.extend(items.clone());
378 }
379 }
380 } else if let Some(items) = json_value.as_array() {
381 if items.is_empty() {
383 break;
384 }
385 all_items.extend(items.clone());
386 } else {
387 return Ok(response_text);
389 }
390
391 if let Some(page_obj) = json_value.get("page") {
393 page_info = Some(page_obj.clone());
394 if let (Some(current), Some(total)) = (
395 page_obj.get("number").and_then(|n| n.as_u64()),
396 page_obj.get("totalPages").and_then(|n| n.as_u64())
397 ) {
398 if current + 1 >= total {
399 break; }
401 }
402 }
403 } else {
404 return Ok(response_text);
406 }
407
408 page += 1;
409
410 if page > 100 {
412 break;
413 }
414 }
415
416 let combined_response = if let Some(page_info) = page_info {
418 serde_json::json!({
420 "_embedded": {
421 "roles": all_items },
423 "page": page_info
424 })
425 } else {
426 serde_json::Value::Array(all_items)
428 };
429
430 Ok(combined_response.to_string())
431 }
432
433 pub async fn get_with_params(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<reqwest::Response, VeracodeError> {
444 let url = format!("{}{}", self.config.base_url, endpoint);
445 let mut request_url = Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
446
447 if !params.is_empty() {
449 let mut query_pairs = request_url.query_pairs_mut();
450 for (key, value) in params {
451 query_pairs.append_pair(key, value);
452 }
453 }
454
455 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
456
457 let response = self.client
458 .get(request_url)
459 .header("Authorization", auth_header)
460 .header("User-Agent", "Veracode Rust Client")
461 .send()
462 .await?;
463
464 Ok(response)
465 }
466
467 pub async fn post_form(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<reqwest::Response, VeracodeError> {
478 let url = format!("{}{}", self.config.base_url, endpoint);
479
480 let mut form_data = Vec::new();
482 for (key, value) in params {
483 form_data.push((key.to_string(), value.to_string()));
484 }
485
486 let auth_header = self.generate_auth_header("POST", &url)?;
487
488 let response = self.client
489 .post(&url)
490 .header("Authorization", auth_header)
491 .header("User-Agent", "Veracode Rust Client")
492 .form(&form_data)
493 .send()
494 .await?;
495
496 Ok(response)
497 }
498
499 pub async fn upload_file_multipart(
513 &self,
514 endpoint: &str,
515 params: HashMap<&str, &str>,
516 file_field_name: &str,
517 filename: &str,
518 file_data: Vec<u8>,
519 ) -> Result<reqwest::Response, VeracodeError> {
520 let url = format!("{}{}", self.config.base_url, endpoint);
521
522 let mut form = multipart::Form::new();
524
525 for (key, value) in params {
527 form = form.text(key.to_string(), value.to_string());
528 }
529
530 let part = multipart::Part::bytes(file_data)
532 .file_name(filename.to_string())
533 .mime_str("application/octet-stream")
534 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
535
536 form = form.part(file_field_name.to_string(), part);
537
538 let auth_header = self.generate_auth_header("POST", &url)?;
539
540 let response = self.client
541 .post(&url)
542 .header("Authorization", auth_header)
543 .header("User-Agent", "Veracode Rust Client")
544 .multipart(form)
545 .send()
546 .await?;
547
548 Ok(response)
549 }
550
551 pub async fn upload_file_multipart_put(
565 &self,
566 url: &str,
567 file_field_name: &str,
568 filename: &str,
569 file_data: Vec<u8>,
570 additional_headers: Option<HashMap<&str, &str>>,
571 ) -> Result<reqwest::Response, VeracodeError> {
572 let part = multipart::Part::bytes(file_data)
574 .file_name(filename.to_string())
575 .mime_str("application/octet-stream")
576 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
577
578 let form = multipart::Form::new().part(file_field_name.to_string(), part);
579
580 let auth_header = self.generate_auth_header("PUT", url)?;
581
582 let mut request = self.client
583 .put(url)
584 .header("Authorization", auth_header)
585 .header("User-Agent", "Veracode Rust Client")
586 .multipart(form);
587
588 if let Some(headers) = additional_headers {
590 for (key, value) in headers {
591 request = request.header(key, value);
592 }
593 }
594
595 let response = request.send().await?;
596 Ok(response)
597 }
598
599 pub async fn upload_file_with_query_params(
616 &self,
617 endpoint: &str,
618 query_params: &[(&str, &str)],
619 file_field_name: &str,
620 filename: &str,
621 file_data: Vec<u8>,
622 ) -> Result<reqwest::Response, VeracodeError> {
623 let mut url = format!("{}/{}", self.config.base_url, endpoint);
625
626 if !query_params.is_empty() {
627 url.push('?');
628 let encoded_params: Vec<String> = query_params
629 .iter()
630 .map(|(key, value)| {
631 format!("{}={}",
632 urlencoding::encode(key),
633 urlencoding::encode(value))
634 })
635 .collect();
636 url.push_str(&encoded_params.join("&"));
637 }
638
639 let part = multipart::Part::bytes(file_data)
641 .file_name(filename.to_string())
642 .mime_str("application/octet-stream")
643 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
644
645 let form = multipart::Form::new().part(file_field_name.to_string(), part);
646
647 let auth_header = self.generate_auth_header("POST", &url)?;
648
649 let response = self.client
650 .post(&url)
651 .header("Authorization", auth_header)
652 .header("User-Agent", "Veracode Rust Client")
653 .multipart(form)
654 .send()
655 .await?;
656
657 Ok(response)
658 }
659
660 pub async fn post_with_query_params(
674 &self,
675 endpoint: &str,
676 query_params: &[(&str, &str)],
677 ) -> Result<reqwest::Response, VeracodeError> {
678 let mut url = format!("{}/{}", self.config.base_url, endpoint);
680
681 if !query_params.is_empty() {
682 url.push('?');
683 let encoded_params: Vec<String> = query_params
684 .iter()
685 .map(|(key, value)| {
686 format!("{}={}",
687 urlencoding::encode(key),
688 urlencoding::encode(value))
689 })
690 .collect();
691 url.push_str(&encoded_params.join("&"));
692 }
693
694 let auth_header = self.generate_auth_header("POST", &url)?;
695
696 let response = self.client
697 .post(&url)
698 .header("Authorization", auth_header)
699 .header("User-Agent", "Veracode Rust Client")
700 .send()
701 .await?;
702
703 Ok(response)
704 }
705
706 pub async fn get_with_query_params(
720 &self,
721 endpoint: &str,
722 query_params: &[(&str, &str)],
723 ) -> Result<reqwest::Response, VeracodeError> {
724 let mut url = format!("{}/{}", self.config.base_url, endpoint);
726
727 if !query_params.is_empty() {
728 url.push('?');
729 let encoded_params: Vec<String> = query_params
730 .iter()
731 .map(|(key, value)| {
732 format!("{}={}",
733 urlencoding::encode(key),
734 urlencoding::encode(value))
735 })
736 .collect();
737 url.push_str(&encoded_params.join("&"));
738 }
739
740 let auth_header = self.generate_auth_header("GET", &url)?;
741
742 let response = self.client
743 .get(&url)
744 .header("Authorization", auth_header)
745 .header("User-Agent", "Veracode Rust Client")
746 .send()
747 .await?;
748
749 Ok(response)
750 }
751
752 pub async fn upload_large_file_chunked<F>(
769 &self,
770 endpoint: &str,
771 query_params: &[(&str, &str)],
772 file_path: &str,
773 content_type: Option<&str>,
774 progress_callback: Option<F>,
775 ) -> Result<reqwest::Response, VeracodeError>
776 where
777 F: Fn(u64, u64, f64) + Send + Sync,
778 {
779 use std::fs::File;
780 use std::io::{Read, Seek, SeekFrom};
781
782 let mut url = format!("{}/{}", self.config.base_url, endpoint);
784
785 if !query_params.is_empty() {
786 url.push('?');
787 let encoded_params: Vec<String> = query_params
788 .iter()
789 .map(|(key, value)| {
790 format!("{}={}",
791 urlencoding::encode(key),
792 urlencoding::encode(value))
793 })
794 .collect();
795 url.push_str(&encoded_params.join("&"));
796 }
797
798 let mut file = File::open(file_path)
800 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
801
802 let file_size = file.metadata()
803 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
804 .len();
805
806 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
809 return Err(VeracodeError::InvalidConfig(
810 format!("File size ({} bytes) exceeds maximum limit of {} bytes", file_size, MAX_FILE_SIZE)
811 ));
812 }
813
814 file.seek(SeekFrom::Start(0))
816 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
817
818 let mut file_data = Vec::with_capacity(file_size as usize);
819 file.read_to_end(&mut file_data)
820 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
821
822 let auth_header = self.generate_auth_header("POST", &url)?;
824
825 let request = self.client
827 .post(&url)
828 .header("Authorization", auth_header)
829 .header("User-Agent", "Veracode Rust Client")
830 .header("Content-Type", content_type.unwrap_or("binary/octet-stream"))
831 .header("Content-Length", file_size.to_string())
832 .body(file_data);
833
834 if let Some(callback) = progress_callback {
836 callback(file_size, file_size, 100.0);
837 }
838
839 let response = request.send().await?;
840 Ok(response)
841 }
842
843 pub async fn upload_file_binary(
859 &self,
860 endpoint: &str,
861 query_params: &[(&str, &str)],
862 file_data: Vec<u8>,
863 content_type: &str,
864 ) -> Result<reqwest::Response, VeracodeError> {
865 let mut url = format!("{}/{}", self.config.base_url, endpoint);
867
868 if !query_params.is_empty() {
869 url.push('?');
870 let encoded_params: Vec<String> = query_params
871 .iter()
872 .map(|(key, value)| {
873 format!("{}={}",
874 urlencoding::encode(key),
875 urlencoding::encode(value))
876 })
877 .collect();
878 url.push_str(&encoded_params.join("&"));
879 }
880
881 let auth_header = self.generate_auth_header("POST", &url)?;
882
883 let response = self.client
884 .post(&url)
885 .header("Authorization", auth_header)
886 .header("User-Agent", "Veracode Rust Client")
887 .header("Content-Type", content_type)
888 .header("Content-Length", file_data.len().to_string())
889 .body(file_data)
890 .send()
891 .await?;
892
893 Ok(response)
894 }
895}