use super::{
Error, get_bool_conf, get_hash_key, get_int_conf, get_plugin_factory,
get_str_conf,
};
use async_trait::async_trait;
use ctor::ctor;
use http::StatusCode;
use pingap_config::PluginConf;
use pingap_core::{
Ctx, HttpResponse, Plugin, PluginStep, RequestPluginResult,
convert_headers, get_host,
};
use pingora::proxy::Session;
use std::borrow::Cow;
use std::sync::Arc;
use tracing::debug;
type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Redirect {
prefix: String,
http_to_https: bool,
status: StatusCode,
plugin_step: PluginStep,
hash_value: String,
}
impl Redirect {
pub fn new(params: &PluginConf) -> Result<Self> {
debug!(params = params.to_string(), "new redirect plugin");
let hash_value = get_hash_key(params);
let mut prefix = get_str_conf(params, "prefix");
if prefix.len() <= 1 {
prefix = "".to_string();
} else if !prefix.starts_with("/") {
prefix = format!("/{prefix}");
}
let status = match get_int_conf(params, "status") as u16 {
301 => StatusCode::MOVED_PERMANENTLY,
302 => StatusCode::FOUND,
308 => StatusCode::PERMANENT_REDIRECT,
_ => StatusCode::TEMPORARY_REDIRECT,
};
Ok(Self {
hash_value,
prefix,
http_to_https: get_bool_conf(params, "http_to_https"),
status,
plugin_step: PluginStep::Request,
})
}
}
#[async_trait]
impl Plugin for Redirect {
#[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 schema_match = ctx.conn.tls_version.is_some() == self.http_to_https;
if schema_match
&& session.req_header().uri.path().starts_with(&self.prefix)
{
return Ok(RequestPluginResult::Skipped);
}
let host = get_host(session.req_header()).unwrap_or_default();
let schema = if self.http_to_https { "https" } else { "http" };
let location = format!(
"Location: {}://{host}{}{}",
schema,
self.prefix,
session.req_header().uri
);
Ok(RequestPluginResult::Respond(HttpResponse {
status: self.status,
headers: Some(convert_headers(&[location]).unwrap_or_default()),
..Default::default()
}))
}
}
#[ctor]
fn init() {
get_plugin_factory()
.register("redirect", |params| Ok(Arc::new(Redirect::new(params)?)));
}
#[cfg(test)]
mod tests {
use super::*;
use http::StatusCode;
use pingap_config::PluginConf;
use pingap_core::{Ctx, PluginStep};
use pingora::proxy::Session;
use pretty_assertions::assert_eq;
use tokio_test::io::Builder;
#[tokio::test]
async fn test_redirect() {
let redirect = Redirect::new(
&toml::from_str::<PluginConf>(
r###"
http_to_https = true
prefix = "/api"
"###,
)
.unwrap(),
)
.unwrap();
let headers = ["Host: 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 = redirect
.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::TEMPORARY_REDIRECT, resp.status);
assert_eq!(
r###"Some([("location", "https://github.com/api/vicanso/pingap?size=1")])"###,
format!("{:?}", resp.headers)
);
}
#[tokio::test]
async fn test_redirect_with_status() {
for (status_conf, expected_status) in [
(301, StatusCode::MOVED_PERMANENTLY),
(302, StatusCode::FOUND),
(307, StatusCode::TEMPORARY_REDIRECT),
(308, StatusCode::PERMANENT_REDIRECT),
] {
let redirect = Redirect::new(
&toml::from_str::<PluginConf>(&format!(
r###"
http_to_https = true
status = {status_conf}
"###,
))
.unwrap(),
)
.unwrap();
let headers = ["Host: github.com"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap 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 = redirect
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx::default(),
)
.await
.unwrap();
let RequestPluginResult::Respond(resp) = result else {
panic!("result is not Respond for status {status_conf}");
};
assert_eq!(expected_status, resp.status);
}
}
}