leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
Documentation
use std::time::{Duration, SystemTime};

use crate::util::epoch_plus;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WaitFormat {
    Seconds,
    UnixSeconds,
    HttpDate,
}

#[derive(Debug, Clone)]
pub(crate) struct WaitHeader {
    pub(crate) name: String,
    pub(crate) format: WaitFormat,
}

pub(crate) const RETRY_AFTER: &str = "retry-after";
const RETRY_AFTER_FORMATS: [WaitFormat; 2] = [WaitFormat::Seconds, WaitFormat::HttpDate];

impl WaitFormat {
    pub(crate) fn parse(self, value: &str) -> Option<Duration> {
        let value = value.trim();
        match self {
            Self::Seconds => parse_seconds(value),
            Self::UnixSeconds => until(epoch_plus(parse_seconds(value)?)?),
            Self::HttpDate => until(parse_imf_fixdate(value)?),
        }
    }
}

pub(crate) fn parse_retry_after(value: &str) -> Option<Duration> {
    RETRY_AFTER_FORMATS
        .iter()
        .find_map(|format| format.parse(value))
}

fn parse_seconds(value: &str) -> Option<Duration> {
    if let Ok(secs) = value.parse::<u64>() {
        return Some(Duration::from_secs(secs));
    }
    let secs = value.parse::<f64>().ok()?;
    Duration::try_from_secs_f64(secs).ok()
}

fn until(when: SystemTime) -> Option<Duration> {
    Some(
        when.duration_since(SystemTime::now())
            .unwrap_or(Duration::ZERO),
    )
}

fn parse_imf_fixdate(s: &str) -> Option<SystemTime> {
    let rest = s.split_once(", ")?.1;
    let mut parts = rest.split_whitespace();
    let day: u32 = parts.next()?.parse().ok()?;
    let month = month_num(parts.next()?)?;
    let year: i32 = parts.next()?.parse().ok()?;
    let hms = parts.next()?;
    let tz = parts.next()?;
    if !tz.eq_ignore_ascii_case("GMT") && !tz.eq_ignore_ascii_case("UTC") {
        return None;
    }
    let mut t = hms.split(':');
    let hour: u32 = t.next()?.parse().ok()?;
    let min: u32 = t.next()?.parse().ok()?;
    let sec: u32 = t.next()?.parse().ok()?;
    unix_from_ymd_hms(year, month, day, hour, min, sec)
}

const MONTHS: [&str; 12] = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];

const IMF_FIXDATE_YEARS: std::ops::RangeInclusive<i32> = 1970..=9999;

fn month_num(month: &str) -> Option<u32> {
    let index = MONTHS.iter().position(|m| *m == month)?;
    u32::try_from(index + 1).ok()
}

pub(super) fn unix_from_ymd_hms(
    year: i32,
    month: u32,
    day: u32,
    hour: u32,
    min: u32,
    sec: u32,
) -> Option<SystemTime> {
    if !date_in_range(year, month, day) || hour > 23 || min > 59 || sec > 59 {
        return None;
    }
    let secs = days_from_civil(year, month, day)?
        .checked_mul(86400)?
        .checked_add(i64::from(hour) * 3600 + i64::from(min) * 60 + i64::from(sec))?;
    epoch_plus(Duration::from_secs(u64::try_from(secs).ok()?))
}

fn date_in_range(year: i32, month: u32, day: u32) -> bool {
    IMF_FIXDATE_YEARS.contains(&year) && (1..=12).contains(&month) && (1..=31).contains(&day)
}

fn days_from_civil(year: i32, month: u32, day: u32) -> Option<i64> {
    let y = if month <= 2 {
        year.checked_sub(1)?
    } else {
        year
    };
    let era = y.div_euclid(400);
    let yoe = u32::try_from(y.checked_sub(era.checked_mul(400)?)?).ok()?;
    let shifted = if month > 2 { month - 3 } else { month + 9 };
    let doy = (153 * shifted + 2) / 5 + day - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    Some(i64::from(era) * 146097 + i64::from(doe) - 719468)
}