Files
RustProxy/src/web_server/response/headers.rs
T
2026-10-08 22:22:46 +02:00

389 lines
13 KiB
Rust

use super::{
config::CompiledCacheMatcher,
filesystem::{DirectoryEntry, Opened},
htaccess,
CompiledService,
};
use axum::{body::Body, response::Response};
use http::{
header::{
ACCEPT_RANGES, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING,
CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH,
IF_RANGE, LAST_MODIFIED, LOCATION, RANGE, VARY,
},
HeaderMap, HeaderValue, Method, StatusCode, Uri,
};
use std::{
io,
path::Path,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio_util::io::ReaderStream;
pub(super) fn status_response(status: StatusCode, message: &'static str) -> Response {
let mut response = Response::new(Body::from(message));
*response.status_mut() = status;
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
response
}
pub(super) fn builtin_error_response(status: StatusCode, method: &Method) -> Response {
let html = crate::pages::builtin_gateway_error(status)
.unwrap_or("<!doctype html><html><body><h1>Error</h1></body></html>");
let mut response = Response::new(if *method == Method::HEAD {
Body::empty()
} else {
Body::from(html)
});
*response.status_mut() = status;
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
);
if let Ok(length) = HeaderValue::from_str(&html.len().to_string()) {
response.headers_mut().insert(CONTENT_LENGTH, length);
}
response
.headers_mut()
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
response.headers_mut().insert(
"x-content-type-options",
HeaderValue::from_static("nosniff"),
);
response
}
pub(super) fn io_response(error: &io::Error) -> Response {
#[cfg(unix)]
if matches!(error.raw_os_error(), Some(libc::ELOOP)) {
return status_response(StatusCode::FORBIDDEN, "RustProxy: Web Server path is not allowed\n");
}
match error.kind() {
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory => {
status_response(StatusCode::NOT_FOUND, "RustProxy: Web Server resource not found\n")
}
io::ErrorKind::PermissionDenied | io::ErrorKind::InvalidInput => {
status_response(StatusCode::FORBIDDEN, "RustProxy: Web Server path is not allowed\n")
}
_ => status_response(
StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: Web Server filesystem error\n",
),
}
}
fn builtin_content_type(path: &Path) -> Option<&'static str> {
let ext = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase();
Some(match ext.as_str() {
"html" | "htm" => "text/html; charset=utf-8",
"css" => "text/css; charset=utf-8",
"js" | "mjs" | "cjs" => "text/javascript; charset=utf-8",
"txt" | "log" | "md" => "text/plain; charset=utf-8",
"csv" => "text/csv; charset=utf-8",
"tsv" => "text/tab-separated-values; charset=utf-8",
"json" | "map" => "application/json; charset=utf-8",
"yaml" | "yml" => "application/yaml; charset=utf-8",
"xml" => "application/xml; charset=utf-8",
"rss" => "application/rss+xml; charset=utf-8",
"atom" => "application/atom+xml; charset=utf-8",
"ics" => "text/calendar; charset=utf-8",
"vcf" => "text/vcard; charset=utf-8",
"pdf" => "application/pdf",
"zip" => "application/zip",
"7z" => "application/x-7z-compressed",
"rar" => "application/vnd.rar",
"gz" => "application/gzip",
"bz2" => "application/x-bzip2",
"xz" => "application/x-xz",
"tar" => "application/x-tar",
"wasm" => "application/wasm",
"webmanifest" => "application/manifest+json; charset=utf-8",
"svg" => "image/svg+xml",
"png" => "image/png",
"apng" => "image/apng",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"avif" => "image/avif",
"heic" => "image/heic",
"heif" => "image/heif",
"jxl" => "image/jxl",
"ico" => "image/x-icon",
"bmp" => "image/bmp",
"mp3" => "audio/mpeg",
"m4a" => "audio/mp4",
"ogg" | "oga" => "audio/ogg",
"wav" => "audio/wav",
"flac" => "audio/flac",
"mp4" | "m4v" => "video/mp4",
"mov" => "video/quicktime",
"mpeg" | "mpg" => "video/mpeg",
"avi" => "video/x-msvideo",
"webm" => "video/webm",
"ogv" => "video/ogg",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"otf" => "font/otf",
"eot" => "application/vnd.ms-fontobject",
"doc" => "application/msword",
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xls" => "application/vnd.ms-excel",
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ppt" => "application/vnd.ms-powerpoint",
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
_ => return None,
})
}
fn content_type(service: &CompiledService, path: &Path) -> HeaderValue {
if let Some(value) = htaccess::mime_type(service.htaccess.as_ref(), path) {
return value;
}
let extension = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase();
if let Some(value) = service.mime_types.get(&extension) {
return value.clone();
}
if let Some(value) = builtin_content_type(path) {
return HeaderValue::from_static(value);
}
service.default_mime_type.clone()
}
fn cache_control_for(
service: &CompiledService,
path: &Path,
content_type: &HeaderValue,
) -> Option<HeaderValue> {
let extension = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let mime = content_type
.to_str()
.unwrap_or("")
.to_ascii_lowercase();
for rule in &service.cache_rules {
let matches = match &rule.matcher {
CompiledCacheMatcher::Extension(value) => extension == *value,
CompiledCacheMatcher::MimePrefix(value) => mime.starts_with(value),
};
if matches {
return Some(rule.cache_control.clone());
}
}
service.cache_control.clone()
}
fn add_cache_control(
response: &mut Response,
service: &CompiledService,
path: &Path,
content_type: &HeaderValue,
) {
if let Some(value) = cache_control_for(service, path, content_type) {
response.headers_mut().insert(CACHE_CONTROL, value);
}
}
pub(super) fn add_service_headers(response: &mut Response, service: &CompiledService) {
for (name, value) in &service.response_headers {
response.headers_mut().insert(name.clone(), value.clone());
}
}
fn download_extension(service: &CompiledService, path: &Path) -> bool {
path.extension()
.and_then(|value| value.to_str())
.map(|value| value.to_ascii_lowercase())
.is_some_and(|value| service.download_extensions.contains(&value))
}
fn add_download_header(response: &mut Response, service: &CompiledService, path: &Path) {
if !download_extension(service, path) {
return;
}
let filename = path.file_name().and_then(|value| value.to_str()).unwrap_or("download");
let value = format!("attachment; filename*=UTF-8''{}", percent_encode_segment(filename));
if let Ok(value) = HeaderValue::from_str(&value) {
response.headers_mut().insert(CONTENT_DISPOSITION, value);
}
}
pub(super) fn accepts_content_encoding(headers: &HeaderMap, wanted: &str) -> bool {
let Some(value) = headers
.get(http::header::ACCEPT_ENCODING)
.and_then(|value| value.to_str().ok())
else {
return false;
};
let mut wildcard = None;
for item in value.split(',') {
let mut parts = item.trim().split(';');
let encoding = parts.next().unwrap_or("").trim();
let mut q = 1.0f32;
for parameter in parts {
if let Some(value) = parameter.trim().strip_prefix("q=") {
q = value.parse::<f32>().unwrap_or(0.0);
}
}
if encoding.eq_ignore_ascii_case(wanted) {
return q > 0.0;
}
if encoding == "*" {
wildcard = Some(q > 0.0);
}
}
wildcard.unwrap_or(false)
}
fn metadata_validators(metadata: &std::fs::Metadata) -> (Option<String>, Option<SystemTime>) {
let modified = metadata.modified().ok();
let etag = modified.and_then(|modified| {
modified.duration_since(UNIX_EPOCH).ok().map(|stamp| {
format!(
"\"{:x}-{:x}-{:x}\"",
metadata.len(),
stamp.as_secs(),
stamp.subsec_nanos()
)
})
});
(etag, modified)
}
fn weak_etag_value(value: &str) -> &str {
value.trim().strip_prefix("W/").unwrap_or(value.trim())
}
fn if_none_match_matches(value: &HeaderValue, etag: &str) -> bool {
value.to_str().ok().is_some_and(|value| {
value.split(',').any(|candidate| {
let candidate = candidate.trim();
candidate == "*" || weak_etag_value(candidate) == weak_etag_value(etag)
})
})
}
fn second_precision(value: SystemTime) -> Option<SystemTime> {
value
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| UNIX_EPOCH + Duration::from_secs(duration.as_secs()))
}
fn not_modified(headers: &HeaderMap, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
if let (Some(etag), Some(value)) = (etag, headers.get(IF_NONE_MATCH)) {
return if_none_match_matches(value, etag);
}
if headers.contains_key(IF_NONE_MATCH) {
return false;
}
let Some(modified) = modified.and_then(second_precision) else {
return false;
};
headers
.get(IF_MODIFIED_SINCE)
.and_then(|value| value.to_str().ok())
.and_then(|value| httpdate::parse_http_date(value).ok())
.is_some_and(|since| modified <= since)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ByteRange {
start: u64,
end: u64,
}
const MAX_BYTE_RANGES: usize = 16;
fn parse_range_item(value: &str, length: u64) -> Result<Option<ByteRange>, ()> {
if length == 0 {
return Ok(None);
}
let (start, end) = value.split_once('-').ok_or(())?;
if start.is_empty() {
let suffix: u64 = end.parse().map_err(|_| ())?;
if suffix == 0 {
return Ok(None);
}
let size = suffix.min(length);
return Ok(Some(ByteRange {
start: length - size,
end: length - 1,
}));
}
let start: u64 = start.parse().map_err(|_| ())?;
if start >= length {
return Ok(None);
}
let end = if end.is_empty() {
length - 1
} else {
end.parse::<u64>().map_err(|_| ())?.min(length - 1)
};
Ok((end >= start).then_some(ByteRange { start, end }))
}
fn parse_ranges(value: &str, length: u64) -> Option<Vec<ByteRange>> {
let value = value.strip_prefix("bytes=")?.trim();
if value.is_empty() {
return None;
}
let mut ranges = Vec::new();
for (index, item) in value.split(',').enumerate() {
if index >= MAX_BYTE_RANGES {
return None;
}
match parse_range_item(item.trim(), length).ok()? {
Some(range) => ranges.push(range),
None => continue,
}
}
(!ranges.is_empty()).then_some(ranges)
}
fn if_range_allows(headers: &HeaderMap, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
let Some(value) = headers.get(IF_RANGE).and_then(|value| value.to_str().ok()) else {
return true;
};
let value = value.trim();
if value.starts_with('"') || value.starts_with("W/") {
return etag.is_some_and(|etag| !value.starts_with("W/") && value == etag);
}
let Some(modified) = modified.and_then(second_precision) else {
return false;
};
httpdate::parse_http_date(value)
.ok()
.is_some_and(|date| modified <= date)
}
fn add_validators(
response: &mut Response,
etag: Option<&str>,
modified: Option<SystemTime>,
) {
if let Some(etag) = etag.and_then(|value| HeaderValue::from_str(value).ok()) {
response.headers_mut().insert(ETAG, etag);
}
if let Some(modified) = modified {
if let Ok(value) = HeaderValue::from_str(&httpdate::fmt_http_date(modified)) {
response.headers_mut().insert(LAST_MODIFIED, value);
}
}
}