use super::{
Error, RestrictionCategory, get_hash_key, get_restriction_category_conf,
get_str_conf, get_str_slice_conf,
};
use async_trait::async_trait;
use bytes::Bytes;
use http::StatusCode;
use pingap_config::PluginConf;
use pingap_core::{Ctx, HttpResponse, Plugin, PluginStep, RequestPluginResult};
use pingora::proxy::Session;
use std::borrow::Cow;
use tracing::debug;
type Result<T, E = Error> = std::result::Result<T, E>;
pub struct RefererRestriction {
plugin_step: PluginStep,
referer_list: Vec<String>,
prefix_referer_list: Vec<String>,
restriction_category: RestrictionCategory,
forbidden_resp: HttpResponse,
hash_value: String,
}
impl TryFrom<&PluginConf> for RefererRestriction {
type Error = Error;
fn try_from(value: &PluginConf) -> Result<Self> {
let hash_value = get_hash_key(value);
let mut referer_list = vec![];
let mut prefix_referer_list = vec![];
for item in get_str_slice_conf(value, "referer_list").iter() {
let item = item.trim().to_ascii_lowercase();
if let Some(suffix) = item.strip_prefix('*') {
prefix_referer_list.push(suffix.to_string());
} else {
referer_list.push(item);
}
}
let mut message = get_str_conf(value, "message");
if message.is_empty() {
message = "Request is forbidden".to_string();
}
let params = Self {
hash_value,
plugin_step: PluginStep::Request,
prefix_referer_list,
referer_list,
restriction_category: get_restriction_category_conf(
value,
"referer_restriction",
)?,
forbidden_resp: HttpResponse {
status: StatusCode::FORBIDDEN,
body: Bytes::from(message),
..Default::default()
},
};
Ok(params)
}
}
impl RefererRestriction {
pub fn new(params: &PluginConf) -> Result<Self> {
debug!(
params = pingap_config::masked_toml(params),
"new referer restriction plugin"
);
Self::try_from(params)
}
}
#[async_trait]
impl Plugin for RefererRestriction {
#[inline]
fn config_key(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.hash_value)
}
#[inline]
async fn handle_request(
&self,
step: PluginStep,
session: &mut Session,
_ctx: &mut Ctx,
) -> pingora::Result<RequestPluginResult> {
if step != self.plugin_step {
return Ok(RequestPluginResult::Skipped);
}
let found = session
.get_header(http::header::REFERER)
.and_then(|value| value.to_str().ok())
.and_then(|referer| url::Url::parse(referer).ok())
.is_some_and(|referer| {
referer.host_str().is_some_and(|host| {
self.referer_list.iter().any(|item| item == host)
|| self
.prefix_referer_list
.iter()
.any(|item| host.ends_with(item.as_str()))
})
});
if !self.restriction_category.allows(found) {
return Ok(RequestPluginResult::Respond(
self.forbidden_resp.clone(),
));
}
Ok(RequestPluginResult::Continue)
}
}
register_plugin!("referer_restriction", RefererRestriction);
#[cfg(test)]
mod tests {
use super::*;
use http::StatusCode;
use pingap_config::PluginConf;
use pingap_core::{ConnectionInfo, Ctx, PluginStep};
use pingora::proxy::Session;
use pretty_assertions::assert_eq;
use tokio_test::io::Builder;
#[test]
fn test_referer_restriction_params() {
let params = RefererRestriction::try_from(
&toml::from_str::<PluginConf>(
r###"
referer_list = [
"github.com",
"*.bing.cn",
]
type = "deny"
"###,
)
.unwrap(),
)
.unwrap();
assert_eq!("request", params.plugin_step.to_string());
assert_eq!(".bing.cn", params.prefix_referer_list.join(","));
assert_eq!("github.com", params.referer_list.join(","));
let params = RefererRestriction::try_from(
&toml::from_str::<PluginConf>(
"referer_list = [\"GitHub.com\", \"*.Bing.CN \"]\ntype = \"deny\"",
)
.unwrap(),
)
.unwrap();
assert_eq!(".bing.cn", params.prefix_referer_list.join(","));
assert_eq!("github.com", params.referer_list.join(","));
}
#[tokio::test]
async fn test_referer_restriction() {
let deny = RefererRestriction::new(
&toml::from_str::<PluginConf>(
r###"
referer_list = [
"github.com",
"*.bing.cn",
]
type = "deny"
"###,
)
.unwrap(),
)
.unwrap();
let headers = ["Referer: https://google.com/"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let result = deny
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx::default(),
)
.await
.unwrap();
assert_eq!(true, result == RequestPluginResult::Continue);
let headers = ["Referer: https://github.com/"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let result = deny
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx::default(),
)
.await
.unwrap();
let RequestPluginResult::Respond(resp) = result else {
panic!("result is not Respond");
};
assert_eq!(StatusCode::FORBIDDEN, resp.status);
let headers = ["Referer: https://test.bing.cn/"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let result = deny
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx {
conn: ConnectionInfo {
client_ip: Some("1.1.1.2".to_string()),
..Default::default()
},
..Default::default()
},
)
.await
.unwrap();
let RequestPluginResult::Respond(resp) = result else {
panic!("result is not Respond");
};
assert_eq!(StatusCode::FORBIDDEN, resp.status);
}
}