use std::{borrow::Cow, net::IpAddr, sync::Arc, sync::OnceLock};
use bytes::Bytes;
use http::{HeaderName, Method};
use pingora::{
http::{RequestHeader, ResponseHeader},
proxy::Session,
};
use crate::proxy_server::https_proxy::RouterContext;
mod actions;
mod compile;
mod eval;
mod fields;
mod index;
mod model;
#[cfg(test)]
mod bench;
pub use model::{one_or_many, unlabel_rules, Rule};
use actions::Then;
use compile::Compiled;
use fields::Request;
pub struct Rules {
compiled: Compiled,
index: Option<index::Index>,
}
impl Rules {
pub fn compile(rules: &[Rule]) -> Result<Option<Arc<Self>>, String> {
if rules.is_empty() {
return Ok(None);
}
let compiled = compile::compile(rules)?;
let index = index::Index::build(&compiled);
Ok(Some(Arc::new(Self { compiled, index })))
}
fn candidates(&self, request: &impl Request) -> index::RuleSet {
match &self.index {
Some(index) => {
let host = fields::lowercase(request.host());
let path = crate::routing::paths::normalize_path(request.path());
index.candidates(request.method(), &host, &path)
}
None => index::all(self.compiled.rules.len()),
}
}
#[cfg(test)]
fn matching(&self, request: &impl Request) -> usize {
let compiled = &self.compiled;
let mut state = eval::State::new(compiled);
let candidates = self.candidates(request);
let mut matched = 0;
let mut from = 0;
while let Some(rule) = candidates.next(from) {
from = rule + 1;
if let Some(rule) = compiled.rules.get(rule) {
matched += usize::from(state.holds(compiled, &rule.condition, request));
}
}
matched
}
pub async fn request(&self, session: &mut Session, ctx: &mut RouterContext) -> bool {
let compiled = &self.compiled;
let mut state = eval::State::new(compiled);
let mut candidates = self.candidates(&ProxiedRequest {
session,
host: &ctx.host,
});
let mut from = 0;
while let Some(index) = candidates.next(from) {
from = index + 1;
let Some(rule) = compiled.rules.get(index) else {
break;
};
let holds = {
let request = ProxiedRequest {
session,
host: &ctx.host,
};
state.holds(compiled, &rule.condition, &request)
};
if !holds {
continue;
}
if rule.then.change_request(session.req_header_mut()) {
state.forget();
candidates = self.candidates(&ProxiedRequest {
session,
host: &ctx.host,
});
}
if rule.then.blocks.request(session, ctx).await.is_some() {
return true;
}
ctx.rules.0.push(rule.then.clone());
}
false
}
}
static SERVER: OnceLock<Option<Arc<Rules>>> = OnceLock::new();
pub fn set_server_rules(rules: &[Rule]) -> Result<(), String> {
let _ = SERVER.set(Rules::compile(rules)?);
Ok(())
}
pub fn server() -> Option<&'static Arc<Rules>> {
SERVER.get()?.as_ref()
}
#[derive(Default)]
pub struct Applied(Vec<Arc<Then>>);
impl Applied {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub fn request_body(body: Option<&Bytes>, ctx: &mut RouterContext) -> pingora::Result<()> {
if ctx.rules.is_empty() {
return Ok(());
}
let applied = std::mem::take(&mut ctx.rules);
let result = applied
.0
.iter()
.try_for_each(|then| then.blocks.request_body(body, ctx).map_err(|(_, err)| err));
ctx.rules = applied;
result
}
pub async fn upstream_request(
session: &mut Session,
request: &mut RequestHeader,
ctx: &mut RouterContext,
) {
if ctx.rules.is_empty() {
return;
}
let applied = std::mem::take(&mut ctx.rules);
for then in &applied.0 {
then.blocks.upstream_request(session, request, ctx).await;
}
ctx.rules = applied;
}
pub fn upstream_response(
session: &mut Session,
response: &mut ResponseHeader,
ctx: &mut RouterContext,
) {
if ctx.rules.is_empty() {
return;
}
let applied = std::mem::take(&mut ctx.rules);
for then in applied.0.iter().rev() {
then.blocks.upstream_response(session, response, ctx);
}
ctx.rules = applied;
}
pub async fn response(
session: &mut Session,
response: &mut ResponseHeader,
ctx: &mut RouterContext,
) {
if ctx.rules.is_empty() {
return;
}
let applied = std::mem::take(&mut ctx.rules);
for then in applied.0.iter().rev() {
then.blocks.response(session, response, ctx).await;
}
ctx.rules = applied;
}
struct ProxiedRequest<'a> {
session: &'a Session,
host: &'a str,
}
impl Request for ProxiedRequest<'_> {
fn method(&self) -> &Method {
&self.session.req_header().method
}
fn host(&self) -> &str {
self.host
}
fn path(&self) -> &str {
self.session.req_header().uri.path()
}
fn query(&self) -> Option<&str> {
self.session.req_header().uri.query()
}
fn header(&self, name: &HeaderName) -> Option<Cow<'_, str>> {
let mut values = self.session.req_header().headers.get_all(name).iter();
let first = values.next()?.to_str().ok()?;
let Some(second) = values.next() else {
return Some(Cow::Borrowed(first));
};
let mut joined = format!("{first}, {}", second.to_str().unwrap_or_default());
for value in values {
joined.push_str(", ");
joined.push_str(value.to_str().unwrap_or_default());
}
Some(Cow::Owned(joined))
}
fn client(&self) -> Option<IpAddr> {
crate::proxy_server::client::of_session(self.session)
}
}
#[cfg(test)]
mod tests {
use figment::providers::Format;
use crate::config::hcl::HclFormat;
use super::{one_or_many, Rules};
const PAGES: [(&str, &str); 5] = [
("index", include_str!("../../../../docs/rules/index.mdx")),
(
"conditions",
include_str!("../../../../docs/rules/conditions.mdx"),
),
(
"actions",
include_str!("../../../../docs/rules/actions.mdx"),
),
(
"examples",
include_str!("../../../../docs/rules/examples.mdx"),
),
(
"performance",
include_str!("../../../../docs/rules/performance.mdx"),
),
];
const OTHER_PAGES: [(&str, &str); 1] = [("README", include_str!("../../../../README.md"))];
fn examples(text: &str) -> impl Iterator<Item = &str> {
text.split("```hcl\n")
.skip(1)
.map(|example| example.split("```").next().unwrap_or_default())
}
fn rules(value: &serde_json::Value, found: &mut Vec<Vec<super::Rule>>) {
match value {
serde_json::Value::Object(map) => {
for (key, value) in map {
if key == "rule" {
found.push(one_or_many(value.clone()).unwrap());
} else {
rules(value, found);
}
}
}
serde_json::Value::Array(items) => items.iter().for_each(|item| rules(item, found)),
_ => {}
}
}
fn load(page: &str, example: &str) -> usize {
let value: serde_json::Value =
HclFormat::from_str(example).unwrap_or_else(|err| panic!("{page}: {err}\n{example}"));
let mut found = vec![];
rules(&value, &mut found);
for scope in &found {
if let Err(err) = Rules::compile(scope) {
panic!("{page}: {err}\n{example}");
}
}
found.len()
}
#[test]
fn test_the_examples_of_the_documentation_load() {
let mut count = 0;
for (page, text) in PAGES {
for example in examples(text) {
assert!(
load(page, example) > 0,
"{page}: an example without rules\n{example}"
);
count += 1;
}
}
assert!(count >= 10, "{count} examples");
for (page, text) in OTHER_PAGES {
for example in examples(text).filter(|example| example.contains("rule \"")) {
load(page, example);
}
}
}
}