/* medal *\ * Copyright (C) 2020 Bundesweite Informatikwettbewerbe * * * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero * * General Public License as published by the Free Software Foundation, either version 3 of the License, or (at * * your option) any later version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the * * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * * License for more details. * * * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see * \* . */ use std::path::Path; pub use handlebars_iron::handlebars::to_json; use handlebars_iron::{DirectorySource, HandlebarsEngine, Template}; use iron; use iron::modifiers::Redirect; use iron::modifiers::RedirectRaw; use iron::prelude::*; use iron::{status, AfterMiddleware, AroundMiddleware, Handler}; use iron::mime::Mime; use iron_sessionstorage; use iron_sessionstorage::backends::SignedCookieBackend; use iron_sessionstorage::traits::*; use iron_sessionstorage::SessionStorage; use mount::Mount; use persistent::{Read, Write}; use reqwest; use router::Router; use staticfile::Static; use urlencoded::{UrlEncodedBody, UrlEncodedQuery}; #[cfg(feature = "debug")] use iron::BeforeMiddleware; use config::{Config, OauthProvider}; use core; use db_conn::MedalConnection; use iron::typemap::Key; pub use serde_json::value as json_val; #[cfg(feature = "signup")] use db_conn::SignupResult; static TASK_DIR: &str = "tasks"; macro_rules! mime { ($top:tt / $sub:tt) => ( mime!($top / $sub;) ); ($top:tt / $sub:tt ; $($attr:tt = $val:tt),*) => ( iron::mime::Mime( iron::mime::TopLevel::$top, iron::mime::SubLevel::$sub, vec![ $((Attr::$attr,Value::$val)),* ] ) ); } macro_rules! with_conn { ( $x:expr , $c:ident, $r:expr , $($y:expr),* ) => { { let mutex = $r.get::>>().unwrap(); let conn = mutex.lock().unwrap_or_else(|e| e.into_inner()); $x(&*conn, $($y),*) } }; } macro_rules! template_ok { ( $x:expr ) => {{ let (template, data) = $x; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) }}; } /** Show error messages on commandline */ struct ErrorReporter; impl AfterMiddleware for ErrorReporter { fn catch(&self, _: &mut Request, err: IronError) -> IronResult { println!("{}", err); Err(err) } } /** Show error messages to users */ struct ErrorShower; impl AfterMiddleware for ErrorShower { fn catch(&self, _: &mut Request, err: IronError) -> IronResult { let IronError { error, response } = err; if response.body.is_none() { Ok(match response.status { Some(s) => { let n = s.to_u16(); if (400..=599).contains(&n) { response.set((mime!(Text / Html), format!("

{} {}

", n, s.canonical_reason().unwrap_or("(Unknown error)")))) } else { response } } _ => response, }) } else { Err(IronError { error, response }) } } } #[derive(Debug)] struct SessionToken { token: String, } impl iron_sessionstorage::Value for SessionToken { fn get_key() -> &'static str { "medal_session" } fn into_raw(self) -> String { self.token } fn from_raw(value: String) -> Option { if value.is_empty() { None } else { Some(SessionToken { token: value }) } } } pub struct CookieDistributor {} impl AroundMiddleware for CookieDistributor { fn around(self, handler: Box) -> Box { use rand::{distributions::Alphanumeric, thread_rng, Rng}; Box::new(move |req: &mut Request| -> IronResult { if req.session().get::().expect("blub...").is_none() { let session_token: String = thread_rng().sample_iter(&Alphanumeric).take(10).collect(); req.session().set(SessionToken { token: session_token }).unwrap(); } handler.handle(req) }) } } #[cfg(feature = "debug")] pub struct RequestLogger {} #[cfg(feature = "debug")] impl BeforeMiddleware for RequestLogger { fn before(&self, req: &mut Request) -> IronResult<()> { println!("{}: {}", req.method, req.url); Ok(()) } } #[derive(Debug)] struct SessionError { message: String, } impl ::std::error::Error for SessionError { fn description(&self) -> &str { &self.message } } impl ::std::fmt::Display for SessionError { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { write!(f, "{}", self.message) } } trait RequestSession { fn get_session_token(&mut self) -> Option; fn require_session_token(&mut self) -> IronResult; fn expect_session_token(&mut self) -> IronResult; } impl<'a, 'b> RequestSession for Request<'a, 'b> { fn get_session_token(&mut self) -> Option { let session_token = self.session().get::().unwrap(); (|st: Option| -> Option { Some(st?.token) })(session_token) } fn require_session_token(&mut self) -> IronResult { match self.session().get::().unwrap() { Some(SessionToken { token: session }) => Ok(session), _ => { use rand::{distributions::Alphanumeric, thread_rng, Rng}; let new_session_key: String = thread_rng().sample_iter(&Alphanumeric).take(28).collect(); self.session().set(SessionToken { token: new_session_key }).unwrap(); Err(IronError { error: Box::new(SessionError { message: "No valid session found, redirecting to cookie page".to_string(), }), response: Response::with(( status::Found, RedirectRaw(format!("/cookie?{}", self.url.path().join("/"))), )), }) } } } fn expect_session_token(&mut self) -> IronResult { match self.session().get::().unwrap() { Some(SessionToken { token: session }) => Ok(session), _ => Err(IronError { error: Box::new(SessionError { message: "No valid session found, access denied".to_string() }), response: Response::with(status::Forbidden) }), } } } trait RequestRouterParam { fn get_str(&mut self, key: &str) -> Option; fn get_int(&mut self, key: &str) -> Option; fn expect_int(&mut self, key: &str) -> IronResult; fn expect_str(&mut self, key: &str) -> IronResult; } impl<'a, 'b> RequestRouterParam for Request<'a, 'b> { fn get_str(&mut self, key: &str) -> Option { Some(self.extensions.get::()?.find(key)?.to_owned()) } fn get_int(&mut self, key: &str) -> Option { Some(self.extensions.get::()?.find(key)?.parse::().ok()?) } fn expect_int(&mut self, key: &str) -> IronResult { match self.get_int::(key) { Some(i) => Ok(i), _ => Err(IronError { error: Box::new(SessionError { message: "No valid routing parameter".to_string() }), response: Response::with(status::Forbidden) }), } } fn expect_str(&mut self, key: &str) -> IronResult { match self.get_str(key) { Some(s) => Ok(s), _ => Err(IronError { error: Box::new(SessionError { message: "Routing parameter missing".to_string() }), response: Response::with(status::Forbidden) }), } } } struct AugMedalError<'c, 'a: 'c, 'b: 'c + 'a>(core::MedalError, &'c mut Request<'a, 'b>); impl<'c, 'a, 'b> From> for IronError { fn from(AugMedalError(me, req): AugMedalError<'c, 'a, 'b>) -> Self { match me { core::MedalError::NotLoggedIn => { IronError { error: Box::new(SessionError { message: "Not Logged in, redirecting to login page".to_string() }), response: Response::with((status::Found, RedirectRaw(format!("/login?{}", req.url.path().join("/"))))) } } core::MedalError::AccessDenied => IronError { error: Box::new(SessionError { message: "Access denied".to_string() }), response: Response::with(status::Unauthorized) }, core::MedalError::CsrfCheckFailed => IronError { error: Box::new(SessionError { message: "CSRF Error".to_string() }), response: Response::with(status::Forbidden) }, core::MedalError::SessionTimeout => { IronError { error: Box::new(SessionError { message: "Session timed out".to_string() }), response: Response::with(status::Forbidden) } } core::MedalError::DatabaseError => { IronError { error: Box::new(SessionError { message: "Database Error".to_string() }), response: Response::with(status::InternalServerError) } } core::MedalError::ConfigurationError => { IronError { error: Box::new(SessionError { message: "Server misconfiguration. Please contact an administrator!".to_string() }), response: Response::with(status::InternalServerError) } } core::MedalError::DatabaseConnectionError => { IronError { error: Box::new(SessionError { message: "Database Connection Error".to_string() }), response: Response::with(status::InternalServerError) } } core::MedalError::PasswordHashingError => { IronError { error: Box::new(SessionError { message: "Error hashing the passwords".to_string() }), response: Response::with(status::InternalServerError) } } core::MedalError::UnmatchedPasswords => { IronError { error: Box::new(SessionError { message: "The two passwords did not match.".to_string() }), response: Response::with(status::Forbidden) } } core::MedalError::NotFound => IronError { error: Box::new(SessionError { message: "Not found".to_string() }), response: Response::with(status::NotFound) }, core::MedalError::AccountIncomplete => IronError { error: Box::new(SessionError { message: "Account incomplete".to_string() }), response: Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status=firstlogin", &url_for!(req, "profile"))).unwrap()))) }, core::MedalError::OauthError(errstr) => { IronError { error: Box::new(SessionError { message: format!("Access denied (Error {})", errstr) }), response: Response::with(status::Unauthorized) } } } } } trait RequestAugmentMedalError<'c, 'a: 'c, 'b: 'c + 'a, R> { fn aug(self, req: &'c mut Request<'a, 'b>) -> Result>; } impl<'c, 'a: 'c, 'b: 'c + 'a, T> RequestAugmentMedalError<'c, 'a, 'b, T> for Result { fn aug(self, req: &'c mut Request<'a, 'b>) -> Result> { self.map_err(move |me| AugMedalError(me, req)) } } fn login_info(config: &Config) -> core::LoginInfo { core::LoginInfo { password_login: config.enable_password_login == Some(true), self_url: config.self_url.clone(), oauth_providers: config.oauth_providers.clone() } } fn greet_personal(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); // hier ggf. Daten aus dem Request holen let config = req.get::>().unwrap(); let (template, mut data) = with_conn![core::index, C, req, session_token, login_info(&config)].aug(req)?; data.insert("config".to_string(), to_json(&config.template_params)); // Antwort erstellen und zurücksenden let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn dbstatus(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let config = req.get::>().unwrap(); let query_string = req.url.query().map(|s| s.to_string()); let status = with_conn![core::status, C, req, config.dbstatus_secret.clone(), query_string].aug(req)?; let mut resp = Response::new(); resp.set_mut(status).set_mut(status::Ok); Ok(resp) } fn debug(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); let (template, data) = with_conn![core::debug, C, req, session_token]; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn debug_new_token(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); #[cfg(feature = "debug")] println!("Logging out session {:?}", session_token); with_conn![core::logout, C, req, session_token]; Ok(Response::with((status::Found, Redirect(url_for!(req, "debug"))))) } fn debug_logout(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); #[cfg(feature = "debug")] println!("Logging out session {:?}", session_token); with_conn![core::logout, C, req, session_token]; Ok(Response::with((status::Found, Redirect(url_for!(req, "debug"))))) } fn debug_create_session(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); with_conn![core::debug_create_session, C, req, session_token]; Ok(Response::with((status::Found, Redirect(url_for!(req, "debug"))))) } fn contests(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let query_string = req.url.query().unwrap_or("").to_string(); // TODO: Move to core::* ? let visibility = if query_string.contains("open") { core::ContestVisibility::Open } else if query_string.contains("current") { core::ContestVisibility::Current } else if query_string.contains("challenge") { core::ContestVisibility::LoginRequired } else { core::ContestVisibility::All }; let config = req.get::>().unwrap(); let res = with_conn![core::show_contests, C, req, &session_token, login_info(&config), visibility]; if res.is_err() { // Database connection failed … Create a new database connection! // TODO: This code should be unified with the database creation code in main.rs println!("DATABASE CONNECTION LOST! Restarting database connection."); let conn = C::reconnect(&config); let mutex = req.get::>>().unwrap(); let mut sharedconn = mutex.lock().unwrap_or_else(|e| e.into_inner()); *sharedconn = conn; // return ServerError(); } let (template, mut data) = res.unwrap(); data.insert("config".to_string(), to_json(&config.template_params)); if query_string.contains("results") { data.insert("direct_link_to_results".to_string(), to_json(&true)); } let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn contest(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let contest_id = req.expect_int::("contestid")?; let session_token = req.require_session_token()?; let query_string = req.url.query().map(|s| s.to_string()); let config = req.get::>().unwrap(); let (template, data) = with_conn![core::show_contest, C, req, contest_id, &session_token, query_string, login_info(&config)].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn contestresults(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let config = req.get::>().unwrap(); let disable_contest_results = config.disable_results_page.unwrap_or(false); if disable_contest_results { let mut resp = Response::new(); resp.set_mut(Template::new(&"nocontestresults", 2)).set_mut(status::Locked); return Ok(resp); } let contest_id = req.expect_int::("contestid")?; let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_contest_results, C, req, contest_id, &session_token].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn contestresults_download(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let config = req.get::>().unwrap(); let disable_contest_results = config.disable_results_page.unwrap_or(false); println!("test"); if disable_contest_results { let mut resp = Response::new(); resp.set_mut(Template::new(&"nocontestresults", 2)).set_mut(status::Locked); return Ok(resp); } let contest_id = req.expect_int::("contestid")?; let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_contest_results, C, req, contest_id, &session_token].aug(req)?; use iron::headers::{Charset, ContentDisposition, DispositionParam, DispositionType}; let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename( Charset::Ext("Utf-8".to_string()), // The character set for the bytes of the filename None, // The optional language tag (see `language-tag` crate) format!("{}.csv", data.get("contestname").unwrap().as_str().unwrap()).as_bytes().to_vec(), // the actual bytes of the filename // TODO: The name should be returned by core::show_contest_results directly )] }; let mime: Mime = "text/csv".parse().unwrap(); let mut resp = Response::new(); resp.headers.set(cd); resp.set_mut(Template::new(&format!("{}_download", template), data)).set_mut(status::Ok).set_mut(mime); Ok(resp) } fn contest_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let contest_id = req.expect_int::("contestid")?; let session_token = req.expect_session_token()?; let csrf_token = { let formdata = itry!(req.get_ref::()); iexpect!(formdata.get("csrf_token"))[0].to_owned() }; // TODO: Was mit dem Result? with_conn![core::start_contest, C, req, contest_id, &session_token, &csrf_token].aug(req)?; Ok(Response::with((status::Found, Redirect(url_for!(req, "contest", "contestid" => format!("{}",contest_id)))))) } fn login(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); let config = req.get::>().unwrap(); let (template, mut data) = with_conn![core::show_login, C, req, session_token, login_info(&config)]; let query_string = req.url.query().map(|s| s.to_string()); if let Some(query) = query_string { data.insert("forward".to_string(), to_json(&query)); } // Antwort erstellen und zurücksenden let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn login_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let logindata = { let formdata = itry!(req.get_ref::()); (iexpect!(formdata.get("username"))[0].to_owned(), iexpect!(formdata.get("password"))[0].to_owned()) }; let config = req.get::>().unwrap(); // TODO: Submit current session to login let loginresult = with_conn![core::login, C, req, logindata, login_info(&config)]; match loginresult { // Login successful Ok(sessionkey) => { req.session().set(SessionToken { token: sessionkey }).unwrap(); Ok(Response::with((status::Found, Redirect(url_for!(req, "greet"))))) } // Login failed Err((template, data)) => { let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } } } fn login_code_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let code = { let formdata = itry!(req.get_ref::()); iexpect!(formdata.get("code"))[0].to_owned() }; let config = req.get::>().unwrap(); // TODO: Submit current session to login let loginresult = with_conn![core::login_with_code, C, req, &code, login_info(&config)]; match loginresult { // Login successful Ok(Ok(sessionkey)) => { req.session().set(SessionToken { token: sessionkey }).unwrap(); Ok(Response::with((status::Found, Redirect(url_for!(req, "greet"))))) } Ok(Err(sessionkey)) => { req.session().set(SessionToken { token: sessionkey }).unwrap(); //Ok(Response::with((status::Found, Redirect(url_for!(req, "profile"))))) Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status=firstlogin", &url_for!(req, "profile"))).unwrap())))) } // Login failed Err((template, data)) => { let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } } } fn logout(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); println!("Loggin out session {:?}", session_token); with_conn![core::logout, C, req, session_token]; Ok(Response::with((status::Found, Redirect(url_for!(req, "greet"))))) } #[cfg(feature = "signup")] fn signup(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let query_string = req.url.query().map(|s| s.to_string()); let data = core::signupdata(query_string); let mut resp = Response::new(); resp.set_mut(Template::new("signup", data)).set_mut(status::Ok); Ok(resp) } #[cfg(feature = "signup")] fn signup_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.get_session_token(); let signupdata = { let formdata = itry!(req.get_ref::()); (iexpect!(formdata.get("username"))[0].to_owned(), iexpect!(formdata.get("email"))[0].to_owned(), iexpect!(formdata.get("password"))[0].to_owned()) }; let signupresult = with_conn![core::signup, C, req, session_token, signupdata].aug(req)?; match signupresult { SignupResult::SignedUp => Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status={:?}", &url_for!(req, "profile"), signupresult)).unwrap())))), _ => Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status={:?}", &url_for!(req, "signup"), signupresult)).unwrap())))), } } #[cfg(not(feature = "signup"))] fn signup(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { Err(core::MedalError::NotFound).aug(req).map_err(|x| x.into()) } #[cfg(not(feature = "signup"))] fn signup_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { Err(core::MedalError::NotFound).aug(req).map_err(|x| x.into()) } fn submission(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let task_id = req.expect_int::("taskid")?; let session_token = req.expect_session_token()?; let subtask: Option = (|| -> Option { req.get_ref::().ok()?.get("subtask")?.get(0).map(|x| x.to_owned()) })(); let result = with_conn![core::load_submission, C, req, task_id, &session_token, subtask]; match result { Ok(data) => Ok(Response::with((status::Ok, mime!(Application / Json), data))), Err(_) => Ok(Response::with((status::BadRequest, mime!(Application / Json), "{}".to_string()))), } } fn submission_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let task_id = req.expect_int::("taskid")?; let session_token = req.expect_session_token()?; let (csrf_token, data, grade, subtask) = { let formdata = iexpect!(req.get_ref::().ok()); (iexpect!(formdata.get("csrf_token"))[0].to_owned(), iexpect!(formdata.get("data"))[0].to_owned(), iexpect!(formdata.get("grade").unwrap_or(&vec!["0".to_owned()])[0].parse::().ok()), formdata.get("subtask").map(|x| x[0].to_owned())) }; #[cfg(feature = "debug")] println!("New submission for task {} (graded {}): {}", task_id, grade, data); let result = with_conn![core::save_submission, C, req, task_id, &session_token, &csrf_token, data, grade, subtask].aug(req)?; Ok(Response::with((status::Ok, mime!(Application / Json), result))) } fn task(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let task_id = req.expect_int::("taskid")?; let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_task, C, req, task_id, &session_token].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn groups(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_groups, C, req, &session_token].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn group(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let group_id = req.expect_int::("groupid")?; let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_group, C, req, group_id, &session_token].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn group_download(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let group_id = req.expect_int::("groupid")?; let session_token = req.require_session_token()?; let (template, data) = with_conn![core::show_group, C, req, group_id, &session_token].aug(req)?; use iron::headers::{Charset, ContentDisposition, DispositionParam, DispositionType}; let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename( Charset::Ext("Utf-8".to_string()), // The character set for the bytes of the filename None, // The optional language tag (see `language-tag` crate) format!("{}.csv", data.get("groupname").unwrap().as_str().unwrap()).as_bytes().to_vec(), // the actual bytes of the filename // TODO: The name should be returned by core::show_group directly )] }; let mime: Mime = "text/csv".parse().unwrap(); let mut resp = Response::new(); resp.headers.set(cd); resp.set_mut(Template::new(&format!("{}_download", template), data)).set_mut(status::Ok).set_mut(mime); Ok(resp) } //TODO: Secure with CSRF-Token? fn group_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let group_id = req.expect_int::("groupid")?; let session_token = req.expect_session_token()?; //TODO: use result? with_conn![core::modify_group, C, req, group_id, &session_token].aug(req)?; Ok(Response::with((status::Found, Redirect(url_for!(req, "group", "groupid" => format!("{}",group_id)))))) } fn new_group(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let (csrf_token, name, tag) = { let formdata = iexpect!(req.get_ref::().ok()); (iexpect!(formdata.get("csrf_token"))[0].to_owned(), iexpect!(formdata.get("name"))[0].to_owned(), iexpect!(formdata.get("tag"))[0].to_owned()) }; let group_id = with_conn![core::add_group, C, req, &session_token, &csrf_token, name, tag].aug(req)?; Ok(Response::with((status::Found, Redirect(url_for!(req, "group", "groupid" => format!("{}",group_id)))))) } fn group_csv(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let si = { let config = req.get::>().unwrap(); core::SexInformation { require_sex: config.require_sex.unwrap_or(false), allow_sex_na: config.allow_sex_na.unwrap_or(true), allow_sex_diverse: config.allow_sex_diverse.unwrap_or(false), allow_sex_other: config.allow_sex_other.unwrap_or(true) } }; template_ok!(with_conn![core::group_csv, C, req, &session_token, si].aug(req)?) } fn group_csv_upload(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let (csrf_token, group_data) = { let formdata = iexpect!(req.get_ref::().ok()); (iexpect!(formdata.get("csrf_token"))[0].to_owned(), iexpect!(formdata.get("group_data"))[0].to_owned()) }; println!("{}", group_data); with_conn![core::upload_groups, C, req, &session_token, &csrf_token, &group_data].aug(req)?; Ok(Response::with((status::Found, Redirect(url_for!(req, "groups"))))) } fn profile(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; let query_string = req.url.query().map(|s| s.to_string()); let si = { let config = req.get::>().unwrap(); core::SexInformation { require_sex: config.require_sex.unwrap_or(false), allow_sex_na: config.allow_sex_na.unwrap_or(true), allow_sex_diverse: config.allow_sex_diverse.unwrap_or(false), allow_sex_other: config.allow_sex_other.unwrap_or(true) } }; let (template, data) = with_conn![core::show_profile, C, req, &session_token, None, query_string, si].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn profile_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let (csrf_token, firstname, lastname, street, zip, city, pwd, pwd_repeat, grade, sex) = { let formdata = itry!(req.get_ref::()); (iexpect!(formdata.get("csrf_token"))[0].to_owned(), iexpect!(formdata.get("firstname"))[0].to_owned(), iexpect!(formdata.get("lastname"))[0].to_owned(), formdata.get("street").map(|x| x[0].to_owned()), formdata.get("zip").map(|x| x[0].to_owned()), formdata.get("city").map(|x| x[0].to_owned()), formdata.get("password").map(|x| x[0].to_owned()), formdata.get("password_repeat").map(|x| x[0].to_owned()), iexpect!(formdata.get("grade"))[0].parse::().unwrap_or(0), iexpect!(formdata.get("sex"))[0].parse::().ok()) }; let profilechangeresult = with_conn![core::edit_profile, C, req, &session_token, None, &csrf_token, (firstname, lastname, street, zip, city, pwd, pwd_repeat, grade, sex)].aug(req)?; Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status={:?}", &url_for!(req, "profile"), profilechangeresult)).unwrap())))) } fn user(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let user_id = req.expect_int::("userid")?; let session_token = req.expect_session_token()?; let query_string = req.url.query().map(|s| s.to_string()); let si = { let config = req.get::>().unwrap(); core::SexInformation { require_sex: config.require_sex.unwrap_or(false), allow_sex_na: config.allow_sex_na.unwrap_or(true), allow_sex_diverse: config.allow_sex_diverse.unwrap_or(false), allow_sex_other: config.allow_sex_other.unwrap_or(true) } }; let (template, data) = with_conn![core::show_profile, C, req, &session_token, Some(user_id), query_string, si].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn user_post(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let user_id = req.expect_int::("userid")?; let session_token = req.expect_session_token()?; let (csrf_token, firstname, lastname, street, zip, city, pwd, pwd_repeat, grade, sex) = { let formdata = itry!(req.get_ref::()); (iexpect!(formdata.get("csrf_token"))[0].to_owned(), iexpect!(formdata.get("firstname"))[0].to_owned(), iexpect!(formdata.get("lastname"))[0].to_owned(), formdata.get("street").map(|x| x[0].to_owned()), formdata.get("zip").map(|x| x[0].to_owned()), formdata.get("city").map(|x| x[0].to_owned()), formdata.get("password").map(|x| x[0].to_owned()), formdata.get("password_repeat").map(|x| x[0].to_owned()), iexpect!(formdata.get("grade"))[0].parse::().unwrap_or(0), iexpect!(formdata.get("sex"))[0].parse::().ok()) }; let profilechangeresult = with_conn![core::edit_profile, C, req, &session_token, Some(user_id), &csrf_token, (firstname, lastname, street, zip, city, pwd, pwd_repeat, grade, sex)].aug(req)?; Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status={:?}", &url_for!(req, "user", "userid" => format!("{}",user_id)), profilechangeresult)).unwrap())))) //old: Ok(Response::with((status::Found, Redirect(url_for!(req, "user", "userid" => format!("{}",user_id)))))) } fn teacherinfos(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let config = req.get::>().unwrap(); let (template, mut data) = with_conn![core::teacher_infos, C, req, &session_token].aug(req)?; data.insert("config".to_string(), to_json(&config.template_params)); let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let config = req.get::>().unwrap(); let (template, mut data) = with_conn![core::admin_index, C, req, &session_token].aug(req)?; data.insert("dbstatus_secret".to_string(), to_json(&config.dbstatus_secret)); let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_users(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let (s_id, s_firstname, s_lastname, s_logincode, s_groupcode, s_pms_id) = { let formdata = itry!(req.get_ref::()); (formdata.get("id").map(|x| x[0].parse::().unwrap_or(0)), formdata.get("firstname").map(|x| x[0].to_owned()), formdata.get("lastname").map(|x| x[0].to_owned()), formdata.get("logincode").map(|x| x[0].to_owned()), formdata.get("groupcode").map(|x| x[0].to_owned()), formdata.get("pmsid").map(|x| x[0].to_owned())) }; let (template, data) = with_conn![core::admin_search_users, C, req, &session_token, (s_id, s_firstname, s_lastname, s_logincode, s_groupcode, s_pms_id)].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_user(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let user_id = req.expect_int::("userid")?; let session_token = req.expect_session_token()?; let csrf_token = if let Ok(formdata) = req.get_ref::() { // or iexpect!(formdata.get("csrf_token"))[0].to_owned(), ? formdata.get("csrf_token").map(|x| x[0].to_owned()) } else { None }; let (template, data) = if let Some(csrf_token) = csrf_token { with_conn![core::admin_delete_user, C, req, user_id, &session_token, &csrf_token].aug(req)? } else { with_conn![core::admin_show_user, C, req, user_id, &session_token].aug(req)? }; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_group(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let group_id = req.expect_int::("groupid")?; let session_token = req.expect_session_token()?; let csrf_token = if let Ok(formdata) = req.get_ref::() { formdata.get("csrf_token").map(|x| x[0].to_owned()) } else { None }; let (template, data) = if let Some(csrf_token) = csrf_token { with_conn![core::admin_delete_group, C, req, group_id, &session_token, &csrf_token].aug(req)? } else { with_conn![core::admin_show_group, C, req, group_id, &session_token].aug(req)? }; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_participation(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let user_id = req.expect_int::("userid")?; let contest_id = req.expect_int::("contestid")?; let session_token = req.expect_session_token()?; let csrf_token = if let Ok(formdata) = req.get_ref::() { formdata.get("csrf_token").map(|x| x[0].to_owned()) } else { None }; let (template, data) = if let Some(csrf_token) = csrf_token { with_conn![core::admin_delete_participation, C, req, user_id, contest_id, &session_token, &csrf_token].aug(req)? } else { with_conn![core::admin_show_participation, C, req, user_id, contest_id, &session_token].aug(req)? }; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_contests(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let (template, data) = with_conn![core::admin_show_contests, C, req, &session_token].aug(req)?; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn admin_export_contest(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let contest_id = req.expect_int::("contestid")?; let session_token = req.expect_session_token()?; let filename = with_conn![core::admin_contest_export, C, req, contest_id, &session_token].aug(req)?; Ok(Response::with((status::Found, RedirectRaw(format!("/export/{}", filename))))) } fn admin_cleanup(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { let session_token = req.expect_session_token()?; let csrf_token = if let Ok(formdata) = req.get_ref::() { formdata.get("csrf_token").map(|x| x[0].to_owned()) } else { None }; let (template, data) = if let Some(csrf_token) = csrf_token { with_conn![core::admin_do_cleanup, C, req, &session_token, &csrf_token].aug(req)? } else { with_conn![core::admin_show_cleanup, C, req, &session_token].aug(req)? }; let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } fn oauth(req: &mut Request) -> IronResult where C: MedalConnection + std::marker::Send + 'static { println!("{:?}", req.url.query().unwrap_or("")); let oauth_id = req.expect_str("oauthid")?; let school_id = req.get_str("schoolid"); let oauth_provider = { let config = req.get::>().unwrap(); let mut result: Option = None; if let Some(ref oauth_providers) = config.oauth_providers { for oauth_provider in oauth_providers { if oauth_provider.provider_id == oauth_id { result = Some(oauth_provider.clone()); break; } } if let Some(result) = result { result } else { return Ok(Response::with(iron::status::NotFound)); } } else { return Ok(Response::with(iron::status::NotFound)); } }; let user_data_result = match oauth_provider.medal_oauth_type.as_ref() { "pms" => oauth_pms(req, oauth_provider, school_id.as_ref()).aug(req)?, _ => return Ok(Response::with(iron::status::NotFound)), }; let user_data = match user_data_result { Err(response) => return Ok(response), Ok(user_data) => user_data, }; let user_type = user_data.foreign_type; let oauthloginresult = { // hier ggf. Daten aus dem Request holen let mutex = req.get::>>().unwrap(); let conn = mutex.lock().unwrap_or_else(|e| e.into_inner()); // Antwort erstellen und zurücksenden core::login_oauth(&*conn, user_data, oauth_id) }; match oauthloginresult { // Login successful Ok((sessionkey, redirectprofile)) => { req.session().set(SessionToken { token: sessionkey }).unwrap(); use core::UserType; if user_type == UserType::User && redirectprofile { Ok(Response::with((status::Found, Redirect(iron::Url::parse(&format!("{}?status=firstlogin", &url_for!(req, "profile"))).unwrap())))) } else { Ok(Response::with((status::Found, Redirect(url_for!(req, "greet"))))) } } // Login failed Err((template, data)) => { let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); Ok(resp) } } } #[derive(Deserialize, Debug)] struct OAuthAccess { access_token: String, token_type: String, refresh_token: String, expires: Option, // documented as 'expires_in' expires_in: Option, // sent as 'expires' } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] #[serde(untagged)] pub enum SchoolIdOrSchoolIds { None(i32), SchoolId(String), SchoolIds(Vec), } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct OAuthUserData { userID: Option, // documented as 'userId' userId: Option, // sent as 'userID' userType: String, gender: String, firstName: String, lastName: String, dateOfBirth: Option, eMail: Option, schoolId: Option, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct OAuthSchoolData { name: Option, city: Option, error: Option, } fn pms_hash_school(school_id: &str, secret: &str) -> String { use sha2::{Digest, Sha512}; let mut hasher = Sha512::default(); let string_to_hash = format!("{}{}", school_id, secret); hasher.input(string_to_hash.as_bytes()); let hashed_string = hasher.result(); format!("{:02X?}", hashed_string).chars().filter(|c| c.is_ascii_alphanumeric()).collect() } fn oauth_pms(req: &mut Request, oauth_provider: OauthProvider, school_id: Option<&String>) -> Result, core::MedalError> { use core::{UserSex, UserType}; use params::{Params, Value}; fn er(e: &str) -> core::MedalError { core::MedalError::OauthError(e.to_string()) } fn e(e: &str) -> Result { Err::(er(e)) } let (_, _, code): (String, String, String) = { let map = req.get_ref::().unwrap(); match (map.find(&["state"]), map.find(&["scope"]), map.find(&["code"])) { (Some(&Value::String(ref state)), Some(&Value::String(ref scope)), Some(&Value::String(ref code))) if state == "42" => { (state.clone(), scope.clone(), code.clone()) } _ => return e("#70"), } }; let client = reqwest::Client::new(); let params = [("code", code), ("grant_type", "authorization_code".to_string())]; // TODO: This can fail if to much time has passed let res = client.post(&oauth_provider.access_token_url) .basic_auth(oauth_provider.client_id, Some(oauth_provider.client_secret)) .form(¶ms) .send(); let access: OAuthAccess = res.or(e("#00"))?.json().or(e("#01"))?; let res = client.get(&oauth_provider.user_data_url).bearer_auth(access.access_token).send(); let mut user_data: OAuthUserData = res.or(e("#10"))?.json().or(e("#11"))?; // Unify ambiguous fields user_data.userId = user_data.userID.or(user_data.userId); // Does the user has an array of school (i.e. is he a teacher)? if let Some(SchoolIdOrSchoolIds::SchoolIds(school_ids)) = user_data.schoolId { // Has there been a school selected? if let Some(school_id) = school_id { if school_id == "none" && oauth_provider.allow_teacher_login_without_school == Some(true) { // Nothing to do } // Is the school a valid school for the user? else if school_ids.contains(&school_id) { if let Some(mut user_id) = user_data.userId { user_id.push('/'); user_id.push_str(&school_id); user_data.userId = Some(user_id); } } else { return e("#40"); } } else { // No school has been selected // Check if school data query is configured. Otherwise there is nothing to do. if let (Some(school_data_url), Some(school_data_secret)) = (oauth_provider.school_data_url, oauth_provider.school_data_secret) { // Gather school information of all schools let school_infos: Vec<(String, String)> = school_ids.iter() .map(|school_id| -> Result<(String, String), core::MedalError> { let params = [("schoolId", school_id.clone()), ("hash", pms_hash_school(&school_id, &school_data_secret))]; let res = client.post(&school_data_url).form(¶ms).send(); let school_data: OAuthSchoolData = res.or(e("#30"))?.json().or(e("#31"))?; Ok((school_id.clone(), format!("{}, {}", school_data.name .or(school_data.error) .unwrap_or_else(|| "Information missing".to_string()), school_data.city.unwrap_or_else(|| "–".to_string())))) }) .collect::>()?; let mut data = json_val::Map::new(); data.insert("schools".to_string(), to_json(&school_infos)); data.insert("query".to_string(), to_json(&req.url.query().unwrap_or(""))); data.insert("parent".to_string(), to_json(&"base")); data.insert("no_login".to_string(), to_json(&true)); data.insert("teacher_login_without_school".to_string(), to_json(&oauth_provider.allow_teacher_login_without_school.unwrap_or(false))); let mut resp = Response::new(); resp.set_mut(Template::new(&"oauth_school_selector", data)).set_mut(status::Ok); return Ok(Err(resp)); } else { // Configuration error: return Err(core::MedalError::ConfigurationError); } } } else if school_id.is_some() { // A school has apparently been selected but the user is actually not a teacher return e("#50"); } Ok(Ok(core::ForeignUserData { foreign_id: user_data.userId.ok_or(er("#60"))?, foreign_type: match user_data.userType.as_ref() { "a" | "A" => UserType::Admin, "t" | "T" => UserType::Teacher, "s" | "S" => UserType::User, _ => UserType::User, }, sex: match user_data.gender.as_ref() { "m" | "M" => UserSex::Male, "f" | "F" | "w" | "W" => UserSex::Female, "?" => UserSex::Unknown, _ => UserSex::Unknown, }, firstname: user_data.firstName, lastname: user_data.lastName })) } // Share Database connection between workers #[derive(Copy, Clone)] pub struct SharedDatabaseConnection where C: MedalConnection { phantom: std::marker::PhantomData, } impl Key for SharedDatabaseConnection where C: MedalConnection + 'static { type Value = C; } // Share Configuration between workers #[derive(Copy, Clone)] pub struct SharedConfiguration; impl Key for SharedConfiguration { type Value = Config; } #[cfg(feature = "watch")] pub fn get_handlebars_engine(template_name: &str) -> impl AfterMiddleware { // HandlebarsEngine will look up all files with "./examples/templates/**/*.hbs" let mut hbse = HandlebarsEngine::new(); hbse.add(Box::new(DirectorySource::new(&format!("./templates/{}/", template_name) as &str, ".hbs"))); // load templates from all registered sources if let Err(r) = hbse.reload() { panic!("{}", r); } use handlebars_iron::Watchable; use std::sync::Arc; let hbse_ref = Arc::new(hbse); hbse_ref.watch("./templates/"); hbse_ref } #[cfg(not(feature = "watch"))] pub fn get_handlebars_engine(template_name: &str) -> impl AfterMiddleware { // HandlebarsEngine will look up all files with "./templates/