Skip to main content

pg_proto/
cancel.rs

1//! Session-external cancellation-key translation for proxies.
2
3use std::collections::HashMap;
4
5use bytes::Bytes;
6
7use crate::demux::CancelKey;
8
9/// Application policy for minting client-facing cancellation keys.
10///
11/// Implementations may use cryptographic randomness, an external allocator, or
12/// another process-specific strategy. The protocol library does not prescribe
13/// key lifecycle or storage.
14pub trait CancelKeyMint {
15    /// Error returned when a client-facing key cannot be minted.
16    type Error;
17
18    /// Mints a key to expose in client-facing `BackendKeyData`.
19    ///
20    /// # Errors
21    ///
22    /// Returns an implementation-defined allocation or entropy error.
23    fn mint_cancel_key(&mut self) -> Result<CancelKey, Self::Error>;
24}
25
26/// Application-owned translation policy for out-of-band cancellation keys.
27///
28/// A proxy can implement this over local memory, shared storage, or routing
29/// metadata. [`CancelKeyMap`] is deliberately only a small reference
30/// implementation.
31pub trait CancelKeyRegistry {
32    /// Error returned when a key association cannot be registered.
33    type Error;
34
35    /// Observes the association between a client-facing and upstream key.
36    ///
37    /// # Errors
38    ///
39    /// Returns an implementation-defined validation, collision, or storage
40    /// error.
41    fn register_cancel_key(
42        &mut self,
43        client: CancelKey,
44        upstream: CancelKey,
45    ) -> Result<(), Self::Error>;
46
47    /// Resolves an incoming client cancellation request without borrowing the
48    /// registry, so the result can safely cross an asynchronous boundary.
49    fn resolve_cancel_key(&self, client: &CancelKey) -> Option<CancelKey>;
50
51    /// Removes an association when either side of a session is detached.
52    fn remove_cancel_key(&mut self, client: &CancelKey) -> Option<CancelKey>;
53}
54
55/// Client-facing cancellation keys mapped to their current upstream keys.
56#[derive(Debug, Default)]
57pub struct CancelKeyMap {
58    mappings: HashMap<CancelKey, CancelKey>,
59}
60
61/// Validation or collision failure while registering a cancellation mapping.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub enum RegisterError {
64    /// Client-facing secret length was outside `PostgreSQL`'s accepted range.
65    InvalidClientKeyLength(usize),
66    /// Upstream secret length was outside `PostgreSQL`'s accepted range.
67    InvalidUpstreamKeyLength(usize),
68    /// The client-facing key already names another live mapping.
69    ClientKeyCollision,
70}
71
72impl CancelKeyMap {
73    /// Creates an empty in-memory cancellation-key registry.
74    #[must_use]
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Registers one proxy-minted client key for an attached upstream session.
80    ///
81    /// # Errors
82    ///
83    /// Rejects keys outside the protocol's 4–256 byte range and client-key
84    /// collisions. Existing mappings are never silently replaced.
85    pub fn register(
86        &mut self,
87        client: CancelKey,
88        upstream: CancelKey,
89    ) -> Result<(), RegisterError> {
90        validate_key(&client).map_err(RegisterError::InvalidClientKeyLength)?;
91        validate_key(&upstream).map_err(RegisterError::InvalidUpstreamKeyLength)?;
92        if self.mappings.contains_key(&client) {
93            return Err(RegisterError::ClientKeyCollision);
94        }
95        self.mappings.insert(client, upstream);
96        Ok(())
97    }
98
99    /// Resolves an inspected client `CancelRequest` to its upstream key.
100    #[must_use]
101    pub fn resolve(&self, process_id: u32, secret_key: &[u8]) -> Option<&CancelKey> {
102        self.mappings.get(&CancelKey {
103            process_id,
104            secret_key: Bytes::copy_from_slice(secret_key),
105        })
106    }
107
108    /// Detaches a client key when its upstream session is released or replaced.
109    pub fn remove(&mut self, client: &CancelKey) -> Option<CancelKey> {
110        self.mappings.remove(client)
111    }
112
113    /// Returns the number of live client-to-upstream mappings.
114    #[must_use]
115    pub fn len(&self) -> usize {
116        self.mappings.len()
117    }
118
119    /// Returns whether the registry contains no mappings.
120    #[must_use]
121    pub fn is_empty(&self) -> bool {
122        self.mappings.is_empty()
123    }
124}
125
126impl CancelKeyRegistry for CancelKeyMap {
127    type Error = RegisterError;
128
129    fn register_cancel_key(
130        &mut self,
131        client: CancelKey,
132        upstream: CancelKey,
133    ) -> Result<(), Self::Error> {
134        self.register(client, upstream)
135    }
136
137    fn resolve_cancel_key(&self, client: &CancelKey) -> Option<CancelKey> {
138        self.mappings.get(client).cloned()
139    }
140
141    fn remove_cancel_key(&mut self, client: &CancelKey) -> Option<CancelKey> {
142        self.remove(client)
143    }
144}
145
146fn validate_key(key: &CancelKey) -> Result<(), usize> {
147    if (4..=256).contains(&key.secret_key.len()) {
148        Ok(())
149    } else {
150        Err(key.secret_key.len())
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn maps_variable_length_client_keys_without_overwriting_collisions() {
160        let client = CancelKey {
161            process_id: 7,
162            secret_key: Bytes::from(vec![0xAA; 32]),
163        };
164        let upstream = CancelKey {
165            process_id: 42,
166            secret_key: Bytes::from_static(b"upstream"),
167        };
168        let mut map = CancelKeyMap::new();
169        map.register(client.clone(), upstream.clone()).unwrap();
170
171        assert_eq!(map.resolve(7, &[0xAA; 32]), Some(&upstream));
172        assert_eq!(
173            map.register(client.clone(), upstream.clone()),
174            Err(RegisterError::ClientKeyCollision)
175        );
176        assert_eq!(map.remove(&client), Some(upstream));
177        assert!(map.is_empty());
178    }
179
180    #[test]
181    fn rejects_keys_which_cannot_be_encoded_as_cancel_requests() {
182        let mut map = CancelKeyMap::new();
183        let client = CancelKey {
184            process_id: 1,
185            secret_key: Bytes::from_static(b"bad"),
186        };
187        let upstream = CancelKey {
188            process_id: 2,
189            secret_key: Bytes::from_static(b"valid"),
190        };
191        assert_eq!(
192            map.register(client, upstream),
193            Err(RegisterError::InvalidClientKeyLength(3))
194        );
195    }
196
197    #[test]
198    fn reference_map_can_be_used_through_the_policy_hook() {
199        let client = CancelKey {
200            process_id: 11,
201            secret_key: Bytes::from_static(b"client"),
202        };
203        let upstream = CancelKey {
204            process_id: 22,
205            secret_key: Bytes::from_static(b"server"),
206        };
207        let registry: &mut dyn CancelKeyRegistry<Error = RegisterError> = &mut CancelKeyMap::new();
208
209        registry
210            .register_cancel_key(client.clone(), upstream.clone())
211            .unwrap();
212        assert_eq!(registry.resolve_cancel_key(&client), Some(upstream.clone()));
213        assert_eq!(registry.remove_cancel_key(&client), Some(upstream));
214    }
215}