mnml/server/src/http.rs

530 lines
17 KiB
Rust

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::{SmtpTransport};
// 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 name is unacceptable. 20 char max")]
AccountNameUnacceptable,
#[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<bcrypt::BcryptError> for MnmlHttpError {
fn from(err: bcrypt::BcryptError) -> Self {
warn!("{:?}", err);
MnmlHttpError::ServerError
}
}
impl From<postgres::Error> 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<r2d2::Error> for MnmlHttpError {
fn from(err: r2d2::Error) -> Self {
warn!("{:?}", err);
MnmlHttpError::DbError
}
}
impl From<failure::Error> 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::<Mime>().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::<Mime>().unwrap();
return Response::with((content_type, status, object));
}
impl From<MnmlHttpError> 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::AccountNameUnacceptable |
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::<Read<State>>().unwrap();
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
match req.headers.get::<CookieHdr>() {
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::<account::Account>(a),
Err(_) => return Err(MnmlHttpError::TokenDoesNotMatch.into()),
};
}
}
}
None => (),
};
Ok(())
}
}
struct ErrorHandler;
impl AfterMiddleware for ErrorHandler {
fn catch(&self, _: &mut Request, mut err: IronError) -> IronResult<Response> {
// 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<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<RegisterBody>>() {
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(&params.name, &params.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<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<LoginBody>>() {
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, &params.name, &params.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<Response> {
let state = req.get::<Read<State>>().unwrap();
match req.extensions.get::<account::Account>() {
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<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<EmailPost>>() {
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, &params.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::<Write<Mailer>>().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<Response> {
let state = req.get::<Read<State>>().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::<UrlEncodedQuery>() {
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<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<SetPassword>>() {
Ok(Some(b)) => b,
_ => return Err(MnmlHttpError::BadRequest.into()),
};
match req.extensions.get::<account::Account>() {
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, &params.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<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<EmailPost>>() {
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::<account::Account>() {
Some(a) => {
let (_id, token) = match mail::set(&mut tx, a.id, &params.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::<Write<Mailer>>().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<Response> {
let state = req.get::<Read<State>>().unwrap();
let account = match req.extensions.get::<account::Account>() {
Some(a) => a.clone(),
None => return Err(MnmlHttpError::Unauthorized.into()),
};
match req.get_ref::<UrlEncodedQuery>() {
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::<State>::both(State { pool }));
chain.link(Write::<Mailer>::both(Mailer { mailer }));
chain.link_before(Read::<bodyparser::MaxBodyLength>::one(MAX_BODY_LENGTH));
chain.link_before(AuthMiddleware);
chain.link_after(ErrorHandler);
Iron::new(chain).http("127.0.0.1:40000").unwrap();
}