1use std::time::Duration;
8
9use reqwest::{Certificate, Client, Identity};
10use tracing::{debug, warn};
11
12use crate::config::DogtagConfig;
13use crate::{DogtagError, DogtagResult};
14
15pub struct DogtagClient {
32 http: Client,
33 base_url: String,
34 retry_max: u32,
35 retry_delay: Duration,
36}
37
38impl DogtagClient {
39 pub fn new(config: &DogtagConfig) -> DogtagResult<Self> {
45 let cert_pem = std::fs::read(&config.agent_cert_file)?;
46 let key_pem = std::fs::read(&config.agent_key_file)?;
47 let ca_pem = std::fs::read(&config.ca_cert_file)?;
48
49 let mut identity_pem = cert_pem;
51 identity_pem.extend_from_slice(b"\n");
52 identity_pem.extend_from_slice(&key_pem);
53
54 let identity = Identity::from_pem(&identity_pem)
55 .map_err(|e| DogtagError::TlsError(format!("Failed to load agent identity: {e}")))?;
56
57 let ca_cert = Certificate::from_pem(&ca_pem)
58 .map_err(|e| DogtagError::TlsError(format!("Failed to load CA certificate: {e}")))?;
59
60 let http = Client::builder()
61 .identity(identity)
62 .add_root_certificate(ca_cert)
63 .timeout(Duration::from_secs(config.timeout_secs))
64 .build()?;
65
66 let base_url = config.ca_url.as_str().trim_end_matches('/').to_owned();
68
69 Ok(Self {
70 http,
71 base_url,
72 retry_max: config.retry_max,
73 retry_delay: Duration::from_millis(config.retry_delay_ms),
74 })
75 }
76
77 pub async fn health_check(&self) -> DogtagResult<bool> {
82 let url = format!("{}/ca/rest/info", self.base_url);
83 debug!(url = %url, "Dogtag health check");
84
85 match self.http.get(&url).send().await {
86 Ok(resp) => Ok(resp.status().is_success()),
87 Err(e) => {
88 warn!(error = %e, "Dogtag health check failed");
89 Ok(false)
90 }
91 }
92 }
93
94 pub(crate) async fn get(&self, path: &str) -> DogtagResult<reqwest::Response> {
96 let url = format!("{}{}", self.base_url, path);
97 self.request_with_retry(|| self.http.get(&url).send()).await
98 }
99
100 pub(crate) async fn post_json<T: serde::Serialize + ?Sized>(
102 &self,
103 path: &str,
104 body: &T,
105 ) -> DogtagResult<reqwest::Response> {
106 let url = format!("{}{}", self.base_url, path);
107 self.request_with_retry(|| self.http.post(&url).json(body).send())
108 .await
109 }
110
111 pub(crate) async fn post_bytes(
113 &self,
114 path: &str,
115 body: Vec<u8>,
116 content_type: &str,
117 ) -> DogtagResult<reqwest::Response> {
118 let url = format!("{}{}", self.base_url, path);
119 let ct = content_type.to_owned();
120 self.request_with_retry(|| {
121 self.http
122 .post(&url)
123 .header("Content-Type", &ct)
124 .body(body.clone())
125 .send()
126 })
127 .await
128 }
129
130 async fn request_with_retry<F, Fut>(&self, make_request: F) -> DogtagResult<reqwest::Response>
135 where
136 F: Fn() -> Fut,
137 Fut: std::future::Future<Output = reqwest::Result<reqwest::Response>>,
138 {
139 let mut last_error = None;
140
141 for attempt in 0..=self.retry_max {
142 if attempt > 0 {
143 debug!(attempt, max = self.retry_max, "Retrying request");
144 tokio::time::sleep(self.retry_delay).await;
145 }
146
147 match make_request().await {
148 Ok(resp) if resp.status().is_server_error() => {
149 let status = resp.status();
150 let body = resp.text().await.unwrap_or_default();
151 warn!(
152 attempt,
153 status = status.as_u16(),
154 "Server error, will retry"
155 );
156 last_error = Some(DogtagError::ApiError {
157 status: status.as_u16(),
158 body,
159 });
160 }
161 Ok(resp) => return Ok(resp),
162 Err(e) => {
163 warn!(attempt, error = %e, "Request failed, will retry");
164 last_error = Some(DogtagError::Http(e));
165 }
166 }
167 }
168
169 Err(last_error.unwrap_or(DogtagError::ApiError {
170 status: 0,
171 body: "All retry attempts exhausted".into(),
172 }))
173 }
174
175 pub fn base_url(&self) -> &str {
177 &self.base_url
178 }
179
180 pub(crate) async fn json_response<T: serde::de::DeserializeOwned>(
182 resp: reqwest::Response,
183 ) -> DogtagResult<T> {
184 let status = resp.status();
185 if !status.is_success() {
186 let body = resp.text().await.unwrap_or_default();
187 return Err(DogtagError::ApiError {
188 status: status.as_u16(),
189 body,
190 });
191 }
192 resp.json::<T>()
193 .await
194 .map_err(|e| DogtagError::ParseError(e.to_string()))
195 }
196}