Compare commits
4 Commits
4d9880701d
...
215bca866f
Author | SHA1 | Date |
---|---|---|
neri | 215bca866f | |
neri | 48fa99002a | |
neri | 40fba9992a | |
neri | 701c86f64c |
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "datatrash"
|
name = "datatrash"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
authors = ["neri"]
|
authors = ["neri"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,4 @@ ALTER TABLE files ADD COLUMN IF NOT EXISTS content_type varchar(255) not null
|
||||||
GENERATED ALWAYS AS (CASE WHEN kind = 'text' THEN 'text/plain' ELSE 'application/octet-stream' END) STORED;
|
GENERATED ALWAYS AS (CASE WHEN kind = 'text' THEN 'text/plain' ELSE 'application/octet-stream' END) STORED;
|
||||||
ALTER TABLE files ALTER COLUMN content_type DROP EXPRESSION IF EXISTS;
|
ALTER TABLE files ALTER COLUMN content_type DROP EXPRESSION IF EXISTS;
|
||||||
ALTER TABLE files DROP COLUMN IF EXISTS kind;
|
ALTER TABLE files DROP COLUMN IF EXISTS kind;
|
||||||
|
ALTER TABLE files ALTER COLUMN file_name DROP NOT NULL;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<h2>{file_name}</h2>
|
|
@ -36,10 +36,14 @@ pub async fn download(
|
||||||
let path = config.files_dir.join(&file_id);
|
let path = config.files_dir.join(&file_id);
|
||||||
|
|
||||||
let mime = Mime::from_str(&content_type).unwrap_or(APPLICATION_OCTET_STREAM);
|
let mime = Mime::from_str(&content_type).unwrap_or(APPLICATION_OCTET_STREAM);
|
||||||
|
let computed_file_name = file_name.clone().unwrap_or_else(|| {
|
||||||
|
let extension = mime_relations::get_extension(&mime).unwrap_or("txt");
|
||||||
|
format!("{file_id}.{extension}")
|
||||||
|
});
|
||||||
let mut response = match get_view_type(&req, &mime, &path, delete).await {
|
let mut response = match get_view_type(&req, &mime, &path, delete).await {
|
||||||
ViewType::Raw => build_file_response(false, &file_name, path, mime, &req),
|
ViewType::Raw => build_file_response(false, &computed_file_name, path, mime, &req),
|
||||||
ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
|
ViewType::Download => build_file_response(true, &computed_file_name, path, mime, &req),
|
||||||
ViewType::Html => build_html_response(&path, &config, &req).await,
|
ViewType::Html => build_html_response(file_name.as_deref(), &path, &config, &req).await,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
insert_cache_headers(&mut response, valid_till);
|
insert_cache_headers(&mut response, valid_till);
|
||||||
|
@ -59,7 +63,7 @@ pub async fn download(
|
||||||
async fn load_file_info(
|
async fn load_file_info(
|
||||||
id: &str,
|
id: &str,
|
||||||
db: &web::Data<sqlx::Pool<sqlx::Postgres>>,
|
db: &web::Data<sqlx::Pool<sqlx::Postgres>>,
|
||||||
) -> Result<(String, String, OffsetDateTime, String, bool), Error> {
|
) -> Result<(String, Option<String>, OffsetDateTime, String, bool), Error> {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT file_id, file_name, valid_till, content_type, delete_on_download from files WHERE file_id = $1",
|
"SELECT file_id, file_name, valid_till, content_type, delete_on_download from files WHERE file_id = $1",
|
||||||
)
|
)
|
||||||
|
@ -118,6 +122,7 @@ async fn get_file_size(file_path: &Path) -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_html_response(
|
async fn build_html_response(
|
||||||
|
file_name: Option<&str>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
|
@ -126,7 +131,7 @@ async fn build_html_response(
|
||||||
log::error!("file could not be read {:?}", file_err);
|
log::error!("file could not be read {:?}", file_err);
|
||||||
error::ErrorInternalServerError("this file should be here but could not be found")
|
error::ErrorInternalServerError("this file should be here but could not be found")
|
||||||
})?;
|
})?;
|
||||||
let html_view = template::build_html_view_template(&content, req, config);
|
let html_view = template::build_html_view_template(&content, file_name, req, config);
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type(TEXT_HTML.to_string())
|
.content_type(TEXT_HTML.to_string())
|
||||||
.body(html_view))
|
.body(html_view))
|
||||||
|
@ -172,24 +177,26 @@ fn get_disposition_params(filename: &str) -> Vec<DispositionParam> {
|
||||||
parameters
|
parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ALLOWED_CONTEXTS: [&str; 6] = ["audio", "document", "empty", "font", "image", "video"];
|
||||||
|
|
||||||
fn append_security_headers(response: &mut HttpResponse, req: &HttpRequest) {
|
fn append_security_headers(response: &mut HttpResponse, req: &HttpRequest) {
|
||||||
// if the browser is trying to fetch this resource in a secure context pretend the reponse is
|
// if the browser is trying to fetch this resource in a secure context pretend the response is
|
||||||
// just binary data so it won't be executed
|
// just binary data so it won't be executed
|
||||||
let sec_fetch_mode = req
|
let sec_fetch_dest = req
|
||||||
.headers()
|
.headers()
|
||||||
.get("sec-fetch-mode")
|
.get("sec-fetch-dest")
|
||||||
.and_then(|v| v.to_str().ok());
|
.map(|v| v.to_str().unwrap_or("unknown"));
|
||||||
if sec_fetch_mode.is_some() && sec_fetch_mode != Some("navigate") {
|
if sec_fetch_dest.is_some_and(|sec_fetch_dest| !ALLOWED_CONTEXTS.contains(&sec_fetch_dest)) {
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
CONTENT_TYPE,
|
CONTENT_TYPE,
|
||||||
HeaderValue::from_str(APPLICATION_OCTET_STREAM.as_ref())
|
HeaderValue::from_str(APPLICATION_OCTET_STREAM.as_ref())
|
||||||
.expect("mime type can be encoded to header value"),
|
.expect("mime type can be encoded to header value"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// the reponse varies based on these request headers
|
// the response varies based on these request headers
|
||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.append(VARY, HeaderValue::from_static("sec-fetch-mode"));
|
.append(VARY, HeaderValue::from_static("sec-fetch-dest"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_cache_headers(response: &mut HttpResponse, valid_till: OffsetDateTime) {
|
fn insert_cache_headers(response: &mut HttpResponse, valid_till: OffsetDateTime) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use actix_web::{
|
||||||
HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY,
|
HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY,
|
||||||
X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION,
|
X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION,
|
||||||
},
|
},
|
||||||
middleware::{self, Condition, DefaultHeaders, Logger},
|
middleware::{self, Condition, DefaultHeaders},
|
||||||
web::{self, Data},
|
web::{self, Data},
|
||||||
App, Error, HttpResponse, HttpServer,
|
App, Error, HttpResponse, HttpServer,
|
||||||
};
|
};
|
||||||
|
@ -81,7 +81,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
let http_server = HttpServer::new({
|
let http_server = HttpServer::new({
|
||||||
move || {
|
move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#))
|
|
||||||
.wrap(
|
.wrap(
|
||||||
DefaultHeaders::new()
|
DefaultHeaders::new()
|
||||||
.add(DEFAULT_CONTENT_SECURITY_POLICY)
|
.add(DEFAULT_CONTENT_SECURITY_POLICY)
|
||||||
|
|
|
@ -11,6 +11,7 @@ const INDEX_HTML: &str = include_str!("../template/index.html");
|
||||||
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
||||||
const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet");
|
const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet");
|
||||||
const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet");
|
const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet");
|
||||||
|
const FILE_NAME_SNIPPET_HTML: &str = include_str!("../snippet/file_name.html.snippet");
|
||||||
|
|
||||||
const ABUSE_SNIPPET_HTML: &str = include_str!("../snippet/abuse.html.snippet");
|
const ABUSE_SNIPPET_HTML: &str = include_str!("../snippet/abuse.html.snippet");
|
||||||
|
|
||||||
|
@ -46,15 +47,26 @@ pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_html_view_template(content: &str, req: &HttpRequest, config: &Config) -> String {
|
pub fn build_html_view_template(
|
||||||
let encoded = htmlescape::encode_minimal(content);
|
content: &str,
|
||||||
|
file_name: Option<&str>,
|
||||||
|
req: &HttpRequest,
|
||||||
|
config: &Config,
|
||||||
|
) -> String {
|
||||||
|
let encoded_content = htmlescape::encode_minimal(content);
|
||||||
|
let name_snippet = file_name
|
||||||
|
.map(htmlescape::encode_minimal)
|
||||||
|
.map(|name| FILE_NAME_SNIPPET_HTML.replace("{file_name}", &name))
|
||||||
|
.unwrap_or_default();
|
||||||
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
|
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
|
||||||
let attribute_encoded = htmlescape::encode_attribute(content);
|
let attribute_encoded_content = htmlescape::encode_attribute(content);
|
||||||
URL_VIEW_HTML
|
URL_VIEW_HTML
|
||||||
.replace("{link_content}", &encoded)
|
.replace("{link_content}", &encoded_content)
|
||||||
.replace("{link_attribute}", &attribute_encoded)
|
.replace("{link_attribute}", &attribute_encoded_content)
|
||||||
} else {
|
} else {
|
||||||
TEXT_VIEW_HTML.replace("{text}", &encoded)
|
TEXT_VIEW_HTML
|
||||||
|
.replace("{file_name}", &name_snippet)
|
||||||
|
.replace("{text}", &encoded_content)
|
||||||
};
|
};
|
||||||
insert_abuse_template(html, Some(req), config)
|
insert_abuse_template(html, Some(req), config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::io::ErrorKind;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::multipart::UploadConfig;
|
use crate::multipart::UploadConfig;
|
||||||
use crate::{mime_relations, multipart, template};
|
use crate::{multipart, template};
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
use actix_web::http::header::LOCATION;
|
use actix_web::http::header::LOCATION;
|
||||||
|
@ -39,18 +39,12 @@ pub async fn upload(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let upload_config = multipart::parse_multipart(payload, &file_path, &config).await?;
|
let upload_config = multipart::parse_multipart(payload, &file_path, &config).await?;
|
||||||
let file_name = upload_config.original_name.clone().unwrap_or_else(|| {
|
let file_name = upload_config.original_name.clone();
|
||||||
format!(
|
|
||||||
"{file_id}.{}",
|
|
||||||
mime_relations::get_extension(&upload_config.content_type).unwrap_or("txt")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
insert_file_metadata(&file_id, file_name, &file_path, &upload_config, db).await?;
|
insert_file_metadata(&file_id, file_name, &file_path, &upload_config, db).await?;
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"{} create new file {} (valid_till: {}, content_type: {}, delete_on_download: {})",
|
"create new file {} (valid_till: {}, content_type: {}, delete_on_download: {})",
|
||||||
req.connection_info().realip_remote_addr().unwrap_or("-"),
|
|
||||||
file_id,
|
file_id,
|
||||||
upload_config.valid_till,
|
upload_config.valid_till,
|
||||||
upload_config.content_type,
|
upload_config.content_type,
|
||||||
|
@ -68,7 +62,7 @@ pub async fn upload(
|
||||||
|
|
||||||
async fn insert_file_metadata(
|
async fn insert_file_metadata(
|
||||||
file_id: &String,
|
file_id: &String,
|
||||||
file_name: String,
|
file_name: Option<String>,
|
||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
upload_config: &UploadConfig,
|
upload_config: &UploadConfig,
|
||||||
db: web::Data<sqlx::Pool<sqlx::Postgres>>,
|
db: web::Data<sqlx::Pool<sqlx::Postgres>>,
|
||||||
|
@ -78,7 +72,7 @@ async fn insert_file_metadata(
|
||||||
VALUES ($1, $2, $3, $4, $5)",
|
VALUES ($1, $2, $3, $4, $5)",
|
||||||
)
|
)
|
||||||
.bind(file_id)
|
.bind(file_id)
|
||||||
.bind(&file_name)
|
.bind(file_name)
|
||||||
.bind(&upload_config.content_type.to_string())
|
.bind(&upload_config.content_type.to_string())
|
||||||
.bind(upload_config.valid_till)
|
.bind(upload_config.valid_till)
|
||||||
.bind(upload_config.delete_on_download)
|
.bind(upload_config.delete_on_download)
|
||||||
|
|
|
@ -108,11 +108,11 @@ a:focus-within {
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 + textarea {
|
form > textarea {
|
||||||
height: 60vh;
|
height: 30vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<h1>
|
<h1>
|
||||||
<a href="/">datatrash<img src="/static/favicon.svg" class="icon" /></a>
|
<a href="/">datatrash<img src="/static/favicon.svg" class="icon" /></a>
|
||||||
</h1>
|
</h1>
|
||||||
|
{file_name}
|
||||||
<textarea id="text" rows="20" cols="120" readonly>{text}</textarea>
|
<textarea id="text" rows="20" cols="120" readonly>{text}</textarea>
|
||||||
<br />
|
<br />
|
||||||
<a class="main button" href="?dl">herunterladen</a>
|
<a class="main button" href="?dl">herunterladen</a>
|
||||||
|
|
Loading…
Reference in New Issue