Skip to content

Commit f3af561

Browse files
committed
Add static file serving
This depends on #414 Add serve_dir method to Route Update serve_dir.rs Adjust methods so body can correctly be sent along tweak static file serving simplify internals cargo fmt & fix tests fix all tests cargo fmt Fix merge conflicts with master undo bonus changes Fix static path prefix stripping err
1 parent 1b7fceb commit f3af561

File tree

9 files changed

+195
-24
lines changed

9 files changed

+195
-24
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ serde = { version = "1.0.102", features = ["derive"] }
6161
structopt = "0.3.3"
6262
surf = "2.0.0-alpha.1"
6363
futures = "0.3.1"
64+
femme = "1.3.0"
6465

6566
[[test]]
6667
name = "nested"

examples/static_file.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use async_std::task;
2+
3+
fn main() -> Result<(), std::io::Error> {
4+
femme::start(log::LevelFilter::Info).unwrap();
5+
task::block_on(async {
6+
let mut app = tide::new();
7+
app.at("/").get(|_| async move { Ok("visit /src/*") });
8+
app.at("/src").serve_dir("src/")?;
9+
app.listen("127.0.0.1:8080").await?;
10+
Ok(())
11+
})
12+
}

src/request.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ impl<State> Request<State> {
302302
let locked_jar = cookie_data.content.read().unwrap();
303303
locked_jar.get(name).cloned()
304304
}
305+
306+
/// Get the length of the body.
307+
pub fn len(&self) -> Option<usize> {
308+
self.request.len()
309+
}
305310
}
306311

307312
impl<State> Read for Request<State> {

src/response/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ impl Response {
6464
self
6565
}
6666

67+
/// Get the length of the body.
68+
pub fn len(&self) -> Option<usize> {
69+
self.res.len()
70+
}
71+
6772
/// Insert an HTTP header.
6873
pub fn set_header(
6974
mut self,
@@ -121,6 +126,11 @@ impl Response {
121126
self.set_mime(mime::APPLICATION_OCTET_STREAM)
122127
}
123128

129+
/// Set the body reader.
130+
pub fn set_body(&mut self, body: impl Into<Body>) {
131+
self.res.set_body(body);
132+
}
133+
124134
/// Encode a struct as a form and set as the response body.
125135
///
126136
/// # Mime

src/server/mod.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ use async_std::io;
55
use async_std::net::ToSocketAddrs;
66
use async_std::sync::Arc;
77
use async_std::task::{Context, Poll};
8-
98
use http_service::HttpService;
109

10+
use std::fmt::Debug;
1111
use std::pin::Pin;
1212

13-
use crate::middleware::{Middleware, Next};
13+
use crate::middleware::{cookies, Middleware, Next};
1414
use crate::router::{Router, Selection};
1515
use crate::utils::BoxFuture;
1616
use crate::{Endpoint, Request, Response};
1717

1818
mod route;
19+
mod serve_dir;
1920

2021
pub use route::Route;
2122

@@ -198,13 +199,13 @@ impl<State: Send + Sync + 'static> Server<State> {
198199
/// # Ok(()) }) }
199200
/// ```
200201
pub fn with_state(state: State) -> Server<State> {
201-
Server {
202+
let mut server = Server {
202203
router: Arc::new(Router::new()),
203-
middleware: Arc::new(vec![Arc::new(
204-
crate::middleware::cookies::CookiesMiddleware::new(),
205-
)]),
204+
middleware: Arc::new(vec![]),
206205
state: Arc::new(state),
207-
}
206+
};
207+
server.middleware(cookies::CookiesMiddleware::new());
208+
server
208209
}
209210

210211
/// Add a new route at the given `path`, relative to root.
@@ -269,10 +270,14 @@ impl<State: Send + Sync + 'static> Server<State> {
269270
///
270271
/// Middleware can only be added at the "top level" of an application,
271272
/// and is processed in the order in which it is applied.
272-
pub fn middleware(&mut self, m: impl Middleware<State>) -> &mut Self {
273-
let middleware = Arc::get_mut(&mut self.middleware)
273+
pub fn middleware<M>(&mut self, middleware: M) -> &mut Self
274+
where
275+
M: Middleware<State> + Debug,
276+
{
277+
log::trace!("Adding middleware {:?}", middleware);
278+
let m = Arc::get_mut(&mut self.middleware)
274279
.expect("Registering middleware is not possible after the Server has started");
275-
middleware.push(Arc::new(m));
280+
m.push(Arc::new(middleware));
276281
self
277282
}
278283

src/server/route.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
use std::fmt::Debug;
2+
use std::io;
3+
use std::path::Path;
14
use std::sync::Arc;
25

6+
use super::serve_dir::ServeDir;
37
use crate::endpoint::MiddlewareEndpoint;
48
use crate::utils::BoxFuture;
59
use crate::{router::Router, Endpoint, Middleware, Response};
@@ -54,6 +58,11 @@ impl<'a, State: 'static> Route<'a, State> {
5458
}
5559
}
5660

61+
/// Get the current path.
62+
pub fn path(&self) -> &str {
63+
&self.path
64+
}
65+
5766
/// Treat the current path as a prefix, and strip prefixes from requests.
5867
///
5968
/// This method is marked unstable as its name might change in the near future.
@@ -67,7 +76,15 @@ impl<'a, State: 'static> Route<'a, State> {
6776
}
6877

6978
/// Apply the given middleware to the current route.
70-
pub fn middleware(&mut self, middleware: impl Middleware<State>) -> &mut Self {
79+
pub fn middleware<M>(&mut self, middleware: M) -> &mut Self
80+
where
81+
M: Middleware<State> + Debug,
82+
{
83+
log::trace!(
84+
"Adding middleware {:?} to route {:?}",
85+
middleware,
86+
self.path
87+
);
7188
self.middleware.push(Arc::new(middleware));
7289
self
7390
}
@@ -92,6 +109,33 @@ impl<'a, State: 'static> Route<'a, State> {
92109
self
93110
}
94111

112+
/// Serve a directory statically.
113+
///
114+
/// Each file will be streamed from disk, and a mime type will be determined
115+
/// based on magic bytes.
116+
///
117+
/// # Examples
118+
///
119+
/// Serve the contents of the local directory `./public/images/*` from
120+
/// `localhost:8080/images/*`.
121+
///
122+
/// ```no_run
123+
/// #[async_std::main]
124+
/// async fn main() -> Result<(), std::io::Error> {
125+
/// let mut app = tide::new();
126+
/// app.at("/public/images").serve_dir("images/")?;
127+
/// app.listen("127.0.0.1:8080").await?;
128+
/// Ok(())
129+
/// }
130+
/// ```
131+
pub fn serve_dir(&mut self, dir: impl AsRef<Path>) -> io::Result<()> {
132+
// Verify path exists, return error if it doesn't.
133+
let dir = dir.as_ref().to_owned().canonicalize()?;
134+
let prefix = self.path().to_string();
135+
self.at("*").get(ServeDir::new(prefix, dir));
136+
Ok(())
137+
}
138+
95139
/// Add an endpoint for the given HTTP method
96140
pub fn method(&mut self, method: http_types::Method, ep: impl Endpoint<State>) -> &mut Self {
97141
if self.prefix {

src/server/serve_dir.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use async_std::fs::File;
2+
use async_std::io::BufReader;
3+
use http_types::{Body, StatusCode};
4+
5+
use crate::{Endpoint, Request, Response, Result};
6+
7+
use std::path::{Path, PathBuf};
8+
9+
type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + 'a + Send>>;
10+
pub struct ServeDir {
11+
prefix: String,
12+
dir: PathBuf,
13+
}
14+
15+
impl ServeDir {
16+
/// Create a new instance of `ServeDir`.
17+
pub(crate) fn new(prefix: String, dir: PathBuf) -> Self {
18+
Self { prefix, dir }
19+
}
20+
}
21+
22+
impl<State> Endpoint<State> for ServeDir {
23+
fn call<'a>(&'a self, req: Request<State>) -> BoxFuture<'a, Result<Response>> {
24+
let path = req.uri().path();
25+
let path = path.replacen(&self.prefix, "", 1);
26+
let path = path.trim_start_matches('/');
27+
let mut dir = self.dir.clone();
28+
for p in Path::new(path) {
29+
dir.push(&p);
30+
}
31+
log::info!("Requested file: {:?}", dir);
32+
33+
Box::pin(async move {
34+
let file = match async_std::fs::canonicalize(&dir).await {
35+
Err(_) => {
36+
// This needs to return the same status code as the
37+
// unauthorized case below to ensure we don't leak
38+
// information of which files exist to adversaries.
39+
log::warn!("File not found: {:?}", dir);
40+
return Ok(Response::new(StatusCode::NotFound));
41+
}
42+
Ok(mut file_path) => {
43+
// Verify this is a sub-path of the original dir.
44+
let mut file_iter = (&mut file_path).iter();
45+
if !dir.iter().all(|lhs| Some(lhs) == file_iter.next()) {
46+
// This needs to return the same status code as the
47+
// 404 case above to ensure we don't leak
48+
// information about the local fs to adversaries.
49+
log::warn!("Unauthorized attempt to read: {:?}", file_path);
50+
return Ok(Response::new(StatusCode::NotFound));
51+
}
52+
53+
// Open the file and send back the contents.
54+
match File::open(&file_path).await {
55+
Ok(file) => file,
56+
Err(_) => {
57+
log::warn!("Could not open {:?}", file_path);
58+
return Ok(Response::new(StatusCode::InternalServerError));
59+
}
60+
}
61+
}
62+
};
63+
64+
let len = match file.metadata().await {
65+
Ok(metadata) => metadata.len() as usize,
66+
Err(_) => {
67+
log::warn!("Could not retrieve metadata");
68+
return Ok(Response::new(StatusCode::InternalServerError));
69+
}
70+
};
71+
72+
let body = Body::from_reader(BufReader::new(file), Some(len));
73+
// TODO: fix related bug where async-h1 crashes on large files
74+
let mut res = Response::new(StatusCode::Ok);
75+
res.set_body(body);
76+
Ok(res)
77+
})
78+
}
79+
}

tests/nested.rs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use http_service_mock::make_server;
33
use http_types::headers::{HeaderName, HeaderValue};
44
use http_types::{Method, Request, Url};
55
use std::str::FromStr;
6+
use tide::{Middleware, Next};
67

78
#[async_std::test]
89
async fn nested() {
@@ -36,24 +37,37 @@ async fn nested() {
3637
#[async_std::test]
3738
async fn nested_middleware() {
3839
let echo_path = |req: tide::Request<()>| async move { Ok(req.uri().path().to_string()) };
39-
fn test_middleware(
40-
req: tide::Request<()>,
41-
next: tide::Next<'_, ()>,
42-
) -> BoxFuture<'_, tide::Result<tide::Response>> {
43-
Box::pin(async move {
44-
let res = next.run(req).await?;
45-
let res = res.set_header(
46-
HeaderName::from_ascii("X-Tide-Test".to_owned().into_bytes()).unwrap(),
47-
"1",
48-
);
49-
Ok(res)
50-
})
40+
41+
#[derive(Debug, Clone, Default)]
42+
pub struct TestMiddleware;
43+
44+
impl TestMiddleware {
45+
pub fn new() -> Self {
46+
Self {}
47+
}
48+
}
49+
50+
impl<State: Send + Sync + 'static> Middleware<State> for TestMiddleware {
51+
fn handle<'a>(
52+
&'a self,
53+
req: tide::Request<State>,
54+
next: Next<'a, State>,
55+
) -> BoxFuture<'a, tide::Result<tide::Response>> {
56+
Box::pin(async move {
57+
let res = next.run(req).await?;
58+
let res = res.set_header(
59+
HeaderName::from_ascii("X-Tide-Test".to_owned().into_bytes()).unwrap(),
60+
"1",
61+
);
62+
Ok(res)
63+
})
64+
}
5165
}
5266

5367
let mut app = tide::new();
5468

5569
let mut inner_app = tide::new();
56-
inner_app.middleware(test_middleware);
70+
inner_app.middleware(TestMiddleware::new());
5771
inner_app.at("/echo").get(echo_path);
5872
inner_app.at("/:foo/bar").strip_prefix().get(echo_path);
5973
app.at("/foo").nest(inner_app);

tests/route_middleware.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use http_types::{headers::HeaderName, Method, Request};
44
use std::convert::TryInto;
55
use tide::Middleware;
66

7+
#[derive(Debug)]
78
struct TestMiddleware(HeaderName, &'static str);
89

910
impl TestMiddleware {

0 commit comments

Comments
 (0)