Skip to content

Commit f50b604

Browse files
Improve FileServer rewrite API.
Finalizes the FileServer rewrite API implementation. Primarily reworks how the built-in rewriters are written (now as structs instead of free functions) and reorganizes the `fs` module. Co-authored-by: Matthew Pomes <[email protected]>
1 parent 65e3b87 commit f50b604

File tree

17 files changed

+673
-600
lines changed

17 files changed

+673
-600
lines changed

core/http/src/uri/segments.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,9 @@ impl<'a> Segments<'a, Path> {
178178
}
179179

180180
/// Creates a `PathBuf` from `self`. The returned `PathBuf` is
181-
/// percent-decoded. If a segment is equal to `..`, the previous segment (if
182-
/// any) is skipped.
181+
/// percent-decoded and guaranteed to be relative. If a segment is equal to
182+
/// `.`, it is skipped. If a segment is equal to `..`, the previous segment
183+
/// (if any) is skipped.
183184
///
184185
/// For security purposes, if a segment meets any of the following
185186
/// conditions, an `Err` is returned indicating the condition met:
@@ -193,7 +194,7 @@ impl<'a> Segments<'a, Path> {
193194
/// Additionally, if `allow_dotfiles` is `false`, an `Err` is returned if
194195
/// the following condition is met:
195196
///
196-
/// * Decoded segment starts with any of: `.` (except `..`)
197+
/// * Decoded segment starts with any of: `.` (except `..` and `.`)
197198
///
198199
/// As a result of these conditions, a `PathBuf` derived via `FromSegments`
199200
/// is safe to interpolate within, or use as a suffix of, a path without
@@ -216,10 +217,10 @@ impl<'a> Segments<'a, Path> {
216217
pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result<PathBuf, PathError> {
217218
let mut buf = PathBuf::new();
218219
for segment in self.clone() {
219-
if segment == ".." {
220-
buf.pop();
221-
} else if segment == "." {
220+
if segment == "." {
222221
continue;
222+
} else if segment == ".." {
223+
buf.pop();
223224
} else if !allow_dotfiles && segment.starts_with('.') {
224225
return Err(PathError::BadStart('.'))
225226
} else if segment.starts_with('*') {

core/lib/src/fs/mod.rs

+52-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,59 @@ mod named_file;
55
mod temp_file;
66
mod file_name;
77

8+
pub mod rewrite;
9+
810
pub use server::*;
911
pub use named_file::*;
1012
pub use temp_file::*;
1113
pub use file_name::*;
12-
pub use server::relative;
14+
15+
crate::export! {
16+
/// Generates a crate-relative version of a path.
17+
///
18+
/// This macro is primarily intended for use with [`FileServer`] to serve
19+
/// files from a path relative to the crate root.
20+
///
21+
/// The macro accepts one parameter, `$path`, an absolute or (preferably)
22+
/// relative path. It returns a path as an `&'static str` prefixed with the
23+
/// path to the crate root. Use `Path::new(relative!($path))` to retrieve an
24+
/// `&'static Path`.
25+
///
26+
/// # Example
27+
///
28+
/// Serve files from the crate-relative `static/` directory:
29+
///
30+
/// ```rust
31+
/// # #[macro_use] extern crate rocket;
32+
/// use rocket::fs::{FileServer, relative};
33+
///
34+
/// #[launch]
35+
/// fn rocket() -> _ {
36+
/// rocket::build().mount("/", FileServer::new(relative!("static")))
37+
/// }
38+
/// ```
39+
///
40+
/// Path equivalences:
41+
///
42+
/// ```rust
43+
/// use std::path::Path;
44+
///
45+
/// use rocket::fs::relative;
46+
///
47+
/// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static");
48+
/// let automatic_1 = Path::new(relative!("static"));
49+
/// let automatic_2 = Path::new(relative!("/static"));
50+
/// assert_eq!(manual, automatic_1);
51+
/// assert_eq!(automatic_1, automatic_2);
52+
/// ```
53+
///
54+
macro_rules! relative {
55+
($path:expr) => {
56+
if cfg!(windows) {
57+
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
58+
} else {
59+
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
60+
}
61+
};
62+
}
63+
}

core/lib/src/fs/rewrite.rs

+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use std::borrow::Cow;
2+
use std::path::{Path, PathBuf};
3+
4+
use crate::Request;
5+
use crate::http::{ext::IntoOwned, HeaderMap};
6+
use crate::response::Redirect;
7+
8+
/// A file server [`Rewrite`] rewriter.
9+
///
10+
/// A [`FileServer`] is a sequence of [`Rewriter`]s which transform the incoming
11+
/// request path into a [`Rewrite`] or `None`. The first rewriter is called with
12+
/// the request path as a [`Rewrite::File`]. Each `Rewriter` thereafter is
13+
/// called in-turn with the previously returned [`Rewrite`], and the value
14+
/// returned from the last `Rewriter` is used to respond to the request. If the
15+
/// final rewrite is `None` or a nonexistent path or a directory, [`FileServer`]
16+
/// responds with [`Status::NotFound`]. Otherwise it responds with the file
17+
/// contents, if [`Rewrite::File`] is specified, or a redirect, if
18+
/// [`Rewrite::Redirect`] is specified.
19+
///
20+
/// [`FileServer`]: super::FileServer
21+
/// [`Status::NotFound`]: crate::http::Status::NotFound
22+
pub trait Rewriter: Send + Sync + 'static {
23+
/// Alter the [`Rewrite`] as needed.
24+
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &'r Request<'_>) -> Option<Rewrite<'r>>;
25+
}
26+
27+
/// A Response from a [`FileServer`](super::FileServer)
28+
#[derive(Debug, Clone)]
29+
#[non_exhaustive]
30+
pub enum Rewrite<'r> {
31+
/// Return the contents of the specified file.
32+
File(File<'r>),
33+
/// Returns a Redirect.
34+
Redirect(Redirect),
35+
}
36+
37+
/// A File response from a [`FileServer`](super::FileServer) and a rewriter.
38+
#[derive(Debug, Clone)]
39+
#[non_exhaustive]
40+
pub struct File<'r> {
41+
/// The path to the file that [`FileServer`](super::FileServer) will respond with.
42+
pub path: Cow<'r, Path>,
43+
/// A list of headers to be added to the generated response.
44+
pub headers: HeaderMap<'r>,
45+
}
46+
47+
impl<'r> File<'r> {
48+
/// A new `File`, with not additional headers.
49+
pub fn new(path: impl Into<Cow<'r, Path>>) -> Self {
50+
Self { path: path.into(), headers: HeaderMap::new() }
51+
}
52+
53+
/// A new `File`, with not additional headers.
54+
///
55+
/// # Panics
56+
///
57+
/// Panics if the `path` does not exist.
58+
pub fn checked<P: AsRef<Path>>(path: P) -> Self {
59+
let path = path.as_ref();
60+
if !path.exists() {
61+
let path = path.display();
62+
error!(%path, "FileServer path does not exist.\n\
63+
Panicking to prevent inevitable handler error.");
64+
panic!("missing file {}: refusing to continue", path);
65+
}
66+
67+
Self::new(path.to_path_buf())
68+
}
69+
70+
/// Replace the path in `self` with the result of applying `f` to the path.
71+
pub fn map_path<F, P>(self, f: F) -> Self
72+
where F: FnOnce(Cow<'r, Path>) -> P, P: Into<Cow<'r, Path>>,
73+
{
74+
Self {
75+
path: f(self.path).into(),
76+
headers: self.headers,
77+
}
78+
}
79+
80+
/// Returns `true` if the file is a dotfile. A dotfile is a file whose
81+
/// name or any directory in it's path start with a period (`.`) and is
82+
/// considered hidden.
83+
///
84+
/// # Windows Note
85+
///
86+
/// This does *not* check the file metadata on any platform, so hidden files
87+
/// on Windows will not be detected by this method.
88+
pub fn is_hidden(&self) -> bool {
89+
self.path.iter().any(|s| s.as_encoded_bytes().starts_with(b"."))
90+
}
91+
92+
/// Returns `true` if the file is not hidden. This is the inverse of
93+
/// [`File::is_hidden()`].
94+
pub fn is_visible(&self) -> bool {
95+
!self.is_hidden()
96+
}
97+
}
98+
99+
/// Prefixes all paths with a given path.
100+
///
101+
/// # Example
102+
///
103+
/// ```rust,no_run
104+
/// use rocket::fs::FileServer;
105+
/// use rocket::fs::rewrite::Prefix;
106+
///
107+
/// FileServer::identity()
108+
/// .filter(|f, _| f.is_visible())
109+
/// .rewrite(Prefix::checked("static"));
110+
/// ```
111+
pub struct Prefix(PathBuf);
112+
113+
impl Prefix {
114+
/// Panics if `path` does not exist.
115+
pub fn checked<P: AsRef<Path>>(path: P) -> Self {
116+
let path = path.as_ref();
117+
if !path.is_dir() {
118+
let path = path.display();
119+
error!(%path, "FileServer path is not a directory.");
120+
warn!("Aborting early to prevent inevitable handler error.");
121+
panic!("invalid directory: refusing to continue");
122+
}
123+
124+
Self(path.to_path_buf())
125+
}
126+
127+
/// Creates a new `Prefix` from a path.
128+
pub fn unchecked<P: AsRef<Path>>(path: P) -> Self {
129+
Self(path.as_ref().to_path_buf())
130+
}
131+
}
132+
133+
impl Rewriter for Prefix {
134+
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
135+
opt.map(|r| match r {
136+
Rewrite::File(f) => Rewrite::File(f.map_path(|p| self.0.join(p))),
137+
Rewrite::Redirect(r) => Rewrite::Redirect(r),
138+
})
139+
}
140+
}
141+
142+
impl Rewriter for PathBuf {
143+
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
144+
Some(Rewrite::File(File::new(self.clone())))
145+
}
146+
}
147+
148+
/// Normalize directories to always include a trailing slash by redirecting
149+
/// (with a 302 temporary redirect) requests for directories without a trailing
150+
/// slash to the same path with a trailing slash.
151+
///
152+
/// # Example
153+
///
154+
/// ```rust,no_run
155+
/// use rocket::fs::FileServer;
156+
/// use rocket::fs::rewrite::{Prefix, TrailingDirs};
157+
///
158+
/// FileServer::identity()
159+
/// .filter(|f, _| f.is_visible())
160+
/// .rewrite(TrailingDirs);
161+
/// ```
162+
pub struct TrailingDirs;
163+
164+
impl Rewriter for TrailingDirs {
165+
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &Request<'_>) -> Option<Rewrite<'r>> {
166+
if let Some(Rewrite::File(f)) = &opt {
167+
if !req.uri().path().ends_with('/') && f.path.is_dir() {
168+
let uri = req.uri().clone().into_owned();
169+
let uri = uri.map_path(|p| format!("{p}/")).unwrap();
170+
return Some(Rewrite::Redirect(Redirect::temporary(uri)));
171+
}
172+
}
173+
174+
opt
175+
}
176+
}
177+
178+
/// Rewrite a directory to a file inside of that directory.
179+
///
180+
/// # Example
181+
///
182+
/// Rewrites all directory requests to `directory/index.html`.
183+
///
184+
/// ```rust,no_run
185+
/// use rocket::fs::FileServer;
186+
/// use rocket::fs::rewrite::DirIndex;
187+
///
188+
/// FileServer::without_index("static")
189+
/// .rewrite(DirIndex::if_exists("index.htm"))
190+
/// .rewrite(DirIndex::unconditional("index.html"));
191+
/// ```
192+
pub struct DirIndex {
193+
path: PathBuf,
194+
check: bool,
195+
}
196+
197+
impl DirIndex {
198+
/// Appends `path` to every request for a directory.
199+
pub fn unconditional(path: impl AsRef<Path>) -> Self {
200+
Self { path: path.as_ref().to_path_buf(), check: false }
201+
}
202+
203+
/// Only appends `path` to a request for a directory if the file exists.
204+
pub fn if_exists(path: impl AsRef<Path>) -> Self {
205+
Self { path: path.as_ref().to_path_buf(), check: true }
206+
}
207+
}
208+
209+
impl Rewriter for DirIndex {
210+
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
211+
match opt? {
212+
Rewrite::File(f) if f.path.is_dir() => {
213+
let candidate = f.path.join(&self.path);
214+
if self.check && !candidate.is_file() {
215+
return Some(Rewrite::File(f));
216+
}
217+
218+
Some(Rewrite::File(f.map_path(|_| candidate)))
219+
}
220+
r => Some(r),
221+
}
222+
}
223+
}
224+
225+
impl<'r> From<File<'r>> for Rewrite<'r> {
226+
fn from(value: File<'r>) -> Self {
227+
Self::File(value)
228+
}
229+
}
230+
231+
impl<'r> From<Redirect> for Rewrite<'r> {
232+
fn from(value: Redirect) -> Self {
233+
Self::Redirect(value)
234+
}
235+
}
236+
237+
impl<F: Send + Sync + 'static> Rewriter for F
238+
where F: for<'r> Fn(Option<Rewrite<'r>>, &Request<'_>) -> Option<Rewrite<'r>>
239+
{
240+
fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
241+
self(f, r)
242+
}
243+
}
244+
245+
impl Rewriter for Rewrite<'static> {
246+
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
247+
Some(self.clone())
248+
}
249+
}
250+
251+
impl Rewriter for File<'static> {
252+
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
253+
Some(Rewrite::File(self.clone()))
254+
}
255+
}
256+
257+
impl Rewriter for Redirect {
258+
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
259+
Some(Rewrite::Redirect(self.clone()))
260+
}
261+
}

0 commit comments

Comments
 (0)