Skip to content

Commit

Permalink
Add static file serving
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yoshuawuyts committed Apr 21, 2020
1 parent 1b7fceb commit f3af561
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 24 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ serde = { version = "1.0.102", features = ["derive"] }
structopt = "0.3.3"
surf = "2.0.0-alpha.1"
futures = "0.3.1"
femme = "1.3.0"

[[test]]
name = "nested"
Expand Down
12 changes: 12 additions & 0 deletions examples/static_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use async_std::task;

fn main() -> Result<(), std::io::Error> {
femme::start(log::LevelFilter::Info).unwrap();
task::block_on(async {
let mut app = tide::new();
app.at("/").get(|_| async move { Ok("visit /src/*") });
app.at("/src").serve_dir("src/")?;
app.listen("127.0.0.1:8080").await?;
Ok(())
})
}
5 changes: 5 additions & 0 deletions src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ impl<State> Request<State> {
let locked_jar = cookie_data.content.read().unwrap();
locked_jar.get(name).cloned()
}

/// Get the length of the body.
pub fn len(&self) -> Option<usize> {
self.request.len()
}
}

impl<State> Read for Request<State> {
Expand Down
10 changes: 10 additions & 0 deletions src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ impl Response {
self
}

/// Get the length of the body.
pub fn len(&self) -> Option<usize> {
self.res.len()
}

/// Insert an HTTP header.
pub fn set_header(
mut self,
Expand Down Expand Up @@ -121,6 +126,11 @@ impl Response {
self.set_mime(mime::APPLICATION_OCTET_STREAM)
}

/// Set the body reader.
pub fn set_body(&mut self, body: impl Into<Body>) {
self.res.set_body(body);
}

/// Encode a struct as a form and set as the response body.
///
/// # Mime
Expand Down
25 changes: 15 additions & 10 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ use async_std::io;
use async_std::net::ToSocketAddrs;
use async_std::sync::Arc;
use async_std::task::{Context, Poll};

use http_service::HttpService;

use std::fmt::Debug;
use std::pin::Pin;

use crate::middleware::{Middleware, Next};
use crate::middleware::{cookies, Middleware, Next};
use crate::router::{Router, Selection};
use crate::utils::BoxFuture;
use crate::{Endpoint, Request, Response};

mod route;
mod serve_dir;

pub use route::Route;

Expand Down Expand Up @@ -198,13 +199,13 @@ impl<State: Send + Sync + 'static> Server<State> {
/// # Ok(()) }) }
/// ```
pub fn with_state(state: State) -> Server<State> {
Server {
let mut server = Server {
router: Arc::new(Router::new()),
middleware: Arc::new(vec![Arc::new(
crate::middleware::cookies::CookiesMiddleware::new(),
)]),
middleware: Arc::new(vec![]),
state: Arc::new(state),
}
};
server.middleware(cookies::CookiesMiddleware::new());
server
}

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

Expand Down
46 changes: 45 additions & 1 deletion src/server/route.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::fmt::Debug;
use std::io;
use std::path::Path;
use std::sync::Arc;

use super::serve_dir::ServeDir;
use crate::endpoint::MiddlewareEndpoint;
use crate::utils::BoxFuture;
use crate::{router::Router, Endpoint, Middleware, Response};
Expand Down Expand Up @@ -54,6 +58,11 @@ impl<'a, State: 'static> Route<'a, State> {
}
}

/// Get the current path.
pub fn path(&self) -> &str {
&self.path
}

/// Treat the current path as a prefix, and strip prefixes from requests.
///
/// This method is marked unstable as its name might change in the near future.
Expand All @@ -67,7 +76,15 @@ impl<'a, State: 'static> Route<'a, State> {
}

/// Apply the given middleware to the current route.
pub fn middleware(&mut self, middleware: impl Middleware<State>) -> &mut Self {
pub fn middleware<M>(&mut self, middleware: M) -> &mut Self
where
M: Middleware<State> + Debug,
{
log::trace!(
"Adding middleware {:?} to route {:?}",
middleware,
self.path
);
self.middleware.push(Arc::new(middleware));
self
}
Expand All @@ -92,6 +109,33 @@ impl<'a, State: 'static> Route<'a, State> {
self
}

/// Serve a directory statically.
///
/// Each file will be streamed from disk, and a mime type will be determined
/// based on magic bytes.
///
/// # Examples
///
/// Serve the contents of the local directory `./public/images/*` from
/// `localhost:8080/images/*`.
///
/// ```no_run
/// #[async_std::main]
/// async fn main() -> Result<(), std::io::Error> {
/// let mut app = tide::new();
/// app.at("/public/images").serve_dir("images/")?;
/// app.listen("127.0.0.1:8080").await?;
/// Ok(())
/// }
/// ```
pub fn serve_dir(&mut self, dir: impl AsRef<Path>) -> io::Result<()> {
// Verify path exists, return error if it doesn't.
let dir = dir.as_ref().to_owned().canonicalize()?;
let prefix = self.path().to_string();
self.at("*").get(ServeDir::new(prefix, dir));
Ok(())
}

/// Add an endpoint for the given HTTP method
pub fn method(&mut self, method: http_types::Method, ep: impl Endpoint<State>) -> &mut Self {
if self.prefix {
Expand Down
79 changes: 79 additions & 0 deletions src/server/serve_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use async_std::fs::File;
use async_std::io::BufReader;
use http_types::{Body, StatusCode};

use crate::{Endpoint, Request, Response, Result};

use std::path::{Path, PathBuf};

type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + 'a + Send>>;
pub struct ServeDir {
prefix: String,
dir: PathBuf,
}

impl ServeDir {
/// Create a new instance of `ServeDir`.
pub(crate) fn new(prefix: String, dir: PathBuf) -> Self {
Self { prefix, dir }
}
}

impl<State> Endpoint<State> for ServeDir {
fn call<'a>(&'a self, req: Request<State>) -> BoxFuture<'a, Result<Response>> {
let path = req.uri().path();
let path = path.replacen(&self.prefix, "", 1);
let path = path.trim_start_matches('/');
let mut dir = self.dir.clone();
for p in Path::new(path) {
dir.push(&p);
}
log::info!("Requested file: {:?}", dir);

Box::pin(async move {
let file = match async_std::fs::canonicalize(&dir).await {
Err(_) => {
// This needs to return the same status code as the
// unauthorized case below to ensure we don't leak
// information of which files exist to adversaries.
log::warn!("File not found: {:?}", dir);
return Ok(Response::new(StatusCode::NotFound));
}
Ok(mut file_path) => {
// Verify this is a sub-path of the original dir.
let mut file_iter = (&mut file_path).iter();
if !dir.iter().all(|lhs| Some(lhs) == file_iter.next()) {
// This needs to return the same status code as the
// 404 case above to ensure we don't leak
// information about the local fs to adversaries.
log::warn!("Unauthorized attempt to read: {:?}", file_path);
return Ok(Response::new(StatusCode::NotFound));
}

// Open the file and send back the contents.
match File::open(&file_path).await {
Ok(file) => file,
Err(_) => {
log::warn!("Could not open {:?}", file_path);
return Ok(Response::new(StatusCode::InternalServerError));
}
}
}
};

let len = match file.metadata().await {
Ok(metadata) => metadata.len() as usize,
Err(_) => {
log::warn!("Could not retrieve metadata");
return Ok(Response::new(StatusCode::InternalServerError));
}
};

let body = Body::from_reader(BufReader::new(file), Some(len));
// TODO: fix related bug where async-h1 crashes on large files
let mut res = Response::new(StatusCode::Ok);
res.set_body(body);
Ok(res)
})
}
}
40 changes: 27 additions & 13 deletions tests/nested.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use http_service_mock::make_server;
use http_types::headers::{HeaderName, HeaderValue};
use http_types::{Method, Request, Url};
use std::str::FromStr;
use tide::{Middleware, Next};

#[async_std::test]
async fn nested() {
Expand Down Expand Up @@ -36,24 +37,37 @@ async fn nested() {
#[async_std::test]
async fn nested_middleware() {
let echo_path = |req: tide::Request<()>| async move { Ok(req.uri().path().to_string()) };
fn test_middleware(
req: tide::Request<()>,
next: tide::Next<'_, ()>,
) -> BoxFuture<'_, tide::Result<tide::Response>> {
Box::pin(async move {
let res = next.run(req).await?;
let res = res.set_header(
HeaderName::from_ascii("X-Tide-Test".to_owned().into_bytes()).unwrap(),
"1",
);
Ok(res)
})

#[derive(Debug, Clone, Default)]
pub struct TestMiddleware;

impl TestMiddleware {
pub fn new() -> Self {
Self {}
}
}

impl<State: Send + Sync + 'static> Middleware<State> for TestMiddleware {
fn handle<'a>(
&'a self,
req: tide::Request<State>,
next: Next<'a, State>,
) -> BoxFuture<'a, tide::Result<tide::Response>> {
Box::pin(async move {
let res = next.run(req).await?;
let res = res.set_header(
HeaderName::from_ascii("X-Tide-Test".to_owned().into_bytes()).unwrap(),
"1",
);
Ok(res)
})
}
}

let mut app = tide::new();

let mut inner_app = tide::new();
inner_app.middleware(test_middleware);
inner_app.middleware(TestMiddleware::new());
inner_app.at("/echo").get(echo_path);
inner_app.at("/:foo/bar").strip_prefix().get(echo_path);
app.at("/foo").nest(inner_app);
Expand Down
1 change: 1 addition & 0 deletions tests/route_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use http_types::{headers::HeaderName, Method, Request};
use std::convert::TryInto;
use tide::Middleware;

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

impl TestMiddleware {
Expand Down

0 comments on commit f3af561

Please sign in to comment.