Compare commits

..

No commits in common. "0a469ae367319b3fafefbf13136e0ad5c32dc953" and "95c867eb38d1587b8dcd2d24a25767bcc249a916" have entirely different histories.

14 changed files with 53 additions and 101 deletions

2
Cargo.lock generated
View File

@ -424,7 +424,7 @@ dependencies = [
[[package]] [[package]]
name = "datatrash" name = "datatrash"
version = "2.2.0" version = "2.1.1"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-governor", "actix-governor",

View File

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

View File

@ -2,7 +2,7 @@
A file and text uploading service with configurable time limit A file and text uploading service with configurable time limit
![Application screenshot showing the web form for file or text upload](./screenshot.png) ![Application screenshot](./screenshot.png)
## running ## running
@ -28,7 +28,6 @@ To run the software directly, use the compiling instructions below.
| RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for | | RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for |
| RATE_LIMIT_REPLENISH_SECONDS | 60 | seconds to wait between requests | | RATE_LIMIT_REPLENISH_SECONDS | 60 | seconds to wait between requests |
| RATE_LIMIT_BURST | 480 | allowed request burst | | RATE_LIMIT_BURST | 480 | allowed request burst |
| ABUSE_MAIL | | email address to report abuse to |
### Database configuration ### Database configuration

View File

@ -1,6 +0,0 @@
<a
href="mailto:{abusemail}?subject=Inhaltsmeldung%20datatrash&body=Dies%20ist%20eine%20Inhaltsmeldung%20f%C3%BCr%20eine%20Datei%20auf%20deiner%20datatrash-Instanz.%0ADie%20illegale%20Datei%20ist%20aktuell%20unter%20folgender%20URL%20abrufbar%3A%0A%0A{url}"
>
inhalt melden
</a>

View File

@ -16,7 +16,6 @@ pub struct Config {
pub proxied: bool, pub proxied: bool,
pub rate_limit_replenish_seconds: u64, pub rate_limit_replenish_seconds: u64,
pub rate_limit_burst: u32, pub rate_limit_burst: u32,
pub abuse_mail: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -54,7 +53,6 @@ pub async fn from_env() -> Config {
.ok() .ok()
.and_then(|burst| burst.parse().ok()) .and_then(|burst| burst.parse().ok())
.unwrap_or(480); .unwrap_or(480);
let abuse_mail = env::var("ABUSE_MAIL").ok();
Config { Config {
static_dir, static_dir,
@ -65,7 +63,6 @@ pub async fn from_env() -> Config {
proxied, proxied,
rate_limit_replenish_seconds, rate_limit_replenish_seconds,
rate_limit_burst, rate_limit_burst,
abuse_mail,
} }
} }

View File

@ -15,8 +15,12 @@ use sqlx::postgres::PgPool;
use std::path::Path; use std::path::Path;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::fs; use tokio::fs;
use url::Url;
use crate::{config::Config, deleter, mime_relations, template}; use crate::{config::Config, deleter, mime_relations};
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB
@ -39,7 +43,7 @@ pub async fn download(
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, &file_name, path, mime, &req),
ViewType::Download => build_file_response(true, &file_name, path, mime, &req), ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
ViewType::Html => build_html_response(&path, &config, &req).await, ViewType::Html => build_html_response(&path).await,
}?; }?;
insert_cache_headers(&mut response, valid_till); insert_cache_headers(&mut response, valid_till);
@ -117,19 +121,23 @@ async fn get_file_size(file_path: &Path) -> u64 {
.unwrap_or(0) .unwrap_or(0)
} }
async fn build_html_response( async fn build_html_response(path: &Path) -> Result<HttpResponse, Error> {
path: &Path,
config: &Config,
req: &HttpRequest,
) -> Result<HttpResponse, Error> {
let content = fs::read_to_string(path).await.map_err(|file_err| { let content = fs::read_to_string(path).await.map_err(|file_err| {
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 encoded = htmlescape::encode_minimal(&content);
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
let attribute_encoded = htmlescape::encode_attribute(&content);
URL_VIEW_HTML
.replace("{link_content}", &encoded)
.replace("{link_attribute}", &attribute_encoded)
} else {
TEXT_VIEW_HTML.replace("{text}", &encoded)
};
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type(TEXT_HTML.to_string()) .content_type(TEXT_HTML.to_string())
.body(html_view)) .body(html))
} }
fn build_file_response( fn build_file_response(

View File

@ -1,9 +1,8 @@
use std::{cmp, io::ErrorKind, str::FromStr}; use std::{cmp, io::ErrorKind};
use actix_web::HttpRequest; use actix_web::HttpRequest;
use time::Duration; use time::Duration;
use tokio::fs; use tokio::fs;
use url::Url;
use crate::config::Config; use crate::config::Config;
@ -12,53 +11,6 @@ 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 ABUSE_SNIPPET_HTML: &str = include_str!("../snippet/abuse.html.snippet");
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
pub fn build_uploaded_html(
req: &HttpRequest,
id: &str,
name: Option<&str>,
config: &Config,
) -> String {
let upload_html = if name.is_some() {
UPLOAD_SHORT_HTML
.replace("{link}", &get_file_url(req, id, name))
.replace("{shortlink}", &get_file_url(req, id, None))
} else {
UPLOAD_HTML.replace("{link}", &get_file_url(req, id, name))
};
insert_abuse_template(upload_html, None, config)
}
pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
let host = get_host_url(req);
if let Some(name) = name {
let encoded_name = urlencoding::encode(name);
format!("{host}/{id}/{encoded_name}")
} else {
format!("{host}/{id}")
}
}
pub fn build_html_view_template(content: &str, req: &HttpRequest, config: &Config) -> String {
let encoded = htmlescape::encode_minimal(content);
let html = if !content.trim().contains(['\n', '\r']) && Url::from_str(content.trim()).is_ok() {
let attribute_encoded = htmlescape::encode_attribute(content);
URL_VIEW_HTML
.replace("{link_content}", &encoded)
.replace("{link_attribute}", &attribute_encoded)
} else {
TEXT_VIEW_HTML.replace("{text}", &encoded)
};
insert_abuse_template(html, Some(req), config)
}
pub async fn write_prefillable_templates(config: &Config) { pub async fn write_prefillable_templates(config: &Config) {
let index_path = config.static_dir.join("index.html"); let index_path = config.static_dir.join("index.html");
fs::write(index_path, build_index_html(config)) fs::write(index_path, build_index_html(config))
@ -95,7 +47,6 @@ fn build_index_html(config: &Config) -> String {
} else { } else {
html = html.replace("{auth_snippet}", ""); html = html.replace("{auth_snippet}", "");
} }
html = insert_abuse_template(html, None, config);
if let Some(max_file_size) = config.max_file_size { if let Some(max_file_size) = config.max_file_size {
html = html html = html
.replace("{max_size_snippet}", MAX_SIZE_SNIPPET_HTML.trim_end()) .replace("{max_size_snippet}", MAX_SIZE_SNIPPET_HTML.trim_end())
@ -106,22 +57,6 @@ fn build_index_html(config: &Config) -> String {
html html
} }
pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &Config) -> String {
if let Some(abuse_mail) = &config.abuse_mail {
let url = if let Some(req) = req {
format!("{host}{path}", host = get_host_url(req), path = req.uri())
} else {
String::new()
};
// urlencoding::encode(data)
html.replace("{abuse}", ABUSE_SNIPPET_HTML.trim_end())
.replace("{abusemail}", abuse_mail)
.replace("{url}", &urlencoding::encode(&url))
} else {
html.replace("{abuse}", "")
}
}
fn render_file_size(size: u64) -> String { fn render_file_size(size: u64) -> String {
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5); let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize]; let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];

View File

@ -13,6 +13,9 @@ use std::path::PathBuf;
use tokio::fs::{self, OpenOptions}; use tokio::fs::{self, OpenOptions};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
const ID_CHARS: &[char] = &[ const ID_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
@ -60,7 +63,7 @@ pub async fn upload(
expiry_watch_sender.send(()).await.unwrap(); expiry_watch_sender.send(()).await.unwrap();
let redirect = get_redirect_url(&file_id, upload_config.original_name.as_deref()); let redirect = get_redirect_url(&file_id, upload_config.original_name.as_deref());
let url = template::get_file_url(&req, &file_id, upload_config.original_name.as_deref()); let url = get_file_url(&req, &file_id, upload_config.original_name.as_deref());
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.insert_header((LOCATION, redirect)) .insert_header((LOCATION, redirect))
.body(format!("{url}\n"))) .body(format!("{url}\n")))
@ -122,6 +125,16 @@ fn gen_file_id() -> String {
.collect() .collect()
} }
fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
let host = template::get_host_url(req);
if let Some(name) = name {
let encoded_name = urlencoding::encode(name);
format!("{host}/{id}/{encoded_name}")
} else {
format!("{host}/{id}")
}
}
fn get_redirect_url(id: &str, name: Option<&str>) -> String { fn get_redirect_url(id: &str, name: Option<&str>) -> String {
if let Some(name) = name { if let Some(name) = name {
let encoded_name = urlencoding::encode(name); let encoded_name = urlencoding::encode(name);
@ -131,7 +144,7 @@ fn get_redirect_url(id: &str, name: Option<&str>) -> String {
} }
} }
pub async fn uploaded(req: HttpRequest, config: web::Data<Config>) -> Result<HttpResponse, Error> { pub async fn uploaded(req: HttpRequest) -> Result<HttpResponse, Error> {
let id = req.match_info().query("id"); let id = req.match_info().query("id");
let name = req let name = req
.match_info() .match_info()
@ -139,8 +152,14 @@ pub async fn uploaded(req: HttpRequest, config: web::Data<Config>) -> Result<Htt
.map(urlencoding::decode) .map(urlencoding::decode)
.transpose() .transpose()
.map_err(|_| error::ErrorBadRequest("name is invalid utf-8"))?; .map_err(|_| error::ErrorBadRequest("name is invalid utf-8"))?;
let uploaded_html = template::build_uploaded_html(&req, id, name.as_deref(), &config); let upload_html = if name.is_some() {
UPLOAD_SHORT_HTML
.replace("{link}", &get_file_url(&req, id, name.as_deref()))
.replace("{shortlink}", &get_file_url(&req, id, None))
} else {
UPLOAD_HTML.replace("{link}", &get_file_url(&req, id, name.as_deref()))
};
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html") .content_type("text/html")
.body(uploaded_html)) .body(upload_html))
} }

View File

@ -107,7 +107,7 @@ h1 + textarea {
display: none; display: none;
} }
input[type="checkbox"] { input[type='checkbox'] {
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
@ -167,16 +167,16 @@ footer {
padding: 0.5em; padding: 0.5em;
} }
footer > * { .repo {
padding: 0.5em; padding: 0.5em;
color: var(--fg-light); color: var(--fg-light);
text-decoration: none; text-decoration: none;
} }
footer > *:visited { .repo:visited {
color: var(--fg-light); color: var(--fg-light);
} }
footer > *:hover { .repo:hover {
color: var(--fg); color: var(--fg);
} }

View File

@ -59,8 +59,8 @@ authentifizieren
</details> </details>
</main> </main>
<footer> <footer>
{abuse}
<a <a
class="repo"
href="https://git.ctdo.de/neri/datatrash" href="https://git.ctdo.de/neri/datatrash"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -18,8 +18,8 @@
<a class="button" href="?raw">roh anzeigen</a> <a class="button" href="?raw">roh anzeigen</a>
</main> </main>
<footer> <footer>
{abuse}
<a <a
class="repo"
href="https://git.ctdo.de/neri/datatrash" href="https://git.ctdo.de/neri/datatrash"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -19,8 +19,8 @@
</button> </button>
</main> </main>
<footer> <footer>
{abuse}
<a <a
class="repo"
href="https://git.ctdo.de/neri/datatrash" href="https://git.ctdo.de/neri/datatrash"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -15,8 +15,8 @@
</button> </button>
</main> </main>
<footer> <footer>
{abuse}
<a <a
class="repo"
href="https://git.ctdo.de/neri/datatrash" href="https://git.ctdo.de/neri/datatrash"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -27,8 +27,8 @@
<a class="button" href="?raw">roh anzeigen</a> <a class="button" href="?raw">roh anzeigen</a>
</main> </main>
<footer> <footer>
{abuse}
<a <a
class="repo"
href="https://git.ctdo.de/neri/datatrash" href="https://git.ctdo.de/neri/datatrash"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"