diff --git a/examples/doc_server.rs b/examples/doc_server.rs index ee56d58..7b1cbb9 100644 --- a/examples/doc_server.rs +++ b/examples/doc_server.rs @@ -25,5 +25,5 @@ fn main() { println!("Doc server running on http://localhost:3000/doc/"); - Iron::new(mount).http("127.0.0.1:3000").unwrap(); + Iron::new(mount).http("127.0.0.1:3000"); } diff --git a/examples/router.rs b/examples/router.rs index e645963..4e8975f 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -17,7 +17,7 @@ extern crate mount; extern crate router; extern crate staticfile; -use iron::status; +use iron::StatusCode; use iron::{Iron, Request, Response, IronResult}; use mount::Mount; @@ -28,7 +28,7 @@ use std::path::Path; fn say_hello(req: &mut Request) -> IronResult { println!("Running send_hello handler, URL path: {}", req.url.path().join("/")); - Ok(Response::with((status::Ok, "This request was routed!"))) + Ok(Response::with((StatusCode::OK, "This request was routed!"))) } fn main() { @@ -41,5 +41,5 @@ fn main() { .mount("/", router) .mount("/docs/", Static::new(Path::new("target/doc"))); - Iron::new(mount).http("127.0.0.1:3000").unwrap(); + Iron::new(mount).http("127.0.0.1:3000"); } diff --git a/src/httpdate.rs b/src/httpdate.rs new file mode 100644 index 0000000..71b6bcd --- /dev/null +++ b/src/httpdate.rs @@ -0,0 +1,120 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use time; + +/// A timestamp with HTTP formatting and parsing +// Prior to 1995, there were three different formats commonly used by +// servers to communicate timestamps. For compatibility with old +// implementations, all three are defined here. The preferred format is +// a fixed-length and single-zone subset of the date and time +// specification used by the Internet Message Format [RFC5322]. +// +// HTTP-date = IMF-fixdate / obs-date +// +// An example of the preferred format is +// +// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate +// +// Examples of the two obsolete formats are +// +// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format +// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format +// +// A recipient that parses a timestamp value in an HTTP header field +// MUST accept all three HTTP-date formats. When a sender generates a +// header field that contains one or more timestamps defined as +// HTTP-date, the sender MUST generate those timestamps in the +// IMF-fixdate format. +// +// This code is based on `src/hyper/header/shared/httpdate.rs` from +// hyper 0.11 (https://github.com/hyperium/hyper) +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HttpDate(pub time::Tm); + +impl FromStr for HttpDate { + type Err = (); + fn from_str(s: &str) -> Result { + match time::strptime(s, "%a, %d %b %Y %T %Z").or_else(|_| { + time::strptime(s, "%A, %d-%b-%y %T %Z") + }).or_else(|_| { + time::strptime(s, "%c") + }) { + Ok(t) => Ok(HttpDate(t)), + Err(_) => Err(()), + } + } +} + +impl Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + } +} + +impl From for HttpDate { + fn from(sys: SystemTime) -> HttpDate { + let tmspec = match sys.duration_since(UNIX_EPOCH) { + Ok(dur) => { + time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) + }, + Err(err) => { + let neg = err.duration(); + time::Timespec::new(-(neg.as_secs() as i64), -(neg.subsec_nanos() as i32)) + }, + }; + HttpDate(time::at_utc(tmspec)) + } +} + +impl From for SystemTime { + fn from(date: HttpDate) -> SystemTime { + let spec = date.0.to_timespec(); + if spec.sec >= 0 { + UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) + } else { + UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) + } + } +} + +#[cfg(test)] +mod tests { + use time::Tm; + use super::HttpDate; + + const NOV_07: HttpDate = HttpDate(Tm { + tm_nsec: 0, + tm_sec: 37, + tm_min: 48, + tm_hour: 8, + tm_mday: 7, + tm_mon: 10, + tm_year: 94, + tm_wday: 0, + tm_isdst: 0, + tm_yday: 0, + tm_utcoff: 0, + }); + + #[test] + fn test_imf_fixdate() { + assert_eq!("Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), NOV_07); + } + + #[test] + fn test_rfc_850() { + assert_eq!("Sunday, 07-Nov-94 08:48:37 GMT".parse::().unwrap(), NOV_07); + } + + #[test] + fn test_asctime() { + assert_eq!("Sun Nov 7 08:48:37 1994".parse::().unwrap(), NOV_07); + } + + #[test] + fn test_no_date() { + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 00fada8..a12e113 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,4 @@ pub use static_handler::Cache; mod requested_path; mod static_handler; +mod httpdate; diff --git a/src/static_handler.rs b/src/static_handler.rs index 5663e9a..1d57e8a 100644 --- a/src/static_handler.rs +++ b/src/static_handler.rs @@ -8,8 +8,8 @@ use time::{self, Timespec}; #[cfg(feature = "cache")] use std::time::Duration; +use iron::{Handler, Url, StatusCode}; use iron::prelude::*; -use iron::{Handler, Url, status}; #[cfg(feature = "cache")] use iron::modifier::Modifier; use iron::modifiers::Redirect; @@ -28,7 +28,7 @@ use url; /// ## Errors /// /// If the path doesn't match any real object in the filesystem, the handler will return -/// a Response with `status::NotFound`. If an IO error occurs whilst attempting to serve +/// a Response with `StatusCode::NOT_FOUND`. If an IO error occurs whilst attempting to serve /// a file, `FileError(IoError)` will be returned. #[derive(Clone)] pub struct Static { @@ -76,7 +76,7 @@ impl Static { #[cfg(feature = "cache")] fn try_cache>(&self, req: &mut Request, path: P) -> IronResult { match self.cache { - None => Ok(Response::with((status::Ok, path.as_ref()))), + None => Ok(Response::with((StatusCode::OK, path.as_ref()))), Some(ref cache) => cache.handle(req, path.as_ref()), } } @@ -92,9 +92,9 @@ impl Handler for Static { Ok(meta) => meta, Err(e) => { let status = match e.kind() { - io::ErrorKind::NotFound => status::NotFound, - io::ErrorKind::PermissionDenied => status::Forbidden, - _ => status::InternalServerError, + io::ErrorKind::NotFound => StatusCode::NOT_FOUND, + io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, + _ => StatusCode::INTERNAL_SERVER_ERROR, }; return Err(IronError::new(e, status)) @@ -117,21 +117,21 @@ impl Handler for Static { original_url.path_segments_mut().unwrap().push(""); let redirect_path = Url::from_generic_url(original_url).unwrap(); - return Ok(Response::with((status::MovedPermanently, + return Ok(Response::with((StatusCode::MOVED_PERMANENTLY, format!("Redirecting to {}", redirect_path), Redirect(redirect_path)))); } match requested_path.get_file(&metadata) { // If no file is found, return a 404 response. - None => Err(IronError::new(NoFile, status::NotFound)), + None => Err(IronError::new(NoFile, StatusCode::NOT_FOUND)), // Won't panic because we know the file exists from get_file. #[cfg(feature = "cache")] Some(path) => self.try_cache(req, path), #[cfg(not(feature = "cache"))] Some(path) => { let path: &Path = &path; - Ok(Response::with((status::Ok, path))) + Ok(Response::with((StatusCode::OK, path))) }, } } @@ -155,12 +155,14 @@ impl Cache { } fn handle>(&self, req: &mut Request, path: P) -> IronResult { - use iron::headers::{IfModifiedSince, HttpDate}; + use iron::headers::IF_MODIFIED_SINCE; + use httpdate::HttpDate; + use std::str::FromStr; let path = path.as_ref(); let (size, last_modified_time) = match fs::metadata(path) { - Err(error) => return Err(IronError::new(error, status::InternalServerError)), + Err(error) => return Err(IronError::new(error, StatusCode::INTERNAL_SERVER_ERROR)), Ok(metadata) => { use filetime::FileTime; @@ -169,13 +171,14 @@ impl Cache { }, }; - let if_modified_since = match req.headers.get::().cloned() { + let if_modified_since = match req.headers.get(IF_MODIFIED_SINCE).cloned() { None => return self.response_with_cache(req, path, size, last_modified_time), - Some(IfModifiedSince(HttpDate(time))) => time.to_timespec(), + // TODO: Error handling for to_str() & from_str() + Some(time) => HttpDate::from_str(time.to_str().unwrap()).unwrap().0.to_timespec(), }; if last_modified_time <= if_modified_since { - Ok(Response::with(status::NotModified)) + Ok(Response::with(StatusCode::NOT_MODIFIED)) } else { self.response_with_cache(req, path, size, last_modified_time) } @@ -186,32 +189,35 @@ impl Cache { path: P, size: u64, modified: Timespec) -> IronResult { - use iron::headers::{CacheControl, LastModified, CacheDirective, HttpDate}; - use iron::headers::{ContentLength, ContentType, ETag, EntityTag}; + use iron::headers::{CACHE_CONTROL, LAST_MODIFIED, CONTENT_LENGTH, CONTENT_TYPE, ETAG}; + use httpdate::HttpDate; use iron::method::Method; - use iron::mime::{Mime, TopLevel, SubLevel}; - use iron::modifiers::Header; + use iron::mime; let seconds = self.duration.as_secs() as u32; - let cache = vec![CacheDirective::Public, CacheDirective::MaxAge(seconds)]; let metadata = fs::metadata(path.as_ref()); - let metadata = try!(metadata.map_err(|e| IronError::new(e, status::InternalServerError))); + let metadata = try!(metadata.map_err(|e| IronError::new(e, StatusCode::INTERNAL_SERVER_ERROR))); - let mut response = if req.method == Method::Head { - let has_ct = req.headers.get::(); + let mut response = if req.method == Method::HEAD { + let has_ct = req.headers.get(CONTENT_TYPE); let cont_type = match has_ct { - None => ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![])), + None => mime::TEXT_PLAIN.as_ref().parse().unwrap(), Some(t) => t.clone() }; - Response::with((status::Ok, Header(cont_type), Header(ContentLength(metadata.len())))) + let mut response = Response::with(StatusCode::OK); + response.headers.insert(CONTENT_TYPE, cont_type); + response.headers.insert(CONTENT_LENGTH, metadata.len().to_string().parse().unwrap()); + response + } else { - Response::with((status::Ok, path.as_ref())) + Response::with((StatusCode::OK, path.as_ref())) }; - response.headers.set(CacheControl(cache)); - response.headers.set(LastModified(HttpDate(time::at(modified)))); - response.headers.set(ETag(EntityTag::weak(format!("{0:x}-{1:x}.{2:x}", size, modified.sec, modified.nsec)))); + response.headers.insert(CACHE_CONTROL, "public".parse().unwrap());; + response.headers.insert(CACHE_CONTROL, format!("max-age={}", seconds).parse().unwrap()); + response.headers.insert(LAST_MODIFIED, format!("{}",HttpDate(time::at(modified))).parse().unwrap()); + response.headers.insert(ETAG, format!("{0:x}-{1:x}.{2:x}", size, modified.sec, modified.nsec).parse().unwrap()); Ok(response) }