mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-04-01 10:13:43 -04:00
replace resource.rs with new http-entity crate
This crate is a slightly-more-polished and MIT-licensed version of resource.rs. So far it has one advantage: running the tests doesn't require RUST_TEST_THREADS=1.
This commit is contained in:
parent
86dd36d7a5
commit
fee4141dc6
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)",
|
"ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)",
|
||||||
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity)",
|
||||||
"hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
"hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -186,6 +187,17 @@ dependencies = [
|
|||||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-entity"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = "git+https://github.com/scottlamb/http-entity#49e13e116ffc84f518f39c8a2e57d8066a1f3387"
|
||||||
|
dependencies = [
|
||||||
|
"hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -833,6 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum gcc 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "771e4a97ff6f237cf0f7d5f5102f6e28bb9743814b6198d684da5c58b76c11e0"
|
"checksum gcc 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "771e4a97ff6f237cf0f7d5f5102f6e28bb9743814b6198d684da5c58b76c11e0"
|
||||||
"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
|
"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
|
||||||
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
|
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
|
||||||
|
"checksum http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity)" = "<none>"
|
||||||
"checksum httparse 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8abece705b1d32c478f49447b3a575cd07f6e362ff12518f2ee2c9b9ced64e"
|
"checksum httparse 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8abece705b1d32c478f49447b3a575cd07f6e362ff12518f2ee2c9b9ced64e"
|
||||||
"checksum hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "86ea0c0ff7e6ef09eff72234800ddb48b6263277936e7ecd6ecd3250345d705f"
|
"checksum hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "86ea0c0ff7e6ef09eff72234800ddb48b6263277936e7ecd6ecd3250345d705f"
|
||||||
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
|
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
|
||||||
|
@ -13,6 +13,7 @@ chan = "0.1"
|
|||||||
chan-signal = "0.1"
|
chan-signal = "0.1"
|
||||||
docopt = "0.6"
|
docopt = "0.6"
|
||||||
fnv = "1.0"
|
fnv = "1.0"
|
||||||
|
http-entity = { git = "https://github.com/scottlamb/http-entity" }
|
||||||
hyper = "0.9"
|
hyper = "0.9"
|
||||||
lazy_static = "0.2"
|
lazy_static = "0.2"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
@ -139,7 +139,7 @@ For instructions, you can skip to "[Camera configuration and hard disk mounting]
|
|||||||
|
|
||||||
Once prerequisites are installed, Moonfire NVR can be built as follows:
|
Once prerequisites are installed, Moonfire NVR can be built as follows:
|
||||||
|
|
||||||
$ RUST_TEST_THREADS=1 cargo test
|
$ cargo test
|
||||||
$ cargo build --release
|
$ cargo build --release
|
||||||
$ sudo install -m 755 target/release/moonfire-nvr /usr/local/bin
|
$ sudo install -m 755 target/release/moonfire-nvr /usr/local/bin
|
||||||
|
|
||||||
|
2
prep.sh
2
prep.sh
@ -158,7 +158,7 @@ if [ ! -x "${SERVICE_BIN}" ]; then
|
|||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! RUST_TEST_THREADS=1 cargo test; then
|
if ! cargo test; then
|
||||||
echo "test failed. Try to run the following manually for more info"
|
echo "test failed. Try to run the following manually for more info"
|
||||||
echo "RUST_TEST_THREADS=1 cargo test --verbose"
|
echo "RUST_TEST_THREADS=1 cargo test --verbose"
|
||||||
echo
|
echo
|
||||||
|
@ -38,6 +38,7 @@ extern crate docopt;
|
|||||||
#[macro_use] extern crate ffmpeg;
|
#[macro_use] extern crate ffmpeg;
|
||||||
extern crate ffmpeg_sys;
|
extern crate ffmpeg_sys;
|
||||||
extern crate fnv;
|
extern crate fnv;
|
||||||
|
extern crate http_entity;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
#[macro_use] extern crate lazy_static;
|
#[macro_use] extern crate lazy_static;
|
||||||
extern crate libc;
|
extern crate libc;
|
||||||
@ -76,7 +77,6 @@ mod mmapfile;
|
|||||||
mod mp4;
|
mod mp4;
|
||||||
mod pieces;
|
mod pieces;
|
||||||
mod recording;
|
mod recording;
|
||||||
mod resource;
|
|
||||||
mod stream;
|
mod stream;
|
||||||
mod streamer;
|
mod streamer;
|
||||||
mod strutil;
|
mod strutil;
|
||||||
|
@ -38,7 +38,7 @@ use std::io;
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
/// Memory-mapped file slice.
|
/// Memory-mapped file slice.
|
||||||
/// This struct is meant to be used in constructing an implementation of the `resource::Resource`
|
/// This struct is meant to be used in constructing an implementation of the `http_entity::Entity`
|
||||||
/// or `pieces::ContextWriter` traits. The file in question should be immutable, as files shrinking
|
/// or `pieces::ContextWriter` traits. The file in question should be immutable, as files shrinking
|
||||||
/// during `mmap` will cause the process to fail with `SIGBUS`. Moonfire NVR sample files satisfy
|
/// during `mmap` will cause the process to fail with `SIGBUS`. Moonfire NVR sample files satisfy
|
||||||
/// this requirement:
|
/// this requirement:
|
||||||
|
17
src/mp4.rs
17
src/mp4.rs
@ -83,6 +83,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
|||||||
use db;
|
use db;
|
||||||
use dir;
|
use dir;
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
|
use http_entity;
|
||||||
use hyper::header;
|
use hyper::header;
|
||||||
use mmapfile;
|
use mmapfile;
|
||||||
use mime;
|
use mime;
|
||||||
@ -91,7 +92,6 @@ use pieces;
|
|||||||
use pieces::ContextWriter;
|
use pieces::ContextWriter;
|
||||||
use pieces::Slices;
|
use pieces::Slices;
|
||||||
use recording::{self, TIME_UNITS_PER_SEC};
|
use recording::{self, TIME_UNITS_PER_SEC};
|
||||||
use resource;
|
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
@ -1153,7 +1153,7 @@ impl Mp4File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl resource::Resource for Mp4File {
|
impl http_entity::Entity<Error> for Mp4File {
|
||||||
fn content_type(&self) -> mime::Mime { "video/mp4".parse().unwrap() }
|
fn content_type(&self) -> mime::Mime { "video/mp4".parse().unwrap() }
|
||||||
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
||||||
fn etag(&self) -> Option<&header::EntityTag> { Some(&self.etag) }
|
fn etag(&self) -> Option<&header::EntityTag> { Some(&self.etag) }
|
||||||
@ -1181,11 +1181,12 @@ mod tests {
|
|||||||
use db;
|
use db;
|
||||||
use dir;
|
use dir;
|
||||||
use ffmpeg;
|
use ffmpeg;
|
||||||
|
use error::Error;
|
||||||
#[cfg(nightly)] use hyper;
|
#[cfg(nightly)] use hyper;
|
||||||
use hyper::header;
|
use hyper::header;
|
||||||
use openssl::crypto::hash;
|
use openssl::crypto::hash;
|
||||||
use recording::{self, TIME_UNITS_PER_SEC};
|
use recording::{self, TIME_UNITS_PER_SEC};
|
||||||
use resource::{self, Resource};
|
use http_entity::{self, Entity};
|
||||||
#[cfg(nightly)] use self::test::Bencher;
|
#[cfg(nightly)] use self::test::Bencher;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -1217,7 +1218,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SHA-1 digest of the given `Resource`.
|
/// Returns the SHA-1 digest of the given `Resource`.
|
||||||
fn digest(r: &Resource) -> Vec<u8> {
|
fn digest(r: &http_entity::Entity<Error>) -> Vec<u8> {
|
||||||
let mut sha1 = Sha1::new();
|
let mut sha1 = Sha1::new();
|
||||||
r.write_to(0 .. r.len(), &mut sha1).unwrap();
|
r.write_to(0 .. r.len(), &mut sha1).unwrap();
|
||||||
sha1.finish()
|
sha1.finish()
|
||||||
@ -1236,12 +1237,12 @@ mod tests {
|
|||||||
/// stack, not backward. Panics on error.
|
/// stack, not backward. Panics on error.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct BoxCursor<'a> {
|
struct BoxCursor<'a> {
|
||||||
mp4: &'a resource::Resource,
|
mp4: &'a http_entity::Entity<Error>,
|
||||||
stack: Vec<Mp4Box>,
|
stack: Vec<Mp4Box>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> BoxCursor<'a> {
|
impl<'a> BoxCursor<'a> {
|
||||||
pub fn new(mp4: &'a resource::Resource) -> BoxCursor<'a> {
|
pub fn new(mp4: &'a http_entity::Entity<Error>) -> BoxCursor<'a> {
|
||||||
BoxCursor{
|
BoxCursor{
|
||||||
mp4: mp4,
|
mp4: mp4,
|
||||||
stack: Vec::new(),
|
stack: Vec::new(),
|
||||||
@ -1346,7 +1347,7 @@ mod tests {
|
|||||||
|
|
||||||
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
||||||
/// exist.
|
/// exist.
|
||||||
fn find_track(mp4: & resource::Resource, track_id: u32) -> Track {
|
fn find_track(mp4: &http_entity::Entity<Error>, track_id: u32) -> Track {
|
||||||
let mut cursor = BoxCursor::new(mp4);
|
let mut cursor = BoxCursor::new(mp4);
|
||||||
cursor.down();
|
cursor.down();
|
||||||
assert!(cursor.find(b"moov"));
|
assert!(cursor.find(b"moov"));
|
||||||
@ -1766,7 +1767,7 @@ mod tests {
|
|||||||
let (db, dir) = (db.db.clone(), db.dir.clone());
|
let (db, dir) = (db.db.clone(), db.dir.clone());
|
||||||
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
||||||
let mp4 = create_mp4_from_db(db.clone(), dir.clone(), 0, 0, false);
|
let mp4 = create_mp4_from_db(db.clone(), dir.clone(), 0, 0, false);
|
||||||
resource::serve(&mp4, &req, res).unwrap();
|
http_entity::serve(&mp4, &req, res).unwrap();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
BenchServer{
|
BenchServer{
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//! Tools for implementing a `resource::Resource` body composed from many "slices".
|
//! Tools for implementing a `http_entity::Entity` body composed from many "slices".
|
||||||
|
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -50,7 +50,7 @@ struct SliceInfo<W> {
|
|||||||
/// Writes a byte range to the given `io::Write` given a context argument; meant for use with
|
/// Writes a byte range to the given `io::Write` given a context argument; meant for use with
|
||||||
/// `Slices`.
|
/// `Slices`.
|
||||||
pub trait ContextWriter<Ctx> {
|
pub trait ContextWriter<Ctx> {
|
||||||
/// Writes `r` to `out`, as in `resource::Resource::write_to`.
|
/// Writes `r` to `out`, as in `http_entity::Entity::write_to`.
|
||||||
/// The additional argument `ctx` is as supplied to the `Slices`.
|
/// The additional argument `ctx` is as supplied to the `Slices`.
|
||||||
/// The additional argument `l` is the length of this slice, as determined by the `Slices`.
|
/// The additional argument `l` is the length of this slice, as determined by the `Slices`.
|
||||||
fn write_to(&self, ctx: &Ctx, r: Range<u64>, l: u64, out: &mut io::Write) -> Result<()>;
|
fn write_to(&self, ctx: &Ctx, r: Range<u64>, l: u64, out: &mut io::Write) -> Result<()>;
|
||||||
@ -122,7 +122,7 @@ impl<W, C> Slices<W, C> where W: ContextWriter<C> {
|
|||||||
pub fn num(&self) -> usize { self.slices.len() }
|
pub fn num(&self) -> usize { self.slices.len() }
|
||||||
|
|
||||||
/// Writes `range` to `out`.
|
/// Writes `range` to `out`.
|
||||||
/// This interface mirrors `resource::Resource::write_to`, with the additional `ctx` argument.
|
/// This interface mirrors `http_entity::Entity::write_to`, with the additional `ctx` argument.
|
||||||
pub fn write_to(&self, ctx: &C, range: Range<u64>, out: &mut io::Write) -> Result<()> {
|
pub fn write_to(&self, ctx: &C, range: Range<u64>, out: &mut io::Write) -> Result<()> {
|
||||||
if range.start > range.end || range.end > self.len {
|
if range.start > range.end || range.end > self.len {
|
||||||
return Err(Error{
|
return Err(Error{
|
||||||
|
760
src/resource.rs
760
src/resource.rs
@ -1,760 +0,0 @@
|
|||||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
|
||||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// In addition, as a special exception, the copyright holders give
|
|
||||||
// permission to link the code of portions of this program with the
|
|
||||||
// OpenSSL library under certain conditions as described in each
|
|
||||||
// individual source file, and distribute linked combinations including
|
|
||||||
// the two.
|
|
||||||
//
|
|
||||||
// You must obey the GNU General Public License in all respects for all
|
|
||||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
||||||
// exception, you may extend this exception to your version of the
|
|
||||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
||||||
// so, delete this exception statement from your version. If you delete
|
|
||||||
// this exception statement from all source files in the program, then
|
|
||||||
// also delete it here.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
extern crate core;
|
|
||||||
extern crate hyper;
|
|
||||||
extern crate time;
|
|
||||||
|
|
||||||
use error::Result;
|
|
||||||
use hyper::server::{Request, Response};
|
|
||||||
use hyper::header;
|
|
||||||
use hyper::method::Method;
|
|
||||||
use hyper::net::Fresh;
|
|
||||||
use mime;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::cmp;
|
|
||||||
use std::io;
|
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
/// An HTTP resource for GET and HEAD serving.
|
|
||||||
pub trait Resource {
|
|
||||||
/// Returns the length of the slice in bytes.
|
|
||||||
fn len(&self) -> u64;
|
|
||||||
|
|
||||||
/// Writes bytes within this slice indicated by `range` to `out.`
|
|
||||||
/// TODO: different result type?
|
|
||||||
fn write_to(&self, range: Range<u64>, out: &mut io::Write) -> Result<()>;
|
|
||||||
|
|
||||||
fn content_type(&self) -> mime::Mime;
|
|
||||||
fn etag(&self) -> Option<&header::EntityTag>;
|
|
||||||
fn last_modified(&self) -> &header::HttpDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
enum ResolvedRanges {
|
|
||||||
AbsentOrInvalid,
|
|
||||||
NotSatisfiable,
|
|
||||||
Satisfiable(SmallVec<[Range<u64>; 1]>)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_range_header(range: Option<&header::Range>, resource_len: u64) -> ResolvedRanges {
|
|
||||||
if let Some(&header::Range::Bytes(ref byte_ranges)) = range {
|
|
||||||
let mut ranges: SmallVec<[Range<u64>; 1]> = SmallVec::new();
|
|
||||||
for range in byte_ranges {
|
|
||||||
match *range {
|
|
||||||
header::ByteRangeSpec::FromTo(range_from, range_to) => {
|
|
||||||
let end = cmp::min(range_to + 1, resource_len);
|
|
||||||
if range_from >= end {
|
|
||||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ranges.push(Range{start: range_from, end: end});
|
|
||||||
},
|
|
||||||
header::ByteRangeSpec::AllFrom(range_from) => {
|
|
||||||
if range_from >= resource_len {
|
|
||||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ranges.push(Range{start: range_from, end: resource_len});
|
|
||||||
},
|
|
||||||
header::ByteRangeSpec::Last(last) => {
|
|
||||||
if last >= resource_len {
|
|
||||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ranges.push(Range{start: resource_len - last,
|
|
||||||
end: resource_len});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ranges.is_empty() {
|
|
||||||
debug!("Ranges {:?} all satisfiable with length {:?}", range, resource_len);
|
|
||||||
return ResolvedRanges::Satisfiable(ranges);
|
|
||||||
}
|
|
||||||
return ResolvedRanges::NotSatisfiable;
|
|
||||||
}
|
|
||||||
ResolvedRanges::AbsentOrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
|
||||||
fn none_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
|
||||||
match req.headers.get::<header::IfNoneMatch>() {
|
|
||||||
Some(&header::IfNoneMatch::Any) => false,
|
|
||||||
Some(&header::IfNoneMatch::Items(ref items)) => {
|
|
||||||
if let Some(some_etag) = etag {
|
|
||||||
for item in items {
|
|
||||||
if item.weak_eq(some_etag) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
},
|
|
||||||
None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
|
||||||
fn any_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
|
||||||
match req.headers.get::<header::IfMatch>() {
|
|
||||||
// The absent header and "If-Match: *" cases differ only when there is no entity to serve.
|
|
||||||
// We always have an entity to serve, so consider them identical.
|
|
||||||
None | Some(&header::IfMatch::Any) => true,
|
|
||||||
Some(&header::IfMatch::Items(ref items)) => {
|
|
||||||
if let Some(some_etag) = etag {
|
|
||||||
for item in items {
|
|
||||||
if item.strong_eq(some_etag) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serves GET and HEAD requests for a given byte-ranged resource.
|
|
||||||
/// Handles conditional & subrange requests.
|
|
||||||
/// The caller is expected to have already determined the correct resource and appended
|
|
||||||
/// Expires, Cache-Control, and Vary headers.
|
|
||||||
///
|
|
||||||
/// TODO: is it appropriate to include those headers on all response codes used in this function?
|
|
||||||
///
|
|
||||||
/// TODO: check HTTP rules about weak vs strong comparisons with range requests. I don't think I'm
|
|
||||||
/// doing this correctly.
|
|
||||||
pub fn serve(rsrc: &Resource, req: &Request, mut res: Response<Fresh>) -> Result<()> {
|
|
||||||
if req.method != Method::Get && req.method != Method::Head {
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::MethodNotAllowed;
|
|
||||||
res.headers_mut().set(header::ContentType(mime!(Text/Plain)));
|
|
||||||
res.headers_mut().set(header::Allow(vec![Method::Get, Method::Head]));
|
|
||||||
res.send(b"This resource only supports GET and HEAD.")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_modified = rsrc.last_modified();
|
|
||||||
let etag = rsrc.etag();
|
|
||||||
res.headers_mut().set(header::AcceptRanges(vec![header::RangeUnit::Bytes]));
|
|
||||||
res.headers_mut().set(header::LastModified(*last_modified));
|
|
||||||
if let Some(some_etag) = etag {
|
|
||||||
res.headers_mut().set(header::ETag(some_etag.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(&header::IfUnmodifiedSince(ref since)) = req.headers.get() {
|
|
||||||
if last_modified.0.to_timespec() > since.0.to_timespec() {
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
|
||||||
res.send(b"Precondition failed")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !any_match(etag, req) {
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
|
||||||
res.send(b"Precondition failed")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !none_match(etag, req) {
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
|
||||||
res.send(b"")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(&header::IfModifiedSince(ref since)) = req.headers.get() {
|
|
||||||
if last_modified <= since {
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
|
||||||
res.send(b"")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut range_hdr = req.headers.get::<header::Range>();
|
|
||||||
|
|
||||||
// See RFC 2616 section 10.2.7: a Partial Content response should include certain
|
|
||||||
// entity-headers or not based on the If-Range response.
|
|
||||||
let include_entity_headers_on_range = match req.headers.get::<header::IfRange>() {
|
|
||||||
Some(&header::IfRange::EntityTag(ref if_etag)) => {
|
|
||||||
if let Some(some_etag) = etag {
|
|
||||||
if if_etag.strong_eq(some_etag) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
range_hdr = None;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
range_hdr = None;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(&header::IfRange::Date(ref if_date)) => {
|
|
||||||
// The to_timespec conversion appears necessary because in the If-Range off the wire,
|
|
||||||
// fields such as tm_yday are absent, causing strict equality to spuriously fail.
|
|
||||||
if if_date.0.to_timespec() != last_modified.0.to_timespec() {
|
|
||||||
range_hdr = None;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
let len = rsrc.len();
|
|
||||||
let (range, include_entity_headers) = match parse_range_header(range_hdr, len) {
|
|
||||||
ResolvedRanges::AbsentOrInvalid => (0 .. len, true),
|
|
||||||
ResolvedRanges::Satisfiable(rs) => {
|
|
||||||
if rs.len() == 1 {
|
|
||||||
res.headers_mut().set(header::ContentRange(
|
|
||||||
header::ContentRangeSpec::Bytes{
|
|
||||||
range: Some((rs[0].start, rs[0].end-1)),
|
|
||||||
instance_length: Some(len)}));
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::PartialContent;
|
|
||||||
(rs[0].clone(), include_entity_headers_on_range)
|
|
||||||
} else {
|
|
||||||
// Ignore multi-part range headers for now. They require additional complexity, and
|
|
||||||
// I don't see clients sending them in the wild.
|
|
||||||
(0 .. len, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ResolvedRanges::NotSatisfiable => {
|
|
||||||
res.headers_mut().set(header::ContentRange(
|
|
||||||
header::ContentRangeSpec::Bytes{
|
|
||||||
range: None,
|
|
||||||
instance_length: Some(len)}));
|
|
||||||
*res.status_mut() = hyper::status::StatusCode::RangeNotSatisfiable;
|
|
||||||
res.send(b"")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if include_entity_headers {
|
|
||||||
res.headers_mut().set(header::ContentType(rsrc.content_type()));
|
|
||||||
}
|
|
||||||
res.headers_mut().set(header::ContentLength(range.end - range.start));
|
|
||||||
let mut stream = res.start()?;
|
|
||||||
if req.method == Method::Get {
|
|
||||||
rsrc.write_to(range, &mut stream)?;
|
|
||||||
}
|
|
||||||
stream.end()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use error::Result;
|
|
||||||
use hyper;
|
|
||||||
use hyper::header::{self, ByteRangeSpec, ContentRangeSpec, EntityTag};
|
|
||||||
use hyper::header::Range::Bytes;
|
|
||||||
use mime;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::ops::Range;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use super::{ResolvedRanges, parse_range_header};
|
|
||||||
use super::*;
|
|
||||||
use testutil;
|
|
||||||
use time;
|
|
||||||
|
|
||||||
/// Tests the specific examples enumerated in RFC 2616 section 14.35.1.
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_ranges_rfc() {
|
|
||||||
let mut v = SmallVec::new();
|
|
||||||
|
|
||||||
v.push(0 .. 500);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(500 .. 1000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 999)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(9500 .. 10000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(500)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(9500 .. 10000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(9500)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(0 .. 1);
|
|
||||||
v.push(9999 .. 10000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0),
|
|
||||||
ByteRangeSpec::Last(1)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
// Non-canonical ranges. Possibly the point of these is that the adjacent and overlapping
|
|
||||||
// ranges are supposed to be coalesced into one? I'm not going to do that for now.
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(500 .. 601);
|
|
||||||
v.push(601 .. 1000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 600),
|
|
||||||
ByteRangeSpec::FromTo(601, 999)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(500 .. 701);
|
|
||||||
v.push(601 .. 1000);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 700),
|
|
||||||
ByteRangeSpec::FromTo(601, 999)])),
|
|
||||||
10000));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_ranges_satisfiability() {
|
|
||||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(10000)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
let mut v = SmallVec::new();
|
|
||||||
v.push(0 .. 500);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499),
|
|
||||||
ByteRangeSpec::AllFrom(10000)])),
|
|
||||||
10000));
|
|
||||||
|
|
||||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(1)])), 0));
|
|
||||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 0));
|
|
||||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(0)])), 0));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(0 .. 1);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 1));
|
|
||||||
|
|
||||||
v.clear();
|
|
||||||
v.push(0 .. 500);
|
|
||||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
|
||||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 10000)])),
|
|
||||||
500));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_resolve_ranges_absent_or_invalid() {
|
|
||||||
assert_eq!(ResolvedRanges::AbsentOrInvalid, parse_range_header(None, 10000));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FakeResource {
|
|
||||||
etag: Option<EntityTag>,
|
|
||||||
mime: mime::Mime,
|
|
||||||
last_modified: header::HttpDate,
|
|
||||||
body: &'static [u8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resource for FakeResource {
|
|
||||||
fn len(&self) -> u64 { self.body.len() as u64 }
|
|
||||||
fn write_to(&self, range: Range<u64>, out: &mut Write) -> Result<()> {
|
|
||||||
Ok(out.write_all(&self.body[range.start as usize .. range.end as usize])?)
|
|
||||||
}
|
|
||||||
fn content_type(&self) -> mime::Mime { self.mime.clone() }
|
|
||||||
fn etag(&self) -> Option<&EntityTag> { self.etag.as_ref() }
|
|
||||||
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_server() -> String {
|
|
||||||
let mut listener = hyper::net::HttpListener::new("127.0.0.1:0").unwrap();
|
|
||||||
use hyper::net::NetworkListener;
|
|
||||||
let addr = listener.local_addr().unwrap();
|
|
||||||
let server = hyper::Server::new(listener);
|
|
||||||
use std::thread::spawn;
|
|
||||||
spawn(move || {
|
|
||||||
use hyper::server::{Request, Response, Fresh};
|
|
||||||
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
|
||||||
let l = RESOURCE.lock().unwrap();
|
|
||||||
let resource = l.as_ref().unwrap();
|
|
||||||
serve(resource, &req, res).unwrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
format!("http://{}:{}/", addr.ip(), addr.port())
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref RESOURCE: Mutex<Option<FakeResource>> = { Mutex::new(None) };
|
|
||||||
static ref SERVER: String = { new_server() };
|
|
||||||
static ref SOME_DATE: header::HttpDate = {
|
|
||||||
header::HttpDate(time::at_utc(time::Timespec::new(1430006400i64, 0)))
|
|
||||||
};
|
|
||||||
static ref LATER_DATE: header::HttpDate = {
|
|
||||||
header::HttpDate(time::at_utc(time::Timespec::new(1430092800i64, 0)))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serve_without_etag() {
|
|
||||||
testutil::init();
|
|
||||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
|
||||||
etag: None,
|
|
||||||
mime: mime!(Application/OctetStream),
|
|
||||||
last_modified: *SOME_DATE,
|
|
||||||
body: b"01234",
|
|
||||||
});
|
|
||||||
let client = hyper::Client::new();
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
// Full body.
|
|
||||||
let mut resp = client.get(&*SERVER).send().unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// If-Match any should still send the full body.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Any)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// If-Match by etag doesn't match (as this request has no etag).
|
|
||||||
let resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
|
||||||
|
|
||||||
// If-None-Match any.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Any)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"", &buf[..]);
|
|
||||||
|
|
||||||
// If-None-Match by etag doesn't match (as this request has no etag).
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// Unmodified since supplied date.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(header::IfModifiedSince(*SOME_DATE))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - basic case.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
|
||||||
range: Some((1, 3)),
|
|
||||||
instance_length: Some(5),
|
|
||||||
})), resp.headers.get());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"123", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - multiple ranges. Currently falls back to whole range.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(0, 1),
|
|
||||||
ByteRangeSpec::FromTo(3, 4)]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - not satisfiable.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::AllFrom(500)]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::RangeNotSatisfiable, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
|
||||||
range: None,
|
|
||||||
instance_length: Some(5),
|
|
||||||
})), resp.headers.get());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - matching If-Range by date honors the range.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::Date(*SOME_DATE))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
|
||||||
range: Some((1, 3)),
|
|
||||||
instance_length: Some(5),
|
|
||||||
})), resp.headers.get());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"123", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - non-matching If-Range by date ignores the range.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::Date(*LATER_DATE))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - this resource has no etag, so any If-Range by etag ignores the range.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serve_with_strong_etag() {
|
|
||||||
testutil::init();
|
|
||||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
|
||||||
etag: Some(EntityTag::strong("foo".to_owned())),
|
|
||||||
mime: mime!(Application/OctetStream),
|
|
||||||
last_modified: *SOME_DATE,
|
|
||||||
body: b"01234",
|
|
||||||
});
|
|
||||||
let client = hyper::Client::new();
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
// If-Match any should still send the full body.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Any)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// If-Match by matching etag should send the full body.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// If-Match by etag which doesn't match.
|
|
||||||
let resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Items(vec![EntityTag::strong("bar".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
|
||||||
|
|
||||||
// If-None-Match by etag which matches.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"", &buf[..]);
|
|
||||||
|
|
||||||
// If-None-Match by etag which doesn't match.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("bar".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - If-Range matching by etag.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
|
||||||
range: Some((1, 3)),
|
|
||||||
instance_length: Some(5),
|
|
||||||
})), resp.headers.get());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"123", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - If-Range not matching by etag.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::EntityTag(EntityTag::strong("bar".to_owned())))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serve_with_weak_etag() {
|
|
||||||
testutil::init();
|
|
||||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
|
||||||
etag: Some(EntityTag::weak("foo".to_owned())),
|
|
||||||
mime: mime!(Application/OctetStream),
|
|
||||||
last_modified: *SOME_DATE,
|
|
||||||
body: b"01234",
|
|
||||||
});
|
|
||||||
let client = hyper::Client::new();
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
// If-Match any should still send the full body.
|
|
||||||
let mut resp = client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Any)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// If-Match by etag doesn't match because matches use the strong comparison function.
|
|
||||||
let resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfMatch::Items(vec![EntityTag::weak("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
|
||||||
|
|
||||||
// If-None-Match by identical weak etag is sufficient.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("foo".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"", &buf[..]);
|
|
||||||
|
|
||||||
// If-None-Match by etag which doesn't match.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("bar".to_owned())]))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
|
|
||||||
// Range serving - If-Range matching by weak etag isn't sufficient.
|
|
||||||
let mut resp =
|
|
||||||
client.get(&*SERVER)
|
|
||||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
|
||||||
.header(header::IfRange::EntityTag(EntityTag::weak("foo".to_owned())))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
|
||||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
|
||||||
resp.headers.get::<header::ContentType>());
|
|
||||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
|
||||||
buf.clear();
|
|
||||||
resp.read_to_end(&mut buf).unwrap();
|
|
||||||
assert_eq!(b"01234", &buf[..]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,12 +35,12 @@ use core::str::FromStr;
|
|||||||
use db;
|
use db;
|
||||||
use dir::SampleFileDir;
|
use dir::SampleFileDir;
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
|
use http_entity;
|
||||||
use hyper::{header,server,status};
|
use hyper::{header,server,status};
|
||||||
use hyper::uri::RequestUri;
|
use hyper::uri::RequestUri;
|
||||||
use mime;
|
use mime;
|
||||||
use mp4;
|
use mp4;
|
||||||
use recording;
|
use recording;
|
||||||
use resource;
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@ -416,7 +416,7 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
builder.include_timestamp_subtitle_track(include_ts);
|
builder.include_timestamp_subtitle_track(include_ts);
|
||||||
let mp4 = builder.build(self.db.clone(), self.dir.clone())?;
|
let mp4 = builder.build(self.db.clone(), self.dir.clone())?;
|
||||||
resource::serve(&mp4, req, res)?;
|
http_entity::serve(&mp4, req, res)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user