use waf_core::RequestContext;
use waf_normalizer::url::percent_decode;
use super::ast::{TargetKind, Variable};
pub fn collect_values(vars: &[Variable], ctx: &RequestContext) -> Vec<String> {
let mut exclusions: Vec<(TargetKind, String)> = Vec::new();
for v in vars.iter().filter(|v| v.negated) {
if let Some(sel) = &v.selector {
exclusions.push((v.kind.clone(), sel.to_ascii_lowercase()));
}
}
let mut out = Vec::new();
for v in vars.iter().filter(|v| !v.negated) {
for (name, value) in entries(&v.kind, ctx) {
if let Some(sel) = &v.selector {
if !name.eq_ignore_ascii_case(sel) {
continue;
}
}
if exclusions.iter().any(|(k, n)| *k == v.kind && name.eq_ignore_ascii_case(n)) {
continue;
}
out.push(value);
}
}
out
}
fn entries(kind: &TargetKind, ctx: &RequestContext) -> Vec<(String, String)> {
match kind {
TargetKind::Args => {
let mut e = query_args(ctx);
e.extend(post_args(ctx));
e
}
TargetKind::ArgsGet => query_args(ctx),
TargetKind::ArgsPost => post_args(ctx),
TargetKind::ArgsNames => {
let mut e = query_args(ctx);
e.extend(post_args(ctx));
e.into_iter().map(|(n, _)| (n.clone(), n)).collect()
}
TargetKind::RequestHeaders => {
ctx.headers.iter().map(|(n, v)| (n.clone(), v.clone())).collect()
}
TargetKind::RequestHeadersNames => {
ctx.headers.iter().map(|(n, _)| (n.clone(), n.clone())).collect()
}
TargetKind::RequestCookies => ctx
.cookies
.iter()
.map(|(n, v)| (n.clone(), percent_decode(v, false).0))
.collect(),
TargetKind::RequestCookiesNames => {
ctx.cookies.iter().map(|(n, _)| (n.clone(), n.clone())).collect()
}
TargetKind::RequestUri | TargetKind::RequestUriRaw => {
vec![("REQUEST_URI".to_string(), raw_uri(ctx))]
}
TargetKind::RequestFilename => {
vec![("REQUEST_FILENAME".to_string(), percent_decode(&ctx.path, false).0)]
}
TargetKind::QueryString => {
vec![("QUERY_STRING".to_string(), ctx.query.clone().unwrap_or_default())]
}
TargetKind::RequestMethod => vec![("REQUEST_METHOD".to_string(), ctx.method.clone())],
TargetKind::RequestProtocol => {
vec![("REQUEST_PROTOCOL".to_string(), ctx.http_version.clone())]
}
TargetKind::RequestLine => {
let line = format!("{} {} {}", ctx.method, raw_uri(ctx), ctx.http_version);
vec![("REQUEST_LINE".to_string(), line)]
}
TargetKind::RequestBody => {
vec![("REQUEST_BODY".to_string(), String::from_utf8_lossy(&ctx.body).into_owned())]
}
TargetKind::Unsupported(_) => Vec::new(),
}
}
fn query_args(ctx: &RequestContext) -> Vec<(String, String)> {
match &ctx.query {
Some(q) => split_args(q),
None => Vec::new(),
}
}
fn post_args(ctx: &RequestContext) -> Vec<(String, String)> {
if !is_form_urlencoded(ctx) {
return Vec::new();
}
match std::str::from_utf8(&ctx.body) {
Ok(s) => split_args(s),
Err(_) => Vec::new(),
}
}
fn split_args(s: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
for pair in s.split('&') {
if pair.is_empty() {
continue;
}
let (k, v) = match pair.split_once('=') {
Some((k, v)) => (k, v),
None => (pair, ""),
};
out.push((percent_decode(k, true).0, percent_decode(v, true).0));
}
out
}
fn is_form_urlencoded(ctx: &RequestContext) -> bool {
header(ctx, "content-type")
.map(|ct| ct.to_ascii_lowercase().contains("application/x-www-form-urlencoded"))
.unwrap_or(false)
}
fn raw_uri(ctx: &RequestContext) -> String {
match &ctx.query {
Some(q) => format!("{}?{}", ctx.path, q),
None => ctx.path.clone(),
}
}
fn header<'a>(ctx: &'a RequestContext, name: &str) -> Option<&'a str> {
ctx.headers
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}