use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex,
},
time::{Duration, Instant},
};
use jsonwebtoken::jwk::JwkSet;
use jsonwebtoken::Validation;
use salvo_core::http::header::HeaderValue;
use super::{current_time, decode_jwk, DecodingInfo, JwkSetFetch};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CachePolicy {
pub max_age: Duration,
pub stale_while_revalidate: Option<Duration>,
pub stale_if_error: Option<Duration>,
}
impl CachePolicy {
#[must_use]
pub fn from_header_val(value: Option<&HeaderValue>) -> Self {
Self::from_header_values(value)
}
#[must_use]
pub fn from_header_values<'a>(values: impl IntoIterator<Item = &'a HeaderValue>) -> Self {
let mut config = Self::default();
let mut values = values.into_iter();
if let Some(first) = values.next() {
if let Some(second) = values.next() {
let mut combined = first.as_bytes().to_vec();
for value in std::iter::once(second).chain(values) {
combined.push(b',');
combined.extend_from_slice(value.as_bytes());
}
config.parse_bytes(&combined);
} else {
config.parse_bytes(first.as_bytes());
}
}
config
}
fn parse_bytes(&mut self, value: &[u8]) {
const MAX_CACHE_AGE_SECS: u64 = 30 * 24 * 60 * 60;
let mut quoted = false;
let mut escaped = false;
for token in value.split(|byte| {
if escaped {
escaped = false;
return false;
}
if quoted && *byte == b'\\' {
escaped = true;
} else if *byte == b'"' {
quoted = !quoted;
}
*byte == b',' && !quoted
}) {
let Some(equal) = token.iter().position(|byte| *byte == b'=') else {
continue;
};
let key = token[..equal].trim_ascii();
let Some(secs) = delta_seconds(token[equal + 1..].trim_ascii()) else {
continue;
};
if key.eq_ignore_ascii_case(b"max-age") {
self.max_age = Duration::from_secs(secs.clamp(1, MAX_CACHE_AGE_SECS));
} else if key.eq_ignore_ascii_case(b"stale-while-revalidate") {
self.stale_while_revalidate =
Some(Duration::from_secs(secs.min(MAX_CACHE_AGE_SECS)));
} else if key.eq_ignore_ascii_case(b"stale-if-error") {
self.stale_if_error = Some(Duration::from_secs(secs.min(MAX_CACHE_AGE_SECS)));
}
}
}
}
fn delta_seconds(value: &[u8]) -> Option<u64> {
let quoted = value.first() == Some(&b'"');
let value = if quoted {
value.strip_prefix(b"\"")?.strip_suffix(b"\"")?
} else {
value
};
let mut bytes = value.iter().copied();
let mut result = 0u64;
let mut digits = 0;
while let Some(byte) = bytes.next() {
let byte = if quoted && byte == b'\\' {
bytes.next()?
} else {
byte
};
if !byte.is_ascii_digit() {
return None;
}
result = result
.checked_mul(10)?
.checked_add(u64::from(byte - b'0'))?;
digits += 1;
}
(digits != 0).then_some(result)
}
impl Default for CachePolicy {
fn default() -> Self {
Self {
max_age: Duration::from_secs(1),
stale_while_revalidate: Some(Duration::from_secs(1)),
stale_if_error: Some(Duration::from_secs(60)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateAction {
NoUpdate,
JwksUpdate,
CacheUpdate(CachePolicy),
JwksAndCacheUpdate(CachePolicy),
}
#[derive(Debug)]
pub struct CacheState {
last_update: AtomicU64,
is_revalidating: AtomicBool,
is_error: AtomicBool,
last_revalidation: Mutex<Option<Instant>>,
}
impl CacheState {
#[must_use]
pub fn new() -> Self {
Self {
last_update: AtomicU64::new(current_time()),
is_revalidating: AtomicBool::new(false),
is_error: AtomicBool::new(false),
last_revalidation: Mutex::new(None),
}
}
pub fn is_error(&self) -> bool {
self.is_error.load(Ordering::SeqCst)
}
pub fn set_is_error(&self, value: bool) {
self.is_error.store(value, Ordering::SeqCst);
}
pub fn last_update(&self) -> u64 {
self.last_update.load(Ordering::SeqCst)
}
pub fn set_last_update(&self, timestamp: u64) {
self.last_update.store(timestamp, Ordering::SeqCst);
}
pub fn is_revalidating(&self) -> bool {
self.is_revalidating.load(Ordering::Acquire)
}
pub fn begin_revalidation(&self) -> bool {
self.is_revalidating
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
pub(super) fn begin_revalidation_after(&self, interval: Duration) -> bool {
let mut last = self
.last_revalidation
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if last.is_some_and(|last| last.elapsed() < interval) || !self.begin_revalidation() {
return false;
}
*last = Some(Instant::now());
true
}
pub fn finish_revalidation(&self) {
let mut last = self
.last_revalidation
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if last.is_some() {
*last = Some(Instant::now());
}
self.is_revalidating.store(false, Ordering::Release);
}
}
impl Default for CacheState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct JwkSetStore {
pub jwks: JwkSet,
decoding_map: HashMap<String, Arc<DecodingInfo>>,
pub cache_policy: CachePolicy,
validation: Validation,
allow_symmetric_jwks: bool,
}
impl JwkSetStore {
#[must_use]
pub fn new(jwks: JwkSet, cache_policy: CachePolicy, validation: Validation) -> Self {
Self {
jwks,
decoding_map: HashMap::new(),
cache_policy,
validation,
allow_symmetric_jwks: false,
}
}
#[must_use]
pub fn with_allow_symmetric_jwks(mut self, allow: bool) -> Self {
self.allow_symmetric_jwks = allow;
self
}
fn update_jwks(&mut self, new_jwks: JwkSet) {
self.jwks = new_jwks;
let allow_symmetric = self.allow_symmetric_jwks;
let keys = self
.jwks
.keys
.iter()
.filter_map(|i| decode_jwk(i, &self.validation, allow_symmetric).ok());
self.decoding_map.clear();
for key in keys {
self.decoding_map.insert(key.0, Arc::new(key.1));
}
}
#[must_use]
pub fn get_key(&self, kid: &str) -> Option<Arc<DecodingInfo>> {
self.decoding_map.get(kid).cloned()
}
pub(crate) fn update_fetch(&mut self, fetch: JwkSetFetch) -> UpdateAction {
tracing::debug!("Decoding JWKS");
let time = Instant::now();
let new_jwks = fetch.jwks;
let cache_policy = fetch.cache_policy.unwrap_or(self.cache_policy);
let result = match (self.jwks == new_jwks, self.cache_policy == cache_policy) {
(true, true) => {
tracing::debug!("JWKS Content has not changed since last update");
UpdateAction::NoUpdate
}
(false, true) => {
tracing::info!("JWKS Content has changed since last update");
self.update_jwks(new_jwks);
UpdateAction::JwksUpdate
}
(true, false) => {
self.cache_policy = cache_policy;
UpdateAction::CacheUpdate(cache_policy)
}
(false, false) => {
tracing::info!("cache-control header and JWKS content has changed since last update");
self.update_jwks(new_jwks);
self.cache_policy = cache_policy;
UpdateAction::JwksAndCacheUpdate(cache_policy)
}
};
let elapsed = time.elapsed();
tracing::debug!("Decoded and parsed JWKS in {:#?}", elapsed);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_policy_default() {
let policy = CachePolicy::default();
assert_eq!(policy.max_age, Duration::from_secs(1));
assert_eq!(policy.stale_while_revalidate, Some(Duration::from_secs(1)));
assert_eq!(policy.stale_if_error, Some(Duration::from_secs(60)));
}
#[test]
fn test_cache_policy_from_header_val_max_age() {
let header_value = HeaderValue::from_static("max-age=3600");
let policy = CachePolicy::from_header_val(Some(&header_value));
assert_eq!(policy.max_age, Duration::from_secs(3600));
assert_eq!(policy.stale_while_revalidate, Some(Duration::from_secs(1)));
assert_eq!(policy.stale_if_error, Some(Duration::from_secs(60)));
}
#[test]
fn test_cache_policy_from_header_val_stale_while_revalidate() {
let header_value = HeaderValue::from_static("max-age=3600, stale-while-revalidate=60");
let policy = CachePolicy::from_header_val(Some(&header_value));
assert_eq!(policy.max_age, Duration::from_secs(3600));
assert_eq!(policy.stale_while_revalidate, Some(Duration::from_secs(60)));
assert_eq!(policy.stale_if_error, Some(Duration::from_secs(60)));
}
#[test]
fn test_cache_policy_from_header_val_stale_if_error() {
let header_value = HeaderValue::from_static("max-age=3600, stale-if-error=120");
let policy = CachePolicy::from_header_val(Some(&header_value));
assert_eq!(policy.max_age, Duration::from_secs(3600));
assert_eq!(policy.stale_while_revalidate, Some(Duration::from_secs(1)));
assert_eq!(policy.stale_if_error, Some(Duration::from_secs(120)));
}
#[test]
fn test_cache_policy_from_header_val_all() {
let header_value = HeaderValue::from_static("max-age=3600, stale-while-revalidate=60, stale-if-error=120");
let policy = CachePolicy::from_header_val(Some(&header_value));
assert_eq!(policy.max_age, Duration::from_secs(3600));
assert_eq!(policy.stale_while_revalidate, Some(Duration::from_secs(60)));
assert_eq!(policy.stale_if_error, Some(Duration::from_secs(120)));
}
#[test]
fn test_cache_policy_from_header_val_none() {
let policy = CachePolicy::from_header_val(None);
assert_eq!(policy, CachePolicy::default());
}
#[test]
fn test_cache_policy_from_header_val_invalid() {
let header_value = HeaderValue::from_static("invalid-directive");
let policy = CachePolicy::from_header_val(Some(&header_value));
assert_eq!(policy, CachePolicy::default());
}
#[test]
fn test_cache_state_revalidation_claim_is_atomic() {
let state = CacheState::new();
assert!(state.begin_revalidation());
assert!(state.is_revalidating());
assert!(!state.begin_revalidation());
state.finish_revalidation();
assert!(!state.is_revalidating());
assert!(state.begin_revalidation());
}
#[test]
fn revalidation_cooldown_is_shared_and_expires() {
let state = Arc::new(CacheState::new());
let clone = Arc::clone(&state);
let interval = Duration::from_secs(1);
assert!(state.begin_revalidation_after(interval));
assert!(!clone.begin_revalidation_after(interval));
state.finish_revalidation();
assert!(!clone.begin_revalidation_after(interval));
*state.last_revalidation.lock().unwrap() = Some(Instant::now() - interval);
assert!(clone.begin_revalidation_after(interval));
}
}