From fae7b2a9624492ad91a1845c396ffab76a0c5edb Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Thu, 18 Feb 2021 20:31:03 -0500 Subject: [PATCH 1/4] Add tests for JWT decoding --- src/data/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/data/mod.rs b/src/data/mod.rs index 92a1404..d21cd6d 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -11,14 +11,19 @@ pub struct Token<'a> { pub token: &'a str, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Claims { pub sub: String, pub exp: i64, } pub fn secret_key() -> String { - dotenv::var("SECRET_KEY").expect("SECRET_KEY env not set!") + if cfg!(test) { + String::from("test_key") + } else { + println!("USING REAL KEY"); + dotenv::var("SECRET_KEY").expect("SECRET_KEY env not set!") + } } impl FromStr for Claims { @@ -85,3 +90,39 @@ pub struct CreateAccountRequest { pub struct AddFavoriteRequest { pub path: String, } + +#[cfg(test)] +mod tests { + use super::Claims; + use jsonwebtoken::errors::ErrorKind; + use std::str::FromStr; + + #[test] + fn test_token_from_claims() { + let claims = Claims { + exp: 16136164790, // 2481-ish + sub: String::from("9"), + }; + + let c = Claims::from_str( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5IiwiZXhwIjoxNjEzNjE2NDc5MH0.9wwK4l8vhvq55YoueEljMbN_5uVTaAsGLLRPr0AuymE") + .unwrap(); + + assert_eq!(claims.sub, c.sub); + assert_eq!(claims.exp, c.exp); + } + + #[test] + fn test_expired_token() { + let err = Claims::from_str( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5IiwiZXhwIjoxNn0.eZnfaNfiD54VMbphIqeBICeG9SzAtwNXntLwtTBihjY"); + + match err.unwrap_err().into_kind() { + ErrorKind::ExpiredSignature => assert!(true), + kind => { + println!("Unexpected error: {:?}", kind); + assert!(false) + } + } + } +} -- 2.49.1 From 8b5ba9d48ccda3605bb99c7eb42e26f464f913da Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Thu, 18 Feb 2021 20:31:29 -0500 Subject: [PATCH 2/4] Move auth related methods to their own module --- src/auth.rs | 43 ++++++++++++++++++++++++++++++++ src/main.rs | 71 ++++++++++++++--------------------------------------- 2 files changed, 61 insertions(+), 53 deletions(-) create mode 100644 src/auth.rs diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..9cd1609 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,43 @@ +use actix_web::web::{HttpResponse, Json}; +use actix_web::{post, Responder}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{encode, EncodingKey, Header}; + +use crate::data::LoginRequest; +use crate::data::{secret_key, Claims, CreateAccountRequest, Token}; +use crate::database::{create_user, get_user, user_exists}; + +#[post("/register")] +async fn register(user: Json) -> impl Responder { + if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation { + if user_exists(&user.username) { + HttpResponse::BadRequest() + } else if let Some(_user) = create_user(&user.username, &user.password) { + HttpResponse::Ok() + } else { + HttpResponse::InternalServerError() + } + } else { + HttpResponse::BadRequest() + } +} + +#[post("/login")] +async fn login(creds: Json) -> impl Responder { + println!("Logging in: {}", creds.username); + if let Some(user) = get_user(&creds.username, &creds.password) { + let claims = Claims { + sub: user.id.to_string(), + exp: (Utc::now() + Duration::days(5)).timestamp(), + }; + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret_key().as_bytes()), + ) + .unwrap(); + HttpResponse::Ok().json(Token { token: &token }) + } else { + HttpResponse::NotFound().finish() + } +} diff --git a/src/main.rs b/src/main.rs index a7d9b09..de79c5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,71 +2,36 @@ extern crate diesel; extern crate rayon; +use crate::auth::login; +use futures::stream::StreamExt; use std::fs::File; use std::io::prelude::*; use std::path::{Path, PathBuf}; -use std::sync::{Arc}; use std::sync::mpsc::channel; +use std::sync::Arc; use actix::{Actor, Addr}; use actix_files::NamedFile; use actix_multipart as mp; -use actix_web::{App, get, HttpServer, post, Responder, web}; use actix_web::web::{HttpRequest, HttpResponse, Json}; -use chrono::{Duration, Utc}; -use futures::stream::StreamExt; -use jsonwebtoken::{encode, EncodingKey, Header}; -use notify::{DebouncedEvent, RecursiveMode, watcher, Watcher}; +use actix_web::{get, post, web, App, HttpServer, Responder}; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use rayon::prelude::*; use serde::Serialize; -use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest}; +use data::{AddFavoriteRequest, ThumbnailRequest}; -use crate::data::{Claims, CreateAccountRequest, secret_key, Token}; -use crate::database::{add_favorite, create_user, get_favorites, get_user, user_exists}; +use crate::data::Claims; +use crate::database::{add_favorite, get_favorites}; use crate::files::{is_valid_path, list_files}; use crate::video::*; +mod auth; mod data; mod database; mod files; mod video; -#[post("/register")] -async fn register(user: Json) -> impl Responder { - if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation { - if user_exists(&user.username) { - HttpResponse::BadRequest() - } else if let Some(_user) = create_user(&user.username, &user.password) { - HttpResponse::Ok() - } else { - HttpResponse::InternalServerError() - } - } else { - HttpResponse::BadRequest() - } -} - -#[post("/login")] -async fn login(creds: Json) -> impl Responder { - println!("Logging in: {}", creds.username); - if let Some(user) = get_user(&creds.username, &creds.password) { - let claims = Claims { - sub: user.id.to_string(), - exp: (Utc::now() + Duration::days(5)).timestamp(), - }; - let token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret_key().as_bytes()), - ) - .unwrap(); - HttpResponse::Ok().json(Token { token: &token }) - } else { - HttpResponse::NotFound().finish() - } -} - #[post("/photos")] async fn list_photos(_claims: Claims, req: Json) -> impl Responder { println!("{}", req.path); @@ -187,7 +152,8 @@ async fn generate_video( let playlist = format!("tmp/{}.m3u8", filename); if let Some(path) = is_valid_path(&body.path) { if let Ok(child) = create_playlist(&path.to_str().unwrap(), &playlist) { - data.stream_manager.do_send(ProcessMessage(playlist.clone(), child)); + data.stream_manager + .do_send(ProcessMessage(playlist.clone(), child)); } } else { return HttpResponse::BadRequest().finish(); @@ -291,7 +257,7 @@ fn create_thumbnails() { .filter(|entry| { let path = entry.path(); let relative_path = &path.strip_prefix(&images).unwrap(); - let thumb_path = Path::new(thumbnail_directory).join(relative_path); + let thumb_path = Path::new(thumbnail_directory).join(dbg!(relative_path)); !thumb_path.exists() }) .map(|entry| (image::open(entry.path()), entry.path().to_path_buf())) @@ -303,7 +269,7 @@ fn create_thumbnails() { let thumb_path = Path::new(thumbnail_directory).join(relative_path); std::fs::create_dir_all(&thumb_path.parent().unwrap()) .expect("There was an issue creating directory"); - println!("{:?}", thumb_path); + println!("Saving thumbnail: {:?}", thumb_path); image.save(thumb_path).expect("Failure saving thumbnail"); }) .for_each(drop); @@ -322,11 +288,10 @@ fn main() -> std::io::Result<()> { .unwrap(); loop { - let ev = wrx.recv_timeout(std::time::Duration::from_secs(10)); + let ev = wrx.recv(); if let Ok(event) = ev { match event { - DebouncedEvent::Create(_) => create_thumbnails(), - DebouncedEvent::Rename(_, _) => create_thumbnails(), + DebouncedEvent::Create(_) | DebouncedEvent::Rename(_, _) => create_thumbnails(), _ => continue, }; } @@ -353,9 +318,9 @@ fn main() -> std::io::Result<()> { .service(post_add_favorite) .app_data(app_data.clone()) }) - .bind(dotenv::var("BIND_URL").unwrap())? - .bind("localhost:8088")? - .run(); + .bind(dotenv::var("BIND_URL").unwrap())? + .bind("localhost:8088")? + .run(); system.run() } -- 2.49.1 From f9983240df1d2c668765978c92b19ce028aebba1 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Wed, 24 Feb 2021 21:26:11 -0500 Subject: [PATCH 3/4] Use log crate for logging instead of println --- Cargo.lock | 41 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ README.md | 1 + src/auth.rs | 3 ++- src/data/mod.rs | 5 +++-- src/main.rs | 44 +++++++++++++++++++++++++++++++++----------- src/video.rs | 7 ++++--- 7 files changed, 86 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97ac18c..7354095 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,6 +390,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -856,6 +867,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -1169,6 +1193,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "idna" version = "0.2.0" @@ -1213,10 +1243,12 @@ dependencies = [ "chrono", "diesel", "dotenv", + "env_logger", "futures", "hmac", "image", "jsonwebtoken", + "log", "notify", "path-absolutize", "rayon", @@ -2200,6 +2232,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index a1565e5..a52af6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,5 @@ rayon = "1.3" notify = "4.0" tokio = "0.2" path-absolutize = "3.0.6" +log="0.4" +env_logger="0.8" diff --git a/README.md b/README.md index f15cdcf..429e3d6 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,5 @@ They should be defined where the binary is located or above it in an `.env` file - `THUMBNAILS` is a path where generated thumbnails should be stored - `BIND_URL` is the url and port to bind to (typically your own IP address) - `SECRET_KEY` is the *hopefully* random string to sign Tokens with +- `RUST_LOG` is one of `off, error, warn, info, debug, trace`, from least to most noisy [error is default] diff --git a/src/auth.rs b/src/auth.rs index 9cd1609..818da18 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,6 +2,7 @@ use actix_web::web::{HttpResponse, Json}; use actix_web::{post, Responder}; use chrono::{Duration, Utc}; use jsonwebtoken::{encode, EncodingKey, Header}; +use log::debug; use crate::data::LoginRequest; use crate::data::{secret_key, Claims, CreateAccountRequest, Token}; @@ -24,7 +25,7 @@ async fn register(user: Json) -> impl Responder { #[post("/login")] async fn login(creds: Json) -> impl Responder { - println!("Logging in: {}", creds.username); + debug!("Logging in: {}", creds.username); if let Some(user) = get_user(&creds.username, &creds.password) { let claims = Claims { sub: user.id.to_string(), diff --git a/src/data/mod.rs b/src/data/mod.rs index d21cd6d..1aed598 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,5 +1,7 @@ use std::str::FromStr; +use log::error; + use actix_web::error::ErrorUnauthorized; use actix_web::{dev, http::header, Error, FromRequest, HttpRequest}; use futures::future::{err, ok, Ready}; @@ -21,7 +23,6 @@ pub fn secret_key() -> String { if cfg!(test) { String::from("test_key") } else { - println!("USING REAL KEY"); dotenv::var("SECRET_KEY").expect("SECRET_KEY env not set!") } } @@ -39,7 +40,7 @@ impl FromStr for Claims { ) { Ok(data) => Ok(data.claims), Err(other) => { - println!("DecodeError: {}", other); + error!("DecodeError: {}", other); Err(other) } } diff --git a/src/main.rs b/src/main.rs index de79c5c..b3035c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use rayon::prelude::*; use serde::Serialize; use data::{AddFavoriteRequest, ThumbnailRequest}; +use log::{debug, error, info}; use crate::data::Claims; use crate::database::{add_favorite, get_favorites}; @@ -34,7 +35,7 @@ mod video; #[post("/photos")] async fn list_photos(_claims: Claims, req: Json) -> impl Responder { - println!("{}", req.path); + info!("{}", req.path); let path = &req.path; if let Some(path) = is_valid_path(path) { @@ -54,6 +55,7 @@ async fn list_photos(_claims: Claims, req: Json) -> impl Respo HttpResponse::Ok().json(PhotosResponse { photos, dirs }) } else { + error!("Bad photos request: {}", req.path); HttpResponse::BadRequest().finish() } } @@ -78,7 +80,7 @@ async fn get_image( .expect("Error stripping prefix"); let thumb_path = Path::new(&thumbs).join(relative_path); - println!("{:?}", thumb_path); + debug!("{:?}", thumb_path); if let Ok(file) = NamedFile::open(&thumb_path) { file.into_response(&request).unwrap() } else { @@ -90,6 +92,7 @@ async fn get_image( HttpResponse::NotFound().finish() } } else { + error!("Bad photos request: {}", req.path); HttpResponse::BadRequest().finish() } } @@ -102,9 +105,9 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder { while let Some(Ok(mut part)) = payload.next().await { if let Some(content_type) = part.content_disposition() { - println!("{:?}", content_type); + debug!("{:?}", content_type); if let Some(filename) = content_type.get_filename() { - println!("Name: {:?}", filename); + debug!("Name: {:?}", filename); file_name = Some(filename.to_string()); while let Some(Ok(data)) = part.next().await { @@ -128,9 +131,11 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder { let mut file = File::create(full_path).unwrap(); file.write_all(&file_content).unwrap(); } else { + error!("File already exists: {:?}", full_path); return HttpResponse::BadRequest().body("File already exists"); } } else { + error!("Invalid path for upload: {:?}", full_path); return HttpResponse::BadRequest().body("Path was not valid"); } } else { @@ -161,6 +166,7 @@ async fn generate_video( HttpResponse::Ok().json(playlist) } else { + error!("Unable to get file name: {:?}", filename); HttpResponse::BadRequest().finish() } } @@ -172,7 +178,7 @@ async fn stream_video( path: web::Query, ) -> impl Responder { let playlist = &path.path; - println!("Playlist: {}", playlist); + debug!("Playlist: {}", playlist); // Extract video playlist dir to dotenv if !playlist.starts_with("tmp") && is_valid_path(playlist) != None { @@ -191,11 +197,12 @@ async fn get_video_part( path: web::Path, ) -> impl Responder { let part = &path.path; - println!("Video part: {}", part); + debug!("Video part: {}", part); if let Ok(file) = NamedFile::open(String::from("tmp/") + part) { file.into_response(&request).unwrap() } else { + error!("Video part not found: tmp/{}", part); HttpResponse::NotFound().finish() } } @@ -206,6 +213,7 @@ async fn favorites(claims: Claims) -> impl Responder { .into_iter() .map(|favorite| favorite.path) .collect::>(); + HttpResponse::Ok().json(PhotosResponse { photos: &favorites, dirs: &Vec::new(), @@ -216,8 +224,10 @@ async fn favorites(claims: Claims) -> impl Responder { async fn post_add_favorite(claims: Claims, body: web::Json) -> impl Responder { if let Ok(user_id) = claims.sub.parse::() { add_favorite(user_id, body.path.clone()); + debug!("Adding favorite \"{}\" for userid: {}", user_id, body.path); HttpResponse::Ok() } else { + error!("Unable to parse sub as i32: {}", claims.sub); HttpResponse::BadRequest() } } @@ -233,8 +243,9 @@ fn create_thumbnails() { .collect::>>() .into_par_iter() .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file()) .filter(|entry| { - println!("{:?}", entry.path()); + debug!("{:?}", entry.path()); if let Some(ext) = entry .path() .extension() @@ -245,23 +256,31 @@ fn create_thumbnails() { let thumb_path = Path::new(thumbnail_directory).join(relative_path); std::fs::create_dir_all(&thumb_path.parent().unwrap()) .expect("Error creating directory"); + + debug!("Generating video thumbnail: {:?}", thumb_path); generate_video_thumbnail(entry.path(), &thumb_path); false } else { ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "nef" } } else { + error!("Unable to get extension for file: {:?}", entry.path()); false } }) .filter(|entry| { let path = entry.path(); let relative_path = &path.strip_prefix(&images).unwrap(); - let thumb_path = Path::new(thumbnail_directory).join(dbg!(relative_path)); + let thumb_path = Path::new(thumbnail_directory).join(relative_path); !thumb_path.exists() }) .map(|entry| (image::open(entry.path()), entry.path().to_path_buf())) - .filter(|(img, _)| img.is_ok()) + .filter(|(img, _)| { + if let Err(e) = img { + error!("Unable to open image: {}", e); + } + img.is_ok() + }) .map(|(img, path)| (img.unwrap(), path)) .map(|(image, path)| (image.thumbnail(200, u32::MAX), path)) .map(|(image, path)| { @@ -269,15 +288,18 @@ fn create_thumbnails() { let thumb_path = Path::new(thumbnail_directory).join(relative_path); std::fs::create_dir_all(&thumb_path.parent().unwrap()) .expect("There was an issue creating directory"); - println!("Saving thumbnail: {:?}", thumb_path); + debug!("Saving thumbnail: {:?}", thumb_path); image.save(thumb_path).expect("Failure saving thumbnail"); }) .for_each(drop); - println!("Finished"); + debug!("Finished"); } fn main() -> std::io::Result<()> { + dotenv::dotenv().ok(); + env_logger::init(); + create_thumbnails(); std::thread::spawn(|| { diff --git a/src/video.rs b/src/video.rs index 5797103..4395e66 100644 --- a/src/video.rs +++ b/src/video.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::{Child, Command, ExitStatus, Stdio}; use actix::prelude::*; +use log::{debug, trace}; // ffmpeg -i test.mp4 -c:v h264 -flags +cgop -g 30 -hls_time 3 out.m3u8 // ffmpeg -i "filename.mp4" -preset veryfast -c:v libx264 -f hls -hls_list_size 100 -hls_time 2 -crf 24 -vf scale=1080:-2,setsar=1:1 attempt/vid_out.m3u8 @@ -23,11 +24,11 @@ impl Handler for StreamActor { type Result = Result; fn handle(&mut self, msg: ProcessMessage, _ctx: &mut Self::Context) -> Self::Result { - println!("Message received"); + trace!("Message received"); let mut process = msg.1; let result = process.wait(); - println!( + debug!( "Finished waiting for: {:?}. Code: {:?}", msg.0, result @@ -40,7 +41,7 @@ impl Handler for StreamActor { pub fn create_playlist(video_path: &str, playlist_file: &str) -> Result { if Path::new(playlist_file).exists() { - println!("Playlist already exists: {}", playlist_file); + debug!("Playlist already exists: {}", playlist_file); return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)); } -- 2.49.1 From e0d2a14d0f354c52032bbdcaffcf15765f576e33 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Wed, 24 Feb 2021 22:28:46 -0500 Subject: [PATCH 4/4] Report path when an image fails to open --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index b3035c3..3549230 100644 --- a/src/main.rs +++ b/src/main.rs @@ -275,9 +275,9 @@ fn create_thumbnails() { !thumb_path.exists() }) .map(|entry| (image::open(entry.path()), entry.path().to_path_buf())) - .filter(|(img, _)| { + .filter(|(img, path)| { if let Err(e) = img { - error!("Unable to open image: {}", e); + error!("Unable to open image: {:?}. {}", path, e); } img.is_ok() }) -- 2.49.1