Files
RustProxy/src/web_server/response/file.rs
T
2026-10-06 11:56:10 +02:00

212 lines
11 KiB
Rust

pub(super) async fn file_response(
service: &CompiledService,
opened: Opened,
path: &Path,
method: &Method,
headers: &HeaderMap,
content_encoding: Option<&'static str>,
) -> Response {
let length = opened.metadata.len();
let (etag, modified) = metadata_validators(&opened.metadata);
let mime = content_type(service, path);
if not_modified(headers, etag.as_deref(), modified) {
let mut response = Response::new(Body::empty());
*response.status_mut() = StatusCode::NOT_MODIFIED;
add_validators(&mut response, etag.as_deref(), modified);
add_cache_control(&mut response, service, path, &mime);
add_download_header(&mut response, service, path);
if let Some(encoding) = content_encoding {
response
.headers_mut()
.insert(CONTENT_ENCODING, HeaderValue::from_static(encoding));
response
.headers_mut()
.insert(VARY, HeaderValue::from_static("Accept-Encoding"));
}
response
.headers_mut()
.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
return response;
}
let range_header = headers.get(RANGE).and_then(|value| value.to_str().ok());
let range = if range_header.is_some() && if_range_allows(headers, etag.as_deref(), modified) {
match range_header.and_then(|value| parse_range(value, length)) {
Some(range) => Some(range),
None => {
let mut response = Response::new(Body::empty());
*response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
response.headers_mut().insert(
CONTENT_RANGE,
HeaderValue::from_str(&format!("bytes */{length}")).unwrap(),
);
response
.headers_mut()
.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
response
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
add_validators(&mut response, etag.as_deref(), modified);
add_cache_control(&mut response, service, path, &mime);
add_download_header(&mut response, service, path);
if let Some(encoding) = content_encoding {
response
.headers_mut()
.insert(CONTENT_ENCODING, HeaderValue::from_static(encoding));
response
.headers_mut()
.insert(VARY, HeaderValue::from_static("Accept-Encoding"));
}
return response;
}
}
} else {
None
};
let (status, start, body_length) = match range {
Some(range) => (
StatusCode::PARTIAL_CONTENT,
range.start,
range.end - range.start + 1,
),
None => (StatusCode::OK, 0, length),
};
let mut response = Response::new(Body::empty());
*response.status_mut() = status;
response.headers_mut().insert(CONTENT_TYPE, mime.clone());
response.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(&body_length.to_string()).unwrap(),
);
response
.headers_mut()
.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
if let Some(range) = range {
response.headers_mut().insert(
CONTENT_RANGE,
HeaderValue::from_str(&format!("bytes {}-{}/{length}", range.start, range.end))
.unwrap(),
);
}
add_validators(&mut response, etag.as_deref(), modified);
add_cache_control(&mut response, service, path, &mime);
add_download_header(&mut response, service, path);
if let Some(encoding) = content_encoding {
response
.headers_mut()
.insert(CONTENT_ENCODING, HeaderValue::from_static(encoding));
response
.headers_mut()
.insert(VARY, HeaderValue::from_static("Accept-Encoding"));
}
if method == Method::HEAD || body_length == 0 {
return response;
}
let mut file = tokio::fs::File::from_std(opened.file);
if start != 0 {
if file.seek(std::io::SeekFrom::Start(start)).await.is_err() {
return status_response(
StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: Web Server seek failed\n",
);
}
}
let stream = ReaderStream::new(file.take(body_length));
*response.body_mut() = Body::from_stream(stream);
response
}
fn percent_encode_segment(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.as_bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
out.push(*byte as char);
} else {
use std::fmt::Write;
let _ = write!(&mut out, "%{byte:02X}");
}
}
out
}
fn html_escape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
'\'' => out.push_str("&#39;"),
_ => out.push(ch),
}
}
out
}
pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u8> {
let title = html_escape(path);
let mut html = String::with_capacity(4096 + entries.len() * 180);
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Index of "#);
html.push_str(&title);
html.push_str(r#" · RustProxy</title><style>
:root{color-scheme:dark;--bg:#131615;--panel:#1b1f1d;--panel2:#202622;--line:#323a34;--muted:#a0aaa0;--text:#edf0e9;--accent:#f69b74;--green:#bdd293;--row:#171b19;--mono:ui-monospace,SFMono-Regular,Consolas,monospace;--sans:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 var(--sans)}a{color:inherit}.shell{width:min(1040px,calc(100% - 32px));margin:0 auto;padding:34px 0}.brand{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:18px}.brand-main{display:flex;align-items:center;gap:10px}.brand-name{font:700 24px var(--mono);letter-spacing:-1px}.pill{border:1px solid #576847;color:var(--green);padding:3px 7px;font:9px var(--mono);letter-spacing:.08em}.path{max-width:60%;overflow-wrap:anywhere;color:var(--muted);font:11px/1.45 var(--mono)}.panel{border:1px solid var(--line);background:var(--panel);padding:18px}.heading{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;padding-bottom:14px;border-bottom:1px solid var(--line)}.eyebrow{display:block;color:var(--muted);font:9px var(--mono);letter-spacing:.12em}.heading h1{margin:4px 0 0;font:600 20px/1.25 var(--sans);overflow-wrap:anywhere}.count{color:var(--muted);font:10px var(--mono);white-space:nowrap}.listing{display:grid;margin-top:10px;border:1px solid var(--line)}.entry{display:grid;grid-template-columns:52px minmax(0,1fr) 92px;gap:12px;align-items:center;min-height:44px;padding:8px 12px;text-decoration:none;background:var(--row);border-bottom:1px solid var(--line)}.entry:last-child{border-bottom:0}.entry:hover{background:var(--panel2)}.entry .type{color:var(--green);font:9px var(--mono);letter-spacing:.08em}.entry.file .type{color:var(--accent)}.entry .name{overflow-wrap:anywhere;font:12px/1.45 var(--mono)}.entry .kind{text-align:right;color:var(--muted);font:10px var(--mono)}.parent .name{color:var(--green)}.page-footer{display:flex;justify-content:space-between;gap:15px;padding:24px 0 0;margin-top:24px;border-top:1px solid var(--line);font:9px var(--mono);color:var(--muted);letter-spacing:.5px}.page-footer b{color:var(--accent);font-weight:400;margin-left:9px}
@media(prefers-color-scheme:light){:root{color-scheme:light;--bg:#f4f6f2;--panel:#fff;--panel2:#edf1eb;--line:#cbd3c7;--muted:#5e685f;--text:#1d241f;--accent:#b85b38;--green:#5f783f;--row:#f9faf8}}
@media(max-width:620px){.shell{width:min(100% - 20px,1040px);padding:20px 0}.brand,.heading,.page-footer{align-items:flex-start;flex-direction:column}.path{max-width:100%}.entry{grid-template-columns:44px minmax(0,1fr)}.entry .kind{display:none}.panel{padding:12px}}
</style></head><body><main class="shell"><header class="brand"><div class="brand-main"><span class="brand-name">RustProxy</span><span class="pill">WEB SERVER</span></div><span class="path">"#);
html.push_str(&title);
html.push_str(r#"</span></header><section class="panel"><div class="heading"><div><span class="eyebrow">DIRECTORY LISTING</span><h1>Index of "#);
html.push_str(&title);
html.push_str("</h1></div><span class=\"count\">");
html.push_str(&format!("{} entries", entries.len()));
html.push_str("</span></div><div class=\"listing\">");
if path != "/" {
html.push_str(r#"<a class="entry parent" href="../"><span class="type">UP</span><span class="name">../</span><span class="kind">Parent</span></a>"#);
}
for entry in entries {
let label = html_escape(&entry.name);
let mut href = percent_encode_segment(&entry.name);
if entry.directory {
href.push('/');
}
let href = html_escape(&href);
let slash = if entry.directory { "/" } else { "" };
let class = if entry.directory {
"entry directory"
} else {
"entry file"
};
let kind = if entry.directory { "Directory" } else { "File" };
let type_label = if entry.directory { "DIR" } else { "FILE" };
html.push_str(&format!(
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
));
}
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.5</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.into_bytes()
}
pub(super) fn directory_redirect(uri: &Uri, method: &Method) -> Response {
let mut target = uri.path().to_owned();
if !target.ends_with('/') {
target.push('/');
}
if let Some(query) = uri.query() {
target.push('?');
target.push_str(query);
}
let mut response = Response::new(Body::empty());
*response.status_mut() = if *method == Method::GET || *method == Method::HEAD {
StatusCode::MOVED_PERMANENTLY
} else {
StatusCode::PERMANENT_REDIRECT
};
if let Ok(value) = HeaderValue::from_str(&target) {
response.headers_mut().insert(LOCATION, value);
}
response
}