rama/cli/service/
serve.rs

1//! Serve '[`Service`] that serves a file or directory using [`ServeFile`] or [`ServeDir`], or a placeholder page.
2
3use crate::{
4    Layer, Service,
5    cli::ForwardKind,
6    combinators::{Either3, Either7},
7    error::{BoxError, OpaqueError},
8    http::{
9        Request, Response, Version,
10        headers::forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
11        layer::{
12            forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
13            trace::TraceLayer, ua::UserAgentClassifierLayer,
14        },
15        server::HttpServer,
16        service::{
17            fs::{DirectoryServeMode, ServeDir, ServeFile},
18            web::StaticService,
19            web::response::{Html, IntoResponse},
20        },
21    },
22    layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
23    net::stream::layer::http::BodyLimitLayer,
24    proxy::haproxy::server::HaProxyLayer,
25    rt::Executor,
26    telemetry::tracing,
27};
28
29use std::{convert::Infallible, path::PathBuf, time::Duration};
30use tokio::net::TcpStream;
31
32#[cfg(feature = "boring")]
33use crate::{
34    net::tls::server::ServerConfig,
35    tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
36};
37
38#[cfg(all(feature = "rustls", not(feature = "boring")))]
39use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
40
41#[cfg(feature = "boring")]
42type TlsConfig = ServerConfig;
43
44#[cfg(all(feature = "rustls", not(feature = "boring")))]
45type TlsConfig = TlsAcceptorData;
46
47#[derive(Debug, Clone)]
48/// Builder that can be used to run your own serve [`Service`],
49/// serving a file or directory, or a placeholder page.
50pub struct ServeServiceBuilder<H> {
51    concurrent_limit: usize,
52    body_limit: usize,
53    timeout: Duration,
54    forward: Option<ForwardKind>,
55
56    #[cfg(any(feature = "rustls", feature = "boring"))]
57    tls_server_config: Option<TlsConfig>,
58
59    http_version: Option<Version>,
60
61    http_service_builder: H,
62
63    content_path: Option<PathBuf>,
64    dir_serve_mode: DirectoryServeMode,
65}
66
67impl Default for ServeServiceBuilder<()> {
68    fn default() -> Self {
69        Self {
70            concurrent_limit: 0,
71            body_limit: 1024 * 1024,
72            timeout: Duration::ZERO,
73            forward: None,
74
75            #[cfg(any(feature = "rustls", feature = "boring"))]
76            tls_server_config: None,
77
78            http_version: None,
79
80            http_service_builder: (),
81
82            content_path: None,
83            dir_serve_mode: DirectoryServeMode::HtmlFileList,
84        }
85    }
86}
87
88impl ServeServiceBuilder<()> {
89    /// Create a new [`ServeServiceBuilder`].
90    #[must_use]
91    pub fn new() -> Self {
92        Self::default()
93    }
94}
95
96impl<H> ServeServiceBuilder<H> {
97    /// set the number of concurrent connections to allow
98    ///
99    /// (0 = no limit)
100    #[must_use]
101    pub fn concurrent(mut self, limit: usize) -> Self {
102        self.concurrent_limit = limit;
103        self
104    }
105
106    /// set the number of concurrent connections to allow
107    ///
108    /// (0 = no limit)
109    pub fn set_concurrent(&mut self, limit: usize) -> &mut Self {
110        self.concurrent_limit = limit;
111        self
112    }
113
114    /// set the body limit in bytes for each request
115    #[must_use]
116    pub fn body_limit(mut self, limit: usize) -> Self {
117        self.body_limit = limit;
118        self
119    }
120
121    /// set the body limit in bytes for each request
122    pub fn set_body_limit(&mut self, limit: usize) -> &mut Self {
123        self.body_limit = limit;
124        self
125    }
126
127    /// set the timeout in seconds for each connection
128    ///
129    /// (0 = no timeout)
130    #[must_use]
131    pub fn timeout(mut self, timeout: Duration) -> Self {
132        self.timeout = timeout;
133        self
134    }
135
136    /// set the timeout in seconds for each connection
137    ///
138    /// (0 = no timeout)
139    pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
140        self.timeout = timeout;
141        self
142    }
143
144    /// enable support for one of the following "forward" headers or protocols
145    ///
146    /// Supported headers:
147    ///
148    /// Forwarded ("for="), X-Forwarded-For
149    ///
150    /// X-Client-IP Client-IP, X-Real-IP
151    ///
152    /// CF-Connecting-IP, True-Client-IP
153    ///
154    /// Or using HaProxy protocol.
155    #[must_use]
156    pub fn forward(self, kind: ForwardKind) -> Self {
157        self.maybe_forward(Some(kind))
158    }
159
160    /// enable support for one of the following "forward" headers or protocols
161    ///
162    /// Same as [`Self::forward`] but without consuming `self`.
163    pub fn set_forward(&mut self, kind: ForwardKind) -> &mut Self {
164        self.forward = Some(kind);
165        self
166    }
167
168    /// maybe enable support for one of the following "forward" headers or protocols.
169    ///
170    /// See [`Self::forward`] for more information.
171    #[must_use]
172    pub fn maybe_forward(mut self, maybe_kind: Option<ForwardKind>) -> Self {
173        self.forward = maybe_kind;
174        self
175    }
176
177    #[cfg(any(feature = "rustls", feature = "boring"))]
178    /// define a tls server cert config to be used for tls terminaton
179    /// by the serve service.
180    #[must_use]
181    pub fn tls_server_config(mut self, cfg: TlsConfig) -> Self {
182        self.tls_server_config = Some(cfg);
183        self
184    }
185
186    #[cfg(any(feature = "rustls", feature = "boring"))]
187    /// define a tls server cert config to be used for tls terminaton
188    /// by the serve service.
189    pub fn set_tls_server_config(&mut self, cfg: TlsConfig) -> &mut Self {
190        self.tls_server_config = Some(cfg);
191        self
192    }
193
194    #[cfg(any(feature = "rustls", feature = "boring"))]
195    /// define a tls server cert config to be used for tls terminaton
196    /// by the serve service.
197    #[must_use]
198    pub fn maybe_tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
199        self.tls_server_config = cfg;
200        self
201    }
202
203    /// set the http version to use for the http server (auto by default)
204    #[must_use]
205    pub fn http_version(mut self, version: Version) -> Self {
206        self.http_version = Some(version);
207        self
208    }
209
210    /// maybe set the http version to use for the http server (auto by default)
211    #[must_use]
212    pub fn maybe_http_version(mut self, version: Option<Version>) -> Self {
213        self.http_version = version;
214        self
215    }
216
217    /// set the http version to use for the http server (auto by default)
218    pub fn set_http_version(&mut self, version: Version) -> &mut Self {
219        self.http_version = Some(version);
220        self
221    }
222
223    /// add a custom http layer which will be applied to the existing http layers
224    #[must_use]
225    pub fn http_layer<H2>(self, layer: H2) -> ServeServiceBuilder<(H, H2)> {
226        ServeServiceBuilder {
227            concurrent_limit: self.concurrent_limit,
228            body_limit: self.body_limit,
229            timeout: self.timeout,
230            forward: self.forward,
231
232            #[cfg(any(feature = "rustls", feature = "boring"))]
233            tls_server_config: self.tls_server_config,
234
235            http_version: self.http_version,
236
237            http_service_builder: (self.http_service_builder, layer),
238
239            content_path: self.content_path,
240            dir_serve_mode: self.dir_serve_mode,
241        }
242    }
243
244    /// Set the content path to serve (by default it will serve the rama homepage).
245    #[must_use]
246    pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
247        self.content_path = Some(path.into());
248        self
249    }
250
251    /// Maybe set the content path to serve (by default it will serve the rama homepage).
252    #[must_use]
253    pub fn maybe_content_path(mut self, path: Option<PathBuf>) -> Self {
254        self.content_path = path;
255        self
256    }
257
258    /// Set the content path to serve (by default it will serve the rama homepage).
259    pub fn set_content_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
260        self.content_path = Some(path.into());
261        self
262    }
263
264    /// Set the [`DirectoryServeMode`] which defines how to serve directories.
265    ///
266    /// By default it will use [`DirectoryServeMode::HtmlFileList`].
267    ///
268    /// Note that this is only used in case the content path is defined
269    /// (e.g. using [`Self::content_path`])
270    /// and that path points to a valid directory.
271    #[must_use]
272    pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
273        self.dir_serve_mode = mode;
274        self
275    }
276
277    /// Set the [`DirectoryServeMode`] which defines how to serve directories.
278    ///
279    /// By default it will use [`DirectoryServeMode::HtmlFileList`].
280    ///
281    /// Note that this is only used in case the content path is defined (e.g. using [`Self::content_path`])
282    /// and that path points to a valid directory.
283    pub fn set_directory_serve_mode(&mut self, mode: DirectoryServeMode) -> &mut Self {
284        self.dir_serve_mode = mode;
285        self
286    }
287}
288
289impl<H> ServeServiceBuilder<H>
290where
291    H: Layer<ServeService, Service: Service<Request, Response = Response, Error = BoxError>>,
292{
293    /// build a tcp service ready to serve files
294    pub fn build(
295        self,
296        executor: Executor,
297    ) -> Result<impl Service<TcpStream, Response = (), Error = Infallible>, BoxError> {
298        let tcp_forwarded_layer = match &self.forward {
299            Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
300            _ => None,
301        };
302
303        let http_service = self.build_http()?;
304
305        #[cfg(all(feature = "rustls", not(feature = "boring")))]
306        let tls_cfg = self.tls_server_config;
307
308        #[cfg(feature = "boring")]
309        let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config {
310            Some(cfg) => Some(cfg.try_into()?),
311            None => None,
312        };
313
314        let tcp_service_builder = (
315            ConsumeErrLayer::trace(tracing::Level::DEBUG),
316            (self.concurrent_limit > 0)
317                .then(|| LimitLayer::new(ConcurrentPolicy::max(self.concurrent_limit))),
318            (!self.timeout.is_zero()).then(|| TimeoutLayer::new(self.timeout)),
319            tcp_forwarded_layer,
320            BodyLimitLayer::request_only(self.body_limit),
321            #[cfg(any(feature = "rustls", feature = "boring"))]
322            tls_cfg.map(|cfg| {
323                #[cfg(feature = "boring")]
324                return TlsAcceptorLayer::new(cfg).with_store_client_hello(true);
325                #[cfg(all(feature = "rustls", not(feature = "boring")))]
326                TlsAcceptorLayer::new(cfg).with_store_client_hello(true)
327            }),
328        );
329
330        let http_transport_service = match self.http_version {
331            Some(Version::HTTP_2) => Either3::A(HttpServer::h2(executor).service(http_service)),
332            Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
333                Either3::B(HttpServer::http1().service(http_service))
334            }
335            Some(_) => {
336                return Err(OpaqueError::from_display("unsupported http version").into_boxed());
337            }
338            None => Either3::C(HttpServer::auto(executor).service(http_service)),
339        };
340
341        Ok(tcp_service_builder.into_layer(http_transport_service))
342    }
343
344    /// build an http service ready to serve files
345    pub fn build_http(
346        &self,
347    ) -> Result<impl Service<Request, Response: IntoResponse, Error = Infallible> + use<H>, BoxError>
348    {
349        let http_forwarded_layer = match &self.forward {
350            None | Some(ForwardKind::HaProxy) => None,
351            Some(ForwardKind::Forwarded) => Some(Either7::A(GetForwardedHeaderLayer::forwarded())),
352            Some(ForwardKind::XForwardedFor) => {
353                Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for()))
354            }
355            Some(ForwardKind::XClientIp) => {
356                Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new()))
357            }
358            Some(ForwardKind::ClientIp) => {
359                Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new()))
360            }
361            Some(ForwardKind::XRealIp) => {
362                Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new()))
363            }
364            Some(ForwardKind::CFConnectingIp) => {
365                Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new()))
366            }
367            Some(ForwardKind::TrueClientIp) => {
368                Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new()))
369            }
370        };
371
372        let serve_service = match &self.content_path {
373            None => Either3::A(StaticService::new(Html(include_str!(
374                "../../../docs/index.html"
375            )))),
376            Some(path) if path.is_file() => Either3::B(ServeFile::new(path.clone())),
377            Some(path) if path.is_dir() => {
378                Either3::C(ServeDir::new(path).with_directory_serve_mode(self.dir_serve_mode))
379            }
380            Some(path) => {
381                return Err(OpaqueError::from_display(format!(
382                    "invalid path {path:?}: no such file or directory"
383                ))
384                .into_boxed());
385            }
386        };
387
388        let http_service = (
389            TraceLayer::new_for_http(),
390            AddRequiredResponseHeadersLayer::default(),
391            UserAgentClassifierLayer::new(),
392            ConsumeErrLayer::default(),
393            http_forwarded_layer,
394        )
395            .into_layer(self.http_service_builder.layer(serve_service));
396
397        Ok(http_service)
398    }
399}
400
401type ServeStaticHtml = StaticService<Html<&'static str>>;
402type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;