1use std::{
8 future::Future,
9 pin::Pin,
10 sync::Arc,
11 task::{ready, Context, Poll},
12 time::Duration,
13};
14
15use async_trait::async_trait;
16use http::{Request, Response};
17use http_body::Body;
18use hyper::{
19 body::Incoming,
20 rt::{Read, Write},
21 service::Service,
22};
23use hyper_util::{
24 rt::{TokioExecutor, TokioIo, TokioTimer},
25 server::conn::auto::Builder,
26};
27use rustls::ServerConfig;
28use saluki_common::{
29 sync::shutdown::{ShutdownCoordinator, ShutdownHandle},
30 task::{spawn_traced_named, HandleExt as _},
31};
32use saluki_core::runtime::{InitializationError, ShutdownStrategy, Supervisable, SupervisorFuture};
33use saluki_error::{ErrorContext as _, GenericError};
34use saluki_tls::ensure_server_config_fips_compliant;
35use tokio::{pin, runtime::Handle, select, sync::oneshot, time::timeout};
36use tokio_rustls::TlsAcceptor;
37use tracing::{debug, error, info, warn};
38
39use crate::net::{listener::ConnectionOrientedListener, ListenAddress};
40
41fn build_conn_builder() -> Builder<TokioExecutor> {
42 let mut builder = Builder::new(TokioExecutor::new());
43 builder
44 .http1()
45 .timer(TokioTimer::new())
46 .header_read_timeout(Duration::from_secs(10));
47 builder
48}
49
50pub struct HttpServer<S> {
64 listen_address: ListenAddress,
65 tls_config: Option<ServerConfig>,
66 conn_builder: Builder<TokioExecutor>,
67 graceful_shutdown_timeout: Option<Duration>,
68 service: S,
69}
70
71impl<S> HttpServer<S> {
72 pub fn from_listen_address(listen_address: ListenAddress, service: S) -> Self {
74 Self {
75 listen_address,
76 tls_config: None,
77 conn_builder: build_conn_builder(),
78 graceful_shutdown_timeout: None,
79 service,
80 }
81 }
82
83 pub fn with_graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
93 self.graceful_shutdown_timeout = Some(timeout);
94 self
95 }
96
97 pub fn with_tls_config(mut self, config: ServerConfig) -> Self {
103 self.tls_config = Some(config);
104 self
105 }
106}
107
108#[async_trait]
109impl<S, B> Supervisable for HttpServer<S>
110where
111 S: Service<Request<Incoming>, Response = Response<B>> + Send + Sync + Clone + 'static,
112 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
113 S::Future: Send + 'static,
114 B: Body + Send + 'static,
115 B::Data: Send,
116 B::Error: std::error::Error + Send + Sync,
117{
118 fn name(&self) -> &str {
119 "http_server"
120 }
121
122 fn shutdown_strategy(&self) -> ShutdownStrategy {
123 ShutdownStrategy::Graceful(Duration::MAX)
125 }
126
127 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
128 let listener = ConnectionOrientedListener::from_listen_address(self.listen_address.clone())
130 .await
131 .with_error_context(|| format!("Failed to bind listener for HTTP server ({}).", self.listen_address))?;
132
133 let conn_builder = self.conn_builder.clone();
134 let service = self.service.clone();
135 let tls_config = self.tls_config.clone();
136 let maybe_shutdown_timeout = self.graceful_shutdown_timeout;
137
138 let executor = Handle::current();
144
145 Ok(Box::pin(run_accept_loop(
146 listener,
147 conn_builder,
148 service,
149 tls_config,
150 executor,
151 process_shutdown,
152 maybe_shutdown_timeout,
153 )))
154 }
155}
156
157pub struct UnsupervisedHttpServer<S> {
164 listener: ConnectionOrientedListener,
165 tls_config: Option<ServerConfig>,
166 conn_builder: Builder<TokioExecutor>,
167 executor: Handle,
168 service: S,
169}
170
171impl<S> UnsupervisedHttpServer<S> {
172 pub fn from_listener(listener: ConnectionOrientedListener, service: S) -> Self {
178 Self {
179 listener,
180 tls_config: None,
181 conn_builder: build_conn_builder(),
182 executor: Handle::current(),
183 service,
184 }
185 }
186
187 pub fn with_tls_config(mut self, config: ServerConfig) -> Self {
193 self.tls_config = Some(config);
194 self
195 }
196
197 pub fn with_executor(mut self, executor: Handle) -> Self {
204 self.executor = executor;
205 self
206 }
207}
208
209impl<S, B> UnsupervisedHttpServer<S>
210where
211 S: Service<Request<Incoming>, Response = Response<B>> + Send + Clone + 'static,
212 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
213 S::Future: Send + 'static,
214 B: Body + Send + 'static,
215 B::Data: Send,
216 B::Error: std::error::Error + Send + Sync,
217{
218 pub fn listen(self) -> (ShutdownCoordinator, ErrorHandle) {
223 let (shutdown_coordinator, shutdown) = ShutdownHandle::paired();
224 let (error_tx, error_rx) = oneshot::channel();
225
226 let Self {
227 executor,
228 listener,
229 conn_builder,
230 service,
231 tls_config,
232 } = self;
233
234 spawn_traced_named("http-server-acceptor", async move {
235 if let Err(e) = run_accept_loop(listener, conn_builder, service, tls_config, executor, shutdown, None).await
236 {
237 let _ = error_tx.send(e);
238 }
239 });
240
241 (shutdown_coordinator, ErrorHandle(error_rx))
242 }
243}
244
245async fn run_accept_loop<S, B>(
250 mut listener: ConnectionOrientedListener, conn_builder: Builder<TokioExecutor>, service: S,
251 tls_config: Option<ServerConfig>, executor: Handle, shutdown: ShutdownHandle,
252 maybe_shutdown_timeout: Option<Duration>,
253) -> Result<(), GenericError>
254where
255 S: Service<Request<Incoming>, Response = Response<B>> + Send + Clone + 'static,
256 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
257 S::Future: Send + 'static,
258 B: Body + Send + 'static,
259 B::Data: Send,
260 B::Error: std::error::Error + Send + Sync,
261{
262 let maybe_tls_acceptor = match tls_config {
263 Some(mut config) => {
264 config.alpn_protocols.push(b"h2".to_vec());
266 config.alpn_protocols.push(b"http/1.1".to_vec());
267
268 ensure_server_config_fips_compliant(&mut config)?;
269
270 Some(TlsAcceptor::from(Arc::new(config)))
271 }
272 None => None,
273 };
274 let tls_enabled = maybe_tls_acceptor.is_some();
275 let listen_addr = listener.listen_address().clone();
276
277 info!(%listen_addr, tls_enabled, "HTTP server started.");
278
279 let mut conn_shutdown_coordinator = ShutdownCoordinator::default();
282
283 pin!(shutdown);
284
285 let result = loop {
286 select! {
287 result = listener.accept() => match result {
288 Ok(stream) => {
289 let conn_builder = conn_builder.clone();
290 let service = service.clone();
291 let listen_addr = listen_addr.clone();
292 let conn_shutdown = conn_shutdown_coordinator.register();
293
294 match &maybe_tls_acceptor {
295 Some(acceptor) => {
296 let tls_stream = match acceptor.accept(stream).await {
297 Ok(stream) => stream,
298 Err(e) => {
299 error!(%listen_addr, error = %e, "Failed to complete TLS handshake.");
300 continue
301 },
302 };
303
304 executor.spawn_traced_named("http_server_tls_conn", drive_connection(
305 conn_builder, TokioIo::new(tls_stream), service, listen_addr, conn_shutdown, maybe_shutdown_timeout
306 ));
307 },
308 None => {
309 executor.spawn_traced_named("http_server_conn", drive_connection(
310 conn_builder, TokioIo::new(stream), service, listen_addr, conn_shutdown, maybe_shutdown_timeout
311 ));
312 },
313 }
314 },
315 Err(e) => break Err(GenericError::from(e)),
316 },
317
318 _ = &mut shutdown => {
319 debug!(%listen_addr, "Received shutdown signal.");
320 break Ok(());
321 }
322 }
323 };
324
325 debug!(%listen_addr, "Waiting for in-flight HTTP connections to finish...");
327 conn_shutdown_coordinator.shutdown_and_wait().await;
328
329 info!(%listen_addr, "HTTP server stopped.");
330
331 result
332}
333
334async fn drive_connection<I, S, B>(
339 conn_builder: Builder<TokioExecutor>, io: I, service: S, listen_addr: ListenAddress, shutdown: ShutdownHandle,
340 maybe_shutdown_timeout: Option<Duration>,
341) where
342 I: Read + Write + Unpin + Send + 'static,
343 S: Service<Request<Incoming>, Response = Response<B>> + 'static,
344 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
345 S::Future: Send + 'static,
346 B: Body + Send + 'static,
347 B::Data: Send,
348 B::Error: std::error::Error + Send + Sync,
349{
350 let conn = conn_builder.serve_connection(io, service);
351 pin!(conn, shutdown);
352
353 select! {
354 result = conn.as_mut() => if let Err(e) = result {
355 error!(%listen_addr, error = %e, "Failed to serve HTTP connection.");
356 },
357
358 _ = &mut shutdown => {
359 debug!(%listen_addr, "Draining HTTP connection.");
360
361 conn.as_mut().graceful_shutdown();
362
363 let shutdown_timeout = maybe_shutdown_timeout.unwrap_or(Duration::MAX);
364 match timeout(shutdown_timeout, conn.as_mut()).await {
365 Ok(Ok(())) => {},
366 Ok(Err(e)) => warn!(%listen_addr, error = %e, "Failed to drain HTTP connection."),
367 Err(_) => warn!(%listen_addr, "Failed to gracefully drain HTTP connection after {:?}.", shutdown_timeout)
368 }
369 },
370 }
371}
372
373pub struct ErrorHandle(oneshot::Receiver<GenericError>);
375
376impl Future for ErrorHandle {
377 type Output = Option<GenericError>;
378
379 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
380 match ready!(Pin::new(&mut self.0).poll(cx)) {
381 Ok(err) => Poll::Ready(Some(err)),
382 Err(_) => Poll::Ready(None),
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use std::net::{SocketAddr, TcpListener as StdTcpListener};
390 use std::sync::atomic::{AtomicBool, Ordering};
391
392 use http_body_util::Full;
393 use hyper::service::service_fn;
394 use saluki_common::sync::shutdown::ShutdownCoordinator;
395 use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
396 use tokio::net::TcpStream;
397 use tokio::time::timeout;
398
399 use super::*;
400
401 const TEST_TIMEOUT: Duration = Duration::from_secs(5);
403
404 fn free_local_addr() -> SocketAddr {
409 let listener = StdTcpListener::bind("127.0.0.1:0").expect("should bind an ephemeral port");
410 let addr = listener.local_addr().expect("should have a local address");
411 drop(listener);
412 addr
413 }
414
415 fn server_with<F, Fut>(
417 addr: SocketAddr, f: F,
418 ) -> HttpServer<
419 impl Service<
420 Request<Incoming>,
421 Response = Response<Full<bytes::Bytes>>,
422 Error = std::convert::Infallible,
423 Future = Fut,
424 > + Send
425 + Sync
426 + Clone
427 + 'static,
428 >
429 where
430 F: Fn() -> Fut + Send + Sync + Clone + 'static,
431 Fut: Future<Output = Result<Response<Full<bytes::Bytes>>, std::convert::Infallible>> + Send + 'static,
432 {
433 let service = service_fn(move |_req: Request<Incoming>| f());
434 HttpServer::from_listen_address(ListenAddress::Tcp(addr), service)
435 }
436
437 async fn ok_response() -> Result<Response<Full<bytes::Bytes>>, std::convert::Infallible> {
439 Ok(Response::new(Full::new(bytes::Bytes::from_static(b"ok"))))
440 }
441
442 #[tokio::test]
443 async fn binds_during_initialization() {
444 let addr = free_local_addr();
447 let server = server_with(addr, ok_response);
448
449 let run = server
450 .initialize(ShutdownHandle::noop())
451 .await
452 .expect("should initialize");
453
454 assert!(
455 StdTcpListener::bind(addr).is_err(),
456 "initialization should have bound {addr} before the worker future ran"
457 );
458
459 drop(run);
460 }
461
462 #[tokio::test]
463 async fn bind_failure_is_an_initialization_error() {
464 let addr = free_local_addr();
467 let _held = StdTcpListener::bind(addr).expect("should hold the address");
468
469 let server = server_with(addr, ok_response);
470 match server.initialize(ShutdownHandle::noop()).await {
471 Ok(_) => panic!("initialization should have failed to bind {addr}"),
472 Err(e) => {
473 let error = e.to_string();
474 assert!(error.contains("Failed to bind listener"), "unexpected error: {error}");
475 }
476 }
477 }
478
479 #[tokio::test]
480 async fn releases_its_port_once_the_worker_finishes() {
481 let addr = free_local_addr();
484 let server = server_with(addr, ok_response);
485
486 let mut coordinator = ShutdownCoordinator::default();
487 let run = server
488 .initialize(coordinator.register())
489 .await
490 .expect("should initialize");
491
492 coordinator.shutdown();
493 timeout(TEST_TIMEOUT, run)
494 .await
495 .expect("server should stop on shutdown")
496 .expect("server should stop cleanly");
497
498 assert!(
499 StdTcpListener::bind(addr).is_ok(),
500 "the server should have released {addr} when its worker finished"
501 );
502 }
503
504 #[tokio::test]
505 async fn a_half_sent_request_does_not_wedge_the_drain() {
506 let addr = free_local_addr();
511 let server = server_with(addr, ok_response).with_graceful_shutdown_timeout(Duration::from_secs(1));
512
513 let mut coordinator = ShutdownCoordinator::default();
514 let run = server
515 .initialize(coordinator.register())
516 .await
517 .expect("should initialize");
518 let run = tokio::spawn(run);
519
520 let mut stream = TcpStream::connect(addr).await.expect("should connect");
521 stream
522 .write_all(b"GET / HTTP/1.1\r\nHost: localhost")
523 .await
524 .expect("should write a partial request head");
525 stream.flush().await.expect("should flush");
526
527 tokio::time::sleep(Duration::from_millis(100)).await;
529 coordinator.shutdown();
530
531 timeout(TEST_TIMEOUT, run)
532 .await
533 .expect("server should finish draining rather than waiting on a half-sent request")
534 .expect("server task should not panic")
535 .expect("server should stop cleanly");
536 }
537
538 #[tokio::test]
539 async fn does_not_finish_until_in_flight_requests_do() {
540 let addr = free_local_addr();
543
544 let handler_started = Arc::new(AtomicBool::new(false));
545 let started = Arc::clone(&handler_started);
546 let server = server_with(addr, move || {
547 let started = Arc::clone(&started);
548 async move {
549 started.store(true, Ordering::SeqCst);
550 tokio::time::sleep(Duration::from_millis(300)).await;
551 ok_response().await
552 }
553 });
554
555 let mut coordinator = ShutdownCoordinator::default();
556 let run = server
557 .initialize(coordinator.register())
558 .await
559 .expect("should initialize");
560 let mut run = tokio::spawn(run);
561
562 let mut stream = TcpStream::connect(addr).await.expect("should connect");
564 stream
565 .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
566 .await
567 .expect("should write request");
568
569 while !handler_started.load(Ordering::SeqCst) {
570 tokio::time::sleep(Duration::from_millis(5)).await;
571 }
572
573 coordinator.shutdown();
575
576 assert!(
579 timeout(Duration::from_millis(50), &mut run).await.is_err(),
580 "server should not finish while a request is still being served"
581 );
582
583 let mut response = Vec::new();
584 timeout(TEST_TIMEOUT, stream.read_to_end(&mut response))
585 .await
586 .expect("response should arrive")
587 .expect("should read response");
588 assert!(
589 response.ends_with(b"ok"),
590 "expected the in-flight response to complete, got {:?}",
591 String::from_utf8_lossy(&response)
592 );
593
594 timeout(TEST_TIMEOUT, &mut run)
595 .await
596 .expect("server should finish after draining")
597 .expect("server task should not panic")
598 .expect("server should stop cleanly");
599 }
600}