Skip to main content

galata_vault/client/
transport.rs

1//! The seam: one canonical request in, one response out.
2
3use std::sync::Arc;
4
5/// The methods the protocol uses. There is no other: a request cannot be
6/// built with a method the protocol does not know.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum Method {
9    /// `GET`
10    Get,
11    /// `PUT`
12    Put,
13    /// `POST`
14    Post,
15    /// `DELETE` (a signed tombstone travels in its body)
16    Delete,
17}
18
19impl Method {
20    /// The method as it is sent and signed.
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Method::Get => "GET",
24            Method::Put => "PUT",
25            Method::Post => "POST",
26            Method::Delete => "DELETE",
27        }
28    }
29}
30
31impl std::fmt::Display for Method {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.write_str(self.as_str())
34    }
35}
36
37/// The canonical request, exactly as the protocol authenticates it: the
38/// signature in `authorization` covers the method, the path and query, the
39/// body's digest and both preconditions as they appear here. A transport
40/// sends these bytes unchanged; it never needs to know which operation they
41/// are.
42///
43/// `Debug` never shows the authorization value.
44#[derive(Clone, Copy)]
45#[non_exhaustive]
46pub struct Request<'a> {
47    /// The method.
48    pub method: Method,
49    /// The path and query, e.g. `/v1/secrets?limit=500`, relative to the
50    /// server the transport is bound to.
51    pub path_and_query: &'a str,
52    /// The `If-Match` header, if any.
53    pub if_match: Option<&'a str>,
54    /// The `If-None-Match` header, if any.
55    pub if_none_match: Option<&'a str>,
56    /// The `Authorization` header (`GV-Sig v=1,…`), if the request is
57    /// authenticated.
58    pub authorization: Option<&'a str>,
59    /// The JSON body, if any. A transport sends it as `application/json`.
60    pub body: Option<&'a [u8]>,
61}
62
63impl<'a> Request<'a> {
64    /// A request with no preconditions, no authorization and no body: what
65    /// a proxy, a test or a conformance check sends when it needs a request
66    /// the typed [`crate::client::Api`] would never build.
67    pub fn new(method: Method, path_and_query: &'a str) -> Request<'a> {
68        Request {
69            method,
70            path_and_query,
71            if_match: None,
72            if_none_match: None,
73            authorization: None,
74            body: None,
75        }
76    }
77
78    /// With this `Authorization` value.
79    pub fn with_authorization(mut self, authorization: Option<&'a str>) -> Request<'a> {
80        self.authorization = authorization;
81        self
82    }
83
84    /// With this body.
85    pub fn with_body(mut self, body: Option<&'a [u8]>) -> Request<'a> {
86        self.body = body;
87        self
88    }
89
90    /// With this `If-Match` value.
91    pub fn with_if_match(mut self, if_match: Option<&'a str>) -> Request<'a> {
92        self.if_match = if_match;
93        self
94    }
95
96    /// With this `If-None-Match` value.
97    pub fn with_if_none_match(mut self, if_none_match: Option<&'a str>) -> Request<'a> {
98        self.if_none_match = if_none_match;
99        self
100    }
101}
102
103impl std::fmt::Debug for Request<'_> {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("Request")
106            .field("method", &self.method)
107            .field("path_and_query", &self.path_and_query)
108            .field("if_match", &self.if_match)
109            .field("if_none_match", &self.if_none_match)
110            .field("authorized", &self.authorization.is_some())
111            .field("body_len", &self.body.map(<[u8]>::len))
112            .finish()
113    }
114}
115
116/// What came back: the status, the response headers the protocol reads,
117/// and the body.
118#[derive(Clone, PartialEq, Eq)]
119#[non_exhaustive]
120pub struct Response {
121    /// The HTTP status.
122    pub status: u16,
123    /// The `ETag` header, if any.
124    pub etag: Option<String>,
125    /// The `X-GV-Expires-At` header (Unix seconds), if any.
126    pub expires_at: Option<String>,
127    /// The body.
128    pub body: Vec<u8>,
129}
130
131impl Response {
132    /// A response with this status and body and no headers.
133    pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Response {
134        Response {
135            status,
136            etag: None,
137            expires_at: None,
138            body: body.into(),
139        }
140    }
141
142    /// With an `ETag` header.
143    pub fn with_etag(mut self, etag: impl Into<String>) -> Response {
144        self.etag = Some(etag.into());
145        self
146    }
147
148    /// With an `X-GV-Expires-At` header.
149    pub fn with_expires_at(mut self, expires_at: impl Into<String>) -> Response {
150        self.expires_at = Some(expires_at.into());
151        self
152    }
153}
154
155impl std::fmt::Debug for Response {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("Response")
158            .field("status", &self.status)
159            .field("etag", &self.etag)
160            .field("expires_at", &self.expires_at)
161            .field("body_len", &self.body.len())
162            .finish()
163    }
164}
165
166/// The request did not get an answer: the server could not be reached, the
167/// connection or TLS failed, the answer was cut short or too large, or it
168/// was a redirect (which is never followed).
169#[derive(Debug, Clone)]
170pub struct TransportError {
171    server: String,
172    message: String,
173    source: Option<Arc<dyn std::error::Error + Send + Sync>>,
174}
175
176impl TransportError {
177    /// A failure reaching `server`, described by `message`.
178    pub fn new(server: impl Into<String>, message: impl Into<String>) -> TransportError {
179        TransportError {
180            server: server.into(),
181            message: message.into(),
182            source: None,
183        }
184    }
185
186    /// A failure reaching `server` caused by `source` (an I/O or TLS error,
187    /// say), which [`std::error::Error::source`] returns.
188    pub fn with_source(
189        server: impl Into<String>,
190        source: impl std::error::Error + Send + Sync + 'static,
191    ) -> TransportError {
192        TransportError {
193            server: server.into(),
194            message: source.to_string(),
195            source: Some(Arc::new(source)),
196        }
197    }
198
199    /// The server the request was for.
200    pub fn server(&self) -> &str {
201        &self.server
202    }
203
204    /// What went wrong, without the server.
205    pub fn message(&self) -> &str {
206        &self.message
207    }
208
209    /// The underlying cause, shared.
210    pub fn cause(&self) -> Option<Arc<dyn std::error::Error + Send + Sync>> {
211        self.source.clone()
212    }
213}
214
215impl std::fmt::Display for TransportError {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(f, "could not reach {}: {}", self.server, self.message)
218    }
219}
220
221impl std::error::Error for TransportError {
222    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223        self.source
224            .as_deref()
225            .map(|e| e as &(dyn std::error::Error + 'static))
226    }
227}
228
229/// Sends canonical requests to one server. `HttpTransport` is the HTTP
230/// implementation; a test, a proxy or an in-process backend can bring its
231/// own. The trait is deliberately small: [`crate::client::Api`] builds, signs and
232/// interprets every request, so an implementation only moves bytes.
233pub trait Transport: Send + Sync {
234    /// Send one request and return the answer, whatever its status. Only a
235    /// failure to get an answer is an error.
236    fn send(&self, request: Request<'_>) -> Result<Response, TransportError>;
237}
238
239impl<T: Transport + ?Sized> Transport for Arc<T> {
240    fn send(&self, request: Request<'_>) -> Result<Response, TransportError> {
241        (**self).send(request)
242    }
243}
244
245impl<T: Transport + ?Sized> Transport for Box<T> {
246    fn send(&self, request: Request<'_>) -> Result<Response, TransportError> {
247        (**self).send(request)
248    }
249}