1use hex;
7use hmac::{Hmac, Mac};
8use log::{info, warn};
9use reqwest::{Client, multipart};
10use secrecy::ExposeSecret;
11use serde::Serialize;
12use sha2::Sha256;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17use std::sync::Arc;
18use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
19use url::Url;
20
21use crate::{VeracodeConfig, VeracodeError};
22
23type HmacSha256 = Hmac<Sha256>;
25
26const INVALID_URL_MSG: &str = "Invalid URL";
28const INVALID_API_KEY_MSG: &str = "Invalid API key format - must be hex string";
29const INVALID_NONCE_MSG: &str = "Invalid nonce format";
30const HMAC_CREATION_FAILED_MSG: &str = "Failed to create HMAC";
31
32#[derive(Clone)]
37pub struct VeracodeClient {
38 config: VeracodeConfig,
39 client: Client,
40}
41
42impl VeracodeClient {
43 fn build_url_with_params(&self, endpoint: &str, query_params: &[(&str, &str)]) -> String {
45 let estimated_capacity =
47 self.config.base_url.len() + endpoint.len() + query_params.len() * 32; let mut url = String::with_capacity(estimated_capacity);
50 url.push_str(&self.config.base_url);
51 url.push_str(endpoint);
52
53 if !query_params.is_empty() {
54 url.push('?');
55 for (i, (key, value)) in query_params.iter().enumerate() {
56 if i > 0 {
57 url.push('&');
58 }
59 url.push_str(&urlencoding::encode(key));
60 url.push('=');
61 url.push_str(&urlencoding::encode(value));
62 }
63 }
64
65 url
66 }
67
68 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
78 let mut client_builder = Client::builder();
79
80 if !config.validate_certificates {
82 client_builder = client_builder
83 .danger_accept_invalid_certs(true)
84 .danger_accept_invalid_hostnames(true);
85 }
86
87 client_builder = client_builder
89 .connect_timeout(Duration::from_secs(config.connect_timeout))
90 .timeout(Duration::from_secs(config.request_timeout));
91
92 if let Some(proxy_url) = &config.proxy_url {
94 let mut proxy = reqwest::Proxy::all(proxy_url)
95 .map_err(|e| VeracodeError::InvalidConfig(format!("Invalid proxy URL: {e}")))?;
96
97 if let (Some(username), Some(password)) =
99 (&config.proxy_username, &config.proxy_password)
100 {
101 proxy = proxy.basic_auth(username.expose_secret(), password.expose_secret());
102 }
103
104 client_builder = client_builder.proxy(proxy);
105 }
106
107 let client = client_builder.build().map_err(VeracodeError::Http)?;
108 Ok(Self { config, client })
109 }
110
111 #[must_use]
113 pub fn base_url(&self) -> &str {
114 &self.config.base_url
115 }
116
117 #[must_use]
119 pub fn config(&self) -> &VeracodeConfig {
120 &self.config
121 }
122
123 #[must_use]
125 pub fn client(&self) -> &Client {
126 &self.client
127 }
128
129 async fn execute_with_retry<F>(
145 &self,
146 request_builder: F,
147 operation_name: Cow<'_, str>,
148 ) -> Result<reqwest::Response, VeracodeError>
149 where
150 F: Fn() -> reqwest::RequestBuilder,
151 {
152 let retry_config = &self.config.retry_config;
153 let start_time = Instant::now();
154 let mut total_delay = std::time::Duration::from_millis(0);
155
156 if retry_config.max_attempts == 0 {
158 return match request_builder().send().await {
159 Ok(response) => Ok(response),
160 Err(e) => Err(VeracodeError::Http(e)),
161 };
162 }
163
164 let mut last_error = None;
165 let mut rate_limit_attempts = 0;
166
167 for attempt in 1..=retry_config.max_attempts + 1 {
168 match request_builder().send().await {
170 Ok(response) => {
171 if response.status().as_u16() == 429 {
173 let retry_after_seconds = response
175 .headers()
176 .get("retry-after")
177 .and_then(|h| h.to_str().ok())
178 .and_then(|s| s.parse::<u64>().ok());
179
180 let message = "HTTP 429: Rate limit exceeded".to_string();
181 let veracode_error = VeracodeError::RateLimited {
182 retry_after_seconds,
183 message,
184 };
185
186 rate_limit_attempts += 1;
188
189 if attempt > retry_config.max_attempts
191 || rate_limit_attempts > retry_config.rate_limit_max_attempts
192 {
193 last_error = Some(veracode_error);
194 break;
195 }
196
197 let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
199 total_delay += delay;
200
201 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
203 let msg = format!(
204 "{} exceeded maximum total retry time of {}ms after {} attempts",
205 operation_name, retry_config.max_total_delay_ms, attempt
206 );
207 last_error = Some(VeracodeError::RetryExhausted(msg));
208 break;
209 }
210
211 let wait_time = match retry_after_seconds {
213 Some(seconds) => format!("{seconds}s (from Retry-After header)"),
214 None => format!("{}s (until next minute window)", delay.as_secs()),
215 };
216 warn!(
217 "🚦 {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
218 );
219
220 tokio::time::sleep(delay).await;
222 last_error = Some(veracode_error);
223 continue;
224 }
225
226 if attempt > 1 {
227 info!("✅ {operation_name} succeeded on attempt {attempt}");
229 }
230 return Ok(response);
231 }
232 Err(e) => {
233 let veracode_error = VeracodeError::Http(e);
235
236 if attempt > retry_config.max_attempts
238 || !retry_config.is_retryable_error(&veracode_error)
239 {
240 last_error = Some(veracode_error);
241 break;
242 }
243
244 let delay = retry_config.calculate_delay(attempt);
246 total_delay += delay;
247
248 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
250 let msg = format!(
252 "{} exceeded maximum total retry time of {}ms after {} attempts",
253 operation_name, retry_config.max_total_delay_ms, attempt
254 );
255 last_error = Some(VeracodeError::RetryExhausted(msg));
256 break;
257 }
258
259 warn!(
261 "⚠️ {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
262 delay.as_millis()
263 );
264
265 tokio::time::sleep(delay).await;
267 last_error = Some(veracode_error);
268 }
269 }
270 }
271
272 match last_error {
274 Some(error) => {
275 let elapsed = start_time.elapsed();
276 match error {
277 VeracodeError::RetryExhausted(_) => Err(error),
278 _ => {
279 let msg = format!(
280 "{} failed after {} attempts over {}ms: {}",
281 operation_name,
282 retry_config.max_attempts + 1,
283 elapsed.as_millis(),
284 error
285 );
286 Err(VeracodeError::RetryExhausted(msg))
287 }
288 }
289 }
290 None => {
291 let msg = format!(
292 "{} failed after {} attempts with unknown error",
293 operation_name,
294 retry_config.max_attempts + 1
295 );
296 Err(VeracodeError::RetryExhausted(msg))
297 }
298 }
299 }
300
301 fn generate_hmac_signature(
303 &self,
304 method: &str,
305 url: &str,
306 timestamp: u64,
307 nonce: &str,
308 ) -> Result<String, VeracodeError> {
309 let url_parsed = Url::parse(url)
310 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
311
312 let path_and_query = match url_parsed.query() {
313 Some(query) => format!("{}?{}", url_parsed.path(), query),
314 None => url_parsed.path().to_string(),
315 };
316
317 let host = url_parsed.host_str().unwrap_or("");
318
319 let data = format!(
322 "id={}&host={}&url={}&method={}",
323 self.config.credentials.expose_api_id(),
324 host,
325 path_and_query,
326 method
327 );
328
329 let timestamp_str = timestamp.to_string();
330 let ver_str = "vcode_request_version_1";
331
332 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
334 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
335
336 let nonce_bytes = hex::decode(nonce)
337 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
338
339 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
341 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
342 mac1.update(&nonce_bytes);
343 let hashed_nonce = mac1.finalize().into_bytes();
344
345 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
347 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
348 mac2.update(timestamp_str.as_bytes());
349 let hashed_timestamp = mac2.finalize().into_bytes();
350
351 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
353 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
354 mac3.update(ver_str.as_bytes());
355 let hashed_ver_str = mac3.finalize().into_bytes();
356
357 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
359 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
360 mac4.update(data.as_bytes());
361 let signature = mac4.finalize().into_bytes();
362
363 Ok(hex::encode(signature).to_lowercase())
365 }
366
367 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
369 let timestamp = SystemTime::now()
370 .duration_since(UNIX_EPOCH)
371 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
372 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
376 let nonce = hex::encode(nonce_bytes);
377
378 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
379
380 Ok(format!(
381 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
382 self.config.credentials.expose_api_id(),
383 timestamp,
384 nonce,
385 signature
386 ))
387 }
388
389 pub async fn get(
400 &self,
401 endpoint: &str,
402 query_params: Option<&[(String, String)]>,
403 ) -> Result<reqwest::Response, VeracodeError> {
404 let param_count = query_params.map_or(0, |p| p.len());
406 let estimated_capacity = self.config.base_url.len() + endpoint.len() + param_count * 32;
407 let mut url = String::with_capacity(estimated_capacity);
408 url.push_str(&self.config.base_url);
409 url.push_str(endpoint);
410
411 if let Some(params) = query_params
412 && !params.is_empty()
413 {
414 url.push('?');
415 for (i, (key, value)) in params.iter().enumerate() {
416 if i > 0 {
417 url.push('&');
418 }
419 url.push_str(key);
420 url.push('=');
421 url.push_str(value);
422 }
423 }
424
425 let request_builder = || {
427 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
429 return self.client.get("invalid://url");
430 };
431
432 self.client
433 .get(&url)
434 .header("Authorization", auth_header)
435 .header("Content-Type", "application/json")
436 };
437
438 let operation_name = if endpoint.len() < 50 {
440 Cow::Owned(format!("GET {endpoint}"))
441 } else {
442 Cow::Borrowed("GET [long endpoint]")
443 };
444 self.execute_with_retry(request_builder, operation_name)
445 .await
446 }
447
448 pub async fn post<T: Serialize>(
459 &self,
460 endpoint: &str,
461 body: Option<&T>,
462 ) -> Result<reqwest::Response, VeracodeError> {
463 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
464 url.push_str(&self.config.base_url);
465 url.push_str(endpoint);
466
467 let serialized_body = if let Some(body) = body {
469 Some(serde_json::to_string(body)?)
470 } else {
471 None
472 };
473
474 let request_builder = || {
476 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
478 return self.client.post("invalid://url");
479 };
480
481 let mut request = self
482 .client
483 .post(&url)
484 .header("Authorization", auth_header)
485 .header("Content-Type", "application/json");
486
487 if let Some(ref body_str) = serialized_body {
488 request = request.body(body_str.clone());
489 }
490
491 request
492 };
493
494 let operation_name = if endpoint.len() < 50 {
495 Cow::Owned(format!("POST {endpoint}"))
496 } else {
497 Cow::Borrowed("POST [long endpoint]")
498 };
499 self.execute_with_retry(request_builder, operation_name)
500 .await
501 }
502
503 pub async fn put<T: Serialize>(
514 &self,
515 endpoint: &str,
516 body: Option<&T>,
517 ) -> Result<reqwest::Response, VeracodeError> {
518 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
519 url.push_str(&self.config.base_url);
520 url.push_str(endpoint);
521
522 let serialized_body = if let Some(body) = body {
524 Some(serde_json::to_string(body)?)
525 } else {
526 None
527 };
528
529 let request_builder = || {
531 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
533 return self.client.put("invalid://url");
534 };
535
536 let mut request = self
537 .client
538 .put(&url)
539 .header("Authorization", auth_header)
540 .header("Content-Type", "application/json");
541
542 if let Some(ref body_str) = serialized_body {
543 request = request.body(body_str.clone());
544 }
545
546 request
547 };
548
549 let operation_name = if endpoint.len() < 50 {
550 Cow::Owned(format!("PUT {endpoint}"))
551 } else {
552 Cow::Borrowed("PUT [long endpoint]")
553 };
554 self.execute_with_retry(request_builder, operation_name)
555 .await
556 }
557
558 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
568 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
569 url.push_str(&self.config.base_url);
570 url.push_str(endpoint);
571
572 let request_builder = || {
574 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
576 return self.client.delete("invalid://url");
577 };
578
579 self.client
580 .delete(&url)
581 .header("Authorization", auth_header)
582 .header("Content-Type", "application/json")
583 };
584
585 let operation_name = if endpoint.len() < 50 {
586 Cow::Owned(format!("DELETE {endpoint}"))
587 } else {
588 Cow::Borrowed("DELETE [long endpoint]")
589 };
590 self.execute_with_retry(request_builder, operation_name)
591 .await
592 }
593
594 pub async fn handle_response(
606 response: reqwest::Response,
607 ) -> Result<reqwest::Response, VeracodeError> {
608 if !response.status().is_success() {
609 let status = response.status();
610 let error_text = response.text().await?;
611 return Err(VeracodeError::InvalidResponse(format!(
612 "HTTP {status}: {error_text}"
613 )));
614 }
615 Ok(response)
616 }
617
618 pub async fn get_with_query(
631 &self,
632 endpoint: &str,
633 query_params: Option<Vec<(String, String)>>,
634 ) -> Result<reqwest::Response, VeracodeError> {
635 let query_slice = query_params.as_deref();
636 let response = self.get(endpoint, query_slice).await?;
637 Self::handle_response(response).await
638 }
639
640 pub async fn post_with_response<T: Serialize>(
651 &self,
652 endpoint: &str,
653 body: Option<&T>,
654 ) -> Result<reqwest::Response, VeracodeError> {
655 let response = self.post(endpoint, body).await?;
656 Self::handle_response(response).await
657 }
658
659 pub async fn put_with_response<T: Serialize>(
670 &self,
671 endpoint: &str,
672 body: Option<&T>,
673 ) -> Result<reqwest::Response, VeracodeError> {
674 let response = self.put(endpoint, body).await?;
675 Self::handle_response(response).await
676 }
677
678 pub async fn delete_with_response(
688 &self,
689 endpoint: &str,
690 ) -> Result<reqwest::Response, VeracodeError> {
691 let response = self.delete(endpoint).await?;
692 Self::handle_response(response).await
693 }
694
695 pub async fn get_paginated(
710 &self,
711 endpoint: &str,
712 base_query_params: Option<Vec<(String, String)>>,
713 page_size: Option<u32>,
714 ) -> Result<String, VeracodeError> {
715 let size = page_size.unwrap_or(500);
716 let mut page = 0;
717 let mut all_items = Vec::new();
718 let mut page_info = None;
719
720 loop {
721 let mut query_params = base_query_params.clone().unwrap_or_default();
722 query_params.push(("page".to_string(), page.to_string()));
723 query_params.push(("size".to_string(), size.to_string()));
724
725 let response = self.get_with_query(endpoint, Some(query_params)).await?;
726 let response_text = response.text().await?;
727
728 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
730 if let Some(embedded) = json_value.get("_embedded") {
732 if let Some(items_array) =
733 embedded.as_object().and_then(|obj| obj.values().next())
734 && let Some(items) = items_array.as_array()
735 {
736 if items.is_empty() {
737 break; }
739 all_items.extend(items.clone());
740 }
741 } else if let Some(items) = json_value.as_array() {
742 if items.is_empty() {
744 break;
745 }
746 all_items.extend(items.clone());
747 } else {
748 return Ok(response_text);
750 }
751
752 if let Some(page_obj) = json_value.get("page") {
754 page_info = Some(page_obj.clone());
755 if let (Some(current), Some(total)) = (
756 page_obj.get("number").and_then(|n| n.as_u64()),
757 page_obj.get("totalPages").and_then(|n| n.as_u64()),
758 ) && current + 1 >= total
759 {
760 break; }
762 }
763 } else {
764 return Ok(response_text);
766 }
767
768 page += 1;
769
770 if page > 100 {
772 break;
773 }
774 }
775
776 let combined_response = if let Some(page_info) = page_info {
778 serde_json::json!({
780 "_embedded": {
781 "roles": all_items },
783 "page": page_info
784 })
785 } else {
786 serde_json::Value::Array(all_items)
788 };
789
790 Ok(combined_response.to_string())
791 }
792
793 pub async fn get_with_params(
804 &self,
805 endpoint: &str,
806 params: &[(&str, &str)],
807 ) -> Result<reqwest::Response, VeracodeError> {
808 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
809 url.push_str(&self.config.base_url);
810 url.push_str(endpoint);
811 let mut request_url =
812 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
813
814 if !params.is_empty() {
816 let mut query_pairs = request_url.query_pairs_mut();
817 for (key, value) in params {
818 query_pairs.append_pair(key, value);
819 }
820 }
821
822 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
823
824 let response = self
825 .client
826 .get(request_url)
827 .header("Authorization", auth_header)
828 .header("User-Agent", "Veracode Rust Client")
829 .send()
830 .await?;
831
832 Ok(response)
833 }
834
835 pub async fn post_form(
846 &self,
847 endpoint: &str,
848 params: &[(&str, &str)],
849 ) -> Result<reqwest::Response, VeracodeError> {
850 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
851 url.push_str(&self.config.base_url);
852 url.push_str(endpoint);
853
854 let form_data: Vec<(&str, &str)> = params.to_vec();
856
857 let auth_header = self.generate_auth_header("POST", &url)?;
858
859 let response = self
860 .client
861 .post(&url)
862 .header("Authorization", auth_header)
863 .header("User-Agent", "Veracode Rust Client")
864 .form(&form_data)
865 .send()
866 .await?;
867
868 Ok(response)
869 }
870
871 pub async fn upload_file_multipart(
885 &self,
886 endpoint: &str,
887 params: HashMap<&str, &str>,
888 file_field_name: &str,
889 filename: &str,
890 file_data: Vec<u8>,
891 ) -> Result<reqwest::Response, VeracodeError> {
892 let mut url = String::with_capacity(self.config.base_url.len() + endpoint.len());
893 url.push_str(&self.config.base_url);
894 url.push_str(endpoint);
895
896 let mut form = multipart::Form::new();
898
899 for (key, value) in params {
901 form = form.text(key.to_string(), value.to_string());
902 }
903
904 let part = multipart::Part::bytes(file_data)
906 .file_name(filename.to_string())
907 .mime_str("application/octet-stream")
908 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
909
910 form = form.part(file_field_name.to_string(), part);
911
912 let auth_header = self.generate_auth_header("POST", &url)?;
913
914 let response = self
915 .client
916 .post(&url)
917 .header("Authorization", auth_header)
918 .header("User-Agent", "Veracode Rust Client")
919 .multipart(form)
920 .send()
921 .await?;
922
923 Ok(response)
924 }
925
926 pub async fn upload_file_multipart_put(
940 &self,
941 url: &str,
942 file_field_name: &str,
943 filename: &str,
944 file_data: Vec<u8>,
945 additional_headers: Option<HashMap<&str, &str>>,
946 ) -> Result<reqwest::Response, VeracodeError> {
947 let part = multipart::Part::bytes(file_data)
949 .file_name(filename.to_string())
950 .mime_str("application/octet-stream")
951 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
952
953 let form = multipart::Form::new().part(file_field_name.to_string(), part);
954
955 let auth_header = self.generate_auth_header("PUT", url)?;
956
957 let mut request = self
958 .client
959 .put(url)
960 .header("Authorization", auth_header)
961 .header("User-Agent", "Veracode Rust Client")
962 .multipart(form);
963
964 if let Some(headers) = additional_headers {
966 for (key, value) in headers {
967 request = request.header(key, value);
968 }
969 }
970
971 let response = request.send().await?;
972 Ok(response)
973 }
974
975 pub async fn upload_file_with_query_params(
995 &self,
996 endpoint: &str,
997 query_params: &[(&str, &str)],
998 file_field_name: &str,
999 filename: &str,
1000 file_data: Vec<u8>,
1001 ) -> Result<reqwest::Response, VeracodeError> {
1002 let url = self.build_url_with_params(endpoint, query_params);
1004
1005 let file_data_arc = Arc::new(file_data);
1007
1008 let filename_cow: Cow<str> = if filename.len() < 128 {
1010 Cow::Borrowed(filename)
1011 } else {
1012 Cow::Owned(filename.to_string())
1013 };
1014
1015 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1016 Cow::Borrowed(file_field_name)
1017 } else {
1018 Cow::Owned(file_field_name.to_string())
1019 };
1020
1021 let request_builder = || {
1023 let file_data_clone = Arc::clone(&file_data_arc);
1025
1026 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1028 .file_name(filename_cow.to_string())
1029 .mime_str("application/octet-stream")
1030 else {
1031 return self.client.post("invalid://url");
1032 };
1033
1034 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1035
1036 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1038 return self.client.post("invalid://url");
1039 };
1040
1041 self.client
1042 .post(&url)
1043 .header("Authorization", auth_header)
1044 .header("User-Agent", "Veracode Rust Client")
1045 .multipart(form)
1046 };
1047
1048 let operation_name: Cow<str> = if endpoint.len() < 50 {
1050 Cow::Owned(format!("File Upload POST {endpoint}"))
1051 } else {
1052 Cow::Borrowed("File Upload POST [long endpoint]")
1053 };
1054
1055 self.execute_with_retry(request_builder, operation_name)
1056 .await
1057 }
1058
1059 pub async fn post_with_query_params(
1073 &self,
1074 endpoint: &str,
1075 query_params: &[(&str, &str)],
1076 ) -> Result<reqwest::Response, VeracodeError> {
1077 let url = self.build_url_with_params(endpoint, query_params);
1079
1080 let auth_header = self.generate_auth_header("POST", &url)?;
1081
1082 let response = self
1083 .client
1084 .post(&url)
1085 .header("Authorization", auth_header)
1086 .header("User-Agent", "Veracode Rust Client")
1087 .send()
1088 .await?;
1089
1090 Ok(response)
1091 }
1092
1093 pub async fn get_with_query_params(
1107 &self,
1108 endpoint: &str,
1109 query_params: &[(&str, &str)],
1110 ) -> Result<reqwest::Response, VeracodeError> {
1111 let url = self.build_url_with_params(endpoint, query_params);
1113
1114 let auth_header = self.generate_auth_header("GET", &url)?;
1115
1116 let response = self
1117 .client
1118 .get(&url)
1119 .header("Authorization", auth_header)
1120 .header("User-Agent", "Veracode Rust Client")
1121 .send()
1122 .await?;
1123
1124 Ok(response)
1125 }
1126
1127 pub async fn upload_large_file_chunked<F>(
1144 &self,
1145 endpoint: &str,
1146 query_params: &[(&str, &str)],
1147 file_path: &str,
1148 content_type: Option<&str>,
1149 progress_callback: Option<F>,
1150 ) -> Result<reqwest::Response, VeracodeError>
1151 where
1152 F: Fn(u64, u64, f64) + Send + Sync,
1153 {
1154 let url = self.build_url_with_params(endpoint, query_params);
1156
1157 let mut file = File::open(file_path)
1159 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1160
1161 let file_size = file
1162 .metadata()
1163 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1164 .len();
1165
1166 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1169 return Err(VeracodeError::InvalidConfig(format!(
1170 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1171 )));
1172 }
1173
1174 file.seek(SeekFrom::Start(0))
1176 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1177
1178 let mut file_data = Vec::with_capacity(file_size as usize);
1179 file.read_to_end(&mut file_data)
1180 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1181
1182 let file_data_arc = Arc::new(file_data);
1184 let content_type_cow: Cow<str> =
1185 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1186 if ct.len() < 64 {
1187 Cow::Borrowed(ct)
1188 } else {
1189 Cow::Owned(ct.to_string())
1190 }
1191 });
1192
1193 let request_builder = || {
1195 let file_data_clone = Arc::clone(&file_data_arc);
1197
1198 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1200 return self.client.post("invalid://url");
1201 };
1202
1203 self.client
1204 .post(&url)
1205 .header("Authorization", auth_header)
1206 .header("User-Agent", "Veracode Rust Client")
1207 .header("Content-Type", content_type_cow.as_ref())
1208 .header("Content-Length", file_size.to_string())
1209 .body((*file_data_clone).clone())
1210 };
1211
1212 if let Some(callback) = progress_callback {
1214 callback(file_size, file_size, 100.0);
1215 }
1216
1217 let operation_name: Cow<str> = if endpoint.len() < 50 {
1219 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1220 } else {
1221 Cow::Borrowed("Large File Upload POST [long endpoint]")
1222 };
1223
1224 self.execute_with_retry(request_builder, operation_name)
1225 .await
1226 }
1227
1228 pub async fn upload_file_binary(
1247 &self,
1248 endpoint: &str,
1249 query_params: &[(&str, &str)],
1250 file_data: Vec<u8>,
1251 content_type: &str,
1252 ) -> Result<reqwest::Response, VeracodeError> {
1253 let url = self.build_url_with_params(endpoint, query_params);
1255
1256 let file_data_arc = Arc::new(file_data);
1258 let file_size = file_data_arc.len();
1259
1260 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1262 Cow::Borrowed(content_type)
1263 } else {
1264 Cow::Owned(content_type.to_string())
1265 };
1266
1267 let request_builder = || {
1269 let file_data_clone = Arc::clone(&file_data_arc);
1271
1272 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1274 return self.client.post("invalid://url");
1275 };
1276
1277 self.client
1278 .post(&url)
1279 .header("Authorization", auth_header)
1280 .header("User-Agent", "Veracode Rust Client")
1281 .header("Content-Type", content_type_cow.as_ref())
1282 .header("Content-Length", file_size.to_string())
1283 .body((*file_data_clone).clone())
1284 };
1285
1286 let operation_name: Cow<str> = if endpoint.len() < 50 {
1288 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1289 } else {
1290 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1291 };
1292
1293 self.execute_with_retry(request_builder, operation_name)
1294 .await
1295 }
1296}