diff --git a/client/src/components/login.jsx b/client/src/components/login.jsx index dca8ade2..2d76d3ad 100644 --- a/client/src/components/login.jsx +++ b/client/src/components/login.jsx @@ -3,16 +3,45 @@ const preact = require('preact'); const { Component } = require('preact') const { connect } = require('preact-redux'); +const SERVER = process.env.NODE_ENV === 'production' ? '/' : 'http://localhost:40000'; + +function postData(url = '/', data = {}) { + // Default options are marked with * + return fetch(url, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + // mode: "no-cors", // no-cors, cors, *same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "include", // include, same-origin, *omit + headers: { + 'Accept': 'application/json', + 'content-type': 'application/json' + }, + redirect: "error", // manual, *follow, error + // referrer: "", // no-referrer, *client + body: JSON.stringify(data), // body data type must match "Content-Type" header + }) + .then(response => response.json()); // parses response to JSON +} + const addState = connect( - (state) => { - const { ws, account } = state; + null, + (dispatch) => { function submitLogin(name, password) { - return ws.sendAccountLogin(name, password); + postData(`${SERVER}/login`, { name, password }) + .then(data => console.log(JSON.stringify(data))) + .catch(error => console.error(error)); } + function submitRegister(name, password, code) { - return ws.sendAccountCreate(name, password, code); + postData(`${SERVER}/register`, { name, password, code }) + .then(data => console.log(JSON.stringify(data))) + .catch(error => console.error(error)); + } + + return { + submitLogin, + submitRegister, } - return { account, submitLogin, submitRegister }; }, ); diff --git a/client/src/socket.jsx b/client/src/socket.jsx index d3e84d3b..6d3b6849 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -15,21 +15,20 @@ function errorToast(err) { function createSocket(events) { let ws; - // handle account auth within the socket itself - // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html - let account; - try { - account = JSON.parse(localStorage.getItem('account')); - } catch (e) { - localStorage.removeItem('account'); - } + // // handle account auth within the socket itself + // // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html + // let account; + // try { + // account = JSON.parse(localStorage.getItem('account')); + // } catch (e) { + // localStorage.removeItem('account'); + // } // ------------- // Outgoing // ------------- function send(msg) { if (msg.method !== 'ping') console.log('outgoing msg', msg); - msg.token = account && account.token && account.token; ws.send(cbor.encode(msg)); } @@ -144,8 +143,6 @@ function createSocket(events) { // Incoming // ------------- function onAccount(login) { - account = login; - localStorage.setItem('account', JSON.stringify(login)); events.setAccount(login); sendAccountConstructs(); sendAccountInstances(); @@ -231,7 +228,6 @@ function createSocket(events) { function errHandler(error) { switch (error) { case 'invalid token': return logout(); - case 'no active zone': return sendZoneCreate(); case 'no constructs selected': return events.errorPrompt('select_constructs'); case 'node requirements not met': return events.errorPrompt('complete_nodes'); case 'construct at max skills (4)': return events.errorPrompt('max_skills'); @@ -249,7 +245,7 @@ function createSocket(events) { const res = cbor.decode(blob); const [msgType, params] = res; - if (msgType !== 'pong') console.log(res); + if (msgType !== 'Pong') console.log(res); // check for error and split into response type and data if (res.err) return errHandler(res.err); @@ -271,13 +267,6 @@ function createSocket(events) { sendPing(); sendItemInfo(); - if (account) { - events.setAccount(account); - sendAccountInstances(); - sendInstanceList(); - sendAccountConstructs(); - } - return true; }); diff --git a/server/.env b/server/.env index c5e1d4fe..cf48a7c1 100644 --- a/server/.env +++ b/server/.env @@ -1 +1,2 @@ DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml +DEV_CORS=true diff --git a/server/src/account.rs b/server/src/account.rs index 80dc7f7f..5cc62349 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -7,8 +7,6 @@ use serde_cbor::{from_slice}; use postgres::transaction::Transaction; -use rpc::{AccountCreateParams, AccountLoginParams}; - use construct::{Construct, construct_recover}; use instance::{Instance, instance_delete}; @@ -58,23 +56,22 @@ pub fn account_from_token(token: String, tx: &mut Transaction) -> Result Result { - let id = Uuid::new_v4(); - - if params.password.len() < PASSWORD_MIN_LEN { +pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result { + if password.len() < PASSWORD_MIN_LEN { return Err(err_msg("password must be at least 12 characters")); } - if params.code.to_lowercase() != "grep842" { + if code.to_lowercase() != "grep842" { return Err(err_msg("https://discord.gg/YJJgurM")); } - if params.name.len() == 0 { + if name.len() == 0 { return Err(err_msg("account name not supplied")); } + let id = Uuid::new_v4(); let rounds = 8; - let password = hash(¶ms.password, rounds)?; + let password = hash(&password, rounds)?; let mut rng = thread_rng(); let token: String = iter::repeat(()) @@ -82,77 +79,72 @@ pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Resu .take(64) .collect(); - let account = AccountEntry { - name: params.name, - id, - password, - token, - }; - let query = " INSERT INTO accounts (id, name, password, token) VALUES ($1, $2, $3, $4) RETURNING id, name, token; "; - let result = tx - .query(query, &[&account.id, &account.name, &account.password, &account.token])?; + .query(query, &[&id, &name, &password, &token])?; - let returned = result.iter().next().expect("no row returned"); + if result.is_empty() { + return Err(err_msg("no row returned")); + } - let entry = Account { - id: returned.get(0), - name: returned.get(1), - token: returned.get(2), - }; + info!("registration account={:?}", name); - info!("registration account={:?}", entry.name); - - return Ok(entry); + return Ok(token); } -pub fn account_login(params: AccountLoginParams, tx: &mut Transaction) -> Result { +pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result { let query = " - SELECT id, name, token, password + SELECT id, password FROM accounts WHERE name = $1; "; let result = tx - .query(query, &[¶ms.name])?; + .query(query, &[&name])?; + + let mut rng = thread_rng(); + let token: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); let returned = match result.iter().next() { Some(row) => row, - // MAYBE - // verify gibberish to delay response for timing attacks - None => return Err(err_msg("account not found")), + None => { + // verify garbage to prevent timing attacks + verify(token.clone(), &token).ok(); + return Err(err_msg("account not found")); + }, }; - let entry = AccountEntry { - id: returned.get(0), - name: returned.get(1), - token: returned.get(2), - password: returned.get(3), - }; + let id: Uuid = returned.get(0); + let hash: String = returned.get(1); - if !verify(¶ms.password, &entry.password)? { + if !verify(password, &hash)? { return Err(err_msg("password does not match")); } - info!("login account={:?}", entry.name); + // update token + let query = " + UPDATE accounts + SET token = $1, updated_at = now() + WHERE id = $2 + RETURNING id; + "; - // MAYBE - // update token? - // don't necessarily want to log every session out when logging in + let result = tx + .query(query, &[&token, &id])?; - let account = Account { - id: entry.id, - name: entry.name, - token: entry.token, - }; + result.iter().next().ok_or(format_err!("user {:?} could not be updated", id))?; - return Ok(account); + info!("login account={:?}", name); + + return Ok(token); } pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result, Error> { diff --git a/server/src/net.rs b/server/src/net.rs index 0543f566..7e0ba83f 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -1,18 +1,26 @@ use std::time::{Instant, Duration}; use std::env; +use chrono::Duration as ChronoDuration; + +use failure::err_msg; + use serde_cbor::{to_vec}; use actix::prelude::*; use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::middleware::cors::Cors; +use actix_web::error::ResponseError; +use actix_web::http::{StatusCode, Cookie}; use actix_web_actors::ws; use r2d2::{Pool}; use r2d2::{PooledConnection}; use r2d2_postgres::{TlsMode, PostgresConnectionManager}; -use rpc::{receive, RpcErrorResponse}; +use rpc::{receive, RpcErrorResponse, AccountLoginParams, AccountCreateParams}; use warden::{warden}; +use account::{Account, account_login, account_create}; pub type Db = PooledConnection; type PgPool = Pool; @@ -103,18 +111,80 @@ impl MnmlSocket { } } +#[derive(Fail, Debug)] +enum MnmlError { + #[fail(display="internal server error")] + ServerError, + #[fail(display="unauthorized")] + Unauthorized, + #[fail(display="bad request")] + BadRequest, +} + +impl ResponseError for MnmlError { + fn error_response(&self) -> HttpResponse { + match *self { + MnmlError::ServerError => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + MnmlError::Unauthorized => HttpResponse::new(StatusCode::UNAUTHORIZED), + MnmlError::BadRequest => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + // idk how this stuff works // but the args extract what you need from the incoming requests // this grabs // the req obj itself which we need for cookies // the application state // and the websocket stream -fn ws_index(r: HttpRequest, state: web::Data, stream: web::Payload) -> Result { - let res = ws::start(MnmlSocket::new(state), &r, stream); +fn ws(r: HttpRequest, state: web::Data, stream: web::Payload) -> Result { + ws::start(MnmlSocket::new(state), &r, stream) +} - // response of upgrade being sent back - // info!("res={:?}", res.as_ref().unwrap()); - res +fn login(state: web::Data, params: web::Json::) -> Result { + let db = state.pool.get().or(Err(MnmlError::ServerError))?; + let mut tx = db.transaction().or(Err(MnmlError::ServerError))?; + + match account_login(¶ms.name, ¶ms.password, &mut tx) { + Ok(token) => { + tx.commit().or(Err(MnmlError::ServerError))?; + Ok(HttpResponse::Ok() + .cookie(Cookie::build("x-auth-token", token) + // .path("/") + .http_only(true) + // .secure(true) + .max_age(60 * 60 * 24 * 7) // 1 week + .finish()) + .finish()) + }, + Err(e) => { + info!("{:?}", e); + Err(MnmlError::Unauthorized) + } + } +} + +fn register(state: web::Data, params: web::Json::) -> Result { + let db = state.pool.get().or(Err(MnmlError::ServerError))?; + let mut tx = db.transaction().or(Err(MnmlError::ServerError))?; + + match account_create(¶ms.name, ¶ms.password, ¶ms.code, &mut tx) { + Ok(token) => { + tx.commit().or(Err(MnmlError::ServerError))?; + Ok(HttpResponse::Created() + .cookie(Cookie::build("x-auth-token", token) + .path("/") + .http_only(true) + .secure(true) + .max_age(60 * 60 * 24 * 7) // 1 week + .finish()) + .finish()) + }, + Err(e) => { + info!("{:?}", e); + Err(MnmlError::Unauthorized) + } + } } fn create_pool(url: String) -> Pool { @@ -137,14 +207,27 @@ pub fn start() { let pool = create_pool(database_url); - HttpServer::new(move || { - App::new() - .data(State { pool: pool.clone() }) - // enable logger - .wrap(middleware::Logger::default()) - // websocket route - .service(web::resource("/ws/").route(web::get().to(ws_index))) - }) - .bind("127.0.0.1:40000").expect("could not bind to port") - .run().expect("could not start http server"); + match env::var("DEV_CORS") { + Ok(_) => { + warn!("enabling dev CORS middleware"); + HttpServer::new(move || App::new() + .data(State { pool: pool.clone() }) + .wrap(middleware::Logger::default()) + .wrap(Cors::new().supports_credentials()) + .service(web::resource("/login").route(web::post().to(login))) + .service(web::resource("/register").route(web::post().to(register))) + .service(web::resource("/ws/").route(web::get().to(ws)))) + .bind("127.0.0.1:40000").expect("could not bind to port") + .run().expect("could not start http server") + }, + Err(_) => + HttpServer::new(move || App::new() + .data(State { pool: pool.clone() }) + .wrap(middleware::Logger::default()) + .service(web::resource("/login").route(web::post().to(login))) + .service(web::resource("/register").route(web::post().to(register))) + .service(web::resource("/ws/").route(web::get().to(ws)))) + .bind("127.0.0.1:40000").expect("could not bind to port") + .run().expect("could not start http server"), + } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index d2271225..75fcfa5d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -11,7 +11,7 @@ use failure::err_msg; use net::{Db, MnmlSocket}; use construct::{Construct, construct_spawn, construct_delete}; use game::{Game, game_state, game_skill, game_ready}; -use account::{Account, account_create, account_login, account_from_token, account_constructs, account_instances}; +use account::{Account, account_from_token, account_constructs, account_instances}; use skill::{Skill}; use instance::{Instance, instance_state, instance_list, instance_new, instance_ready, instance_join}; use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip}; @@ -43,7 +43,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> // if no auth required match v.method.as_ref() { "account_create" => (), - "account_login" => (), + // "account_login" => (), "item_info" => (), _ => match account { Some(_) => (), @@ -55,8 +55,8 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> // match on that to determine what fn to call let response = match v.method.as_ref() { // NO AUTH - "account_create" => handle_account_create(data, &mut tx), - "account_login" => handle_account_login(data, &mut tx), + // "account_create" => handle_account_create(data, &mut tx), + // "account_login" => handle_account_login(data, &mut tx), "item_info" => Ok(RpcResult::ItemInfo(item_info())), // AUTH METHODS @@ -139,16 +139,16 @@ fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account } -fn handle_account_create(data: Vec, tx: &mut Transaction) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - let account = account_create(msg.params, tx)?; - Ok(RpcResult::Account(account)) -} +// fn handle_account_create(data: Vec, tx: &mut Transaction) -> Result { +// let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +// let account = account_create(msg.params, tx)?; +// Ok(RpcResult::Account(account)) +// } -fn handle_account_login(data: Vec, tx: &mut Transaction) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - Ok(RpcResult::Account(account_login(msg.params, tx)?)) -} +// fn handle_account_login(data: Vec, tx: &mut Transaction) -> Result { +// let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +// Ok(RpcResult::Account(account_login(msg.params, tx)?)) +// } fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: Account) -> Result { Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?))