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