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