1use std::sync::Arc;
8use std::time::Duration;
9
10use crate::client::api::Api;
11use crate::client::events::{Events, NoEvents};
12use crate::client::transport::{Method, Request, Response, Transport, TransportError};
13
14pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120);
16
17const MAX_RESPONSE_BYTES: u64 = 16 << 20;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum BuildError {
23 InvalidServer(String),
25 InvalidProxy(String),
27}
28
29impl std::fmt::Display for BuildError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 BuildError::InvalidServer(m) => f.write_str(m),
33 BuildError::InvalidProxy(m) => write!(f, "the proxy URL is not valid: {m}"),
34 }
35 }
36}
37
38impl std::error::Error for BuildError {}
39
40#[derive(Clone)]
54pub struct ClientBuilder {
55 server: String,
56 timeout: Duration,
57 proxy: Option<String>,
58 roots: Option<Vec<Vec<u8>>>,
59 platform_verifier: bool,
60 user_agent: String,
61 events: Arc<dyn Events>,
62}
63
64impl std::fmt::Debug for ClientBuilder {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("ClientBuilder")
67 .field("server", &self.server)
68 .field("timeout", &self.timeout)
69 .field("proxy", &self.proxy)
70 .field("roots", &self.roots.as_ref().map(Vec::len))
71 .field("platform_verifier", &self.platform_verifier)
72 .field("user_agent", &self.user_agent)
73 .finish_non_exhaustive()
74 }
75}
76
77impl ClientBuilder {
78 pub fn new(server: &str) -> ClientBuilder {
81 ClientBuilder {
82 server: server.to_owned(),
83 timeout: DEFAULT_TIMEOUT,
84 proxy: None,
85 roots: None,
86 platform_verifier: false,
87 user_agent: format!("galata-vault/{}", env!("CARGO_PKG_VERSION")),
88 events: Arc::new(NoEvents),
89 }
90 }
91
92 pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
94 self.timeout = timeout;
95 self
96 }
97
98 pub fn proxy(mut self, proxy: Option<&str>) -> ClientBuilder {
101 self.proxy = proxy.map(str::to_owned);
102 self
103 }
104
105 pub fn root_certificates(mut self, roots: Vec<Vec<u8>>) -> ClientBuilder {
109 self.roots = Some(roots);
110 self
111 }
112
113 #[cfg(feature = "platform-verifier")]
116 pub fn platform_verifier(mut self, on: bool) -> ClientBuilder {
117 self.platform_verifier = on;
118 self
119 }
120
121 pub fn user_agent(mut self, user_agent: impl Into<String>) -> ClientBuilder {
123 self.user_agent = user_agent.into();
124 self
125 }
126
127 pub fn events(mut self, events: Arc<dyn Events>) -> ClientBuilder {
130 self.events = events;
131 self
132 }
133
134 pub fn build_transport(&self) -> Result<HttpTransport, BuildError> {
136 let server = crate::proto::url::validate_server_url(&self.server)
137 .map_err(BuildError::InvalidServer)?;
138 let proxy = match &self.proxy {
139 Some(p) => {
140 Some(ureq::Proxy::new(p).map_err(|e| BuildError::InvalidProxy(e.to_string()))?)
141 }
142 None => None,
143 };
144 let roots = if self.platform_verifier {
145 ureq::tls::RootCerts::PlatformVerifier
146 } else {
147 match &self.roots {
148 Some(roots) => {
149 let certs: Vec<ureq::tls::Certificate<'static>> = roots
150 .iter()
151 .map(|der| ureq::tls::Certificate::from_der(der).to_owned())
152 .collect();
153 ureq::tls::RootCerts::new_with_certs(&certs)
154 }
155 None => ureq::tls::RootCerts::WebPki,
156 }
157 };
158 let agent: ureq::Agent = ureq::Agent::config_builder()
159 .http_status_as_error(false)
160 .max_redirects(0)
161 .timeout_global(Some(self.timeout))
162 .proxy(proxy)
164 .user_agent(self.user_agent.as_str())
165 .tls_config(ureq::tls::TlsConfig::builder().root_certs(roots).build())
166 .build()
167 .into();
168 Ok(HttpTransport { agent, server })
169 }
170
171 pub fn build(&self) -> Result<Api, BuildError> {
174 Ok(Api::new(self.build_transport()?).with_events(self.events.clone()))
175 }
176}
177
178#[derive(Clone)]
181pub struct HttpTransport {
182 agent: ureq::Agent,
183 server: String,
184}
185
186impl std::fmt::Debug for HttpTransport {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("HttpTransport")
189 .field("server", &self.server)
190 .finish_non_exhaustive()
191 }
192}
193
194impl HttpTransport {
195 pub fn server(&self) -> &str {
197 &self.server
198 }
199
200 fn fail(&self, e: ureq::Error) -> TransportError {
201 match e {
202 ureq::Error::Io(io) => TransportError::with_source(&self.server, io),
203 other => TransportError::with_source(&self.server, other),
204 }
205 }
206}
207
208fn with_headers<B>(
209 mut rb: ureq::RequestBuilder<B>,
210 request: &Request<'_>,
211) -> ureq::RequestBuilder<B> {
212 if let Some(a) = request.authorization {
213 rb = rb.header("authorization", a);
214 }
215 if let Some(v) = request.if_match {
216 rb = rb.header("if-match", v);
217 }
218 if let Some(v) = request.if_none_match {
219 rb = rb.header("if-none-match", v);
220 }
221 if request.body.is_some() {
222 rb = rb.header("content-type", "application/json");
223 }
224 rb
225}
226
227impl Transport for HttpTransport {
228 fn send(&self, request: Request<'_>) -> Result<Response, TransportError> {
229 let url = format!("{}{}", self.server, request.path_and_query);
230 let payload = request.body.unwrap_or(&[]);
231 let result = match (request.method, request.body) {
232 (Method::Get, _) => with_headers(self.agent.get(&url), &request).call(),
233 (Method::Delete, None) => with_headers(self.agent.delete(&url), &request).call(),
234 (Method::Delete, Some(_)) => {
236 with_headers(self.agent.delete(&url).force_send_body(), &request).send(payload)
237 }
238 (Method::Post, _) => with_headers(self.agent.post(&url), &request).send(payload),
239 (Method::Put, _) => with_headers(self.agent.put(&url), &request).send(payload),
240 };
241 let mut response = result.map_err(|e| self.fail(e))?;
242 let status = response.status().as_u16();
243 if (300..400).contains(&status) {
244 return Err(TransportError::new(
245 &self.server,
246 format!("the server answered {status} with a redirect, which is never followed"),
247 ));
248 }
249 let header = |name: &str| {
250 response
251 .headers()
252 .get(name)
253 .and_then(|v| v.to_str().ok())
254 .map(str::to_owned)
255 };
256 let etag = header("etag");
257 let expires_at = header("x-gv-expires-at");
258 let body = response
259 .body_mut()
260 .with_config()
261 .limit(MAX_RESPONSE_BYTES)
262 .read_to_vec()
263 .map_err(|e| self.fail(e))?;
264 Ok(Response {
265 status,
266 etag,
267 expires_at,
268 body,
269 })
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn server_urls_are_checked_when_built() {
279 assert_eq!(
280 ClientBuilder::new("https://vault.example/")
281 .build_transport()
282 .unwrap()
283 .server(),
284 "https://vault.example"
285 );
286 for bad in [
287 "http://vault.example",
288 "http://127.0.0.1.evil.example",
289 "ftp://x",
290 "https://user@vault.example",
291 ] {
292 assert!(matches!(
293 ClientBuilder::new(bad).build_transport(),
294 Err(BuildError::InvalidServer(_))
295 ));
296 }
297 assert!(matches!(
298 ClientBuilder::new("https://vault.example")
299 .proxy(Some("not a url"))
300 .build_transport(),
301 Err(BuildError::InvalidProxy(_))
302 ));
303 }
304}