Compare commits

...

16 Commits

Author SHA1 Message Date
Yosh ebe7e4de2d
Merge pull request #398 from janbaudisch/update-serde_qs
Update serde_qs to 0.9.1
2022-03-23 13:56:36 +01:00
Yosh 0b60560968
Merge pull request #397 from janbaudisch/update-infer
Update infer to 0.7.0
2022-03-23 13:55:47 +01:00
Jan Baudisch 2519f4a91c
Update serde_qs to 0.9.1 2022-03-23 13:19:00 +01:00
Jan Baudisch 3d904e678c
Update infer to 0.7.0 2022-03-23 12:31:26 +01:00
Yosh 2d3e391776
Merge pull request #394 from http-rs/hsts
Add typed `Strict-Transport-Security` header
2022-02-06 12:57:45 +01:00
Yoshua Wuyts 9579c07eb7 Add typed `Strict-Transport-Security` header 2022-02-04 19:03:55 +01:00
Yosh 4ea8ff1c34
Merge pull request #393 from http-rs/fix-duration-parsing
attempt to fix duration parsing
2022-02-04 18:32:07 +01:00
Yoshua Wuyts 21c5aa9905 attempt to fix duration parsing 2022-02-04 18:20:34 +01:00
Jeremiah Senkpiel 2858179881
Merge pull request #384 from sorcix/mimetypes
Add font and additional image mime types
2022-01-27 10:19:12 -08:00
Jeremiah Senkpiel 49dd60324d
Merge pull request #364 from pbzweihander/status-for-crate-error
Implement `Status` for `Result<T, http_types::Error>`
2022-01-27 10:16:17 -08:00
Yosh b84a7a67e5
Merge pull request #391 from 06chaynes/cookie-v0.16
updates cookie to v0.16.0
2022-01-25 11:48:01 +01:00
Christian Haynes 583ae8910d updated cookie to v0.16.0 2022-01-24 14:02:27 -05:00
rusty 97acd4d3f2
Fix type annotation error 2021-11-23 16:23:33 +09:00
rusty 220f7ea708
Implement Status for Result<T, crate::Error> 2021-11-23 16:23:32 +09:00
Vic Demuzere 9ffd9eb921
Add common video, audio, archies and syndication
References:

https://github.com/nginx/nginx/blob/master/conf/mime.types
https://www.iana.org/assignments/media-types/media-types.xhtml
2021-11-03 20:42:47 +01:00
Vic Demuzere 48e783f08f
Add font and additional image mime types 2021-11-03 20:42:46 +01:00
12 changed files with 335 additions and 23 deletions

View File

@ -31,7 +31,7 @@ fastrand = "1.4.0"
base64 = "0.13.0"
futures-lite = "1.11.1"
async-channel = "1.5.1"
infer = "0.2.3"
infer = "0.7.0"
pin-project-lite = "0.2.0"
url = "2.1.1"
anyhow = "1.0.26"
@ -43,13 +43,13 @@ async-std = { version = "1.6.0", optional = true }
http = { version = "0.2.0", optional = true }
# features: cookies
cookie = { version = "0.14.0", features = ["percent-encode"], optional = true }
cookie = { version = "0.16.0", features = ["percent-encode"], optional = true }
# features: serde
serde_json = { version = "1.0.51", optional = true }
serde_crate = { version = "1.0.106", features = ["derive"], optional = true, package = "serde" }
serde_urlencoded = { version = "0.7.0", optional = true}
serde_qs = { version = "0.8.3", optional = true }
serde_qs = { version = "0.9.1", optional = true }
[dev-dependencies]

2
src/cache/age.rs vendored
View File

@ -62,7 +62,7 @@ impl Age {
// entry. We want the last entry.
let header = headers.iter().last().unwrap();
let num: u64 = header.as_str().parse().status(400)?;
let num: u64 = header.as_str().parse::<u64>().status(400)?;
let dur = Duration::from_secs_f64(num as f64);
Ok(Some(Self { dur }))

View File

@ -94,7 +94,7 @@ impl CacheDirective {
let mut get_dur = || -> crate::Result<Duration> {
let dur = parts.next().status(400)?;
let dur: u64 = dur.parse().status(400)?;
let dur: u64 = dur.parse::<u64>().status(400)?;
Ok(Duration::new(dur, 0))
};
@ -112,7 +112,7 @@ impl CacheDirective {
"max-age" => Some(MaxAge(get_dur()?)),
"max-stale" => match parts.next() {
Some(secs) => {
let dur: u64 = secs.parse().status(400)?;
let dur: u64 = secs.parse::<u64>().status(400)?;
Some(MaxStale(Some(Duration::new(dur, 0))))
}
None => Some(MaxStale(None)),

View File

@ -47,7 +47,7 @@ impl ContentLength {
// If we successfully parsed the header then there's always at least one
// entry. We want the last entry.
let value = headers.iter().last().unwrap();
let length = value.as_str().trim().parse().status(400)?;
let length = value.as_str().trim().parse::<u64>().status(400)?;
Ok(Some(Self { length }))
}

View File

@ -153,6 +153,10 @@ pub const SERVER_TIMING: HeaderName = HeaderName::from_lowercase_str("server-tim
/// The `SourceMap` Header
pub const SOURCE_MAP: HeaderName = HeaderName::from_lowercase_str("sourcemap");
/// The `Strict-Transport-Security` Header
pub const STRICT_TRANSPORT_SECURITY: HeaderName =
HeaderName::from_lowercase_str("strict-transport-security");
/// The `Te` Header
pub const TE: HeaderName = HeaderName::from_lowercase_str("te");

View File

@ -57,17 +57,50 @@ utf8_mime_const!(CSS, "CSS", "text", "css");
utf8_mime_const!(HTML, "HTML", "text", "html");
utf8_mime_const!(PLAIN, "Plain text", "text", "plain");
utf8_mime_const!(XML, "XML", "application", "xml");
utf8_mime_const!(RSS, "RSS Feed", "application", "rss+xml");
utf8_mime_const!(ATOM, "Atom Feed", "application", "atom+xml");
mime_const!(ANY, "matching anything", "*", "*");
mime_const!(JSON, "JSON", "application", "json");
mime_const!(SVG, "SVG", "image", "svg+xml");
mime_const!(PNG, "PNG images", "image", "png");
mime_const!(JPEG, "JPEG images", "image", "jpeg");
mime_const!(SSE, "Server Sent Events", "text", "event-stream");
mime_const!(BYTE_STREAM, "byte streams", "application", "octet-stream");
mime_const!(FORM, "forms", "application", "x-www-form-urlencoded");
mime_const!(MULTIPART_FORM, "multipart forms", "multipart", "form-data");
mime_const!(WASM, "webassembly", "application", "wasm");
// Images
// https://www.iana.org/assignments/media-types/media-types.xhtml#image
mime_const!(BMP, "BMP images", "image", "bmp");
mime_const!(JPEG, "JPEG images", "image", "jpeg");
mime_const!(PNG, "PNG images", "image", "png");
mime_const!(SVG, "SVG", "image", "svg+xml");
mime_const!(WEBP, "WebP images", "image", "webp");
// Audio
// https://www.iana.org/assignments/media-types/media-types.xhtml#audio
mime_const!(MIDI, "MIDI audio", "audio", "midi");
mime_const!(MP3, "MPEG audio layer 3", "audio", "mpeg");
mime_const!(OGG, "Ogg vorbis audio", "audio", "ogg");
mime_const!(OPUS, "Opus audio", "audio", "opus");
mime_const!(M4A, "MPEG audio layer 4", "audio", "mp4");
// Video
// https://www.iana.org/assignments/media-types/media-types.xhtml#video
mime_const!(MP4, "MPEG video layer 4", "video", "mp4");
mime_const!(MPEG, "MPEG video", "video", "mpeg");
mime_const!(WEBM, "WebM video", "video", "webm");
mime_const!(AVI, "Microsoft AVI video", "video", "x-msvideo");
// There are multiple `.ico` mime types known, but `image/x-icon`
// is what most browser use. See:
// https://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type
mime_const!(ICO, "ICO icons", "image", "x-icon");
// Fonts
// https://www.iana.org/assignments/media-types/media-types.xhtml#font
mime_const!(OTF, "OTF", "font", "otf");
mime_const!(TTF, "TTF", "font", "ttf");
mime_const!(WOFF, "WOFF", "font", "woff");
mime_const!(WOFF2, "WOFF2", "font", "woff2");
// Archives
mime_const!(ZIP, "Zip archive", "application", "zip");
mime_const!(SEVENZIP, "7Zip archive", "application", "x-7z-compressed");

View File

@ -44,21 +44,43 @@ impl Mime {
pub fn sniff(bytes: &[u8]) -> crate::Result<Self> {
let info = Infer::new();
let mime = match info.get(bytes) {
Some(info) => info.mime,
Some(info) => info.mime_type(),
None => crate::bail!("Could not sniff the mime type"),
};
Mime::from_str(&mime)
Mime::from_str(mime)
}
/// Guess the mime type from a file extension
pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
match extension.as_ref() {
"7z" => Some(SEVENZIP),
"atom" => Some(ATOM),
"avi" => Some(AVI),
"bin" | "exe" | "dll" | "iso" | "img" => Some(BYTE_STREAM),
"bmp" => Some(BMP),
"css" => Some(CSS),
"html" => Some(HTML),
"ico" => Some(ICO),
"js" | "mjs" | "jsonp" => Some(JAVASCRIPT),
"json" => Some(JSON),
"css" => Some(CSS),
"svg" => Some(SVG),
"m4a" => Some(M4A),
"mid" | "midi" | "kar" => Some(MIDI),
"mp3" => Some(MP3),
"mp4" => Some(MP4),
"mpeg" | "mpg" => Some(MPEG),
"ogg" => Some(OGG),
"otf" => Some(OTF),
"rss" => Some(RSS),
"svg" | "svgz" => Some(SVG),
"ttf" => Some(TTF),
"txt" => Some(PLAIN),
"wasm" => Some(WASM),
"webm" => Some(WEBM),
"webp" => Some(WEBP),
"woff" => Some(WOFF),
"woff2" => Some(WOFF2),
"xml" => Some(XML),
"zip" => Some(ZIP),
_ => None,
}
}

View File

@ -18,9 +18,11 @@
use crate::headers::{HeaderName, HeaderValue, Headers};
mod csp;
mod strict_transport_security;
mod timing_allow_origin;
pub use csp::{ContentSecurityPolicy, Source};
pub use strict_transport_security::StrictTransportSecurity;
#[cfg(feature = "serde")]
pub use csp::{ReportTo, ReportToEndpoint};

View File

@ -0,0 +1,215 @@
use crate::headers::{Header, HeaderName, HeaderValue, Headers};
use crate::Status;
use crate::headers::STRICT_TRANSPORT_SECURITY;
use std::time::Duration;
/// Inform browsers that the site should only be accessed using HTTPS.
///
/// # Specifications
///
/// - [RFC 6797, section 6.1: Strict-Transport-Security](https://www.rfc-editor.org/rfc/rfc6797#section-6.1)
#[derive(Debug)]
#[doc(alias = "hsts")]
pub struct StrictTransportSecurity {
max_age: Duration,
include_subdomains: bool,
preload: bool,
}
impl Default for StrictTransportSecurity {
/// Defaults to 1 year with "preload" enabled, passing the minimum requirements to
/// qualify for inclusion in browser's HSTS preload lists.
/// [Read more](https://hstspreload.org/)
fn default() -> Self {
Self {
max_age: Duration::from_secs(31536000), // 1 year
include_subdomains: false,
preload: true,
}
}
}
impl StrictTransportSecurity {
/// Create a new instance.
pub fn new(duration: Duration) -> Self {
Self {
max_age: duration,
include_subdomains: false,
preload: false,
}
}
/// Get a reference to the strict transport security's include subdomains.
pub fn include_subdomains(&self) -> bool {
self.include_subdomains
}
/// Set the strict transport security's include subdomains.
pub fn set_include_subdomains(&mut self, include_subdomains: bool) {
self.include_subdomains = include_subdomains;
}
/// Get a reference to the strict transport security's preload.
pub fn preload(&self) -> bool {
self.preload
}
/// Set the strict transport security's preload.
pub fn set_preload(&mut self, preload: bool) {
self.preload = preload;
}
/// Get a reference to the strict transport security's max_age.
pub fn max_age(&self) -> Duration {
self.max_age
}
/// Set the strict transport security's max_age.
pub fn set_max_age(&mut self, duration: Duration) {
self.max_age = duration;
}
}
impl Header for StrictTransportSecurity {
fn header_name(&self) -> HeaderName {
STRICT_TRANSPORT_SECURITY
}
fn header_value(&self) -> HeaderValue {
let max_age = self.max_age.as_secs();
let mut output = format!("max-age={}", max_age);
if self.include_subdomains {
output.push_str(";includeSubdomains");
}
if self.preload {
output.push_str(";preload");
}
// SAFETY: the internal string is validated to be ASCII.
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
}
}
// TODO: move to new header traits
impl StrictTransportSecurity {
/// Create a new instance from headers.
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
let headers = match headers.as_ref().get(STRICT_TRANSPORT_SECURITY) {
Some(headers) => headers,
None => return Ok(None),
};
// If we successfully parsed the header then there's always at least one
// entry. We want the last entry.
let value = headers.iter().last().unwrap();
let mut max_age = None;
let mut include_subdomains = false;
let mut preload = false;
// Attempt to parse all values. If we don't recognize a directive, per
// the spec we should just ignore it.
for s in value.as_str().split(';') {
let s = s.trim();
if s == "includesubdomains" {
include_subdomains = true;
} else if s == "preload" {
preload = true;
} else {
let (key, value) = match s.split_once("=") {
Some(kv) => kv,
None => continue, // We don't recognize the directive, continue.
};
if key == "max-age" {
let secs = value.parse::<u64>().status(400)?;
max_age = Some(Duration::from_secs(secs));
}
}
}
let max_age = match max_age {
Some(max_age) => max_age,
None => {
return Err(crate::format_err_status!(
400,
"`Strict-Transport-Security` header did not contain a `max-age` directive",
));
}
};
Ok(Some(Self {
max_age,
include_subdomains,
preload,
}))
}
}
impl From<StrictTransportSecurity> for Duration {
fn from(stc: StrictTransportSecurity) -> Self {
stc.max_age
}
}
impl From<Duration> for StrictTransportSecurity {
fn from(duration: Duration) -> Self {
Self::new(duration)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Response;
use std::time::Duration;
#[test]
fn smoke() -> crate::Result<()> {
let duration = Duration::from_secs(30);
let stc = StrictTransportSecurity::new(duration);
let mut headers = Response::new(200);
stc.apply_header(&mut headers);
let stc = StrictTransportSecurity::from_headers(headers)?.unwrap();
assert_eq!(stc.max_age(), duration);
assert!(!stc.preload);
assert!(!stc.include_subdomains);
Ok(())
}
#[test]
fn bad_request_on_parse_error() {
let mut headers = Response::new(200);
headers
.insert_header(STRICT_TRANSPORT_SECURITY, "<nori ate the tag. yum.>")
.unwrap();
let err = StrictTransportSecurity::from_headers(headers).unwrap_err();
assert_eq!(err.status(), 400);
}
#[test]
fn no_panic_on_invalid_number() {
let mut headers = Response::new(200);
headers
.insert_header(STRICT_TRANSPORT_SECURITY, "max-age=birds")
.unwrap();
let err = StrictTransportSecurity::from_headers(headers).unwrap_err();
assert_eq!(err.status(), 400);
}
#[test]
fn parse_optional_whitespace() {
let mut headers = Response::new(200);
headers
.insert_header(STRICT_TRANSPORT_SECURITY, "max-age=30; preload")
.unwrap();
let policy = StrictTransportSecurity::from_headers(headers)
.unwrap()
.unwrap();
assert_eq!(policy.max_age, Duration::from_secs(30));
assert!(policy.preload());
}
}

View File

@ -71,6 +71,48 @@ where
}
}
impl<T> Status<T, Error> for Result<T, Error> {
/// Wrap the error value with an additional status code.
///
/// # Panics
///
/// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode].
///
/// [status]: crate::Status
/// [statuscode]: crate::StatusCode
fn status<S>(self, status: S) -> Result<T, Error>
where
S: TryInto<StatusCode>,
S::Error: Debug,
{
self.map_err(|mut error| {
error.set_status(status);
error
})
}
/// Wrap the error value with an additional status code that is evaluated
/// lazily only once an error does occur.
///
/// # Panics
///
/// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode].
///
/// [status]: crate::Status
/// [statuscode]: crate::StatusCode
fn with_status<S, F>(self, f: F) -> Result<T, Error>
where
S: TryInto<StatusCode>,
S::Error: Debug,
F: FnOnce() -> S,
{
self.map_err(|mut error| {
error.set_status(f());
error
})
}
}
impl<T> Status<T, Infallible> for Option<T> {
/// Wrap the error value with an additional status code.
///

View File

@ -65,7 +65,7 @@ fn parse_entry(s: &str) -> crate::Result<Metric> {
let millis: f64 = value.parse().map_err(|_| {
format_err!("Server timing duration params must be a valid double-precision floating-point number.")
})?;
dur = Some(Duration::from_secs_f64(millis / 1000.0));
dur = Some(Duration::from_secs_f64(millis) / 1000);
}
"desc" => {
// Ensure quotes line up, and strip them from the resulting output

View File

@ -32,10 +32,7 @@ fn unsuccessfully_deserialize_query() {
let params = req.query::<Params>();
assert!(params.is_err());
assert_eq!(
params.err().unwrap().to_string(),
"failed with reason: missing field `msg`"
);
assert_eq!(params.err().unwrap().to_string(), "missing field `msg`");
}
#[test]
@ -47,10 +44,7 @@ fn malformatted_query() {
let params = req.query::<Params>();
assert!(params.is_err());
assert_eq!(
params.err().unwrap().to_string(),
"failed with reason: missing field `msg`"
);
assert_eq!(params.err().unwrap().to_string(), "missing field `msg`");
}
#[test]