use crate::error::{Error, Result};
use phf::phf_map;
#[derive(Debug, Clone)]
pub struct VariableSpec {
pub name: VariableName,
pub selection: Option<Selection>,
pub count_mode: bool,
pub exclusions: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum Selection {
Key(String),
Regex(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VariableName {
Args, ArgsGet, ArgsPost, ArgsNames, ArgsGetNames, ArgsPostNames, ArgsCombinedSize,
RequestUri, RequestUriRaw, RequestFilename, RequestBasename, RequestLine,
RequestMethod, RequestProtocol, RequestHeaders, RequestHeadersNames,
RequestCookies, RequestCookiesNames, RequestBody, RequestBodyLength, QueryString,
ResponseStatus, ResponseProtocol, ResponseHeaders, ResponseHeadersNames,
ResponseBody, ResponseContentType, ResponseContentLength,
RemoteAddr, RemotePort, RemoteHost, RemoteUser,
ServerAddr, ServerPort, ServerName,
Tx, Session, Env, Ip, Global, Resource, User, Geo,
MatchedVar, MatchedVars, MatchedVarName, MatchedVarsNames,
Time, TimeEpoch, TimeDay, TimeHour, TimeMin, TimeSec, TimeWday, TimeMon, TimeYear,
Files, FilesSizes, FilesTmpnames, FilesCombinedSize, FilesNames,
UniqueId, InboundAnomalyScore, OutboundAnomalyScore, Duration,
MultipartBoundaryQuoted, MultipartBoundaryWhitespace, MultipartDataAfter,
MultipartDataBefore, MultipartFileLimitExceeded, MultipartHeaderFolding,
MultipartInvalidHeaderFolding, MultipartInvalidPart, MultipartInvalidQuoting,
MultipartLfLine, MultipartMissingSemicolon, MultipartStrictError,
MultipartUnmatchedBoundary, MultipartPartHeaders,
Xml,
WebserverErrorLog, HighestSeverity, StatusLine, FullRequest, FullRequestLength,
AuthType,
ReqBodyProcessor, ReqBodyError, ReqBodyErrorMsg, ReqBodyProcessorError, ReqBodyProcessorErrorMsg,
MultipartStrictCheck,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XmlTarget {
Elements,
Attributes,
All,
}
impl XmlTarget {
pub fn from_selection(selection: Option<&Selection>) -> Option<Self> {
match selection {
None => Some(XmlTarget::All),
Some(Selection::Key(sel)) => match sel.trim() {
"/*" => Some(XmlTarget::Elements),
"//@*" => Some(XmlTarget::Attributes),
_ => None,
},
Some(Selection::Regex(_)) => None,
}
}
}
impl VariableName {
pub fn is_implemented(&self) -> bool {
match self {
VariableName::Xml | VariableName::Args | VariableName::ArgsGet | VariableName::ArgsPost |
VariableName::ArgsNames | VariableName::ArgsGetNames | VariableName::ArgsPostNames |
VariableName::ArgsCombinedSize | VariableName::RequestUri | VariableName::RequestUriRaw |
VariableName::RequestFilename | VariableName::RequestBasename | VariableName::RequestLine |
VariableName::RequestMethod | VariableName::RequestProtocol | VariableName::RequestHeaders |
VariableName::RequestHeadersNames | VariableName::RequestCookies | VariableName::RequestCookiesNames |
VariableName::RequestBody | VariableName::RequestBodyLength | VariableName::QueryString |
VariableName::ResponseStatus | VariableName::ResponseHeaders | VariableName::ResponseBody |
VariableName::ResponseContentType | VariableName::RemoteAddr | VariableName::RemotePort |
VariableName::ServerAddr | VariableName::ServerPort | VariableName::ServerName |
VariableName::Tx | VariableName::MatchedVar | VariableName::MatchedVars |
VariableName::MatchedVarName | VariableName::MatchedVarsNames | VariableName::Files |
VariableName::FilesNames | VariableName::MultipartPartHeaders | VariableName::ReqBodyProcessor |
VariableName::ReqBodyError | VariableName::ReqBodyErrorMsg | VariableName::ReqBodyProcessorError |
VariableName::ReqBodyProcessorErrorMsg => true,
VariableName::ResponseProtocol | VariableName::ResponseHeadersNames | VariableName::ResponseContentLength |
VariableName::RemoteHost | VariableName::RemoteUser | VariableName::Session |
VariableName::Env | VariableName::Ip | VariableName::Global |
VariableName::Resource | VariableName::User | VariableName::Geo |
VariableName::Time | VariableName::TimeEpoch | VariableName::TimeDay |
VariableName::TimeHour | VariableName::TimeMin | VariableName::TimeSec |
VariableName::TimeWday | VariableName::TimeMon | VariableName::TimeYear |
VariableName::FilesSizes | VariableName::FilesTmpnames | VariableName::FilesCombinedSize |
VariableName::UniqueId | VariableName::InboundAnomalyScore | VariableName::OutboundAnomalyScore |
VariableName::Duration | VariableName::MultipartBoundaryQuoted | VariableName::MultipartBoundaryWhitespace |
VariableName::MultipartDataAfter | VariableName::MultipartDataBefore | VariableName::MultipartFileLimitExceeded |
VariableName::MultipartHeaderFolding | VariableName::MultipartInvalidHeaderFolding | VariableName::MultipartInvalidPart |
VariableName::MultipartInvalidQuoting | VariableName::MultipartLfLine | VariableName::MultipartMissingSemicolon |
VariableName::MultipartStrictError | VariableName::MultipartUnmatchedBoundary |
VariableName::WebserverErrorLog | VariableName::HighestSeverity | VariableName::StatusLine |
VariableName::FullRequest | VariableName::FullRequestLength | VariableName::AuthType |
VariableName::MultipartStrictCheck => false,
}
}
}
static VARIABLE_MAP: phf::Map<&'static str, VariableName> = phf_map! {
"ARGS" => VariableName::Args,
"ARGS_GET" => VariableName::ArgsGet,
"ARGS_POST" => VariableName::ArgsPost,
"ARGS_NAMES" => VariableName::ArgsNames,
"ARGS_GET_NAMES" => VariableName::ArgsGetNames,
"ARGS_POST_NAMES" => VariableName::ArgsPostNames,
"ARGS_COMBINED_SIZE" => VariableName::ArgsCombinedSize,
"REQUEST_URI" => VariableName::RequestUri,
"REQUEST_URI_RAW" => VariableName::RequestUriRaw,
"REQUEST_FILENAME" => VariableName::RequestFilename,
"REQUEST_BASENAME" => VariableName::RequestBasename,
"REQUEST_LINE" => VariableName::RequestLine,
"REQUEST_METHOD" => VariableName::RequestMethod,
"REQUEST_PROTOCOL" => VariableName::RequestProtocol,
"REQUEST_HEADERS" => VariableName::RequestHeaders,
"REQUEST_HEADERS_NAMES" => VariableName::RequestHeadersNames,
"REQUEST_COOKIES" => VariableName::RequestCookies,
"REQUEST_COOKIES_NAMES" => VariableName::RequestCookiesNames,
"REQUEST_BODY" => VariableName::RequestBody,
"REQUEST_BODY_LENGTH" => VariableName::RequestBodyLength,
"QUERY_STRING" => VariableName::QueryString,
"RESPONSE_STATUS" => VariableName::ResponseStatus,
"RESPONSE_PROTOCOL" => VariableName::ResponseProtocol,
"RESPONSE_HEADERS" => VariableName::ResponseHeaders,
"RESPONSE_HEADERS_NAMES" => VariableName::ResponseHeadersNames,
"RESPONSE_BODY" => VariableName::ResponseBody,
"RESPONSE_CONTENT_TYPE" => VariableName::ResponseContentType,
"RESPONSE_CONTENT_LENGTH" => VariableName::ResponseContentLength,
"REMOTE_ADDR" => VariableName::RemoteAddr,
"REMOTE_PORT" => VariableName::RemotePort,
"REMOTE_HOST" => VariableName::RemoteHost,
"REMOTE_USER" => VariableName::RemoteUser,
"SERVER_ADDR" => VariableName::ServerAddr,
"SERVER_PORT" => VariableName::ServerPort,
"SERVER_NAME" => VariableName::ServerName,
"TX" => VariableName::Tx,
"SESSION" => VariableName::Session,
"ENV" => VariableName::Env,
"IP" => VariableName::Ip,
"GLOBAL" => VariableName::Global,
"RESOURCE" => VariableName::Resource,
"USER" => VariableName::User,
"GEO" => VariableName::Geo,
"MATCHED_VAR" => VariableName::MatchedVar,
"MATCHED_VARS" => VariableName::MatchedVars,
"MATCHED_VAR_NAME" => VariableName::MatchedVarName,
"MATCHED_VARS_NAMES" => VariableName::MatchedVarsNames,
"TIME" => VariableName::Time,
"TIME_EPOCH" => VariableName::TimeEpoch,
"TIME_DAY" => VariableName::TimeDay,
"TIME_HOUR" => VariableName::TimeHour,
"TIME_MIN" => VariableName::TimeMin,
"TIME_SEC" => VariableName::TimeSec,
"TIME_WDAY" => VariableName::TimeWday,
"TIME_MON" => VariableName::TimeMon,
"TIME_YEAR" => VariableName::TimeYear,
"FILES" => VariableName::Files,
"FILES_SIZES" => VariableName::FilesSizes,
"FILES_TMPNAMES" => VariableName::FilesTmpnames,
"FILES_COMBINED_SIZE" => VariableName::FilesCombinedSize,
"FILES_NAMES" => VariableName::FilesNames,
"UNIQUE_ID" => VariableName::UniqueId,
"DURATION" => VariableName::Duration,
"HIGHEST_SEVERITY" => VariableName::HighestSeverity,
"STATUS_LINE" => VariableName::StatusLine,
"FULL_REQUEST" => VariableName::FullRequest,
"FULL_REQUEST_LENGTH" => VariableName::FullRequestLength,
"AUTH_TYPE" => VariableName::AuthType,
"XML" => VariableName::Xml,
"REQBODY_PROCESSOR" => VariableName::ReqBodyProcessor,
"REQBODY_ERROR" => VariableName::ReqBodyError,
"REQBODY_ERROR_MSG" => VariableName::ReqBodyErrorMsg,
"REQBODY_PROCESSOR_ERROR" => VariableName::ReqBodyProcessorError,
"REQBODY_PROCESSOR_ERROR_MSG" => VariableName::ReqBodyProcessorErrorMsg,
"MULTIPART_STRICT_ERROR" => VariableName::MultipartStrictCheck,
"MULTIPART_PART_HEADERS" => VariableName::MultipartPartHeaders,
};
impl VariableName {
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
if s.bytes().all(|b| b.is_ascii_uppercase() || b == b'_') {
return VARIABLE_MAP.get(s).copied();
}
let mut buf = [0u8; 64];
let len = s.len().min(64);
for (i, b) in s.bytes().take(len).enumerate() {
buf[i] = b.to_ascii_uppercase();
}
let upper = std::str::from_utf8(&buf[..len]).ok()?;
VARIABLE_MAP.get(upper).copied()
}
#[inline]
pub fn is_collection(&self) -> bool {
matches!(
self,
Self::Args | Self::ArgsGet | Self::ArgsPost | Self::ArgsNames
| Self::RequestHeaders | Self::RequestHeadersNames
| Self::RequestCookies | Self::RequestCookiesNames
| Self::ResponseHeaders | Self::ResponseHeadersNames
| Self::Tx | Self::Session | Self::Env | Self::Ip
| Self::Global | Self::Resource | Self::User | Self::Geo
| Self::MatchedVars | Self::MatchedVarsNames
| Self::Files | Self::FilesSizes | Self::FilesTmpnames | Self::FilesNames
| Self::MultipartPartHeaders
)
}
}
#[inline]
pub fn parse_variables(input: &str) -> Result<Vec<VariableSpec>> {
let mut variables = Vec::with_capacity(4);
let mut exclusions: Vec<String> = Vec::new();
for part in input.split('|') {
let part = part.trim();
if part.is_empty() {
continue;
}
if part.starts_with('!') {
exclusions.push(part[1..].to_string());
continue;
}
let spec = parse_single_variable(part)?;
variables.push(spec);
}
if !exclusions.is_empty() {
for var in &mut variables {
var.exclusions = exclusions.clone();
}
}
Ok(variables)
}
pub fn parse_update_targets(input: &str) -> Result<(Vec<VariableSpec>, Vec<String>)> {
let mut additions = Vec::new();
let mut exclusions = Vec::new();
for part in input.split('|') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(excl) = part.strip_prefix('!') {
let name_end = excl.find(':').unwrap_or(excl.len());
let name_str = &excl[..name_end];
if VariableName::from_str(name_str).is_none() {
return Err(Error::UnknownVariable {
name: name_str.to_string(),
});
}
exclusions.push(excl.to_string());
} else {
additions.push(parse_single_variable(part)?);
}
}
Ok((additions, exclusions))
}
#[inline]
pub(crate) fn parse_single_variable(input: &str) -> Result<VariableSpec> {
let input = input.trim();
let bytes = input.as_bytes();
let (count_mode, input) = if bytes.first() == Some(&b'&') {
(true, &input[1..])
} else {
(false, input)
};
let colon_pos = input.bytes().position(|b| b == b':');
let (name_str, selection) = match colon_pos {
Some(pos) => {
let name = &input[..pos];
let sel_str = &input[pos + 1..];
let selection = if sel_str.starts_with('/') && sel_str.ends_with('/') && sel_str.len() > 2 {
Some(Selection::Regex(sel_str[1..sel_str.len() - 1].to_string()))
} else {
Some(Selection::Key(sel_str.to_string()))
};
(name, selection)
}
None => (input, None),
};
let name = VariableName::from_str(name_str).ok_or_else(|| Error::UnknownVariable {
name: name_str.to_string(),
})?;
Ok(VariableSpec {
name,
selection,
count_mode,
exclusions: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_variable() {
let vars = parse_variables("REQUEST_URI").unwrap();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, VariableName::RequestUri);
assert!(vars[0].selection.is_none());
assert!(!vars[0].count_mode);
}
#[test]
fn test_parse_variable_with_selection() {
let vars = parse_variables("ARGS:username").unwrap();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, VariableName::Args);
assert!(matches!(&vars[0].selection, Some(Selection::Key(k)) if k == "username"));
}
#[test]
fn test_parse_variable_with_regex() {
let vars = parse_variables("ARGS:/^user/").unwrap();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, VariableName::Args);
assert!(matches!(&vars[0].selection, Some(Selection::Regex(r)) if r == "^user"));
}
#[test]
fn test_parse_multipart_part_headers() {
let vars = parse_variables("MULTIPART_PART_HEADERS").unwrap();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, VariableName::MultipartPartHeaders);
}
#[test]
fn test_parse_count_mode() {
let vars = parse_variables("&ARGS").unwrap();
assert_eq!(vars.len(), 1);
assert!(vars[0].count_mode);
}
#[test]
fn test_parse_multiple_variables() {
let vars = parse_variables("REQUEST_URI|ARGS|REQUEST_HEADERS").unwrap();
assert_eq!(vars.len(), 3);
}
#[test]
fn test_variable_lookup_case_insensitive() {
assert_eq!(VariableName::from_str("REQUEST_URI"), Some(VariableName::RequestUri));
assert_eq!(VariableName::from_str("request_uri"), Some(VariableName::RequestUri));
assert_eq!(VariableName::from_str("Request_Uri"), Some(VariableName::RequestUri));
}
}