pub mod policy;
use crate::{Method, Request, Response, StatusCode, StreamingBody, header::LOCATION};
use iri_string::types::{UriAbsoluteString, UriReferenceStr};
use rama_core::{
Layer, Service,
extensions::{Extension, ExtensionsRef},
};
use rama_http_types::{
HeaderMap,
header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING},
};
use rama_net::uri::Uri;
use rama_utils::macros::define_inner_service_accessors;
use std::fmt;
use self::policy::{Action, Attempt, Policy, Standard};
#[derive(Clone)]
pub struct FollowRedirectLayer<P = Standard> {
policy: P,
}
impl FollowRedirectLayer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for FollowRedirectLayer {
fn default() -> Self {
Self::with_policy(Standard::default())
}
}
impl<P: fmt::Debug> fmt::Debug for FollowRedirectLayer<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FollowRedirectLayer")
.field("policy", &self.policy)
.finish()
}
}
impl<P> FollowRedirectLayer<P> {
pub const fn with_policy(policy: P) -> Self {
Self { policy }
}
}
impl<S, P> Layer<S> for FollowRedirectLayer<P>
where
S: Clone,
P: Clone,
{
type Service = FollowRedirect<S, P>;
fn layer(&self, inner: S) -> Self::Service {
FollowRedirect {
inner,
policy: self.policy.clone(),
}
}
fn into_layer(self, inner: S) -> Self::Service {
FollowRedirect {
inner,
policy: self.policy,
}
}
}
#[derive(Debug, Clone)]
pub struct FollowRedirect<S, P = Standard> {
inner: S,
policy: P,
}
impl<S> FollowRedirect<S> {
pub fn new(inner: S) -> Self {
Self::with_policy(inner, Standard::default())
}
}
impl<S, P> FollowRedirect<S, P> {
pub const fn with_policy(inner: S, policy: P) -> Self {
Self { inner, policy }
}
define_inner_service_accessors!();
}
impl<ReqBody, ResBody, S, P> Service<Request<ReqBody>> for FollowRedirect<S, P>
where
S: Service<Request<ReqBody>, Output = Response<ResBody>>,
ReqBody: StreamingBody + Default + Send + 'static,
ResBody: Send + 'static,
P: Policy<ReqBody, S::Error> + Clone,
{
type Output = Response<ResBody>;
type Error = S::Error;
fn serve(
&self,
mut req: Request<ReqBody>,
) -> impl Future<Output = Result<Self::Output, Self::Error>> {
let mut method = req.method().clone();
let mut uri = req.uri().clone();
let version = req.version();
let mut policy = self.policy.clone();
let mut body = BodyRepr::None;
body.try_clone_from(&mut policy, req.body());
let caller_extensions = req.fork_extensions_in_place();
policy.on_request(&mut req);
let mut headers = req.headers().clone();
let service = &self.inner;
async move {
loop {
let res = service.serve(req).await?;
res.extensions().insert(RequestUri(uri.clone()));
let previous_method = method.clone();
let drop_payload_headers = |headers: &mut HeaderMap| {
for header in &[
CONTENT_TYPE,
CONTENT_LENGTH,
CONTENT_ENCODING,
TRANSFER_ENCODING,
] {
headers.remove(header);
}
};
match res.status() {
StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND => {
if method == Method::POST {
method = Method::GET;
body = BodyRepr::Empty;
drop_payload_headers(&mut headers);
}
}
StatusCode::SEE_OTHER => {
if method != Method::HEAD {
method = Method::GET;
}
body = BodyRepr::Empty;
drop_payload_headers(&mut headers);
}
StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {}
_ => return Ok(res),
};
let Some(taken_body) = body.take() else {
return Ok(res);
};
let location = res
.headers()
.get(&LOCATION)
.and_then(|loc| resolve_uri(std::str::from_utf8(loc.as_bytes()).ok()?, &uri));
let Some(location) = location else {
return Ok(res);
};
let attempt = Attempt {
status: res.status(),
method: &method,
location: &location,
previous_method: &previous_method,
previous: &uri,
};
match policy.redirect(&attempt)? {
Action::Follow => {
uri = location;
body.try_clone_from(&mut policy, &taken_body);
req = Request::new(taken_body);
*req.uri_mut() = uri.clone();
*req.method_mut() = method.clone();
*req.version_mut() = version;
*req.headers_mut() = headers.clone();
req.set_extensions(caller_extensions.fork());
policy.on_request(&mut req);
headers = req.headers().clone();
}
Action::Stop => return Ok(res),
}
}
}
}
}
#[derive(Debug, Clone, Extension)]
#[extension(tags(http))]
pub struct RequestUri(pub Uri);
#[derive(Debug)]
enum BodyRepr<B> {
Some(B),
Empty,
None,
}
impl<B> BodyRepr<B>
where
B: StreamingBody + Default,
{
fn take(&mut self) -> Option<B> {
match std::mem::replace(self, Self::None) {
Self::Some(body) => Some(body),
Self::Empty => {
*self = Self::Empty;
Some(B::default())
}
Self::None => None,
}
}
fn try_clone_from<P, E>(&mut self, policy: &mut P, body: &B)
where
P: Policy<B, E>,
{
match self {
Self::Some(_) | Self::Empty => {}
Self::None => {
if let Some(body) = clone_body(policy, body) {
*self = Self::Some(body);
}
}
}
}
}
fn clone_body<P, B, E>(policy: &mut P, body: &B) -> Option<B>
where
P: Policy<B, E>,
B: StreamingBody + Default,
{
if body.size_hint().exact() == Some(0) {
Some(B::default())
} else {
policy.clone_body(body)
}
}
fn resolve_uri(relative: &str, base: &Uri) -> Option<Uri> {
let relative = UriReferenceStr::new(relative).ok()?;
let base = UriAbsoluteString::try_from(base.to_string()).ok()?;
let uri = relative.resolve_against(&base).to_string();
Uri::try_from(uri).ok()
}
#[cfg(test)]
mod tests {
use super::{policy::*, *};
use crate::{Body, header::LOCATION};
use parking_lot::Mutex;
use rama_core::Layer;
use rama_core::extensions::ExtensionsRef;
use rama_core::service::service_fn;
use std::{convert::Infallible, sync::Arc};
#[test]
fn layer_debug_includes_policy() {
assert_eq!(
format!("{:?}", FollowRedirectLayer::with_policy(Action::Follow)),
"FollowRedirectLayer { policy: Follow }"
);
}
#[test]
fn body_representation_preserves_clone_refusal_and_empty_state() {
let mut none = BodyRepr::<Body>::None;
assert!(none.take().is_none());
let mut empty = BodyRepr::<Body>::Empty;
assert!(empty.take().is_some());
assert!(empty.take().is_some());
let mut policy = Action::Follow;
assert!(
clone_body::<_, _, Infallible>(&mut policy, &Body::from("not cloneable")).is_none()
);
assert!(clone_body::<_, _, Infallible>(&mut policy, &Body::empty()).is_some());
}
#[tokio::test]
async fn follows() {
let svc = FollowRedirectLayer::with_policy(Action::Follow).into_layer(service_fn(handle));
let req = Request::builder()
.uri("http://example.com/42")
.body(Body::empty())
.unwrap();
let res = svc.serve(req).await.unwrap();
assert_eq!(*res.body(), 0);
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/0"
);
}
#[tokio::test]
async fn stops() {
let svc = FollowRedirectLayer::with_policy(Action::Stop).into_layer(service_fn(handle));
let req = Request::builder()
.uri("http://example.com/42")
.body(Body::empty())
.unwrap();
let res = svc.serve(req).await.unwrap();
assert_eq!(*res.body(), 42);
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/42"
);
}
#[tokio::test]
async fn limited() {
let svc = FollowRedirectLayer::with_policy(Limited::new(10)).into_layer(service_fn(handle));
let req = Request::builder()
.uri("http://example.com/42")
.body(Body::empty())
.unwrap();
let res = svc.serve(req).await.unwrap();
assert_eq!(*res.body(), 42 - 10);
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/32"
);
}
async fn handle<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
let n: u64 = req
.uri()
.first_path_segment()
.and_then(|segment| segment.as_encoded_str().parse().ok())
.unwrap();
let mut res = Response::builder();
if n > 0 {
res = res
.status(StatusCode::MOVED_PERMANENTLY)
.header(LOCATION, format!("/{}", n - 1));
}
Ok::<_, Infallible>(res.body(n).unwrap())
}
#[derive(Clone, Debug, PartialEq, rama_core::extensions::Extension)]
struct Marker(u32);
async fn handle_marker<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
let n: u64 = req
.uri()
.first_path_segment()
.and_then(|segment| segment.as_encoded_str().parse().ok())
.unwrap();
let mut res = Response::builder();
if n > 0 {
res = res
.status(StatusCode::MOVED_PERMANENTLY)
.header(LOCATION, format!("/{}", n - 1));
}
let res = res.body(n).unwrap();
if let Some(marker) = req.extensions().get_ref::<Marker>() {
res.extensions().insert(marker.clone());
}
Ok::<_, Infallible>(res)
}
#[tokio::test]
async fn final_hop_reads_the_callers_extensions() {
let svc = FollowRedirectLayer::new().into_layer(service_fn(handle_marker));
let req = Request::builder()
.uri("http://example.com/3")
.body(Body::empty())
.unwrap();
req.extensions().insert(Marker(7));
let res = svc.serve(req).await.unwrap();
assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
}
#[tokio::test]
async fn fork_reads_the_callers_extensions_on_every_hop() {
let hops = Arc::new(Mutex::new(Vec::new()));
let svc = FollowRedirectLayer::new().into_layer(service_fn({
let hops = hops.clone();
move |req: Request<Body>| {
hops.lock()
.push(req.extensions().get_ref::<Marker>().cloned());
handle(req)
}
}));
let req = Request::builder()
.uri("http://example.com/2")
.body(Body::empty())
.unwrap();
req.extensions().insert(Marker(7));
svc.serve(req).await.unwrap();
assert_eq!(
hops.lock().as_slice(),
[Some(Marker(7)), Some(Marker(7)), Some(Marker(7))],
);
}
#[derive(Debug, Clone, Default)]
struct StampingPolicy(u32);
impl<B, E> Policy<B, E> for StampingPolicy {
fn redirect(&mut self, _: &Attempt<'_>) -> Result<Action, E> {
Ok(Action::Follow)
}
fn on_request(&mut self, req: &mut Request<B>) {
if !req.extensions().contains::<Marker>() {
req.extensions().insert(Marker(self.0));
self.0 += 1;
}
}
}
#[tokio::test]
async fn policy_on_request_inserts_stay_per_hop() {
let hops = Arc::new(Mutex::new(Vec::new()));
let svc =
FollowRedirectLayer::with_policy(StampingPolicy::default()).into_layer(service_fn({
let hops = hops.clone();
move |req: Request<Body>| {
hops.lock()
.push(req.extensions().get_ref::<Marker>().cloned());
handle(req)
}
}));
let req = Request::builder()
.uri("http://example.com/2")
.body(Body::empty())
.unwrap();
let caller_extensions = req.extensions().clone();
svc.serve(req).await.unwrap();
assert_eq!(
hops.lock().as_slice(),
[Some(Marker(0)), Some(Marker(1)), Some(Marker(2))],
);
assert!(!caller_extensions.contains::<Marker>());
}
#[derive(Debug, Clone, Default)]
struct RemoveHeaderOnce(bool);
impl<B, E> Policy<B, E> for RemoveHeaderOnce {
fn redirect(&mut self, _: &Attempt<'_>) -> Result<Action, E> {
Ok(Action::Follow)
}
fn on_request(&mut self, req: &mut Request<B>) {
if !self.0 {
req.headers_mut().remove("x-remove-on-first-hop");
self.0 = true;
}
}
}
#[tokio::test]
async fn first_hop_policy_header_changes_are_carried_forward() {
let seen = Arc::new(Mutex::new(Vec::new()));
let svc =
FollowRedirectLayer::with_policy(RemoveHeaderOnce::default()).into_layer(service_fn({
let seen = seen.clone();
move |req: Request<Body>| {
seen.lock()
.push(req.headers().contains_key("x-remove-on-first-hop"));
handle(req)
}
}));
let req = Request::builder()
.uri("http://example.com/1")
.header("x-remove-on-first-hop", "yes")
.body(Body::empty())
.unwrap();
svc.serve(req).await.unwrap();
assert_eq!(seen.lock().as_slice(), [false, false]);
}
async fn handle_cookie_chain<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
let host = req.uri().host_str();
let path = req.uri().path_ref_or_root();
let location = if host.as_deref() == Some("a.example.com") {
Some("http://b.example.com/second")
} else if host.as_deref() == Some("b.example.com") && path == "/second" {
Some("http://b.example.com/final")
} else {
None
};
let mut res = Response::builder();
if let Some(location) = location {
res = res
.status(StatusCode::MOVED_PERMANENTLY)
.header(LOCATION, location);
}
let mut res = res.body(0u64).unwrap();
if req.headers().contains_key(crate::header::COOKIE) {
res.headers_mut()
.insert("x-saw-cookie", crate::HeaderValue::from_static("1"));
}
Ok::<_, Infallible>(res)
}
#[tokio::test]
async fn credentials_do_not_resurrect_after_cross_origin() {
let svc = FollowRedirectLayer::default().into_layer(service_fn(handle_cookie_chain));
let req = Request::builder()
.uri("http://a.example.com/")
.header(crate::header::COOKIE, "session=secret")
.body(Body::empty())
.unwrap();
let res = svc.serve(req).await.unwrap();
assert!(
!res.headers().contains_key("x-saw-cookie"),
"Cookie resurrected on a same-origin hop after being dropped cross-origin",
);
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://b.example.com/final"
);
}
#[derive(Debug, Clone)]
struct RouteDecider<S> {
inner: S,
seen: Arc<Mutex<Vec<String>>>,
}
#[derive(Clone, Debug, PartialEq, rama_core::extensions::Extension)]
struct Route(String);
impl<S, B> Service<Request<B>> for RouteDecider<S>
where
S: Service<Request<B>>,
B: Send + 'static,
{
type Output = S::Output;
type Error = S::Error;
async fn serve(&self, req: Request<B>) -> Result<Self::Output, Self::Error> {
if req.extensions().get_ref::<Route>().is_none() {
let host = req.uri().host_str().unwrap_or_default().into_owned();
req.extensions().insert(Route(host));
}
self.seen
.lock()
.push(req.extensions().get_ref::<Route>().unwrap().0.clone());
self.inner.serve(req).await
}
}
#[tokio::test]
async fn inner_per_hop_decision_does_not_leak_into_the_next_hop() {
let seen = Arc::new(Mutex::new(Vec::new()));
let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
inner: service_fn(handle_cookie_chain),
seen: seen.clone(),
});
let req = Request::builder()
.uri("http://a.example.com/")
.body(Body::empty())
.unwrap();
svc.serve(req).await.unwrap();
assert_eq!(
seen.lock().as_slice(),
["a.example.com", "b.example.com", "b.example.com"],
);
}
#[tokio::test]
async fn outer_per_hop_decision_is_inherited_by_every_hop() {
let seen = Arc::new(Mutex::new(Vec::new()));
let svc = RouteDecider {
inner: FollowRedirectLayer::new().into_layer(service_fn({
let seen = seen.clone();
move |req: Request<Body>| {
seen.lock()
.push(req.extensions().get_ref::<Route>().unwrap().0.clone());
handle_cookie_chain(req)
}
})),
seen: Arc::new(Mutex::new(Vec::new())),
};
let req = Request::builder()
.uri("http://a.example.com/")
.body(Body::empty())
.unwrap();
svc.serve(req).await.unwrap();
assert_eq!(
seen.lock().as_slice(),
["a.example.com", "a.example.com", "a.example.com"],
);
}
#[tokio::test]
async fn hop_inserts_never_reach_the_callers_request_store() {
let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
inner: service_fn(handle_cookie_chain),
seen: Arc::new(Mutex::new(Vec::new())),
});
let req = Request::builder()
.uri("http://a.example.com/")
.body(Body::empty())
.unwrap();
let caller_extensions = req.extensions().clone();
svc.serve(req).await.unwrap();
assert!(
!caller_extensions.contains::<Route>(),
"a hop's insert leaked back into the caller's request extensions",
);
}
#[tokio::test]
async fn response_still_exposes_the_final_hops_extensions() {
let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
inner: service_fn(async |req: Request<Body>| {
let request_extensions = req.extensions().clone();
let mut res = handle_cookie_chain(req).await?;
res.set_extensions(request_extensions.fork());
Ok::<_, Infallible>(res)
}),
seen: Arc::new(Mutex::new(Vec::new())),
});
let req = Request::builder()
.uri("http://a.example.com/")
.body(Body::empty())
.unwrap();
req.extensions().insert(Marker(7));
let res = svc.serve(req).await.unwrap();
assert_eq!(
res.extensions().get_ref::<Route>(),
Some(&Route("b.example.com".to_owned())),
);
assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
}
#[tokio::test]
async fn test_301_redirects() {
let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
if attempt.previous_method() == Method::POST && attempt.method() == Method::GET {
Ok(Action::Stop)
} else {
Ok(Action::Follow)
}
});
let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
{
let req = Request::builder()
.method(Method::POST)
.uri("http://example.com/301")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/301");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/301"
);
}
{
let req = Request::builder()
.method(Method::GET)
.uri("http://example.com/301")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/301/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/301"
);
}
}
#[tokio::test]
async fn test_302_redirects() {
let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
if attempt.previous_method() != attempt.method() {
Ok(Action::Stop)
} else {
Ok(Action::Follow)
}
});
let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
{
let req = Request::builder()
.method(Method::POST)
.uri("http://example.com/302")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/302");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/302"
);
}
{
let req = Request::builder()
.method(Method::PUT)
.uri("http://example.com/302")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/302/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/302"
);
}
{
let req = Request::builder()
.method(Method::HEAD)
.uri("http://example.com/302")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/302/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/302"
);
}
}
#[tokio::test]
async fn test_303_redirects() {
let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
if attempt.previous_method() != attempt.method() {
Ok(Action::Stop)
} else {
Ok(Action::Follow)
}
});
let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
{
let req = Request::builder()
.method(Method::POST)
.uri("http://example.com/303")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/303");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/303"
);
}
{
let req = Request::builder()
.method(Method::PUT)
.uri("http://example.com/303")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/303");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/303"
);
}
{
let req = Request::builder()
.method(Method::HEAD)
.uri("http://example.com/303")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/303/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/303"
);
}
}
#[tokio::test]
async fn test_307_308_redirects() {
let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
if attempt.previous_method() != Method::POST || attempt.method() != Method::POST {
Ok(Action::Stop)
} else {
Ok(Action::Follow)
}
});
let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
{
let req = Request::builder()
.method(Method::POST)
.uri("http://example.com/307")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/307/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/307"
);
}
{
let req = Request::builder()
.method(Method::POST)
.uri("http://example.com/308")
.body(Body::empty())
.unwrap();
let res = svc.clone().serve(req).await.unwrap();
assert_eq!(*res.body(), "/target/308/final");
assert_eq!(
res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
"http://example.com/target/308"
);
}
}
async fn redirections<B>(req: Request<B>) -> Result<Response<String>, Infallible> {
let path = req.uri().path_ref_or_root();
let mut res = Response::builder();
let body_str;
res = if path == "/301" {
let case = "/target/301";
body_str = case.to_owned();
res.status(StatusCode::MOVED_PERMANENTLY)
.header(LOCATION, case)
} else if path == "/302" {
let case = "/target/302";
body_str = case.to_owned();
res.status(StatusCode::FOUND).header(LOCATION, case)
} else if path == "/303" {
let case = "/target/303";
body_str = case.to_owned();
res.status(StatusCode::SEE_OTHER).header(LOCATION, case)
} else if path == "/307" {
let case = "/target/307";
body_str = case.to_owned();
res.status(StatusCode::TEMPORARY_REDIRECT)
.header(LOCATION, case)
} else if path == "/308" {
let case = "/target/308";
body_str = case.to_owned();
res.status(StatusCode::PERMANENT_REDIRECT)
.header(LOCATION, case)
} else {
body_str = format!("{path}/final");
res.status(StatusCode::OK)
};
Ok::<_, Infallible>(res.body(body_str).unwrap())
}
}