Skip to content

Commit 4a40a7b

Browse files
committed
Implement basic rewrite API
1 parent 370287c commit 4a40a7b

File tree

2 files changed

+175
-50
lines changed

2 files changed

+175
-50
lines changed

core/http/src/uri/segments.rs

+33
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,39 @@ impl<'a> Segments<'a, Path> {
245245

246246
Ok(buf)
247247
}
248+
249+
/// Similar to `to_path_buf`, but always allows dotfiles, and reports whether
250+
/// the path contains dotfiles.
251+
pub fn to_path_buf_dotfiles(&self) -> Result<(PathBuf, bool), PathError> {
252+
let mut buf = PathBuf::new();
253+
let mut is_dotfile = false;
254+
for segment in self.clone() {
255+
if segment == ".." {
256+
buf.pop();
257+
} else if segment.starts_with('.') {
258+
buf.push(segment);
259+
is_dotfile = true;
260+
} else if segment.starts_with('*') {
261+
return Err(PathError::BadStart('*'))
262+
} else if segment.ends_with(':') {
263+
return Err(PathError::BadEnd(':'))
264+
} else if segment.ends_with('>') {
265+
return Err(PathError::BadEnd('>'))
266+
} else if segment.ends_with('<') {
267+
return Err(PathError::BadEnd('<'))
268+
} else if segment.contains('/') {
269+
return Err(PathError::BadChar('/'))
270+
} else if cfg!(windows) && segment.contains('\\') {
271+
return Err(PathError::BadChar('\\'))
272+
} else if cfg!(windows) && segment.contains(':') {
273+
return Err(PathError::BadChar(':'))
274+
} else {
275+
buf.push(segment)
276+
}
277+
}
278+
279+
Ok((buf, is_dotfile))
280+
}
248281
}
249282

250283
impl<'a> Segments<'a, Query> {

core/lib/src/fs/server.rs

+142-50
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use core::fmt;
12
use std::path::{PathBuf, Path};
3+
use std::sync::Arc;
4+
use std::time::SystemTime;
25

3-
use crate::{Request, Data};
4-
use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
6+
7+
use crate::{Data, Request};
8+
use crate::http::{Method, Status, uri::{Segments, Uri}, ext::IntoOwned, HeaderMap, Header};
59
use crate::route::{Route, Handler, Outcome};
610
use crate::response::{Redirect, Responder};
711
use crate::outcome::IntoOutcome;
@@ -60,13 +64,118 @@ use crate::fs::NamedFile;
6064
/// rocket::build().mount("/", FileServer::from(relative!("static")))
6165
/// }
6266
/// ```
63-
#[derive(Debug, Clone)]
67+
#[derive(Clone)]
6468
pub struct FileServer {
6569
root: PathBuf,
6670
options: Options,
71+
// TODO: I'd prefer box, but this just makes Clone easier.
72+
rewrites: Vec<Arc<dyn Rewrite>>,
6773
rank: isize,
6874
}
6975

76+
impl fmt::Debug for FileServer {
77+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78+
f.debug_struct("FileServer")
79+
.field("root", &self.root)
80+
.field("options", &self.options)
81+
.field("rewrites", &DebugListRewrite(&self.rewrites))
82+
.field("rank", &self.rank)
83+
.finish()
84+
}
85+
}
86+
87+
struct DebugListRewrite<'a>(&'a Vec<Arc<dyn Rewrite>>);
88+
89+
impl fmt::Debug for DebugListRewrite<'_> {
90+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91+
f.debug_list().entries(self.0.iter().map(|r| r.name())).finish()
92+
}
93+
}
94+
95+
pub trait Rewrite: Send + Sync + 'static {
96+
fn name(&self) -> &'static str {
97+
std::any::type_name::<Self>()
98+
}
99+
/// Modify RewritablePath as needed.
100+
fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, root: &Path) -> FileServerResponse;
101+
}
102+
103+
pub enum HiddenReason {
104+
DotFile,
105+
PermissionDenied,
106+
Other,
107+
}
108+
109+
pub enum FileServerResponse {
110+
/// Status: Ok
111+
File {
112+
name: PathBuf,
113+
modified: Option<SystemTime>,
114+
headers: HeaderMap<'static>,
115+
},
116+
/// Status: NotFound
117+
NotFound { name: PathBuf },
118+
/// Status: NotFound (used to identify if the file does actually exist)
119+
Hidden { name: PathBuf, reason: HiddenReason },
120+
/// Status: Redirect
121+
PermanentRedirect { to: Uri<'static> },
122+
/// Status: Redirect (TODO: should we allow this?)
123+
TemporaryRedirect { to: Uri<'static> },
124+
}
125+
126+
// These might have to remain as basic options (always processed first)
127+
struct DotFiles;
128+
impl Rewrite for DotFiles {
129+
fn rewrite(&self, _req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse {
130+
match path {
131+
FileServerResponse::Hidden { name, reason: HiddenReason::DotFile } => FileServerResponse::File { name, modified: None, headers: HeaderMap::new() },
132+
path => path,
133+
}
134+
}
135+
}
136+
// struct Missing; // This only applies on startup, so this needs to be an option
137+
// impl Rewrite for Missing {
138+
// fn rewrite(&self, req: &Request<'_>, path: &mut RewritablePath<'_>) {
139+
// todo!()
140+
// }
141+
// }
142+
143+
struct Index(&'static str);
144+
impl Rewrite for Index {
145+
fn rewrite(&self, _req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse {
146+
// if path.file_name_path.is_dir() {
147+
// path.file_name_path.push(self.0);
148+
// // TODO: handle file_data_path
149+
// }
150+
match path {
151+
FileServerResponse::File { name, modified, headers } if name.is_dir() => FileServerResponse::File { name: name.join(self.0), modified, headers },
152+
path => path,
153+
}
154+
}
155+
}
156+
// I'm not sure this one works, since we should check it during startup
157+
struct IndexFile;
158+
impl Rewrite for IndexFile {
159+
fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, root: &Path) -> FileServerResponse {
160+
todo!()
161+
}
162+
}
163+
164+
struct NormalizeDirs;
165+
impl Rewrite for NormalizeDirs {
166+
fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse {
167+
match path {
168+
FileServerResponse::File { name, .. } if name.is_dir() && !req.uri().path().ends_with('/') =>
169+
FileServerResponse::PermanentRedirect {
170+
to: req.uri().map_path(|p| format!("{}/", p))
171+
.expect("adding a trailing slash to a known good path => valid path")
172+
.into_owned().into()
173+
},
174+
path => path,
175+
}
176+
}
177+
}
178+
70179
impl FileServer {
71180
/// The default rank use by `FileServer` routes.
72181
const DEFAULT_RANK: isize = 10;
@@ -159,7 +268,12 @@ impl FileServer {
159268
}
160269
}
161270

162-
FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK }
271+
FileServer { root: path.into(), options, rewrites: vec![], rank: Self::DEFAULT_RANK }
272+
}
273+
274+
pub fn rewrite(mut self, rewrite: impl Rewrite) -> Self {
275+
self.rewrites.push(Arc::new(rewrite));
276+
self
163277
}
164278

165279
/// Sets the rank for generated routes to `rank`.
@@ -193,55 +307,33 @@ impl From<FileServer> for Vec<Route> {
193307
#[crate::async_trait]
194308
impl Handler for FileServer {
195309
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
196-
use crate::http::uri::fmt::Path;
197-
198-
// TODO: Should we reject dotfiles for `self.root` if !DotFiles?
199-
let options = self.options;
200-
if options.contains(Options::IndexFile) && self.root.is_file() {
201-
let segments = match req.segments::<Segments<'_, Path>>(0..) {
202-
Ok(segments) => segments,
203-
Err(never) => match never {},
204-
};
205-
206-
if segments.is_empty() {
207-
let file = NamedFile::open(&self.root).await;
208-
return file.respond_to(req).or_forward((data, Status::NotFound));
209-
} else {
210-
return Outcome::forward(data, Status::NotFound);
211-
}
310+
use crate::http::uri::fmt::Path as UriPath;
311+
// let options = self.options;
312+
let path = req.segments::<Segments<'_, UriPath>>(0..).ok()
313+
.and_then(|segments| segments.to_path_buf_dotfiles().ok());
314+
// .map(|path| self.root.join(path));
315+
let mut response = match path {
316+
Some((name, false)) => FileServerResponse::File { name, modified: None, headers: HeaderMap::new() },
317+
Some((name, true)) => FileServerResponse::Hidden { name, reason: HiddenReason::DotFile },
318+
None => return Outcome::forward(data, Status::NotFound),
319+
};
320+
for rewrite in &self.rewrites {
321+
response = rewrite.rewrite(req, response, &self.root);
212322
}
213-
214-
// Get the segments as a `PathBuf`, allowing dotfiles requested.
215-
let allow_dotfiles = options.contains(Options::DotFiles);
216-
let path = req.segments::<Segments<'_, Path>>(0..).ok()
217-
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
218-
.map(|path| self.root.join(path));
219-
220-
match path {
221-
Some(p) if p.is_dir() => {
222-
// Normalize '/a/b/foo' to '/a/b/foo/'.
223-
if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') {
224-
let normal = req.uri().map_path(|p| format!("{}/", p))
225-
.expect("adding a trailing slash to a known good path => valid path")
226-
.into_owned();
227-
228-
return Redirect::permanent(normal)
229-
.respond_to(req)
230-
.or_forward((data, Status::InternalServerError));
323+
match response {
324+
FileServerResponse::File { name, modified, headers } => NamedFile::open(self.root.join(name)).await.respond_to(req).map(|mut r| {
325+
for header in headers {
326+
r.adjoin_raw_header(header.name.as_str().to_owned(), header.value);
231327
}
232-
233-
if !options.contains(Options::Index) {
234-
return Outcome::forward(data, Status::NotFound);
328+
if let Some(modified) = modified {
329+
// TODO: must be converted to http-data format
330+
// r.set_header(Header::new("Last-Modified", format!("{:?}", modified)));
235331
}
236-
237-
let index = NamedFile::open(p.join("index.html")).await;
238-
index.respond_to(req).or_forward((data, Status::NotFound))
239-
},
240-
Some(p) => {
241-
let file = NamedFile::open(p).await;
242-
file.respond_to(req).or_forward((data, Status::NotFound))
243-
}
244-
None => Outcome::forward(data, Status::NotFound),
332+
r
333+
}).or_forward((data, Status::NotFound)),
334+
FileServerResponse::Hidden { .. } | FileServerResponse::NotFound { ..} => Outcome::forward(data, Status::NotFound),
335+
FileServerResponse::PermanentRedirect { to } => Redirect::permanent(to).respond_to(req).or_forward((data, Status::InternalServerError)),
336+
FileServerResponse::TemporaryRedirect { to } => Redirect::temporary(to).respond_to(req).or_forward((data, Status::InternalServerError)),
245337
}
246338
}
247339
}

0 commit comments

Comments
 (0)