212 lines
11 KiB
Rust
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("&"),
|
|
'<' => out.push_str("<"),
|
|
'>' => out.push_str(">"),
|
|
'"' => out.push_str("""),
|
|
'\'' => out.push_str("'"),
|
|
_ => 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
|
|
}
|