1use 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
38pub 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
135pub 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 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#[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#[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
235fn 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
246fn 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 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 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
303struct 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 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 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 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
492fn 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, Op::Contains(needle) => {
551 value.is_some_and(|v| scalar_str(needle).is_some_and(|n| v.contains(&n)))
552 }
553 }
554}
555
556fn 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
594fn 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
606pub(crate) fn split_remote_addr(addr: &str) -> (&str, Option<&str>) {
610 if let Some(stripped) = addr.strip_prefix('[') {
611 if let Some((ip, rest)) = stripped.split_once(']') {
613 return (ip, rest.strip_prefix(':'));
614 }
615 }
616 match addr.rsplit_once(':') {
617 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¬e=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 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 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 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 serde_json::json!([["$.items[*].price", ">", 1]]),
947 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 serde_json::json!([["$.user.age", "==", "30"]]),
960 serde_json::json!([["$.user.name", "==", "jill"]]),
961 serde_json::json!([["$.missing", "==", "x"]]),
963 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 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 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"]]), serde_json::json!([["http_x_missing", "absent"]]),
1021 serde_json::json!([["http_x_missing", "!", "present"]]),
1022 serde_json::json!([["$.a", "present"]]), 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"]]), 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"]]), 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"]]), serde_json::json!([["$.nums", "contains", "2"]]), 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 serde_json::json!([["http_x", "is_null"]]),
1087 serde_json::json!([["http_x", "present", "y"]]),
1089 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 assert!(!Expr::parse(&serde_json::json!([[
1106 "NOT",
1107 ["http_authorization", "present"]
1108 ]]))
1109 .unwrap()
1110 .eval(&ctx));
1111
1112 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 assert!(!e.eval(&ctx));
1125
1126 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 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 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 assert!(Expr::parse(&serde_json::json!([["NOT"]])).is_err());
1151 assert!(Expr::parse(&serde_json::json!([[
1153 "NOT",
1154 ["http_a", "present"],
1155 ["http_b", "present"]
1156 ]]))
1157 .is_err());
1158 }
1159
1160 #[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 #[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 #[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}