use crate::core::New;
use crate::header::Header;
use crate::request::Request;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use super::router::prefix_matches;
use super::types::*;
pub fn modify_headers(headers: &mut Vec<Header>, filter: &HttpHeaderFilter) {
for name in &filter.remove {
headers.retain(|h| !h.name.eq_ignore_ascii_case(name));
}
for header in &filter.set {
headers.retain(|h| !h.name.eq_ignore_ascii_case(&header.name));
headers.push(Header::new(header.name.clone(), header.value.clone()));
}
for header in &filter.add {
headers.push(Header::new(header.name.clone(), header.value.clone()));
}
}
pub fn modified_path(path: &str, matched: &HttpRouteMatch, modifier: &HttpPathModifier) -> String {
match modifier.modifier_type.as_str() {
"ReplaceFullPath" => modifier.replace_full_path.clone().unwrap_or_else(|| "/".to_string()),
"ReplacePrefixMatch" => {
let prefix = matched.path.as_ref().and_then(|p| p.value.as_deref()).unwrap_or("/");
if !prefix_matches(prefix, path) {
return path.to_string();
}
let rest = &path[prefix.trim_end_matches('/').len()..];
let replacement = modifier.replace_prefix_match.as_deref().unwrap_or("/").trim_end_matches('/');
let joined = format!("{replacement}{rest}");
match joined.as_str() {
"" => "/".to_string(),
j if !j.starts_with('/') => format!("/{j}"),
_ => joined,
}
}
_ => path.to_string(),
}
}
pub fn split_target(uri: &str) -> (&str, &str) {
match uri.find('?') {
Some(i) => (&uri[..i], &uri[i..]),
None => (uri, ""),
}
}
fn default_port(scheme: &str) -> u16 {
if scheme.eq_ignore_ascii_case("https") { 443 } else { 80 }
}
pub fn redirect(request: &Request, host: &str, listener_https: bool, listener_port: u16, matched: &HttpRouteMatch, filter: &HttpRequestRedirectFilter) -> Response {
let scheme = filter.scheme.clone().unwrap_or_else(|| if listener_https { "https" } else { "http" }.to_string());
let hostname = filter.hostname.clone().unwrap_or_else(|| host.to_string());
let port = match (filter.port, &filter.scheme) {
(Some(port), _) => port as u16,
(None, Some(scheme)) => default_port(scheme),
(None, None) => listener_port,
};
let (path, query) = split_target(&request.request_uri);
let path = filter.path.as_ref().map(|m| modified_path(path, matched, m)).unwrap_or_else(|| path.to_string());
let authority = if port == default_port(&scheme) { hostname } else { format!("{hostname}:{port}") };
let code = match filter.status_code.unwrap_or(302) {
301 => STATUS_CODE_REASON_PHRASE.n301_moved_permanently,
303 => STATUS_CODE_REASON_PHRASE.n303_see_other,
307 => STATUS_CODE_REASON_PHRASE.n307_temporary_redirect,
308 => STATUS_CODE_REASON_PHRASE.n308_permanent_redirect,
_ => STATUS_CODE_REASON_PHRASE.n302_found,
};
let mut response = status(code);
response.headers.push(Header::new("Location", format!("{scheme}://{authority}{path}{query}")));
response
}
pub(crate) fn status(code: &'static crate::response::StatusCodeReasonPhrase) -> Response {
let mut response = Response::new();
response.status_code = *code.status_code;
response.reason_phrase = code.reason_phrase.to_string();
response
}
fn origin_allowed(pattern: &str, origin: &str) -> bool {
if pattern == "*" || pattern.eq_ignore_ascii_case(origin) {
return true;
}
match pattern.split_once('*') {
Some((before, after)) => {
origin.len() > before.len() + after.len()
&& origin[..before.len()].eq_ignore_ascii_case(before)
&& origin[origin.len() - after.len()..].eq_ignore_ascii_case(after)
}
None => false,
}
}
#[derive(Debug, PartialEq)]
pub enum Cors {
Pass,
Preflight(Vec<Header>),
Actual(Vec<Header>),
}
pub fn cors(request: &Request, filter: &HttpCorsFilter) -> Cors {
let Some(origin) = request.header("origin").map(|h| h.value.to_string()) else { return Cors::Pass };
let preflight = request.method == "OPTIONS" && request.header("access-control-request-method").is_some();
let allowed = filter.allow_origins.iter().any(|p| origin_allowed(p, &origin));
if !allowed {
return if preflight { Cors::Preflight(Vec::new()) } else { Cors::Pass };
}
let credentials = filter.allow_credentials == Some(true);
let wildcard = !credentials && filter.allow_origins.iter().any(|p| p == "*");
let mut headers = vec![Header::new("Access-Control-Allow-Origin", if wildcard { "*".to_string() } else { origin })];
if !wildcard {
headers.push(Header::new("Vary", "Origin"));
}
if credentials {
headers.push(Header::new("Access-Control-Allow-Credentials", "true"));
}
if !preflight {
if !filter.expose_headers.is_empty() {
headers.push(Header::new("Access-Control-Expose-Headers", filter.expose_headers.join(", ")));
}
return Cors::Actual(headers);
}
let requested = |name: &str| request.header(name).map(|h| h.value.to_string()).unwrap_or_default();
let methods = if filter.allow_methods.iter().any(|m| m == "*") {
if credentials { requested("access-control-request-method") } else { "*".to_string() }
} else {
filter.allow_methods.join(", ")
};
if !methods.is_empty() {
headers.push(Header::new("Access-Control-Allow-Methods", methods));
}
let allow_headers = if filter.allow_headers.iter().any(|h| h == "*") {
if credentials { requested("access-control-request-headers") } else { "*".to_string() }
} else {
filter.allow_headers.join(", ")
};
if !allow_headers.is_empty() {
headers.push(Header::new("Access-Control-Allow-Headers", allow_headers));
}
if !filter.expose_headers.is_empty() {
headers.push(Header::new("Access-Control-Expose-Headers", filter.expose_headers.join(", ")));
}
let max_age = if filter.max_age > 0 { filter.max_age } else { 5 };
headers.push(Header::new("Access-Control-Max-Age", max_age.to_string()));
Cors::Preflight(headers)
}
pub fn duration(value: &str) -> Option<std::time::Duration> {
let mut total = 0u64;
let mut rest = value;
let mut parts = 0;
while !rest.is_empty() {
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
if digits == 0 || digits > 5 {
return None;
}
let n: u64 = rest[..digits].parse().ok()?;
rest = &rest[digits..];
let (millis, unit_len) = if rest.starts_with("ms") {
(1, 2)
} else {
match rest.as_bytes().first()? {
b'h' => (3_600_000, 1),
b'm' => (60_000, 1),
b's' => (1_000, 1),
_ => return None,
}
};
total += n * millis;
rest = &rest[unit_len..];
parts += 1;
}
(1..=4).contains(&parts).then(|| std::time::Duration::from_millis(total))
}
#[cfg(test)]
mod tests;