use rustlavel_client::Client;
use rustlavel_core::Result;
use rustlavel_http::{Headers, Method, Request, Response, Status};
use crate::route::{Upstream, is_forwardable};
pub async fn forward(client: &Client, upstream: &Upstream, request: Request) -> Result<Response> {
let url = upstream.url_for(request.target());
let method = request.method();
let peer = request.ip();
let mut outbound = client.request(method, url);
for (name, value) in request.headers().iter() {
if is_forwardable(name) {
outbound = outbound.header(name, value.to_string());
}
}
if let Some(host) = upstream.forwarded_host() {
outbound = outbound.header("host", host.to_string());
}
if let Some(address) = peer {
outbound = outbound.header("x-forwarded-for", address);
}
outbound = outbound.header("x-forwarded-proto", request.scheme());
let body = request.body().to_vec();
if !body.is_empty() {
outbound = outbound.body(body);
}
match outbound.send().await {
Ok(upstream_response) => {
let mut response = Response::new(upstream_response.status)
.with_body(upstream_response.body.clone());
for (name, value) in upstream_response.headers.iter() {
if is_forwardable(name) {
response = response.with_header(name, value.to_string());
}
}
Ok(response)
}
Err(error) => {
rustlavel_core::warn!("gateway: {} is not answering: {error}", upstream.base());
Ok(Response::new(Status(502))
.with_json(rustlavel_core::Json::object([(
"message",
rustlavel_core::Json::from("The service behind this endpoint is not responding."),
)])))
}
}
}
pub fn forwardable(headers: &Headers) -> Vec<(String, String)> {
headers
.iter()
.filter(|(name, _)| is_forwardable(name))
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect()
}
pub fn carries_body(method: Method) -> bool {
matches!(method, Method::Post | Method::Put | Method::Patch | Method::Delete)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hop_by_hop_headers_are_dropped_in_both_directions() {
let mut headers = Headers::new();
headers.set("authorization", "Bearer abc");
headers.set("connection", "close");
headers.set("transfer-encoding", "chunked");
headers.set("content-type", "application/json");
let crossing: Vec<String> =
forwardable(&headers).into_iter().map(|(name, _)| name).collect();
assert!(crossing.contains(&"authorization".to_string()));
assert!(crossing.contains(&"content-type".to_string()));
assert!(
!crossing.contains(&"connection".to_string()),
"a connection header crossed the gateway, which is smuggling with an extra hop"
);
assert!(!crossing.contains(&"transfer-encoding".to_string()));
}
#[test]
fn only_methods_that_define_a_body_carry_one() {
assert!(carries_body(Method::Post));
assert!(carries_body(Method::Put));
assert!(!carries_body(Method::Get));
assert!(!carries_body(Method::Head));
}
}