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;