drogue_bazaar/actix/http/
builder.rs

1use super::{bind::bind_http, config::HttpConfig};
2use crate::actix::http::{BuildCors, CorsConfig};
3use crate::app::{Startup, StartupExt};
4use crate::{
5    app::RuntimeConfig,
6    core::tls::{TlsAuthConfig, WithTlsAuthConfig},
7};
8use actix_cors::Cors;
9use actix_http::Extensions;
10use actix_web::{
11    middleware,
12    web::{self, ServiceConfig},
13    App, HttpServer,
14};
15use actix_web_extras::middleware::Condition;
16use futures_core::future::BoxFuture;
17use futures_util::{FutureExt, TryFutureExt};
18use std::any::Any;
19
20pub type OnConnectFn = dyn Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static;
21
22/// Build an HTTP server.
23pub struct HttpBuilder<F>
24where
25    F: Fn(&mut ServiceConfig) + Send + Clone + 'static,
26{
27    config: HttpConfig,
28    default_cors: Option<CorsConfig>,
29    app_builder: Box<F>,
30    on_connect: Option<Box<OnConnectFn>>,
31    tls_auth_config: TlsAuthConfig,
32    tracing: bool,
33}
34
35impl<F> HttpBuilder<F>
36where
37    F: Fn(&mut ServiceConfig) + Send + Clone + 'static,
38{
39    /// Start building a new HTTP server instance.
40    pub fn new(config: HttpConfig, runtime: Option<&RuntimeConfig>, app_builder: F) -> Self {
41        Self {
42            config,
43            default_cors: None,
44            app_builder: Box::new(app_builder),
45            on_connect: None,
46            tls_auth_config: TlsAuthConfig::default(),
47            tracing: runtime.map(|r| r.tracing.is_enabled()).unwrap_or_default(),
48        }
49    }
50
51    /// Set a default CORS config without overriding the existing one.
52    pub fn default_cors<C: Into<Option<CorsConfig>>>(mut self, default_cors: C) -> Self {
53        self.default_cors = default_cors.into();
54        self
55    }
56
57    /// Set an "on connect" handler.
58    pub fn on_connect<O>(mut self, on_connect: O) -> Self
59    where
60        O: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
61    {
62        self.on_connect = Some(Box::new(on_connect));
63        self
64    }
65
66    /// Set the TLS mode.
67    pub fn tls_auth_config<I: Into<TlsAuthConfig>>(mut self, tls_auth_config: I) -> Self {
68        self.tls_auth_config = tls_auth_config.into();
69        self
70    }
71
72    /// Start the server on the provided startup context.
73    pub fn start(self, startup: &mut dyn Startup) -> anyhow::Result<()> {
74        startup.spawn(self.run()?);
75        Ok(())
76    }
77
78    /// Get the effective CORS config.
79    ///
80    /// This will either the configuration provided through the [`HttpConfig`], or the one
81    /// registered by the application using [`Self::default_cors()`]
82    fn cors_config(&self) -> Option<CorsConfig> {
83        self.config
84            .cors
85            .as_ref()
86            .or(self.default_cors.as_ref())
87            .cloned()
88    }
89
90    /// Run the server.
91    ///
92    /// **NOTE:** This only returns a future, which was to be scheduled on some executor. Possibly
93    /// using [`crate::app::Startup`].
94    ///
95    /// In most cases you want to use [`Self::start`] instead.
96    pub fn run(
97        #[allow(unused_mut)] mut self,
98    ) -> Result<BoxFuture<'static, Result<(), anyhow::Error>>, anyhow::Error> {
99        let max_payload_size = self.config.max_payload_size;
100        let max_json_payload_size = self.config.max_json_payload_size;
101
102        let prometheus = actix_web_prom::PrometheusMetricsBuilder::new(
103            self.config.metrics_namespace.as_deref().unwrap_or("drogue"),
104        )
105        .registry(prometheus::default_registry().clone())
106        .build()
107        // FIXME: replace with direct conversion once nlopes/actix-web-prom#67 is merged
108        .map_err(|err| anyhow::anyhow!("Failed to build prometheus middleware: {err}"))?;
109
110        let cors = self.cors_config();
111        log::debug!("Effective CORS config {cors:?}");
112
113        // we just try to parse it once, so we can be sure it doesn't panic later
114        let _: Option<Cors> = cors.build_cors()?;
115
116        let mut main = HttpServer::new(move || {
117            let app = App::new();
118
119            // add wrapper (the last added is executed first)
120
121            // enable CORS support
122            // this should not panic, as we did parse the configuration once, before the http builder
123            let cors: Option<Cors> = cors.build_cors().expect("Configuration must be valid");
124            let app = app.wrap(Condition::from_option(cors));
125
126            // record request metrics
127            let app = app.wrap(prometheus.clone());
128
129            // request logging
130            let (logger, tracing_logger) = match self.tracing {
131                false => (Some(middleware::Logger::default()), None),
132                true => (None, Some(tracing_actix_web::TracingLogger::default())),
133            };
134            log::debug!(
135                "Loggers ({}) - logger: {}, tracing: {}",
136                self.tracing,
137                logger.is_some(),
138                tracing_logger.is_some()
139            );
140            let app = app
141                .wrap(Condition::from_option(logger))
142                // logging: ... other tracing
143                .wrap(Condition::from_option(tracing_logger));
144
145            // configure payload and JSON payload limits
146            let app = app
147                .app_data(web::PayloadConfig::new(max_payload_size))
148                .app_data(web::JsonConfig::default().limit(max_json_payload_size));
149
150            // configure main http application
151            app.configure(|cfg| (self.app_builder)(cfg))
152        });
153
154        if let Some(on_connect) = self.on_connect {
155            main = main.on_connect(on_connect);
156        }
157
158        if self.config.disable_tls_psk {
159            #[cfg(feature = "openssl")]
160            self.tls_auth_config.psk.take();
161        }
162
163        let mut main = bind_http(
164            main,
165            self.config.bind_addr,
166            self.config
167                .disable_tls
168                .with_tls_auth_config(self.tls_auth_config),
169            self.config.key_file,
170            self.config.cert_bundle_file,
171        )?;
172
173        if let Some(workers) = self.config.workers {
174            main = main.workers(workers)
175        }
176
177        Ok(main.run().err_into().boxed())
178    }
179}