Skip to main content

pg_proto/
intermediary.rs

1//! Policy-neutral composition of independent downstream and upstream sessions.
2
3use std::future::Future;
4
5use crate::pipeline::{NoPipeline, Pipeline, PipelinePolicy};
6
7/// Result of changing one side while preserving the complete intermediary on rejection.
8pub type IntermediaryTransition<Current, Next, Output, Error> =
9    Result<(Next, Output), (Current, Error)>;
10
11/// Owns two independently typed sides and optional pipeline orchestration.
12///
13/// `Downstream` is normally a server-role session facing a client and `Upstream`
14/// is normally a client-role session facing `PostgreSQL`. No phase, transport,
15/// authentication mechanism, or cleanliness index is coupled between them.
16/// `Policy` defaults to [`NoPipeline`]; call [`Self::with_pipeline`] to opt into
17/// bounded pipelining without changing either session type.
18#[must_use = "dropping an intermediary abandons both PostgreSQL sessions"]
19#[derive(Debug)]
20pub struct Intermediary<Downstream, Upstream, Policy = NoPipeline> {
21    downstream: Downstream,
22    upstream: Upstream,
23    pipeline: Pipeline<Policy>,
24}
25
26impl<Downstream, Upstream> Intermediary<Downstream, Upstream, NoPipeline> {
27    /// Pairs two independently established protocol sessions.
28    pub fn new(downstream: Downstream, upstream: Upstream) -> Self {
29        Self {
30            downstream,
31            upstream,
32            pipeline: Pipeline::new(NoPipeline),
33        }
34    }
35}
36
37impl<Downstream, Upstream, Policy: PipelinePolicy> Intermediary<Downstream, Upstream, Policy> {
38    /// Replaces the pipeline policy while no pipeline operations are outstanding.
39    ///
40    /// # Panics
41    ///
42    /// Panics if operations were accepted before replacing the policy.
43    pub fn with_pipeline<Next: PipelinePolicy>(
44        self,
45        policy: Next,
46    ) -> Intermediary<Downstream, Upstream, Next> {
47        let Self {
48            downstream,
49            upstream,
50            pipeline,
51        } = self;
52        assert!(
53            pipeline.is_empty(),
54            "pipeline policy cannot change with outstanding operations"
55        );
56        Intermediary {
57            downstream,
58            upstream,
59            pipeline: Pipeline::new(policy),
60        }
61    }
62
63    /// Returns the reusable request/response pipeline component.
64    pub const fn pipeline(&self) -> &Pipeline<Policy> {
65        &self.pipeline
66    }
67
68    /// Returns mutable access to bounded pipeline orchestration.
69    pub const fn pipeline_mut(&mut self) -> &mut Pipeline<Policy> {
70        &mut self.pipeline
71    }
72
73    /// Borrows the client-facing side without weakening its typestate.
74    pub const fn downstream(&self) -> &Downstream {
75        &self.downstream
76    }
77
78    /// Borrows the upstream-facing side without weakening its typestate.
79    pub const fn upstream(&self) -> &Upstream {
80        &self.upstream
81    }
82
83    /// Mutably borrows both sides for transport-level orchestration.
84    pub const fn sides_mut(&mut self) -> (&mut Downstream, &mut Upstream) {
85        (&mut self.downstream, &mut self.upstream)
86    }
87
88    /// Deliberately separates the independently typed sessions.
89    pub fn into_parts(self) -> (Downstream, Upstream) {
90        (self.downstream, self.upstream)
91    }
92
93    /// Applies one fallible downstream transition while retaining the upstream
94    /// session unchanged.
95    ///
96    /// A rejected transition must return its original downstream value, allowing
97    /// this method to reconstruct the original intermediary without runtime state.
98    ///
99    /// # Errors
100    ///
101    /// Returns the reconstructed intermediary and transition error when the
102    /// downstream side rejects the transition.
103    pub fn transition_downstream<Next, Output, Error>(
104        self,
105        transition: impl FnOnce(Downstream) -> Result<(Next, Output), (Downstream, Error)>,
106    ) -> IntermediaryTransition<Self, Intermediary<Next, Upstream, Policy>, Output, Error> {
107        let Self {
108            downstream,
109            upstream,
110            pipeline,
111        } = self;
112        match transition(downstream) {
113            Ok((downstream, output)) => Ok((
114                Intermediary {
115                    downstream,
116                    upstream,
117                    pipeline,
118                },
119                output,
120            )),
121            Err((downstream, error)) => Err((
122                Self {
123                    downstream,
124                    upstream,
125                    pipeline,
126                },
127                error,
128            )),
129        }
130    }
131
132    /// Applies one fallible upstream transition while retaining the downstream
133    /// session unchanged.
134    ///
135    /// # Errors
136    ///
137    /// Returns the reconstructed intermediary and transition error when the
138    /// upstream side rejects the transition.
139    pub fn transition_upstream<Next, Output, Error>(
140        self,
141        transition: impl FnOnce(Upstream) -> Result<(Next, Output), (Upstream, Error)>,
142    ) -> IntermediaryTransition<Self, Intermediary<Downstream, Next, Policy>, Output, Error> {
143        let Self {
144            downstream,
145            upstream,
146            pipeline,
147        } = self;
148        match transition(upstream) {
149            Ok((upstream, output)) => Ok((
150                Intermediary {
151                    downstream,
152                    upstream,
153                    pipeline,
154                },
155                output,
156            )),
157            Err((upstream, error)) => Err((
158                Self {
159                    downstream,
160                    upstream,
161                    pipeline,
162                },
163                error,
164            )),
165        }
166    }
167
168    /// Runs custom synchronous policy with mutable access to both sides.
169    ///
170    /// The message and result types are chosen by downstream code; `pg-proto`
171    /// neither prescribes a rewrite policy nor advances either session implicitly.
172    ///
173    /// # Errors
174    ///
175    /// Returns any error produced by the inspection policy.
176    pub fn inspect<Message, Output, Error>(
177        &mut self,
178        message: Message,
179        inspect: impl FnOnce(&mut Downstream, &mut Upstream, Message) -> Result<Output, Error>,
180    ) -> Result<Output, Error> {
181        inspect(&mut self.downstream, &mut self.upstream, message)
182    }
183
184    /// Runs custom asynchronous policy with mutable access to both sides.
185    ///
186    /// # Errors
187    ///
188    /// Returns any error produced by the asynchronous inspection policy.
189    pub async fn inspect_async<Message, Output, Error, Work>(
190        &mut self,
191        message: Message,
192        inspect: impl FnOnce(&mut Downstream, &mut Upstream, Message) -> Work,
193    ) -> Result<Output, Error>
194    where
195        Work: Future<Output = Result<Output, Error>>,
196    {
197        inspect(&mut self.downstream, &mut self.upstream, message).await
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use bytes::Bytes;
204
205    use super::Intermediary;
206    use crate::{
207        Conn, Pristine,
208        auth::{Auth, AuthOffer, SaslInitial, TlsServerEndPoint},
209        codec::Authentication,
210        grammar::{backend, frontend},
211        server_auth::{ServerAuth, ServerPassword},
212    };
213
214    #[derive(Debug)]
215    struct ClientFacingTls;
216
217    #[derive(Debug)]
218    struct UpstreamTls;
219
220    impl TlsServerEndPoint for ClientFacingTls {
221        fn tls_server_end_point(&self) -> &[u8] {
222            b"client-facing-certificate"
223        }
224    }
225
226    impl TlsServerEndPoint for UpstreamTls {
227        fn tls_server_end_point(&self) -> &[u8] {
228            b"upstream-certificate"
229        }
230    }
231
232    #[test]
233    fn each_side_transitions_without_coupling_the_other() {
234        #[derive(Debug)]
235        struct Clean;
236
237        let downstream: backend::TypedSession<(), backend::Ready, Clean> =
238            backend::TypedSession::with_transport(());
239        let upstream: frontend::TypedSession<(), frontend::Ready, Clean> =
240            frontend::TypedSession::with_transport(());
241        let intermediary = Intermediary::new(downstream, upstream);
242
243        let (intermediary, downstream_query) = intermediary
244            .transition_downstream(|session| {
245                session.query(Bytes::from_static(b"select 1"), |(), query| {
246                    Ok::<_, &'static str>(query)
247                })
248            })
249            .expect("downstream inspection succeeds");
250        assert_eq!(downstream_query, Bytes::from_static(b"select 1"));
251
252        let (intermediary, upstream_query) = intermediary
253            .transition_upstream(|session| {
254                session.query(Bytes::from_static(b"select 2"), |(), query| {
255                    Ok::<_, &'static str>(query)
256                })
257            })
258            .expect("upstream inspection succeeds");
259        assert_eq!(upstream_query, Bytes::from_static(b"select 2"));
260
261        let (_downstream, _upstream): (
262            backend::TypedSession<(), backend::Simple, backend::Dirty>,
263            frontend::TypedSession<(), frontend::Simple, frontend::Dirty>,
264        ) = intermediary.into_parts();
265    }
266
267    #[test]
268    fn tls_and_authentication_mechanisms_remain_asymmetric() {
269        let downstream: Conn<ClientFacingTls, ServerAuth, Pristine> =
270            Conn::new(ClientFacingTls).transition();
271        let upstream: Conn<UpstreamTls, Auth, Pristine> = Conn::new(UpstreamTls).transition();
272
273        let (downstream, cleartext_request) = downstream.request_cleartext().unwrap();
274        let AuthOffer::Sasl {
275            conn: upstream,
276            mechanisms,
277        } = upstream
278            .offer(Authentication::Sasl {
279                mechanisms: vec![Bytes::from_static(b"SCRAM-SHA-256-PLUS")],
280            })
281            .unwrap()
282        else {
283            panic!("upstream did not independently select SASL")
284        };
285        assert_eq!(cleartext_request.tag, b'R');
286        assert_eq!(mechanisms, [Bytes::from_static(b"SCRAM-SHA-256-PLUS")]);
287
288        let intermediary: Intermediary<
289            Conn<ClientFacingTls, ServerPassword, Pristine>,
290            Conn<UpstreamTls, SaslInitial, Pristine>,
291        > = Intermediary::new(downstream, upstream);
292        assert_eq!(
293            intermediary.downstream().tls_server_end_point(),
294            b"client-facing-certificate"
295        );
296        assert_eq!(
297            intermediary.upstream().tls_server_end_point(),
298            b"upstream-certificate"
299        );
300        let (downstream, upstream) = intermediary.into_parts();
301        let _downstream_transport = downstream.into_transport();
302        let _upstream_transport = upstream.into_transport();
303    }
304
305    #[test]
306    fn rejected_transition_reconstructs_both_original_sides() {
307        let intermediary = Intermediary::new(vec![1_u8], vec![2_u8]);
308        let (intermediary, error) = intermediary
309            .transition_downstream(|downstream| Err::<(Vec<u8>, ()), _>((downstream, "reject")))
310            .unwrap_err();
311        assert_eq!(error, "reject");
312        assert_eq!(intermediary.into_parts(), (vec![1], vec![2]));
313    }
314
315    #[tokio::test]
316    async fn asynchronous_policy_can_modify_or_replace_a_typed_message() {
317        let mut intermediary = Intermediary::new(Vec::<Bytes>::new(), Vec::<Bytes>::new());
318        let rewritten = intermediary
319            .inspect_async(
320                Bytes::from_static(b"select secret"),
321                |downstream, upstream, query| {
322                    downstream.push(query);
323                    upstream.push(Bytes::from_static(b"select public"));
324                    std::future::ready(Ok::<_, std::convert::Infallible>(Bytes::from_static(
325                        b"select public",
326                    )))
327                },
328            )
329            .await
330            .unwrap();
331        assert_eq!(rewritten, Bytes::from_static(b"select public"));
332        assert_eq!(intermediary.downstream().len(), 1);
333        assert_eq!(intermediary.upstream().len(), 1);
334    }
335}