show limits explicitly, improve code

This commit is contained in:
neri 2021-08-18 23:22:50 +02:00
parent 1c7930d0c6
commit 34247604db
12 changed files with 558 additions and 477 deletions

739
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ edition = "2018"
[dependencies] [dependencies]
actix-web = "3.3.2" actix-web = "3.3.2"
sqlx = { version = "0.5.1", default-features = false, features = [ "runtime-async-std-rustls", "postgres", "chrono" ] } sqlx = { version = "0.5.1", default-features = false, features = [ "runtime-async-std-rustls", "postgres", "chrono" ] }
env_logger = "0.8.3" env_logger = "0.9.0"
log = "0.4.14" log = "0.4.14"
actix-files = "0.5.0" actix-files = "0.5.0"
async-std = "1.9.0" async-std = "1.9.0"
@ -18,6 +18,6 @@ futures = "0.3.13"
rand = "0.8.3" rand = "0.8.3"
chrono = "0.4.19" chrono = "0.4.19"
htmlescape = "0.3.1" htmlescape = "0.3.1"
urlencoding = "1.1.1" urlencoding = "2.1.0"
tree_magic_mini = "1.0.1" tree_magic_mini = "3.0.0"
mime = "0.3.16" mime = "0.3.16"

View File

@ -0,0 +1,9 @@
<div id="password-input">
<label for="password">
passwort benötigt (&gt;{auth_time} oder datei &gt;{auth_large_size} und &gt;{auth_large_time})
</label>
<br />
<input id="password" name="password" type="password" />
</div>
<script src="/assets/auth-hide.js" lang="javascript"></script>

View File

@ -0,0 +1 @@
(maximal {max_size})

View File

@ -1,6 +1,7 @@
use std::env; use std::env;
use async_std::{fs, path::PathBuf}; use async_std::{fs, path::PathBuf};
use chrono::Duration;
#[derive(Clone)] #[derive(Clone)]
pub struct Config { pub struct Config {
@ -12,8 +13,8 @@ pub struct Config {
#[derive(Clone)] #[derive(Clone)]
pub struct NoAuthLimits { pub struct NoAuthLimits {
pub auth_password: String, pub auth_password: String,
pub max_time: u64, pub max_time: Duration,
pub large_file_max_time: u64, pub large_file_max_time: Duration,
pub large_file_size: u64, pub large_file_size: u64,
} }
@ -29,22 +30,7 @@ pub async fn get_config() -> Config {
.await .await
.expect("could not create directory for storing files"); .expect("could not create directory for storing files");
let no_auth_limits = match ( let no_auth_limits = get_no_auth_limits();
env::var("AUTH_PASSWORD").ok(),
env_number("NO_AUTH_MAX_TIME"),
env_number("NO_AUTH_LARGE_FILE_MAX_TIME"),
env_number("NO_AUTH_LARGE_FILE_SIZE"),
) {
(Some(auth_password), Some(max_time), Some(large_file_max_time), Some(large_file_size)) => {
Some(NoAuthLimits {
auth_password,
max_time,
large_file_max_time,
large_file_size,
})
}
_ => None,
};
Config { Config {
files_dir, files_dir,
@ -53,6 +39,26 @@ pub async fn get_config() -> Config {
} }
} }
fn get_no_auth_limits() -> Option<NoAuthLimits> {
match (
env::var("AUTH_PASSWORD").ok(),
env_number("NO_AUTH_MAX_TIME"),
env_number("NO_AUTH_LARGE_FILE_MAX_TIME"),
env_number("NO_AUTH_LARGE_FILE_SIZE"),
) {
(Some(auth_password), Some(max_time), Some(large_file_max_time), Some(large_file_size)) => {
Some(NoAuthLimits {
auth_password,
max_time: Duration::seconds(max_time as i64),
large_file_max_time: Duration::seconds(large_file_max_time as i64),
large_file_size,
})
}
(None, None, None, None) => None,
_ => panic!("Incomplete NO_AUTH configuration: All environment variables must be specified")
}
}
fn env_number(variable: &str) -> Option<u64> { fn env_number(variable: &str) -> Option<u64> {
env::var(variable).ok().and_then(|n| n.parse::<u64>().ok()) env::var(variable).ok().and_then(|n| n.parse::<u64>().ok())
} }

View File

@ -7,7 +7,7 @@ mod multipart;
mod upload; mod upload;
use actix_files::Files; use actix_files::Files;
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer}; use actix_web::{App, Error, HttpResponse, HttpServer, middleware::Logger, web};
use async_std::{channel, task}; use async_std::{channel, task};
use env_logger::Env; use env_logger::Env;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
@ -29,20 +29,20 @@ async fn main() -> std::io::Result<()> {
log::info!("omnomnom"); log::info!("omnomnom");
task::spawn(deleter::delete_old_files( let db = web::Data::new(pool.clone());
receiver,
pool.clone(),
config.files_dir.clone(),
));
let db = web::Data::new(pool);
let expiry_watch_sender = web::Data::new(sender); let expiry_watch_sender = web::Data::new(sender);
let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:8000".to_owned()); let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:8000".to_owned());
task::spawn(deleter::delete_old_files(
receiver,
pool,
config.files_dir.clone(),
));
HttpServer::new({ HttpServer::new({
move || { move || {
App::new() App::new()
.wrap(middleware::Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#)) .wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#))
.app_data(db.clone()) .app_data(db.clone())
.app_data(expiry_watch_sender.clone()) .app_data(expiry_watch_sender.clone())
.data(config.clone()) .data(config.clone())

View File

@ -5,6 +5,9 @@ use async_std::{fs, fs::File, path::Path, prelude::*};
use chrono::{prelude::*, Duration}; use chrono::{prelude::*, Duration};
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
const MAX_UPLOAD_SECONDS: u64 = 31 * 24 * 60 * 60;
const DEFAULT_UPLOAD_SECONDS: u64 = 30 * 60;
pub(crate) struct UploadConfig { pub(crate) struct UploadConfig {
pub original_name: String, pub original_name: String,
pub valid_till: DateTime<Local>, pub valid_till: DateTime<Local>,
@ -77,20 +80,20 @@ pub(crate) async fn parse_multipart(
let seconds = keep_for.parse().map_err(|e| { let seconds = keep_for.parse().map_err(|e| {
error::ErrorBadRequest(format!("field keep_for is not a number: {}", e)) error::ErrorBadRequest(format!("field keep_for is not a number: {}", e))
})?; })?;
let max_keep_for = Duration::days(31).num_seconds() as u64; if seconds > MAX_UPLOAD_SECONDS {
if seconds > max_keep_for {
return Err(error::ErrorBadRequest(format!( return Err(error::ErrorBadRequest(format!(
"maximum allowed validity is {} seconds, but you specified {} seconds", "maximum allowed validity is {} seconds, but you specified {} seconds",
max_keep_for, seconds MAX_UPLOAD_SECONDS, seconds
))); )));
} }
seconds seconds
} else { } else {
1800 DEFAULT_UPLOAD_SECONDS
}; };
let valid_till = Local::now() + Duration::seconds(validated_keep_for as i64); let valid_duration = Duration::seconds(validated_keep_for as i64);
let valid_till = Local::now() + valid_duration;
check_auth_requirements(size, validated_keep_for, password, config)?; check_auth_requirements(size, valid_duration, password, config)?;
Ok(UploadConfig { Ok(UploadConfig {
original_name, original_name,
@ -102,7 +105,7 @@ pub(crate) async fn parse_multipart(
fn check_auth_requirements( fn check_auth_requirements(
size: u64, size: u64,
validated_keep_for: u64, validated_keep_for: Duration,
password: Option<String>, password: Option<String>,
config: &config::Config, config: &config::Config,
) -> Result<(), error::Error> { ) -> Result<(), error::Error> {

View File

@ -1,3 +1,5 @@
use std::{cmp, vec};
use crate::config::Config; use crate::config::Config;
use crate::file_kind::FileKind; use crate::file_kind::FileKind;
use crate::multipart; use crate::multipart;
@ -5,13 +7,15 @@ use crate::multipart::UploadConfig;
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{error, web, Error, HttpResponse}; use actix_web::{error, web, Error, HttpResponse};
use async_std::{channel::Sender, fs}; use async_std::{channel::Sender, fs};
use chrono::Duration;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
const INDEX_HTML: &str = include_str!("../template/index.html"); const INDEX_HTML: &str = include_str!("../template/index.html");
const INDEX_AUTH_HTML: &str = include_str!("../template/index-auth.html");
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js"); const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
const UPLOAD_HTML: &str = include_str!("../template/upload.html"); const UPLOAD_HTML: &str = include_str!("../template/upload.html");
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 ID_CHARS: &[char] = &[ const ID_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v',
@ -22,25 +26,102 @@ pub async fn index(
req: web::HttpRequest, req: web::HttpRequest,
config: web::Data<Config>, config: web::Data<Config>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let upload_url = format!("{}/upload", get_host_url(&req)); let filled_index_html = fill_index_html(req, config);
let index_html = if config.no_auth_limits.is_some() {
INDEX_AUTH_HTML
} else {
INDEX_HTML
};
let filled_index_html = index_html.replace("{upload_url}", upload_url.as_str());
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html") .content_type("text/html")
.body(filled_index_html)) .body(filled_index_html))
} }
fn fill_index_html(req: web::HttpRequest, config: web::Data<Config>) -> String {
let upload_url = format!("{}/upload", get_host_url(&req));
let auth_snippet = config
.no_auth_limits
.as_ref()
.map_or("", |_| AUTH_SNIPPET_HTML);
let max_size_snippet = config
.max_file_size
.as_ref()
.map_or("", |_| MAX_SIZE_SNIPPET_HTML);
INDEX_HTML
.replace("{max_size_snippet}", max_size_snippet)
.replace(
"{max_size}",
&render_file_size(config.max_file_size.unwrap_or(0)),
)
.replace("{auth_snippet}", auth_snippet)
.replace(
"{auth_time}",
&config
.no_auth_limits
.as_ref()
.map(|limit| limit.max_time)
.map_or("".into(), render_duration),
)
.replace(
"{auth_large_time}",
&config
.no_auth_limits
.as_ref()
.map(|limit| limit.large_file_max_time)
.map_or("".into(), render_duration),
)
.replace(
"{auth_large_size}",
&config
.no_auth_limits
.as_ref()
.map(|limit| limit.large_file_size)
.map_or("".into(), render_file_size),
)
.replace("{upload_url}", upload_url.as_str())
}
fn render_file_size(size: u64) -> String {
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
let value = size / (1024_u64.pow(magnitude));
format!("{}{}B", value, prefix)
}
fn render_duration(duration: Duration) -> String {
let days = duration.num_days();
let hours = duration.num_hours() % 24;
let minutes = duration.num_minutes() % 60;
let seconds = duration.num_seconds() % 60;
let mut elements = vec![];
if let Some(name) = pluralize(days, "tag", "e") {
elements.push(name);
}
if let Some(name) = pluralize(hours, "stunde", "n") {
elements.push(name);
}
if let Some(name) = pluralize(minutes, "minute", "n") {
elements.push(name);
}
if let Some(name) = pluralize(seconds, "sekunde", "n") {
elements.push(name);
}
elements.join("+")
}
fn pluralize(number: i64, word: &str, suffix: &str) -> Option<String> {
match number {
0 => None,
1 => Some(format!("{} {}", number, word)),
_ => Some(format!("{} {}{}", number, word, suffix)),
}
}
pub async fn auth_hide(config: web::Data<Config>) -> Result<HttpResponse, Error> { pub async fn auth_hide(config: web::Data<Config>) -> Result<HttpResponse, Error> {
if let Some(no_auth_limits) = &config.no_auth_limits { if let Some(no_auth_limits) = &config.no_auth_limits {
let auth_hide_js = AUTH_HIDE_JS let auth_hide_js = AUTH_HIDE_JS
.replace("{no_auth_max_time}", &no_auth_limits.max_time.to_string()) .replace(
"{no_auth_max_time}",
&no_auth_limits.max_time.num_seconds().to_string(),
)
.replace( .replace(
"{no_auth_large_file_max_time}", "{no_auth_large_file_max_time}",
&no_auth_limits.large_file_max_time.to_string(), &no_auth_limits.large_file_max_time.num_seconds().to_string(),
) )
.replace( .replace(
"{no_auth_large_file_size}", "{no_auth_large_file_size}",

View File

@ -69,6 +69,8 @@ input[type="checkbox"] {
.button { .button {
cursor: pointer; cursor: pointer;
font-family: sans;
font-weight: normal;
font-size: 1rem; font-size: 1rem;
} }

View File

@ -6,30 +6,32 @@ const passwordInput = document.getElementById("password-input");
const maxTime = Number("{no_auth_max_time}"); const maxTime = Number("{no_auth_max_time}");
const largeFileMaxTime = Number("{no_auth_large_file_max_time}"); const largeFileMaxTime = Number("{no_auth_large_file_max_time}");
const largeFileSize = Number("{no_auth_large_file_size}"); const largeFileSize = Number("{no_auth_large_file_size}");
const updatePasswordInput = () => {
const requirePassword = keep > maxTime || (size > largeFileSize && keep > largeFileMaxTime);
passwordInput.className = requirePassword ? "" : "hidden";
};
let keep = Number(keepFor.value); let keep = Number(keepFor.value);
let size = fileUpload.files[0] let size = fileUpload.files[0]
? fileUpload.files[0].size ? fileUpload.files[0].size
: textUpload.value.length; : textUpload.value.length;
const updatePasswordInput = () => {
const requirePassword = keep > maxTime || (size > largeFileSize && keep > largeFileMaxTime);
passwordInput.className = requirePassword ? "" : "hidden";
};
updatePasswordInput(); updatePasswordInput();
fileUpload.addEventListener("change", (e) => { fileUpload.addEventListener("change", () => {
size = fileUpload.files[0] size = fileUpload.files[0]
? fileUpload.files[0].size ? fileUpload.files[0].size
: textUpload.value.length; : textUpload.value.length;
updatePasswordInput(); updatePasswordInput();
}); });
textUpload.addEventListener("input", (e) => { textUpload.addEventListener("input", () => {
if (!fileUpload.files[0]) { if (!fileUpload.files[0]) {
size = textUpload.value.length; size = textUpload.value.length;
updatePasswordInput(); updatePasswordInput();
} }
}); });
keepFor.addEventListener("change", (e) => { keepFor.addEventListener("change", () => {
keep = Number(keepFor.value); keep = Number(keepFor.value);
updatePasswordInput(); updatePasswordInput();
}); });

View File

@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="de-DE">
<head>
<title>datatrash</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Temporärer Dateiaustausch" />
<link href="/static/index.css" rel="stylesheet" />
</head>
<body>
<main>
<h1>datatrash</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file-upload">datei</label>
<br />
<input id="file-upload" type="file" name="file" />
<br />
<label for="text-upload">oder asciitrash</label>
<br />
<textarea id="text-upload" name="text" rows="20" cols="120"></textarea>
<br />
<label for="keep_for">gültig für</label>
<select id="keep_for" name="keep_for">
<option value="1800">30 minuten</option>
<option value="3600">60 minuten</option>
<option value="43200">12 stunden</option>
<option value="86400">24 stunden</option>
<option value="604800">eine woche</option>
<option value="2678400">einen monat</option>
</select>
<br />
<input
id="delete_on_download"
type="checkbox"
name="delete_on_download"
/>
<label for="delete_on_download">nach einem download löschen</label>
<br />
<div id="password-input">
<label for="password">
authentifizierung für große, oder lang gültige uploads
</label>
<br />
<input id="password" name="password" type="password" />
</div>
<input class="main button" type="submit" value="hochladen" />
</form>
<details class="usage">
<summary>nutzung als api</summary>
<pre>
file upload
curl -F 'file=@yourfile.rs' {upload_url}
text upload
curl -F 'text=your text' {upload_url}
including time
curl -F 'text=your text' -F 'keep_for=1800' {upload_url}
limit to one download
curl -F 'text=your text' -F 'delete_on_download=true' {upload_url}</pre
>
</details>
</main>
<script src="/assets/auth-hide.js" lang="javascript"></script>
</body>
</html>

View File

@ -11,7 +11,7 @@
<main> <main>
<h1>datatrash</h1> <h1>datatrash</h1>
<form action="/upload" method="POST" enctype="multipart/form-data"> <form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file-upload">datei</label> <label for="file-upload">datei{max_size_snippet}</label>
<br /> <br />
<input id="file-upload" type="file" name="file" /> <input id="file-upload" type="file" name="file" />
<br /> <br />
@ -35,20 +35,26 @@
name="delete_on_download" name="delete_on_download"
/> />
<label for="delete_on_download">nach einem download löschen</label> <label for="delete_on_download">nach einem download löschen</label>
<br /> <br />{auth_snippet}
<input class="main button" type="submit" value="hochladen" /> <input class="main button" type="submit" value="hochladen" />
</form> </form>
<details class="usage"> <details class="usage">
<summary>nutzung als api</summary> <summary>nutzung als api</summary>
<pre> <pre>
file upload datei hochladen
curl -F 'file=@yourfile.rs' {upload_url} curl -F 'file=@yourfile.rs' {upload_url}
text upload
text hochladen
curl -F 'text=your text' {upload_url} curl -F 'text=your text' {upload_url}
including time
zeitbegrenzung setzen
curl -F 'text=your text' -F 'keep_for=1800' {upload_url} curl -F 'text=your text' -F 'keep_for=1800' {upload_url}
limit to one download
curl -F 'text=your text' -F 'delete_on_download=true' {upload_url}</pre nach einem download löschen
curl -F 'text=your text' -F 'delete_on_download=true' {upload_url}
authentifizieren
curl -F 'text=your text' -F 'password=…' {upload_url}</pre
> >
</details> </details>
</main> </main>