Skip to main content

mkit_server/hooks/
channel.rs

1//! The transport a hook call travels over (SPEC-SERVER §§6.1, 7.3).
2//!
3//! Core builds, signs and validates hook traffic; a channel only moves bytes.
4//! The native adapter's HTTPS client (WP-3.8) and the Workers service binding
5//! (WP-3.9) implement [`HookChannel`]; tests use an in-memory one.
6
7use core::time::Duration;
8
9use zeroize::Zeroizing;
10
11use crate::error::Redacted;
12use crate::rt::{MaybeSend, MaybeSync};
13
14/// One Connect unary call, ready to send.
15///
16/// The body may carry admission credentials, so [`core::fmt::Debug`] prints
17/// only its length and the body is wiped when dropped. Channels must not log
18/// it.
19#[non_exhaustive]
20pub struct HookRequest {
21    /// The full Connect path, e.g. `/mkit.server.hooks.v1.HooksService/Admit`;
22    /// the channel appends it to its base URL and does not follow redirects.
23    pub procedure: &'static str,
24    /// Every header to send: `Content-Type`, `Connect-Protocol-Version` and,
25    /// on a signed channel, the eight `X-Mkit-Hook-*` headers.
26    pub headers: Vec<(&'static str, String)>,
27    /// The exact JSON body; the signature covers these bytes.
28    pub body: Zeroizing<Vec<u8>>,
29    /// How long the call may take. Core also enforces it, so a channel that
30    /// can cancel the underlying request should.
31    pub timeout: Duration,
32    /// Stop reading the response after this many bytes plus one. Return what
33    /// was read with the real status, so core sees the oversize itself and a
34    /// 2xx to a delivery still acknowledges (SPEC-SERVER §8); report
35    /// [`ChannelError::TooLarge`] only when no status is known. Core re-checks
36    /// the length it receives either way.
37    pub max_response_bytes: usize,
38}
39
40impl HookRequest {
41    /// A request, for a channel's own tests.
42    #[must_use]
43    pub fn new(
44        procedure: &'static str,
45        headers: Vec<(&'static str, String)>,
46        body: Vec<u8>,
47        timeout: Duration,
48        max_response_bytes: usize,
49    ) -> Self {
50        Self {
51            procedure,
52            headers,
53            body: Zeroizing::new(body),
54            timeout,
55            max_response_bytes,
56        }
57    }
58}
59
60impl core::fmt::Debug for HookRequest {
61    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62        f.debug_struct("HookRequest")
63            .field("procedure", &self.procedure)
64            .field("body_bytes", &self.body.len())
65            .field("timeout", &self.timeout)
66            .finish_non_exhaustive()
67    }
68}
69
70/// A hook's HTTP response, whatever its status.
71#[non_exhaustive]
72pub struct HookResponse {
73    /// The HTTP status.
74    pub status: u16,
75    /// The `Content-Type` header, if any.
76    pub content_type: Option<String>,
77    /// At most `max_response_bytes + 1` bytes of the body.
78    pub body: Vec<u8>,
79}
80
81impl HookResponse {
82    /// A response with `status`, `content_type` and `body`.
83    #[must_use]
84    pub fn new(status: u16, content_type: Option<String>, body: Vec<u8>) -> Self {
85        Self {
86            status,
87            content_type,
88            body,
89        }
90    }
91}
92
93impl core::fmt::Debug for HookResponse {
94    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95        f.debug_struct("HookResponse")
96            .field("status", &self.status)
97            .field("body_bytes", &self.body.len())
98            .finish_non_exhaustive()
99    }
100}
101
102/// Why a channel produced no response. Every variant means "the hook did not
103/// answer": the adapter fails closed (SPEC-SERVER §8).
104#[derive(Debug, Clone, thiserror::Error)]
105#[non_exhaustive]
106pub enum ChannelError {
107    /// The channel gave up waiting.
108    #[error("hook call timed out")]
109    Timeout,
110    /// The response exceeded `max_response_bytes` and its status is unknown.
111    #[error("hook response too large")]
112    TooLarge,
113    /// Connection, TLS, binding or other failure. Core never prints the text.
114    #[error("hook transport failed")]
115    Transport(Redacted),
116}
117
118/// A route to one hook service.
119pub trait HookChannel: MaybeSend + MaybeSync {
120    /// The hook endpoint's canonical origin, the `<audience>` a signature
121    /// binds (SPEC-SERVER §7.1). `None` for a channel with no origin, which is
122    /// then only usable unsigned and isolated. Plain `http://` is accepted
123    /// only for a loopback host (§6.1); any other channel must use TLS with
124    /// certificate verification and must not follow redirects.
125    fn audience(&self) -> Option<&str>;
126
127    /// Whether this is a platform service binding unreachable from the public
128    /// internet, the only channel that may skip signing (SPEC-SERVER §7.3).
129    fn isolated(&self) -> bool {
130        false
131    }
132
133    /// Send `request` and return the hook's response.
134    ///
135    /// # Errors
136    /// [`ChannelError`] when there is no response to return.
137    fn call(
138        &self,
139        request: HookRequest,
140    ) -> impl core::future::Future<Output = Result<HookResponse, ChannelError>> + MaybeSend;
141}