Compare commits
3 Commits
1527445857
...
c9a3af3756
Author | SHA1 | Date |
---|---|---|
neri | c9a3af3756 | |
neri | f80035ac82 | |
neri | e4ff237905 |
|
@ -42,6 +42,18 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-governor"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69a09c3caabdac53c829ad01be8b0251428ec3eb87521367d4b900befd820056"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"futures",
|
||||
"governor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.0.4"
|
||||
|
@ -413,15 +425,29 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3495912c9c1ccf2e18976439f4443f3fee0fd61f424ff99fde6a66b15ecb448f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.12.3",
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "datatrash"
|
||||
version = "1.1.2"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-governor",
|
||||
"actix-multipart",
|
||||
"actix-web",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"governor",
|
||||
"htmlescape",
|
||||
"log",
|
||||
"mime",
|
||||
|
@ -564,6 +590,21 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.21"
|
||||
|
@ -580,6 +621,17 @@ version = "0.3.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-intrusive"
|
||||
version = "0.4.0"
|
||||
|
@ -591,6 +643,12 @@ dependencies = [
|
|||
"parking_lot 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.21"
|
||||
|
@ -614,16 +672,25 @@ version = "0.3.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
|
@ -650,6 +717,23 @@ dependencies = [
|
|||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"no-std-compat",
|
||||
"nonzero_ext",
|
||||
"parking_lot 0.12.0",
|
||||
"quanta",
|
||||
"rand",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.13"
|
||||
|
@ -678,13 +762,19 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -788,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -888,6 +978,15 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
|
@ -963,6 +1062,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
|
@ -973,6 +1078,12 @@ dependencies = [
|
|||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.7"
|
||||
|
@ -1034,7 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.2",
|
||||
"parking_lot_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1053,9 +1164,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
|
||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
|
@ -1119,6 +1230,22 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"mach",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
|
@ -1158,6 +1285,15 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "10.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aa2540135b6a94f74c7bc90ad4b794f822026a894f3d7bcd185c100d13d4ad6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
|
@ -2034,9 +2170,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
|
@ -2047,33 +2183,33 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.34.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
|
|
|
@ -30,3 +30,5 @@ urlencoding = "2.1.0"
|
|||
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
|
||||
mime = "0.3.16"
|
||||
url = "2.2.2"
|
||||
actix-governor = "0.3.1"
|
||||
governor = "0.4.2"
|
||||
|
|
16
README.md
16
README.md
|
@ -18,12 +18,16 @@ To run the software directly, use the compiling instructions below.
|
|||
|
||||
### General configuration
|
||||
|
||||
| environment variable | default value |
|
||||
| -------------------- | -------------- |
|
||||
| STATIC_DIR | ./static |
|
||||
| FILES_DIR | ./files |
|
||||
| UPLOAD_MAX_BYTES | 8388608 (8MiB) |
|
||||
| BIND_ADDRESS | 0.0.0.0:8000 |
|
||||
| environment variable | default value | description |
|
||||
| --------------------- | -------------- | ---------------------------------------------- |
|
||||
| STATIC_DIR | ./static | directory to generate "static" files into |
|
||||
| FILES_DIR | ./files | directory to save uploaded files into |
|
||||
| UPLOAD_MAX_BYTES | 8388608 (8MiB) | maximum size for uploaded files |
|
||||
| BIND_ADDRESS | 0.0.0.0:8000 | address to bind the server to |
|
||||
| RATE_LIMIT | true | whether download rate should be limited |
|
||||
| RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for |
|
||||
| RATE_LIMIT_PER_SECOND | 60 | seconds to wait between requests |
|
||||
| RATE_LIMIT_BURST | 1440 | allowed request burst |
|
||||
|
||||
### Database configuration
|
||||
|
||||
|
|
|
@ -10,3 +10,5 @@ CREATE TABLE IF NOT EXISTS files (
|
|||
ALTER TABLE files ADD COLUMN IF NOT EXISTS delete_on_download boolean;
|
||||
ALTER TABLE files ALTER COLUMN delete_on_download set not null;
|
||||
ALTER TABLE files ALTER COLUMN valid_till TYPE timestamptz;
|
||||
ALTER TABLE files DROP COLUMN IF EXISTS kind;
|
||||
ALTER TABLE files ADD COLUMN IF NOT EXISTS content_type varchar(255);
|
||||
|
|
|
@ -10,6 +10,10 @@ pub struct Config {
|
|||
pub files_dir: PathBuf,
|
||||
pub max_file_size: Option<u64>,
|
||||
pub no_auth_limits: Option<NoAuthLimits>,
|
||||
pub enable_rate_limit: bool,
|
||||
pub proxied: bool,
|
||||
pub rate_limit_per_second: u64,
|
||||
pub rate_limit_burst: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -36,11 +40,26 @@ pub async fn get_config() -> Config {
|
|||
|
||||
let no_auth_limits = get_no_auth_limits();
|
||||
|
||||
let enable_rate_limit = matches!(env::var("RATE_LIMIT").as_deref(), Ok("true") | Err(_));
|
||||
let proxied = env::var("PROXIED").as_deref() == Ok("true");
|
||||
let rate_limit_per_second = env::var("RATE_LIMIT_PER_SECOND")
|
||||
.ok()
|
||||
.and_then(|rate_limit| rate_limit.parse().ok())
|
||||
.unwrap_or(60);
|
||||
let rate_limit_burst = env::var("RATE_LIMIT_BURST")
|
||||
.ok()
|
||||
.and_then(|rate_limit| rate_limit.parse().ok())
|
||||
.unwrap_or(1440);
|
||||
|
||||
Config {
|
||||
static_dir,
|
||||
files_dir,
|
||||
max_file_size,
|
||||
no_auth_limits,
|
||||
enable_rate_limit,
|
||||
proxied,
|
||||
rate_limit_per_second,
|
||||
rate_limit_burst,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use futures_util::TryStreamExt;
|
||||
use time::OffsetDateTime;
|
||||
use sqlx::{postgres::PgPool, Row};
|
||||
use std::cmp::max;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::ext::NumericalStdDuration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::fs;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::time::timeout;
|
||||
|
|
|
@ -5,18 +5,18 @@ use actix_web::{
|
|||
error,
|
||||
http::header::{
|
||||
Accept, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
Header,
|
||||
Header, HeaderValue, CONTENT_TYPE, VARY,
|
||||
},
|
||||
web, Error, HttpRequest, HttpResponse,
|
||||
};
|
||||
use mime::{Mime, TEXT_HTML};
|
||||
use mime::{Mime, APPLICATION_OCTET_STREAM, TEXT_HTML};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::deleter;
|
||||
use crate::{config::Config, file_kind::FileKind};
|
||||
|
||||
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
|
||||
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
|
||||
|
@ -35,14 +35,14 @@ pub async fn download(
|
|||
config: web::Data<Config>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let id = req.match_info().query("id");
|
||||
let (file_id, file_name, file_kind, delete) = load_file_info(id, &db).await?;
|
||||
let (file_id, file_name, content_type, delete) = load_file_info(id, &db).await?;
|
||||
let mut path = config.files_dir.clone();
|
||||
path.push(&file_id);
|
||||
|
||||
let file_mime = get_content_type(&path);
|
||||
let response = match get_view_type(&req, &file_kind, &file_mime, &path, delete).await {
|
||||
ViewType::Raw => build_file_response(false, &file_name, path, file_mime, &req),
|
||||
ViewType::Download => build_file_response(true, &file_name, path, file_mime, &req),
|
||||
let mime = Mime::from_str(&content_type).unwrap_or(APPLICATION_OCTET_STREAM);
|
||||
let response = match get_view_type(&req, &mime, &path, delete).await {
|
||||
ViewType::Raw => build_file_response(false, &file_name, path, mime, &req),
|
||||
ViewType::Download => build_file_response(true, &file_name, path, mime, &req),
|
||||
ViewType::Html => build_text_response(&path).await,
|
||||
};
|
||||
if delete {
|
||||
|
@ -61,7 +61,7 @@ async fn load_file_info(
|
|||
db: &web::Data<sqlx::Pool<sqlx::Postgres>>,
|
||||
) -> Result<(String, String, String, bool), Error> {
|
||||
sqlx::query_as(
|
||||
"SELECT file_id, file_name, kind, delete_on_download from files WHERE file_id = $1",
|
||||
"SELECT file_id, file_name, content_type, delete_on_download from files WHERE file_id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(db.as_ref())
|
||||
|
@ -73,18 +73,9 @@ async fn load_file_info(
|
|||
.ok_or_else(|| error::ErrorNotFound("file does not exist or has expired"))
|
||||
}
|
||||
|
||||
fn get_content_type(path: &Path) -> Mime {
|
||||
let std_path = std::path::Path::new(path.as_os_str());
|
||||
tree_magic_mini::from_filepath(std_path)
|
||||
.unwrap_or("application/octet-stream")
|
||||
.parse::<Mime>()
|
||||
.expect("tree_magic_mini should not produce invalid mime")
|
||||
}
|
||||
|
||||
async fn get_view_type(
|
||||
req: &HttpRequest,
|
||||
file_kind: &str,
|
||||
file_mime: &Mime,
|
||||
mime: &Mime,
|
||||
file_path: &Path,
|
||||
delete_on_download: bool,
|
||||
) -> ViewType {
|
||||
|
@ -94,9 +85,7 @@ async fn get_view_type(
|
|||
if req.query_string().contains("raw") {
|
||||
return ViewType::Raw;
|
||||
}
|
||||
let is_text =
|
||||
FileKind::from_str(file_kind) == Ok(FileKind::Text) || file_mime.type_() == mime::TEXT;
|
||||
if !is_text {
|
||||
if mime.type_() != mime::TEXT {
|
||||
return ViewType::Raw;
|
||||
}
|
||||
if get_file_size(file_path).await >= TEXT_VIEW_SIZE_LIMIT {
|
||||
|
@ -107,7 +96,7 @@ async fn get_view_type(
|
|||
if accept_mime == TEXT_HTML {
|
||||
return ViewType::Html;
|
||||
}
|
||||
if mime_matches(&accept_mime, file_mime) {
|
||||
if mime_matches(&accept_mime, mime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -151,7 +140,7 @@ fn build_file_response(
|
|||
download: bool,
|
||||
file_name: &str,
|
||||
path: PathBuf,
|
||||
content_type: Mime,
|
||||
mime: Mime,
|
||||
req: &HttpRequest,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let content_disposition = ContentDisposition {
|
||||
|
@ -167,9 +156,31 @@ fn build_file_response(
|
|||
log::error!("file could not be read {:?}", file_err);
|
||||
error::ErrorInternalServerError("this file should be here but could not be found")
|
||||
})?
|
||||
.set_content_type(content_type)
|
||||
.set_content_type(mime)
|
||||
.set_content_disposition(content_disposition);
|
||||
Ok(file.into_response(req))
|
||||
let mut response = file.into_response(req);
|
||||
add_headers(req, download, &mut response);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn add_headers(req: &HttpRequest, download: bool, response: &mut HttpResponse) {
|
||||
// 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
|
||||
let sec_fetch_mode = req
|
||||
.headers()
|
||||
.get("sec-fetch-mode")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
if !download && sec_fetch_mode.is_some() && sec_fetch_mode != Some("navigate") {
|
||||
response.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(APPLICATION_OCTET_STREAM.as_ref())
|
||||
.expect("mime type can be encoded to header value"),
|
||||
);
|
||||
}
|
||||
// the reponse varies based on these request headers
|
||||
response
|
||||
.headers_mut()
|
||||
.append(VARY, HeaderValue::from_static("accept, sec-fetch-mode"));
|
||||
}
|
||||
|
||||
fn get_disposition_params(filename: &str) -> Vec<DispositionParam> {
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FileKind {
|
||||
Text,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Display for FileKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FileKind::Text => write!(f, "text"),
|
||||
FileKind::Binary => write!(f, "binary"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileKind {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"text" => Ok(FileKind::Text),
|
||||
"binary" => Ok(FileKind::Binary),
|
||||
_ => Err(format!("unknown kind {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
45
src/main.rs
45
src/main.rs
|
@ -2,14 +2,17 @@ mod config;
|
|||
mod db;
|
||||
mod deleter;
|
||||
mod download;
|
||||
mod file_kind;
|
||||
mod multipart;
|
||||
mod rate_limit;
|
||||
mod template;
|
||||
mod upload;
|
||||
|
||||
use crate::rate_limit::ForwardedPeerIpKeyExtractor;
|
||||
use actix_files::Files;
|
||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
||||
use actix_web::{
|
||||
middleware::{self, Logger},
|
||||
http::header::{HeaderName, HeaderValue, CONTENT_SECURITY_POLICY, X_CONTENT_TYPE_OPTIONS},
|
||||
middleware::{self, DefaultHeaders, Logger},
|
||||
web::{self, Data},
|
||||
App, Error, HttpResponse, HttpServer,
|
||||
};
|
||||
|
@ -18,6 +21,11 @@ use sqlx::postgres::PgPool;
|
|||
use std::env;
|
||||
use tokio::{sync::mpsc::channel, task};
|
||||
|
||||
const DEFAULT_CSP: (HeaderName, &str) = (
|
||||
CONTENT_SECURITY_POLICY,
|
||||
"default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self';"
|
||||
);
|
||||
|
||||
async fn not_found() -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/plain")
|
||||
|
@ -47,10 +55,25 @@ async fn main() -> std::io::Result<()> {
|
|||
template::write_prefillable_templates(&config).await;
|
||||
let config = Data::new(config);
|
||||
|
||||
let governor_conf = GovernorConfigBuilder::default()
|
||||
.per_second(config.rate_limit_per_second)
|
||||
.burst_size(config.rate_limit_burst)
|
||||
.key_extractor(ForwardedPeerIpKeyExtractor {
|
||||
proxied: config.proxied,
|
||||
})
|
||||
.use_headers()
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
HttpServer::new({
|
||||
move || {
|
||||
App::new()
|
||||
let app = App::new()
|
||||
.wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#))
|
||||
.wrap(
|
||||
DefaultHeaders::new()
|
||||
.add(DEFAULT_CSP)
|
||||
.add((X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"))),
|
||||
)
|
||||
.wrap(middleware::Compress::default())
|
||||
.app_data(db.clone())
|
||||
.app_data(expiry_watch_sender.clone())
|
||||
|
@ -62,7 +85,19 @@ async fn main() -> std::io::Result<()> {
|
|||
.route(web::get().to(upload::uploaded)),
|
||||
)
|
||||
.service(Files::new("/static", "static").disable_content_disposition())
|
||||
.service(
|
||||
.default_service(web::route().to(not_found));
|
||||
if config.enable_rate_limit {
|
||||
app.service(
|
||||
web::resource([
|
||||
"/{id:[a-z0-9]{5}}",
|
||||
"/{id:[a-z0-9]{5}}/",
|
||||
"/{id:[a-z0-9]{5}}/{name}",
|
||||
])
|
||||
.wrap(Governor::new(&governor_conf))
|
||||
.route(web::get().to(download::download)),
|
||||
)
|
||||
} else {
|
||||
app.service(
|
||||
web::resource([
|
||||
"/{id:[a-z0-9]{5}}",
|
||||
"/{id:[a-z0-9]{5}}/",
|
||||
|
@ -70,7 +105,7 @@ async fn main() -> std::io::Result<()> {
|
|||
])
|
||||
.route(web::get().to(download::download)),
|
||||
)
|
||||
.default_service(web::route().to(not_found))
|
||||
}
|
||||
}
|
||||
})
|
||||
.bind(bind_address)?
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::{config, file_kind::FileKind};
|
||||
use crate::config;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::{error, http::header::DispositionParam, Error};
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use mime::{Mime, TEXT_PLAIN};
|
||||
use std::path::Path;
|
||||
use time::OffsetDateTime;
|
||||
use time::{ext::NumericalDuration, Duration};
|
||||
|
@ -11,21 +12,20 @@ const MAX_UPLOAD_SECONDS: i64 = 31 * 24 * 60 * 60;
|
|||
const DEFAULT_UPLOAD_SECONDS: u32 = 30 * 60;
|
||||
|
||||
pub(crate) struct UploadConfig {
|
||||
pub original_name: String,
|
||||
pub original_name: Option<String>,
|
||||
pub content_type: Mime,
|
||||
pub valid_till: OffsetDateTime,
|
||||
pub kind: FileKind,
|
||||
pub delete_on_download: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn parse_multipart(
|
||||
mut payload: Multipart,
|
||||
file_id: &str,
|
||||
file_name: &Path,
|
||||
file_path: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<UploadConfig, error::Error> {
|
||||
let mut original_name: Option<String> = None;
|
||||
let mut content_type: Option<Mime> = None;
|
||||
let mut keep_for: Option<String> = None;
|
||||
let mut kind: Option<FileKind> = None;
|
||||
let mut delete_on_download = false;
|
||||
let mut password = None;
|
||||
let mut size = 0;
|
||||
|
@ -38,22 +38,20 @@ pub(crate) async fn parse_multipart(
|
|||
keep_for = Some(parse_string(name, field).await?);
|
||||
}
|
||||
"file" => {
|
||||
let file_original_name = get_original_filename(&field);
|
||||
if file_original_name == None || file_original_name.map(|f| f.as_str()) == Some("")
|
||||
{
|
||||
let (mime, uploaded_name) = get_file_metadata(&field);
|
||||
if uploaded_name == None || uploaded_name.map(|f| f.as_str()) == Some("") {
|
||||
continue;
|
||||
}
|
||||
original_name = file_original_name.map(|f| f.to_string());
|
||||
kind = Some(FileKind::Binary);
|
||||
size = create_file(file_name, field, config.max_file_size).await?;
|
||||
original_name = uploaded_name.map(|f| f.to_string());
|
||||
content_type = Some(mime.clone());
|
||||
size = create_file(file_path, field, config.max_file_size).await?;
|
||||
}
|
||||
"text" => {
|
||||
if original_name.is_some() {
|
||||
continue;
|
||||
}
|
||||
original_name = Some(format!("{}.txt", file_id));
|
||||
kind = Some(FileKind::Text);
|
||||
size = create_file(file_name, field, config.max_file_size).await?;
|
||||
size = create_file(file_path, field, config.max_file_size).await?;
|
||||
content_type = Some(get_content_type(file_path));
|
||||
}
|
||||
"delete_on_download" => {
|
||||
delete_on_download = parse_string(name, field).await? != "false";
|
||||
|
@ -65,8 +63,8 @@ pub(crate) async fn parse_multipart(
|
|||
};
|
||||
}
|
||||
|
||||
let original_name = original_name.ok_or_else(|| error::ErrorBadRequest("no content found"))?;
|
||||
let kind = kind.ok_or_else(|| error::ErrorBadRequest("no content found"))?;
|
||||
let content_type =
|
||||
content_type.ok_or_else(|| error::ErrorBadRequest("no content type found"))?;
|
||||
let keep_for: u32 = keep_for
|
||||
.map(|k| k.parse())
|
||||
.transpose()
|
||||
|
@ -77,8 +75,8 @@ pub(crate) async fn parse_multipart(
|
|||
|
||||
let upload_config = UploadConfig {
|
||||
original_name,
|
||||
content_type,
|
||||
valid_till,
|
||||
kind,
|
||||
delete_on_download,
|
||||
};
|
||||
|
||||
|
@ -94,8 +92,10 @@ fn check_requirements(
|
|||
valid_duration: &Duration,
|
||||
config: &config::Config,
|
||||
) -> Result<(), error::Error> {
|
||||
if upload_config.original_name.len() > 255 {
|
||||
return Err(error::ErrorBadRequest("filename is too long"));
|
||||
if let Some(original_name) = upload_config.original_name.as_ref() {
|
||||
if original_name.len() > 255 {
|
||||
return Err(error::ErrorBadRequest("filename is too long"));
|
||||
}
|
||||
}
|
||||
|
||||
let valid_seconds = valid_duration.whole_seconds();
|
||||
|
@ -181,13 +181,22 @@ async fn write_to_file(
|
|||
Ok(written_bytes)
|
||||
}
|
||||
|
||||
fn get_original_filename(field: &actix_multipart::Field) -> Option<&String> {
|
||||
field
|
||||
fn get_file_metadata(field: &actix_multipart::Field) -> (&Mime, Option<&String>) {
|
||||
let mime = field.content_type();
|
||||
let filename = field
|
||||
.content_disposition()
|
||||
.parameters
|
||||
.iter()
|
||||
.find_map(|param| match param {
|
||||
DispositionParam::Filename(filename) => Some(filename),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
(mime, filename)
|
||||
}
|
||||
|
||||
fn get_content_type(path: &Path) -> Mime {
|
||||
let std_path = std::path::Path::new(path.as_os_str());
|
||||
tree_magic_mini::from_filepath(std_path)
|
||||
.and_then(|mime| mime.parse().ok())
|
||||
.unwrap_or(TEXT_PLAIN)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
use actix_governor::KeyExtractor;
|
||||
use actix_governor::PeerIpKeyExtractor;
|
||||
use actix_web::{dev::ServiceRequest, http::header::ContentType};
|
||||
use governor::clock::{Clock, DefaultClock, QuantaInstant};
|
||||
use governor::NotUntil;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ForwardedPeerIpKeyExtractor {
|
||||
pub proxied: bool,
|
||||
}
|
||||
|
||||
impl KeyExtractor for ForwardedPeerIpKeyExtractor {
|
||||
type Key = IpAddr;
|
||||
type KeyExtractionError = &'static str;
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
fn name(&self) -> &'static str {
|
||||
"Forwarded peer IP"
|
||||
}
|
||||
|
||||
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||
let forwarded_for = req.headers().get("x-forwarded-for");
|
||||
if !self.proxied && forwarded_for.is_some() {
|
||||
let forwarded_for = forwarded_for
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|_| "x-forwarded-for contains invalid header value")?;
|
||||
forwarded_for
|
||||
.parse::<IpAddr>()
|
||||
.map_err(|_| "x-forwarded-for contains invalid ip adress")
|
||||
} else {
|
||||
PeerIpKeyExtractor.extract(req)
|
||||
}
|
||||
}
|
||||
|
||||
fn response_error_content(&self, negative: &NotUntil<QuantaInstant>) -> (String, ContentType) {
|
||||
let wait_time = negative
|
||||
.wait_time_from(DefaultClock::default().now())
|
||||
.as_secs();
|
||||
(
|
||||
format!("too many requests, retry in {}s", wait_time),
|
||||
ContentType::plaintext(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
fn key_name(&self, key: &Self::Key) -> Option<String> {
|
||||
Some(key.to_string())
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
use std::io::ErrorKind;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::file_kind::FileKind;
|
||||
use crate::multipart::UploadConfig;
|
||||
use crate::{multipart, template};
|
||||
use actix_files::NamedFile;
|
||||
|
@ -18,8 +17,8 @@ const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
|||
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
||||
|
||||
const ID_CHARS: &[char] = &[
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'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',
|
||||
];
|
||||
|
||||
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
|
||||
|
@ -42,11 +41,11 @@ pub async fn upload(
|
|||
error::ErrorInternalServerError("could not create file")
|
||||
})?;
|
||||
|
||||
let parsed_multipart = multipart::parse_multipart(payload, &file_id, &file_name, &config).await;
|
||||
let parsed_multipart = multipart::parse_multipart(payload, &file_name, &config).await;
|
||||
let UploadConfig {
|
||||
original_name,
|
||||
content_type,
|
||||
valid_till,
|
||||
kind,
|
||||
delete_on_download,
|
||||
} = match parsed_multipart {
|
||||
Ok(data) => data,
|
||||
|
@ -65,14 +64,17 @@ pub async fn upload(
|
|||
}
|
||||
};
|
||||
|
||||
let file_name = original_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}.txt", file_id));
|
||||
let db_insert = sqlx::query(
|
||||
"INSERT INTO Files (file_id, file_name, valid_till, kind, delete_on_download) \
|
||||
"INSERT INTO Files (file_id, file_name, content_type, valid_till, delete_on_download) \
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
)
|
||||
.bind(&file_id)
|
||||
.bind(&original_name)
|
||||
.bind(&file_name)
|
||||
.bind(&content_type.to_string())
|
||||
.bind(valid_till)
|
||||
.bind(kind.to_string())
|
||||
.bind(delete_on_download)
|
||||
.execute(db.as_ref())
|
||||
.await;
|
||||
|
@ -90,24 +92,24 @@ pub async fn upload(
|
|||
}
|
||||
|
||||
log::info!(
|
||||
"{} create new file {} (valid_till: {}, kind: {}, delete_on_download: {})",
|
||||
"{} create new file {} (valid_till: {}, content_type: {}, delete_on_download: {})",
|
||||
req.connection_info().realip_remote_addr().unwrap_or("-"),
|
||||
file_id,
|
||||
valid_till,
|
||||
kind,
|
||||
content_type,
|
||||
delete_on_download
|
||||
);
|
||||
|
||||
expiry_watch_sender.send(()).await.unwrap();
|
||||
|
||||
let redirect = if kind == FileKind::Binary {
|
||||
let encoded_name = urlencoding::encode(&original_name);
|
||||
let redirect = if let Some(original_name) = original_name.as_ref() {
|
||||
let encoded_name = urlencoding::encode(original_name);
|
||||
format!("/upload/{}/{}", file_id, encoded_name)
|
||||
} else {
|
||||
format!("/upload/{}", file_id)
|
||||
};
|
||||
|
||||
let url = get_file_url(&req, &file_id, Some(&original_name));
|
||||
let url = get_file_url(&req, &file_id, original_name.as_deref());
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.insert_header((LOCATION, redirect))
|
||||
.body(format!("{}\n", url)))
|
||||
|
|
Loading…
Reference in New Issue