Skip to content

Commit

Permalink
Support Partial Content (#1)
Browse files Browse the repository at this point in the history
* Support Partial Content

What
--
Support Partial Content

Support partial file content.

How
--
Return byte ranges when the `Range` header is specified.

Other
--
* Fix stream writes
  • Loading branch information
mattgathu authored Dec 29, 2023
1 parent 0142643 commit 6a27b6d
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "seva"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
authors = ["Matt Gathu <[email protected]>"]
description = "Simple directory http server inspired by Python's http.server"
Expand Down
8 changes: 7 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use core::fmt;
use std::{io, string::FromUtf8Error, time::SystemTimeError};
use std::{io, num::ParseIntError, string::FromUtf8Error, time::SystemTimeError};

use thiserror::Error;

use crate::http::HeaderName;

pub type Result<T> = std::result::Result<T, SevaError>;

#[derive(Error, Debug)]
Expand All @@ -26,6 +28,8 @@ pub enum SevaError {
TestClient(String),
#[error("URI Too Long")]
UriTooLong,
#[error("Missing value for header: {0}")]
MissingHeaderValue(HeaderName),
}

#[derive(Error, Debug)]
Expand All @@ -36,6 +40,8 @@ pub enum ParsingError {
UnknownMethod(String),
PestRuleError(String),
DateTime(String),
IntError(#[from] ParseIntError),
InvalidRangeHeader(String),
}

impl fmt::Display for ParsingError {
Expand Down
130 changes: 128 additions & 2 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub struct Request<'a> {
}

impl<'a> Request<'a> {
pub fn is_partial(&self) -> bool {
self.headers.contains_key(&HeaderName::Range)
}
pub fn parse(req_str: &str) -> Result<Request> {
trace!("Request::parse");
let mut res = HttpParser::parse(Rule::request, req_str)
Expand Down Expand Up @@ -121,6 +124,10 @@ impl ResponseBuilder<Empty> {
Self::new(StatusCode::Ok, BTreeMap::new())
}

pub fn partial() -> ResponseBuilder<Empty> {
Self::new(StatusCode::PartialContent, BTreeMap::new())
}

pub fn not_found() -> ResponseBuilder<Empty> {
Self::new(StatusCode::NotFound, BTreeMap::new())
}
Expand Down Expand Up @@ -273,15 +280,100 @@ header = { header_name ~ ":" ~ whitespace ~ header_value ~ NEWLINE }
header_name = { (!(NEWLINE | ":") ~ ANY)+ }
header_value = { (!NEWLINE ~ ANY)+ }
// accept-encoding header parser
ws = _{( " " | "\t")*}
accept_encoding = { encoding ~ ws ~ ("," ~ ws ~ encoding)* ~ EOI}
algo = {(ASCII_ALPHA+ | "identity" | "*")}
weight = {ws ~ ";" ~ ws ~ "q=" ~ qvalue}
qvalue = { ("0" ~ ("." ~ ASCII_DIGIT{,3}){,1}) | ("1" ~ ("." ~ "0"{,3}){,1}) }
encoding = { algo ~ weight*}
// Range header parser
//
// A range request can specify a single range or a set of ranges within a single representation.
//
// Range = ranges-specifier
// ranges-specifier = range-unit "=" range-set
// range-unit = token
// range-set = 1#range-spec
// range-spec = int-range / suffix-range / other-range
// int-range = first-pos "-" [ last-pos ]
// first-pos = 1*DIGIT
// last-pos = 1*DIGIT
// suffix-range = "-" suffix-length
// suffix-length = 1*DIGIT
// other-range = 1*( %x21-2B / %x2D-7E ) ; 1*(VCHAR excluding comma)
//
bytes_range = { "bytes" ~ ws ~ "=" ~ ws ~ range_sets }
range_sets = _{ range_set ~ ws ~ ("," ~ ws ~ range_set)* ~ EOI }
range_set = _{(int_range | suffix_range)}
int_range = { first_pos ~ "-" ~ last_pos*}
suffix_range = { "-" ~ len}
first_pos = { ASCII_DIGIT+ }
last_pos = { ASCII_DIGIT+ }
len = { ASCII_DIGIT* }
"#]
struct HttpParser;
pub struct HttpParser;

impl HttpParser {
pub fn parse_bytes_range(val: &str, max_len: usize) -> Result<Vec<BytesRange>> {
let br = HttpParser::parse(Rule::bytes_range, val)
.map_err(|e| ParsingError::PestRuleError(format!("{e:?}")))?
.next()
.unwrap();
let mut ranges = vec![];
for pair in br.into_inner() {
match pair.as_rule() {
Rule::int_range => {
let mut inner = pair.into_inner();
let start = inner
.next()
.unwrap()
.as_str()
.parse()
.map_err(ParsingError::IntError)?;
let end = match inner.next() {
Some(r) => {
r.as_str().parse().map_err(ParsingError::IntError)?
}
None => max_len,
};
if start > end {
Err(ParsingError::InvalidRangeHeader(val.to_owned()))?;
}
let size = end - start;
ranges.push(BytesRange { start, size });
}
Rule::suffix_range => {
let mut inner = pair.into_inner();
let size = inner
.next()
.unwrap()
.as_str()
.parse()
.map_err(ParsingError::IntError)?;
if size >= max_len {
Err(ParsingError::InvalidRangeHeader(val.to_owned()))?;
}
let start = max_len - size;
ranges.push(BytesRange { start, size });
}
_ => {}
}
}
if ranges.len() > 10 {
return Err(ParsingError::InvalidRangeHeader(val.to_owned()))?;
}

Ok(ranges)
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct BytesRange {
pub start: usize,
pub size: usize,
}

macro_rules! status_codes {
(
Expand Down Expand Up @@ -394,6 +486,9 @@ status_codes! {
/// This response is sent on an idle connection
(RequestTimeout,408);

/// Indicates that a server cannot serve the requested ranges.
(RangeNotSatisifiable, 416);

/// The user has sent too many requests in a given amount of time ("rate
/// limiting").
(TooManyRequests,429);
Expand Down Expand Up @@ -488,12 +583,18 @@ header_names! {
/// Indicates the size of the entity-body.
(ContentLength, "content-length");

/// Indicates where in a full body message a partial message belongs.
(ContentRange, "content-range");

/// Used to indicate the media type of the resource.
(ContentType, "content-type");

/// Contains the date and time at which the message was originated.
(Date, "date");

/// Identifier for a specific version of a resource.
(ETag, "etag");

/// Specifies the domain name of the server and (optionally) the TCP port
/// number on which the server is listening.
(Host, "host");
Expand All @@ -513,6 +614,9 @@ header_names! {
/// Indicates the part of a document that the server should return.
(Range, "range");

/// Contains the absolute or partial address from which a resource has been requested.
(Referer, "referer");

/// Contains information about the software used by the origin server to
/// handle the request.
(Server, "server");
Expand All @@ -521,6 +625,9 @@ header_names! {
/// software.
(UserAgent, "user-agent");

/// Describes the parts of the request message aside from the method and URL that influenced the content of the response it occurs in.
(Vary, "vary");

/// General HTTP header contains information about possible problems with
/// the status of the message.
(Warning, "warning");
Expand Down Expand Up @@ -567,6 +674,25 @@ mod tests {
Ok(())
}

#[test]
fn bytes_range_parser() -> Result<()> {
for val in [
"bytes=0-499",
"bytes=500-999",
"bytes=-500",
"bytes=9500-",
"bytes=0-0,-1",
"bytes= 0-0, -2",
"bytes= 0-999, 4500-5499, -1000",
"bytes=500-600,601-999",
"bytes=500-700,601-999",
] {
let range = HttpParser::parse_bytes_range(val, 10000);
assert!(range.is_ok(), "failed to parse: {val}. Reason: {range:?}");
}
Ok(())
}

#[test]
fn response_body_type_mapping() -> Result<()> {
let builder = ResponseBuilder::ok();
Expand Down
Loading

0 comments on commit 6a27b6d

Please sign in to comment.