saluki_app/api.rs
1//! API server.
2
3use std::{convert::Infallible, error::Error, future::Future};
4
5use axum::Router;
6use http::{Request, Response};
7use rcgen::{generate_simple_self_signed, CertifiedKey};
8use rustls::{pki_types::PrivateKeyDer, ServerConfig};
9use rustls_pki_types::PrivatePkcs8KeyDer;
10use saluki_api::APIHandler;
11use saluki_error::GenericError;
12use saluki_io::net::{
13 listener::ConnectionOrientedListener,
14 server::{http::HttpServer, multiplex_service::MultiplexService},
15 util::hyper::TowerToHyperService,
16 ListenAddress,
17};
18use saluki_tls::ensure_server_config_fips_compliant;
19use tokio::select;
20use tonic::{body::Body, server::NamedService, service::RoutesBuilder};
21use tower::Service;
22
23/// An API builder.
24///
25/// `APIBuilder` provides a simple and ergonomic builder pattern for constructing an API server from multiple handlers.
26/// This allows composing portions of an API from individual building blocks.
27///
28/// ## Missing
29///
30/// - TLS support
31/// - API-wide authentication support (can be added at the per-handler level)
32/// - graceful shutdown (shutdown stops new connections, but doesn't wait for existing connections to close)
33#[derive(Default)]
34pub struct APIBuilder {
35 http_router: Router,
36 grpc_router: RoutesBuilder,
37 tls_config: Option<ServerConfig>,
38}
39
40impl APIBuilder {
41 /// Create a new `APIBuilder` with an empty router.
42 ///
43 /// A fallback route will be provided that returns a 404 Not Found response for any route that isn't explicitly handled.
44 pub fn new() -> Self {
45 Self {
46 http_router: Router::new(),
47 grpc_router: RoutesBuilder::default(),
48 tls_config: None,
49 }
50 }
51
52 /// Adds the given handler to this builder.
53 ///
54 /// The initial state and routes provided by the handler will be merged into this builder.
55 pub fn with_handler<H>(mut self, handler: H) -> Self
56 where
57 H: APIHandler,
58 {
59 let handler_router = handler.generate_routes();
60 let handler_state = handler.generate_initial_state();
61 self.http_router = self.http_router.merge(handler_router.with_state(handler_state));
62
63 self
64 }
65
66 /// Adds the given optional handler to this builder.
67 ///
68 /// If the handler is `Some`, the initial state and routes provided by the handler will be merged into this builder.
69 /// Otherwise, this builder will be returned unchanged.
70 pub fn with_optional_handler<H>(self, handler: Option<H>) -> Self
71 where
72 H: APIHandler,
73 {
74 if let Some(handler) = handler {
75 self.with_handler(handler)
76 } else {
77 self
78 }
79 }
80
81 /// Add the given gRPC service to this builder.
82 pub fn with_grpc_service<S>(mut self, svc: S) -> Self
83 where
84 S: Service<Request<Body>, Response = Response<Body>, Error = Infallible>
85 + NamedService
86 + Clone
87 + Send
88 + Sync
89 + 'static,
90 S::Future: Send + 'static,
91 S::Error: Into<Box<dyn Error + Send + Sync>> + Send,
92 {
93 self.grpc_router.add_service(svc);
94 self
95 }
96
97 /// Sets the TLS configuration for the server.
98 ///
99 /// This will enable TLS for the server, and the server will only accept connections that are encrypted with TLS.
100 ///
101 /// Defaults to TLS being disabled.
102 pub fn with_tls_config(mut self, config: ServerConfig) -> Self {
103 self.tls_config = Some(config);
104 self
105 }
106
107 /// Sets the TLS configuration for the server based on a dynamically generated self-signed certificate.
108 ///
109 /// This will enable TLS for the server, and the server will only accept connections that are encrypted with TLS.
110 pub fn with_self_signed_tls(self) -> Self {
111 self.try_with_self_signed_tls()
112 .expect("self-signed server TLS configuration should build and pass FIPS validation")
113 }
114
115 /// Sets the TLS configuration for the server based on a dynamically generated self-signed certificate.
116 ///
117 /// This will enable TLS for the server, and the server will only accept connections that are encrypted with TLS.
118 ///
119 /// # Errors
120 ///
121 /// If the certificate cannot be generated, the TLS configuration cannot be built, or the resulting TLS
122 /// configuration is not FIPS compliant, an error is returned.
123 pub fn try_with_self_signed_tls(self) -> Result<Self, GenericError> {
124 let CertifiedKey { cert, signing_key } = generate_simple_self_signed(["localhost".to_owned()])?;
125 let cert_chain = vec![cert.der().clone()];
126 let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(signing_key.serialize_der()));
127
128 let mut config = ServerConfig::builder()
129 .with_no_client_auth()
130 .with_single_cert(cert_chain, key)?;
131
132 ensure_server_config_fips_compliant(&mut config)?;
133
134 Ok(self.with_tls_config(config))
135 }
136
137 /// Serves the API on the given listen address until `shutdown` resolves.
138 ///
139 /// The listen address must be a connection-oriented address (TCP or Unix domain socket in SOCK_STREAM mode).
140 ///
141 /// # Errors
142 ///
143 /// If the given listen address isn't connection-oriented, or if the server fails to bind to the address, or if
144 /// there is an error while accepting for new connections, an error will be returned.
145 pub async fn serve<F>(self, listen_address: ListenAddress, shutdown: F) -> Result<(), GenericError>
146 where
147 F: Future<Output = ()> + Send + 'static,
148 {
149 let listener = ConnectionOrientedListener::from_listen_address(listen_address).await?;
150
151 // Wrap up our HTTP and gRPC routers in a multiplexed service, allowing us to handle both types of requests on
152 // the same port. Additionally, we have to wrap the service to translate from `tower::Service` to `hyper::Service`.
153 let multiplexed_service = TowerToHyperService::new(MultiplexService::new(
154 self.http_router,
155 self.grpc_router.routes().into_axum_router(),
156 ));
157
158 // Create and spawn the HTTP server.
159 let mut http_server = HttpServer::from_listener(listener, multiplexed_service);
160 if let Some(tls_config) = self.tls_config {
161 http_server = http_server.with_tls_config(tls_config);
162 }
163 let (shutdown_handle, error_handle) = http_server.listen();
164
165 // Wait for our shutdown signal, which we'll forward to the listener to stop accepting new connections... or
166 // capture any errors thrown by the listener itself.
167 select! {
168 _ = shutdown => shutdown_handle.shutdown(),
169 maybe_err = error_handle => if let Some(e) = maybe_err {
170 return Err(GenericError::from(e))
171 },
172 }
173
174 Ok(())
175 }
176}