static_files_module/lib.rs
1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Static Files Module for Pingora
16//!
17//! This crate allows extending [Pingora Proxy](https://github.com/cloudflare/pingora) with the
18//! capability to serve static files from a directory.
19//!
20//! ## Supported functionality
21//!
22//! * `GET` and `HEAD` requests
23//! * Configurable directory index files (`index.html` by default)
24//! * Page configurable to display on 404 Not Found errors instead of the standard error page
25//! * Conditional requests via `If-Modified-Since`, `If-Unmodified-Since`, `If-Match`, `If-None`
26//! match HTTP headers
27//! * Byte range requests via `Range` and `If-Range` HTTP headers
28//! * Compression support: serving pre-compressed versions of the files (gzip, zlib deflate,
29//! compress, Brotli, Zstandard algorithms supported)
30//! * Compression support: dynamic compression via Pingora (currently gzip, Brotli and Zstandard
31//! algorithms supported)
32//!
33//! ## Known limitations
34//!
35//! * Requests with multiple byte ranges are not supported and will result in the full file being
36//! returned. The complexity required for implementing this feature isn’t worth this rare use case.
37//! * Zero-copy data transfer (a.k.a. sendfile) cannot currently be supported within the Pingora
38//! framework.
39//!
40//! ## Code example
41//!
42//! You will typically create a [`StaticFilesHandler`] instance and call it during the
43//! `request_filter` stage. If called unconditionally it will handle all requests so that
44//! subsequent stages won’t be reached at all.
45//!
46//! ```rust
47//! use async_trait::async_trait;
48//! use pingora_core::Result;
49//! use pingora_core::upstreams::peer::HttpPeer;
50//! use pingora_proxy::{ProxyHttp, Session};
51//! use module_utils::RequestFilter;
52//! use static_files_module::StaticFilesHandler;
53//!
54//! pub struct MyServer {
55//! static_files_handler: StaticFilesHandler,
56//! }
57//!
58//! #[async_trait]
59//! impl ProxyHttp for MyServer {
60//! type CTX = <StaticFilesHandler as RequestFilter>::CTX;
61//! fn new_ctx(&self) -> Self::CTX {
62//! StaticFilesHandler::new_ctx()
63//! }
64//!
65//! async fn request_filter(
66//! &self,
67//! session: &mut Session,
68//! ctx: &mut Self::CTX
69//! ) -> Result<bool> {
70//! self.static_files_handler.handle(session, ctx).await
71//! }
72//!
73//! async fn upstream_peer(
74//! &self,
75//! _session: &mut Session,
76//! _ctx: &mut Self::CTX,
77//! ) -> Result<Box<HttpPeer>> {
78//! panic!("Unexpected, upstream_peer stage reached");
79//! }
80//! }
81//! ```
82//!
83//! You can create a `StaticFilesHandler` instance by specifying its configuration directly:
84//!
85//! ```rust,no_run
86//! use static_files_module::{StaticFilesConf, StaticFilesHandler};
87//!
88//! let conf = StaticFilesConf {
89//! root: Some("/var/www/html".into()),
90//! ..Default::default()
91//! };
92//! let static_files_handler: StaticFilesHandler = conf.try_into().unwrap();
93//! ```
94//! It is also possible to create a configuration from command line options and a configuration
95//! file, extending the default Pingora data structures. The macros
96//! [`module_utils::merge_opt`] and [`module_utils::merge_conf`] help merging command
97//! line options and configuration structures respectively, and [`module_utils::FromYaml`]
98//! trait helps reading the configuration file.
99//!
100//! ```rust,no_run
101//! use log::error;
102//! use pingora_core::server::configuration::{Opt as ServerOpt, ServerConf};
103//! use pingora_core::server::Server;
104//! use module_utils::{FromYaml, merge_opt, merge_conf};
105//! use serde::Deserialize;
106//! use static_files_module::{StaticFilesConf, StaticFilesHandler, StaticFilesOpt};
107//! use std::fs::File;
108//! use std::io::BufReader;
109//! use structopt::StructOpt;
110//!
111//! // The command line flags from both structures are merged, so that the user doesn't need to
112//! // care which structure they belong to.
113//! #[merge_opt]
114//! struct MyServerOpt {
115//! server: ServerOpt,
116//! static_files: StaticFilesOpt,
117//! }
118//!
119//! // The configuration settings from both structures are merged, so that the user doesn't need to
120//! // care which structure they belong to.
121//! #[merge_conf]
122//! struct MyServerConf {
123//! server: ServerConf,
124//! static_files: StaticFilesConf,
125//! }
126//!
127//! let opt = MyServerOpt::from_args();
128//! let conf = opt
129//! .server
130//! .conf
131//! .as_ref()
132//! .and_then(|path| MyServerConf::load_from_yaml(path).ok())
133//! .unwrap_or_else(MyServerConf::default);
134//!
135//! let mut server = Server::new_with_opt_and_conf(opt.server, conf.server);
136//! server.bootstrap();
137//!
138//! let mut static_files_conf = conf.static_files;
139//! static_files_conf.merge_with_opt(opt.static_files);
140//! let static_files_handler: StaticFilesHandler = static_files_conf.try_into().unwrap();
141//! ```
142//!
143//! For complete and more comprehensive code see `single-static-root` example in the repository.
144//!
145//! ## Compression support
146//!
147//! You can activate support for selected compression algorithms via the `precompressed` configuration setting:
148//!
149//! ```rust
150//! use static_files_module::{CompressionAlgorithm, StaticFilesConf};
151//!
152//! let conf = StaticFilesConf {
153//! root: Some("/var/www/html".into()),
154//! precompressed: vec![CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli],
155//! ..Default::default()
156//! };
157//! ```
158//!
159//! This will make `StaticFilesHandler` look for gzip (`.gz`) and Brotli (`.br`) versions of the requested files and serve these pre-compressed files if supported by the client. For example, a client requesting `file.txt` and sending HTTP header `Accept-Encoding: br, gzip` will receive `file.txt.br` file or, if not found, `file.txt.gz` file. The order in which `StaticFilesHandler` will look for pre-compressed files is determined by the client’s compression algorithm preferences.
160//!
161//! It is also possible to compress files dynamically on the fly via Pingora’s downstream compression. For that, activate compression for the session before calling `StaticFilesHandler`:
162//!
163//! ```rust
164//! # use async_trait::async_trait;
165//! # use pingora_core::Result;
166//! # use pingora_core::upstreams::peer::HttpPeer;
167//! # use pingora_proxy::{ProxyHttp, Session};
168//! # use module_utils::RequestFilter;
169//! # use serde::Deserialize;
170//! # use static_files_module::StaticFilesHandler;
171//! #
172//! # pub struct MyServer {
173//! # static_files_handler: StaticFilesHandler,
174//! # }
175//! #
176//! # #[async_trait]
177//! # impl ProxyHttp for MyServer {
178//! # type CTX = <StaticFilesHandler as RequestFilter>::CTX;
179//! # fn new_ctx(&self) -> Self::CTX {
180//! # StaticFilesHandler::new_ctx()
181//! # }
182//! #
183//! async fn request_filter(
184//! &self,
185//! session: &mut Session,
186//! ctx: &mut Self::CTX
187//! ) -> Result<bool> {
188//! session.downstream_compression.adjust_level(3);
189//! self.static_files_handler.handle(session, ctx).await
190//! }
191//! #
192//! # async fn upstream_peer(
193//! # &self,
194//! # session: &mut Session,
195//! # _ctx: &mut Self::CTX,
196//! # ) -> Result<Box<HttpPeer>> {
197//! # panic!("Unexpected, upstream_peer stage reached");
198//! # }
199//! # }
200//! ```
201
202mod compression;
203mod compression_algorithm;
204mod configuration;
205mod file_writer;
206mod handler;
207pub mod metadata;
208pub mod path;
209pub mod range;
210mod standard_response;
211#[cfg(test)]
212mod tests;
213
214pub use compression_algorithm::{CompressionAlgorithm, UnsupportedCompressionAlgorithm};
215pub use configuration::{StaticFilesConf, StaticFilesOpt};
216pub use handler::StaticFilesHandler;