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 { 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 };
},
);

View File

@ -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;
});

View File

@ -1 +1,2 @@
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 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<Account
return Ok(entry);
}
pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Result<Account, Error> {
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<String, Error> {
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(&params.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");
let entry = Account {
id: returned.get(0),
name: returned.get(1),
token: returned.get(2),
};
info!("registration account={:?}", entry.name);
return Ok(entry);
if result.is_empty() {
return Err(err_msg("no row returned"));
}
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 = "
SELECT id, name, token, password
SELECT id, password
FROM accounts
WHERE name = $1;
";
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() {
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(&params.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<Vec<Construct>, Error> {

View File

@ -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<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
// 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<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
let res = ws::start(MnmlSocket::new(state), &r, stream);
fn ws(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
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<State>, params: web::Json::<AccountLoginParams>) -> Result<HttpResponse, MnmlError> {
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
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> {
@ -137,14 +207,27 @@ pub fn start() {
let pool = create_pool(database_url);
HttpServer::new(move || {
App::new()
match env::var("DEV_CORS") {
Ok(_) => {
warn!("enabling dev CORS middleware");
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)))
})
.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");
.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 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<u8>, 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<u8>, 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<u8>, tx: &mut Transaction, account: Account
}
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 account = account_create(msg.params, tx)?;
Ok(RpcResult::Account(account))
}
// 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 account = account_create(msg.params, tx)?;
// Ok(RpcResult::Account(account))
// }
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")))?;
Ok(RpcResult::Account(account_login(msg.params, tx)?))
}
// 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")))?;
// Ok(RpcResult::Account(account_login(msg.params, tx)?))
// }
fn handle_account_constructs(_data: Vec<u8>, tx: &mut Transaction, account: Account) -> Result<RpcResult, Error> {
Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?))