mirror of https://github.com/http-rs/http-types
Compare commits
16 Commits
e0ae15c814
...
ebe7e4de2d
Author | SHA1 | Date |
---|---|---|
Yosh | ebe7e4de2d | |
Yosh | 0b60560968 | |
Jan Baudisch | 2519f4a91c | |
Jan Baudisch | 3d904e678c | |
Yosh | 2d3e391776 | |
Yoshua Wuyts | 9579c07eb7 | |
Yosh | 4ea8ff1c34 | |
Yoshua Wuyts | 21c5aa9905 | |
Jeremiah Senkpiel | 2858179881 | |
Jeremiah Senkpiel | 49dd60324d | |
Yosh | b84a7a67e5 | |
Christian Haynes | 583ae8910d | |
rusty | 97acd4d3f2 | |
rusty | 220f7ea708 | |
Vic Demuzere | 9ffd9eb921 | |
Vic Demuzere | 48e783f08f |
|
@ -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]
|
||||
|
|
|
@ -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 }))
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue