Krolyxon
Tag v0.1.0

ferreniumsrchandlersraw.rs

102 lines · 3.4 KB · Rust

1//! `/{repo}/raw/{rev}/{path…}` — file contents as stored.
2//!
3//! Nothing served here may run in our origin: text (HTML and SVG included)
4//! is `text/plain`, only raster images keep their type, and every response
5//! is sandboxed and `nosniff`. `?download` sends an attachment instead.
6
7use axum::body::Body;
8use axum::http::{HeaderValue, StatusCode, header};
9use axum::response::{IntoResponse, Response};
10use gix::bstr::ByteSlice;
11
12use super::repo::{Ctx, set_cache_headers};
13use crate::git::tree::{self, PathTarget};
14use crate::render::encode_path;
15
16pub const RAW_CSP: &str = "default-src 'none'; img-src 'self'; sandbox";
17
18/// Image types safe to serve with their real content type.
19const IMAGE_TYPES: &[(&str, &str)] = &[
20 ("png", "image/png"),
21 ("jpg", "image/jpeg"),
22 ("jpeg", "image/jpeg"),
23 ("gif", "image/gif"),
24 ("webp", "image/webp"),
25 ("avif", "image/avif"),
26 ("bmp", "image/bmp"),
27 ("ico", "image/x-icon"),
28];
29
30pub fn serve(ctx: &Ctx, segments: &[Vec<u8>]) -> Response {
31 let (rev, path) = match ctx.resolve(segments) {
32 Ok(found) => found,
33 Err(res) => return *res,
34 };
35 let oid = match tree::lookup(&ctx.git, rev.commit, &path) {
36 Ok(Some(PathTarget::Blob(oid, _))) => oid,
37 Ok(Some(_)) => return ctx.not_found("NOT A FILE"),
38 Ok(None) => return ctx.not_found(&format!("PATH NOT FOUND IN {}", rev.name)),
39 Err(e) => return ctx.internal_error(e),
40 };
41 let download = ctx.query.download.is_some();
42 let etag = format!("W/\"{oid}{}\"", if download { "-d" } else { "" });
43 let immutable = rev.is_immutable();
44 if let Some(res) = ctx.not_modified(&etag, immutable) {
45 return res;
46 }
47 let data = match tree::blob(&ctx.git, oid) {
48 Ok(data) => data,
49 Err(e) => return ctx.internal_error(e),
50 };
51
52 let name = path
53 .rsplit_once_str("/")
54 .map_or(&path[..], |(_, n)| n)
55 .to_str_lossy()
56 .into_owned();
57 let ext = name
58 .rsplit_once('.')
59 .map(|(_, e)| e.to_ascii_lowercase())
60 .unwrap_or_default();
61 let is_text = !data[..data.len().min(8000)].contains(&0);
62 let content_type = match IMAGE_TYPES.iter().find(|(e, _)| *e == ext) {
63 Some((_, mime)) => mime,
64 None if download => "application/octet-stream",
65 None if is_text => "text/plain; charset=utf-8",
66 None => "application/octet-stream",
67 };
68
69 let mut res = Body::from(data).into_response();
70 *res.status_mut() = StatusCode::OK;
71 let headers = res.headers_mut();
72 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
73 headers.insert(
74 header::CONTENT_SECURITY_POLICY,
75 HeaderValue::from_static(RAW_CSP),
76 );
77 headers.insert(
78 header::X_CONTENT_TYPE_OPTIONS,
79 HeaderValue::from_static("nosniff"),
80 );
81 if download {
82 let ascii: String = name
83 .chars()
84 .map(|c| {
85 if c.is_ascii_graphic() && c != '"' && c != '\\' {
86 c
87 } else {
88 '_'
89 }
90 })
91 .collect();
92 let value = format!(
93 "attachment; filename=\"{ascii}\"; filename*=UTF-8''{}",
94 encode_path(&name)
95 );
96 if let Ok(value) = HeaderValue::from_str(&value) {
97 headers.insert(header::CONTENT_DISPOSITION, value);
98 }
99 }
100 set_cache_headers(&mut res, &etag, immutable);
101 res
102}