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}