libauthcekunit 2.0.0

Super robust CSRF token extractor with configurable retry, logging, and strict validation
Documentation
use crate::config::CONFIG;
use crate::error::{AuthError, Result};
use regex::Regex;
use tracing::debug;
pub fn extract_token(html: &str) -> Result<String> {
    debug!("Extracting token from HTML ({} bytes)", html.len());
    let re = Regex::new(r#"(?i)name=["']_token["']\s+value=["']([^"'<>]+)["']"#)?;
    if let Some(caps) = re.captures(html) {
        let token = caps[1].to_string();
        debug!("Regex found token: {}...", &token[..token.len().min(8)]);
        return validate_token(&token);
    }
    if let Some(token) = manual_extract(html) {
        debug!("Manual scan found token");
        return validate_token(&token);
    }
    Err(AuthError::TokenNotFound)
}
fn manual_extract(html: &str) -> Option<String> {
    let needle_name = "name=\"_token\"";
    let needle_name_single = "name='_token'";
    let value_pat = "value=";
    let mut start = 0;
    while let Some(pos) = html[start..].find(needle_name) {
        let abs = start + pos;
        if let Some(token) = extract_value_after(&html[abs..]) {
            return Some(token);
        }
        start = abs + 1;
    }
    start = 0;
    while let Some(pos) = html[start..].find(needle_name_single) {
        let abs = start + pos;
        if let Some(token) = extract_value_after(&html[abs..]) {
            return Some(token);
        }
        start = abs + 1;
    }
    let mut idx = 0;
    while let Some(val_pos) = html[idx..].find(value_pat) {
        let abs = val_pos + idx;
        let after = &html[abs + 6..];
        let quote = after.chars().next()?;
        if quote != '"' && quote != '\'' {
            idx = abs + 1;
            continue;
        }
        let end = after[1..].find(quote)?;
        let token = &after[1..=end];
        let ctx_start = if abs > 200 { abs - 200 } else { 0 };
        let context = &html[ctx_start..abs];
        if context.contains("name=\"_token\"") || context.contains("name='_token'") {
            return Some(token.to_string());
        }
        idx = abs + 1;
    }
    None
}
fn extract_value_after(slice: &str) -> Option<String> {
    let value_pat = "value=";
    let pos = slice.find(value_pat)?;
    let after = &slice[pos + 6..];
    let quote = after.chars().next()?;
    if quote != '"' && quote != '\'' {
        return None;
    }
    let end = after[1..].find(quote)?;
    Some(after[1..=end].to_string())
}
fn validate_token(token: &str) -> Result<String> {
    let len = token.len();
    if len < CONFIG.token_min_length {
        return Err(AuthError::TokenInvalid {
            reason: format!("too short (min {})", CONFIG.token_min_length),
        });
    }
    if len > CONFIG.token_max_length {
        return Err(AuthError::TokenInvalid {
            reason: format!("too long (max {})", CONFIG.token_max_length),
        });
    }
    let valid = token.chars().all(|c| {
        c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+' || c == '=' || c == '/'
    });
    if !valid {
        return Err(AuthError::TokenInvalid {
            reason: "contains invalid characters".into(),
        });
    }
    Ok(token.to_string())
}