1use std::borrow::Cow;
21use std::net::IpAddr;
22
23use ipnet::IpNet;
24use regex::Regex;
25
26use crate::context::Context;
27
28pub fn resolve<'a>(ctx: &'a Context, name: &str) -> Option<Cow<'a, str>> {
51 match name {
52 "uri" => Some(Cow::Borrowed(ctx.request.path.as_str())),
53 "request_uri" => {
54 let qs = query_string(ctx);
55 if qs.is_empty() {
56 Some(Cow::Borrowed(ctx.request.path.as_str()))
57 } else {
58 Some(Cow::Owned(format!("{}?{}", ctx.request.path, qs)))
59 }
60 }
61 "method" | "request_method" => Some(Cow::Borrowed(ctx.request.method.as_str())),
62 "host" => Some(Cow::Borrowed(ctx.request.host.as_str())),
63 "scheme" => Some(Cow::Borrowed(ctx.request.scheme.as_str())),
64 "protocol" => Some(Cow::Owned(
65 format!("{:?}", ctx.request.protocol).to_lowercase(),
66 )),
67 "remote_addr" => Some(Cow::Borrowed(split_remote_addr(&ctx.request.remote_addr).0)),
68 "remote_port" => {
69 let port = split_remote_addr(&ctx.request.remote_addr).1?;
70 Some(Cow::Borrowed(port))
71 }
72 "query_string" => {
73 let qs = query_string(ctx);
74 if qs.is_empty() {
75 None
76 } else {
77 Some(Cow::Owned(qs))
78 }
79 }
80 "status" => Some(Cow::Owned(ctx.response.status_code.to_string())),
81 "resp_body" => Some(Cow::Owned(
82 String::from_utf8_lossy(&ctx.response.body).into_owned(),
83 )),
84 "consumer_name" => message_str(ctx, "consumer.name"),
85 "consumer_group_id" => message_str(ctx, "consumer.group"),
86 _ => {
87 if let Some(arg) = name.strip_prefix("arg_") {
88 ctx.request
89 .query_params
90 .get(arg)
91 .and_then(|v| v.first())
92 .map(|v| Cow::Borrowed(v.as_str()))
93 } else if let Some(header) = name.strip_prefix("http_") {
94 let header = header.replace('_', "-").to_lowercase();
95 ctx.request
96 .headers
97 .get(&header)
98 .and_then(|v| v.first())
99 .map(|v| Cow::Borrowed(v.as_str()))
100 } else if let Some(cookie) = name.strip_prefix("cookie_") {
101 cookie_value(ctx, cookie).map(Cow::Owned)
102 } else if let Some(field) = name.strip_prefix("post_arg_") {
103 post_arg(ctx, field).map(Cow::Owned)
104 } else if let Some(key) = name.strip_prefix("msg_") {
105 message_str(ctx, key)
106 } else {
107 None
108 }
109 }
110 }
111}
112
113pub fn interpolate(ctx: &Context, template: &str) -> String {
119 let mut out = String::with_capacity(template.len());
120 let bytes = template.as_bytes();
121 let mut i = 0;
122
123 while i < bytes.len() {
124 if bytes[i] != b'$' {
125 let start = i;
126 while i < bytes.len() && bytes[i] != b'$' {
127 i += 1;
128 }
129 out.push_str(&template[start..i]);
130 continue;
131 }
132
133 if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
135 if let Some(end) = template[i + 2..].find('}') {
136 let name = &template[i + 2..i + 2 + end];
137 if let Some(v) = resolve(ctx, name) {
138 out.push_str(&v);
139 }
140 i += 2 + end + 1;
141 continue;
142 }
143 out.push('$');
144 i += 1;
145 } else {
146 let start = i + 1;
147 let mut end = start;
148 while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
149 end += 1;
150 }
151 if end == start {
152 out.push('$');
153 i += 1;
154 continue;
155 }
156 if let Some(v) = resolve(ctx, &template[start..end]) {
157 out.push_str(&v);
158 }
159 i = end;
160 }
161 }
162
163 out
164}
165
166pub struct Expr {
171 root: Node,
172}
173
174enum Node {
175 And(Vec<Node>),
176 Or(Vec<Node>),
177 Rule { var: String, negate: bool, op: Op },
178}
179
180enum Op {
181 Eq(String),
182 Ne(String),
183 Gt(f64),
184 Ge(f64),
185 Lt(f64),
186 Le(f64),
187 Regex(Regex),
188 In(Vec<String>),
189 Has(String),
190 IpMatch(Vec<IpNet>),
191}
192
193impl Expr {
194 pub fn parse(v: &serde_json::Value) -> Result<Self, String> {
200 let rules = v.as_array().ok_or("vars must be an array of rules")?;
201 Ok(Self {
202 root: Node::And(
203 rules
204 .iter()
205 .map(parse_node)
206 .collect::<Result<Vec<_>, _>>()?,
207 ),
208 })
209 }
210
211 pub fn eval(&self, ctx: &Context) -> bool {
215 eval_node(&self.root, ctx)
216 }
217}
218
219fn parse_node(v: &serde_json::Value) -> Result<Node, String> {
220 let arr = v.as_array().ok_or("each rule must be an array")?;
221 if arr.is_empty() {
222 return Err("empty rule".to_string());
223 }
224
225 if let Some(first) = arr[0].as_str() {
227 if first.eq_ignore_ascii_case("and") || first.eq_ignore_ascii_case("or") {
228 let children = arr[1..]
229 .iter()
230 .map(parse_node)
231 .collect::<Result<Vec<_>, _>>()?;
232 return Ok(if first.eq_ignore_ascii_case("and") {
233 Node::And(children)
234 } else {
235 Node::Or(children)
236 });
237 }
238 }
239
240 let var = arr[0]
242 .as_str()
243 .ok_or("rule variable must be a string")?
244 .to_string();
245 let (negate, op_idx) = if arr.get(1).and_then(|v| v.as_str()) == Some("!") {
246 (true, 2)
247 } else {
248 (false, 1)
249 };
250 let op_str = arr
251 .get(op_idx)
252 .and_then(|v| v.as_str())
253 .ok_or_else(|| format!("rule for '{}' is missing an operator", var))?;
254 let value = arr
255 .get(op_idx + 1)
256 .ok_or_else(|| format!("rule for '{}' is missing a value", var))?;
257
258 let scalar = || -> Result<String, String> {
259 match value {
260 serde_json::Value::String(s) => Ok(s.clone()),
261 serde_json::Value::Number(n) => Ok(n.to_string()),
262 serde_json::Value::Bool(b) => Ok(b.to_string()),
263 _ => Err(format!("rule for '{}' needs a scalar value", var)),
264 }
265 };
266 let number = || -> Result<f64, String> {
267 value
268 .as_f64()
269 .or_else(|| value.as_str().and_then(|s| s.parse().ok()))
270 .ok_or_else(|| format!("rule for '{}' ({}) needs a numeric value", var, op_str))
271 };
272 let string_list = || -> Result<Vec<String>, String> {
273 value
274 .as_array()
275 .ok_or_else(|| format!("rule for '{}' ({}) needs an array value", var, op_str))?
276 .iter()
277 .map(|item| {
278 item.as_str()
279 .map(String::from)
280 .or_else(|| item.as_f64().map(|n| n.to_string()))
281 .ok_or_else(|| format!("rule for '{}': array items must be scalars", var))
282 })
283 .collect()
284 };
285
286 let op = match op_str {
287 "==" => Op::Eq(scalar()?),
288 "~=" | "!=" => Op::Ne(scalar()?),
289 ">" => Op::Gt(number()?),
290 ">=" => Op::Ge(number()?),
291 "<" => Op::Lt(number()?),
292 "<=" => Op::Le(number()?),
293 "~~" => Op::Regex(
294 Regex::new(&scalar()?).map_err(|e| format!("invalid regex for '{}': {}", var, e))?,
295 ),
296 "~*" => Op::Regex(
297 Regex::new(&format!("(?i){}", scalar()?))
298 .map_err(|e| format!("invalid regex for '{}': {}", var, e))?,
299 ),
300 "in" => Op::In(string_list()?),
301 "has" => Op::Has(scalar()?),
302 "ipmatch" => {
303 let nets = string_list()?
304 .iter()
305 .map(|s| {
306 if let Ok(net) = s.parse::<IpNet>() {
307 Ok(net)
308 } else if let Ok(ip) = s.parse::<IpAddr>() {
309 Ok(IpNet::from(ip))
310 } else {
311 Err(format!("invalid IP/CIDR '{}' for '{}'", s, var))
312 }
313 })
314 .collect::<Result<Vec<_>, _>>()?;
315 Op::IpMatch(nets)
316 }
317 other => {
318 return Err(format!(
319 "unknown operator '{}' — supported: ==, ~=, >, >=, <, <=, ~~, ~*, in, has, ipmatch",
320 other
321 ));
322 }
323 };
324
325 Ok(Node::Rule { var, negate, op })
326}
327
328fn eval_node(node: &Node, ctx: &Context) -> bool {
329 match node {
330 Node::And(children) => children.iter().all(|c| eval_node(c, ctx)),
331 Node::Or(children) => children.iter().any(|c| eval_node(c, ctx)),
332 Node::Rule { var, negate, op } => {
333 let value = resolve(ctx, var);
334 let result = eval_op(op, value.as_deref());
335 if *negate {
336 !result
337 } else {
338 result
339 }
340 }
341 }
342}
343
344fn eval_op(op: &Op, value: Option<&str>) -> bool {
345 let v = value.unwrap_or("");
346 match op {
347 Op::Eq(expected) => v == expected,
348 Op::Ne(expected) => v != expected,
349 Op::Gt(n) => v.parse::<f64>().is_ok_and(|x| x > *n),
350 Op::Ge(n) => v.parse::<f64>().is_ok_and(|x| x >= *n),
351 Op::Lt(n) => v.parse::<f64>().is_ok_and(|x| x < *n),
352 Op::Le(n) => v.parse::<f64>().is_ok_and(|x| x <= *n),
353 Op::Regex(re) => re.is_match(v),
354 Op::In(list) => list.iter().any(|item| item == v),
355 Op::Has(needle) => v.split(',').map(str::trim).any(|part| part == needle),
356 Op::IpMatch(nets) => value
357 .and_then(|v| v.parse::<IpAddr>().ok())
358 .is_some_and(|ip| nets.iter().any(|net| net.contains(&ip))),
359 }
360}
361
362fn split_remote_addr(addr: &str) -> (&str, Option<&str>) {
366 if let Some(stripped) = addr.strip_prefix('[') {
367 if let Some((ip, rest)) = stripped.split_once(']') {
369 return (ip, rest.strip_prefix(':'));
370 }
371 }
372 match addr.rsplit_once(':') {
373 Some((ip, port)) if !ip.contains(':') => (ip, Some(port)),
375 _ => (addr, None),
376 }
377}
378
379fn query_string(ctx: &Context) -> String {
380 let mut pairs: Vec<String> = Vec::new();
381 for (k, values) in &ctx.request.query_params {
382 for v in values {
383 pairs.push(format!("{}={}", k, v));
384 }
385 }
386 pairs.sort();
387 pairs.join("&")
388}
389
390fn cookie_value(ctx: &Context, name: &str) -> Option<String> {
391 let header = ctx.request.headers.get("cookie")?.first()?;
392 for pair in header.split(';') {
393 let (k, v) = pair.trim().split_once('=')?;
394 if k == name {
395 return Some(v.to_string());
396 }
397 }
398 None
399}
400
401fn post_arg(ctx: &Context, field: &str) -> Option<String> {
402 let content_type = ctx.request.headers.get("content-type")?.first()?;
403 if !content_type.starts_with("application/x-www-form-urlencoded") {
404 return None;
405 }
406 let body = std::str::from_utf8(&ctx.request.body).ok()?;
407 for pair in body.split('&') {
408 let (k, v) = pair.split_once('=')?;
409 if k == field {
410 return Some(urldecode(v));
411 }
412 }
413 None
414}
415
416fn urldecode(s: &str) -> String {
417 let mut out = Vec::with_capacity(s.len());
418 let bytes = s.as_bytes();
419 let mut i = 0;
420 while i < bytes.len() {
421 match bytes[i] {
422 b'+' => out.push(b' '),
423 b'%' if i + 2 < bytes.len() => {
424 if let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
425 out.push(h * 16 + l);
426 i += 3;
427 continue;
428 }
429 out.push(b'%');
430 }
431 b => out.push(b),
432 }
433 i += 1;
434 }
435 String::from_utf8_lossy(&out).into_owned()
436}
437
438fn hex_val(b: u8) -> Option<u8> {
439 match b {
440 b'0'..=b'9' => Some(b - b'0'),
441 b'a'..=b'f' => Some(b - b'a' + 10),
442 b'A'..=b'F' => Some(b - b'A' + 10),
443 _ => None,
444 }
445}
446
447fn message_str<'a>(ctx: &'a Context, key: &str) -> Option<Cow<'a, str>> {
448 match ctx.message.get(key)? {
449 serde_json::Value::String(s) => Some(Cow::Borrowed(s.as_str())),
450 other => Some(Cow::Owned(other.to_string())),
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
458 use bytes::Bytes;
459 use std::collections::HashMap;
460
461 fn test_ctx() -> Context {
462 let mut headers = HashMap::new();
463 headers.insert("user-agent".to_string(), vec!["Mozilla/5.0".to_string()]);
464 headers.insert(
465 "cookie".to_string(),
466 vec!["session=abc123; theme=dark".to_string()],
467 );
468 headers.insert("x-tags".to_string(), vec!["beta, internal".to_string()]);
469 let mut query = HashMap::new();
470 query.insert("name".to_string(), vec!["jack".to_string()]);
471 query.insert("age".to_string(), vec!["30".to_string()]);
472 let mut message = HashMap::new();
473 message.insert("consumer.name".to_string(), serde_json::json!("alice"));
474
475 Context {
476 request: GatewayRequest {
477 method: "GET".to_string(),
478 path: "/api/users".to_string(),
479 host: "example.com".to_string(),
480 scheme: "http".to_string(),
481 headers,
482 query_params: query,
483 body: Bytes::new(),
484 remote_addr: "10.1.2.3:44321".to_string(),
485 protocol: Protocol::Http1,
486 },
487 response: GatewayResponse {
488 status_code: 502,
489 headers: HashMap::new(),
490 body: Bytes::from_static(b"bad gateway"),
491 },
492 message,
493 errors: Vec::new(),
494 }
495 }
496
497 #[test]
498 fn test_resolve_basic_vars() {
499 let ctx = test_ctx();
500 let cases = [
501 ("uri", Some("/api/users")),
502 ("method", Some("GET")),
503 ("host", Some("example.com")),
504 ("scheme", Some("http")),
505 ("remote_addr", Some("10.1.2.3")),
506 ("remote_port", Some("44321")),
507 ("status", Some("502")),
508 ("resp_body", Some("bad gateway")),
509 ("arg_name", Some("jack")),
510 ("arg_missing", None),
511 ("http_user_agent", Some("Mozilla/5.0")),
512 ("http_missing", None),
513 ("cookie_session", Some("abc123")),
514 ("cookie_theme", Some("dark")),
515 ("cookie_missing", None),
516 ("consumer_name", Some("alice")),
517 ("consumer_group_id", None),
518 ("unknown_var", None),
519 ];
520 for (name, expected) in cases {
521 assert_eq!(resolve(&ctx, name).as_deref(), expected, "var {name}");
522 }
523 }
524
525 #[test]
526 fn test_resolve_post_arg() {
527 let mut ctx = test_ctx();
528 ctx.request.headers.insert(
529 "content-type".to_string(),
530 vec!["application/x-www-form-urlencoded".to_string()],
531 );
532 ctx.request.body = Bytes::from_static(b"user=bob¬e=hello%20world&plus=a+b");
533 assert_eq!(resolve(&ctx, "post_arg_user").as_deref(), Some("bob"));
534 assert_eq!(
535 resolve(&ctx, "post_arg_note").as_deref(),
536 Some("hello world")
537 );
538 assert_eq!(resolve(&ctx, "post_arg_plus").as_deref(), Some("a b"));
539 assert_eq!(resolve(&ctx, "post_arg_missing"), None);
540
541 ctx.request.headers.insert(
543 "content-type".to_string(),
544 vec!["application/json".to_string()],
545 );
546 assert_eq!(resolve(&ctx, "post_arg_user"), None);
547 }
548
549 #[test]
550 fn test_interpolate() {
551 let ctx = test_ctx();
552 assert_eq!(
553 interpolate(&ctx, "$remote_addr -> $uri"),
554 "10.1.2.3 -> /api/users"
555 );
556 assert_eq!(
557 interpolate(&ctx, "${scheme}://${host}${uri}"),
558 "http://example.com/api/users"
559 );
560 assert_eq!(interpolate(&ctx, "user=$arg_name!"), "user=jack!");
561 assert_eq!(interpolate(&ctx, "missing=[$arg_nope]"), "missing=[]");
562 assert_eq!(interpolate(&ctx, "cost: 5$"), "cost: 5$");
563 assert_eq!(interpolate(&ctx, "no vars here"), "no vars here");
564 }
565
566 fn expr(json: serde_json::Value) -> Expr {
567 Expr::parse(&json).expect("expression should parse")
568 }
569
570 #[test]
571 fn test_expr_operators() {
572 let ctx = test_ctx();
573 let truthy = [
574 serde_json::json!([["arg_name", "==", "jack"]]),
575 serde_json::json!([["arg_name", "~=", "jill"]]),
576 serde_json::json!([["arg_age", ">", 18]]),
577 serde_json::json!([["arg_age", "<=", "30"]]),
578 serde_json::json!([["http_user_agent", "~~", "Mozilla.+"]]),
579 serde_json::json!([["http_user_agent", "~*", "mozilla.+"]]),
580 serde_json::json!([["arg_name", "in", ["jack", "jill"]]]),
581 serde_json::json!([["http_x_tags", "has", "beta"]]),
582 serde_json::json!([["remote_addr", "ipmatch", ["10.0.0.0/8"]]]),
583 serde_json::json!([["remote_addr", "ipmatch", ["10.1.2.3"]]]),
584 serde_json::json!([["arg_name", "!", "==", "jill"]]),
585 serde_json::json!([["arg_name", "==", "jack"], ["arg_age", ">=", 30]]),
586 serde_json::json!([["OR", ["arg_name", "==", "nope"], ["arg_age", "==", "30"]]]),
587 ];
588 for case in &truthy {
589 assert!(expr(case.clone()).eval(&ctx), "should be true: {case}");
590 }
591
592 let falsy = [
593 serde_json::json!([["arg_name", "==", "jill"]]),
594 serde_json::json!([["arg_age", ">", 30]]),
595 serde_json::json!([["remote_addr", "ipmatch", ["192.168.0.0/16"]]]),
596 serde_json::json!([["arg_name", "==", "jack"], ["arg_age", ">", 99]]),
597 serde_json::json!([["AND", ["arg_name", "==", "jack"], ["arg_age", ">", 99]]]),
598 serde_json::json!([["arg_missing", "==", "x"]]),
599 ];
600 for case in &falsy {
601 assert!(!expr(case.clone()).eval(&ctx), "should be false: {case}");
602 }
603 }
604
605 #[test]
606 fn test_expr_parse_errors() {
607 let bad = [
608 serde_json::json!("not an array"),
609 serde_json::json!([["arg_x", "unknown_op", "v"]]),
610 serde_json::json!([["arg_x", "~~", "("]]),
611 serde_json::json!([["remote_addr", "ipmatch", ["not-an-ip"]]]),
612 serde_json::json!([["arg_x", "=="]]),
613 serde_json::json!([[]]),
614 ];
615 for case in &bad {
616 assert!(Expr::parse(case).is_err(), "should fail to parse: {case}");
617 }
618 }
619
620 #[test]
621 fn test_split_remote_addr_forms() {
622 assert_eq!(split_remote_addr("1.2.3.4:80"), ("1.2.3.4", Some("80")));
623 assert_eq!(split_remote_addr("1.2.3.4"), ("1.2.3.4", None));
624 assert_eq!(split_remote_addr("[::1]:8080"), ("::1", Some("8080")));
625 assert_eq!(split_remote_addr("::1"), ("::1", None));
626 }
627}