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