├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── doc_server.rs └── router.rs ├── src ├── lib.rs ├── requested_path.rs └── static_handler.rs └── tests ├── cache.rs └── static.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *# 4 | *.o 5 | *.so 6 | *.swp 7 | *.dylib 8 | *.dSYM 9 | *.dll 10 | *.rlib 11 | *.dummy 12 | *.exe 13 | target/ 14 | Cargo.lock 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: nightly 3 | after_success: 'curl https://raw.githubusercontent.com/iron/build-doc/master/build-doc.sh | sh ' 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **staticfile** uses the same conventions as **[Iron](https://github.com/iron/iron)**. 4 | 5 | ### Overview 6 | 7 | * Fork staticfile to your own account 8 | * Create a feature branch, namespaced by. 9 | * bug/... 10 | * feat/... 11 | * test/... 12 | * doc/... 13 | * refactor/... 14 | * Make commits to your feature branch. Prefix each commit like so: 15 | * (feat) Added a new feature 16 | * (fix) Fixed inconsistent tests [Fixes #0] 17 | * (refactor) ... 18 | * (cleanup) ... 19 | * (test) ... 20 | * (doc) ... 21 | * Make a pull request with your changes directly to master. Include a 22 | description of your changes. 23 | * Wait for one of the reviewers to look at your code and either merge it or 24 | give feedback which you should adapt to. 25 | 26 | #### Thank you for contributing! 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "staticfile" 4 | version = "0.5.0" 5 | authors = ["Zach Pomerantz ", "Jonathan Reem "] 6 | description = "Static file serving for Iron." 7 | repository = "https://github.com/iron/staticfile" 8 | license = "MIT" 9 | keywords = ["iron", "web", "http", "file"] 10 | 11 | [features] 12 | cache = ["filetime"] 13 | 14 | [dependencies] 15 | iron = ">=0.5, <0.7" 16 | mount = ">= 0.3, <0.5" 17 | time = "0.1" 18 | url = "1.1" 19 | 20 | [dependencies.filetime] 21 | version = "0.1" 22 | optional = true 23 | 24 | [dev-dependencies] 25 | hyper = "0.10" 26 | router = ">=0.5, <0.7" 27 | iron-test = ">=0.5, <0.7" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 iron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | staticfile [![Build Status](https://secure.travis-ci.org/iron/staticfile.png?branch=master)](https://travis-ci.org/iron/staticfile) 2 | ==== 3 | 4 | > Static file-serving handler for the [Iron](https://github.com/iron/iron) web framework. 5 | 6 | ## Example 7 | 8 | This example uses the [mounting handler][mounting-handler] to serve files from several directories. 9 | 10 | ```rust 11 | let mut mount = Mount::new(); 12 | 13 | // Serve the shared JS/CSS at / 14 | mount.mount("/", Static::new(Path::new("target/doc/"))); 15 | // Serve the static file docs at /doc/ 16 | mount.mount("/doc/", Static::new(Path::new("target/doc/staticfile/"))); 17 | // Serve the source code at /src/ 18 | mount.mount("/src/", Static::new(Path::new("target/doc/src/staticfile/lib.rs.html"))); 19 | 20 | Iron::new(mount).http("127.0.0.1:3000").unwrap(); 21 | ``` 22 | 23 | See [`examples/doc_server.rs`](examples/doc_server.rs) for a complete example that you can compile. 24 | 25 | ## Overview 26 | 27 | - Serve static files from a given path. 28 | 29 | It works well in combination with the [mounting handler][mounting-handler]. 30 | 31 | ## Installation 32 | 33 | If you're using a `Cargo.toml` to manage dependencies, just add the `staticfile` package to the `[dependencies]` section of the toml: 34 | 35 | ```toml 36 | staticfile = "*" 37 | ``` 38 | 39 | Otherwise, `cargo build`, and the rlib will be in your `target` directory. 40 | 41 | ## [Documentation](http://ironframework.io/doc/staticfile) 42 | 43 | Along with the [online documentation](http://ironframework.io/doc/staticfile), 44 | you can build a local copy with `cargo doc`. 45 | 46 | ## Get Help 47 | 48 | One of us ([@reem](https://github.com/reem/), [@zzmp](https://github.com/zzmp/), 49 | [@theptrk](https://github.com/theptrk/), [@mcreinhard](https://github.com/mcreinhard)) 50 | is usually on `#iron` on the mozilla irc. Come say hi and ask any questions you might have. 51 | We are also usually on `#rust` and `#rust-webdev`. 52 | 53 | [mounting-handler]: https://github.com/iron/mount 54 | -------------------------------------------------------------------------------- /examples/doc_server.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate staticfile; 3 | extern crate mount; 4 | 5 | // This example serves the docs from target/doc/staticfile at /doc/ 6 | // 7 | // Run `cargo doc && cargo run --example doc_server`, then 8 | // point your browser to http://127.0.0.1:3000/doc/ 9 | 10 | use std::path::Path; 11 | 12 | use iron::Iron; 13 | use staticfile::Static; 14 | use mount::Mount; 15 | 16 | fn main() { 17 | let mut mount = Mount::new(); 18 | 19 | // Serve the shared JS/CSS at / 20 | mount.mount("/", Static::new(Path::new("target/doc/"))); 21 | // Serve the static file docs at /doc/ 22 | mount.mount("/doc/", Static::new(Path::new("target/doc/staticfile/"))); 23 | // Serve the source code at /src/ 24 | mount.mount("/src/", Static::new(Path::new("target/doc/src/staticfile/lib.rs.html"))); 25 | 26 | println!("Doc server running on http://localhost:3000/doc/"); 27 | 28 | Iron::new(mount).http("127.0.0.1:3000").unwrap(); 29 | } 30 | -------------------------------------------------------------------------------- /examples/router.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to serve static files at specific 2 | //! mount points, and then delegate the rest of the paths to a router. 3 | //! 4 | //! It serves the docs from target/doc at the /docs/ mount point 5 | //! and delegates the rest to a router, which itself defines a 6 | //! handler for route /hello 7 | //! 8 | //! Make sure to generate the docs first with `cargo doc`, 9 | //! then build the tests with `cargo run --example router`. 10 | //! 11 | //! Visit http://127.0.0.1:3000/hello to view the routed path. 12 | //! 13 | //! Visit http://127.0.0.1:3000/docs/mount/ to view the mounted docs. 14 | 15 | extern crate iron; 16 | extern crate mount; 17 | extern crate router; 18 | extern crate staticfile; 19 | 20 | use iron::status; 21 | use iron::{Iron, Request, Response, IronResult}; 22 | 23 | use mount::Mount; 24 | use router::Router; 25 | use staticfile::Static; 26 | 27 | use std::path::Path; 28 | 29 | fn say_hello(req: &mut Request) -> IronResult { 30 | println!("Running send_hello handler, URL path: {}", req.url.path().join("/")); 31 | Ok(Response::with((status::Ok, "This request was routed!"))) 32 | } 33 | 34 | fn main() { 35 | let mut router = Router::new(); 36 | router 37 | .get("/hello", say_hello, "hello"); 38 | 39 | let mut mount = Mount::new(); 40 | mount 41 | .mount("/", router) 42 | .mount("/docs/", Static::new(Path::new("target/doc"))); 43 | 44 | Iron::new(mount).http("127.0.0.1:3000").unwrap(); 45 | } 46 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "staticfile"] 2 | #![deny(missing_docs)] 3 | #![deny(warnings)] 4 | 5 | //! Static file-serving handler. 6 | 7 | extern crate time; 8 | 9 | #[cfg(feature = "cache")] 10 | extern crate filetime; 11 | 12 | extern crate iron; 13 | extern crate mount; 14 | extern crate url; 15 | 16 | pub use static_handler::Static; 17 | #[cfg(feature = "cache")] 18 | pub use static_handler::Cache; 19 | 20 | mod requested_path; 21 | mod static_handler; 22 | -------------------------------------------------------------------------------- /src/requested_path.rs: -------------------------------------------------------------------------------- 1 | use iron::Request; 2 | use std::iter::FromIterator; 3 | use std::path::{Component, PathBuf, Path}; 4 | use std::fs::{self, Metadata}; 5 | use std::convert::AsRef; 6 | use url::percent_encoding::percent_decode; 7 | 8 | pub struct RequestedPath { 9 | pub path: PathBuf, 10 | } 11 | 12 | #[inline] 13 | fn decode_percents(string: &&str) -> String { 14 | percent_decode(string.as_bytes()).decode_utf8().unwrap().into_owned() 15 | } 16 | 17 | fn normalize_path(path: &Path) -> PathBuf { 18 | path.components().fold(PathBuf::new(), |mut result, p| { 19 | match p { 20 | Component::Normal(x) => { 21 | result.push(x); 22 | result 23 | } 24 | Component::ParentDir => { 25 | result.pop(); 26 | result 27 | }, 28 | _ => result 29 | } 30 | }) 31 | } 32 | 33 | impl RequestedPath { 34 | pub fn new>(root_path: P, request: &Request) -> RequestedPath { 35 | let decoded_req_path = PathBuf::from_iter(request.url.path().iter().map(decode_percents)); 36 | let mut result = root_path.as_ref().to_path_buf(); 37 | result.extend(&normalize_path(&decoded_req_path)); 38 | RequestedPath { path: result } 39 | } 40 | 41 | pub fn should_redirect(&self, metadata: &Metadata, request: &Request) -> bool { 42 | // As per servo/rust-url/serialize_path, URLs ending in a slash have an 43 | // empty string stored as the last component of their path. Rust-url 44 | // even ensures that url.path() is non-empty by appending a forward slash 45 | // to URLs like http://example.com 46 | // Some middleware may mutate the URL's path to violate this property, 47 | // so the empty list case is handled as a redirect. 48 | let has_trailing_slash = match request.url.path().last() { 49 | Some(&"") => true, 50 | _ => false, 51 | }; 52 | 53 | metadata.is_dir() && !has_trailing_slash 54 | } 55 | 56 | pub fn get_file(self, metadata: &Metadata) -> Option { 57 | if metadata.is_file() { 58 | return Some(self.path); 59 | } 60 | 61 | let index_path = self.path.join("index.html"); 62 | 63 | match fs::metadata(&index_path) { 64 | Ok(m) => 65 | if m.is_file() { 66 | Some(index_path) 67 | } else { 68 | None 69 | }, 70 | Err(_) => None, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/static_handler.rs: -------------------------------------------------------------------------------- 1 | use std::path::{PathBuf, Path}; 2 | use std::fs; 3 | use std::error::Error; 4 | use std::fmt; 5 | 6 | #[cfg(feature = "cache")] 7 | use time::{self, Timespec}; 8 | #[cfg(feature = "cache")] 9 | use std::time::Duration; 10 | 11 | use iron::prelude::*; 12 | use iron::{Handler, Url, status}; 13 | #[cfg(feature = "cache")] 14 | use iron::modifier::Modifier; 15 | use iron::modifiers::Redirect; 16 | use mount::OriginalUrl; 17 | use requested_path::RequestedPath; 18 | use url; 19 | 20 | /// The static file-serving `Handler`. 21 | /// 22 | /// This handler serves files from a single filesystem path, which may be absolute or relative. 23 | /// Incoming requests are mapped onto the filesystem by appending their URL path to the handler's 24 | /// root path. If the filesystem path corresponds to a regular file, the handler will attempt to 25 | /// serve it. Otherwise, if the path corresponds to a directory containing an `index.html`, 26 | /// the handler will attempt to serve that instead. 27 | /// 28 | /// ## Errors 29 | /// 30 | /// If the path doesn't match any real object in the filesystem, the handler will return 31 | /// a Response with `status::NotFound`. If an IO error occurs whilst attempting to serve 32 | /// a file, `FileError(IoError)` will be returned. 33 | #[derive(Clone)] 34 | pub struct Static { 35 | /// The path this handler is serving files from. 36 | pub root: PathBuf, 37 | #[cfg(feature = "cache")] 38 | cache: Option, 39 | } 40 | 41 | impl Static { 42 | /// Create a new instance of `Static` with a given root path. 43 | /// 44 | /// If `Path::new("")` is given, files will be served from the current directory. 45 | #[cfg(feature = "cache")] 46 | pub fn new>(root: P) -> Static { 47 | Static { 48 | root: root.into(), 49 | cache: None 50 | } 51 | } 52 | 53 | /// Create a new instance of `Static` with a given root path. 54 | /// 55 | /// If `Path::new("")` is given, files will be served from the current directory. 56 | #[cfg(not(feature = "cache"))] 57 | pub fn new>(root: P) -> Static { 58 | Static { 59 | root: root.into(), 60 | } 61 | } 62 | 63 | /// Specify the response's `cache-control` header with a given duration. Internally, this is 64 | /// a helper function to set a `Cache` on an instance of `Static`. 65 | /// 66 | /// ## Example 67 | /// 68 | /// ```ignore 69 | /// let cached_static_handler = Static::new(path).cache(Duration::from_secs(30*24*60*60)); 70 | /// ``` 71 | #[cfg(feature = "cache")] 72 | pub fn cache(self, duration: Duration) -> Static { 73 | self.set(Cache::new(duration)) 74 | } 75 | 76 | #[cfg(feature = "cache")] 77 | fn try_cache>(&self, req: &mut Request, path: P) -> IronResult { 78 | match self.cache { 79 | None => Ok(Response::with((status::Ok, path.as_ref()))), 80 | Some(ref cache) => cache.handle(req, path.as_ref()), 81 | } 82 | } 83 | } 84 | 85 | impl Handler for Static { 86 | fn handle(&self, req: &mut Request) -> IronResult { 87 | use std::io; 88 | 89 | let requested_path = RequestedPath::new(&self.root, req); 90 | 91 | let metadata = match fs::metadata(&requested_path.path) { 92 | Ok(meta) => meta, 93 | Err(e) => { 94 | let status = match e.kind() { 95 | io::ErrorKind::NotFound => status::NotFound, 96 | io::ErrorKind::PermissionDenied => status::Forbidden, 97 | _ => status::InternalServerError, 98 | }; 99 | 100 | return Err(IronError::new(e, status)) 101 | }, 102 | }; 103 | 104 | // If the URL ends in a slash, serve the file directly. 105 | // Otherwise, redirect to the directory equivalent of the URL. 106 | if requested_path.should_redirect(&metadata, req) { 107 | // Perform an HTTP 301 Redirect. 108 | let mut original_url: url::Url = match req.extensions.get::() { 109 | None => &req.url, 110 | Some(original_url) => original_url, 111 | }.clone().into(); 112 | 113 | // Append the trailing slash 114 | // 115 | // rust-url automatically turns an empty string in the last 116 | // slot in the path into a trailing slash. 117 | original_url.path_segments_mut().unwrap().push(""); 118 | let redirect_path = Url::from_generic_url(original_url).unwrap(); 119 | 120 | return Ok(Response::with((status::MovedPermanently, 121 | format!("Redirecting to {}", redirect_path), 122 | Redirect(redirect_path)))); 123 | } 124 | 125 | match requested_path.get_file(&metadata) { 126 | // If no file is found, return a 404 response. 127 | None => Err(IronError::new(NoFile, status::NotFound)), 128 | // Won't panic because we know the file exists from get_file. 129 | #[cfg(feature = "cache")] 130 | Some(path) => self.try_cache(req, path), 131 | #[cfg(not(feature = "cache"))] 132 | Some(path) => { 133 | let path: &Path = &path; 134 | Ok(Response::with((status::Ok, path))) 135 | }, 136 | } 137 | } 138 | } 139 | 140 | impl Set for Static {} 141 | 142 | /// A modifier for `Static` to specify a response's `cache-control`. 143 | #[cfg(feature = "cache")] 144 | #[derive(Clone)] 145 | pub struct Cache { 146 | /// The length of time the file should be cached for. 147 | pub duration: Duration, 148 | } 149 | 150 | #[cfg(feature = "cache")] 151 | impl Cache { 152 | /// Create a new instance of `Cache` with a given duration. 153 | pub fn new(duration: Duration) -> Cache { 154 | Cache { duration: duration } 155 | } 156 | 157 | fn handle>(&self, req: &mut Request, path: P) -> IronResult { 158 | use iron::headers::{IfModifiedSince, HttpDate}; 159 | 160 | let path = path.as_ref(); 161 | 162 | let (size, last_modified_time) = match fs::metadata(path) { 163 | Err(error) => return Err(IronError::new(error, status::InternalServerError)), 164 | Ok(metadata) => { 165 | use filetime::FileTime; 166 | 167 | let time = FileTime::from_last_modification_time(&metadata); 168 | (metadata.len(), Timespec::new(time.seconds() as i64, 0)) 169 | }, 170 | }; 171 | 172 | let if_modified_since = match req.headers.get::().cloned() { 173 | None => return self.response_with_cache(req, path, size, last_modified_time), 174 | Some(IfModifiedSince(HttpDate(time))) => time.to_timespec(), 175 | }; 176 | 177 | if last_modified_time <= if_modified_since { 178 | Ok(Response::with(status::NotModified)) 179 | } else { 180 | self.response_with_cache(req, path, size, last_modified_time) 181 | } 182 | } 183 | 184 | fn response_with_cache>(&self, 185 | req: &mut Request, 186 | path: P, 187 | size: u64, 188 | modified: Timespec) -> IronResult { 189 | use iron::headers::{CacheControl, LastModified, CacheDirective, HttpDate}; 190 | use iron::headers::{ContentLength, ContentType, ETag, EntityTag}; 191 | use iron::method::Method; 192 | use iron::mime::{Mime, TopLevel, SubLevel}; 193 | use iron::modifiers::Header; 194 | 195 | let seconds = self.duration.as_secs() as u32; 196 | let cache = vec![CacheDirective::Public, CacheDirective::MaxAge(seconds)]; 197 | let metadata = fs::metadata(path.as_ref()); 198 | 199 | let metadata = try!(metadata.map_err(|e| IronError::new(e, status::InternalServerError))); 200 | 201 | let mut response = if req.method == Method::Head { 202 | let has_ct = req.headers.get::(); 203 | let cont_type = match has_ct { 204 | None => ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![])), 205 | Some(t) => t.clone() 206 | }; 207 | Response::with((status::Ok, Header(cont_type), Header(ContentLength(metadata.len())))) 208 | } else { 209 | Response::with((status::Ok, path.as_ref())) 210 | }; 211 | 212 | response.headers.set(CacheControl(cache)); 213 | response.headers.set(LastModified(HttpDate(time::at(modified)))); 214 | response.headers.set(ETag(EntityTag::weak(format!("{0:x}-{1:x}.{2:x}", size, modified.sec, modified.nsec)))); 215 | 216 | Ok(response) 217 | } 218 | } 219 | 220 | #[cfg(feature = "cache")] 221 | impl Modifier for Cache { 222 | fn modify(self, static_handler: &mut Static) { 223 | static_handler.cache = Some(self); 224 | } 225 | } 226 | 227 | /// Thrown if no file is found. It is always accompanied by a NotFound response. 228 | #[derive(Debug)] 229 | pub struct NoFile; 230 | 231 | impl Error for NoFile { 232 | fn description(&self) -> &str { "File not found" } 233 | } 234 | 235 | impl fmt::Display for NoFile { 236 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 237 | f.write_str(self.description()) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /tests/cache.rs: -------------------------------------------------------------------------------- 1 | extern crate time; 2 | 3 | extern crate hyper; 4 | extern crate iron; 5 | extern crate iron_test; 6 | extern crate staticfile; 7 | 8 | #[cfg(feature = "cache")] 9 | mod cache { 10 | use time; 11 | use time::{Timespec}; 12 | 13 | #[cfg(feature = "cache")] 14 | use std::time::Duration; 15 | 16 | use iron::Headers; 17 | use iron::status::Status; 18 | use iron::headers::HttpDate; 19 | use hyper::header::{IfModifiedSince, CacheControl, CacheDirective, LastModified, ETag}; 20 | use iron_test::ProjectBuilder; 21 | use iron_test::request; 22 | use staticfile::Static; 23 | 24 | #[cfg(feature = "cache")] 25 | #[test] 26 | fn it_should_return_cache_headers() { 27 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 28 | p.build(); 29 | 30 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 31 | let iron_res = request::get("http://localhost:3000/file1.html", Headers::new(), &st); 32 | 33 | match iron_res { 34 | Ok(res) => { 35 | assert!(res.headers.get::().is_some()); 36 | assert!(res.headers.get::().is_some()); 37 | assert!(res.headers.get::().is_some()); 38 | let cache = res.headers.get::().unwrap(); 39 | let directives = vec![CacheDirective::Public, CacheDirective::MaxAge(2592000)]; 40 | assert_eq!(*cache, CacheControl(directives)); 41 | }, 42 | Err(e) => panic!("{}", e) 43 | } 44 | } 45 | 46 | #[cfg(feature = "cache")] 47 | #[test] 48 | fn it_should_return_the_file_if_client_sends_no_modified_time() { 49 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 50 | p.build(); 51 | 52 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 53 | let iron_res = request::get("http://localhost:3000/file1.html", Headers::new(), &st); 54 | 55 | match iron_res { 56 | Ok(res) => assert_eq!(res.status.unwrap(), Status::Ok), 57 | Err(e) => panic!("{}", e) 58 | } 59 | } 60 | 61 | #[cfg(feature = "cache")] 62 | #[test] 63 | fn it_should_return_the_file_if_client_has_old_version() { 64 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 65 | p.build(); 66 | 67 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 68 | 69 | let now = time::get_time(); 70 | let one_hour_ago = Timespec::new(now.sec - 3600, now.nsec); 71 | let mut headers = Headers::new(); 72 | headers.set(IfModifiedSince(HttpDate(time::at(one_hour_ago)))); 73 | let iron_res = request::get("http://localhost:3000/file1.html", headers, &st); 74 | 75 | match iron_res { 76 | Ok(res) => assert_eq!(res.status.unwrap(), Status::Ok), 77 | Err(e) => panic!("{}", e) 78 | } 79 | } 80 | 81 | #[cfg(feature = "cache")] 82 | #[test] 83 | fn it_should_return_304_if_client_has_file_cached() { 84 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 85 | p.build(); 86 | 87 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 88 | let mut headers = Headers::new(); 89 | headers.set(IfModifiedSince(HttpDate(time::now_utc()))); 90 | let iron_res = request::get("http://localhost:3000/file1.html", headers, &st); 91 | 92 | match iron_res { 93 | Ok(res) => assert_eq!(res.status.unwrap(), Status::NotModified), 94 | Err(e) => panic!("{}", e) 95 | } 96 | } 97 | 98 | #[cfg(feature = "cache")] 99 | #[test] 100 | fn it_should_cache_index_html_for_directory_path() { 101 | let p = ProjectBuilder::new("example").file("dir/index.html", "this is index"); 102 | p.build(); 103 | 104 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 105 | let mut headers = Headers::new(); 106 | headers.set(IfModifiedSince(HttpDate(time::now_utc()))); 107 | let iron_res = request::get("http://localhost:3000/dir/", headers, &st); 108 | 109 | match iron_res { 110 | Ok(res) => assert_eq!(res.status.unwrap(), Status::NotModified), 111 | Err(e) => panic!("{}", e) 112 | } 113 | } 114 | 115 | #[cfg(feature = "cache")] 116 | #[test] 117 | fn it_should_defer_to_static_handler_if_directory_misses_trailing_slash() { 118 | let p = ProjectBuilder::new("example").file("dir/index.html", "this is index"); 119 | p.build(); 120 | 121 | let st = Static::new(p.root().clone()).cache(Duration::from_secs(30*24*60*60)); 122 | let mut headers = Headers::new(); 123 | headers.set(IfModifiedSince(HttpDate(time::now_utc()))); 124 | let iron_res = request::get("http://localhost:3000/dir", headers, &st); 125 | 126 | match iron_res { 127 | Ok(res) => { 128 | assert_eq!(res.status.unwrap(), Status::MovedPermanently); 129 | assert!(res.headers.get::().is_none()); 130 | }, 131 | Err(e) => panic!("{}", e) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/static.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate iron; 3 | extern crate iron_test; 4 | extern crate staticfile; 5 | 6 | use iron::headers::{Headers, Location}; 7 | use iron::status::Status; 8 | 9 | use iron_test::{request, ProjectBuilder}; 10 | 11 | use staticfile::Static; 12 | 13 | use std::str; 14 | 15 | #[test] 16 | fn serves_non_default_file_from_absolute_root_path() { 17 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 18 | p.build(); 19 | let st = Static::new(p.root().clone()); 20 | match request::get("http://localhost:3000/file1.html", Headers::new(), &st) { 21 | Ok(res) => { 22 | let mut body = Vec::new(); 23 | res.body.unwrap().write_body(&mut body).unwrap(); 24 | assert_eq!(str::from_utf8(&body).unwrap(), "this is file1"); 25 | }, 26 | Err(e) => panic!("{}", e) 27 | } 28 | } 29 | 30 | #[test] 31 | fn serves_default_file_from_absolute_root_path() { 32 | let p = ProjectBuilder::new("example").file("index.html", "this is index"); 33 | p.build(); 34 | let st = Static::new(p.root().clone()); 35 | match request::get("http://localhost:3000/index.html", Headers::new(), &st) { 36 | Ok(res) => { 37 | let mut body = Vec::new(); 38 | res.body.unwrap().write_body(&mut body).unwrap(); 39 | assert_eq!(str::from_utf8(&body).unwrap(), "this is index"); 40 | }, 41 | Err(e) => panic!("{}", e) 42 | } 43 | } 44 | 45 | #[test] 46 | fn returns_404_if_file_not_found() { 47 | let p = ProjectBuilder::new("example"); 48 | p.build(); 49 | let st = Static::new(p.root().clone()); 50 | match request::get("http://localhost:3000", Headers::new(), &st) { 51 | Ok(res) => panic!("Expected IronError, got Response: {}", res), 52 | Err(e) => assert_eq!(e.response.status.unwrap(), Status::NotFound) 53 | } 54 | } 55 | 56 | #[test] 57 | fn redirects_if_trailing_slash_is_missing() { 58 | let p = ProjectBuilder::new("example").file("dir/index.html", "this is index"); 59 | p.build(); 60 | 61 | let st = Static::new(p.root().clone()); 62 | match request::get("http://localhost:3000/dir", Headers::new(), &st) { 63 | Ok(res) => { 64 | assert_eq!(res.status.unwrap(), Status::MovedPermanently); 65 | assert_eq!(res.headers.get::().unwrap(), 66 | &Location("http://localhost:3000/dir/".to_string())); 67 | }, 68 | Err(e) => panic!("{}", e) 69 | } 70 | } 71 | 72 | #[test] 73 | fn decodes_percent_notation() { 74 | let p = ProjectBuilder::new("example").file("has space.html", "file with funky chars"); 75 | p.build(); 76 | let st = Static::new(p.root().clone()); 77 | match request::get("http://localhost:3000/has space.html", Headers::new(), &st) { 78 | Ok(res) => { 79 | let mut body = Vec::new(); 80 | res.body.unwrap().write_body(&mut body).unwrap(); 81 | assert_eq!(str::from_utf8(&body).unwrap(), "file with funky chars"); 82 | }, 83 | Err(e) => panic!("{}", e) 84 | } 85 | } 86 | 87 | #[test] 88 | fn normalizes_path() { 89 | let p = ProjectBuilder::new("example").file("index.html", "this is index"); 90 | p.build(); 91 | let st = Static::new(p.root().clone()); 92 | match request::get("http://localhost:3000/xxx/../index.html", Headers::new(), &st) { 93 | Ok(res) => { 94 | let mut body = Vec::new(); 95 | res.body.unwrap().write_body(&mut body).unwrap(); 96 | assert_eq!(str::from_utf8(&body).unwrap(), "this is index"); 97 | }, 98 | Err(e) => panic!("{}", e) 99 | } 100 | } 101 | 102 | #[test] 103 | fn normalizes_percent_encoded_path() { 104 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 105 | p.build(); 106 | let st = Static::new(p.root().clone()); 107 | match request::get("http://localhost:3000/xxx/..%2ffile1.html", Headers::new(), &st) { 108 | Ok(res) => { 109 | let mut body = Vec::new(); 110 | res.body.unwrap().write_body(&mut body).unwrap(); 111 | assert_eq!(str::from_utf8(&body).unwrap(), "this is file1"); 112 | }, 113 | Err(e) => panic!("{}", e) 114 | } 115 | } 116 | 117 | #[test] 118 | fn prevents_from_escaping_root() { 119 | let p = ProjectBuilder::new("example").file("file1.html", "this is file1"); 120 | p.build(); 121 | let st = Static::new(p.root().clone()); 122 | 123 | match request::get("http://localhost:3000/../file1.html", Headers::new(), &st) { 124 | Ok(res) => { 125 | let mut body = Vec::new(); 126 | res.body.unwrap().write_body(&mut body).unwrap(); 127 | assert_eq!(str::from_utf8(&body).unwrap(), "this is file1"); 128 | }, 129 | Err(e) => panic!("{}", e) 130 | } 131 | 132 | match request::get("http://localhost:3000/..%2ffile1.html", Headers::new(), &st) { 133 | Ok(res) => { 134 | let mut body = Vec::new(); 135 | res.body.unwrap().write_body(&mut body).unwrap(); 136 | assert_eq!(str::from_utf8(&body).unwrap(), "this is file1"); 137 | }, 138 | Err(e) => panic!("{}", e) 139 | } 140 | 141 | match request::get("http://localhost:3000/xxx/..%2f..%2ffile1.html", Headers::new(), &st) { 142 | Ok(res) => { 143 | let mut body = Vec::new(); 144 | res.body.unwrap().write_body(&mut body).unwrap(); 145 | assert_eq!(str::from_utf8(&body).unwrap(), "this is file1"); 146 | }, 147 | Err(e) => panic!("{}", e) 148 | } 149 | 150 | } 151 | --------------------------------------------------------------------------------