use http::HeaderMap;
use praxis_core::reserved_headers::{HOP_BY_HOP_HEADERS, RESPONSE_HOP_BY_HOP_HEADERS};
use crate::{FilterError, HttpFilterContext, SubResponse, actions::Rejection, has_dot_dot_traversal};
pub(crate) fn strip_reserved_headers(headers: &mut HeaderMap) {
let to_remove: Vec<http::header::HeaderName> = headers
.keys()
.filter(|name| praxis_core::reserved_headers::is_reserved(name.as_str()))
.cloned()
.collect();
for name in to_remove {
headers.remove(&name);
}
}
pub(super) fn apply_request_header_mutations(headers: &mut HeaderMap, ctx: &HttpFilterContext<'_>) {
for name in &ctx.request_headers_to_remove {
headers.remove(name);
}
for (name, value) in &ctx.request_headers_to_set {
headers.insert(name.clone(), value.clone());
}
for (name, value) in &ctx.extra_request_headers {
if let (Ok(header_name), Ok(header_value)) = (
http::header::HeaderName::from_bytes(name.as_bytes()),
http::HeaderValue::from_str(value),
) {
headers.insert(header_name, header_value);
} else {
tracing::warn!(header = %name, "dropping invalid extra header on sub-request");
}
}
}
pub(super) fn subrequest_uri(rewritten_path: Option<&String>, current: &http::Uri) -> Result<http::Uri, FilterError> {
rewritten_path.map_or_else(
|| Ok(current.clone()),
|path| {
Some(path.as_str())
.filter(|candidate| candidate.starts_with('/') && !candidate.starts_with("//"))
.and_then(|candidate| http::Uri::try_from(candidate).ok())
.filter(|uri| uri.scheme().is_none() && uri.authority().is_none())
.filter(|uri| !has_dot_dot_traversal(uri.path()))
.ok_or_else(|| -> FilterError {
format!("filtered_subrequest: invalid rewritten sub-request path: {path:?}").into()
})
},
)
}
pub(super) fn apply_pre_read_header_mutations(headers: &mut HeaderMap, ctx: &HttpFilterContext<'_>) {
if ctx.pre_read_mutations.is_empty() {
apply_request_header_mutations(headers, ctx);
return;
}
for mutation in &ctx.pre_read_mutations {
match mutation {
crate::TrustedHeaderMutation::Remove(name) => {
headers.remove(name);
},
crate::TrustedHeaderMutation::Set(name, value) => {
headers.insert(name.clone(), value.clone());
},
crate::TrustedHeaderMutation::Add(name, value) => {
if let Ok(value) = http::HeaderValue::from_str(value) {
headers.append(name.clone(), value);
}
},
}
}
}
pub(super) fn strip_request_framing_headers(headers: &mut HeaderMap) {
headers.remove(http::header::CONTENT_LENGTH);
headers.remove(http::header::TRANSFER_ENCODING);
}
pub(super) fn sanitize_subrequest_headers(headers: &mut HeaderMap) {
strip_hop_by_hop_headers(headers, HOP_BY_HOP_HEADERS);
strip_reserved_headers(headers);
strip_request_framing_headers(headers);
}
pub(super) fn ensure_destination_host(headers: &mut HeaderMap, address: &str) -> Result<(), FilterError> {
if !headers.contains_key(http::header::HOST) {
let value = http::HeaderValue::from_str(address).map_err(|error| -> FilterError {
format!("iterative_request_router: invalid upstream Host: {error}").into()
})?;
headers.insert(http::header::HOST, value);
}
Ok(())
}
pub(super) fn set_authority_host(headers: &mut HeaderMap, authority: &str) -> Result<(), FilterError> {
let value = http::HeaderValue::from_str(authority).map_err(|error| -> FilterError {
format!("filtered_subrequest: invalid upstream authority Host: {error}").into()
})?;
headers.insert(http::header::HOST, value);
Ok(())
}
pub(super) fn sanitize_subresponse_headers(headers: &mut HeaderMap) {
strip_hop_by_hop_headers(headers, RESPONSE_HOP_BY_HOP_HEADERS);
strip_reserved_headers(headers);
}
fn strip_hop_by_hop_headers(headers: &mut HeaderMap, static_headers: &[&str]) {
let connection_values: Vec<_> = headers.get_all(http::header::CONNECTION).iter().cloned().collect();
for name in static_headers {
headers.remove(*name);
}
for value in &connection_values {
for token in praxis_core::reserved_headers::connection_tokens(value) {
if praxis_core::reserved_headers::is_connection_token_protected(token) {
continue;
}
headers.remove(token);
}
}
}
pub(super) fn body_exceeds_limit(mode: crate::body::BodyMode, body_len: usize) -> bool {
match mode {
crate::body::BodyMode::SizeLimit { max_bytes }
| crate::body::BodyMode::StreamBuffer {
max_bytes: Some(max_bytes),
} => body_len > max_bytes,
crate::body::BodyMode::Stream | crate::body::BodyMode::StreamBuffer { max_bytes: None } => false,
}
}
pub(super) fn response_body_overflow_limit(
mode: crate::body::BodyMode,
max_response_bytes: usize,
body_len: usize,
) -> Option<usize> {
let mode_limit = match mode {
crate::body::BodyMode::SizeLimit { max_bytes }
| crate::body::BodyMode::StreamBuffer {
max_bytes: Some(max_bytes),
} => Some(max_bytes),
crate::body::BodyMode::Stream | crate::body::BodyMode::StreamBuffer { max_bytes: None } => None,
};
let effective = mode_limit.map_or(max_response_bytes, |limit| limit.min(max_response_bytes));
(body_len > effective).then_some(effective)
}
pub(super) fn streaming_transport_limit(mode: crate::body::BodyMode) -> Option<usize> {
match mode {
crate::body::BodyMode::SizeLimit { max_bytes } => Some(max_bytes),
crate::body::BodyMode::Stream | crate::body::BodyMode::StreamBuffer { .. } => None,
}
}
pub(crate) fn normalize_response_status(status: u16) -> u16 {
if (200..=599).contains(&status) { status } else { 502 }
}
pub(crate) fn subresponse_from_rejection(rejection: Rejection) -> SubResponse {
let status = normalize_response_status(rejection.status);
let mut headers = HeaderMap::new();
for (name, value) in rejection.headers {
let Ok(name) = http::HeaderName::try_from(name) else {
continue;
};
let Ok(value) = http::HeaderValue::try_from(value) else {
continue;
};
headers.append(name, value);
}
if let Some(header_map) = rejection.header_map {
for (name, value) in header_map.iter() {
headers.append(name.clone(), value.clone());
}
}
let mut response = SubResponse {
status,
headers,
body: rejection.body.unwrap_or_default(),
};
sanitize_subresponse_headers(&mut response.headers);
response
}