use std::sync::Arc;
use std::time::Duration;
use crate::client::api::Api;
use crate::client::events::{Events, NoEvents};
use crate::client::transport::{Method, Request, Response, Transport, TransportError};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120);
const MAX_RESPONSE_BYTES: u64 = 16 << 20;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BuildError {
InvalidServer(String),
InvalidProxy(String),
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::InvalidServer(m) => f.write_str(m),
BuildError::InvalidProxy(m) => write!(f, "the proxy URL is not valid: {m}"),
}
}
}
impl std::error::Error for BuildError {}
#[derive(Clone)]
pub struct ClientBuilder {
server: String,
timeout: Duration,
proxy: Option<String>,
roots: Option<Vec<Vec<u8>>>,
platform_verifier: bool,
user_agent: String,
events: Arc<dyn Events>,
}
impl std::fmt::Debug for ClientBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientBuilder")
.field("server", &self.server)
.field("timeout", &self.timeout)
.field("proxy", &self.proxy)
.field("roots", &self.roots.as_ref().map(Vec::len))
.field("platform_verifier", &self.platform_verifier)
.field("user_agent", &self.user_agent)
.finish_non_exhaustive()
}
}
impl ClientBuilder {
pub fn new(server: &str) -> ClientBuilder {
ClientBuilder {
server: server.to_owned(),
timeout: DEFAULT_TIMEOUT,
proxy: None,
roots: None,
platform_verifier: false,
user_agent: format!("galata-vault/{}", env!("CARGO_PKG_VERSION")),
events: Arc::new(NoEvents),
}
}
pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
self.timeout = timeout;
self
}
pub fn proxy(mut self, proxy: Option<&str>) -> ClientBuilder {
self.proxy = proxy.map(str::to_owned);
self
}
pub fn root_certificates(mut self, roots: Vec<Vec<u8>>) -> ClientBuilder {
self.roots = Some(roots);
self
}
#[cfg(feature = "platform-verifier")]
pub fn platform_verifier(mut self, on: bool) -> ClientBuilder {
self.platform_verifier = on;
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> ClientBuilder {
self.user_agent = user_agent.into();
self
}
pub fn events(mut self, events: Arc<dyn Events>) -> ClientBuilder {
self.events = events;
self
}
pub fn build_transport(&self) -> Result<HttpTransport, BuildError> {
let server = crate::proto::url::validate_server_url(&self.server)
.map_err(BuildError::InvalidServer)?;
let proxy = match &self.proxy {
Some(p) => {
Some(ureq::Proxy::new(p).map_err(|e| BuildError::InvalidProxy(e.to_string()))?)
}
None => None,
};
let roots = if self.platform_verifier {
ureq::tls::RootCerts::PlatformVerifier
} else {
match &self.roots {
Some(roots) => {
let certs: Vec<ureq::tls::Certificate<'static>> = roots
.iter()
.map(|der| ureq::tls::Certificate::from_der(der).to_owned())
.collect();
ureq::tls::RootCerts::new_with_certs(&certs)
}
None => ureq::tls::RootCerts::WebPki,
}
};
let agent: ureq::Agent = ureq::Agent::config_builder()
.http_status_as_error(false)
.max_redirects(0)
.timeout_global(Some(self.timeout))
.proxy(proxy)
.user_agent(self.user_agent.as_str())
.tls_config(ureq::tls::TlsConfig::builder().root_certs(roots).build())
.build()
.into();
Ok(HttpTransport { agent, server })
}
pub fn build(&self) -> Result<Api, BuildError> {
Ok(Api::new(self.build_transport()?).with_events(self.events.clone()))
}
}
#[derive(Clone)]
pub struct HttpTransport {
agent: ureq::Agent,
server: String,
}
impl std::fmt::Debug for HttpTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpTransport")
.field("server", &self.server)
.finish_non_exhaustive()
}
}
impl HttpTransport {
pub fn server(&self) -> &str {
&self.server
}
fn fail(&self, e: ureq::Error) -> TransportError {
match e {
ureq::Error::Io(io) => TransportError::with_source(&self.server, io),
other => TransportError::with_source(&self.server, other),
}
}
}
fn with_headers<B>(
mut rb: ureq::RequestBuilder<B>,
request: &Request<'_>,
) -> ureq::RequestBuilder<B> {
if let Some(a) = request.authorization {
rb = rb.header("authorization", a);
}
if let Some(v) = request.if_match {
rb = rb.header("if-match", v);
}
if let Some(v) = request.if_none_match {
rb = rb.header("if-none-match", v);
}
if request.body.is_some() {
rb = rb.header("content-type", "application/json");
}
rb
}
impl Transport for HttpTransport {
fn send(&self, request: Request<'_>) -> Result<Response, TransportError> {
let url = format!("{}{}", self.server, request.path_and_query);
let payload = request.body.unwrap_or(&[]);
let result = match (request.method, request.body) {
(Method::Get, _) => with_headers(self.agent.get(&url), &request).call(),
(Method::Delete, None) => with_headers(self.agent.delete(&url), &request).call(),
(Method::Delete, Some(_)) => {
with_headers(self.agent.delete(&url).force_send_body(), &request).send(payload)
}
(Method::Post, _) => with_headers(self.agent.post(&url), &request).send(payload),
(Method::Put, _) => with_headers(self.agent.put(&url), &request).send(payload),
};
let mut response = result.map_err(|e| self.fail(e))?;
let status = response.status().as_u16();
if (300..400).contains(&status) {
return Err(TransportError::new(
&self.server,
format!("the server answered {status} with a redirect, which is never followed"),
));
}
let header = |name: &str| {
response
.headers()
.get(name)
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
};
let etag = header("etag");
let expires_at = header("x-gv-expires-at");
let body = response
.body_mut()
.with_config()
.limit(MAX_RESPONSE_BYTES)
.read_to_vec()
.map_err(|e| self.fail(e))?;
Ok(Response {
status,
etag,
expires_at,
body,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn server_urls_are_checked_when_built() {
assert_eq!(
ClientBuilder::new("https://vault.example/")
.build_transport()
.unwrap()
.server(),
"https://vault.example"
);
for bad in [
"http://vault.example",
"http://127.0.0.1.evil.example",
"ftp://x",
"https://user@vault.example",
] {
assert!(matches!(
ClientBuilder::new(bad).build_transport(),
Err(BuildError::InvalidServer(_))
));
}
assert!(matches!(
ClientBuilder::new("https://vault.example")
.proxy(Some("not a url"))
.build_transport(),
Err(BuildError::InvalidProxy(_))
));
}
}