Skip to main content

running_process/broker/backend_lib/
accept_handed_off.rs

1//! Acceptance classifier for backend-side handed-off connections.
2//!
3//! The platform modules that eventually receive duplicated handles or passed
4//! file descriptors can wrap those transport values in [`HandedOffPayload`].
5//! This helper validates the one-time handoff token and returns a typed
6//! accepted/rejected classification without knowing anything about the
7//! underlying transport.
8
9use std::time::Instant;
10
11use crate::broker::server::{
12    HandoffToken, HandoffTokenError, HandoffTokenStore, HANDOFF_TOKEN_BYTES,
13};
14
15/// Platform-neutral payload delivered to a backend accept loop.
16///
17/// `connection` is intentionally generic: future Windows and Unix modules can
18/// store a duplicated handle, an owned file descriptor, or a test double here
19/// without changing the token verification logic.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct HandedOffPayload<T> {
22    /// Token the backend expected for this pending handoff.
23    pub expected_token: HandoffToken,
24    /// Token bytes presented with the platform-specific handoff message.
25    pub presented_token: Vec<u8>,
26    /// Platform-specific connection payload.
27    pub connection: T,
28}
29
30impl<T> HandedOffPayload<T> {
31    /// Build a payload from the expected token, raw presented token bytes, and
32    /// platform-specific connection payload.
33    pub fn new(
34        expected_token: HandoffToken,
35        presented_token: impl Into<Vec<u8>>,
36        connection: T,
37    ) -> Self {
38        Self {
39            expected_token,
40            presented_token: presented_token.into(),
41            connection,
42        }
43    }
44
45    /// Return the presented token bytes.
46    pub fn presented_token(&self) -> &[u8] {
47        &self.presented_token
48    }
49
50    /// Split the payload into its platform-specific connection value.
51    pub fn into_connection(self) -> T {
52        self.connection
53    }
54}
55
56/// A handed-off payload accepted by the backend helper.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct AcceptedHandoff<T> {
59    /// Token that was consumed exactly once.
60    pub token: HandoffToken,
61    /// Platform-specific connection payload.
62    pub connection: T,
63}
64
65/// A handed-off payload rejected by the backend helper.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct RejectedHandoff<T> {
68    /// Original platform-neutral payload. The caller decides how to drop or
69    /// close the platform-specific connection.
70    pub payload: HandedOffPayload<T>,
71    /// Reason the payload was not accepted.
72    pub reason: HandoffRejectionReason,
73}
74
75/// Classification returned after backend-side handoff acceptance.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum HandoffAcceptance<T> {
78    /// Token validation succeeded and the one-time token was consumed.
79    Accepted(AcceptedHandoff<T>),
80    /// Token validation failed; the connection payload should not be adopted.
81    Rejected(RejectedHandoff<T>),
82}
83
84impl<T> HandoffAcceptance<T> {
85    /// Return true when the payload was accepted.
86    pub fn is_accepted(&self) -> bool {
87        matches!(self, Self::Accepted(_))
88    }
89
90    /// Return true when the payload was rejected.
91    pub fn is_rejected(&self) -> bool {
92        matches!(self, Self::Rejected(_))
93    }
94
95    /// Convert the classification into a `Result`.
96    pub fn into_result(self) -> Result<AcceptedHandoff<T>, RejectedHandoff<T>> {
97        match self {
98            Self::Accepted(accepted) => Ok(accepted),
99            Self::Rejected(rejected) => Err(rejected),
100        }
101    }
102}
103
104/// Backend-side reason a handed-off payload was rejected.
105#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
106pub enum HandoffRejectionReason {
107    /// The handoff payload did not include token bytes.
108    #[error("handoff token is missing")]
109    MissingToken,
110    /// The handoff payload included token bytes with the wrong length.
111    #[error("handoff token length was {actual_len} bytes; expected {expected_len}")]
112    InvalidTokenLength {
113        /// Presented token byte length.
114        actual_len: usize,
115        /// Required token byte length.
116        expected_len: usize,
117    },
118    /// The presented token did not match the pending handoff.
119    #[error("handoff token mismatch")]
120    TokenMismatch,
121    /// The pending handoff token exceeded its TTL.
122    #[error("handoff token expired")]
123    TokenExpired,
124    /// The expected handoff token was unknown or already consumed.
125    #[error("handoff token is not pending")]
126    TokenNotPending,
127    /// Unexpected token-store error while accepting a payload.
128    #[error("handoff token store error: {error}")]
129    TokenStore {
130        /// Underlying token-store error.
131        error: HandoffTokenError,
132    },
133}
134
135impl From<HandoffTokenError> for HandoffRejectionReason {
136    fn from(value: HandoffTokenError) -> Self {
137        match value {
138            HandoffTokenError::TokenMismatch => Self::TokenMismatch,
139            HandoffTokenError::TokenExpired => Self::TokenExpired,
140            HandoffTokenError::TokenNotPending => Self::TokenNotPending,
141            error => Self::TokenStore { error },
142        }
143    }
144}
145
146/// Parse raw token bytes into a typed handoff token.
147pub fn parse_handoff_token(token: &[u8]) -> Result<HandoffToken, HandoffRejectionReason> {
148    if token.is_empty() {
149        return Err(HandoffRejectionReason::MissingToken);
150    }
151    if token.len() != HANDOFF_TOKEN_BYTES {
152        return Err(HandoffRejectionReason::InvalidTokenLength {
153            actual_len: token.len(),
154            expected_len: HANDOFF_TOKEN_BYTES,
155        });
156    }
157
158    let mut bytes = [0_u8; HANDOFF_TOKEN_BYTES];
159    bytes.copy_from_slice(token);
160    Ok(HandoffToken::from_bytes(bytes))
161}
162
163/// Validate and classify one backend-side handed-off payload.
164///
165/// On success the pending token is consumed exactly once. Malformed token bytes
166/// and mismatches leave the pending token available so the caller can still
167/// accept a later correctly-presented payload. Expired tokens are pruned by the
168/// token store.
169pub fn accept_handed_off<T>(
170    pending_tokens: &mut HandoffTokenStore,
171    payload: HandedOffPayload<T>,
172    now: Instant,
173) -> HandoffAcceptance<T> {
174    let presented = match parse_handoff_token(payload.presented_token()) {
175        Ok(token) => token,
176        Err(reason) => return reject(payload, reason),
177    };
178
179    match pending_tokens.consume_matching(&payload.expected_token, &presented, now) {
180        Ok(()) => HandoffAcceptance::Accepted(AcceptedHandoff {
181            token: payload.expected_token,
182            connection: payload.connection,
183        }),
184        Err(error) => reject(payload, error.into()),
185    }
186}
187
188fn reject<T>(payload: HandedOffPayload<T>, reason: HandoffRejectionReason) -> HandoffAcceptance<T> {
189    HandoffAcceptance::Rejected(RejectedHandoff { payload, reason })
190}