use super::{Ctx, HttpResponse};
use ahash::AHashMap;
use async_trait::async_trait;
use pingora::http::ResponseHeader;
use pingora::proxy::Session;
use std::borrow::Cow;
use std::sync::Arc;
use strum::EnumString;
#[derive(
PartialEq, Debug, Default, Clone, Copy, EnumString, strum::Display,
)]
#[strum(serialize_all = "snake_case")]
pub enum PluginStep {
EarlyRequest,
#[default]
Request,
ProxyUpstream,
UpstreamResponse,
Response,
}
#[derive(Debug)]
pub enum RequestPluginResult {
Skipped,
Continue,
Respond(HttpResponse),
}
#[derive(Debug, PartialEq, Eq)]
pub enum ResponsePluginResult {
Unchanged,
Modified,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ResponseBodyPluginResult {
Unchanged,
PartialReplaced,
FullyReplaced,
}
impl PartialEq for RequestPluginResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(RequestPluginResult::Skipped, RequestPluginResult::Skipped) => {
true
},
(RequestPluginResult::Continue, RequestPluginResult::Continue) => {
true
},
_ => false,
}
}
}
#[async_trait]
pub trait Plugin: Sync + Send {
fn config_key(&self) -> Cow<'_, str> {
Cow::Borrowed("")
}
#[inline]
async fn handle_request(
&self,
_step: PluginStep,
_session: &mut Session,
_ctx: &mut Ctx,
) -> pingora::Result<RequestPluginResult> {
Ok(RequestPluginResult::Skipped)
}
#[inline]
async fn handle_response(
&self,
_session: &mut Session,
_ctx: &mut Ctx,
_upstream_response: &mut ResponseHeader,
) -> pingora::Result<ResponsePluginResult> {
Ok(ResponsePluginResult::Unchanged)
}
#[inline]
fn handles_plugin_response(&self) -> bool {
false
}
#[inline]
fn error_page(
&self,
_session: &Session,
_status: http::StatusCode,
_message: &str,
) -> Option<(http::HeaderValue, bytes::Bytes)> {
None
}
#[inline]
fn handle_response_body(
&self,
_session: &mut Session,
_ctx: &mut Ctx,
_body: &mut Option<bytes::Bytes>,
_end_of_stream: bool,
) -> pingora::Result<ResponseBodyPluginResult> {
Ok(ResponseBodyPluginResult::Unchanged)
}
#[inline]
fn handle_upstream_response(
&self,
_session: &mut Session,
_ctx: &mut Ctx,
_upstream_response: &mut ResponseHeader,
) -> pingora::Result<ResponsePluginResult> {
Ok(ResponsePluginResult::Unchanged)
}
#[inline]
fn handle_upstream_response_body(
&self,
_session: &mut Session,
_ctx: &mut Ctx,
_body: &mut Option<bytes::Bytes>,
_end_of_stream: bool,
) -> pingora::Result<ResponseBodyPluginResult> {
Ok(ResponseBodyPluginResult::Unchanged)
}
}
pub struct MissingPlugin {
name: Arc<str>,
}
impl MissingPlugin {
pub fn new(name: Arc<str>) -> Self {
Self { name }
}
}
#[async_trait]
impl Plugin for MissingPlugin {
async fn handle_request(
&self,
_step: PluginStep,
_session: &mut Session,
_ctx: &mut Ctx,
) -> pingora::Result<RequestPluginResult> {
Err(crate::new_internal_error(
500,
format!("plugin {} is not available", self.name),
))
}
}
pub trait PluginProvider: Send + Sync {
fn get(&self, name: &str) -> Option<Arc<dyn Plugin>>;
fn version(&self) -> u64 {
0
}
}
pub type Plugins = AHashMap<String, Arc<dyn Plugin>>;
pub async fn decorate_plugin_response(
session: &mut Session,
ctx: &mut Ctx,
header: &mut ResponseHeader,
) -> pingora::Result<()> {
let Some(plugins) = ctx.response_plugins.take() else {
return Ok(());
};
let mut result = Ok(());
for (_, plugin) in plugins.iter() {
if !plugin.handles_plugin_response() {
continue;
}
if let Err(e) = plugin.handle_response(session, ctx, header).await {
result = Err(e);
break;
}
}
ctx.response_plugins = Some(plugins);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NamedPlugin;
use pretty_assertions::assert_eq;
struct Marker {
asks: bool,
}
#[async_trait]
impl Plugin for Marker {
fn handles_plugin_response(&self) -> bool {
self.asks
}
async fn handle_response(
&self,
_session: &mut Session,
_ctx: &mut Ctx,
upstream_response: &mut ResponseHeader,
) -> pingora::Result<ResponsePluginResult> {
upstream_response.append_header("x-marked", "1")?;
Ok(ResponsePluginResult::Modified)
}
}
#[tokio::test]
async fn test_decorate_plugin_response() {
let mut session = crate::new_test_session(&[], "/").await;
let marked = |header: &ResponseHeader| {
header.headers.get_all("x-marked").iter().count()
};
let mut ctx = Ctx::default();
let mut header = ResponseHeader::build(200, None).unwrap();
decorate_plugin_response(&mut session, &mut ctx, &mut header)
.await
.unwrap();
assert_eq!(0, marked(&header));
let plugins: Vec<NamedPlugin> = vec![
(Arc::from("asks"), Arc::new(Marker { asks: true })),
(Arc::from("does-not"), Arc::new(Marker { asks: false })),
];
ctx.response_plugins = Some(Arc::from(plugins));
decorate_plugin_response(&mut session, &mut ctx, &mut header)
.await
.unwrap();
assert_eq!(1, marked(&header));
assert_eq!(true, ctx.response_plugins.is_some());
}
#[test]
fn test_plugin_step() {
let step = "early_request".parse::<PluginStep>().unwrap();
assert_eq!(step, PluginStep::EarlyRequest);
assert_eq!(step.to_string(), "early_request");
let step = "request".parse::<PluginStep>().unwrap();
assert_eq!(step, PluginStep::Request);
assert_eq!(step.to_string(), "request");
let step = "proxy_upstream".parse::<PluginStep>().unwrap();
assert_eq!(step, PluginStep::ProxyUpstream);
assert_eq!(step.to_string(), "proxy_upstream");
let step = "response".parse::<PluginStep>().unwrap();
assert_eq!(step, PluginStep::Response);
assert_eq!(step.to_string(), "response");
}
#[test]
fn test_request_plugin_result() {
let skip1 = RequestPluginResult::Skipped;
let skip2 = RequestPluginResult::Skipped;
assert_eq!(true, skip1 == skip2);
let continue1 = RequestPluginResult::Continue;
let continue2 = RequestPluginResult::Continue;
assert_eq!(true, continue1 == continue2);
let respond1 = RequestPluginResult::Respond(HttpResponse::no_content());
let respond2 = RequestPluginResult::Respond(HttpResponse::no_content());
assert_eq!(false, respond1 == respond2);
}
}