v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,369 @@
|
||||
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,
|
||||
}
|
||||
|
||||
fn parse_range(value: &str, length: u64) -> Option<ByteRange> {
|
||||
let value = value.strip_prefix("bytes=")?.trim();
|
||||
if value.contains(',') || value.is_empty() || length == 0 {
|
||||
return None;
|
||||
}
|
||||
let (start, end) = value.split_once('-')?;
|
||||
if start.is_empty() {
|
||||
let suffix: u64 = end.parse().ok()?;
|
||||
if suffix == 0 {
|
||||
return None;
|
||||
}
|
||||
let size = suffix.min(length);
|
||||
return Some(ByteRange {
|
||||
start: length - size,
|
||||
end: length - 1,
|
||||
});
|
||||
}
|
||||
let start: u64 = start.parse().ok()?;
|
||||
if start >= length {
|
||||
return None;
|
||||
}
|
||||
let end = if end.is_empty() {
|
||||
length - 1
|
||||
} else {
|
||||
end.parse::<u64>().ok()?.min(length - 1)
|
||||
};
|
||||
(end >= start).then_some(ByteRange { start, end })
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user