Skip to main content

featherbit/vars/
mod.rs

1//! Request/response variable resolution and condition expressions.
2//!
3//! The featherbit analogue of APISIX's `ctx.var` + `lua-resty-expr`: plugins
4//! resolve named variables against the [`Context`] (`uri`, `arg_<name>`,
5//! `http_<header>`, ...), interpolate `$var` / `${var}` templates, and
6//! evaluate condition expressions written in APISIX's triple-array form:
7//!
8//! ```yaml
9//! vars:
10//!   - ["arg_name", "==", "jack"]
11//!   - ["http_user_agent", "~~", "Mozilla.+"]
12//! ```
13//!
14//! Rules in a top-level list are ANDed. A rule is `[subject, op, value]`,
15//! the negated `[subject, "!", op, value]`, or unary `[subject, "present"|"absent"|"is_null"]`.
16//! Nested logic uses `["AND", rule...]`, `["OR", rule...]`, and `["NOT", rule-or-group]`
17//! (NOT is a featherbit extension over APISIX's dialect). Subjects are var
18//! names or JSONPath queries over JSON bodies: `$.user.name` (request body),
19//! `request_body:$...`, `response_body:$...` — multi-node matches use
20//! ANY-semantics. Operators: `==`, `~=`, `>`, `>=`, `<`, `<=`, `~~` (regex),
21//! `~*` (case-insensitive regex), `in`, `has`, `ipmatch`, `present`,
22//! `absent`, `is_null` (JSONPath only), `contains`.
23
24use std::borrow::Cow;
25use std::cell::OnceCell;
26use std::net::IpAddr;
27
28use ipnet::IpNet;
29use regex::Regex;
30
31use crate::context::Context;
32use crate::vars::jsonpath::{BodyTarget, JsonSubject};
33
34pub mod catalog;
35pub mod jsonpath;
36pub mod template;
37
38/// Resolves a variable name against the context.
39///
40/// Supported names (mirroring APISIX's `ctx.var` where featherbit has an
41/// equivalent):
42/// - `uri` — request path (no query string)
43/// - `request_uri` — path plus `?query` when query params exist
44/// - `method`, `host`, `scheme`, `protocol`
45/// - `remote_addr` — client IP without port; `remote_port` — the port
46/// - `query_string` — rebuilt from `query_params`
47/// - `status` — response status code
48/// - `resp_body` — response body (lossy UTF-8)
49/// - `request_body` — request body (lossy UTF-8)
50/// - `arg_<name>` — first query parameter value
51/// - `http_<name>` — first request header value (underscores map to dashes)
52/// - `sent_http_<name>` — first response header value (underscores map to dashes)
53/// - `cookie_<name>` — value from the `Cookie` request header
54/// - `post_arg_<name>` — form field, only for
55///   `application/x-www-form-urlencoded` request bodies
56/// - `consumer_name`, `consumer_group_id` — from `message["consumer.name"]` /
57///   `message["consumer.group"]` (set by auth plugins)
58/// - `msg_<key>` — any `context.message` key (stringified)
59///
60/// Returns `None` for unknown names and for known prefixes whose subject is
61/// absent (missing header, missing query param, ...).
62pub fn resolve<'a>(ctx: &'a Context, name: &str) -> Option<Cow<'a, str>> {
63    match name {
64        "uri" => Some(Cow::Borrowed(ctx.request.path.as_str())),
65        "request_uri" => {
66            let qs = query_string(ctx);
67            if qs.is_empty() {
68                Some(Cow::Borrowed(ctx.request.path.as_str()))
69            } else {
70                Some(Cow::Owned(format!("{}?{}", ctx.request.path, qs)))
71            }
72        }
73        "method" | "request_method" => Some(Cow::Borrowed(ctx.request.method.as_str())),
74        "host" => Some(Cow::Borrowed(ctx.request.host.as_str())),
75        "scheme" => Some(Cow::Borrowed(ctx.request.scheme.as_str())),
76        "protocol" => Some(Cow::Owned(
77            format!("{:?}", ctx.request.protocol).to_lowercase(),
78        )),
79        "remote_addr" => Some(Cow::Borrowed(split_remote_addr(&ctx.request.remote_addr).0)),
80        "remote_port" => {
81            let port = split_remote_addr(&ctx.request.remote_addr).1?;
82            Some(Cow::Borrowed(port))
83        }
84        "query_string" => {
85            let qs = query_string(ctx);
86            if qs.is_empty() {
87                None
88            } else {
89                Some(Cow::Owned(qs))
90            }
91        }
92        "status" => Some(Cow::Owned(ctx.response.status_code.to_string())),
93        "resp_body" => Some(Cow::Owned(
94            String::from_utf8_lossy(&ctx.response.body).into_owned(),
95        )),
96        "request_body" => Some(Cow::Owned(
97            String::from_utf8_lossy(&ctx.request.body).into_owned(),
98        )),
99        "consumer_name" => message_str(ctx, "consumer.name"),
100        "consumer_group_id" => message_str(ctx, "consumer.group"),
101        _ => {
102            if let Some(arg) = name.strip_prefix("arg_") {
103                ctx.request
104                    .query_params
105                    .get(arg)
106                    .and_then(|v| v.first())
107                    .map(|v| Cow::Borrowed(v.as_str()))
108            } else if let Some(header) = name.strip_prefix("sent_http_") {
109                let header = header.replace('_', "-").to_lowercase();
110                ctx.response
111                    .headers
112                    .get(&header)
113                    .and_then(|v| v.first())
114                    .map(|v| Cow::Borrowed(v.as_str()))
115            } else if let Some(header) = name.strip_prefix("http_") {
116                let header = header.replace('_', "-").to_lowercase();
117                ctx.request
118                    .headers
119                    .get(&header)
120                    .and_then(|v| v.first())
121                    .map(|v| Cow::Borrowed(v.as_str()))
122            } else if let Some(cookie) = name.strip_prefix("cookie_") {
123                cookie_value(ctx, cookie).map(Cow::Owned)
124            } else if let Some(field) = name.strip_prefix("post_arg_") {
125                post_arg(ctx, field).map(Cow::Owned)
126            } else if let Some(key) = name.strip_prefix("msg_") {
127                message_str(ctx, key)
128            } else {
129                None
130            }
131        }
132    }
133}
134
135/// Interpolates `$var` and `${var}` references in a template.
136///
137/// Unknown or absent variables resolve to the empty string (matching
138/// APISIX's `resolve_var`). A literal `$` not followed by `[A-Za-z_{]`
139/// passes through unchanged; there is no escape syntax.
140pub fn interpolate(ctx: &Context, template: &str) -> String {
141    let mut out = String::with_capacity(template.len());
142    let bytes = template.as_bytes();
143    let mut i = 0;
144
145    while i < bytes.len() {
146        if bytes[i] != b'$' {
147            let start = i;
148            while i < bytes.len() && bytes[i] != b'$' {
149                i += 1;
150            }
151            out.push_str(&template[start..i]);
152            continue;
153        }
154
155        // At a '$'
156        if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
157            if let Some(end) = template[i + 2..].find('}') {
158                let name = &template[i + 2..i + 2 + end];
159                if let Some(v) = resolve(ctx, name) {
160                    out.push_str(&v);
161                }
162                i += 2 + end + 1;
163                continue;
164            }
165            out.push('$');
166            i += 1;
167        } else {
168            let start = i + 1;
169            let mut end = start;
170            while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
171                end += 1;
172            }
173            if end == start {
174                out.push('$');
175                i += 1;
176                continue;
177            }
178            if let Some(v) = resolve(ctx, &template[start..end]) {
179                out.push_str(&v);
180            }
181            i = end;
182        }
183    }
184
185    out
186}
187
188/// A compiled condition expression in APISIX's triple-array form.
189///
190/// Parsed once at config load ([`Expr::parse`]); regexes are compiled at
191/// parse time so evaluation is allocation-light.
192#[derive(Debug)]
193pub struct Expr {
194    root: Node,
195}
196
197#[derive(Debug)]
198enum Node {
199    And(Vec<Node>),
200    Or(Vec<Node>),
201    Not(Box<Node>),
202    Rule {
203        subject: Subject,
204        negate: bool,
205        op: Op,
206    },
207}
208
209/// What a rule's condition is evaluated against: a flat named var, or a
210/// JSONPath query over a request/response body.
211#[derive(Debug)]
212enum Subject {
213    Var(String),
214    Json(JsonSubject),
215}
216
217#[derive(Debug)]
218enum Op {
219    Eq(serde_json::Value),
220    Ne(serde_json::Value),
221    Gt(f64),
222    Ge(f64),
223    Lt(f64),
224    Le(f64),
225    Regex(Regex),
226    In(Vec<serde_json::Value>),
227    Has(serde_json::Value),
228    IpMatch(Vec<IpNet>),
229    Present,
230    Absent,
231    IsNull,
232    Contains(serde_json::Value),
233}
234
235/// Stringifies a scalar config value the way the legacy parser did
236/// (String as-is, Number/Bool via to_string). None for null/array/object.
237fn scalar_str(v: &serde_json::Value) -> Option<String> {
238    match v {
239        serde_json::Value::String(s) => Some(s.clone()),
240        serde_json::Value::Number(n) => Some(n.to_string()),
241        serde_json::Value::Bool(b) => Some(b.to_string()),
242        _ => None,
243    }
244}
245
246/// Whether evaluating `node` reads `context.response.body`.
247///
248/// Walks the whole tree, so a body subject nested inside an AND/OR/NOT group
249/// counts exactly like one at the top level.
250fn node_references_response_body(node: &Node) -> bool {
251    match node {
252        Node::And(children) | Node::Or(children) => {
253            children.iter().any(node_references_response_body)
254        }
255        Node::Not(inner) => node_references_response_body(inner),
256        Node::Rule { subject, .. } => match subject {
257            Subject::Var(name) => name == "resp_body",
258            Subject::Json(j) => matches!(j.target, BodyTarget::Response),
259        },
260    }
261}
262
263impl Expr {
264    /// Parses the APISIX `vars` shape: a JSON array of rules, ANDed.
265    ///
266    /// Each rule is `[var, op, value]`, `[var, "!", op, value]`, or a nested
267    /// `["AND"|"OR", rule...]`. Fails with a descriptive message on unknown
268    /// operators, malformed rules, invalid regexes, or invalid CIDRs.
269    pub fn parse(v: &serde_json::Value) -> Result<Self, String> {
270        let rules = v.as_array().ok_or("vars must be an array of rules")?;
271        Ok(Self {
272            root: Node::And(
273                rules
274                    .iter()
275                    .map(parse_node)
276                    .collect::<Result<Vec<_>, _>>()?,
277            ),
278        })
279    }
280
281    /// Evaluates the expression against the context. Rules referencing absent
282    /// variables evaluate as if the variable were the empty string, except
283    /// `ipmatch`, which is false for an absent/unparsable address. JSONPath
284    /// subjects match zero nodes (rule is false) when the body is empty or
285    /// not valid JSON.
286    /// Whether evaluating this expression reads `context.response.body`.
287    ///
288    /// Consulted at policy-compile time by the nodes that gate on a condition
289    /// (`traffic-label`'s matchers, `response-rewrite`'s `vars`): a condition
290    /// on the response body makes the node a body reader even though none of
291    /// its other config touches the body, so the node must not report itself
292    /// stream-safe.
293    pub fn references_response_body(&self) -> bool {
294        node_references_response_body(&self.root)
295    }
296
297    pub fn eval(&self, ctx: &Context) -> bool {
298        let state = EvalState::new(ctx);
299        eval_node(&self.root, &state)
300    }
301}
302
303/// Per-evaluation state: the context plus lazily-parsed JSON bodies, so a
304/// multi-rule expression parses each body at most once per eval.
305struct EvalState<'a> {
306    ctx: &'a Context,
307    request_json: OnceCell<Option<serde_json::Value>>,
308    response_json: OnceCell<Option<serde_json::Value>>,
309}
310
311impl<'a> EvalState<'a> {
312    fn new(ctx: &'a Context) -> Self {
313        Self {
314            ctx,
315            request_json: OnceCell::new(),
316            response_json: OnceCell::new(),
317        }
318    }
319
320    /// The parsed JSON body, or None when empty/not valid JSON.
321    fn body_json(&self, target: &BodyTarget) -> Option<&serde_json::Value> {
322        let (cell, bytes) = match target {
323            BodyTarget::Request => (&self.request_json, &self.ctx.request.body),
324            BodyTarget::Response => (&self.response_json, &self.ctx.response.body),
325        };
326        cell.get_or_init(|| serde_json::from_slice(bytes).ok())
327            .as_ref()
328    }
329}
330
331fn parse_node(v: &serde_json::Value) -> Result<Node, String> {
332    let arr = v.as_array().ok_or("each rule must be an array")?;
333    if arr.is_empty() {
334        return Err("empty rule".to_string());
335    }
336
337    // Nested logic: ["AND"|"OR", rule...] or ["NOT", rule-or-group]
338    if let Some(first) = arr[0].as_str() {
339        if first.eq_ignore_ascii_case("and") || first.eq_ignore_ascii_case("or") {
340            let children = arr[1..]
341                .iter()
342                .map(parse_node)
343                .collect::<Result<Vec<_>, _>>()?;
344            return Ok(if first.eq_ignore_ascii_case("and") {
345                Node::And(children)
346            } else {
347                Node::Or(children)
348            });
349        }
350        if first.eq_ignore_ascii_case("not") {
351            if arr.len() != 2 {
352                return Err("NOT takes exactly one rule or group".to_string());
353            }
354            return Ok(Node::Not(Box::new(parse_node(&arr[1])?)));
355        }
356    }
357
358    // [var, op, value] or [var, "!", op, value]
359    let var = arr[0]
360        .as_str()
361        .ok_or("rule variable must be a string")?
362        .to_string();
363    let subject = match jsonpath::parse_json_subject(&var) {
364        None => Subject::Var(var.clone()),
365        Some(compiled) => Subject::Json(compiled?),
366    };
367    let (negate, op_idx) = if arr.get(1).and_then(|v| v.as_str()) == Some("!") {
368        (true, 2)
369    } else {
370        (false, 1)
371    };
372    let op_str = arr
373        .get(op_idx)
374        .and_then(|v| v.as_str())
375        .ok_or_else(|| format!("rule for '{}' is missing an operator", var))?;
376
377    let is_unary = matches!(op_str, "present" | "absent" | "is_null");
378    if is_unary {
379        if arr.len() > op_idx + 1 {
380            return Err(format!("rule for '{}': '{}' takes no value", var, op_str));
381        }
382        let op = match op_str {
383            "present" => Op::Present,
384            "absent" => Op::Absent,
385            "is_null" => {
386                if matches!(subject, Subject::Var(_)) {
387                    return Err(format!(
388                        "rule for '{}': 'is_null' requires a JSONPath subject — headers and vars cannot be null (use 'absent')",
389                        var
390                    ));
391                }
392                Op::IsNull
393            }
394            _ => unreachable!(),
395        };
396        return Ok(Node::Rule {
397            subject,
398            negate,
399            op,
400        });
401    }
402
403    let value = arr
404        .get(op_idx + 1)
405        .ok_or_else(|| format!("rule for '{}' is missing a value", var))?;
406
407    let scalar = || -> Result<serde_json::Value, String> {
408        scalar_str(value)
409            .map(|_| value.clone())
410            .ok_or_else(|| format!("rule for '{}' needs a scalar value", var))
411    };
412    let number = || -> Result<f64, String> {
413        value
414            .as_f64()
415            .or_else(|| value.as_str().and_then(|s| s.parse().ok()))
416            .ok_or_else(|| format!("rule for '{}' ({}) needs a numeric value", var, op_str))
417    };
418    let string_list = || -> Result<Vec<String>, String> {
419        value
420            .as_array()
421            .ok_or_else(|| format!("rule for '{}' ({}) needs an array value", var, op_str))?
422            .iter()
423            .map(|item| {
424                item.as_str()
425                    .map(String::from)
426                    .or_else(|| item.as_f64().map(|n| n.to_string()))
427                    .ok_or_else(|| format!("rule for '{}': array items must be scalars", var))
428            })
429            .collect()
430    };
431    let pattern = || -> Result<String, String> {
432        scalar_str(value).ok_or_else(|| format!("rule for '{}' needs a scalar value", var))
433    };
434
435    let op = match op_str {
436        "==" => Op::Eq(scalar()?),
437        "~=" | "!=" => Op::Ne(scalar()?),
438        ">" => Op::Gt(number()?),
439        ">=" => Op::Ge(number()?),
440        "<" => Op::Lt(number()?),
441        "<=" => Op::Le(number()?),
442        "~~" => Op::Regex(
443            Regex::new(&pattern()?).map_err(|e| format!("invalid regex for '{}': {}", var, e))?,
444        ),
445        "~*" => Op::Regex(
446            Regex::new(&format!("(?i){}", pattern()?))
447                .map_err(|e| format!("invalid regex for '{}': {}", var, e))?,
448        ),
449        "in" => {
450            let items = value
451                .as_array()
452                .ok_or_else(|| format!("rule for '{}' (in) needs an array value", var))?;
453            for item in items {
454                if scalar_str(item).is_none() {
455                    return Err(format!("rule for '{}': array items must be scalars", var));
456                }
457            }
458            Op::In(items.clone())
459        }
460        "has" => Op::Has(scalar()?),
461        "contains" => Op::Contains(scalar()?),
462        "ipmatch" => {
463            let nets = string_list()?
464                .iter()
465                .map(|s| {
466                    if let Ok(net) = s.parse::<IpNet>() {
467                        Ok(net)
468                    } else if let Ok(ip) = s.parse::<IpAddr>() {
469                        Ok(IpNet::from(ip))
470                    } else {
471                        Err(format!("invalid IP/CIDR '{}' for '{}'", s, var))
472                    }
473                })
474                .collect::<Result<Vec<_>, _>>()?;
475            Op::IpMatch(nets)
476        }
477        other => {
478            return Err(format!(
479                "unknown operator '{}' — supported: ==, ~=, >, >=, <, <=, ~~, ~*, in, has, ipmatch, present, absent, is_null, contains",
480                other
481            ));
482        }
483    };
484
485    Ok(Node::Rule {
486        subject,
487        negate,
488        op,
489    })
490}
491
492/// Evaluates a node left-to-right with short-circuiting. Absent vars
493/// evaluate as empty string (absent address for `ipmatch`), and a JSONPath
494/// subject over an empty or unparsable body matches zero nodes.
495fn eval_node(node: &Node, state: &EvalState) -> bool {
496    match node {
497        Node::And(children) => children.iter().all(|c| eval_node(c, state)),
498        Node::Or(children) => children.iter().any(|c| eval_node(c, state)),
499        Node::Not(child) => !eval_node(child, state),
500        Node::Rule {
501            subject,
502            negate,
503            op,
504        } => {
505            let result = match subject {
506                Subject::Var(name) => {
507                    let value = resolve(state.ctx, name);
508                    eval_op(op, value.as_deref())
509                }
510                Subject::Json(js) => {
511                    let nodes: Vec<&serde_json::Value> = match state.body_json(&js.target) {
512                        Some(doc) => js.path.query(doc).all(),
513                        None => Vec::new(),
514                    };
515                    eval_op_json(op, &nodes)
516                }
517            };
518            if *negate {
519                !result
520            } else {
521                result
522            }
523        }
524    }
525}
526
527fn eval_op(op: &Op, value: Option<&str>) -> bool {
528    let v = value.unwrap_or("");
529    match op {
530        Op::Eq(expected) => scalar_str(expected).as_deref() == Some(v),
531        Op::Ne(expected) => scalar_str(expected).as_deref() != Some(v),
532        Op::Gt(n) => v.parse::<f64>().is_ok_and(|x| x > *n),
533        Op::Ge(n) => v.parse::<f64>().is_ok_and(|x| x >= *n),
534        Op::Lt(n) => v.parse::<f64>().is_ok_and(|x| x < *n),
535        Op::Le(n) => v.parse::<f64>().is_ok_and(|x| x <= *n),
536        Op::Regex(re) => re.is_match(v),
537        Op::In(list) => list
538            .iter()
539            .any(|item| scalar_str(item).as_deref() == Some(v)),
540        Op::Has(needle) => {
541            let needle = scalar_str(needle).unwrap_or_default();
542            v.split(',').map(str::trim).any(|part| part == needle)
543        }
544        Op::IpMatch(nets) => value
545            .and_then(|v| v.parse::<IpAddr>().ok())
546            .is_some_and(|ip| nets.iter().any(|net| net.contains(&ip))),
547        Op::Present => value.is_some(),
548        Op::Absent => value.is_none(),
549        Op::IsNull => false, // parse-time guarded; a flat var is never null
550        Op::Contains(needle) => {
551            value.is_some_and(|v| scalar_str(needle).is_some_and(|n| v.contains(&n)))
552        }
553    }
554}
555
556/// ANY-match: the rule holds if at least one matched node passes.
557/// `Present`/`Absent`/`IsNull` are match-set operators, evaluated over the
558/// whole node set rather than per-node.
559fn eval_op_json(op: &Op, nodes: &[&serde_json::Value]) -> bool {
560    match op {
561        Op::Present => !nodes.is_empty(),
562        Op::Absent => nodes.is_empty(),
563        Op::IsNull => nodes.iter().any(|n| n.is_null()),
564        _ => nodes.iter().any(|n| eval_op_json_node(op, n)),
565    }
566}
567
568fn eval_op_json_node(op: &Op, node: &serde_json::Value) -> bool {
569    match op {
570        Op::Eq(expected) => json_scalar_eq(node, expected),
571        Op::Ne(expected) => !json_scalar_eq(node, expected),
572        Op::Gt(n) => node.as_f64().is_some_and(|x| x > *n),
573        Op::Ge(n) => node.as_f64().is_some_and(|x| x >= *n),
574        Op::Lt(n) => node.as_f64().is_some_and(|x| x < *n),
575        Op::Le(n) => node.as_f64().is_some_and(|x| x <= *n),
576        Op::Regex(re) => node.as_str().is_some_and(|s| re.is_match(s)),
577        Op::In(list) => list.iter().any(|e| json_scalar_eq(node, e)),
578        Op::Has(v) => node
579            .as_array()
580            .is_some_and(|arr| arr.iter().any(|e| json_scalar_eq(e, v))),
581        Op::IpMatch(nets) => node
582            .as_str()
583            .and_then(|s| s.parse::<IpAddr>().ok())
584            .is_some_and(|ip| nets.iter().any(|net| net.contains(&ip))),
585        Op::Contains(v) => match node {
586            serde_json::Value::String(s) => scalar_str(v).is_some_and(|needle| s.contains(&needle)),
587            serde_json::Value::Array(arr) => arr.iter().any(|e| json_scalar_eq(e, v)),
588            _ => false,
589        },
590        Op::Present | Op::Absent | Op::IsNull => unreachable!("handled in eval_op_json"),
591    }
592}
593
594/// Native scalar equality: string↔string, number↔number, bool↔bool.
595/// null, arrays, and objects never equal a scalar config value.
596fn json_scalar_eq(node: &serde_json::Value, expected: &serde_json::Value) -> bool {
597    use serde_json::Value as V;
598    match (node, expected) {
599        (V::String(a), V::String(b)) => a == b,
600        (V::Number(a), V::Number(b)) => a.as_f64() == b.as_f64(),
601        (V::Bool(a), V::Bool(b)) => a == b,
602        _ => false,
603    }
604}
605
606// ---- helpers ---------------------------------------------------------------
607
608/// Splits `ip:port`, tolerating bare IPs and bracketed IPv6.
609pub(crate) fn split_remote_addr(addr: &str) -> (&str, Option<&str>) {
610    if let Some(stripped) = addr.strip_prefix('[') {
611        // [v6]:port
612        if let Some((ip, rest)) = stripped.split_once(']') {
613            return (ip, rest.strip_prefix(':'));
614        }
615    }
616    match addr.rsplit_once(':') {
617        // An IPv6 without brackets contains multiple ':'; treat as bare IP.
618        Some((ip, port)) if !ip.contains(':') => (ip, Some(port)),
619        _ => (addr, None),
620    }
621}
622
623pub(crate) fn query_string(ctx: &Context) -> String {
624    let mut pairs: Vec<String> = Vec::new();
625    for (k, values) in &ctx.request.query_params {
626        for v in values {
627            pairs.push(format!("{}={}", k, v));
628        }
629    }
630    pairs.sort();
631    pairs.join("&")
632}
633
634pub(crate) fn cookie_value(ctx: &Context, name: &str) -> Option<String> {
635    let header = ctx.request.headers.get("cookie")?.first()?;
636    for pair in header.split(';') {
637        let (k, v) = pair.trim().split_once('=')?;
638        if k == name {
639            return Some(v.to_string());
640        }
641    }
642    None
643}
644
645fn post_arg(ctx: &Context, field: &str) -> Option<String> {
646    let content_type = ctx.request.headers.get("content-type")?.first()?;
647    if !content_type.starts_with("application/x-www-form-urlencoded") {
648        return None;
649    }
650    let body = std::str::from_utf8(&ctx.request.body).ok()?;
651    for pair in body.split('&') {
652        let (k, v) = pair.split_once('=')?;
653        if k == field {
654            return Some(urldecode(v));
655        }
656    }
657    None
658}
659
660fn urldecode(s: &str) -> String {
661    let mut out = Vec::with_capacity(s.len());
662    let bytes = s.as_bytes();
663    let mut i = 0;
664    while i < bytes.len() {
665        match bytes[i] {
666            b'+' => out.push(b' '),
667            b'%' if i + 2 < bytes.len() => {
668                if let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
669                    out.push(h * 16 + l);
670                    i += 3;
671                    continue;
672                }
673                out.push(b'%');
674            }
675            b => out.push(b),
676        }
677        i += 1;
678    }
679    String::from_utf8_lossy(&out).into_owned()
680}
681
682fn hex_val(b: u8) -> Option<u8> {
683    match b {
684        b'0'..=b'9' => Some(b - b'0'),
685        b'a'..=b'f' => Some(b - b'a' + 10),
686        b'A'..=b'F' => Some(b - b'A' + 10),
687        _ => None,
688    }
689}
690
691pub(crate) fn message_str<'a>(ctx: &'a Context, key: &str) -> Option<Cow<'a, str>> {
692    match ctx.message.get(key)? {
693        serde_json::Value::String(s) => Some(Cow::Borrowed(s.as_str())),
694        other => Some(Cow::Owned(other.to_string())),
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::context::{GatewayRequest, GatewayResponse, Protocol};
702    use bytes::Bytes;
703    use std::collections::HashMap;
704
705    fn test_ctx() -> Context {
706        let mut headers = HashMap::new();
707        headers.insert("user-agent".to_string(), vec!["Mozilla/5.0".to_string()]);
708        headers.insert(
709            "cookie".to_string(),
710            vec!["session=abc123; theme=dark".to_string()],
711        );
712        headers.insert("x-tags".to_string(), vec!["beta, internal".to_string()]);
713        let mut query = HashMap::new();
714        query.insert("name".to_string(), vec!["jack".to_string()]);
715        query.insert("age".to_string(), vec!["30".to_string()]);
716        let mut message = HashMap::new();
717        message.insert("consumer.name".to_string(), serde_json::json!("alice"));
718
719        Context {
720            request: GatewayRequest {
721                method: "GET".to_string(),
722                path: "/api/users".to_string(),
723                host: "example.com".to_string(),
724                scheme: "http".to_string(),
725                headers,
726                query_params: query,
727                body: Bytes::new(),
728                remote_addr: "10.1.2.3:44321".to_string(),
729                protocol: Protocol::Http1,
730            },
731            response: GatewayResponse {
732                status_code: 502,
733                headers: HashMap::new(),
734                body: Bytes::from_static(b"bad gateway"),
735                stream: None,
736            },
737            message,
738            errors: Vec::new(),
739        }
740    }
741
742    #[test]
743    fn test_resolve_basic_vars() {
744        let ctx = test_ctx();
745        let cases = [
746            ("uri", Some("/api/users")),
747            ("method", Some("GET")),
748            ("host", Some("example.com")),
749            ("scheme", Some("http")),
750            ("remote_addr", Some("10.1.2.3")),
751            ("remote_port", Some("44321")),
752            ("status", Some("502")),
753            ("resp_body", Some("bad gateway")),
754            ("arg_name", Some("jack")),
755            ("arg_missing", None),
756            ("http_user_agent", Some("Mozilla/5.0")),
757            ("http_missing", None),
758            ("cookie_session", Some("abc123")),
759            ("cookie_theme", Some("dark")),
760            ("cookie_missing", None),
761            ("consumer_name", Some("alice")),
762            ("consumer_group_id", None),
763            ("unknown_var", None),
764        ];
765        for (name, expected) in cases {
766            assert_eq!(resolve(&ctx, name).as_deref(), expected, "var {name}");
767        }
768    }
769
770    #[test]
771    fn test_resolve_post_arg() {
772        let mut ctx = test_ctx();
773        ctx.request.headers.insert(
774            "content-type".to_string(),
775            vec!["application/x-www-form-urlencoded".to_string()],
776        );
777        ctx.request.body = Bytes::from_static(b"user=bob&note=hello%20world&plus=a+b");
778        assert_eq!(resolve(&ctx, "post_arg_user").as_deref(), Some("bob"));
779        assert_eq!(
780            resolve(&ctx, "post_arg_note").as_deref(),
781            Some("hello world")
782        );
783        assert_eq!(resolve(&ctx, "post_arg_plus").as_deref(), Some("a b"));
784        assert_eq!(resolve(&ctx, "post_arg_missing"), None);
785
786        // wrong content type -> no post args
787        ctx.request.headers.insert(
788            "content-type".to_string(),
789            vec!["application/json".to_string()],
790        );
791        assert_eq!(resolve(&ctx, "post_arg_user"), None);
792    }
793
794    #[test]
795    fn test_interpolate() {
796        let ctx = test_ctx();
797        assert_eq!(
798            interpolate(&ctx, "$remote_addr -> $uri"),
799            "10.1.2.3 -> /api/users"
800        );
801        assert_eq!(
802            interpolate(&ctx, "${scheme}://${host}${uri}"),
803            "http://example.com/api/users"
804        );
805        assert_eq!(interpolate(&ctx, "user=$arg_name!"), "user=jack!");
806        assert_eq!(interpolate(&ctx, "missing=[$arg_nope]"), "missing=[]");
807        assert_eq!(interpolate(&ctx, "cost: 5$"), "cost: 5$");
808        assert_eq!(interpolate(&ctx, "no vars here"), "no vars here");
809    }
810
811    fn expr(json: serde_json::Value) -> Expr {
812        Expr::parse(&json).expect("expression should parse")
813    }
814
815    #[test]
816    fn test_expr_operators() {
817        let ctx = test_ctx();
818        let truthy = [
819            serde_json::json!([["arg_name", "==", "jack"]]),
820            serde_json::json!([["arg_name", "~=", "jill"]]),
821            serde_json::json!([["arg_age", ">", 18]]),
822            serde_json::json!([["arg_age", "<=", "30"]]),
823            serde_json::json!([["http_user_agent", "~~", "Mozilla.+"]]),
824            serde_json::json!([["http_user_agent", "~*", "mozilla.+"]]),
825            serde_json::json!([["arg_name", "in", ["jack", "jill"]]]),
826            serde_json::json!([["http_x_tags", "has", "beta"]]),
827            serde_json::json!([["remote_addr", "ipmatch", ["10.0.0.0/8"]]]),
828            serde_json::json!([["remote_addr", "ipmatch", ["10.1.2.3"]]]),
829            serde_json::json!([["arg_name", "!", "==", "jill"]]),
830            serde_json::json!([["arg_name", "==", "jack"], ["arg_age", ">=", 30]]),
831            serde_json::json!([["OR", ["arg_name", "==", "nope"], ["arg_age", "==", "30"]]]),
832        ];
833        for case in &truthy {
834            assert!(expr(case.clone()).eval(&ctx), "should be true: {case}");
835        }
836
837        let falsy = [
838            serde_json::json!([["arg_name", "==", "jill"]]),
839            serde_json::json!([["arg_age", ">", 30]]),
840            serde_json::json!([["remote_addr", "ipmatch", ["192.168.0.0/16"]]]),
841            serde_json::json!([["arg_name", "==", "jack"], ["arg_age", ">", 99]]),
842            serde_json::json!([["AND", ["arg_name", "==", "jack"], ["arg_age", ">", 99]]]),
843            serde_json::json!([["arg_missing", "==", "x"]]),
844        ];
845        for case in &falsy {
846            assert!(!expr(case.clone()).eval(&ctx), "should be false: {case}");
847        }
848    }
849
850    #[test]
851    fn test_expr_parse_errors() {
852        let bad = [
853            serde_json::json!("not an array"),
854            serde_json::json!([["arg_x", "unknown_op", "v"]]),
855            serde_json::json!([["arg_x", "~~", "("]]),
856            serde_json::json!([["remote_addr", "ipmatch", ["not-an-ip"]]]),
857            serde_json::json!([["arg_x", "=="]]),
858            serde_json::json!([[]]),
859        ];
860        for case in &bad {
861            assert!(Expr::parse(case).is_err(), "should fail to parse: {case}");
862        }
863    }
864
865    #[test]
866    fn test_expr_in_accepts_bool_items() {
867        // widened on purpose: `in` items may be any scalar, including bools;
868        // flat vars compare against their stringified form.
869        let e = Expr::parse(&serde_json::json!([["arg_flag", "in", [true]]]))
870            .expect("should parse bool in list");
871        let mut ctx = test_ctx();
872        ctx.request
873            .query_params
874            .insert("flag".to_string(), vec!["true".to_string()]);
875        assert!(e.eval(&ctx), "flag=true should match in [true]");
876
877        ctx.request
878            .query_params
879            .insert("flag".to_string(), vec!["false".to_string()]);
880        assert!(!e.eval(&ctx), "flag=false should not match in [true]");
881
882        // Also verify non-scalar items still fail to parse
883        assert!(
884            Expr::parse(&serde_json::json!([["arg_x", "in", [[1]]]])).is_err(),
885            "array items in in-list should fail"
886        );
887    }
888
889    #[test]
890    fn test_split_remote_addr_forms() {
891        assert_eq!(split_remote_addr("1.2.3.4:80"), ("1.2.3.4", Some("80")));
892        assert_eq!(split_remote_addr("1.2.3.4"), ("1.2.3.4", None));
893        assert_eq!(split_remote_addr("[::1]:8080"), ("::1", Some("8080")));
894        assert_eq!(split_remote_addr("::1"), ("::1", None));
895    }
896
897    #[test]
898    fn test_sent_http_resolves_response_header() {
899        let mut ctx = test_ctx();
900        ctx.response.headers.insert(
901            "x-cache-status".to_string(),
902            vec!["HIT".to_string(), "second".to_string()],
903        );
904        assert_eq!(
905            resolve(&ctx, "sent_http_x_cache_status").as_deref(),
906            Some("HIT"),
907            "underscore->dash mapping and first-value pick must mirror http_*"
908        );
909        assert!(resolve(&ctx, "sent_http_missing").is_none());
910    }
911
912    #[test]
913    fn test_request_body_lossy_utf8() {
914        let mut ctx = test_ctx();
915        ctx.request.body = bytes::Bytes::from_static(b"hello=world");
916        assert_eq!(
917            resolve(&ctx, "request_body").as_deref(),
918            Some("hello=world")
919        );
920
921        ctx.request.body = bytes::Bytes::from_static(&[0xff, 0x61]);
922        assert_eq!(resolve(&ctx, "request_body").as_deref(), Some("\u{fffd}a"));
923    }
924
925    fn json_body_ctx(body: &str) -> Context {
926        let mut ctx = test_ctx();
927        ctx.request.body = Bytes::from(body.to_string());
928        ctx
929    }
930
931    #[test]
932    fn test_expr_jsonpath_subjects() {
933        let ctx = json_body_ctx(
934            r#"{"user":{"name":"jack","age":30,"tags":["a","b"],"admin":true},"items":[{"price":5},{"price":0}]}"#,
935        );
936
937        let truthy = [
938            serde_json::json!([["$.user.name", "==", "jack"]]),
939            serde_json::json!([["request_body:$.user.name", "==", "jack"]]),
940            serde_json::json!([["$.user.age", ">", 18]]),
941            serde_json::json!([["$.user.admin", "==", true]]),
942            serde_json::json!([["$.user.name", "~~", "^ja"]]),
943            serde_json::json!([["$.user.age", "in", [30, 40]]]),
944            serde_json::json!([["$.user.tags", "has", "a"]]),
945            // ANY-match: one item has price > 1
946            serde_json::json!([["$.items[*].price", ">", 1]]),
947            // ALL via negation: NOT(any price < 0)
948            serde_json::json!([["$.items[*].price", "!", "<", 0]]),
949        ];
950        for case in &truthy {
951            assert!(
952                Expr::parse(case).unwrap().eval(&ctx),
953                "should be true: {case}"
954            );
955        }
956
957        let falsy = [
958            // number node never equals a string scalar
959            serde_json::json!([["$.user.age", "==", "30"]]),
960            serde_json::json!([["$.user.name", "==", "jill"]]),
961            // absent path matches nothing -> comparison false
962            serde_json::json!([["$.missing", "==", "x"]]),
963            // object node never equals a scalar
964            serde_json::json!([["$.user", "==", "jack"]]),
965        ];
966        for case in &falsy {
967            assert!(
968                !Expr::parse(case).unwrap().eval(&ctx),
969                "should be false: {case}"
970            );
971        }
972    }
973
974    #[test]
975    fn test_expr_jsonpath_response_body_and_non_json() {
976        let mut ctx = test_ctx();
977        ctx.response.body = Bytes::from(r#"{"ok":true}"#.to_string());
978        assert!(
979            Expr::parse(&serde_json::json!([["response_body:$.ok", "==", true]]))
980                .unwrap()
981                .eval(&ctx)
982        );
983
984        // non-JSON request body: every request-body path matches zero nodes
985        let ctx = json_body_ctx("plain text");
986        assert!(!Expr::parse(&serde_json::json!([["$.a", "==", "x"]]))
987            .unwrap()
988            .eval(&ctx));
989    }
990
991    #[test]
992    fn test_expr_jsonpath_parse_errors() {
993        assert!(Expr::parse(&serde_json::json!([["$.[", "==", "x"]])).is_err());
994    }
995
996    #[test]
997    fn test_interpolate_sent_http_and_request_body() {
998        let mut ctx = test_ctx();
999        ctx.response
1000            .headers
1001            .insert("x-id".to_string(), vec!["42".to_string()]);
1002        ctx.request.body = bytes::Bytes::from_static(b"B");
1003        assert_eq!(
1004            interpolate(&ctx, "h=$sent_http_x_id b=$request_body"),
1005            "h=42 b=B"
1006        );
1007    }
1008
1009    #[test]
1010    fn test_expr_present_absent() {
1011        // ctx() has header x-api-version (adapt to the module's factory);
1012        // build one with a known header + JSON body:
1013        let mut ctx = json_body_ctx(r#"{"a": null, "b": 1}"#);
1014        ctx.request
1015            .headers
1016            .insert("x-empty".to_string(), vec!["".to_string()]);
1017
1018        let truthy = [
1019            serde_json::json!([["http_x_empty", "present"]]), // empty value still present
1020            serde_json::json!([["http_x_missing", "absent"]]),
1021            serde_json::json!([["http_x_missing", "!", "present"]]),
1022            serde_json::json!([["$.a", "present"]]), // null node counts as present
1023            serde_json::json!([["$.missing", "absent"]]),
1024            serde_json::json!([["$.a", "is_null"]]),
1025            serde_json::json!([["$.b", "!", "is_null"]]),
1026        ];
1027        for case in &truthy {
1028            assert!(
1029                Expr::parse(case).unwrap().eval(&ctx),
1030                "should be true: {case}"
1031            );
1032        }
1033
1034        let falsy = [
1035            serde_json::json!([["http_x_empty", "absent"]]),
1036            serde_json::json!([["$.missing", "present"]]),
1037            serde_json::json!([["$.missing", "is_null"]]), // absent is NOT null
1038            serde_json::json!([["$.b", "is_null"]]),
1039        ];
1040        for case in &falsy {
1041            assert!(
1042                !Expr::parse(case).unwrap().eval(&ctx),
1043                "should be false: {case}"
1044            );
1045        }
1046    }
1047
1048    #[test]
1049    fn test_expr_contains() {
1050        let mut ctx = json_body_ctx(r#"{"tags":["a","b"],"nums":[1,2],"name":"hello world"}"#);
1051        ctx.request.headers.insert(
1052            "authorization".to_string(),
1053            vec!["Bearer abc123".to_string()],
1054        );
1055
1056        let truthy = [
1057            serde_json::json!([["http_authorization", "contains", "Bearer"]]),
1058            serde_json::json!([["$.name", "contains", "lo wo"]]),
1059            serde_json::json!([["$.tags", "contains", "a"]]), // array element equality
1060            serde_json::json!([["$.nums", "contains", 2]]),
1061        ];
1062        for case in &truthy {
1063            assert!(
1064                Expr::parse(case).unwrap().eval(&ctx),
1065                "should be true: {case}"
1066            );
1067        }
1068        let falsy = [
1069            serde_json::json!([["http_authorization", "contains", "Basic"]]),
1070            serde_json::json!([["http_x_missing", "contains", "x"]]), // absent -> false
1071            serde_json::json!([["$.nums", "contains", "2"]]),         // "2" != 2 in arrays
1072            serde_json::json!([["$.nums", "contains", 3]]),
1073        ];
1074        for case in &falsy {
1075            assert!(
1076                !Expr::parse(case).unwrap().eval(&ctx),
1077                "should be false: {case}"
1078            );
1079        }
1080    }
1081
1082    #[test]
1083    fn test_expr_new_operator_parse_errors() {
1084        let cases = [
1085            // is_null on a flat var
1086            serde_json::json!([["http_x", "is_null"]]),
1087            // unary op given a value
1088            serde_json::json!([["http_x", "present", "y"]]),
1089            // binary op missing a value (already an error; pin it stays one)
1090            serde_json::json!([["http_x", "contains"]]),
1091        ];
1092        for case in &cases {
1093            assert!(Expr::parse(case).is_err(), "should fail to parse: {case}");
1094        }
1095    }
1096
1097    #[test]
1098    fn test_expr_not_group() {
1099        let mut ctx = json_body_ctx(r#"{"user":{"id":null}}"#);
1100        ctx.request
1101            .headers
1102            .insert("authorization".to_string(), vec!["Bearer tok".to_string()]);
1103
1104        // NOT over a rule
1105        assert!(!Expr::parse(&serde_json::json!([[
1106            "NOT",
1107            ["http_authorization", "present"]
1108        ]]))
1109        .unwrap()
1110        .eval(&ctx));
1111
1112        // NOT over a group, nested logic (spec example shape)
1113        let e = Expr::parse(&serde_json::json!([
1114            ["http_authorization", "present"],
1115            ["http_authorization", "contains", "Bearer"],
1116            [
1117                "OR",
1118                ["$.user.email", "present"],
1119                ["NOT", ["$.user.id", "is_null"]]
1120            ]
1121        ]))
1122        .unwrap();
1123        // email absent AND id is null -> OR arm: (false OR NOT(true)) = false
1124        assert!(!e.eval(&ctx));
1125
1126        // nested NOT(NOT(x)) == x
1127        assert!(Expr::parse(&serde_json::json!([[
1128            "NOT",
1129            ["NOT", ["http_authorization", "present"]]
1130        ]]))
1131        .unwrap()
1132        .eval(&ctx));
1133    }
1134
1135    #[test]
1136    fn test_expr_eval_absent_subjects_are_lenient() {
1137        // absent var = empty string: positive comparison false, != true
1138        let ctx = test_ctx();
1139        assert!(!expr(serde_json::json!([["arg_missing", "==", "x"]])).eval(&ctx));
1140        assert!(expr(serde_json::json!([["arg_missing", "!=", "x"]])).eval(&ctx));
1141        // a JSONPath over a non-JSON body matches zero nodes
1142        let plain = json_body_ctx("plain text");
1143        assert!(!expr(serde_json::json!([["$.a", "==", "x"]])).eval(&plain));
1144        assert!(expr(serde_json::json!([["$.a", "absent"]])).eval(&plain));
1145    }
1146
1147    #[test]
1148    fn test_expr_not_parse_errors() {
1149        // zero children
1150        assert!(Expr::parse(&serde_json::json!([["NOT"]])).is_err());
1151        // two children
1152        assert!(Expr::parse(&serde_json::json!([[
1153            "NOT",
1154            ["http_a", "present"],
1155            ["http_b", "present"]
1156        ]]))
1157        .is_err());
1158    }
1159
1160    /// A condition on `resp_body` reads the response body, so any node gating
1161    /// on it must force buffering.
1162    #[test]
1163    fn test_expr_reports_a_resp_body_subject() {
1164        let e = Expr::parse(&serde_json::json!([["resp_body", "~~", "error"]])).unwrap();
1165        assert!(e.references_response_body());
1166    }
1167
1168    /// The JSONPath form reads it too, and must be detected through a nested
1169    /// boolean group rather than only at the top level.
1170    #[test]
1171    fn test_expr_reports_a_response_body_jsonpath_subject() {
1172        let e = Expr::parse(&serde_json::json!([[
1173            "OR",
1174            ["status", "==", 200],
1175            ["response_body:$.error", "==", true]
1176        ]]))
1177        .unwrap();
1178        assert!(e.references_response_body());
1179    }
1180
1181    /// Request-side subjects, including a request-body JSONPath, leave the
1182    /// response body untouched and must stay stream-safe.
1183    #[test]
1184    fn test_expr_without_a_response_body_subject_is_stream_safe() {
1185        let e = Expr::parse(&serde_json::json!([
1186            ["uri", "==", "/a"],
1187            ["request_body:$.id", "==", 7]
1188        ]))
1189        .unwrap();
1190        assert!(!e.references_response_body());
1191    }
1192}