#![cfg_attr(feature = "strict", deny(warnings))] #[macro_use] extern crate iron; #[macro_use] extern crate router; #[macro_use] extern crate serde_derive; extern crate handlebars_iron; extern crate iron_sessionstorage; extern crate mount; extern crate params; extern crate persistent; #[cfg(feature = "postgres")] extern crate postgres; extern crate rand; extern crate reqwest; #[cfg(feature = "rusqlite")] extern crate rusqlite; extern crate serde_json; extern crate serde_yaml; extern crate staticfile; extern crate structopt; extern crate time; extern crate urlencoded; #[cfg(feature = "webbrowser")] extern crate webbrowser; pub mod config; pub mod contestreader_yaml; pub mod db_conn; pub mod functions; pub mod oauth_provider; mod db_apply_migrations; mod db_conn_postgres; mod db_conn_sqlite; mod db_objects; mod webfw_iron; use db_conn::{MedalConnection, MedalObject}; use db_objects::*; use functions::SetPassword; // TODO: Refactor, so we don't need to take this from there! use webfw_iron::start_server; use config::Config; use structopt::StructOpt; use std::path::{Path, PathBuf}; fn read_contest(p: &PathBuf) -> Option { use std::fs::File; use std::io::Read; let mut file = File::open(p).unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); contestreader_yaml::parse_yaml(&contents, p.file_name().to_owned()?.to_str()?, &format!("{}/", p.parent().unwrap().to_str()?)) } fn get_all_contest_info(task_dir: &str) -> Vec { fn walk_me_recursively(p: &PathBuf, contests: &mut Vec) { if let Ok(paths) = std::fs::read_dir(p) { for path in paths { let p = path.unwrap().path(); walk_me_recursively(&p, contests); } } if p.file_name().unwrap().to_string_lossy().to_string().ends_with(".yaml") { read_contest(p).map(|contest| contests.push(contest)); }; }; let mut contests = Vec::new(); match std::fs::read_dir(task_dir) { Err(why) => println!("Error opening tasks directory! {:?}", why.kind()), Ok(paths) => { for path in paths { walk_me_recursively(&path.unwrap().path(), &mut contests); } } }; contests } fn refresh_all_contests(conn: &mut C) where C: MedalConnection, db_objects::Contest: db_conn::MedalObject { let v = get_all_contest_info("tasks/"); for mut contest_info in v { contest_info.save(conn); } } fn add_admin_user(conn: &mut C, resetpw: bool) where C: MedalConnection { let mut admin = match conn.get_user_by_id(1) { None => { print!("New Database. Creating new admin user with credentials 'admin':"); conn.new_session("") } Some(user) => { if !resetpw { return; } print!("Request to reset admin password. Set credentials 'admin':"); user } }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; let password: String = thread_rng().sample_iter(&Alphanumeric) .filter(|x| { let x = *x; !(x == 'l' || x == 'I' || x == '1' || x == 'O' || x == 'o' || x == '0') }) .take(8) .collect(); print!("'{}' …", &password); admin.username = Some("admin".into()); match admin.set_password(&password) { None => println!(" FAILED! (Password hashing error)"), _ => { conn.save_session(admin); println!(" Done"); } } } fn prepare_and_start_server(mut conn: C, config: Config, onlycontestscan: bool, resetadminpw: bool) where C: MedalConnection + std::marker::Send + 'static, db_objects::Contest: db_conn::MedalObject { db_apply_migrations::test(&mut conn); if onlycontestscan || config.no_contest_scan == Some(false) { print!("Scanning for contests …"); refresh_all_contests(&mut conn); println!(" Done") } if !onlycontestscan { add_admin_user(&mut conn, resetadminpw); #[cfg(feature = "webbrowser")] let self_url = config.self_url.clone(); #[cfg(feature = "webbrowser")] let open_browser = config.open_browser; match start_server(conn, config) { Ok(_) => { println!("Server started"); #[cfg(feature = "webbrowser")] { if let (Some(self_url), Some(true)) = (self_url, open_browser) { open_browser_window(&self_url); } } } Err(_) => println!("Error on server start …"), }; println!("Could not run server. Is the port already in use?"); } } #[cfg(feature = "webbrowser")] fn open_browser_window(self_url: &str) { match webbrowser::open(&self_url) { Ok(_) => (), Err(e) => println!("Error while opening webbrowser: {:?}", e), } } fn main() { let opt = config::Opt::from_args(); //println!("{:?}", opt); // Show in different debug level? let mut config = config::read_config_from_file(&opt.configfile); if opt.databasefile.is_some() { config.database_file = opt.databasefile; } if config.database_file.is_none() { config.database_file = Some(Path::new("medal.db").to_owned()) } if opt.databaseurl.is_some() { config.database_url = opt.databaseurl; } if opt.port.is_some() { config.port = opt.port; } if opt.nocontestscan { config.no_contest_scan = Some(true); } if opt.openbrowser { config.open_browser = Some(true) } #[cfg(feature = "postgres")] { if let Some(url) = config.database_url.clone() { print!("Using database {} … ", &url); let conn = postgres::Connection::connect(url, postgres::TlsMode::None).unwrap(); println!("Connected"); prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw); return; } } #[cfg(feature = "rusqlite")] { if let Some(path) = config.database_file.clone() { print!("Using database file {} … ", &path.to_str().unwrap_or("")); let conn = rusqlite::Connection::open(path).unwrap(); println!("Connected"); prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw); return; } } println!("No database configured. Try enableing the 'rusqlite' feature during compilation.\nLeaving now."); } #[cfg(test)] mod tests { use super::*; use reqwest::StatusCode; fn start_server_and_fn(port: u16, set_user: Option<(String, String, bool)>, f: F) where F: Fn() { use std::sync::mpsc::channel; use std::{thread, time}; let (start_tx, start_rx) = channel(); let (stop_tx, stop_rx) = channel(); thread::spawn(move || { let mut conn = rusqlite::Connection::open_in_memory().unwrap(); db_apply_migrations::test(&mut conn); if let Some(user) = set_user { let mut test_user = conn.new_session(""); test_user.username = Some(user.0); test_user.is_teacher = user.2; test_user.set_password(&user.1).expect("Set Password did not work correctly."); conn.save_session(test_user); } let mut contest = Contest::new("directory".to_string(), "public.yaml".to_string(), "RenamedContestName".to_string(), 1, true, None, None); contest.save(&conn); let mut contest = Contest::new("directory".to_string(), "public.yaml".to_string(), "PublicContestName".to_string(), 1, true, None, None); let mut taskgroup = Taskgroup::new("TaksgroupName".to_string()); let task = Task::new("taskdir1".to_string(), 3); taskgroup.tasks.push(task); let task = Task::new("taskdir2".to_string(), 4); taskgroup.tasks.push(task); contest.taskgroups.push(taskgroup); contest.save(&conn); let mut contest = Contest::new("directory".to_string(), "private.yaml".to_string(), "PrivateContestName".to_string(), 1, false, None, None); let mut taskgroup = Taskgroup::new("TaksgroupName".to_string()); let task = Task::new("taskdir1".to_string(), 3); taskgroup.tasks.push(task); let task = Task::new("taskdir2".to_string(), 4); taskgroup.tasks.push(task); contest.taskgroups.push(taskgroup); contest.save(&conn); let mut contest = Contest::new("directory".to_string(), "infinte.yaml".to_string(), "InfiniteContestName".to_string(), 0, true, None, None); let mut taskgroup = Taskgroup::new("TaksgroupName".to_string()); let task = Task::new("taskdir1".to_string(), 3); taskgroup.tasks.push(task); let task = Task::new("taskdir2".to_string(), 4); taskgroup.tasks.push(task); contest.taskgroups.push(taskgroup); contest.save(&conn); let mut config = config::read_config_from_file(Path::new("thisfileshoudnotexist")); config.port = Some(port); let srvr = start_server(conn, config); // Message server started start_tx.send(()).unwrap(); // Wait for test to finish stop_rx.recv().unwrap(); srvr.unwrap().close().unwrap(); }); // Wait for server to start start_rx.recv().unwrap(); thread::sleep(time::Duration::from_millis(100)); f(); // Message test finished stop_tx.send(()).unwrap(); } fn login(port: u16, client: &reqwest::Client, username: &str, password: &str) -> reqwest::Response { let params = [("username", username), ("password", password)]; let resp = client.post(&format!("http://localhost:{}/login", port)).form(¶ms).send().unwrap(); resp } fn login_code(port: u16, client: &reqwest::Client, code: &str) -> reqwest::Response { let params = [("code", code)]; let resp = client.post(&format!("http://localhost:{}/clogin", port)).form(¶ms).send().unwrap(); resp } #[test] fn start_server_and_check_requests() { start_server_and_fn(8080, None, || { let mut resp = reqwest::get("http://localhost:8080").unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("Jugendwettbewerb Informatik")); assert!(!content.contains("Error")); assert!(!content.contains("Gruppenverwaltung")); let mut resp = reqwest::get("http://localhost:8080/contest").unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("

Wettbewerbe

")); assert!(!content.contains("Error")); let mut resp = reqwest::get("http://localhost:8080/group").unwrap(); let content = resp.text().unwrap(); assert!(content.contains("

Login

")); }) } #[test] fn check_login_wrong_credentials() { start_server_and_fn(8081, None, || { let client = reqwest::Client::new(); let mut resp = login(8081, &client, "nonexistingusername", "wrongpassword"); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("

Login

")); assert!(content.contains("Login fehlgeschlagen.")); assert!(!content.contains("Error")); let mut resp = login_code(8081, &client, "g23AgaV"); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("

Login

")); assert!(content.contains("Kein gültiger Code.")); assert!(!content.contains("Error")); let mut resp = login_code(8081, &client, "u9XuAbH7p"); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("

Login

")); assert!(content.contains("Kein gültiger Code.")); assert!(!content.contains("Error")); }) } #[test] fn check_login() { start_server_and_fn(8082, Some(("testusr".to_string(), "testpw".to_string(), false)), || { let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let mut resp = login(8082, &client, "testusr", "testpw"); assert_eq!(resp.status(), StatusCode::FOUND); let content = resp.text().unwrap(); assert!(!content.contains("Error")); let mut set_cookie = resp.headers().get_all("Set-Cookie").iter(); assert!(set_cookie.next().is_some()); assert!(set_cookie.next().is_none()); let location = resp.headers().get(reqwest::header::LOCATION).unwrap().to_str().unwrap(); assert_eq!(location, "http://localhost:8082/"); let mut resp = client.get(location).send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(!content.contains("Error")); assert!(!content.contains("Gruppenverwaltung")); assert!(content.contains("Eingeloggt als testusr")); assert!(content.contains("Jugendwettbewerb Informatik")); }) } #[test] fn check_logout() { start_server_and_fn(8083, Some(("testusr".to_string(), "testpw".to_string(), false)), || { let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let resp = login(8083, &client, "testusr", "testpw"); assert_eq!(resp.status(), StatusCode::FOUND); let resp = client.get("http://localhost:8083/logout").send().unwrap(); assert_eq!(resp.status(), StatusCode::FOUND); let mut resp = client.get("http://localhost:8083").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("Benutzername")); assert!(content.contains("Passwort")); assert!(content.contains("Gruppencode / Teilnahmecode")); assert!(content.contains("Jugendwettbewerb Informatik")); }) } #[test] fn check_group_creation_and_group_code_login() { start_server_and_fn(8084, Some(("testusr".to_string(), "testpw".to_string(), true)), || { let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let resp = login(8084, &client, "testusr", "testpw"); assert_eq!(resp.status(), StatusCode::FOUND); let mut resp = client.get("http://localhost:8084").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("[Lehrer]")); assert!(content.contains("Gruppenverwaltung")); let mut resp = client.get("http://localhost:8084/group/").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("Gruppe anlegen")); let params = [("name", "WrongGroupname"), ("tag", "WrongMarker"), ("csrf_token", "76CfTPJaoz")]; let resp = client.post("http://localhost:8084/group/").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); let pos = content.find("type=\"hidden\" name=\"csrf_token\" value=\"").expect("CSRF-Token not found"); let csrf = &content[pos + 39..pos + 49]; let params = [("name", "Groupname"), ("tag", "Marker"), ("csrf_token", csrf)]; let resp = client.post("http://localhost:8084/group/").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::FOUND); let mut resp = client.get("http://localhost:8084/group/").send().unwrap(); let content = resp.text().unwrap(); assert!(!content.contains("WrongGroupname")); let pos = content.find("Groupname").expect("Group not found"); let groupcode = &content[pos + 58..pos + 65]; // New client to test group code login let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let resp = login_code(8084, &client, groupcode); assert_eq!(resp.status(), StatusCode::FOUND); let mut set_cookie = resp.headers().get_all("Set-Cookie").iter(); assert!(set_cookie.next().is_some()); assert!(set_cookie.next().is_none()); let location = resp.headers().get(reqwest::header::LOCATION).unwrap().to_str().unwrap(); assert_eq!(location, "http://localhost:8084/profile"); let mut resp = client.get(location).send().unwrap(); let content = resp.text().unwrap(); let pos = content.find("

Login-Code: ").expect("Logincode not found"); let logincode = &content[pos + 15..pos + 24]; // New client to test login code login let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let resp = login_code(8084, &client, logincode); assert_eq!(resp.status(), StatusCode::FOUND); let location = resp.headers().get(reqwest::header::LOCATION).unwrap().to_str().unwrap(); assert_eq!(location, "http://localhost:8084/"); let mut resp = client.get(location).send().unwrap(); let content = resp.text().unwrap(); assert!(content.contains("Eingeloggt als ")); }) } #[test] fn check_contest_start() { start_server_and_fn(8085, Some(("testusr".to_string(), "testpw".to_string(), false)), || { let client = reqwest::Client::builder().cookie_store(true) .redirect(reqwest::RedirectPolicy::none()) .build() .unwrap(); let resp = login(8085, &client, "testusr", "testpw"); assert_eq!(resp.status(), StatusCode::FOUND); let mut resp = client.get("http://localhost:8085/contest/").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("PublicContestName")); assert!(content.contains("InfiniteContestName")); //assert!(content.contains("PrivateContestName")); assert!(!content.contains("WrongContestName")); assert!(!content.contains("RenamedContestName")); assert!(content.contains("PublicContestName")); let mut resp = client.get("http://localhost:8085/contest/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("PublicContestName")); assert!(!content.contains("InfiniteContestName")); assert!(!content.contains("PrivateContestName")); assert!(!content.contains("WrongContestName")); assert!(!content.contains("RenamedContestName")); let params = [("csrf_token", "76CfTPJaoz")]; let resp = client.post("http://localhost:8085/contest/1").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); let pos = content.find("type=\"hidden\" name=\"csrf_token\" value=\"").expect("CSRF-Token not found"); let csrf = &content[pos + 39..pos + 49]; let params = [("csrf_token", csrf)]; let resp = client.post("http://localhost:8085/contest/1").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::FOUND); let mut resp = client.get("http://localhost:8085/contest/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert!(content.contains("☆☆☆")); assert!(content.contains("☆☆☆☆")); let mut resp = client.get("http://localhost:8085/task/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); let pos = content.find("#taskid=1&csrftoken=").expect("CSRF-Token not found"); let csrf = &content[pos + 20..pos + 30]; let mut resp = client.get("http://localhost:8085/load/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert_eq!(content, "{}"); let params = [("data","WrongData"),("grade","1"),("csrf_token", "FNQU4QsEMY")]; let mut resp = client.post("http://localhost:8085/save/1").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); let mut resp = client.get("http://localhost:8085/load/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert_eq!(content, "{}"); let params = [("data","SomeData"),("grade","2"),("csrf_token", csrf)]; let mut resp = client.post("http://localhost:8085/save/1").form(¶ms).send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert_eq!(content, "{}"); let mut resp = client.get("http://localhost:8085/load/1").send().unwrap(); assert_eq!(resp.status(), StatusCode::OK); let content = resp.text().unwrap(); assert_eq!(content, "SomeData"); }) } }