diff --git a/Cargo.lock b/Cargo.lock index b5817e7..1a3d2d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1361,6 +1361,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "012bb02250fdd38faa5feee63235f7a459974440b9b57593822414c31f92839e" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kamadak-exif" version = "0.5.4" @@ -1745,6 +1759,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1796,6 +1821,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -1862,6 +1896,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2034,6 +2077,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "rand 0.8.4", +] + [[package]] name = "quote" version = "1.0.14" @@ -2231,6 +2283,21 @@ dependencies = [ "quick-error", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -2390,6 +2457,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.7", +] + [[package]] name = "siphasher" version = "0.3.9" @@ -2429,6 +2508,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "standback" version = "0.2.17" @@ -2633,11 +2718,24 @@ dependencies = [ "libc", "standback", "stdweb", - "time-macros", + "time-macros 0.1.1", "version_check 0.9.4", "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +dependencies = [ + "itoa 1.0.1", + "libc", + "num_threads", + "quickcheck", + "time-macros 0.2.3", +] + [[package]] name = "time-macros" version = "0.1.1" @@ -2648,6 +2746,12 @@ dependencies = [ "time-macros-impl", ] +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + [[package]] name = "time-macros-impl" version = "0.1.2" @@ -2899,6 +3003,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "upend" version = "0.0.61" @@ -2923,6 +3033,7 @@ dependencies = [ "id3", "image", "is_executable", + "jsonwebtoken", "kamadak-exif", "lazy_static", "lexpr", @@ -2933,6 +3044,7 @@ dependencies = [ "nonempty", "once_cell", "opener", + "rand 0.8.4", "rayon", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1860c5c..4491276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ actix-files = "^0.5" actix-rt = "^2.0" actix-web = "^3.3" actix_derive = "^0.5" +jsonwebtoken = "8" chrono = { version = "0.4", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } @@ -46,13 +47,16 @@ lexpr = "0.2.6" regex = "1" bs58 = "^0.4" -filebuffer = "0.4.0" tiny-keccak = { version = "2.0", features = ["k12"] } unsigned-varint = { version = "^0", features = ["std"] } uuid = { version = "0.8", features = ["v4"] } + +filebuffer = "0.4.0" tempfile = "^3.2.0" walkdir = "2" +rand = "0.8" + mime = "^0.3.16" tree_magic_mini = "3.0.2" diff --git a/src/main.rs b/src/main.rs index f17d145..17be3a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use actix_web::{middleware, App, HttpServer}; use anyhow::Result; use clap::{App as ClapApp, Arg}; use log::{debug, info, warn}; +use rand::{thread_rng, Rng}; use std::sync::Arc; use crate::{ @@ -85,6 +86,20 @@ fn main() -> Result<()> { .takes_value(true) .long("name") .help("Name of the vault."), + ) + .arg( + Arg::with_name("SECRET") + .takes_value(true) + .long("secret") + .env("UPEND_SECRET") + .help("Secret to use for authentication."), + ) + .arg( + Arg::with_name("KEY") + .takes_value(true) + .long("key") + .env("UPEND_KEY") + .help("Authentication key users must supply."), ); let matches = app.get_matches(); @@ -142,6 +157,21 @@ fn main() -> Result<()> { .parse() .expect("Incorrect bind format."); + let secret = matches + .value_of("SECRET") + .map(String::from) + .unwrap_or_else(|| { + warn!("No secret supplied, generating one at random."); + + thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(32) + .map(char::from) + .collect() + }); + + let key = matches.value_of("KEY").map(String::from); + let state = routes::State { upend: upend.clone(), vault_name: Some( @@ -160,6 +190,8 @@ fn main() -> Result<()> { job_container: job_container.clone(), preview_store, desktop_enabled, + secret, + key, }; // Start HTTP server @@ -175,6 +207,7 @@ fn main() -> Result<()> { .app_data(actix_web::web::PayloadConfig::new(4_294_967_296)) .data(state.clone()) .wrap(middleware::Logger::default().exclude("/api/jobs")) + .service(routes::login) .service(routes::get_raw) .service(routes::get_thumbnail) .service(routes::get_query) diff --git a/src/routes.rs b/src/routes.rs index 40b23cb..fe615d2 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -12,14 +12,19 @@ use crate::util::hash::{b58_decode, b58_encode, Hashable}; use crate::util::jobs::JobContainer; use actix_files::NamedFile; use actix_multipart::Multipart; -use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}; -use actix_web::http::header::{CacheControl, CacheDirective, ContentDisposition, DispositionType}; +use actix_web::error::{ + ErrorBadRequest, ErrorInternalServerError, ErrorNotFound, ErrorUnauthorized, +}; use actix_web::{delete, error, get, post, put, web, Either, Error, HttpResponse}; use actix_web::{http, Responder}; +use actix_web::{ + http::header::{CacheControl, CacheDirective, ContentDisposition, DispositionType}, + HttpRequest, +}; use anyhow::{anyhow, Result}; use futures_util::TryStreamExt; use log::{debug, info, trace, warn}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::json; use std::convert::{TryFrom, TryInto}; use std::fs; @@ -41,6 +46,69 @@ pub struct State { pub job_container: JobContainer, pub preview_store: Option>, pub desktop_enabled: bool, + pub secret: String, + pub key: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct JwtClaims { + exp: usize, +} + +#[derive(Deserialize)] +pub struct LoginRequest { + key: String, +} + +#[post("/api/auth/login")] +pub async fn login( + state: web::Data, + payload: web::Json, +) -> Result { + if state.key.is_none() || Some(&payload.key) == state.key.as_ref() { + let claims = JwtClaims { + exp: (SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(ErrorInternalServerError)? + .as_secs() + + 7 * 24 * 60 * 60) as usize, + }; + + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claims, + &jsonwebtoken::EncodingKey::from_secret(state.secret.as_ref()), + ) + .map_err(ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(json!({ "token": token }))) + } else { + Err(ErrorUnauthorized("Incorrect token.")) + } +} + +fn check_auth(req: &HttpRequest, state: &State) -> Result<(), actix_web::Error> { + if let Some(key) = &state.key { + if let Some(auth_header) = req.headers().get("Authorization") { + let auth_header = auth_header.to_str().map_err(|err| { + ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}")) + })?; + + let token = jsonwebtoken::decode::( + auth_header, + &jsonwebtoken::DecodingKey::from_secret(key.as_ref()), + &jsonwebtoken::Validation::default(), + ); + + token + .map(|_| ()) + .map_err(|err| ErrorUnauthorized(format!("Invalid token: {err:?}"))) + } else { + Err(ErrorUnauthorized("Authorization required.")) + } + } else { + Ok(()) + } } #[derive(Deserialize)] @@ -303,9 +371,12 @@ impl TryInto
for InAddress { #[put("/api/obj")] pub async fn put_object( + req: HttpRequest, state: web::Data, payload: Either, Multipart>, ) -> Result { + check_auth(&req, &state)?; + let (entry_address, entity_address) = match payload { Either::A(in_entry) => { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; @@ -494,10 +565,12 @@ pub async fn put_object( #[put("/api/obj/{address}/{attribute}")] pub async fn put_object_attribute( + req: HttpRequest, state: web::Data, web::Path((address, attribute)): web::Path<(Address, String)>, value: web::Json, ) -> Result { + check_auth(&req, &state)?; let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let new_address = web::block(move || { @@ -526,9 +599,12 @@ pub async fn put_object_attribute( #[delete("/api/obj/{address_str}")] pub async fn delete_object( + req: HttpRequest, state: web::Data, address: web::Path
, ) -> Result { + check_auth(&req, &state)?; + let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let _ = web::block(move || connection.remove_object(address.into_inner())) .await @@ -649,9 +725,12 @@ pub struct RescanRequest { #[post("/api/refresh")] pub async fn api_refresh( + req: HttpRequest, state: web::Data, web::Query(query): web::Query, ) -> Result { + check_auth(&req, &state)?; + block_background::<_, _, anyhow::Error>(move || { let _ = crate::filesystem::rescan_vault( state.upend.clone(),