diff --git a/Cargo.lock b/Cargo.lock index 3ba626f247e6d231df838d46c1a70c5d182c431e..6b0eac14803b6a6e18a700b923f21b7cbd099eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,6 +997,7 @@ dependencies = [ "env_logger", "fs_extra", "futures", + "rand", "regex", "rustls", "rustls-pemfile", diff --git a/Cargo.toml b/Cargo.toml index 1d59dbcb2a344c6c74fb5af601a84a469a16bcbc..c06876316e54b3546d8c24ae41cd477f179d5b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ actix-web-lab = "0.17.0" actix-files = "0.6.2" actix-multipart = "0.4" futures = "0.3.24" +rand = "0.8" diff --git a/default_static/script.js b/default_static/script.js index 2aff4460919b0fa7ed8f898d7f32ab00a2f4df9d..065f3cd0d05b7661f0b25a4b30f2910918436262 100644 --- a/default_static/script.js +++ b/default_static/script.js @@ -1 +1,10 @@ -console.log("Hello from default js") \ No newline at end of file +document.getElementById('admin-login-form').onsubmit = function (e) { + e.preventDefault(); + fetch('/admin/login', { method: 'POST', body: new URLSearchParams(new FormData(e.target)) }).then(res => { + if (res.status >= 200 && res.status < 400) { + console.log(res) + window.location = '/admin/auth/workspace'; + } + + }).catch(err => console.log(err)) +} \ No newline at end of file diff --git a/src/app/args.rs b/src/app/args.rs index 9f8a0d7742d3f8fb94bd23e6eba663c6229af737..8fa78ef3728913f442ba67d5a66db071e558a570 100644 --- a/src/app/args.rs +++ b/src/app/args.rs @@ -25,8 +25,11 @@ pub struct AppArgs { pub ssl_certs_dir: PathBuf, #[structopt(long = "adm", default_value = "admin")] - pub admin_id: String, + pub admin_username: String, #[structopt(long = "pwd", default_value = "password")] pub admin_pwd: String, + + #[structopt(long = "admin_cookie_name", default_value = "krustacea_admin_auth")] + pub admin_cookie_name: String, } diff --git a/src/app/config.rs b/src/app/config.rs index f4c026d066beaa3a65cf32c14651ff7b6364141d..1a031d4684555d7a4a024982518aed90c4c64b1f 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -18,8 +18,9 @@ pub struct AppConfig { pub port_tls: u16, pub load: Option<PathBuf>, pub ssl_certs_dir: PathBuf, - pub admin_id: String, + pub admin_username: String, pub admin_pwd: String, + pub admin_cookie_name: String, } impl AppConfig { @@ -59,8 +60,9 @@ impl AppConfig { port_tls: app_args.port_tls, load: app_args.load, ssl_certs_dir, - admin_id: app_args.admin_id, + admin_username: app_args.admin_username, admin_pwd: app_args.admin_pwd, + admin_cookie_name: app_args.admin_cookie_name, } } pub fn get_log_level(&self) -> String { diff --git a/src/app/state.rs b/src/app/state.rs index eb498c1c1e2c549dc7374756b14067e0e8f71af1..8b9fc0f660bf92c49a516e199f308bfa3f35c304 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,19 +1,56 @@ use crate::app::{AppArgs, AppConfig, AppPreferences}; +use rand::{distributions::Alphanumeric, Rng}; use structopt::StructOpt; #[derive(Clone)] pub struct AppState { pub config: AppConfig, pub preferences: AppPreferences, - // authentication - // ... + pub admin_auth_token: AdminAuthToken, } impl AppState { pub fn new() -> Self { + let config = AppConfig::new(AppArgs::from_args()); + let admin_cookie_name = config.admin_cookie_name.to_owned(); AppState { - config: AppConfig::new(AppArgs::from_args()), + config, preferences: AppPreferences {}, + admin_auth_token: AdminAuthToken::new(admin_cookie_name), } } } + +#[derive(Clone)] +pub struct AdminAuthToken { + pub value: Option<String>, + pub cookie_name: String, +} + +impl AdminAuthToken { + pub fn new(cookie_name: String) -> Self { + AdminAuthToken { + value: None, + cookie_name, + } + } + + pub fn match_value(&self, value: String) -> bool { + match &self.value { + Some(token) => token.eq(&value), + None => false, + } + } + + pub fn generate(&mut self) { + // Thanks to https://stackoverflow.com/questions/54275459/how-do-i-create-a-random-string-by-sampling-from-alphanumeric-characters#54277357 + self.value = Some( + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(36) + .map(char::from) + .collect::<String>() + .to_ascii_lowercase(), + ) + } +} diff --git a/src/main.rs b/src/main.rs index 3bf320a3a4ba4a48437166bae37cfb7efe126fbb..c302b94a9fb24167c65ea90649c5908626f86b7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,11 +14,6 @@ use static_files::StaticFilesManager; use tls_config::tls_config; use website::WebSiteBuilder; -#[actix_web::get("/admin")] -async fn test_unauthorized() -> impl actix_web::Responder { - actix_web::HttpResponse::Ok().finish() -} - #[actix_web::main] async fn main() -> std::io::Result<()> { let app_state = AppState::new(); @@ -38,9 +33,8 @@ async fn main() -> std::io::Result<()> { let srv_conf = tls_config(&app_state.config); let static_dir = website.static_files_manager.dir.clone(); - - let app_state = web::Data::new(std::sync::Mutex::new(app_state)); - + let mut_app_state = std::sync::Mutex::new(app_state); + let app_state = web::Data::new(mut_app_state); let mut_website = web::Data::new(std::sync::Mutex::new(website)); HttpServer::new(move || { @@ -53,10 +47,13 @@ async fn main() -> std::io::Result<()> { .service(Files::new("/static/", &static_dir)) .service( web::scope("/admin") - .wrap(AuthService { - token: String::from("abc"), - }) - .service(test_unauthorized), + .service(service::admin_login) + .service(service::admin_authenticate) + .service( + web::scope("/auth") + .wrap(AuthService {}) + .service(service::admin_workspace), + ), ) .service(service::files::favicon) .service(service::page) diff --git a/src/middleware/authentication.rs b/src/middleware/authentication.rs index 99e6fd1e3af4153d80bc331dad4880eccf92636e..8b4985491aeeac25c0ce9d74ea8a0bffae9bc490 100644 --- a/src/middleware/authentication.rs +++ b/src/middleware/authentication.rs @@ -1,3 +1,4 @@ +use crate::{app::AdminAuthToken, AppState}; use actix_web::{ body::{EitherBody, MessageBody}, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, @@ -7,9 +8,7 @@ use futures::prelude::future::LocalBoxFuture; use std::future::{ready, Ready}; #[derive(Clone)] -pub struct AuthService { - pub token: String, -} +pub struct AuthService; impl<S, B> Transform<S, ServiceRequest> for AuthService where @@ -25,20 +24,17 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(AuthenticatedMiddleware { service: std::rc::Rc::new(service), - auth: self.clone(), })) } } pub struct AuthenticatedMiddleware<S> { service: std::rc::Rc<S>, - auth: AuthService, } -async fn authenticate(req: &mut ServiceRequest, token: String) -> bool { - let cookie = req.cookie("auth"); - match cookie { - Some(cookie) => return cookie.value().to_string().eq(&token), +async fn authenticate(req: &mut ServiceRequest, token: &AdminAuthToken) -> bool { + match req.cookie(&token.cookie_name) { + Some(cookie) => token.match_value(cookie.value().to_string()), None => false, } } @@ -55,15 +51,23 @@ where forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { + let token = { + let app_state = req + .app_data::<actix_web::web::Data<std::sync::Mutex<AppState>>>() + .expect("Failed to extract AppState from ServiceRequest") + .lock() + .expect("Failed to lock AppState Mutex"); + app_state.admin_auth_token.clone() + }; + let service = self.service.clone(); - let token = self.auth.token.to_owned(); Box::pin(async move { let mut req = req; - if let false = authenticate(&mut req, token).await { + if let false = authenticate(&mut req, &token).await { return Ok(req.into_response( actix_web::HttpResponse::Unauthorized() - .finish() + .body("Error 401 - Unauthorized") // TODO a proper 401 view ? .map_into_right_body(), )); } diff --git a/src/service/admin.rs b/src/service/admin.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ed3bf0704d74df84c1449ef4a3a260a68430bb4 --- /dev/null +++ b/src/service/admin.rs @@ -0,0 +1,91 @@ +use crate::AppState; +use actix_web::{ + cookie::{time::Duration, Cookie, SameSite}, + get, post, + web::{Data, Form}, + HttpRequest, HttpResponse, Responder, +}; + +#[get("/workspace")] +async fn admin_workspace() -> impl Responder { + actix_web::HttpResponse::Ok().body("Welcome Admin") +} + +#[derive(serde::Deserialize)] +struct Credentials { + username: String, + password: String, +} + +#[post("/login")] +async fn admin_authenticate( + credentials: Form<Credentials>, + app_state: Data<std::sync::Mutex<AppState>>, + req: HttpRequest, +) -> impl Responder { + let (admin_username, admin_pwd, cookie_name) = { + let app_state = app_state.lock().unwrap(); + ( + app_state.config.admin_username.to_owned(), + app_state.config.admin_pwd.to_owned(), + app_state.config.admin_cookie_name.to_owned(), + ) + }; + + if admin_username.eq(&credentials.username) && admin_pwd.eq(&credentials.password) { + let cookie_value = { + let mut app_state = app_state.lock().unwrap(); + app_state.admin_auth_token.generate(); + app_state + .admin_auth_token + .value + .as_ref() + .unwrap() + .to_owned() + }; + + let cookie = Cookie::build(cookie_name, cookie_value) + .path("/") + .http_only(true) + .max_age(Duration::days(7)) + .same_site(SameSite::Strict) + .secure(true) + .finish(); + return HttpResponse::Accepted().cookie(cookie).finish(); + } else { + let mut res = HttpResponse::Unauthorized().finish(); + match req.cookie(&cookie_name) { + Some(_) => { + res.del_cookie(&cookie_name); + return res; + } + None => return res, + } + } +} + +#[get("/login")] +pub async fn admin_login() -> impl Responder { + // TODO real HTML doc - create a module with admin views and a js file to load. + HttpResponse::Ok().body( + " +<form id='admin-login-form'> + <input type='text' name='username'/> + <input type='password' name='password' /> + <input type='submit' /> +</form> +<script> +document.getElementById('admin-login-form').onsubmit = function (e) { + e.preventDefault(); + fetch('/admin/login', { method: 'POST', body: new URLSearchParams(new FormData(e.target)) }).then(res => { + if (res.status >= 200 && res.status < 400) { + console.log(res) + window.location = '/admin/auth/workspace'; + } + + }).catch(err => console.log(err)) +} +</script> +", + ) +} diff --git a/src/service/mod.rs b/src/service/mod.rs index b27ee732153f842b45a10dd99898468a3e696b8d..61f75aee4dccd0132a927a8ab004bde5b73b488d 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,3 +1,5 @@ +mod admin; pub mod files; mod page; +pub use admin::*; pub use page::*;