aboutsummaryrefslogtreecommitdiffstats
path: root/components/net/hsts.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/net/hsts.rs')
-rw-r--r--components/net/hsts.rs134
1 files changed, 112 insertions, 22 deletions
diff --git a/components/net/hsts.rs b/components/net/hsts.rs
index d74794ce60a..be955980d2b 100644
--- a/components/net/hsts.rs
+++ b/components/net/hsts.rs
@@ -4,26 +4,37 @@
use std::collections::HashMap;
use std::net::{Ipv4Addr, Ipv6Addr};
+use std::num::NonZeroU64;
+use std::sync::LazyLock;
use std::time::Duration;
-use base::cross_process_instant::CrossProcessInstant;
use embedder_traits::resources::{self, Resource};
use headers::{HeaderMapExt, StrictTransportSecurity};
use http::HeaderMap;
-use log::{error, info};
+use log::{debug, error, info};
use malloc_size_of_derive::MallocSizeOf;
use net_traits::IncludeSubdomains;
use net_traits::pub_domains::reg_suffix;
use serde::{Deserialize, Serialize};
use servo_config::pref;
use servo_url::{Host, ServoUrl};
+use time::UtcDateTime;
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct HstsEntry {
pub host: String,
pub include_subdomains: bool,
- pub max_age: Option<Duration>,
- pub timestamp: Option<CrossProcessInstant>,
+ // Nonzero to allow for memory optimization
+ pub expires_at: Option<NonZeroU64>,
+}
+
+// Zero and negative times are all expired
+fn unix_timestamp_to_nonzerou64(timestamp: i64) -> NonZeroU64 {
+ if timestamp <= 0 {
+ NonZeroU64::new(1).unwrap()
+ } else {
+ NonZeroU64::new(timestamp.try_into().unwrap()).unwrap()
+ }
}
impl HstsEntry {
@@ -32,43 +43,59 @@ impl HstsEntry {
subdomains: IncludeSubdomains,
max_age: Option<Duration>,
) -> Option<HstsEntry> {
+ let expires_at = max_age.map(|duration| {
+ unix_timestamp_to_nonzerou64((UtcDateTime::now() + duration).unix_timestamp())
+ });
if host.parse::<Ipv4Addr>().is_ok() || host.parse::<Ipv6Addr>().is_ok() {
None
} else {
Some(HstsEntry {
host,
include_subdomains: (subdomains == IncludeSubdomains::Included),
- max_age,
- timestamp: Some(CrossProcessInstant::now()),
+ expires_at,
})
}
}
pub fn is_expired(&self) -> bool {
- match (self.max_age, self.timestamp) {
- (Some(max_age), Some(timestamp)) => CrossProcessInstant::now() - timestamp >= max_age,
-
+ match self.expires_at {
+ Some(timestamp) => {
+ unix_timestamp_to_nonzerou64(UtcDateTime::now().unix_timestamp()) >= timestamp
+ },
_ => false,
}
}
fn matches_domain(&self, host: &str) -> bool {
- !self.is_expired() && self.host == host
+ self.host == host
}
fn matches_subdomain(&self, host: &str) -> bool {
- !self.is_expired() && host.ends_with(&format!(".{}", self.host))
+ host.ends_with(&format!(".{}", self.host))
}
}
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
pub struct HstsList {
+ // Map from base domains to a list of entries that are subdomains of base domain
pub entries_map: HashMap<String, Vec<HstsEntry>>,
}
-impl HstsList {
+/// Represents the portion of the HSTS list that comes from the preload list
+/// it is split out to allow sharing between the private and public http state
+/// as well as potentially swpaping out the underlying type to something immutable
+/// and more efficient like FSTs or DAFSA/DAWGs.
+#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
+pub struct HstsPreloadList {
+ pub entries_map: HashMap<String, Vec<HstsEntry>>,
+}
+
+pub static PRELOAD_LIST_ENTRIES: LazyLock<HstsPreloadList> =
+ LazyLock::new(HstsPreloadList::from_servo_preload);
+
+impl HstsPreloadList {
/// Create an `HstsList` from the bytes of a JSON preload file.
- pub fn from_preload(preload_content: &str) -> Option<HstsList> {
+ pub fn from_preload(preload_content: &str) -> Option<HstsPreloadList> {
#[derive(Deserialize)]
struct HstsEntries {
entries: Vec<HstsEntry>,
@@ -77,7 +104,7 @@ impl HstsList {
let hsts_entries: Option<HstsEntries> = serde_json::from_str(preload_content).ok();
hsts_entries.map(|hsts_entries| {
- let mut hsts_list: HstsList = HstsList::default();
+ let mut hsts_list: HstsPreloadList = HstsPreloadList::default();
for hsts_entry in hsts_entries.entries {
hsts_list.push(hsts_entry);
@@ -87,17 +114,21 @@ impl HstsList {
})
}
- pub fn from_servo_preload() -> HstsList {
+ pub fn from_servo_preload() -> HstsPreloadList {
+ debug!("Intializing HSTS Preload list");
let list = resources::read_string(Resource::HstsPreloadList);
- HstsList::from_preload(&list).unwrap_or_else(|| {
+ HstsPreloadList::from_preload(&list).unwrap_or_else(|| {
error!("HSTS preload file is invalid. Setting HSTS list to default values");
- HstsList::default()
+ HstsPreloadList {
+ entries_map: Default::default(),
+ }
})
}
pub fn is_host_secure(&self, host: &str) -> bool {
let base_domain = reg_suffix(host);
self.entries_map.get(base_domain).is_some_and(|entries| {
+ // No need to check for expiration in the preload list
entries.iter().any(|e| {
if e.include_subdomains {
e.matches_subdomain(host) || e.matches_domain(host)
@@ -108,16 +139,74 @@ impl HstsList {
})
}
- fn has_domain(&self, host: &str, base_domain: &str) -> bool {
+ pub fn has_domain(&self, host: &str, base_domain: &str) -> bool {
self.entries_map
.get(base_domain)
.is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host)))
}
- fn has_subdomain(&self, host: &str, base_domain: &str) -> bool {
+ pub fn has_subdomain(&self, host: &str, base_domain: &str) -> bool {
+ self.entries_map.get(base_domain).is_some_and(|entries| {
+ entries
+ .iter()
+ .any(|e| e.include_subdomains && e.matches_subdomain(host))
+ })
+ }
+
+ pub fn push(&mut self, entry: HstsEntry) {
+ let host = entry.host.clone();
+ let base_domain = reg_suffix(&host);
+ let have_domain = self.has_domain(&entry.host, base_domain);
+ let have_subdomain = self.has_subdomain(&entry.host, base_domain);
+
+ let entries = self.entries_map.entry(base_domain.to_owned()).or_default();
+ if !have_domain && !have_subdomain {
+ entries.push(entry);
+ } else if !have_subdomain {
+ for e in entries {
+ if e.matches_domain(&entry.host) {
+ e.include_subdomains = entry.include_subdomains;
+ // TODO(sebsebmc): We could shrink the the HSTS preload memory use further by using a type
+ // that doesn't store an expiry since all preload entries should be "forever"
+ e.expires_at = entry.expires_at;
+ }
+ }
+ }
+ }
+}
+
+impl HstsList {
+ pub fn is_host_secure(&self, host: &str) -> bool {
+ debug!("HSTS: is {host} secure?");
+ if PRELOAD_LIST_ENTRIES.is_host_secure(host) {
+ info!("{host} is in the preload list");
+ return true;
+ }
+
+ let base_domain = reg_suffix(host);
+ self.entries_map.get(base_domain).is_some_and(|entries| {
+ entries.iter().filter(|e| !e.is_expired()).any(|e| {
+ if e.include_subdomains {
+ e.matches_subdomain(host) || e.matches_domain(host)
+ } else {
+ e.matches_domain(host)
+ }
+ })
+ })
+ }
+
+ fn has_domain(&self, host: &str, base_domain: &str) -> bool {
self.entries_map
.get(base_domain)
- .is_some_and(|entries| entries.iter().any(|e| e.matches_subdomain(host)))
+ .is_some_and(|entries| entries.iter().any(|e| e.matches_domain(host)))
+ }
+
+ fn has_subdomain(&self, host: &str, base_domain: &str) -> bool {
+ self.entries_map.get(base_domain).is_some_and(|entries| {
+ entries
+ .iter()
+ .any(|e| e.include_subdomains && e.matches_subdomain(host))
+ })
}
pub fn push(&mut self, entry: HstsEntry) {
@@ -130,13 +219,14 @@ impl HstsList {
if !have_domain && !have_subdomain {
entries.push(entry);
} else if !have_subdomain {
- for e in entries {
+ for e in entries.iter_mut() {
if e.matches_domain(&entry.host) {
e.include_subdomains = entry.include_subdomains;
- e.max_age = entry.max_age;
+ e.expires_at = entry.expires_at;
}
}
}
+ entries.retain(|e| !e.is_expired());
}
/// Step 2.9 of <https://fetch.spec.whatwg.org/#concept-main-fetch>.