Skip to main content

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}