Skip to content

Commit

Permalink
Add support for Range requests
Browse files Browse the repository at this point in the history
This is a rebase by richo of PR rwf2#887 as a starting point toward
mergability.
  • Loading branch information
richo committed Nov 14, 2020
1 parent fa77435 commit 1bd2de3
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 5 deletions.
2 changes: 1 addition & 1 deletion core/lib/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod status;

pub use self::response::DEFAULT_CHUNK_SIZE;
pub use self::response::{Response, ResponseBody, ResponseBuilder, Body};
pub use self::responder::Responder;
pub use self::responder::{RangeResponder, Responder};
pub use self::redirect::Redirect;
pub use self::flash::Flash;
pub use self::named_file::NamedFile;
Expand Down
126 changes: 122 additions & 4 deletions core/lib/src/response/responder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::fs::File;
use std::io::Cursor;
use std::io::{self, Cursor};
use std::str::FromStr;

use crate::http::{Status, ContentType, StatusClass};
use crate::http::{Status, ContentType, StatusClass, Method};
use crate::http::hyper::header::{AcceptRanges, Range, RangeUnit};
use crate::response::{self, Response};
use crate::request::Request;

Expand Down Expand Up @@ -55,14 +57,14 @@ use crate::request::Request;
///
/// Sets the `Content-Type` to `application/octet-stream`. The slice
/// is used as the body of the response, which is fixed size and not
/// streamed. To stream a slice of bytes, use
/// streamed. Supports range requests. To stream a slice of bytes, use
/// `Stream::from(Cursor::new(data))`.
///
/// * **Vec<u8>**
///
/// Sets the `Content-Type` to `application/octet-stream`. The vector's data
/// is used as the body of the response, which is fixed size and not
/// streamed. To stream a vector of bytes, use
/// streamed. Supports range requests. To stream a vector of bytes, use
/// `Stream::from(Cursor::new(vec))`.
///
/// * **File**
Expand Down Expand Up @@ -215,6 +217,122 @@ impl<'r> Responder<'r, 'static> for String {
}
}

/// A special responder type, that may be used by custom [`Responder`][response::Responder]
/// implementation to wrap any type implementing [`io::Seek`](std::io::Seek) and [`io::Read`](std::io::Read)
/// to support [range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests).
///
/// # **Note:**
/// `&[u8]`, `Vec<u8>`, [`File`](std::fs::File) and [`NamedFile`](response::NamedFile)
/// are implemented using `RangeResponder`. There is no need to wrap these types,
/// they do support range requests out of the box.
///
/// # Example
/// ```rust
/// # #![feature(proc_macro_hygiene, decl_macro)]
/// # #[macro_use] extern crate rocket;
/// #
/// # struct CustomIoType;
/// # impl std::io::Read for CustomIoType {
/// # fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { Ok(0) }
/// # }
/// # impl std::io::Seek for CustomIoType {
/// # fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { Ok(0) }
/// # }
/// #
/// use rocket::request::Request;
/// use rocket::response::{self, Response, Responder, RangeResponder};
/// use rocket::http::ContentType;
///
/// impl<'r> Responder<'r> for CustomIoType {
/// fn respond_to(self, req: &Request) -> response::Result<'r> {
/// Response::build_from(RangeResponder(self).respond_to(req)?)
/// .header(ContentType::Binary)
/// .ok()
/// }
/// }
/// ```
pub struct RangeResponder<B: io::Seek + io::Read>(pub B);

impl<'r, B: io::Seek + io::Read + 'r> Responder<'r> for RangeResponder<B> {
fn respond_to(self, req: &Request) -> response::Result<'r> {
use http::hyper::header::{ContentRange, ByteRangeSpec, ContentRangeSpec};

let mut body = self.0;
// A server MUST ignore a Range header field received with a request method other than GET.
if req.method() == Method::Get {
let range = req.headers().get_one("Range").map(|x| Range::from_str(x));
match range {
Some(Ok(Range::Bytes(ranges))) => {
if ranges.len() == 1 {
let size = body.seek(io::SeekFrom::End(0))
.expect("Attempted to retrieve size by seeking, but failed.");

let (start, end) = match ranges[0] {
ByteRangeSpec::FromTo(start, mut end) => {
// make end exclusive
end += 1;
if end > size {
end = size;
}
(start, end)
},
ByteRangeSpec::AllFrom(start) => {
(start, size)
},
ByteRangeSpec::Last(len) => {
// we could seek to SeekFrom::End(-len), but if we reach a value < 0, that is an error.
// but the RFC reads:
// If the selected representation is shorter than the specified
// suffix-length, the entire representation is used.
let start = size.checked_sub(len).unwrap_or(0);
(start, size)
}
};

if start > size {
return Response::build()
.status(Status::RangeNotSatisfiable)
.header(AcceptRanges(vec![RangeUnit::Bytes]))
.ok()
}

body.seek(io::SeekFrom::Start(start))
.expect("Attempted to seek to the start of the requested range, but failed.");

return Response::build()
.status(Status::PartialContent)
.header(AcceptRanges(vec![RangeUnit::Bytes]))
.header(ContentRange(ContentRangeSpec::Bytes {
// make end inclusive again
range: Some((start, end - 1)),
instance_length: Some(size),
}))
.raw_body(Body::Sized(body, end - start))
.ok()
}
// A server MAY ignore the Range header field.
},
// An origin server MUST ignore a Range header field that contains a
// range unit it does not understand.
Some(Ok(Range::Unregistered(_, _))) => {},
Some(Err(_)) => {
// Malformed
return Response::build()
.status(Status::RangeNotSatisfiable)
.header(AcceptRanges(vec![RangeUnit::Bytes]))
.ok()
}
None => {},
};
}

Response::build()
.header(AcceptRanges(vec![RangeUnit::Bytes]))
.sized_body(body)
.ok()
}
}

/// Returns a response with Content-Type `application/octet-stream` and a
/// fixed-size body containing the data in `self`. Always returns `Ok`.
impl<'r, 'o: 'r> Responder<'r, 'o> for &'o [u8] {
Expand Down
137 changes: 137 additions & 0 deletions core/lib/tests/range_responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;

#[get("/")]
fn data() -> &'static [u8] {
&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

mod tests {
use super::*;

use rocket::Rocket;
use rocket::local::Client;
use rocket::http::Status;
use rocket::http::hyper::header::{Range, ByteRangeSpec};

fn rocket() -> Rocket {
rocket::ignite()
.mount("/", routes![data])
}

#[test]
fn head() {
let client = Client::new(rocket()).unwrap();
let response = client.head("/").dispatch();
let headers = response.headers();

assert_eq!(headers.get_one("Accept-Ranges"), Some("bytes"));
}

#[test]
fn range_between() {
let client = Client::new(rocket()).unwrap();
let mut response = client.get("/")
.header(Range::bytes(1, 4))
.dispatch();

assert_eq!(response.status(), Status::PartialContent);

{
let headers = response.headers();
assert_eq!(headers.get_one("Content-Range"), Some("bytes 1-4/10"));
}

assert_eq!(response.body_bytes(), Some(vec![1, 2, 3, 4]));
}

#[test]
fn range_between_invalid() {
let client = Client::new(rocket()).unwrap();
let response = client.get("/")
.header(Range::bytes(4, 2))
.dispatch();

assert_eq!(response.status(), Status::RangeNotSatisfiable);
}

#[test]
fn range_between_overflow() {
let client = Client::new(rocket()).unwrap();
let response = client.get("/")
.header(Range::bytes(11, 12))
.dispatch();

assert_eq!(response.status(), Status::RangeNotSatisfiable);
}

#[test]
fn range_from() {
let client = Client::new(rocket()).unwrap();
let mut response = client.get("/")
.header(Range::Bytes(vec![
ByteRangeSpec::AllFrom(4),
]))
.dispatch();

assert_eq!(response.status(), Status::PartialContent);

{
let headers = response.headers();
assert_eq!(headers.get_one("Content-Range"), Some("bytes 4-9/10"));
}

assert_eq!(response.body_bytes(), Some(vec![4, 5, 6, 7, 8, 9]));
}

#[test]
fn range_from_overflow() {
let client = Client::new(rocket()).unwrap();
let response = client.get("/")
.header(Range::Bytes(vec![
ByteRangeSpec::AllFrom(12),
]))
.dispatch();

assert_eq!(response.status(), Status::RangeNotSatisfiable);
}

#[test]
fn range_last() {
let client = Client::new(rocket()).unwrap();
let mut response = client.get("/")
.header(Range::Bytes(vec![
ByteRangeSpec::Last(3),
]))
.dispatch();

assert_eq!(response.status(), Status::PartialContent);

{
let headers = response.headers();
assert_eq!(headers.get_one("Content-Range"), Some("bytes 7-9/10"));
}

assert_eq!(response.body_bytes(), Some(vec![7, 8, 9]));
}

#[test]
fn range_last_overflow() {
let client = Client::new(rocket()).unwrap();
let mut response = client.get("/")
.header(Range::Bytes(vec![
ByteRangeSpec::Last(12),
]))
.dispatch();

assert_eq!(response.status(), Status::PartialContent);

{
let headers = response.headers();
assert_eq!(headers.get_one("Content-Range"), Some("bytes 0-9/10"));
}

assert_eq!(response.body_bytes(), Some(Vec::from(data())));
}
}

0 comments on commit 1bd2de3

Please sign in to comment.