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