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::{typemap, BeforeMiddleware,AfterMiddleware}; use persistent::Read; use router::Router; use serde::{Serialize, Deserialize}; use account; 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="account name taken or invalid")] AccountNameNotProvided, #[fail(display="account name not provided")] AccountNotFound, #[fail(display="account not found")] AccountNameTaken, #[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, #[fail(display="invalid code. https://discord.gg/YJJgurM")] InvalidCode, } impl From for MnmlHttpError { fn from(_err: bcrypt::BcryptError) -> Self { MnmlHttpError::ServerError } } impl From for MnmlHttpError { fn from(_err: postgres::Error) -> Self { MnmlHttpError::DbError } } impl From for MnmlHttpError { fn from(_err: failure::Error) -> Self { MnmlHttpError::ServerError } } #[derive(Serialize, Deserialize)] struct JsonResponse { response: Option, success: bool, error_message: Option } impl JsonResponse { fn success(response: String) -> Self { JsonResponse { response: Some(response), success: true, error_message: None } } fn error(msg: String) -> Self { JsonResponse { response: None, success: false, error_message: Some(msg) } } } fn iron_response (status: status::Status, message: String) -> Response { let content_type = "application/json".parse::().unwrap(); let msg = match status { status::Ok => JsonResponse::success(message), _ => JsonResponse::error(message) }; let msg_out = serde_json::to_string(&msg).unwrap(); return Response::with((content_type, status, msg_out)); } impl From for IronError { fn from(m_err: MnmlHttpError) -> Self { let (err, res) = match m_err { MnmlHttpError::ServerError | MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError), MnmlHttpError::AccountNameNotProvided | MnmlHttpError::AccountNameTaken | MnmlHttpError::AccountNotFound | MnmlHttpError::BadRequest | MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest), MnmlHttpError::PasswordNotMatch | MnmlHttpError::InvalidCode | MnmlHttpError::TokenDoesNotMatch | MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized), }; IronError { error: Box::new(err), response: iron_response(res, m_err.to_string()) } } } 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(IronError::from(MnmlHttpError::TokenDoesNotMatch)), }; } } } 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 = iron_response(status::Ok, "token_res".to_string()); res.headers.set(SetCookie(vec![v.to_string()])); return res; } #[derive(Debug,Clone,Deserialize)] struct RegisterBody { name: String, password: String, code: String, } fn register(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let params = match req.get::>() { Ok(Some(b)) => b, _ => return Err(IronError::from(MnmlHttpError::BadRequest)), }; 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, ¶ms.code, &mut tx) { Ok(token) => { tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(token_res(token)) }, Err(e) => { warn!("{:?}", e); Err(IronError::from(e)) } } } #[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(IronError::from(MnmlHttpError::BadRequest)), }; 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(IronError::from(e)) } } } 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 = iron_response(status::Ok, "logout".to_string()); res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()])); Ok(res) }, None => Err(IronError::from(MnmlHttpError::Unauthorized)), } } #[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(IronError::from(MnmlHttpError::BadRequest)), }; 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(IronError::from(MnmlHttpError::Unauthorized)), } } const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10; pub struct State { pub pool: PgPool, // pub events: Events, } impl Key for State { type Value = State; } pub fn start(pool: PgPool) { let mut router = Router::new(); // auth router.post("/api/account/login", login, "login"); router.post("/api/account/logout", logout, "logout"); router.post("/api/account/register", register, "register"); router.post("/api/account/password", set_password, "set_password"); router.post("/api/account/email", logout, "email"); // payments router.post("/api/payments/stripe", stripe, "stripe"); let mut chain = Chain::new(router); chain.link(Read::::both(State { pool })); 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(); }