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("

Error

"); 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 { 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::().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, Option) { 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 { 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) -> 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, ()> { 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::().map_err(|_| ())?.min(length - 1) }; Ok((end >= start).then_some(ByteRange { start, end })) } fn parse_ranges(value: &str, length: u64) -> Option> { 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) -> 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, ) { 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); } } }