1use crate::api::AuthInfo;
2use crate::api::{token::responses::LookupTokenResponse, EndpointMiddleware};
3use crate::error::ClientError;
4use async_trait::async_trait;
5pub use reqwest::Identity;
6use rustify::clients::reqwest::Client as HTTPClient;
7use std::time::Duration;
8use std::{env, fs};
9use url::Url;
10
11const VALID_SCHEMES: [&str; 2] = ["http", "https"];
13
14#[async_trait]
16pub trait Client: Send + Sync + Sized {
17 fn http(&self) -> &HTTPClient;
19
20 fn middle(&self) -> &EndpointMiddleware;
22
23 fn settings(&self) -> &VaultClientSettings;
25
26 fn set_token(&mut self, token: &str);
28
29 async fn lookup(&self) -> Result<LookupTokenResponse, ClientError> {
31 crate::token::lookup_self(self).await
32 }
33
34 async fn renew(&self, increment: Option<&str>) -> Result<AuthInfo, ClientError> {
36 crate::token::renew_self(self, increment).await
37 }
38
39 async fn revoke(&self) -> Result<(), ClientError> {
41 crate::token::revoke_self(self).await
42 }
43
44 async fn status(&self) -> Result<crate::sys::ServerStatus, ClientError> {
46 crate::sys::status(self).await
47 }
48}
49
50pub struct VaultClient {
56 pub http: HTTPClient,
57 pub middle: EndpointMiddleware,
58 pub settings: VaultClientSettings,
59}
60
61#[async_trait]
62impl Client for VaultClient {
63 fn http(&self) -> &HTTPClient {
64 &self.http
65 }
66
67 fn middle(&self) -> &EndpointMiddleware {
68 &self.middle
69 }
70
71 fn settings(&self) -> &VaultClientSettings {
72 &self.settings
73 }
74
75 fn set_token(&mut self, token: &str) {
76 self.settings.token = token.to_string();
77 self.middle.token = token.to_string();
78 }
79}
80
81impl VaultClient {
82 #[instrument(skip(settings), err)]
84 pub fn new(settings: VaultClientSettings) -> Result<VaultClient, ClientError> {
85 #[cfg(not(feature = "rustls"))]
86 let mut http_client = reqwest::ClientBuilder::new();
87
88 #[cfg(feature = "rustls")]
89 let mut http_client = reqwest::ClientBuilder::new().use_rustls_tls();
90
91 http_client = if let Some(timeout) = settings.timeout {
93 http_client.timeout(timeout)
94 } else {
95 http_client
96 };
97
98 if !settings.verify {
100 event!(tracing::Level::WARN, "Disabling TLS verification");
101 }
102 http_client = http_client.danger_accept_invalid_certs(!settings.verify);
103
104 for path in &settings.ca_certs {
106 let content = std::fs::read(path).map_err(|e| ClientError::FileReadError {
107 source: e,
108 path: path.clone(),
109 })?;
110 let cert = reqwest::Certificate::from_pem(&content).map_err(|e| {
111 ClientError::ParseCertificateError {
112 source: e,
113 path: path.clone(),
114 }
115 })?;
116
117 debug!("Importing CA certificate from {}", path);
118 http_client = http_client.add_root_certificate(cert);
119 }
120
121 if let Some(identity) = &settings.identity {
123 http_client = http_client.identity(identity.clone());
124 }
125
126 debug!("Using API version {}", settings.version);
128 let version_str = format!("v{}", settings.version);
129 let middle = EndpointMiddleware {
130 token: settings.token.clone(),
131 version: version_str,
132 wrap: None,
133 namespace: settings.namespace.clone(),
134 };
135
136 let http_client = http_client
137 .build()
138 .map_err(|e| ClientError::RestClientBuildError { source: e })?;
139 let http = HTTPClient::new(settings.address.as_str(), http_client);
140 Ok(VaultClient {
141 settings,
142 middle,
143 http,
144 })
145 }
146}
147
148#[derive(Builder, Clone, Debug)]
161#[builder(build_fn(validate = "Self::validate"))]
162pub struct VaultClientSettings {
163 #[builder(setter(custom), default = "self.default_address()?")]
164 pub address: Url,
165 #[builder(default = "self.default_ca_certs()")]
166 pub ca_certs: Vec<String>,
167 #[builder(default = "self.default_identity()")]
168 pub identity: Option<Identity>,
169 #[builder(default)]
170 pub timeout: Option<Duration>,
171 #[builder(setter(into), default = "self.default_token()")]
172 pub token: String,
173 #[builder(default = "self.default_verify()")]
174 pub verify: bool,
175 #[builder(setter(into, strip_option), default = "1")]
176 pub version: u8,
177 #[builder(default = "false")]
178 pub wrapping: bool,
179 #[builder(default)]
180 pub namespace: Option<String>,
181}
182
183impl VaultClientSettingsBuilder {
184 pub fn address<T>(&mut self, address: T) -> &mut Self
192 where
193 T: AsRef<str>,
194 {
195 let url = Url::parse(address.as_ref())
196 .map_err(|_| format!("Invalid URL format: {}", address.as_ref()))
197 .unwrap();
198 self.address = Some(url);
199 self
200 }
201
202 pub fn set_namespace(&mut self, str: String) -> &mut Self {
203 self.namespace = Some(Some(str));
204 self
205 }
206
207 fn default_address(&self) -> Result<Url, String> {
208 let address = if let Ok(address) = env::var("VAULT_ADDR") {
209 debug!("Using vault address from $VAULT_ADDR: {address}");
210 address
211 } else {
212 debug!("Using default vault address http://127.0.0.1:8200");
213 String::from("http://127.0.0.1:8200")
214 };
215 let url = Url::parse(&address);
216 let url = url.map_err(|_| format!("Invalid URL format: {}", &address))?;
217 self.validate_url(&url)?;
220 Ok(url)
221 }
222
223 fn default_token(&self) -> String {
224 match env::var("VAULT_TOKEN") {
225 Ok(s) => {
226 debug!("Using vault token from $VAULT_TOKEN");
227 s
228 }
229 Err(_) => {
230 debug!("Using default empty vault token");
231 String::from("")
232 }
233 }
234 }
235
236 fn default_verify(&self) -> bool {
237 debug!("Checking TLS verification using $VAULT_SKIP_VERIFY");
238 match env::var("VAULT_SKIP_VERIFY") {
239 Ok(value) => !matches!(value.to_lowercase().as_str(), "0" | "f" | "false"),
240 Err(_) => true,
241 }
242 }
243
244 fn default_ca_certs(&self) -> Vec<String> {
245 let mut paths: Vec<String> = Vec::new();
246
247 if let Ok(s) = env::var("VAULT_CACERT") {
248 debug!("Found CA certificate in $VAULT_CACERT");
249 paths.push(s);
250 }
251
252 if let Ok(s) = env::var("VAULT_CAPATH") {
253 debug!("Found CA certificate path in $VAULT_CAPATH");
254 if let Ok(p) = fs::read_dir(s) {
255 for path in p {
256 paths.push(path.unwrap().path().to_str().unwrap().to_string())
257 }
258 }
259 }
260
261 paths
262 }
263
264 fn default_identity(&self) -> Option<reqwest::Identity> {
265 let env_client_cert = env::var("VAULT_CLIENT_CERT").unwrap_or_default();
267 let env_client_key = env::var("VAULT_CLIENT_KEY").unwrap_or_default();
268
269 if env_client_cert.is_empty() || env_client_key.is_empty() {
270 debug!("No client certificate (env VAULT_CLIENT_CERT & VAULT_CLIENT_KEY are not set)");
271 return None;
272 }
273
274 #[cfg(feature = "rustls")]
275 {
276 let mut client_cert = match fs::read(&env_client_cert) {
277 Ok(content) => content,
278 Err(err) => {
279 error!("error reading client cert '{}': {}", env_client_cert, err);
280 return None;
281 }
282 };
283
284 let mut client_key = match fs::read(&env_client_key) {
285 Ok(content) => content,
286 Err(err) => {
287 error!("error reading client key '{}': {}", env_client_key, err);
288 return None;
289 }
290 };
291
292 client_cert.append(&mut client_key);
294
295 match reqwest::Identity::from_pem(&client_cert) {
296 Ok(pkcs8) => return Some(pkcs8),
297 Err(err) => error!("error creating identity: {}", err),
298 };
299 }
300
301 #[cfg(feature = "native-tls")]
302 {
303 error!("Client certificates not implemented for native-tls");
304 }
305
306 None
307 }
308
309 fn validate(&self) -> Result<(), String> {
310 if let Some(url) = &self.address {
312 self.validate_url(url)
313 } else {
314 Ok(())
315 }
316 }
317
318 fn validate_url(&self, url: &Url) -> Result<(), String> {
319 if !VALID_SCHEMES.contains(&url.scheme()) {
321 Err(format!("Invalid scheme for HTTP URL: {}", url.scheme()))
322 } else {
323 Ok(())
324 }
325 }
326}