Compare commits

..

1 Commits

Author SHA1 Message Date
neri 4d9880701d feat: rate limit ipv6 addresses based on the first /56 2023-11-16 14:51:31 +01:00
10 changed files with 455 additions and 515 deletions

885
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "datatrash" name = "datatrash"
version = "2.5.0" version = "2.4.1"
authors = ["neri"] authors = ["neri"]
edition = "2021" edition = "2021"

View File

@ -14,4 +14,3 @@ 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;

View File

@ -1 +0,0 @@
<h2>{file_name}</h2>

View File

@ -36,14 +36,10 @@ 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, &computed_file_name, path, mime, &req), ViewType::Raw => build_file_response(false, &file_name, path, mime, &req),
ViewType::Download => build_file_response(true, &computed_file_name, path, mime, &req), ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
ViewType::Html => build_html_response(file_name.as_deref(), &path, &config, &req).await, ViewType::Html => build_html_response(&path, &config, &req).await,
}?; }?;
insert_cache_headers(&mut response, valid_till); insert_cache_headers(&mut response, valid_till);
@ -63,7 +59,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, Option<String>, OffsetDateTime, String, bool), Error> { ) -> Result<(String, 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",
) )
@ -122,7 +118,6 @@ 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,
@ -131,7 +126,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, file_name, req, config); let html_view = template::build_html_view_template(&content, 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))
@ -177,26 +172,24 @@ 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 response is // if the browser is trying to fetch this resource in a secure context pretend the reponse is
// just binary data so it won't be executed // just binary data so it won't be executed
let sec_fetch_dest = req let sec_fetch_mode = req
.headers() .headers()
.get("sec-fetch-dest") .get("sec-fetch-mode")
.map(|v| v.to_str().unwrap_or("unknown")); .and_then(|v| v.to_str().ok());
if sec_fetch_dest.is_some_and(|sec_fetch_dest| !ALLOWED_CONTEXTS.contains(&sec_fetch_dest)) { if sec_fetch_mode.is_some() && sec_fetch_mode != Some("navigate") {
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 response varies based on these request headers // the reponse varies based on these request headers
response response
.headers_mut() .headers_mut()
.append(VARY, HeaderValue::from_static("sec-fetch-dest")); .append(VARY, HeaderValue::from_static("sec-fetch-mode"));
} }
fn insert_cache_headers(response: &mut HttpResponse, valid_till: OffsetDateTime) { fn insert_cache_headers(response: &mut HttpResponse, valid_till: OffsetDateTime) {

View File

@ -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}, middleware::{self, Condition, DefaultHeaders, Logger},
web::{self, Data}, web::{self, Data},
App, Error, HttpResponse, HttpServer, App, Error, HttpResponse, HttpServer,
}; };
@ -81,6 +81,7 @@ 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)

View File

@ -11,7 +11,6 @@ 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");
@ -47,26 +46,15 @@ pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
} }
} }
pub fn build_html_view_template( pub fn build_html_view_template(content: &str, req: &HttpRequest, config: &Config) -> String {
content: &str, let encoded = htmlescape::encode_minimal(content);
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_content = htmlescape::encode_attribute(content); let attribute_encoded = htmlescape::encode_attribute(content);
URL_VIEW_HTML URL_VIEW_HTML
.replace("{link_content}", &encoded_content) .replace("{link_content}", &encoded)
.replace("{link_attribute}", &attribute_encoded_content) .replace("{link_attribute}", &attribute_encoded)
} else { } else {
TEXT_VIEW_HTML TEXT_VIEW_HTML.replace("{text}", &encoded)
.replace("{file_name}", &name_snippet)
.replace("{text}", &encoded_content)
}; };
insert_abuse_template(html, Some(req), config) insert_abuse_template(html, Some(req), config)
} }

View File

@ -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::{multipart, template}; use crate::{mime_relations, 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,12 +39,18 @@ 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(); let file_name = upload_config.original_name.clone().unwrap_or_else(|| {
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,
@ -62,7 +68,7 @@ pub async fn upload(
async fn insert_file_metadata( async fn insert_file_metadata(
file_id: &String, file_id: &String,
file_name: Option<String>, file_name: 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>>,
@ -72,7 +78,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)

View File

@ -108,11 +108,11 @@ a:focus-within {
textarea { textarea {
width: 100%; width: 100%;
height: 60vh; height: 30vh;
} }
form > textarea { h1 + textarea {
height: 30vh; height: 60vh;
} }
.hidden { .hidden {

View File

@ -17,7 +17,6 @@
<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>