From 920b28f5f56099683ef0040058346968265c490c Mon Sep 17 00:00:00 2001 From: xenofem Date: Thu, 10 Nov 2022 12:41:09 -0500 Subject: [PATCH] add beginnings of admin panel --- Cargo.lock | 151 +++++++++++++++++++++++++++++++- Cargo.toml | 4 + README.md | 10 +++ src/main.rs | 84 +++++++++++++++++- static/css/transbeam.css | 9 +- templates/admin/signed_in.html | 21 +++++ templates/admin/signed_out.html | 18 ++++ 7 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 templates/admin/signed_in.html create mode 100644 templates/admin/signed_out.html diff --git a/Cargo.lock b/Cargo.lock index 0e71375..dfd9dab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d324d2a6e670f8746ae64f333c2c0fe856bdf624bcb72fb56e250e62a7e9a85f" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "async-trait", + "derive_more", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.0" @@ -262,6 +279,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + [[package]] name = "aes" version = "0.7.5" @@ -274,6 +300,20 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" @@ -309,6 +349,23 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "anyhow" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" + +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash 0.4.2", +] + [[package]] name = "askama" version = "0.11.1" @@ -368,6 +425,17 @@ dependencies = [ "toml", ] +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -403,6 +471,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.2" @@ -543,7 +620,14 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] @@ -596,6 +680,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.14.1" @@ -768,6 +861,16 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "h2" version = "0.3.13" @@ -808,6 +911,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1178,6 +1290,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.7" @@ -1192,7 +1315,7 @@ checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" dependencies = [ "digest", "hmac", - "password-hash", + "password-hash 0.3.2", "sha2", ] @@ -1220,6 +1343,18 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1648,10 +1783,13 @@ dependencies = [ "actix", "actix-files", "actix-http", + "actix-session", "actix-web", "actix-web-actors", + "argon2", "askama", "askama_actix", + "base64", "bytes", "bytesize", "crc32fast", @@ -1663,6 +1801,7 @@ dependencies = [ "log", "mime", "mnemonic", + "password-hash 0.4.2", "pin-project-lite", "rand", "sanitise-file-name", @@ -1712,6 +1851,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "url" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index c91dee1..58d0229 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,13 @@ license = "MIT" actix = "0.13" actix-files = "0.6.0" actix-http = "3.0.4" +actix-session = { version = "0.7.1", features = ["cookie-session"] } actix-web = "4.0.1" actix-web-actors = "4.1.0" +argon2 = "0.4.1" askama = "0.11.1" askama_actix = "0.13" +base64 = "0.13" bytes = "1.1.0" bytesize = "1.1.0" crc32fast = "1.3.2" @@ -24,6 +27,7 @@ jsondb = "0.4.0" log = "0.4" mime = "0.3.16" mnemonic = "1.0.1" +password-hash = { version = "0.4.2", features = ["alloc"] } pin-project-lite = "0.2.9" rand = "0.8.5" sanitise-file-name = "1.0.0" diff --git a/README.md b/README.md index 525eae6..f71c743 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,16 @@ ## configuration transbeam is configured with the following environment variables: +- `TRANSBEAM_ADMIN_PASSWORD_HASH`: Argon2 hash of a password for + accessing the admin interface. To generate: + ```bash + echo -n 'Password: '; head -1 | tr -d '\n' | argon2 $(openssl rand -base64 32) -id -t 2 -m 14 -p 1 -e + ``` +- `TRANSBEAM_COOKIE_SECRET`: Base64-encoded cryptographic random data + for encrypting private session cookies. To generate: + ```bash + openssl rand -base64 64 | tr -d '\n' + ``` - `TRANSBEAM_STORAGE_DIR`: path where uploaded files should be stored (default: `./storage`) - `TRANSBEAM_STATIC_DIR`: path where the web app's static files live diff --git a/src/main.rs b/src/main.rs index 8aec7b1..e08daec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,20 +4,23 @@ mod store; mod upload; mod zip; -use std::{fmt::Debug, path::PathBuf, str::FromStr}; +use std::{fmt::Debug, path::PathBuf, str::FromStr, ops::Deref}; use actix_http::StatusCode; +use actix_session::{SessionMiddleware, storage::CookieSessionStore, Session}; use actix_web::{ error::InternalError, get, middleware::Logger, post, web, App, HttpRequest, HttpResponse, - HttpServer, Responder, + HttpServer, Responder, cookie, }; use actix_web_actors::ws; +use argon2::{Argon2, PasswordVerifier}; use askama_actix::{Template, TemplateToResponse}; use bytesize::ByteSize; use log::{error, warn}; +use password_hash::PasswordHashString; use serde::{Deserialize, Serialize}; use state::StateDb; -use store::StoredFile; +use store::{StoredFile, StoredFiles}; use tokio::fs::File; const APP_NAME: &str = "transbeam"; @@ -40,6 +43,7 @@ struct Config { reverse_proxy: bool, mnemonic_codes: bool, cachebuster: String, + admin_password_hash: PasswordHashString, } pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String { @@ -72,6 +76,67 @@ async fn index(data: web::Data) -> impl Responder { } } +#[derive(Template)] +#[template(path = "admin/signed_out.html")] +struct SignedOutAdminPage { + cachebuster: String, + base_url: String, + incorrect_password: bool, +} + +#[derive(Template)] +#[template(path = "admin/signed_in.html")] +struct AdminPage<'a> { + cachebuster: String, + base_url: String, + stored_files: &'a StoredFiles, +} + +#[get("/admin")] +async fn admin_panel(data: web::Data, session: Session) -> actix_web::Result { + if let Some(true) = session.get::("admin")? { + Ok(AdminPage { + cachebuster: data.config.cachebuster.clone(), + base_url: data.config.base_url.clone(), + stored_files: data.state.read().await.deref(), + }.to_response()) + } else { + Ok(SignedOutAdminPage { + cachebuster: data.config.cachebuster.clone(), + base_url: data.config.base_url.clone(), + incorrect_password: false, + }.to_response()) + } +} + +#[derive(Deserialize)] +struct AdminPanelSignin { + password: String, +} + +#[post("/admin")] +async fn admin_signin(req: HttpRequest, data: web::Data, form: web::Form, session: Session) -> actix_web::Result { + if Argon2::default().verify_password(form.password.as_bytes(), &data.config.admin_password_hash.password_hash()).is_ok() { + session.insert("admin", true)?; + Ok(AdminPage { + cachebuster: data.config.cachebuster.clone(), + base_url: data.config.base_url.clone(), + stored_files: data.state.read().await.deref(), + }.to_response()) + } else { + let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); + log_auth_failure(&ip_addr); + let mut resp = SignedOutAdminPage { + cachebuster: data.config.cachebuster.clone(), + base_url: data.config.base_url.clone(), + incorrect_password: true, + }.to_response(); + *resp.status_mut() = StatusCode::FORBIDDEN; + + Err(InternalError::from_response("Incorrect password", resp).into()) + } +} + #[derive(Deserialize)] struct DownloadRequest { code: String, @@ -307,6 +372,15 @@ async fn main() -> std::io::Result<()> { env_or::("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64(); let upload_password: String = env_or_panic("TRANSBEAM_UPLOAD_PASSWORD"); let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new); + let admin_password_hash: PasswordHashString = env_or_panic("TRANSBEAM_ADMIN_PASSWORD_HASH"); + + let cookie_secret_base64: String = env_or_panic("TRANSBEAM_COOKIE_SECRET"); + let cookie_key = cookie::Key::from( + &base64::decode(&cookie_secret_base64) + .unwrap_or_else( + |_| panic!("Value {} for TRANSBEAM_COOKIE_SECRET is not valid base64", cookie_secret_base64) + ) + ); let state_file: PathBuf = match std::env::var("TRANSBEAM_STATE_FILE") { Ok(v) => v @@ -336,6 +410,7 @@ async fn main() -> std::io::Result<()> { reverse_proxy, mnemonic_codes, cachebuster, + admin_password_hash, }, }); data.cleanup().await?; @@ -349,12 +424,15 @@ async fn main() -> std::io::Result<()> { } else { Logger::default() }) + .wrap(SessionMiddleware::new(CookieSessionStore::default(), cookie_key.clone())) .service(index) .service(handle_download) .service(download_info) .service(handle_upload) .service(check_upload_password) .service(upload_limits) + .service(admin_panel) + .service(admin_signin) .service(actix_files::Files::new("/", static_dir.clone())) }); diff --git a/static/css/transbeam.css b/static/css/transbeam.css index 44dedec..13e373e 100644 --- a/static/css/transbeam.css +++ b/static/css/transbeam.css @@ -234,7 +234,14 @@ summary { td.file_size { text-align: right; - width: 5.12rem; +} + +td.file_size, th.file_size { + width: 6rem; +} + +td.file_expiry, th.file_expiry { + width: 10rem; } td.file_name { diff --git a/templates/admin/signed_in.html b/templates/admin/signed_in.html new file mode 100644 index 0000000..710ba4d --- /dev/null +++ b/templates/admin/signed_in.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block body %} +
+

Admin

+ + + + {% for (code, entry) in stored_files.0.iter() %} + + + + + + {% endfor %} +
Name + Size + Expiry +
{{ entry.file.name }}{{ entry.file.formatted_size() }}{{ entry.file.expiry.format(DATE_DISPLAY_FORMAT).unwrap() }}
+
+{% endblock %} diff --git a/templates/admin/signed_out.html b/templates/admin/signed_out.html new file mode 100644 index 0000000..dd5f725 --- /dev/null +++ b/templates/admin/signed_out.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block body %} +
+

Sign In

+ {% if incorrect_password %} +

Incorrect Password

+ {% endif %} +
+
+ +
+ +
+
+{% endblock %}