diff --git a/Cargo.lock b/Cargo.lock index 3052e80..7878924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + [[package]] name = "async-trait" version = "0.1.50" @@ -1156,6 +1162,7 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-prom", + "anyhow", "bcrypt", "chrono", "diesel", diff --git a/Cargo.toml b/Cargo.toml index 6e95091..7ff382e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,4 @@ env_logger="0.8" actix-web-prom = "0.5.1" prometheus = "0.11" lazy_static = "1.1" +anyhow = "1.0" diff --git a/src/auth.rs b/src/auth.rs index d5fac85..5be9209 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -32,6 +32,7 @@ pub async fn login( user_dao: web::Data>, ) -> HttpResponse { debug!("Logging in: {}", creds.username); + if let Some(user) = user_dao.get_user(&creds.username, &creds.password) { let claims = Claims { sub: user.id.to_string(), @@ -43,6 +44,7 @@ pub async fn login( &EncodingKey::from_secret(secret_key().as_bytes()), ) .unwrap(); + HttpResponse::Ok().json(Token { token: &token }) } else { error!( @@ -56,7 +58,7 @@ pub async fn login( #[cfg(test)] mod tests { use super::*; - use crate::database::testhelpers::{BodyReader, TestUserDao}; + use crate::testhelpers::{BodyReader, TestUserDao}; #[actix_rt::test] async fn test_login_reports_200_when_user_exists() { diff --git a/src/data/mod.rs b/src/data/mod.rs index 98e40ac..cdeb4ae 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -35,7 +35,7 @@ impl FromStr for Claims { let token = *(s.split("Bearer ").collect::>().last().unwrap_or(&"")); match decode::( - &token, + token, &DecodingKey::from_secret(secret_key().as_bytes()), &Validation::new(Algorithm::HS256), ) { @@ -54,18 +54,27 @@ impl FromRequest for Claims { type Config = (); fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future { - let claims = match req.headers().get(header::AUTHORIZATION) { - Some(header) => Claims::from_str(header.to_str().unwrap_or("")), - None => Err(jsonwebtoken::errors::Error::from( - jsonwebtoken::errors::ErrorKind::InvalidToken, - )), - }; - - if let Ok(claims) = claims { - ok(claims) - } else { - err(ErrorUnauthorized("Bad token")) - } + req.headers() + .get(header::AUTHORIZATION) + .map_or_else( + || Err(anyhow!("No authorization header")), + |header| { + header + .to_str() + .context("Unable to read Authorization header to string") + }, + ) + .and_then(|header| { + Claims::from_str(header) + .with_context(|| format!("Unable to decode token from: {}", header)) + }) + .map_or_else( + |e| { + error!("{}", e); + err(ErrorUnauthorized("Bad token")) + }, + ok, + ) } } @@ -156,4 +165,17 @@ mod tests { } } } + + #[test] + fn test_junk_token_is_invalid() { + let err = Claims::from_str("uni-֍ՓՓՓՓՓՓՓՓՓՓՓՓՓՓՓ"); + + match err.unwrap_err().into_kind() { + ErrorKind::InvalidToken => assert!(true), + kind => { + println!("Unexpected error: {:?}", kind); + assert!(false) + } + } + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index e29ba6b..aac67ce 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,7 +8,7 @@ use std::{ use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; -mod models; +pub mod models; mod schema; pub trait UserDao { @@ -141,7 +141,7 @@ impl FavoriteDao for SqliteFavoriteDao { diesel::insert_into(favorites) .values(InsertFavorite { userid: &user_id, - path: &favorite_path, + path: favorite_path, }) .execute(connection) .map_err(|_| DbError::new(DbErrorKind::InsertError)) @@ -168,74 +168,3 @@ impl FavoriteDao for SqliteFavoriteDao { .map_err(|_| DbError::new(DbErrorKind::QueryError)) } } - -#[cfg(test)] -pub mod testhelpers { - use actix_web::dev::{Body, ResponseBody}; - - use super::{models::User, UserDao}; - use std::cell::RefCell; - use std::option::Option; - - pub struct TestUserDao { - pub user_map: RefCell>, - } - - impl TestUserDao { - pub fn new() -> Self { - Self { - user_map: RefCell::new(Vec::new()), - } - } - } - - impl UserDao for TestUserDao { - fn create_user(&self, username: &str, password: &str) -> Option { - let u = User { - id: (self.user_map.borrow().len() + 1) as i32, - username: username.to_string(), - password: password.to_string(), - }; - - self.user_map.borrow_mut().push(u.clone()); - - Some(u) - } - - fn get_user(&self, user: &str, pass: &str) -> Option { - match self - .user_map - .borrow() - .iter() - .find(|&u| u.username == user && u.password == pass) - { - Some(u) => { - let copy = (*u).clone(); - Some(copy) - } - None => None, - } - } - - fn user_exists(&self, user: &str) -> bool { - self.user_map - .borrow() - .iter() - .find(|&u| u.username == user) - .is_some() - } - } - - pub trait BodyReader { - fn read_to_str(&self) -> &str; - } - - impl BodyReader for ResponseBody { - fn read_to_str(&self) -> &str { - match self { - ResponseBody::Body(Body::Bytes(ref b)) => std::str::from_utf8(b).unwrap(), - _ => panic!("Unknown response body"), - } - } - } -} diff --git a/src/main.rs b/src/main.rs index d41bf71..0746183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,9 @@ mod database; mod files; mod video; +#[cfg(test)] +mod testhelpers; + lazy_static! { static ref IMAGE_GAUGE: IntGauge = IntGauge::new( "imageserver_image_total", @@ -190,7 +193,7 @@ async fn generate_video( let filename = name.to_str().expect("Filename should convert to string"); 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).await { + if let Ok(child) = create_playlist(path.to_str().unwrap(), &playlist).await { data.stream_manager .do_send(ProcessMessage(playlist.clone(), child)); } diff --git a/src/testhelpers.rs b/src/testhelpers.rs new file mode 100644 index 0000000..3ba990f --- /dev/null +++ b/src/testhelpers.rs @@ -0,0 +1,86 @@ +use actix_web::dev::{Body, ResponseBody}; +use serde::Deserialize; + +use crate::database::{models::User, UserDao}; +use std::cell::RefCell; +use std::option::Option; + +pub struct TestUserDao { + pub user_map: RefCell>, +} + +impl TestUserDao { + pub fn new() -> Self { + Self { + user_map: RefCell::new(Vec::new()), + } + } +} + +impl UserDao for TestUserDao { + fn create_user(&self, username: &str, password: &str) -> Option { + let u = User { + id: (self.user_map.borrow().len() + 1) as i32, + username: username.to_string(), + password: password.to_string(), + }; + + self.user_map.borrow_mut().push(u.clone()); + + Some(u) + } + + fn get_user(&self, user: &str, pass: &str) -> Option { + match self + .user_map + .borrow() + .iter() + .find(|&u| u.username == user && u.password == pass) + { + Some(u) => { + let copy = (*u).clone(); + Some(copy) + } + None => None, + } + } + + fn user_exists(&self, user: &str) -> bool { + self.user_map + .borrow() + .iter() + .find(|&u| u.username == user) + .is_some() + } +} + +pub trait BodyReader { + fn read_to_str(&self) -> &str; +} + +impl BodyReader for ResponseBody { + fn read_to_str(&self) -> &str { + match self { + ResponseBody::Body(Body::Bytes(ref b)) => std::str::from_utf8(b).unwrap(), + _ => panic!("Unknown response body"), + } + } +} + +pub trait TypedBodyReader<'a, T> +where + T: Deserialize<'a>, +{ + fn read_body(&'a self) -> T; +} + +impl<'a, T: Deserialize<'a>> TypedBodyReader<'a, T> for ResponseBody { + fn read_body(&'a self) -> T { + match self { + ResponseBody::Body(Body::Bytes(ref b)) => { + serde_json::from_str(std::str::from_utf8(b).unwrap()).unwrap() + } + _ => panic!("Unknown response body"), + } + } +}