2022-04-27 20:15:51 -04:00
|
|
|
mod download;
|
2022-04-30 01:38:26 -04:00
|
|
|
mod store;
|
2022-05-24 15:14:31 -04:00
|
|
|
mod timestamp;
|
2022-04-26 23:54:29 -04:00
|
|
|
mod upload;
|
|
|
|
mod zip;
|
|
|
|
|
2022-05-24 15:14:31 -04:00
|
|
|
use std::{fmt::Debug, path::PathBuf, str::FromStr};
|
2022-04-26 23:54:29 -04:00
|
|
|
|
|
|
|
use actix_web::{
|
2022-05-26 14:56:37 -04:00
|
|
|
error::InternalError, get, middleware::Logger, post, web, App, HttpRequest, HttpResponse,
|
|
|
|
HttpServer, Responder,
|
2022-04-26 23:54:29 -04:00
|
|
|
};
|
|
|
|
use actix_web_actors::ws;
|
2022-05-24 15:14:31 -04:00
|
|
|
use askama::Template;
|
2022-05-03 16:28:43 -04:00
|
|
|
use bytesize::ByteSize;
|
|
|
|
use log::{error, warn};
|
|
|
|
use serde::{Deserialize, Serialize};
|
2022-05-24 15:14:31 -04:00
|
|
|
use store::{FileStore, StoredFile};
|
|
|
|
use tokio::{fs::File, sync::RwLock};
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-28 05:18:35 -04:00
|
|
|
const APP_NAME: &str = "transbeam";
|
2022-04-27 00:44:35 -04:00
|
|
|
|
2022-05-26 15:09:37 -04:00
|
|
|
const DATE_DISPLAY_FORMAT: &[time::format_description::FormatItem] =
|
|
|
|
time::macros::format_description!("[year]-[month]-[day]");
|
|
|
|
|
2022-05-03 16:28:43 -04:00
|
|
|
struct AppState {
|
|
|
|
file_store: RwLock<FileStore>,
|
|
|
|
config: Config,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Config {
|
|
|
|
max_upload_size: u64,
|
|
|
|
max_lifetime: u16,
|
|
|
|
upload_password: String,
|
|
|
|
storage_dir: PathBuf,
|
|
|
|
reverse_proxy: bool,
|
|
|
|
mnemonic_codes: bool,
|
2022-05-26 14:43:03 -04:00
|
|
|
cachebuster: String,
|
2022-05-03 16:28:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String {
|
|
|
|
let conn_info = req.connection_info();
|
|
|
|
if reverse_proxy {
|
|
|
|
conn_info.realip_remote_addr()
|
|
|
|
} else {
|
|
|
|
conn_info.peer_addr()
|
|
|
|
}
|
|
|
|
.unwrap()
|
|
|
|
.to_owned()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn log_auth_failure(ip_addr: &str) {
|
|
|
|
warn!("Incorrect authentication attempt from {}", ip_addr);
|
|
|
|
}
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-05-26 14:43:03 -04:00
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "index.html")]
|
2022-05-26 14:56:37 -04:00
|
|
|
struct IndexPage<'a> {
|
|
|
|
cachebuster: &'a str,
|
|
|
|
}
|
2022-05-26 14:43:03 -04:00
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
async fn index(data: web::Data<AppState>) -> impl Responder {
|
2022-05-26 14:56:37 -04:00
|
|
|
HttpResponse::Ok().body(
|
|
|
|
IndexPage {
|
|
|
|
cachebuster: &data.config.cachebuster,
|
|
|
|
}
|
|
|
|
.render()
|
|
|
|
.unwrap(),
|
|
|
|
)
|
2022-05-26 14:43:03 -04:00
|
|
|
}
|
|
|
|
|
2022-05-01 05:11:23 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct DownloadRequest {
|
|
|
|
code: String,
|
2022-05-24 15:14:31 -04:00
|
|
|
download: Option<download::DownloadSelection>,
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:43:03 -04:00
|
|
|
#[derive(Template)]
|
2022-05-24 15:14:31 -04:00
|
|
|
#[template(path = "download.html")]
|
2022-05-26 14:43:03 -04:00
|
|
|
struct DownloadPage<'a> {
|
|
|
|
info: DownloadInfo,
|
|
|
|
cachebuster: &'a str,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
2022-05-24 16:49:48 -04:00
|
|
|
struct DownloadInfo {
|
2022-05-24 15:14:31 -04:00
|
|
|
file: StoredFile,
|
2022-05-24 16:49:48 -04:00
|
|
|
code: String,
|
2022-05-24 15:14:31 -04:00
|
|
|
available: u64,
|
2022-05-24 16:49:48 -04:00
|
|
|
offsets: Option<Vec<u64>>,
|
2022-05-01 05:11:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/download")]
|
2022-04-28 05:18:35 -04:00
|
|
|
async fn handle_download(
|
|
|
|
req: HttpRequest,
|
2022-05-24 16:49:48 -04:00
|
|
|
query: web::Query<DownloadRequest>,
|
2022-05-03 16:28:43 -04:00
|
|
|
data: web::Data<AppState>,
|
2022-04-28 05:18:35 -04:00
|
|
|
) -> actix_web::Result<HttpResponse> {
|
2022-05-24 16:49:48 -04:00
|
|
|
let code = &query.code;
|
2022-05-01 05:11:23 -04:00
|
|
|
if !store::is_valid_storage_code(code) {
|
2022-05-24 15:14:31 -04:00
|
|
|
return not_found(req, data, true);
|
2022-04-27 20:15:51 -04:00
|
|
|
}
|
2022-05-03 18:31:50 -04:00
|
|
|
let info = data.file_store.read().await.lookup_file(code);
|
2022-05-24 15:14:31 -04:00
|
|
|
let info = if let Some(i) = info {
|
|
|
|
i
|
|
|
|
} else {
|
2022-05-24 16:52:00 -04:00
|
|
|
return not_found(req, data, true);
|
2022-05-24 15:14:31 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
let storage_path = data.config.storage_dir.join(code);
|
|
|
|
let file = File::open(&storage_path).await?;
|
2022-05-24 16:49:48 -04:00
|
|
|
if let Some(selection) = query.download {
|
2022-05-24 15:14:31 -04:00
|
|
|
if let download::DownloadSelection::One(n) = selection {
|
|
|
|
if let Some(ref files) = info.contents {
|
|
|
|
if n >= files.len() {
|
|
|
|
return not_found(req, data, false);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return not_found(req, data, false);
|
|
|
|
}
|
|
|
|
}
|
2022-04-27 20:15:51 -04:00
|
|
|
Ok(download::DownloadingFile {
|
2022-05-24 15:14:31 -04:00
|
|
|
file: file.into_std().await,
|
2022-04-29 22:36:44 -04:00
|
|
|
storage_path,
|
2022-04-28 05:18:35 -04:00
|
|
|
info,
|
2022-05-24 15:14:31 -04:00
|
|
|
selection,
|
2022-04-28 05:18:35 -04:00
|
|
|
}
|
2022-05-24 16:52:00 -04:00
|
|
|
.into_response(&req))
|
2022-04-27 20:15:51 -04:00
|
|
|
} else {
|
2022-05-24 16:49:48 -04:00
|
|
|
let offsets = info.contents.as_deref().map(zip::file_data_offsets);
|
2022-05-24 16:52:00 -04:00
|
|
|
Ok(HttpResponse::Ok().body(
|
2022-05-26 14:43:03 -04:00
|
|
|
DownloadPage {
|
|
|
|
info: DownloadInfo {
|
|
|
|
file: info,
|
|
|
|
code: code.clone(),
|
|
|
|
available: file.metadata().await?.len(),
|
|
|
|
offsets,
|
|
|
|
},
|
|
|
|
cachebuster: &data.config.cachebuster,
|
2022-05-24 16:52:00 -04:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
.unwrap(),
|
|
|
|
))
|
2022-04-27 20:15:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-24 16:49:48 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct InfoQuery {
|
|
|
|
code: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/info")]
|
|
|
|
async fn download_info(
|
|
|
|
req: HttpRequest,
|
|
|
|
query: web::Query<InfoQuery>,
|
|
|
|
data: web::Data<AppState>,
|
|
|
|
) -> actix_web::Result<impl Responder> {
|
|
|
|
let code = &query.code;
|
|
|
|
if !store::is_valid_storage_code(code) {
|
|
|
|
return not_found(req, data, true);
|
|
|
|
}
|
|
|
|
let info = data.file_store.read().await.lookup_file(code);
|
|
|
|
let info = if let Some(i) = info {
|
|
|
|
i
|
|
|
|
} else {
|
2022-05-24 16:52:00 -04:00
|
|
|
return not_found(req, data, true);
|
2022-05-24 16:49:48 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
let storage_path = data.config.storage_dir.join(code);
|
|
|
|
let offsets = info.contents.as_deref().map(zip::file_data_offsets);
|
|
|
|
Ok(web::Json(DownloadInfo {
|
|
|
|
file: info,
|
|
|
|
code: code.clone(),
|
|
|
|
available: File::open(&storage_path).await?.metadata().await?.len(),
|
|
|
|
offsets,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:43:03 -04:00
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "404.html")]
|
2022-05-26 14:56:37 -04:00
|
|
|
struct NotFoundPage<'a> {
|
|
|
|
cachebuster: &'a str,
|
|
|
|
}
|
2022-05-26 14:43:03 -04:00
|
|
|
|
2022-05-24 16:52:00 -04:00
|
|
|
fn not_found<T>(req: HttpRequest, data: web::Data<AppState>, report: bool) -> actix_web::Result<T> {
|
2022-05-24 15:14:31 -04:00
|
|
|
if report {
|
|
|
|
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
|
|
|
|
log_auth_failure(&ip_addr);
|
|
|
|
}
|
2022-05-24 16:52:00 -04:00
|
|
|
Err(InternalError::from_response(
|
|
|
|
"Download not found",
|
2022-05-26 14:56:37 -04:00
|
|
|
HttpResponse::NotFound().body(
|
|
|
|
NotFoundPage {
|
|
|
|
cachebuster: &data.config.cachebuster,
|
|
|
|
}
|
|
|
|
.render()
|
|
|
|
.unwrap(),
|
|
|
|
),
|
2022-05-24 16:52:00 -04:00
|
|
|
)
|
|
|
|
.into())
|
2022-05-03 18:31:50 -04:00
|
|
|
}
|
|
|
|
|
2022-04-26 23:54:29 -04:00
|
|
|
#[get("/upload")]
|
2022-05-03 16:28:43 -04:00
|
|
|
async fn handle_upload(
|
|
|
|
req: HttpRequest,
|
|
|
|
stream: web::Payload,
|
|
|
|
data: web::Data<AppState>,
|
|
|
|
) -> impl Responder {
|
|
|
|
if data.file_store.read().await.full() {
|
|
|
|
return Ok(HttpResponse::BadRequest().finish());
|
|
|
|
}
|
|
|
|
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
|
|
|
|
ws::start(upload::Uploader::new(data, ip_addr), &req, stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct UploadPasswordCheck {
|
|
|
|
password: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/upload/check_password")]
|
|
|
|
async fn check_upload_password(
|
|
|
|
req: HttpRequest,
|
|
|
|
body: web::Json<UploadPasswordCheck>,
|
|
|
|
data: web::Data<AppState>,
|
|
|
|
) -> impl Responder {
|
|
|
|
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
|
|
|
|
if body.password != data.config.upload_password {
|
|
|
|
log_auth_failure(&ip_addr);
|
|
|
|
HttpResponse::Forbidden().finish()
|
|
|
|
} else {
|
|
|
|
HttpResponse::NoContent().finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct UploadLimits {
|
|
|
|
open: bool,
|
|
|
|
max_size: u64,
|
|
|
|
max_lifetime: u16,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/upload/limits.json")]
|
|
|
|
async fn upload_limits(data: web::Data<AppState>) -> impl Responder {
|
|
|
|
let file_store = data.file_store.read().await;
|
|
|
|
let open = !file_store.full();
|
|
|
|
let available_size = file_store.available_size();
|
|
|
|
let max_size = std::cmp::min(available_size, data.config.max_upload_size);
|
|
|
|
web::Json(UploadLimits {
|
|
|
|
open,
|
|
|
|
max_size,
|
|
|
|
max_lifetime: data.config.max_lifetime,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn env_or<T: FromStr>(var: &str, default: T) -> T
|
|
|
|
where
|
|
|
|
<T as FromStr>::Err: Debug,
|
|
|
|
{
|
|
|
|
std::env::var(var)
|
|
|
|
.map(|val| {
|
|
|
|
val.parse::<T>()
|
|
|
|
.unwrap_or_else(|_| panic!("Invalid value {} for variable {}", val, var))
|
|
|
|
})
|
|
|
|
.unwrap_or(default)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn env_or_else<T: FromStr, F: FnOnce() -> T>(var: &str, default: F) -> T
|
|
|
|
where
|
|
|
|
<T as FromStr>::Err: Debug,
|
|
|
|
{
|
|
|
|
std::env::var(var)
|
|
|
|
.map(|val| {
|
|
|
|
val.parse::<T>()
|
|
|
|
.unwrap_or_else(|_| panic!("Invalid value {} for variable {}", val, var))
|
|
|
|
})
|
|
|
|
.ok()
|
|
|
|
.unwrap_or_else(default)
|
2022-04-26 23:54:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[actix_web::main]
|
|
|
|
async fn main() -> std::io::Result<()> {
|
2022-05-03 16:28:43 -04:00
|
|
|
dotenv::dotenv().ok();
|
2022-04-26 23:54:29 -04:00
|
|
|
env_logger::init();
|
|
|
|
|
2022-05-03 16:28:43 -04:00
|
|
|
let static_dir: PathBuf = env_or_else("TRANSBEAM_STATIC_DIR", || PathBuf::from("static"));
|
|
|
|
let storage_dir: PathBuf = env_or_else("TRANSBEAM_STORAGE_DIR", || PathBuf::from("storage"));
|
|
|
|
let port: u16 = env_or("TRANSBEAM_PORT", 8080);
|
|
|
|
let mnemonic_codes: bool = env_or("TRANSBEAM_MNEMONIC_CODES", true);
|
2022-05-29 10:54:17 -04:00
|
|
|
let reverse_proxy: bool = env_or("TRANSBEAM_REVERSE_PROXY", true);
|
2022-05-03 16:28:43 -04:00
|
|
|
let max_lifetime: u16 = env_or("TRANSBEAM_MAX_LIFETIME", 30);
|
|
|
|
let max_upload_size: u64 =
|
|
|
|
env_or::<ByteSize>("TRANSBEAM_MAX_UPLOAD_SIZE", ByteSize(16 * bytesize::GB)).as_u64();
|
|
|
|
let max_storage_size: u64 =
|
|
|
|
env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64();
|
|
|
|
let upload_password: String =
|
|
|
|
std::env::var("TRANSBEAM_UPLOAD_PASSWORD").expect("TRANSBEAM_UPLOAD_PASSWORD must be set!");
|
2022-05-26 14:43:03 -04:00
|
|
|
let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new);
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-05-03 16:28:43 -04:00
|
|
|
let data = web::Data::new(AppState {
|
|
|
|
file_store: RwLock::new(FileStore::load(storage_dir.clone(), max_storage_size).await?),
|
|
|
|
config: Config {
|
|
|
|
max_upload_size,
|
|
|
|
max_lifetime,
|
|
|
|
upload_password,
|
|
|
|
storage_dir,
|
|
|
|
reverse_proxy,
|
|
|
|
mnemonic_codes,
|
2022-05-26 14:43:03 -04:00
|
|
|
cachebuster,
|
2022-05-03 16:28:43 -04:00
|
|
|
},
|
|
|
|
});
|
|
|
|
start_reaper(data.clone());
|
2022-04-27 00:53:32 -04:00
|
|
|
|
2022-05-23 21:21:57 -04:00
|
|
|
let server = HttpServer::new(move || {
|
2022-04-26 23:54:29 -04:00
|
|
|
App::new()
|
|
|
|
.app_data(data.clone())
|
2022-05-03 16:28:43 -04:00
|
|
|
.wrap(if data.config.reverse_proxy {
|
|
|
|
Logger::new(r#"%{r}a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
|
|
|
|
} else {
|
|
|
|
Logger::default()
|
|
|
|
})
|
2022-05-26 14:43:03 -04:00
|
|
|
.service(index)
|
2022-04-27 20:15:51 -04:00
|
|
|
.service(handle_download)
|
2022-05-24 16:49:48 -04:00
|
|
|
.service(download_info)
|
2022-05-03 16:28:43 -04:00
|
|
|
.service(handle_upload)
|
|
|
|
.service(check_upload_password)
|
|
|
|
.service(upload_limits)
|
2022-05-26 14:43:03 -04:00
|
|
|
.service(actix_files::Files::new("/", static_dir.clone()))
|
2022-05-23 21:21:57 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
if reverse_proxy {
|
|
|
|
server
|
|
|
|
.bind((std::net::Ipv4Addr::LOCALHOST, port))?
|
|
|
|
.bind((std::net::Ipv6Addr::LOCALHOST, port))?
|
|
|
|
} else {
|
|
|
|
// Looks like this also picks up IPv4?
|
|
|
|
// Binding 0.0.0.0 and :: on the same port fails with an error.
|
|
|
|
server.bind((std::net::Ipv6Addr::UNSPECIFIED, port))?
|
|
|
|
}
|
2022-04-26 23:54:29 -04:00
|
|
|
.run()
|
|
|
|
.await?;
|
2022-05-23 21:21:57 -04:00
|
|
|
|
2022-04-26 23:54:29 -04:00
|
|
|
Ok(())
|
|
|
|
}
|
2022-04-30 01:38:26 -04:00
|
|
|
|
2022-05-03 16:28:43 -04:00
|
|
|
fn start_reaper(data: web::Data<AppState>) {
|
2022-04-30 01:38:26 -04:00
|
|
|
std::thread::spawn(move || {
|
|
|
|
actix_web::rt::System::new().block_on(async {
|
|
|
|
loop {
|
|
|
|
actix_web::rt::time::sleep(core::time::Duration::from_secs(86400)).await;
|
2022-05-03 16:28:43 -04:00
|
|
|
if let Err(e) = data.file_store.write().await.remove_expired_files().await {
|
2022-04-30 01:38:26 -04:00
|
|
|
error!("Error reaping expired files: {}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|