use chrono::Duration; use cookie::{Cookie, SameSite}; use failure::Fail; use iron::headers::{Cookie as CookieHdr, SetCookie}; use iron::prelude::*; use iron::status; use iron::typemap::Key; use iron::mime::Mime; use iron::modifiers::Redirect; use iron::Url; use iron::{typemap, BeforeMiddleware,AfterMiddleware}; use urlencoded::UrlEncodedQuery; use persistent::{Read, Write}; use router::Router; use mount::{Mount}; use serde::{Serialize, Deserialize}; use lettre::{SendableEmail, SmtpClient, SmtpTransport, Transport}; use acp; use account; use mail; use mail::Mail; use pg::PgPool; use payments::{stripe}; pub const TOKEN_HEADER: &str = "x-auth-token"; pub const AUTH_CLEAR: &str = "x-auth-token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=-1;"; #[derive(Clone, Copy, Fail, Debug, Serialize, Deserialize)] pub enum MnmlHttpError { // User Facing Errors #[fail(display="internal server error")] ServerError, #[fail(display="internal server error")] DbError, #[fail(display="unauthorized")] Unauthorized, #[fail(display="bad request")] BadRequest, #[fail(display="not found")] NotFound, #[fail(display="account name not provided")] AccountNameNotProvided, #[fail(display="account name unavailable")] AccountNameUnavailable, #[fail(display="account not found")] AccountNotFound, #[fail(display="password does not match")] PasswordNotMatch, #[fail(display="password unacceptable. must be > 11 characters")] PasswordUnacceptable, #[fail(display="incorrect token. refresh or logout of existing sessions")] TokenDoesNotMatch, } impl From for MnmlHttpError { fn from(err: bcrypt::BcryptError) -> Self { warn!("{:?}", err); MnmlHttpError::ServerError } } impl From for MnmlHttpError { fn from(err: postgres::Error) -> Self { warn!("{:?}", err); match err.as_db() { Some(db) => { let constraint = match db.constraint { Some(ref c) => c, None => return MnmlHttpError::DbError, }; match constraint.as_ref() { "accounts_name_unique" => MnmlHttpError::AccountNameUnavailable, _ => MnmlHttpError::DbError, } }, _ => MnmlHttpError::DbError, } } } impl From for MnmlHttpError { fn from(err: r2d2::Error) -> Self { warn!("{:?}", err); MnmlHttpError::DbError } } impl From for MnmlHttpError { fn from(err: failure::Error) -> Self { warn!("{:?}", err); MnmlHttpError::ServerError } } #[derive(Serialize, Deserialize)] #[serde(rename_all(serialize = "lowercase"))] pub enum Json { Error(String), Message(String), } pub fn json_response(status: status::Status, response: Json) -> Response { let content_type = "application/json".parse::().unwrap(); let json = serde_json::to_string(&response).unwrap(); return Response::with((content_type, status, json)); } pub fn json_object(status: status::Status, object: String) -> Response { let content_type = "application/json".parse::().unwrap(); return Response::with((content_type, status, object)); } impl From for IronError { fn from(m_err: MnmlHttpError) -> Self { let (err, status) = match m_err { MnmlHttpError::ServerError | MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError), MnmlHttpError::AccountNameNotProvided | MnmlHttpError::AccountNameUnavailable | MnmlHttpError::AccountNotFound | MnmlHttpError::BadRequest | MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest), MnmlHttpError::PasswordNotMatch | MnmlHttpError::TokenDoesNotMatch | MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized), MnmlHttpError::NotFound => (m_err.compat(), status::NotFound), }; let response = json_response(status, Json::Error(m_err.to_string())); IronError { error: Box::new(err), response } } } impl typemap::Key for account::Account { type Value = account::Account; } struct AuthMiddleware; impl BeforeMiddleware for AuthMiddleware { fn before(&self, req: &mut Request) -> IronResult<()> { let state = req.get::>().unwrap(); let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; match req.headers.get::() { Some(cookies) => { for c in cookies.iter() { let cookie = Cookie::parse(c) .or(Err(MnmlHttpError::BadRequest))?; // got auth token if cookie.name() == TOKEN_HEADER { match account::from_token(&db, &cookie.value().to_string()) { Ok(a) => req.extensions.insert::(a), Err(_) => return Err(MnmlHttpError::TokenDoesNotMatch.into()), }; } } } None => (), }; Ok(()) } } struct ErrorHandler; impl AfterMiddleware for ErrorHandler { fn catch(&self, _: &mut Request, mut err: IronError) -> IronResult { // on unauthorized we clear the auth token match err.response.status { Some(status::Unauthorized) => { err.response.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()])); }, _ => (), }; warn!("{:?}", err); return Err(err); } } fn token_res(token: String) -> Response { let v = Cookie::build(TOKEN_HEADER, token) .http_only(true) .same_site(SameSite::Strict) .path("/") .max_age(Duration::weeks(1)) // 1 week aligns with db set .finish(); let mut res = json_response( status::Ok, Json::Message("authenticated".to_string()) ); res.headers.set(SetCookie(vec![v.to_string()])); return res; } #[derive(Debug,Clone,Deserialize)] struct RegisterBody { name: String, password: String, } fn register(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(MnmlHttpError::BadRequest.into()), }; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; match account::create(¶ms.name, ¶ms.password, &mut tx) { Ok(token) => { tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(token_res(token)) }, Err(e) => { warn!("{:?}", e); Err(e.into()) } } } #[derive(Debug,Clone,Deserialize)] struct LoginBody { name: String, password: String, } fn login(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(MnmlHttpError::BadRequest.into()), }; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; match account::login(&mut tx, ¶ms.name, ¶ms.password) { Ok(a) => { let token = account::new_token(&mut tx, a.id)?; tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(token_res(token)) }, Err(e) => { warn!("{:?}", e); Err(e.into()) } } } fn logout(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); match req.extensions.get::() { Some(a) => { let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; account::new_token(&mut tx, a.id)?; tx.commit().or(Err(MnmlHttpError::ServerError))?; let mut res = json_response(status::Ok, Json::Message("logged out".to_string())); res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()])); Ok(res) }, None => Err(MnmlHttpError::Unauthorized.into()), } } fn recover_set(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(MnmlHttpError::BadRequest.into()), }; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let user_email = match mail::select(&db, ¶ms.email) { Ok(e) => match e.confirmed { true => e, false => return Ok(json_response(status::NotFound, Json::Error("your email is not confirmed.\nplease contact support at humans@mnml.gg".to_string()))), }, Err(_e) => return Ok(json_response(status::NotFound, Json::Error("email not registered.\nplease contact support at humans@mnml.gg".to_string()))), }; let account = account::select(&db, user_email.account) .or(Err(MnmlHttpError::NotFound))?; let token = mail::set_recovery(&mut tx, &user_email.email) .or(Err(MnmlHttpError::ServerError))?; let app_mailer = req.get::>().unwrap(); let mut lock = app_mailer.lock().unwrap(); let message = Mail::Recover { email: user_email.email.clone(), name: account.name.clone(), token }; let send = match mail::send_mail(&mut lock.mailer, message) { Ok(send) => send, Err(e) => { warn!("{:?}", e); return Err(MnmlHttpError::ServerError.into()); } }; tx.commit().or(Err(MnmlHttpError::ServerError))?; info!("recovery email sent send={:?} account={:?} email={:?}", send, account, user_email.email); Ok(json_response(status::Ok, Json::Message("recovery email sent. check your mailbox for access".to_string()))) } fn recover(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let token = match req.get_ref::() { Ok(ref hashmap) => { match hashmap.get("recover_token") { Some(t) => &t[0], None => return Err(MnmlHttpError::BadRequest.into()), } }, Err(_) => return Err(MnmlHttpError::BadRequest.into()), }; let user_email = match mail::get_recovery(&mut tx, &token.to_string()) { Ok(a) => a, Err(_) => return Err(MnmlHttpError::Unauthorized.into()), }; let token = account::new_token(&mut tx, user_email.account) .or(Err(MnmlHttpError::ServerError))?; let account = account::from_token(&db, &token) .or(Err(MnmlHttpError::ServerError))?; let v = Cookie::build(TOKEN_HEADER, token) .http_only(true) .same_site(SameSite::Strict) .path("/") .max_age(Duration::weeks(1)) // 1 week aligns with db set .finish(); tx.commit().or(Err(MnmlHttpError::ServerError))?; let mut res = Response::with((status::SeeOther, Redirect(Url::parse("https://mnml.gg").unwrap()))); res.headers.set(SetCookie(vec![v.to_string()])); info!("recovered account account={:?}", account); Ok(res) } #[derive(Debug,Clone,Deserialize)] struct SetPassword { current: String, password: String, } fn set_password(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(MnmlHttpError::BadRequest.into()), }; match req.extensions.get::() { Some(a) => { let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let token = account::set_password(&mut tx, a.id, ¶ms.current, ¶ms.password)?; tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(token_res(token)) }, None => Err(MnmlHttpError::Unauthorized.into()), } } #[derive(Debug,Clone,Deserialize)] struct EmailPost { email: String, } fn email_set(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(MnmlHttpError::BadRequest.into()), }; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let (email, account, token) = match req.extensions.get::() { Some(a) => { let (_id, token) = match mail::set(&mut tx, a.id, ¶ms.email) { Ok(res) => res, Err(e) => { warn!("{:?}", e); return Err(MnmlHttpError::ServerError.into()); }, }; (params.email.clone(), a.clone(), token) }, None => return Err(MnmlHttpError::Unauthorized.into()), }; let app_mailer = req.get::>().unwrap(); let mut lock = app_mailer.lock().unwrap(); let message = Mail::Confirm { email: email.clone(), name: account.name.clone(), token }; let send = match mail::send_mail(&mut lock.mailer, message) { Ok(send) => send, Err(e) => { warn!("{:?}", e); return Err(MnmlHttpError::ServerError.into()); } }; tx.commit().or(Err(MnmlHttpError::ServerError))?; info!("confirmation email sent send={:?} account={:?} email={:?}", send, account, email); Ok(json_response(status::Ok, Json::Message("email set. confirmation required".to_string()))) } fn email_confirm(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let account = match req.extensions.get::() { Some(a) => a.clone(), None => return Err(MnmlHttpError::Unauthorized.into()), }; match req.get_ref::() { Ok(ref hashmap) => { let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let token = match hashmap.get("confirm_token") { Some(t) => &t[0], None => return Err(MnmlHttpError::BadRequest.into()), }; let confirmation = match mail::confirm_email(&mut tx, &account, token.to_string()) { Ok(c) => c, Err(_) => return Err(MnmlHttpError::NotFound.into()) }; info!("email confirmed email={:?} account={:?}", confirmation.0, account); tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(Response::with((status::Found, Redirect(Url::parse("https://mnml.gg").unwrap())))) }, Err(_) => Err(MnmlHttpError::BadRequest.into()), } } const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10; pub struct State { pub pool: PgPool, } pub struct Mailer { pub mailer: SmtpTransport, } impl Key for State { type Value = State; } impl Key for Mailer { type Value = Mailer; } fn account_mount() -> Router { let mut router = Router::new(); router.post("login", login, "login"); router.post("logout", logout, "logout"); router.post("register", register, "register"); router.post("password", set_password, "set_password"); router.post("email", email_set, "email_set"); router.post("recover", recover_set, "recover_set"); // it is sent in an email... router.get("email/confirm", email_confirm, "email_confirm"); router.get("recover", recover, "recover"); router } fn payment_mount() -> Router { let mut router = Router::new(); router.post("stripe", stripe, "stripe"); router } pub fn start(pool: PgPool, mailer: SmtpTransport) { let mut mounts = Mount::new(); mounts.mount("/api/account/", account_mount()); mounts.mount("/api/payments/", payment_mount()); mounts.mount("/api/acp/", acp::acp_mount()); let mut chain = Chain::new(mounts); chain.link(Read::::both(State { pool })); chain.link(Write::::both(Mailer { mailer })); chain.link_before(Read::::one(MAX_BODY_LENGTH)); chain.link_before(AuthMiddleware); chain.link_after(ErrorHandler); Iron::new(chain).http("127.0.0.1:40000").unwrap(); }