Skip to main content

pingora_core/apps/
mod.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The abstraction and implementation interface for service application logic
16
17pub mod http_app;
18
19use crate::server::ShutdownWatch;
20use async_trait::async_trait;
21use bytes::BytesMut;
22use log::{debug, error};
23use std::any::Any;
24use std::sync::Arc;
25use std::time::Duration;
26
27use crate::protocols::http::v2::server;
28use crate::protocols::http::{ReusableHttpStream, ServerSession};
29use crate::protocols::Digest;
30use crate::protocols::Stream;
31use crate::protocols::ALPN;
32
33// https://datatracker.ietf.org/doc/html/rfc9113#section-3.4
34const H2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
35
36#[async_trait]
37/// This trait defines the interface of a transport layer (TCP or TLS) application.
38pub trait ServerApp {
39    /// Whenever a new connection is established, this function will be called with the established
40    /// [`Stream`] object provided.
41    ///
42    /// The application can do whatever it wants with the `session`.
43    ///
44    /// After processing the `session`, if the `session`'s connection is reusable, This function
45    /// can return it to the service by returning `Some(session)`. The returned `session` will be
46    /// fed to another [`Self::process_new()`] for another round of processing.
47    /// If not reusable, `None` should be returned.
48    ///
49    /// The `shutdown` argument will change from `false` to `true` when the server receives a
50    /// signal to shutdown. This argument allows the application to react accordingly.
51    async fn process_new(
52        self: &Arc<Self>,
53        mut session: Stream,
54        // TODO: make this ShutdownWatch so that all task can await on this event
55        shutdown: &ShutdownWatch,
56    ) -> Option<Stream>;
57
58    /// This callback will be called once after the service stops listening to its endpoints.
59    async fn cleanup(&self) {}
60}
61#[non_exhaustive]
62#[derive(Clone, Debug, Default)]
63/// HTTP Server options that control how the server handles some transport types.
64pub struct HttpServerOptions {
65    /// Allow HTTP/2 for plaintext.
66    pub h2c: bool,
67
68    /// Allow proxying CONNECT requests when handling HTTP traffic.
69    ///
70    /// When disabled, CONNECT requests are rejected with 405 by proxy services.
71    pub allow_connect_method_proxying: bool,
72
73    #[doc(hidden)]
74    pub force_custom: bool,
75
76    /// Maximum number of requests that this connection will handle. This is
77    /// equivalent to [Nginx's keepalive requests](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive_requests)
78    /// which says:
79    ///
80    /// > Closing connections periodically is necessary to free per-connection
81    /// > memory allocations. Therefore, using too high maximum number of
82    /// > requests could result in excessive memory usage and not recommended.
83    ///
84    /// Unlike nginx, the default behavior here is _no limit_.
85    pub keepalive_request_limit: Option<u32>,
86
87    /// If set, close a downstream HTTP/2 connection that has been idle
88    /// for this duration.
89    ///
90    /// Default: `None`
91    pub h2_idle_timeout: Option<Duration>,
92}
93
94/// Settings persisted across HTTP/1.x keepalive requests on the same downstream connection.
95///
96/// In addition to framework-managed keepalive parameters, this struct can carry an optional
97/// user-defined context via [`set_user_context`](Self::set_user_context). The proxy layer
98/// populates this through `ProxyHttp::persist_connection_context`
99/// and delivers it to the next request through `ProxyHttp::on_connection_reuse`.
100///
101/// Also carries pipelined-prefix bytes when the caller has opted into HTTP/1.1
102/// pipelining on the previous session. See
103/// [`Self::set_pipelined_prefix`] and the
104/// [`HttpSession::set_pipelining_enabled`](crate::protocols::http::v1::server::HttpSession::set_pipelining_enabled)
105/// docs for the RFC 9112 §9.3.2 semantics.
106#[derive(Debug)]
107pub struct HttpPersistentSettings {
108    keepalive_timeout: Option<u64>,
109    keepalive_reuses_remaining: Option<u32>,
110    /// User-defined context to carry to the next request on this connection.
111    user_context: Option<Box<dyn Any + Send + Sync>>,
112    /// Bytes read past the end of the previous request's body, to be parsed
113    /// as the next pipelined request on the reused connection.
114    pipelined_prefix: Option<BytesMut>,
115    /// Whether HTTP/1.1 pipelining was enabled on the previous session;
116    /// propagates to the next session so the proxy-level opt-in sticks
117    /// across keepalive reuses without the adopter having to re-enable
118    /// it on every request.
119    pipelining_enabled: bool,
120}
121
122impl HttpPersistentSettings {
123    pub fn for_session(session: &ServerSession) -> Self {
124        HttpPersistentSettings {
125            keepalive_timeout: session.get_keepalive(),
126            keepalive_reuses_remaining: session.get_keepalive_reuses_remaining(),
127            user_context: None,
128            pipelined_prefix: None,
129            pipelining_enabled: session.pipelining_enabled(),
130        }
131    }
132
133    /// Set a user-defined context to be carried to the next request on this connection.
134    pub fn set_user_context(&mut self, ctx: Box<dyn Any + Send + Sync>) {
135        self.user_context = Some(ctx);
136    }
137
138    /// Take the user-defined context, if any.
139    pub fn take_user_context(&mut self) -> Option<Box<dyn Any + Send + Sync>> {
140        self.user_context.take()
141    }
142
143    /// Set pipelined-prefix bytes to be fed to the next session on this
144    /// connection. Called by the proxy layer when HTTP/1.1 pipelining is
145    /// enabled on the current session and overread bytes were present at
146    /// reuse time.
147    pub fn set_pipelined_prefix(&mut self, prefix: BytesMut) {
148        self.pipelined_prefix = Some(prefix);
149    }
150
151    pub fn apply_to_session(self, session: &mut ServerSession) {
152        let Self {
153            keepalive_timeout,
154            mut keepalive_reuses_remaining,
155            user_context,
156            pipelined_prefix,
157            pipelining_enabled,
158        } = self;
159
160        // Reduce the number of times the connection for this session can be
161        // reused by one. A session with reuse count of zero won't be reused
162        if let Some(reuses) = keepalive_reuses_remaining.as_mut() {
163            *reuses = reuses.saturating_sub(1);
164        }
165
166        session.set_keepalive(keepalive_timeout);
167        session.set_keepalive_reuses_remaining(keepalive_reuses_remaining);
168
169        // Carry user context into the session for the proxy layer to consume
170        session.set_connection_user_context(user_context);
171
172        // Replay pipelining opt-in so it stays on across keepalive reuses.
173        session.set_pipelining_enabled(pipelining_enabled);
174
175        // Feed any pipelined prefix bytes to the new session's request parser
176        // so they are treated as the start of the next request.
177        if let Some(prefix) = pipelined_prefix {
178            session.set_pipelined_prefix(prefix);
179        }
180    }
181}
182
183#[derive(Debug)]
184pub struct ReusedHttpStream {
185    stream: Stream,
186    persistent_settings: Option<HttpPersistentSettings>,
187}
188
189impl ReusedHttpStream {
190    pub fn new(stream: Stream, persistent_settings: Option<HttpPersistentSettings>) -> Self {
191        ReusedHttpStream {
192            stream,
193            persistent_settings,
194        }
195    }
196
197    /// Build a reusable HTTP stream from a finished session, preserving any
198    /// pipelined prefix bytes in the persistent settings for the next request.
199    pub fn from_reusable_stream(
200        reusable: ReusableHttpStream,
201        mut persistent_settings: HttpPersistentSettings,
202    ) -> Self {
203        let (stream, pipelined_prefix) = reusable.into_parts();
204        if let Some(prefix) = pipelined_prefix {
205            persistent_settings.set_pipelined_prefix(prefix);
206        }
207        Self::new(stream, Some(persistent_settings))
208    }
209
210    pub fn consume(self) -> (Stream, Option<HttpPersistentSettings>) {
211        (self.stream, self.persistent_settings)
212    }
213}
214
215/// This trait defines the interface of an HTTP application.
216#[async_trait]
217pub trait HttpServerApp {
218    /// Similar to the [`ServerApp`], this function is called whenever a new HTTP session is established.
219    ///
220    /// After successful processing, [`ServerSession::finish()`] can be
221    /// called to return an optionally reusable connection back to the service.
222    /// The caller needs to make sure that the connection is in a reusable state
223    /// i.e., no error or incomplete read or write headers or bodies. Otherwise
224    /// a `None` should be returned.
225    async fn process_new_http(
226        self: &Arc<Self>,
227        mut session: ServerSession,
228        // TODO: make this ShutdownWatch so that all task can await on this event
229        shutdown: &ShutdownWatch,
230    ) -> Option<ReusedHttpStream>;
231
232    /// Provide options on how HTTP/2 connection should be established. This function will be called
233    /// every time a new HTTP/2 **connection** needs to be established.
234    ///
235    /// A `None` means to use the built-in default options. See [`server::H2Options`] for more details.
236    fn h2_options(&self) -> Option<server::H2Options> {
237        None
238    }
239
240    /// Provide HTTP server options used to override default behavior. This function will be called
241    /// every time a new connection is processed.
242    ///
243    /// A `None` means no server options will be applied.
244    fn server_options(&self) -> Option<&HttpServerOptions> {
245        None
246    }
247
248    async fn http_cleanup(&self) {}
249
250    #[doc(hidden)]
251    async fn process_custom_session(
252        self: Arc<Self>,
253        _stream: Stream,
254        _shutdown: &ShutdownWatch,
255    ) -> Option<Stream> {
256        None
257    }
258}
259
260#[async_trait]
261impl<T> ServerApp for T
262where
263    T: HttpServerApp + Send + Sync + 'static,
264{
265    async fn process_new(
266        self: &Arc<Self>,
267        mut stream: Stream,
268        shutdown: &ShutdownWatch,
269    ) -> Option<Stream> {
270        let mut h2c = self.server_options().as_ref().map_or(false, |o| o.h2c);
271        let custom = self
272            .server_options()
273            .as_ref()
274            .map_or(false, |o| o.force_custom);
275
276        // h2c is for cleartext connections; on TLS, ALPN handles protocol negotiation.
277        // Otherwise, h2c stays true on TLS streams, forcing HTTP/1.1 clients into HTTP/2
278        if stream.get_ssl_digest().is_some() {
279            h2c = false;
280        }
281        // try to read h2 preface
282        else if h2c && !custom {
283            let mut buf = [0u8; H2_PREFACE.len()];
284            let peeked = stream
285                .try_peek(&mut buf)
286                .await
287                .map_err(|e| {
288                    // this error is normal when h1 reuse and close the connection
289                    debug!("Read error while peeking h2c preface {e}");
290                    e
291                })
292                .ok()?;
293            // not all streams support peeking
294            if peeked {
295                // turn off h2c (use h1) if h2 preface doesn't exist
296                h2c = buf == H2_PREFACE;
297            }
298        }
299        if h2c || matches!(stream.selected_alpn_proto(), Some(ALPN::H2)) {
300            // create a shared connection digest
301            let digest = Arc::new(Digest {
302                ssl_digest: stream.get_ssl_digest(),
303                // TODO: log h2 handshake time
304                timing_digest: stream.get_timing_digest(),
305                proxy_digest: stream.get_proxy_digest(),
306                socket_digest: stream.get_socket_digest(),
307            });
308
309            let h2_options = self.h2_options();
310            let h2_conn = match server::handshake(stream, h2_options).await {
311                Err(e) => {
312                    error!("H2 handshake error {e}");
313                    return None;
314                }
315                Ok(c) => c,
316            };
317
318            // The accept-loop body — including the graceful-shutdown state
319            // machine — lives in `server::accept_downstream_sessions` so that
320            // the same code path is exercised by tests in `protocols::http::v2`.
321            let app = self.clone();
322            let shutdown_for_session = shutdown.clone();
323            let h2_idle_timeout = self.server_options().and_then(|o| o.h2_idle_timeout);
324            server::accept_downstream_sessions(
325                h2_conn,
326                digest,
327                shutdown.clone(),
328                h2_idle_timeout,
329                |h2_stream, guard| {
330                    let app = app.clone();
331                    let shutdown = shutdown_for_session.clone();
332                    pingora_runtime::current_handle().spawn(async move {
333                        // hold `guard` for the session's lifetime so the accept
334                        // loop's idle timeout sees this connection as busy.
335                        let _guard = guard;
336                        // Note, `PersistentSettings` not currently relevant for h2
337                        app.process_new_http(ServerSession::new_http2(h2_stream), &shutdown)
338                            .await;
339                    });
340                },
341            )
342            .await;
343        } else if custom || matches!(stream.selected_alpn_proto(), Some(ALPN::Custom(_))) {
344            return self.clone().process_custom_session(stream, shutdown).await;
345        } else {
346            // No ALPN or ALPN::H1 and h2c was not configured, fallback to HTTP/1.1
347            let mut session = ServerSession::new_http1(stream);
348            if *shutdown.borrow() {
349                // stop downstream from reusing if this service is shutting down soon
350                session.set_keepalive(None);
351            } else {
352                // default 60s
353                session.set_keepalive(Some(60));
354            }
355            session.set_keepalive_reuses_remaining(
356                self.server_options()
357                    .and_then(|opts| opts.keepalive_request_limit),
358            );
359
360            let mut result = self.process_new_http(session, shutdown).await;
361            while let Some((stream, persistent_settings)) = result.map(|r| r.consume()) {
362                let mut session = ServerSession::new_http1(stream);
363                if let Some(persistent_settings) = persistent_settings {
364                    persistent_settings.apply_to_session(&mut session);
365                }
366
367                result = self.process_new_http(session, shutdown).await;
368            }
369        }
370        None
371    }
372
373    async fn cleanup(&self) {
374        self.http_cleanup().await;
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use tokio_test::io::Builder;
382
383    #[test]
384    fn test_persistent_settings_user_context_roundtrip() {
385        // Create a mock H1 session
386        let mock_io = Builder::new().build();
387        let mut session = ServerSession::new_http1(Box::new(mock_io));
388        session.set_keepalive(Some(60));
389
390        // Snapshot settings (no user context yet)
391        let mut settings = HttpPersistentSettings::for_session(&session);
392        assert!(settings.take_user_context().is_none());
393
394        // Set user context
395        settings.set_user_context(Box::new(123u64));
396
397        // Apply to a fresh session -- user context should transfer
398        let mock_io2 = Builder::new().build();
399        let mut session2 = ServerSession::new_http1(Box::new(mock_io2));
400        settings.apply_to_session(&mut session2);
401
402        // The user context should now be on the session
403        let ctx = session2.take_connection_user_context();
404        assert!(ctx.is_some());
405        let val = ctx.unwrap().downcast::<u64>().unwrap();
406        assert_eq!(*val, 123u64);
407
408        // Keepalive should also have been applied
409        assert_eq!(session2.get_keepalive(), Some(60));
410    }
411
412    #[test]
413    fn test_persistent_settings_no_user_context_by_default() {
414        let mock_io = Builder::new().build();
415        let mut session = ServerSession::new_http1(Box::new(mock_io));
416        session.set_keepalive(Some(30));
417
418        let settings = HttpPersistentSettings::for_session(&session);
419
420        let mock_io2 = Builder::new().build();
421        let mut session2 = ServerSession::new_http1(Box::new(mock_io2));
422        settings.apply_to_session(&mut session2);
423
424        // No user context should be present
425        assert!(session2.take_connection_user_context().is_none());
426        // Keepalive should still work
427        assert_eq!(session2.get_keepalive(), Some(30));
428    }
429}