Skip to main content

cratestack_client_rust/client/
sealing.rs

1//! What a client carries to seal its calls (cratestack#1007).
2//!
3//! One field on [`CratestackClient`], so the constructors (`new`,
4//! `with_http_client`, `with_middleware_client`) initialise it with
5//! `Default` and none of them knows whether the `cose` feature is on. Without
6//! the feature the struct is empty and `at` is a plain clone.
7
8use crate::client::core::CratestackClient;
9use crate::client::route::RouteRef;
10use crate::codec::HttpClientCodec;
11
12#[cfg(feature = "cose")]
13use crate::client::contract::Contracts;
14#[cfg(feature = "cose")]
15use crate::envelope::ClientEnvelope;
16#[cfg(feature = "cose")]
17use crate::error::ClientError;
18
19/// A route detached from the call that named it.
20#[cfg(feature = "cose")]
21#[derive(Debug, Clone)]
22pub(crate) struct OwnedRoute {
23    pub(crate) template: String,
24    pub(crate) params: Vec<String>,
25}
26
27#[derive(Clone, Default)]
28pub(crate) struct Sealing {
29    #[cfg(feature = "cose")]
30    pub(crate) envelope: Option<ClientEnvelope>,
31    /// The op contract digests the binding needs (`client/contract.rs`).
32    #[cfg(feature = "cose")]
33    pub(crate) contracts: Option<Contracts>,
34    /// The route of the call in flight, set by [`CratestackClient::at`].
35    #[cfg(feature = "cose")]
36    pub(crate) route: Option<OwnedRoute>,
37}
38
39impl<C> CratestackClient<C>
40where
41    C: HttpClientCodec,
42{
43    /// A copy of this client that knows which route its next call is for,
44    /// so a sealed call can bind the route template (see [`RouteRef`]).
45    ///
46    /// Generated code calls it before every REST operation
47    /// (`runtime.at(RouteRef::new("/widgets/{id}", &[&id])).get(..)`) and the
48    /// RPC client before every op. It is a copy rather than a `*_at` twin of
49    /// each of the twelve request methods, the same shape as
50    /// [`with_idempotency`](Self::with_idempotency); it does nothing unless an
51    /// envelope is set.
52    #[must_use]
53    pub fn at(&self, route: RouteRef<'_>) -> Self {
54        #[cfg(not(feature = "cose"))]
55        let _ = route;
56        #[allow(unused_mut)]
57        let mut client = self.clone();
58        client.compact_route = client.compact.as_ref().map(|_| route.template().into());
59        #[cfg(feature = "cose")]
60        if client.sealing.envelope.is_some() {
61            client.sealing.route = Some(OwnedRoute {
62                template: route.template().to_owned(),
63                params: route.params().iter().map(|p| (*p).to_owned()).collect(),
64            });
65        }
66        client
67    }
68}
69
70#[cfg(feature = "cose")]
71impl<C> CratestackClient<C>
72where
73    C: HttpClientCodec,
74{
75    /// Seal every request and open every response with `envelope`, over
76    /// REST and RPC alike. The client then never sends a plain request and
77    /// never accepts a plain response (ADR 0006 `Required` mode): an answer
78    /// without a seal is [`EnvelopeError::Unsigned`](crate::EnvelopeError).
79    ///
80    /// Needs the op contract digests, which the generated `Client::new`
81    /// supplies ([`with_contracts`](Self::with_contracts)); a bare client
82    /// fails its first call with `BadInput`.
83    ///
84    /// The codec's `CONTENT_TYPE` is the type of every sealed request, and
85    /// its [`payload_accept`](HttpClientCodec::payload_accept) the types it
86    /// reads back; CBOR is the default and sends nothing extra, any other
87    /// type travels in the `Cratestack-Payload-Type` / `-Accept` headers and
88    /// is bound in the seal (cratestack#1168), so the server's layer must
89    /// have opted in to it. Fails with `BadInput` for a codec whose types can
90    /// never be sealed (an envelope, a stream, a multipart body, or anything
91    /// outside the lowercase `type/subtype` grammar). A response sealed under
92    /// a type the codec did not list is
93    /// [`EnvelopeError::UnexpectedPayloadType`](crate::EnvelopeError), and is
94    /// never decoded. A sealed `/rpc/batch` is CBOR both ways, so a batch over
95    /// another codec fails with `BadInput` before anything is sent.
96    ///
97    /// **Retries.** A sealed request carries a fresh `cti`, and the server
98    /// answers a replayed one with an unsigned `401`. So sealed requests are
99    /// marked [`RequestIdempotency::new(false)`](crate::RequestIdempotency)
100    /// for `reqwest-middleware` whatever their method, and a retry layer
101    /// must re-enter the client, not replay the bytes.
102    ///
103    /// **Redirects.** A sealed request is bound to one route, so the client
104    /// never follows a redirect: [`CratestackClient::new`] builds its
105    /// `reqwest::Client` with `redirect::Policy::none()`, and an answer from
106    /// any URL other than the one sealed for is
107    /// [`EnvelopeError::Unverified`](crate::EnvelopeError). A client supplied
108    /// through `with_http_client` or `with_middleware_client` **must not
109    /// follow redirects** either; the check after the fact catches a `303`
110    /// that turned the call into a plain `GET`, but not a hop that already
111    /// received the sealed bytes.
112    ///
113    /// **On `wasm32` the redirect is followed by the browser.** reqwest
114    /// 0.13's wasm client cannot set `fetch`'s `redirect: "error"` mode (it
115    /// exposes only `no-cors`), and `fetch` follows a redirect before this
116    /// client sees the response. The `response.url()` check therefore
117    /// detects the redirect only after the redirected request has been
118    /// sent: it stops the answer being accepted, not the request being
119    /// re-sent. Do not put a redirecting hop in front of a browser client.
120    /// **Compact keys** adapt under the seal as on a plain call (`compact` module docs).
121    /// **Streams** ([`RpcClient::call_streaming`](crate::RpcClient), the
122    /// `*_streamed` methods, subscriptions) are refused with
123    /// [`EnvelopeError::StreamsUnsupported`](crate::EnvelopeError) until
124    /// ADR 0006 P1.
125    ///
126    /// **Authorizers** still run, over the *inner* payload with the codec's
127    /// `Content-Type` (none for a bodiless call): the request the server's
128    /// `AuthProvider` sees after it opens the seal.
129    pub fn with_envelope(mut self, envelope: ClientEnvelope) -> Result<Self, ClientError> {
130        crate::envelope::check_payload_types(C::CONTENT_TYPE, self.codec.payload_accept())?;
131        self.sealing.envelope = Some(envelope);
132        Ok(self)
133    }
134
135    /// The generating schema's `OP_CONTRACTS`: the digest of each op's wire
136    /// shape, looked up per call and bound into the sealed request (binding
137    /// version 2, cratestack#1123). Called by the generated `Client::new`,
138    /// next to [`with_schema_sha`](Self::with_schema_sha). A call to an op
139    /// the table lacks fails with `BadInput` and is never sent.
140    #[must_use]
141    pub fn with_contracts(mut self, contracts: cratestack_core::OpContracts) -> Self {
142        self.sealing.contracts = Some(Contracts::Table(contracts));
143        self
144    }
145
146    /// Bind one digest in every sealed call, for a hand-built client of a
147    /// single op (raw calls, tests). Prefer [`with_contracts`](Self::with_contracts).
148    #[must_use]
149    pub fn with_contract_sha(mut self, digest: [u8; 32]) -> Self {
150        self.sealing.contracts = Some(Contracts::Pinned(digest));
151        self
152    }
153}
154
155/// Without the feature the generated `Client::new` still calls this, and it
156/// does nothing.
157#[cfg(not(feature = "cose"))]
158impl<C> CratestackClient<C>
159where
160    C: HttpClientCodec,
161{
162    #[doc(hidden)]
163    #[must_use]
164    pub fn with_contracts(self, contracts: cratestack_core::OpContracts) -> Self {
165        let _ = contracts;
166        self
167    }
168}
169
170mod raw;