use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use bytes::{Buf, Bytes}; use chrono::{DateTime, Days, NaiveTime, Utc}; #[cfg(target_os = "linux")] use daemonize::Daemonize; use http_body_util::{BodyExt, Full}; use hyper::{ body::{Body as HyperBody, Frame, Incoming}, header::{COOKIE, SET_COOKIE}, server::conn::http1, service::service_fn, Error, Method, Request, Response, StatusCode, }; use hyper_util::rt::{TokioIo, TokioTimer}; use rand::distributions::{Alphanumeric, DistString}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{from_reader, Value}; use sqlx::sqlite::SqlitePool; use std::collections::HashMap; use std::convert::Infallible; use std::env; use std::fs::File; use std::io::Read; use std::net::SocketAddr; use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::time::SystemTime; use tokio::net::TcpListener; enum Body { Full(Full), Empty, } impl Body { fn new(data: T) -> Self where Bytes: From, { Body::Full(Full::new(Bytes::from(data))) } } impl HyperBody for Body { type Data = Bytes; type Error = Infallible; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { match &mut *self.get_mut() { Self::Full(incoming) => Pin::new(incoming).poll_frame(cx), Self::Empty => Poll::Ready(None), } } } #[derive(Serialize, Deserialize)] struct Player { id: i64, name: String, } #[derive(Serialize, Deserialize, Debug)] struct Vote { id: Option, submit_date: Option, plus_player_id: i64, plus_nickname: String, plus_reason: String, minus_player_id: i64, minus_nickname: String, minus_reason: String, } #[derive(Serialize, Deserialize)] struct User { username: String, saltyhash: String, permissions: i64, token: String, } #[derive(Serialize, Deserialize)] struct Login { username: String, password: String, } #[derive(Serialize, Deserialize)] struct Settings { database_url: String, bind_address: String, } async fn service( req: Request, db: Arc>, ) -> Result, Error> { match *req.method() { Method::GET => get(req, db).await, Method::POST => post(req, db).await, _ => Ok(Response::builder() .status(StatusCode::IM_A_TEAPOT) .body(Body::Empty) .unwrap()), } } async fn get(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); if path.starts_with("/static") { get_file(path).await } else if path.starts_with("/data") { get_data(path, &req, db).await } else if path.starts_with("/admin") { get_admin(&req, path, db).await } else { get_page(&req, path, db).await } } async fn get_page( req: &Request, path: &str, db: Arc>, ) -> Result, Error> { let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); routes.push("routes.json"); let file = File::open(routes).expect("Could not open routes file."); let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); match map.get(path) { Some(Value::Object(s)) => { let authorised = is_authorised( req, db, s.get("permission").unwrap().as_u64().unwrap() as u8, ) .await; if authorised { get_file(s.get("file").unwrap().as_str().unwrap()).await } else { get_file( map.get("/unauthorised") .unwrap() .get("file") .unwrap() .as_str() .unwrap(), ) .await } } _ => not_found().await, } } async fn get_file(mut path: &str) -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); if path.starts_with(r"/") { path = path.strip_prefix(r"/").unwrap(); } file_path.push(path); match File::open(&file_path) { Ok(mut file) => { let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); let content_type = match file_path.extension().unwrap().to_str().unwrap() { "js" => "text/javascript", "html" => "text/html", "css" => "text/css", _ => "", }; Ok(Response::builder() .header("content-type", content_type) .body(Body::new(buf)) .unwrap()) } Err(_) => not_found().await, } } async fn get_data( path: &str, req: &Request, db: Arc>, ) -> Result, Error> { let pool = db.clone().lock().unwrap().clone(); match path { "/data/players" => { let items = sqlx::query!(r#"SELECT * FROM players"#) .fetch_all(&pool) .await .unwrap(); let players: Vec = items .iter() .map(|x| Player { id: x.id, name: x.name.clone(), }) .collect(); Ok(Response::new(Body::new( serde_json::to_string(&players).unwrap(), ))) } "/data/votes" => { let votes = get_votes(req, db).await; Ok(Response::new(Body::new( serde_json::to_string(&votes).unwrap(), ))) } "/data/results" => { let votes = get_votes(req, db).await; let ids: Vec<(i64, i64)> = votes .iter() .map(|x| (x.plus_player_id, x.minus_player_id)) .collect(); let mut plus_results: HashMap = HashMap::new(); let mut minus_results: HashMap = HashMap::new(); ids.iter().for_each(|x| { let plus_id = x.0; plus_results.entry(plus_id).or_insert(0); *plus_results.get_mut(&plus_id).unwrap() += 1; let minus_id = x.1; minus_results.entry(minus_id).or_insert(0); *minus_results.get_mut(&minus_id).unwrap() += 1; }); let mut plus_results: Vec<(i64, i64)> = plus_results.into_iter().collect(); let mut minus_results: Vec<(i64, i64)> = minus_results.into_iter().collect(); plus_results.sort_by(|a, b| b.1.cmp(&a.1)); minus_results.sort_by(|a, b| b.1.cmp(&a.1)); let sorted_results = vec![plus_results, minus_results]; Ok(Response::new(Body::new( serde_json::to_string(&sorted_results).unwrap(), ))) } _ => not_found().await, } } async fn get_votes(req: &Request, db: Arc>) -> Vec { let pool = db.clone().lock().unwrap().clone(); let headers = req.headers(); let date = match headers.get("Date-to-fetch") { Some(date) => { let date = date.to_str().unwrap(); if let Ok(parsed_date) = date.parse::() { DateTime::from_timestamp_millis(parsed_date) } else { None } } None => Some(DateTime::from(SystemTime::now())), }; if date.is_none() { return Vec::new(); } let formatted_date = format!("{}", date.unwrap().format("%d/%m/%Y")); sqlx::query_as!( Vote, r#"SELECT * FROM votes WHERE submit_date = ?1 ORDER BY id"#, formatted_date ) .fetch_all(&pool) .await .unwrap() } async fn get_admin( req: &Request, path: &str, db: Arc>, ) -> Result, Error> { let authorised = is_authorised(req, db.clone(), 3).await; if !authorised { return get_page(req, "/unauthorised", db).await; } if path.starts_with("/admin/data") { let path = path.strip_prefix("/admin/data").unwrap(); match path { "/users" => { let pool = db.clone().lock().unwrap().clone(); let users = sqlx::query!(r#"SELECT id, username, permissions FROM users"#) .fetch_all(&pool) .await .unwrap(); let users: Vec<(i64, String, i64)> = users .iter() .map(|x| (x.id, x.username.clone(), x.permissions)) .collect(); let stringed = serde_json::to_string(&users).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } "/players" => { let pool = db.clone().lock().unwrap().clone(); let players = sqlx::query_as!(Player, r#"SELECT id, name FROM players"#) .fetch_all(&pool) .await .unwrap(); let stringed = serde_json::to_string(&players).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } "/votes" => { let pool = db.clone().lock().unwrap().clone(); let votes = sqlx::query_as!(Vote, r#"SELECT * FROM votes"#) .fetch_all(&pool) .await .unwrap(); let stringed = serde_json::to_string(&votes).unwrap_or("".to_string()); Ok(Response::builder().body(Body::new(stringed)).unwrap()) } _ => not_found().await, }} else if path.starts_with("/admin") {get_page(req, path, db).await} else {not_found().await} } async fn post(req: Request, db: Arc>) -> Result, Error> { let path = req.uri().path(); if path.starts_with("/admin") { return post_admin(req, db).await; } match path { "/vote" => post_vote(req, db).await, "/login" => login(req, db).await, "/register" => register(req, db).await, "/logout" => logout().await, "/player" => post_player(req, db).await, _ => not_found().await, } } async fn post_vote( req: Request, db: Arc>, ) -> Result, Error> { let data = req_json::(req).await; if data.is_none() { return bad_request().await; } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); let formatted = timestamp.format("%d/%m/%Y").to_string(); let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let result = sqlx::query!(r#"INSERT INTO votes ( plus_player_id, plus_nickname, plus_reason, minus_player_id, minus_nickname, minus_reason, submit_date ) values ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 )"#, vote.plus_player_id, vote.plus_nickname, vote.plus_reason, vote.minus_player_id, vote.minus_nickname, vote.minus_reason, formatted).execute(&mut *conn).await; if result.is_err() { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()); } let date: DateTime = DateTime::from(SystemTime::now()); let date = date.checked_add_days(Days::new(1)).unwrap(); let date = date .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) .unwrap(); Ok(Response::builder() .header( SET_COOKIE, format!( "hasvoted=true; Expires={}; Secure; SameSite=Strict", date.to_rfc2822() ), ) .body(Body::Empty) .unwrap()) } async fn post_player( req: Request, db: Arc>, ) -> Result, Error> { let data = req_json::(req).await; if data.is_none() { return bad_request().await; } let data = data.unwrap(); let name = data.get("name").unwrap().as_str().unwrap(); let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); if let Ok(Some(player)) = sqlx::query!(r#"SELECT * FROM players WHERE name = ?1"#, name) .fetch_optional(&pool) .await { let player = Player { id: player.id, name: player.name, }; return Ok(Response::builder() .body(Body::new(serde_json::to_string(&player).unwrap())) .unwrap()); } let r = sqlx::query!( r#"INSERT INTO players (name) VALUES (?1) RETURNING id"#, name ) .fetch_one(&mut *conn) .await; if r.is_err() { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()); } let player = Player { id: r.unwrap().id, name: name.to_string(), }; Ok(Response::builder() .body(Body::new(serde_json::to_string(&player).unwrap())) .unwrap()) } async fn post_admin( req: Request, db: Arc>, ) -> Result, Error> { let authorised = is_authorised(&req, db.clone(), 3).await; if !authorised { return get_page(&req, "/unauthorised", db).await; } let path = req.uri().path(); match path { "/admin/edit/user" => match req_json::(req).await { Some(Value::Object(user)) => { let username = user.get("username"); let permissions = user.get("permissions"); let id = user.get("id"); if username.is_none() || permissions.is_none() || id.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let username = username.unwrap().as_str().unwrap(); let permissions = permissions.unwrap(); let id = id.unwrap(); let _ = sqlx::query!( r#"UPDATE users SET username = ?1, permissions = ?2 WHERE id = ?3"#, username, permissions, id ) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, "/admin/delete/user" => match req_json::(req).await { Some(Value::Object(user)) => { let id = user.get("id"); if id.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let id = id.unwrap().as_i64().unwrap(); let _ = sqlx::query!(r#"DELETE FROM users WHERE id = ?1"#, id) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, "/admin/edit/player" => match req_json::(req).await { Some(player) => { let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let _ = sqlx::query!( r#"UPDATE players SET name = ?1 WHERE id = ?2"#, player.name, player.id ) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, "/admin/new/player" => match req_json::(req).await { Some(Value::Object(player)) => { let name = player.get("name"); if name.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let name = name.unwrap().as_str().unwrap(); let _ = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, name) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, "/admin/delete/player" => match req_json::(req).await { Some(Value::Object(player)) => { let id = player.get("id"); if id.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let id = id.unwrap().as_i64().unwrap(); let _ = sqlx::query!(r#"DELETE FROM players WHERE id = ?1"#, id) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, "/admin/edit/vote" => match req_json::(req).await { Some(vote) => { if vote.id.is_none() || vote.submit_date.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let _ = sqlx::query!( r#"UPDATE votes SET submit_date = ?1, plus_player_id = ?2, plus_nickname = ?3, plus_reason = ?4, minus_player_id = ?5, minus_nickname = ?6, minus_reason = ?7 WHERE id = ?8"#, vote.submit_date, vote.plus_player_id, vote.plus_nickname, vote.plus_reason, vote.minus_player_id, vote.minus_nickname, vote.minus_reason, vote.id ) .execute(&pool) .await; ok().await } _ => bad_request().await, }, "/admin/delete/vote" => match req_json::(req).await { Some(Value::Object(vote)) => { let id = vote.get("id"); if id.is_none() { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let id = id.unwrap().as_i64().unwrap(); let _ = sqlx::query!(r#"DELETE FROM votes WHERE id = ?1"#, id) .execute(&mut *conn) .await; ok().await } _ => bad_request().await, }, _ => bad_request().await, } } async fn login( req: Request, db: Arc>, ) -> Result, Error> { let body = req.into_body().collect().await; let data: Result = from_reader(body?.aggregate().reader()); if data.is_err() { return bad_request().await; } let data = data.unwrap(); if !check_username(&data.username) { return bad_request().await; } let pool = db.clone().lock().unwrap().clone(); let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username) .fetch_optional(&pool) .await; match result { Ok(Some(user)) => { let argon = Argon2::default(); let hash = PasswordHash::new(&user.saltyhash).unwrap(); match argon.verify_password(data.password.as_bytes(), &hash) { Ok(()) => { let date: DateTime = DateTime::from(SystemTime::now()); let date = date.checked_add_days(Days::new(7)).unwrap(); Ok(Response::builder() .header( SET_COOKIE, format!( "token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822() ), ) .header( SET_COOKIE, format!( "logged=true; Expires={}; Secure; SameSite=Strict", date.to_rfc2822() ), ) .body(Body::Empty) .unwrap()) } Err(_) => bad_request().await, } } Ok(None) => bad_request().await, Err(_) => Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()), } } async fn register( req: Request, db: Arc>, ) -> Result, Error> { match req_json::(req).await { None => Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::Empty) .unwrap()), Some(login) => { if !check_username(&login.username) { return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::Empty) .unwrap()); } if !check_password(&login.password) { return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::Empty) .unwrap()); } let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, login.username) .fetch_optional(&mut *conn) .await; if exists.unwrap().is_some() { return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::Empty) .unwrap()); } let argon2 = Argon2::default(); let hash = argon2 .hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng)) .unwrap() .to_string(); let mut token = Alphanumeric.sample_string(&mut OsRng, 256); while let Ok(Some(_)) = sqlx::query!(r#"SELECT id FROM users WHERE token=?1"#, token) .fetch_optional(&mut *conn) .await { token = Alphanumeric.sample_string(&mut OsRng, 256); } let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await; match result { Ok(_) => Ok(Response::builder().body(Body::Empty).unwrap()), Err(_) => Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::Empty) .unwrap()), } } } } async fn logout() -> Result, Error> { let date: DateTime = DateTime::from(SystemTime::now()); Ok(Response::builder() .header( SET_COOKIE, format!( "token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822() ), ) .header( SET_COOKIE, format!( "logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822() ), ) .body(Body::Empty) .unwrap()) } async fn is_authorised(req: &Request, db: Arc>, level: u8) -> bool { let cookies = req.headers().get(COOKIE); let token = match cookies { Some(cookies) => cookies .to_str() .unwrap_or("") .split("; ") .find(|x| x.starts_with("token=")) .unwrap_or("") .strip_prefix("token=") .unwrap_or(""), None => "", }; let pool = db.clone().lock().unwrap().clone(); let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token) .fetch_optional(&pool) .await; match user { Ok(Some(user)) => { let perm = user.permissions as u8; perm >= level } _ => matches!(level, 0), } } fn check_username(username: &str) -> bool { if username.len() > 21 { return false; } for x in username.chars() { if !x.is_ascii_alphanumeric() { return false; } } true } fn check_password(password: &str) -> bool { // one symbol, 10 chars min, one capital letter, one number if password.len() < 10 { return false; } let mut up = false; let mut num = false; let mut sym = false; for c in password.chars() { if c.is_uppercase() { up = true; } if c.is_numeric() { num = true; } if !c.is_alphanumeric() { sym = true; } } up && num && sym } async fn not_found() -> Result, Error> { let mut file_path = env::current_dir().expect("Could not get app directory."); file_path.push("static/html/404.html"); let mut file = File::open(file_path).unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::new(buf)) .unwrap()) } async fn bad_request() -> Result, Error> { Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::Empty) .unwrap()) } async fn ok() -> Result, Error> { Ok(Response::builder().body(Body::Empty).unwrap()) } async fn req_json(req: Request) -> Option where T: DeserializeOwned, { let body = req.into_body().collect().await.unwrap(); match from_reader(body.aggregate().reader()) { Ok(val) => Some(val), Err(_) => None, } } fn get_settings() -> Settings { let mut settings_path = env::current_dir().expect("Could not get app directory. (Required to read settings)"); settings_path.push("settings.json"); let settings_file = File::open(settings_path).expect("Could not open settings file, does it exists?"); let settings: Settings = from_reader(settings_file).expect("Could not parse settings, please check syntax."); settings } #[cfg(target_os = "linux")] fn main() { let current_directory = env::current_dir().expect("Could not get app directory."); let stdout = File::create("/var/vote/log/daemon.out").unwrap(); let stderr = File::create("/var/vote/log/daemon.err").unwrap(); let daemonize = Daemonize::new() .pid_file("/var/vote/server.pid") .working_directory(current_directory) .stdout(stdout) .stderr(stderr); match daemonize.start() { Ok(_) => run(), Err(e) => eprintln!("Error, {}", e), } } #[cfg(target_os = "windows")] fn main() { run(); } #[tokio::main] async fn run() { let settings = get_settings(); let db_pool = Arc::new(Mutex::new( SqlitePool::connect(&settings.database_url) .await .expect("Could not connect to database. Make sure the url is correct."), )); let bind_address: SocketAddr = SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address."); let listener = TcpListener::bind(bind_address) .await .expect("Could not bind to address."); loop { let (stream, _) = listener .accept() .await .expect("Could not accept incoming stream."); let io = TokioIo::new(stream); let db = db_pool.clone(); let service = service_fn(move |req| service(req, db.clone())); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new() .timer(TokioTimer::new()) .serve_connection(io, service) .await { println!("Failed to serve connection: {:?}", err); } }); } }