use http::HeaderValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AltSvc {
pub protocol: String,
pub host: String,
pub port: u16,
pub max_age: u32,
pub persist: bool,
}
pub(crate) fn parse_alt_svc(header: &HeaderValue) -> Option<Vec<AltSvc>> {
let value = header.to_str().ok()?;
let value = value.trim();
if value.eq_ignore_ascii_case("clear") {
return Some(Vec::new());
}
let mut entries = Vec::new();
for part in split_entries(value) {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(entry) = parse_alt_value(part) {
entries.push(entry);
}
}
if entries.is_empty() {
None
} else {
Some(entries)
}
}
#[derive(Debug, Clone)]
struct AltSvcEntry {
alt_svc: AltSvc,
inserted_at: std::time::Instant,
}
pub(crate) struct AltSvcCache {
entries: tokio::sync::RwLock<std::collections::HashMap<(String, u16), Vec<AltSvcEntry>>>,
}
impl AltSvcCache {
pub(crate) fn new() -> Self {
Self {
entries: tokio::sync::RwLock::new(std::collections::HashMap::new()),
}
}
pub(crate) async fn insert(&self, authority: (String, u16), entries: Vec<AltSvc>) {
if entries.is_empty() {
self.entries.write().await.remove(&authority);
return;
}
let now = std::time::Instant::now();
let alt_svc_entries: Vec<AltSvcEntry> = entries
.into_iter()
.map(|alt_svc| AltSvcEntry {
alt_svc,
inserted_at: now,
})
.collect();
self.entries
.write()
.await
.insert(authority, alt_svc_entries);
}
pub(crate) async fn get(&self, authority: &(String, u16)) -> Option<Vec<AltSvc>> {
let guard = self.entries.read().await;
let cached = guard.get(authority)?;
let now = std::time::Instant::now();
let alive: Vec<AltSvc> = cached
.iter()
.filter(|e| {
let max_age = std::time::Duration::from_secs(u64::from(e.alt_svc.max_age));
e.inserted_at + max_age >= now
})
.map(|e| e.alt_svc.clone())
.collect();
if alive.is_empty() { None } else { Some(alive) }
}
#[allow(dead_code)] pub(crate) async fn invalidate(&self, authority: &(String, u16)) {
self.entries.write().await.remove(authority);
}
}
pub(crate) struct H3FailureTracker {
failures: tokio::sync::RwLock<std::collections::HashMap<(String, u16), std::time::Instant>>,
cooldown: std::time::Duration,
}
impl H3FailureTracker {
pub(crate) fn new(cooldown: std::time::Duration) -> Self {
Self {
failures: tokio::sync::RwLock::new(std::collections::HashMap::new()),
cooldown,
}
}
pub(crate) async fn is_blocked(&self, authority: &(String, u16)) -> bool {
let guard = self.failures.read().await;
match guard.get(authority) {
Some(failure_time) => {
let elapsed = failure_time.elapsed();
elapsed < self.cooldown
}
None => false,
}
}
pub(crate) async fn record_failure(&self, authority: (String, u16)) {
self.failures
.write()
.await
.insert(authority, std::time::Instant::now());
}
pub(crate) async fn clear(&self, authority: &(String, u16)) {
self.failures.write().await.remove(authority);
}
}
fn split_entries(input: &str) -> Vec<&str> {
split_quoted(input, ',')
}
fn split_params(input: &str) -> Vec<&str> {
split_quoted(input, ';')
}
fn split_quoted(input: &str, delimiter: char) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_quotes = false;
let mut escaped = false;
for (i, ch) in input.char_indices() {
if escaped {
escaped = false;
continue;
}
match ch {
'"' => in_quotes = !in_quotes,
'\\' => escaped = true,
c if c == delimiter && !in_quotes => {
parts.push(&input[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < input.len() {
parts.push(&input[start..]);
}
parts
}
fn parse_alt_value(input: &str) -> Option<AltSvc> {
let input = input.trim();
let eq_pos = input.find('=')?;
let protocol = input[..eq_pos].trim();
if !is_token(protocol) {
return None;
}
let rest = input[eq_pos + 1..].trim();
let (authority, rest) = parse_quoted_string(rest)?;
let (host, port) = parse_authority(&authority)?;
let mut max_age: u32 = 86400;
let mut persist = false;
let rest = rest.trim();
for param_str in split_params(rest) {
let param_str = param_str.trim();
if param_str.is_empty() {
continue;
}
let param_eq = match param_str.find('=') {
Some(p) => p,
None => continue,
};
let name = param_str[..param_eq].trim();
let value_part = param_str[param_eq + 1..].trim();
let value = if value_part.starts_with('"') {
match parse_quoted_string(value_part) {
Some((v, _)) => v,
None => continue,
}
} else if is_token(value_part) {
value_part.to_string()
} else {
continue;
};
match name {
"ma" => {
if let Ok(v) = value.parse::<u32>() {
max_age = v;
}
}
"persist" => {
persist = value == "1";
}
_ => {
}
}
}
Some(AltSvc {
protocol: protocol.to_string(),
host,
port,
max_age,
persist,
})
}
fn parse_quoted_string(input: &str) -> Option<(String, &str)> {
let input = input.trim();
let mut chars = input.char_indices();
match chars.next() {
Some((_, '"')) => {}
_ => return None,
}
let mut result = String::new();
let mut escaped = false;
for (pos, ch) in chars {
if escaped {
match ch {
'"' | '\\' => result.push(ch),
_ => return None,
}
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
let rest = &input[pos + ch.len_utf8()..];
return Some((result, rest));
} else {
result.push(ch);
}
}
None
}
fn parse_authority(authority: &str) -> Option<(String, u16)> {
if authority.is_empty() {
return None;
}
if let Some(port_str) = authority.strip_prefix(':') {
let port = port_str.parse::<u16>().ok()?;
return Some((String::new(), port));
}
if let Some(colon_pos) = authority.rfind(':') {
let host = &authority[..colon_pos];
let port_str = &authority[colon_pos + 1..];
let port = port_str.parse::<u16>().ok()?;
if host.is_empty() {
return None;
}
Some((host.to_string(), port))
} else {
Some((authority.to_string(), 443))
}
}
fn is_token(s: &str) -> bool {
if s.is_empty() {
return false;
}
s.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '.'
| '^'
| '_'
| '`'
| '|'
| '~'
)
})
}
#[cfg(test)]
mod tests {
use http::HeaderValue;
use super::*;
fn hv(s: &'static str) -> HeaderValue {
HeaderValue::from_static(s)
}
#[test]
fn alt_svc_parse_valid() {
let entries = parse_alt_svc(&hv(r#"h3=":443""#)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].protocol, "h3");
assert_eq!(entries[0].host, "");
assert_eq!(entries[0].port, 443);
assert_eq!(entries[0].max_age, 86400);
assert!(!entries[0].persist);
}
#[test]
fn alt_svc_parse_clear() {
let entries = parse_alt_svc(&hv("clear")).unwrap();
assert!(entries.is_empty());
}
#[test]
fn alt_svc_parse_malformed() {
assert!(parse_alt_svc(&hv("garbage")).is_none());
}
#[test]
fn alt_svc_parse_multiple() {
let entries = parse_alt_svc(&hv(r#"h3=":443", h2="alt.example.com:8443""#)).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].protocol, "h3");
assert_eq!(entries[0].host, "");
assert_eq!(entries[0].port, 443);
assert_eq!(entries[1].protocol, "h2");
assert_eq!(entries[1].host, "alt.example.com");
assert_eq!(entries[1].port, 8443);
}
#[test]
fn alt_svc_parse_with_ma() {
let entries = parse_alt_svc(&hv(r#"h3=":443"; ma=3600"#)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].max_age, 3600);
}
#[test]
fn alt_svc_parse_with_persist() {
let entries = parse_alt_svc(&hv(r#"h3=":443"; persist=1"#)).unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].persist);
}
#[test]
fn alt_svc_parse_with_host() {
let entries = parse_alt_svc(&hv(r#"h3="alt.example.com:443""#)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].host, "alt.example.com");
assert_eq!(entries[0].port, 443);
}
#[test]
fn alt_svc_parse_case_insensitive_clear() {
let entries = parse_alt_svc(&hv("CLEAR")).unwrap();
assert!(entries.is_empty());
}
#[tokio::test]
async fn alt_svc_cache_insert_lookup() {
let cache = AltSvcCache::new();
let authority = ("example.com".to_string(), 443);
let entries = vec![AltSvc {
protocol: "h3".to_string(),
host: String::new(),
port: 443,
max_age: 86400,
persist: false,
}];
cache.insert(authority.clone(), entries).await;
let result = cache.get(&authority).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].protocol, "h3");
assert_eq!(result[0].host, "");
assert_eq!(result[0].port, 443);
}
#[tokio::test]
async fn alt_svc_cache_expiry() {
let cache = AltSvcCache::new();
let authority = ("example.com".to_string(), 443);
let entries = vec![AltSvc {
protocol: "h3".to_string(),
host: String::new(),
port: 443,
max_age: 0,
persist: false,
}];
cache.insert(authority.clone(), entries).await;
let result = cache.get(&authority).await;
assert!(result.is_none());
}
#[tokio::test]
async fn alt_svc_cache_invalidation() {
let cache = AltSvcCache::new();
let authority = ("example.com".to_string(), 443);
let entries = vec![AltSvc {
protocol: "h3".to_string(),
host: String::new(),
port: 443,
max_age: 86400,
persist: false,
}];
cache.insert(authority.clone(), entries).await;
assert!(cache.get(&authority).await.is_some());
cache.invalidate(&authority).await;
assert!(cache.get(&authority).await.is_none());
}
#[tokio::test]
async fn h3_failure_tracker_is_blocked_after_failure() {
let tracker = H3FailureTracker::new(std::time::Duration::from_secs(60));
let authority = ("example.com".to_string(), 443);
assert!(!tracker.is_blocked(&authority).await);
tracker.record_failure(authority.clone()).await;
assert!(tracker.is_blocked(&authority).await);
}
#[tokio::test]
async fn h3_failure_tracker_clear() {
let tracker = H3FailureTracker::new(std::time::Duration::from_secs(60));
let authority = ("example.com".to_string(), 443);
tracker.record_failure(authority.clone()).await;
assert!(tracker.is_blocked(&authority).await);
tracker.clear(&authority).await;
assert!(!tracker.is_blocked(&authority).await);
}
#[tokio::test]
async fn h3_failure_tracker_cooldown_expires() {
let tracker = H3FailureTracker::new(std::time::Duration::from_millis(100));
let authority = ("example.com".to_string(), 443);
tracker.record_failure(authority.clone()).await;
assert!(tracker.is_blocked(&authority).await);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(!tracker.is_blocked(&authority).await);
}
#[tokio::test]
async fn h3_failure_tracker_different_authorities() {
let tracker = H3FailureTracker::new(std::time::Duration::from_secs(60));
let a1 = ("example.com".to_string(), 443);
let a2 = ("example.com".to_string(), 8443);
tracker.record_failure(a1.clone()).await;
assert!(tracker.is_blocked(&a1).await);
assert!(!tracker.is_blocked(&a2).await);
}
#[tokio::test]
async fn alt_svc_cache_clear_signal() {
let cache = AltSvcCache::new();
let authority = ("example.com".to_string(), 443);
let entries = vec![AltSvc {
protocol: "h3".to_string(),
host: String::new(),
port: 443,
max_age: 86400,
persist: false,
}];
cache.insert(authority.clone(), entries).await;
assert!(cache.get(&authority).await.is_some());
cache.insert(authority.clone(), Vec::new()).await;
assert!(cache.get(&authority).await.is_none());
}
}