a mighty battle against an evil opponent

This commit is contained in:
ntr 2019-06-15 00:00:29 +10:00
parent 5c6e587044
commit 66462ac670
6 changed files with 198 additions and 104 deletions

View File

@ -3,16 +3,45 @@ const preact = require('preact');
const { Component } = require('preact') const { Component } = require('preact')
const { connect } = require('preact-redux'); 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( const addState = connect(
(state) => { null,
const { ws, account } = state; (dispatch) => {
function submitLogin(name, password) { 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) { 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 };
}, },
); );

View File

@ -15,21 +15,20 @@ function errorToast(err) {
function createSocket(events) { function createSocket(events) {
let ws; let ws;
// handle account auth within the socket itself // // handle account auth within the socket itself
// https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html // // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
let account; // let account;
try { // try {
account = JSON.parse(localStorage.getItem('account')); // account = JSON.parse(localStorage.getItem('account'));
} catch (e) { // } catch (e) {
localStorage.removeItem('account'); // localStorage.removeItem('account');
} // }
// ------------- // -------------
// Outgoing // Outgoing
// ------------- // -------------
function send(msg) { function send(msg) {
if (msg.method !== 'ping') console.log('outgoing msg', msg); if (msg.method !== 'ping') console.log('outgoing msg', msg);
msg.token = account && account.token && account.token;
ws.send(cbor.encode(msg)); ws.send(cbor.encode(msg));
} }
@ -144,8 +143,6 @@ function createSocket(events) {
// Incoming // Incoming
// ------------- // -------------
function onAccount(login) { function onAccount(login) {
account = login;
localStorage.setItem('account', JSON.stringify(login));
events.setAccount(login); events.setAccount(login);
sendAccountConstructs(); sendAccountConstructs();
sendAccountInstances(); sendAccountInstances();
@ -231,7 +228,6 @@ function createSocket(events) {
function errHandler(error) { function errHandler(error) {
switch (error) { switch (error) {
case 'invalid token': return logout(); case 'invalid token': return logout();
case 'no active zone': return sendZoneCreate();
case 'no constructs selected': return events.errorPrompt('select_constructs'); case 'no constructs selected': return events.errorPrompt('select_constructs');
case 'node requirements not met': return events.errorPrompt('complete_nodes'); case 'node requirements not met': return events.errorPrompt('complete_nodes');
case 'construct at max skills (4)': return events.errorPrompt('max_skills'); case 'construct at max skills (4)': return events.errorPrompt('max_skills');
@ -249,7 +245,7 @@ function createSocket(events) {
const res = cbor.decode(blob); const res = cbor.decode(blob);
const [msgType, params] = res; 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 // check for error and split into response type and data
if (res.err) return errHandler(res.err); if (res.err) return errHandler(res.err);
@ -271,13 +267,6 @@ function createSocket(events) {
sendPing(); sendPing();
sendItemInfo(); sendItemInfo();
if (account) {
events.setAccount(account);
sendAccountInstances();
sendInstanceList();
sendAccountConstructs();
}
return true; return true;
}); });

View File

@ -1 +1,2 @@
DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml
DEV_CORS=true

View File

@ -7,8 +7,6 @@ use serde_cbor::{from_slice};
use postgres::transaction::Transaction; use postgres::transaction::Transaction;
use rpc::{AccountCreateParams, AccountLoginParams};
use construct::{Construct, construct_recover}; use construct::{Construct, construct_recover};
use instance::{Instance, instance_delete}; use instance::{Instance, instance_delete};
@ -58,23 +56,22 @@ pub fn account_from_token(token: String, tx: &mut Transaction) -> Result<Account
return Ok(entry); return Ok(entry);
} }
pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Result<Account, Error> { pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result<String, Error> {
let id = Uuid::new_v4(); if password.len() < PASSWORD_MIN_LEN {
if params.password.len() < PASSWORD_MIN_LEN {
return Err(err_msg("password must be at least 12 characters")); 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")); 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")); return Err(err_msg("account name not supplied"));
} }
let id = Uuid::new_v4();
let rounds = 8; let rounds = 8;
let password = hash(&params.password, rounds)?; let password = hash(&password, rounds)?;
let mut rng = thread_rng(); let mut rng = thread_rng();
let token: String = iter::repeat(()) let token: String = iter::repeat(())
@ -82,77 +79,72 @@ pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Resu
.take(64) .take(64)
.collect(); .collect();
let account = AccountEntry {
name: params.name,
id,
password,
token,
};
let query = " let query = "
INSERT INTO accounts (id, name, password, token) INSERT INTO accounts (id, name, password, token)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, name, token; RETURNING id, name, token;
"; ";
let result = tx 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={:?}", entry.name);
return Ok(entry);
} }
pub fn account_login(params: AccountLoginParams, tx: &mut Transaction) -> Result<Account, Error> { info!("registration account={:?}", name);
return Ok(token);
}
pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result<String, Error> {
let query = " let query = "
SELECT id, name, token, password SELECT id, password
FROM accounts FROM accounts
WHERE name = $1; WHERE name = $1;
"; ";
let result = tx let result = tx
.query(query, &[&params.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() { let returned = match result.iter().next() {
Some(row) => row, Some(row) => row,
// MAYBE None => {
// verify gibberish to delay response for timing attacks // verify garbage to prevent timing attacks
None => return Err(err_msg("account not found")), verify(token.clone(), &token).ok();
return Err(err_msg("account not found"));
},
}; };
let entry = AccountEntry { let id: Uuid = returned.get(0);
id: returned.get(0), let hash: String = returned.get(1);
name: returned.get(1),
token: returned.get(2),
password: returned.get(3),
};
if !verify(&params.password, &entry.password)? { if !verify(password, &hash)? {
return Err(err_msg("password does not match")); 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 let result = tx
// update token? .query(query, &[&token, &id])?;
// don't necessarily want to log every session out when logging in
let account = Account { result.iter().next().ok_or(format_err!("user {:?} could not be updated", id))?;
id: entry.id,
name: entry.name,
token: entry.token,
};
return Ok(account); info!("login account={:?}", name);
return Ok(token);
} }
pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, Error> { pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, Error> {

View File

@ -1,18 +1,26 @@
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use std::env; use std::env;
use chrono::Duration as ChronoDuration;
use failure::err_msg;
use serde_cbor::{to_vec}; use serde_cbor::{to_vec};
use actix::prelude::*; use actix::prelude::*;
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; 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 actix_web_actors::ws;
use r2d2::{Pool}; use r2d2::{Pool};
use r2d2::{PooledConnection}; use r2d2::{PooledConnection};
use r2d2_postgres::{TlsMode, PostgresConnectionManager}; use r2d2_postgres::{TlsMode, PostgresConnectionManager};
use rpc::{receive, RpcErrorResponse}; use rpc::{receive, RpcErrorResponse, AccountLoginParams, AccountCreateParams};
use warden::{warden}; use warden::{warden};
use account::{Account, account_login, account_create};
pub type Db = PooledConnection<PostgresConnectionManager>; pub type Db = PooledConnection<PostgresConnectionManager>;
type PgPool = Pool<PostgresConnectionManager>; type PgPool = Pool<PostgresConnectionManager>;
@ -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 // idk how this stuff works
// but the args extract what you need from the incoming requests // but the args extract what you need from the incoming requests
// this grabs // this grabs
// the req obj itself which we need for cookies // the req obj itself which we need for cookies
// the application state // the application state
// and the websocket stream // and the websocket stream
fn ws_index(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> { fn ws(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
let res = ws::start(MnmlSocket::new(state), &r, stream); ws::start(MnmlSocket::new(state), &r, stream)
}
// response of upgrade being sent back fn login(state: web::Data<State>, params: web::Json::<AccountLoginParams>) -> Result<HttpResponse, MnmlError> {
// info!("res={:?}", res.as_ref().unwrap()); let db = state.pool.get().or(Err(MnmlError::ServerError))?;
res let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
match account_login(&params.name, &params.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<State>, params: web::Json::<AccountCreateParams>) -> Result<HttpResponse, MnmlError> {
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
match account_create(&params.name, &params.password, &params.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<PostgresConnectionManager> { fn create_pool(url: String) -> Pool<PostgresConnectionManager> {
@ -137,14 +207,27 @@ pub fn start() {
let pool = create_pool(database_url); let pool = create_pool(database_url);
HttpServer::new(move || { match env::var("DEV_CORS") {
App::new() Ok(_) => {
warn!("enabling dev CORS middleware");
HttpServer::new(move || App::new()
.data(State { pool: pool.clone() }) .data(State { pool: pool.clone() })
// enable logger
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
// websocket route .wrap(Cors::new().supports_credentials())
.service(web::resource("/ws/").route(web::get().to(ws_index))) .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") .bind("127.0.0.1:40000").expect("could not bind to port")
.run().expect("could not start http server"); .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"),
}
} }

View File

@ -11,7 +11,7 @@ use failure::err_msg;
use net::{Db, MnmlSocket}; use net::{Db, MnmlSocket};
use construct::{Construct, construct_spawn, construct_delete}; use construct::{Construct, construct_spawn, construct_delete};
use game::{Game, game_state, game_skill, game_ready}; 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 skill::{Skill};
use instance::{Instance, instance_state, instance_list, instance_new, instance_ready, instance_join}; 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}; use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip};
@ -43,7 +43,7 @@ pub fn receive(data: Vec<u8>, db: &Db, _client: &mut MnmlWs, begin: Instant) ->
// if no auth required // if no auth required
match v.method.as_ref() { match v.method.as_ref() {
"account_create" => (), "account_create" => (),
"account_login" => (), // "account_login" => (),
"item_info" => (), "item_info" => (),
_ => match account { _ => match account {
Some(_) => (), Some(_) => (),
@ -55,8 +55,8 @@ pub fn receive(data: Vec<u8>, db: &Db, _client: &mut MnmlWs, begin: Instant) ->
// match on that to determine what fn to call // match on that to determine what fn to call
let response = match v.method.as_ref() { let response = match v.method.as_ref() {
// NO AUTH // NO AUTH
"account_create" => handle_account_create(data, &mut tx), // "account_create" => handle_account_create(data, &mut tx),
"account_login" => handle_account_login(data, &mut tx), // "account_login" => handle_account_login(data, &mut tx),
"item_info" => Ok(RpcResult::ItemInfo(item_info())), "item_info" => Ok(RpcResult::ItemInfo(item_info())),
// AUTH METHODS // AUTH METHODS
@ -139,16 +139,16 @@ fn handle_construct_delete(data: Vec<u8>, tx: &mut Transaction, account: Account
} }
fn handle_account_create(data: Vec<u8>, tx: &mut Transaction) -> Result<RpcResult, Error> { // fn handle_account_create(data: Vec<u8>, tx: &mut Transaction) -> Result<RpcResult, Error> {
let msg = from_slice::<AccountCreateMsg>(&data).or(Err(err_msg("invalid params")))?; // let msg = from_slice::<AccountCreateMsg>(&data).or(Err(err_msg("invalid params")))?;
let account = account_create(msg.params, tx)?; // let account = account_create(msg.params, tx)?;
Ok(RpcResult::Account(account)) // Ok(RpcResult::Account(account))
} // }
fn handle_account_login(data: Vec<u8>, tx: &mut Transaction) -> Result<RpcResult, Error> { // fn handle_account_login(data: Vec<u8>, tx: &mut Transaction) -> Result<RpcResult, Error> {
let msg = from_slice::<AccountLoginMsg>(&data).or(Err(err_msg("invalid params")))?; // let msg = from_slice::<AccountLoginMsg>(&data).or(Err(err_msg("invalid params")))?;
Ok(RpcResult::Account(account_login(msg.params, tx)?)) // Ok(RpcResult::Account(account_login(msg.params, tx)?))
} // }
fn handle_account_constructs(_data: Vec<u8>, tx: &mut Transaction, account: Account) -> Result<RpcResult, Error> { fn handle_account_constructs(_data: Vec<u8>, tx: &mut Transaction, account: Account) -> Result<RpcResult, Error> {
Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?))