1use crate::error::{Error, Result};
6use phf::phf_map;
7
8#[derive(Debug, Clone)]
10pub struct VariableSpec {
11 pub name: VariableName,
13 pub selection: Option<Selection>,
15 pub count_mode: bool,
17 pub exclusions: Vec<String>,
19}
20
21#[derive(Debug, Clone)]
23pub enum Selection {
24 Key(String),
26 Regex(String),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum VariableName {
33 Args, ArgsGet, ArgsPost, ArgsNames, ArgsGetNames, ArgsPostNames, ArgsCombinedSize,
35 RequestUri, RequestUriRaw, RequestFilename, RequestBasename, RequestLine,
36 RequestMethod, RequestProtocol, RequestHeaders, RequestHeadersNames,
37 RequestCookies, RequestCookiesNames, RequestBody, RequestBodyLength, QueryString,
38
39 ResponseStatus, ResponseProtocol, ResponseHeaders, ResponseHeadersNames,
41 ResponseBody, ResponseContentType, ResponseContentLength,
42
43 RemoteAddr, RemotePort, RemoteHost, RemoteUser,
45 ServerAddr, ServerPort, ServerName,
46
47 Tx, Session, Env, Ip, Global, Resource, User, Geo,
49
50 MatchedVar, MatchedVars, MatchedVarName, MatchedVarsNames,
52
53 Time, TimeEpoch, TimeDay, TimeHour, TimeMin, TimeSec, TimeWday, TimeMon, TimeYear,
55
56 Files, FilesSizes, FilesTmpnames, FilesCombinedSize, FilesNames,
58
59 UniqueId, InboundAnomalyScore, OutboundAnomalyScore, Duration,
61 MultipartBoundaryQuoted, MultipartBoundaryWhitespace, MultipartDataAfter,
62 MultipartDataBefore, MultipartFileLimitExceeded, MultipartHeaderFolding,
63 MultipartInvalidHeaderFolding, MultipartInvalidPart, MultipartInvalidQuoting,
64 MultipartLfLine, MultipartMissingSemicolon, MultipartStrictError,
65 MultipartUnmatchedBoundary, MultipartPartHeaders,
66
67 Xml,
69
70 WebserverErrorLog, HighestSeverity, StatusLine, FullRequest, FullRequestLength,
72
73 AuthType,
75
76 ReqBodyProcessor, ReqBodyError, ReqBodyErrorMsg, ReqBodyProcessorError, ReqBodyProcessorErrorMsg,
78
79 MultipartStrictCheck,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum XmlTarget {
96 Elements,
98 Attributes,
100 All,
102}
103
104impl XmlTarget {
105 pub fn from_selection(selection: Option<&Selection>) -> Option<Self> {
107 match selection {
108 None => Some(XmlTarget::All),
109 Some(Selection::Key(sel)) => match sel.trim() {
110 "/*" => Some(XmlTarget::Elements),
111 "//@*" => Some(XmlTarget::Attributes),
112 _ => None,
113 },
114 Some(Selection::Regex(_)) => None,
117 }
118 }
119}
120
121impl VariableName {
122 pub fn is_implemented(&self) -> bool {
130 match self {
131 VariableName::Xml | VariableName::Args | VariableName::ArgsGet | VariableName::ArgsPost |
132 VariableName::ArgsNames | VariableName::ArgsGetNames | VariableName::ArgsPostNames |
133 VariableName::ArgsCombinedSize | VariableName::RequestUri | VariableName::RequestUriRaw |
134 VariableName::RequestFilename | VariableName::RequestBasename | VariableName::RequestLine |
135 VariableName::RequestMethod | VariableName::RequestProtocol | VariableName::RequestHeaders |
136 VariableName::RequestHeadersNames | VariableName::RequestCookies | VariableName::RequestCookiesNames |
137 VariableName::RequestBody | VariableName::RequestBodyLength | VariableName::QueryString |
138 VariableName::ResponseStatus | VariableName::ResponseHeaders | VariableName::ResponseBody |
139 VariableName::ResponseContentType | VariableName::RemoteAddr | VariableName::RemotePort |
140 VariableName::ServerAddr | VariableName::ServerPort | VariableName::ServerName |
141 VariableName::Tx | VariableName::MatchedVar | VariableName::MatchedVars |
142 VariableName::MatchedVarName | VariableName::MatchedVarsNames | VariableName::Files |
143 VariableName::FilesNames | VariableName::MultipartPartHeaders | VariableName::ReqBodyProcessor |
144 VariableName::ReqBodyError | VariableName::ReqBodyErrorMsg | VariableName::ReqBodyProcessorError |
145 VariableName::ReqBodyProcessorErrorMsg => true,
146
147 VariableName::ResponseProtocol | VariableName::ResponseHeadersNames | VariableName::ResponseContentLength |
148 VariableName::RemoteHost | VariableName::RemoteUser | VariableName::Session |
149 VariableName::Env | VariableName::Ip | VariableName::Global |
150 VariableName::Resource | VariableName::User | VariableName::Geo |
151 VariableName::Time | VariableName::TimeEpoch | VariableName::TimeDay |
152 VariableName::TimeHour | VariableName::TimeMin | VariableName::TimeSec |
153 VariableName::TimeWday | VariableName::TimeMon | VariableName::TimeYear |
154 VariableName::FilesSizes | VariableName::FilesTmpnames | VariableName::FilesCombinedSize |
155 VariableName::UniqueId | VariableName::InboundAnomalyScore | VariableName::OutboundAnomalyScore |
156 VariableName::Duration | VariableName::MultipartBoundaryQuoted | VariableName::MultipartBoundaryWhitespace |
157 VariableName::MultipartDataAfter | VariableName::MultipartDataBefore | VariableName::MultipartFileLimitExceeded |
158 VariableName::MultipartHeaderFolding | VariableName::MultipartInvalidHeaderFolding | VariableName::MultipartInvalidPart |
159 VariableName::MultipartInvalidQuoting | VariableName::MultipartLfLine | VariableName::MultipartMissingSemicolon |
160 VariableName::MultipartStrictError | VariableName::MultipartUnmatchedBoundary |
161 VariableName::WebserverErrorLog | VariableName::HighestSeverity | VariableName::StatusLine |
162 VariableName::FullRequest | VariableName::FullRequestLength | VariableName::AuthType |
163 VariableName::MultipartStrictCheck => false,
164 }
165 }
166}
167
168static VARIABLE_MAP: phf::Map<&'static str, VariableName> = phf_map! {
170 "ARGS" => VariableName::Args,
171 "ARGS_GET" => VariableName::ArgsGet,
172 "ARGS_POST" => VariableName::ArgsPost,
173 "ARGS_NAMES" => VariableName::ArgsNames,
174 "ARGS_GET_NAMES" => VariableName::ArgsGetNames,
175 "ARGS_POST_NAMES" => VariableName::ArgsPostNames,
176 "ARGS_COMBINED_SIZE" => VariableName::ArgsCombinedSize,
177 "REQUEST_URI" => VariableName::RequestUri,
178 "REQUEST_URI_RAW" => VariableName::RequestUriRaw,
179 "REQUEST_FILENAME" => VariableName::RequestFilename,
180 "REQUEST_BASENAME" => VariableName::RequestBasename,
181 "REQUEST_LINE" => VariableName::RequestLine,
182 "REQUEST_METHOD" => VariableName::RequestMethod,
183 "REQUEST_PROTOCOL" => VariableName::RequestProtocol,
184 "REQUEST_HEADERS" => VariableName::RequestHeaders,
185 "REQUEST_HEADERS_NAMES" => VariableName::RequestHeadersNames,
186 "REQUEST_COOKIES" => VariableName::RequestCookies,
187 "REQUEST_COOKIES_NAMES" => VariableName::RequestCookiesNames,
188 "REQUEST_BODY" => VariableName::RequestBody,
189 "REQUEST_BODY_LENGTH" => VariableName::RequestBodyLength,
190 "QUERY_STRING" => VariableName::QueryString,
191 "RESPONSE_STATUS" => VariableName::ResponseStatus,
192 "RESPONSE_PROTOCOL" => VariableName::ResponseProtocol,
193 "RESPONSE_HEADERS" => VariableName::ResponseHeaders,
194 "RESPONSE_HEADERS_NAMES" => VariableName::ResponseHeadersNames,
195 "RESPONSE_BODY" => VariableName::ResponseBody,
196 "RESPONSE_CONTENT_TYPE" => VariableName::ResponseContentType,
197 "RESPONSE_CONTENT_LENGTH" => VariableName::ResponseContentLength,
198 "REMOTE_ADDR" => VariableName::RemoteAddr,
199 "REMOTE_PORT" => VariableName::RemotePort,
200 "REMOTE_HOST" => VariableName::RemoteHost,
201 "REMOTE_USER" => VariableName::RemoteUser,
202 "SERVER_ADDR" => VariableName::ServerAddr,
203 "SERVER_PORT" => VariableName::ServerPort,
204 "SERVER_NAME" => VariableName::ServerName,
205 "TX" => VariableName::Tx,
206 "SESSION" => VariableName::Session,
207 "ENV" => VariableName::Env,
208 "IP" => VariableName::Ip,
209 "GLOBAL" => VariableName::Global,
210 "RESOURCE" => VariableName::Resource,
211 "USER" => VariableName::User,
212 "GEO" => VariableName::Geo,
213 "MATCHED_VAR" => VariableName::MatchedVar,
214 "MATCHED_VARS" => VariableName::MatchedVars,
215 "MATCHED_VAR_NAME" => VariableName::MatchedVarName,
216 "MATCHED_VARS_NAMES" => VariableName::MatchedVarsNames,
217 "TIME" => VariableName::Time,
218 "TIME_EPOCH" => VariableName::TimeEpoch,
219 "TIME_DAY" => VariableName::TimeDay,
220 "TIME_HOUR" => VariableName::TimeHour,
221 "TIME_MIN" => VariableName::TimeMin,
222 "TIME_SEC" => VariableName::TimeSec,
223 "TIME_WDAY" => VariableName::TimeWday,
224 "TIME_MON" => VariableName::TimeMon,
225 "TIME_YEAR" => VariableName::TimeYear,
226 "FILES" => VariableName::Files,
227 "FILES_SIZES" => VariableName::FilesSizes,
228 "FILES_TMPNAMES" => VariableName::FilesTmpnames,
229 "FILES_COMBINED_SIZE" => VariableName::FilesCombinedSize,
230 "FILES_NAMES" => VariableName::FilesNames,
231 "UNIQUE_ID" => VariableName::UniqueId,
232 "DURATION" => VariableName::Duration,
233 "HIGHEST_SEVERITY" => VariableName::HighestSeverity,
234 "STATUS_LINE" => VariableName::StatusLine,
235 "FULL_REQUEST" => VariableName::FullRequest,
236 "FULL_REQUEST_LENGTH" => VariableName::FullRequestLength,
237 "AUTH_TYPE" => VariableName::AuthType,
238 "XML" => VariableName::Xml,
239 "REQBODY_PROCESSOR" => VariableName::ReqBodyProcessor,
240 "REQBODY_ERROR" => VariableName::ReqBodyError,
241 "REQBODY_ERROR_MSG" => VariableName::ReqBodyErrorMsg,
242 "REQBODY_PROCESSOR_ERROR" => VariableName::ReqBodyProcessorError,
243 "REQBODY_PROCESSOR_ERROR_MSG" => VariableName::ReqBodyProcessorErrorMsg,
244 "MULTIPART_STRICT_ERROR" => VariableName::MultipartStrictCheck,
245 "MULTIPART_PART_HEADERS" => VariableName::MultipartPartHeaders,
246};
247
248impl VariableName {
249 #[inline]
251 pub fn from_str(s: &str) -> Option<Self> {
252 if s.bytes().all(|b| b.is_ascii_uppercase() || b == b'_') {
254 return VARIABLE_MAP.get(s).copied();
255 }
256 let mut buf = [0u8; 64];
258 let len = s.len().min(64);
259 for (i, b) in s.bytes().take(len).enumerate() {
260 buf[i] = b.to_ascii_uppercase();
261 }
262 let upper = std::str::from_utf8(&buf[..len]).ok()?;
263 VARIABLE_MAP.get(upper).copied()
264 }
265
266 #[inline]
268 pub fn is_collection(&self) -> bool {
269 matches!(
270 self,
271 Self::Args | Self::ArgsGet | Self::ArgsPost | Self::ArgsNames
272 | Self::RequestHeaders | Self::RequestHeadersNames
273 | Self::RequestCookies | Self::RequestCookiesNames
274 | Self::ResponseHeaders | Self::ResponseHeadersNames
275 | Self::Tx | Self::Session | Self::Env | Self::Ip
276 | Self::Global | Self::Resource | Self::User | Self::Geo
277 | Self::MatchedVars | Self::MatchedVarsNames
278 | Self::Files | Self::FilesSizes | Self::FilesTmpnames | Self::FilesNames
279 | Self::MultipartPartHeaders
280 )
281 }
282}
283
284#[inline]
286pub fn parse_variables(input: &str) -> Result<Vec<VariableSpec>> {
287 let mut variables = Vec::with_capacity(4);
288 let mut exclusions: Vec<String> = Vec::new();
289
290 for part in input.split('|') {
292 let part = part.trim();
293 if part.is_empty() {
294 continue;
295 }
296
297 if part.starts_with('!') {
299 exclusions.push(part[1..].to_string());
300 continue;
301 }
302
303 let spec = parse_single_variable(part)?;
304 variables.push(spec);
305 }
306
307 if !exclusions.is_empty() {
309 for var in &mut variables {
310 var.exclusions = exclusions.clone();
311 }
312 }
313
314 Ok(variables)
315}
316
317pub fn parse_update_targets(input: &str) -> Result<(Vec<VariableSpec>, Vec<String>)> {
324 let mut additions = Vec::new();
325 let mut exclusions = Vec::new();
326
327 for part in input.split('|') {
328 let part = part.trim();
329 if part.is_empty() {
330 continue;
331 }
332 if let Some(excl) = part.strip_prefix('!') {
333 let name_end = excl.find(':').unwrap_or(excl.len());
336 let name_str = &excl[..name_end];
337 if VariableName::from_str(name_str).is_none() {
338 return Err(Error::UnknownVariable {
339 name: name_str.to_string(),
340 });
341 }
342 exclusions.push(excl.to_string());
343 } else {
344 additions.push(parse_single_variable(part)?);
345 }
346 }
347
348 Ok((additions, exclusions))
349}
350
351#[inline]
353pub(crate) fn parse_single_variable(input: &str) -> Result<VariableSpec> {
354 let input = input.trim();
355 let bytes = input.as_bytes();
356
357 let (count_mode, input) = if bytes.first() == Some(&b'&') {
359 (true, &input[1..])
360 } else {
361 (false, input)
362 };
363
364 let colon_pos = input.bytes().position(|b| b == b':');
366
367 let (name_str, selection) = match colon_pos {
368 Some(pos) => {
369 let name = &input[..pos];
370 let sel_str = &input[pos + 1..];
371
372 let selection = if sel_str.starts_with('/') && sel_str.ends_with('/') && sel_str.len() > 2 {
373 Some(Selection::Regex(sel_str[1..sel_str.len() - 1].to_string()))
374 } else {
375 Some(Selection::Key(sel_str.to_string()))
376 };
377
378 (name, selection)
379 }
380 None => (input, None),
381 };
382
383 let name = VariableName::from_str(name_str).ok_or_else(|| Error::UnknownVariable {
384 name: name_str.to_string(),
385 })?;
386
387 Ok(VariableSpec {
388 name,
389 selection,
390 count_mode,
391 exclusions: Vec::new(),
392 })
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_parse_simple_variable() {
401 let vars = parse_variables("REQUEST_URI").unwrap();
402 assert_eq!(vars.len(), 1);
403 assert_eq!(vars[0].name, VariableName::RequestUri);
404 assert!(vars[0].selection.is_none());
405 assert!(!vars[0].count_mode);
406 }
407
408 #[test]
409 fn test_parse_variable_with_selection() {
410 let vars = parse_variables("ARGS:username").unwrap();
411 assert_eq!(vars.len(), 1);
412 assert_eq!(vars[0].name, VariableName::Args);
413 assert!(matches!(&vars[0].selection, Some(Selection::Key(k)) if k == "username"));
414 }
415
416 #[test]
417 fn test_parse_variable_with_regex() {
418 let vars = parse_variables("ARGS:/^user/").unwrap();
419 assert_eq!(vars.len(), 1);
420 assert_eq!(vars[0].name, VariableName::Args);
421 assert!(matches!(&vars[0].selection, Some(Selection::Regex(r)) if r == "^user"));
422 }
423
424 #[test]
425 fn test_parse_multipart_part_headers() {
426 let vars = parse_variables("MULTIPART_PART_HEADERS").unwrap();
428 assert_eq!(vars.len(), 1);
429 assert_eq!(vars[0].name, VariableName::MultipartPartHeaders);
430 }
431
432 #[test]
433 fn test_parse_count_mode() {
434 let vars = parse_variables("&ARGS").unwrap();
435 assert_eq!(vars.len(), 1);
436 assert!(vars[0].count_mode);
437 }
438
439 #[test]
440 fn test_parse_multiple_variables() {
441 let vars = parse_variables("REQUEST_URI|ARGS|REQUEST_HEADERS").unwrap();
442 assert_eq!(vars.len(), 3);
443 }
444
445 #[test]
446 fn test_variable_lookup_case_insensitive() {
447 assert_eq!(VariableName::from_str("REQUEST_URI"), Some(VariableName::RequestUri));
448 assert_eq!(VariableName::from_str("request_uri"), Some(VariableName::RequestUri));
449 assert_eq!(VariableName::from_str("Request_Uri"), Some(VariableName::RequestUri));
450 }
451}