use crate::{request::RequestExt, ReadResponseExt};
use crossbeam_utils::atomic::AtomicCell;
use once_cell::sync::Lazy;
use publicsuffix::{List, Psl};
use std::{
error::Error,
sync::{Arc, RwLock},
thread,
time::{Duration, SystemTime},
};
static TTL: Duration = Duration::from_secs(24 * 60 * 60);
static CACHE: Lazy<ListCache> = Lazy::new(Default::default);
#[derive(Clone)]
struct ListCache {
inner: Arc<RwLock<Inner>>,
}
struct Inner {
list: List,
last_refreshed: Option<SystemTime>,
last_modified: Option<SystemTime>,
is_refreshing: AtomicCell<bool>,
}
impl Default for ListCache {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(Inner {
list: include_str!("list/public_suffix_list.dat")
.parse()
.expect("could not parse bundled public suffix list"),
last_refreshed: Default::default(),
last_modified: Default::default(),
is_refreshing: Default::default(),
})),
}
}
}
impl ListCache {
fn is_public_suffix(&self, domain: &str) -> bool {
let domain = domain.as_bytes();
self.inner
.read()
.unwrap()
.list
.suffix(domain)
.filter(publicsuffix::Suffix::is_known)
.filter(|suffix| suffix == &domain)
.is_some()
}
fn refresh(&self) -> Result<(), Box<dyn Error>> {
let mut inner = self.inner.write().unwrap();
let mut request = http::Request::get(publicsuffix::LIST_URL);
if let Some(last_modified) = inner.last_modified {
request = request.header(
http::header::IF_MODIFIED_SINCE,
httpdate::fmt_http_date(last_modified),
);
}
let mut response = request.body(())?.send()?;
match response.status() {
http::StatusCode::OK => {
inner.list = response.text()?.parse()?;
tracing::debug!("public suffix list updated");
}
http::StatusCode::NOT_MODIFIED => {
tracing::debug!("public suffix list not modified");
}
status => {
tracing::warn!(
"could not update public suffix list, got status code {}",
status,
);
return Ok(());
}
}
if let Some(d) = response.headers().get(http::header::LAST_MODIFIED) {
inner.last_modified = httpdate::parse_http_date(d.to_str().unwrap()).ok();
}
inner.last_refreshed = Some(SystemTime::now());
Ok(())
}
fn refresh_in_background(&self, force: bool) {
let inner = self.inner.read().unwrap();
if !force && !inner.needs_refreshed() {
return;
}
if inner.is_refreshing.compare_exchange(false, true).is_ok() {
let cache = self.clone();
thread::spawn(move || {
if let Err(e) = cache.refresh() {
tracing::warn!("could not refresh public suffix list: {}", e);
}
cache.inner.read().unwrap().is_refreshing.store(false);
});
}
}
}
impl Inner {
fn needs_refreshed(&self) -> bool {
match self.last_refreshed {
Some(last_refreshed) => match last_refreshed.elapsed() {
Ok(elapsed) => elapsed > TTL,
Err(_) => false,
},
None => true,
}
}
}
pub(crate) fn is_public_suffix(domain: impl AsRef<str>) -> bool {
let domain = domain.as_ref();
CACHE.refresh_in_background(false);
CACHE.is_public_suffix(domain)
}
#[cfg(test)]
mod tests {
use std::thread::sleep;
use super::*;
#[test]
fn refresh_cache() {
let cache = ListCache::default();
assert!(cache.inner.read().unwrap().last_refreshed.is_none());
assert!(cache.inner.read().unwrap().last_modified.is_none());
assert!(cache.inner.read().unwrap().needs_refreshed());
cache.refresh_in_background(true);
while cache.inner.read().unwrap().is_refreshing.load() {
sleep(Duration::from_millis(100));
}
assert!(cache.inner.read().unwrap().last_refreshed.is_some());
assert!(cache.inner.read().unwrap().last_modified.is_some());
assert!(!cache.inner.read().unwrap().needs_refreshed());
let last_refreshed = cache.inner.read().unwrap().last_refreshed.unwrap();
let last_modified = cache.inner.read().unwrap().last_modified.unwrap();
cache.refresh_in_background(true);
while cache.inner.read().unwrap().is_refreshing.load() {
sleep(Duration::from_millis(100));
}
assert!(cache.inner.read().unwrap().last_refreshed.unwrap() > last_refreshed);
assert!(cache.inner.read().unwrap().last_modified.unwrap() >= last_modified);
}
}