mirror of https://github.com/http-rs/http-types
Add typed `Strict-Transport-Security` header
This commit is contained in:
parent
4ea8ff1c34
commit
9579c07eb7
|
@ -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");
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue