Skip to main content

rama/cli/service/
fs.rs

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