use std::num::NonZeroU64;
use crate::error::{Error, Result};
use crate::http::Response;
use crate::limit::{BucketId, Budget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Sent {
No,
Maybe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Verdict {
Done,
Rejected,
NotRun,
Throttled {
bucket: BucketId,
until_ns: i64,
},
Banned {
bucket: BucketId,
until_ns: i64,
},
ClockSkew,
Unknown,
}
impl Verdict {
#[must_use]
pub const fn from_sent(sent: Sent) -> Self {
match sent {
Sent::No => Self::NotRun,
Sent::Maybe => Self::Unknown,
}
}
#[must_use]
pub const fn may_resend(self) -> bool {
matches!(
self,
Self::NotRun | Self::Throttled { .. } | Self::ClockSkew
)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Now {
pub ns: i64,
pub real_ns: i64,
}
pub fn retry_after_ms(resp: &Response<'_>, server_now_ms: i64) -> Result<Option<u64>> {
retry_after_in(resp.headers, server_now_ms)
}
pub(crate) fn retry_after_in(
headers: &[httparse::Header<'_>],
server_now_ms: i64,
) -> Result<Option<u64>> {
let Some(value) = single(headers, "retry-after")? else {
return Ok(None);
};
if let Some(s) = decimal(value) {
return s
.checked_mul(1_000)
.map(Some)
.ok_or_else(|| malformed("Retry-After", value));
}
let at = http_date_ms(value, server_now_ms).ok_or_else(|| malformed("Retry-After", value))?;
let reference = match single(headers, "date")? {
Some(d) => http_date_ms(d, server_now_ms).ok_or_else(|| malformed("Date", d))?,
None => server_now_ms,
};
Ok(Some(ms_until_epoch(at, reference)))
}
pub fn header_u64(resp: &Response<'_>, name: &str) -> Result<Option<u64>> {
let Some(value) = single(resp.headers, name)? else {
return Ok(None);
};
decimal(value)
.map(Some)
.ok_or_else(|| malformed(name, value))
}
#[must_use]
pub const fn ms_until_epoch(epoch_ms: i64, server_now_ms: i64) -> u64 {
if epoch_ms > server_now_ms {
epoch_ms.abs_diff(server_now_ms)
} else {
0
}
}
pub const BAN_MIN_MS: NonZeroU64 = NonZeroU64::MIN.saturating_add(119_999);
pub fn generic(
resp: &Response<'_>,
scope: BucketId,
budget: &mut Budget,
now: Now,
) -> Result<Verdict> {
Ok(match resp.status {
200..=299 => Verdict::Done,
408 => Verdict::Unknown,
418 => {
let wait = retry_after_ms(resp, now.real_ns.div_euclid(1_000_000));
let ms = wait
.as_ref()
.ok()
.copied()
.flatten()
.and_then(NonZeroU64::new)
.unwrap_or(BAN_MIN_MS);
let until_ns = budget.banned(scope, ms, now.ns)?;
wait?;
Verdict::Banned {
bucket: scope,
until_ns,
}
}
429 => {
let wait = retry_after_ms(resp, now.real_ns.div_euclid(1_000_000));
let until_ns =
budget.throttled(scope, wait.as_ref().ok().copied().flatten(), now.ns)?;
wait?;
Verdict::Throttled {
bucket: scope,
until_ns,
}
}
300..=499 => Verdict::Rejected,
_ => Verdict::Unknown,
})
}
pub fn refused_upgrade(
error: &Error,
scope: BucketId,
budget: &mut Budget,
ban_wait_ms: NonZeroU64,
now: Now,
) -> Result<Option<Verdict>> {
let (status, retry_after_ms, invalid) = match error {
Error::Upgrade {
status,
retry_after_ms,
retry_after_invalid,
..
} => (*status, *retry_after_ms, *retry_after_invalid),
_ => return Ok(None),
};
let verdict = match status {
429 => Verdict::Throttled {
bucket: scope,
until_ns: budget.throttled(scope, retry_after_ms, now.ns)?,
},
418 => {
let wait = retry_after_ms
.and_then(NonZeroU64::new)
.unwrap_or(ban_wait_ms);
Verdict::Banned {
bucket: scope,
until_ns: budget.banned(scope, wait, now.ns)?,
}
}
_ => return Ok(None),
};
if invalid {
return Err(Error::Http(format!(
"refused upgrade ({status}): malformed Retry-After header; held on the fallback wait"
)));
}
Ok(Some(verdict))
}
fn single<'a>(headers: &'a [httparse::Header<'a>], name: &str) -> Result<Option<&'a [u8]>> {
let mut found: Option<&[u8]> = None;
for h in headers.iter().filter(|h| h.name.eq_ignore_ascii_case(name)) {
let v = h.value.trim_ascii();
match found {
Some(f) if f != v => {
return Err(Error::Http(format!(
"header {name} appears twice with different values"
)));
}
_ => found = Some(v),
}
}
Ok(found)
}
fn decimal(v: &[u8]) -> Option<u64> {
if v.is_empty() {
return None;
}
v.iter().try_fold(0u64, |n, &b| {
if b.is_ascii_digit() {
n.checked_mul(10)?.checked_add(u64::from(b - b'0'))
} else {
None
}
})
}
#[cold]
fn malformed(name: &str, value: &[u8]) -> Error {
let shown = String::from_utf8_lossy(&value[..value.len().min(64)]);
Error::Http(format!("malformed {name} header: {shown:?}"))
}
const DAYS: [&[u8]; 7] = [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
const DAYS_LONG: [&[u8]; 7] = [
b"Sunday",
b"Monday",
b"Tuesday",
b"Wednesday",
b"Thursday",
b"Friday",
b"Saturday",
];
const MONTHS: [&[u8]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
];
fn http_date_ms(v: &[u8], now_ms: i64) -> Option<i64> {
imf_fixdate(v)
.or_else(|| rfc850(v, now_ms))
.or_else(|| asctime(v))
}
fn imf_fixdate(v: &[u8]) -> Option<i64> {
let mut c = Cursor(v);
let weekday = c.pick(&DAYS)?;
c.lit(b", ")?;
let day = c.digits(2)?;
c.lit(b" ")?;
let month = c.pick(&MONTHS)?;
c.lit(b" ")?;
let year = c.digits(4)?;
c.lit(b" ")?;
let time = c.time()?;
c.lit(b" GMT")?;
c.end()?;
timestamp(weekday, year, month, day, time)
}
fn rfc850(v: &[u8], now_ms: i64) -> Option<i64> {
let mut c = Cursor(v);
let weekday = c.pick(&DAYS_LONG)?;
c.lit(b", ")?;
let day = c.digits(2)?;
c.lit(b"-")?;
let month = c.pick(&MONTHS)?;
c.lit(b"-")?;
let yy = c.digits(2)?;
c.lit(b" ")?;
let time = c.time()?;
c.lit(b" GMT")?;
c.end()?;
let now_year = year_of_days(now_ms.div_euclid(86_400_000));
let mut year = now_year - now_year.rem_euclid(100) + yy;
if year > now_year + 50 {
year -= 100;
} else if year <= now_year - 50 {
year += 100;
}
timestamp(weekday, year, month, day, time)
}
fn asctime(v: &[u8]) -> Option<i64> {
let mut c = Cursor(v);
let weekday = c.pick(&DAYS)?;
c.lit(b" ")?;
let month = c.pick(&MONTHS)?;
c.lit(b" ")?;
let day = if c.lit(b" ").is_some() {
c.digits(1)?
} else {
c.digits(2)?
};
c.lit(b" ")?;
let time = c.time()?;
c.lit(b" ")?;
let year = c.digits(4)?;
c.end()?;
timestamp(weekday, year, month, day, time)
}
fn timestamp(
weekday: usize,
year: i64,
month: usize,
day: i64,
(h, m, s): (i64, i64, i64),
) -> Option<i64> {
let month = i64::try_from(month).ok()? + 1;
if day < 1 || day > days_in_month(year, month) || h > 23 || m > 59 || s > 60 {
return None;
}
let days = days_from_civil(year, month, day);
if usize::try_from((days + 4).rem_euclid(7)).ok()? != weekday {
return None;
}
Some(((days * 24 + h) * 60 + m) * 60_000 + s * 1_000)
}
const fn days_in_month(year: i64, month: i64) -> i64 {
match month {
2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
const fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let mp = (month + 9) % 12;
let doy = (153 * mp + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
const fn year_of_days(days: i64) -> i64 {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
if mp >= 10 {
yoe + era * 400 + 1
} else {
yoe + era * 400
}
}
struct Cursor<'a>(&'a [u8]);
impl Cursor<'_> {
fn lit(&mut self, s: &[u8]) -> Option<()> {
self.0 = self.0.strip_prefix(s)?;
Some(())
}
fn digits(&mut self, n: usize) -> Option<i64> {
let (d, rest) = self.0.split_at_checked(n)?;
if !d.iter().all(u8::is_ascii_digit) {
return None;
}
self.0 = rest;
Some(d.iter().fold(0, |a, &b| a * 10 + i64::from(b - b'0')))
}
fn pick(&mut self, names: &[&[u8]]) -> Option<usize> {
let i = names.iter().position(|n| self.0.starts_with(n))?;
self.0 = &self.0[names[i].len()..];
Some(i)
}
fn time(&mut self) -> Option<(i64, i64, i64)> {
let h = self.digits(2)?;
self.lit(b":")?;
let m = self.digits(2)?;
self.lit(b":")?;
Some((h, m, self.digits(2)?))
}
const fn end(&self) -> Option<()> {
if self.0.is_empty() { Some(()) } else { None }
}
}