From 59320da37dff73f5baa94c6e0d868e5bef2ee70b Mon Sep 17 00:00:00 2001 From: ntr Date: Wed, 12 Jun 2019 23:33:46 +1000 Subject: [PATCH 01/24] ummm --- WORKLOG.md | 7 + client/src/socket.jsx | 2 +- server/Cargo.toml | 6 + server/src/main.rs | 4 + server/src/net.rs | 305 +++++++++++++------- server/src/rpc.rs | 639 ++++++++++++++++++++---------------------- 6 files changed, 536 insertions(+), 427 deletions(-) diff --git a/WORKLOG.md b/WORKLOG.md index fc20200d..20cb28d1 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -29,6 +29,13 @@ *$$$* +clicks buy + creates stripe order / fill 0x order + +server notified of payment + txs <- payment + + buy supporter pack account credited with features char sets diff --git a/client/src/socket.jsx b/client/src/socket.jsx index ada2fbba..427e320c 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -1,7 +1,7 @@ const toast = require('izitoast'); const cbor = require('borc'); -const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000'; +const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000/ws/'; function errorToast(err) { console.error(err); diff --git a/server/Cargo.toml b/server/Cargo.toml index 619095e4..4cfd6849 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -25,3 +25,9 @@ failure = "0.1" log = "0.4" fern = "0.5" +actix = "0.8.2" +actix-web = "1.0.0" +actix-web-actors = "1.0.0" +futures = "0.1" +bytes = "0.4" + diff --git a/server/src/main.rs b/server/src/main.rs index 3daa167b..eaffe954 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,6 +9,10 @@ extern crate postgres; extern crate r2d2; extern crate r2d2_postgres; +extern crate actix; +extern crate actix_web; +extern crate actix_web_actors; + extern crate serde; extern crate serde_cbor; #[macro_use] extern crate serde_derive; diff --git a/server/src/net.rs b/server/src/net.rs index 7c131084..07cb03fc 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -1,49 +1,122 @@ -use failure::{Error, err_msg}; -use tungstenite::Message; -use tungstenite::protocol::WebSocket; -use tungstenite::server::accept; -use tungstenite::Message::Binary; -use std::net::{TcpListener, TcpStream}; +use std::time::{Instant, Duration}; +use std::env; + use serde_cbor::{to_vec}; -use std::env; -use std::thread::{spawn, sleep}; -use std::time::{Instant, Duration}; +use actix::prelude::*; +use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web_actors::ws; use r2d2::{Pool}; use r2d2::{PooledConnection}; use r2d2_postgres::{TlsMode, PostgresConnectionManager}; -static DB_POOL_SIZE: u32 = 20; - -pub type Db = PooledConnection; - -use rpc::{Rpc}; +use rpc::{receive, RpcErrorResponse}; use warden::{warden}; -#[derive(Debug,Clone,Serialize,Deserialize)] -struct RpcErrorResponse { - err: String +pub type Db = PooledConnection; +type PgPool = Pool; + +const DB_POOL_SIZE: u32 = 20; +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); + +/// websocket connection is long running connection, it easier +/// to handle with an actor +pub struct MnmlSocket { + /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), + /// otherwise we drop connection. + hb: Instant, + pool: PgPool, } -fn receive(db: Db, begin: Instant, rpc: &Rpc, msg: Message, client: &mut WebSocket) -> Result { - match rpc.receive(msg, begin, &db, client) { - Ok(reply) => { - let response = to_vec(&reply) - .expect("failed to serialize response"); - client.write_message(Binary(response))?; - return Ok(reply.method); - }, - Err(e) => { - let response = to_vec(&RpcErrorResponse { err: e.to_string() }) - .expect("failed to serialize error response"); - client.write_message(Binary(response))?; - return Err(err_msg(e)); +impl Actor for MnmlSocket { + type Context = ws::WebsocketContext; + + // once the actor has been started this fn runs + // it starts the heartbeat interval and keepalive + fn started(&mut self, ctx: &mut Self::Context) { + self.hb(ctx); + } +} + +/// Handler for `ws::Message` +impl StreamHandler for MnmlSocket { + fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { + // process websocket messages + println!("msg: {:?}", msg); + match msg { + ws::Message::Ping(msg) => { + self.hb = Instant::now(); + ctx.pong(&msg); + } + ws::Message::Pong(_) => { + self.hb = Instant::now(); + } + ws::Message::Text(text) => (), + ws::Message::Close(_) => { + ctx.stop(); + } + ws::Message::Nop => (), + ws::Message::Binary(bin) => { + let db_connection = self.pool.get().expect("unable to get db connection"); + match receive(bin.to_vec(), &db_connection, ctx) { + Ok(reply) => { + let response = to_vec(&reply) + .expect("failed to serialize response"); + ctx.binary(response); + }, + Err(e) => { + let response = to_vec(&RpcErrorResponse { err: e.to_string() }) + .expect("failed to serialize error response"); + ctx.binary(response); + } + } + } } } } -pub fn db_connection(url: String) -> Pool { +impl MnmlSocket { + fn new(state: web::Data) -> MnmlSocket { + // idk why this has to be cloned again + // i guess because each socket is added as a new thread? + MnmlSocket { hb: Instant::now(), pool: state.pool.clone() } + } + + // starts the keepalive interval once actor started + fn hb(&self, ctx: &mut ::Context) { + ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { + if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { + info!("Websocket Client heartbeat failed, disconnecting!"); + + // stop actor + ctx.stop(); + + // don't try to send a ping + return; + } + + ctx.ping(""); + }); + } +} + +// 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); + + // response of upgrade being sent back + // info!("res={:?}", res.as_ref().unwrap()); + res +} + +fn create_pool(url: String) -> Pool { let manager = PostgresConnectionManager::new(url, TlsMode::None) .expect("could not instantiate pg manager"); @@ -53,76 +126,114 @@ pub fn db_connection(url: String) -> Pool { .expect("Failed to create pool.") } -// fn print_panic_payload(ctx: &str, payload: &(Any + Send + 'static)) { -// let d = format!("{:?}", payload); -// let s = if let Some(s) = payload.downcast_ref::() { -// &s -// } else if let Some(s) = payload.downcast_ref::<&str>() { -// s -// } else { -// // "PAYLOAD IS NOT A STRING" -// d.as_str() -// }; -// info!("{}: PANIC OCCURRED: {}", ctx, s); -// } +// This struct represents state +struct State { + pool: PgPool, +} pub fn start() { - // panic::set_hook(Box::new(|panic_info| { - // print_panic_payload("set_hook", panic_info.payload()); - // if let Some(location) = panic_info.location() { - // info!("LOCATION: {}:{}", location.file(), location.line()); - // } else { - // info!("NO LOCATION INFORMATION"); - // } - // })); - let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); - let pool = db_connection(database_url); + let pool = create_pool(database_url); - // { - // let startup_connection = pool.get().expect("unable to get db connection"); - // startup(startup_connection).unwrap(); - // } - - let server = TcpListener::bind("0.0.0.0:40000").unwrap(); - - let warden_pool = pool.clone(); - spawn(move || { - loop { - let db_connection = warden_pool.get().expect("unable to get db connection"); - if let Err(e) = warden(db_connection) { - info!("{:?}", e); - } - sleep(Duration::new(1, 0)); - } - - }); - - for stream in server.incoming() { - let db = pool.clone(); - spawn(move || { - let mut websocket = accept(stream.unwrap()).unwrap(); - let rpc = Rpc {}; - - loop { - match websocket.read_message() { - Ok(msg) => { - let begin = Instant::now(); - let db_connection = db.get().expect("unable to get db connection"); - match receive(db_connection, begin, &rpc, msg, &mut websocket) { - Ok(_) => (), - Err(e) => warn!("{:?}", e), - } - }, - // connection is closed - Err(e) => { - debug!("{:?}", e); - return; - } - }; - } - }); - } + 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"); } + +// #[derive(Debug,Clone,Serialize,Deserialize)] +// struct RpcErrorResponse { +// err: String +// } + +// pub fn db_connection(url: String) -> Pool { +// let manager = PostgresConnectionManager::new(url, TlsMode::None) +// .expect("could not instantiate pg manager"); + +// Pool::builder() +// .max_size(DB_POOL_SIZE) +// .build(manager) +// .expect("Failed to create pool.") +// } + +// // fn print_panic_payload(ctx: &str, payload: &(Any + Send + 'static)) { +// // let d = format!("{:?}", payload); +// // let s = if let Some(s) = payload.downcast_ref::() { +// // &s +// // } else if let Some(s) = payload.downcast_ref::<&str>() { +// // s +// // } else { +// // // "PAYLOAD IS NOT A STRING" +// // d.as_str() +// // }; +// // info!("{}: PANIC OCCURRED: {}", ctx, s); +// // } + +// pub fn start() { +// // panic::set_hook(Box::new(|panic_info| { +// // print_panic_payload("set_hook", panic_info.payload()); +// // if let Some(location) = panic_info.location() { +// // info!("LOCATION: {}:{}", location.file(), location.line()); +// // } else { +// // info!("NO LOCATION INFORMATION"); +// // } +// // })); + +// let database_url = env::var("DATABASE_URL") +// .expect("DATABASE_URL must be set"); + +// let pool = db_connection(database_url); + +// // { +// // let startup_connection = pool.get().expect("unable to get db connection"); +// // startup(startup_connection).unwrap(); +// // } + +// let server = TcpListener::bind("0.0.0.0:40000").unwrap(); + +// let warden_pool = pool.clone(); +// spawn(move || { +// loop { +// let db_connection = warden_pool.get().expect("unable to get db connection"); +// if let Err(e) = warden(db_connection) { +// info!("{:?}", e); +// } +// sleep(Duration::new(1, 0)); +// } + +// }); + +// for stream in server.incoming() { +// let db = pool.clone(); +// spawn(move || { +// let mut websocket = accept(stream.unwrap()).unwrap(); +// let rpc = Rpc {}; + +// loop { +// match websocket.read_message() { +// Ok(msg) => { +// let begin = Instant::now(); +// let db_connection = db.get().expect("unable to get db connection"); +// match receive(db_connection, begin, &rpc, msg, &mut websocket) { +// Ok(_) => (), +// Err(e) => warn!("{:?}", e), +// } +// }, +// // connection is closed +// Err(e) => { +// debug!("{:?}", e); +// return; +// } +// }; +// } +// }); +// } +// } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 05a812a0..210befe7 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,17 +1,12 @@ -use tungstenite::Message; -use tungstenite::protocol::WebSocket; -use tungstenite::Message::Binary; +use actix_web_actors::ws; + use postgres::transaction::Transaction; -use std::net::{TcpStream}; - -use std::time::Instant; - use serde_cbor::{from_slice, to_vec}; use uuid::Uuid; use failure::Error; use failure::err_msg; -use net::Db; +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}; @@ -20,370 +15,356 @@ use instance::{Instance, instance_state, instance_list, instance_new, instance_r use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip}; use item::{Item, ItemInfoCtr, item_info}; -pub struct Rpc; -impl Rpc { - pub fn receive(&self, msg: Message, begin: Instant, db: &Db, client: &mut WebSocket) -> Result { - // consume the ws data into bytes - let data = msg.into_data(); +type MnmlWs = ws::WebsocketContext; - // cast the msg to this type to receive method name - match from_slice::(&data) { - Ok(v) => { - if v.method == "ping" { - return Ok(RpcResponse { method: "pong".to_string(), params: RpcResult::Pong(()) }); - } +pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs) -> Result { + // cast the data to this type to receive method name + match from_slice::(&data) { + Ok(v) => { + if v.method == "ping" { + return Ok(RpcResponse { method: "pong".to_string(), params: RpcResult::Pong(()) }); + } - let mut tx = db.transaction()?; + let mut tx = db.transaction()?; - let account: Option = match v.token { - Some(t) => Some(account_from_token(t, &mut tx)?), - None => None, - }; + let account: Option = match v.token { + Some(t) => Some(account_from_token(t, &mut tx)?), + None => None, + }; - let account_name = match &account { - Some(a) => a.name.clone(), - None => "none".to_string(), - }; + let account_name = match &account { + Some(a) => a.name.clone(), + None => "none".to_string(), + }; - // check the method - // if no auth required - match v.method.as_ref() { - "account_create" => (), - "account_login" => (), - "item_info" => (), - _ => match account { - Some(_) => (), - None => return Err(err_msg("auth required")), - }, - }; + // check the method + // if no auth required + match v.method.as_ref() { + "account_create" => (), + "account_login" => (), + "item_info" => (), + _ => match account { + Some(_) => (), + None => return Err(err_msg("auth required")), + }, + }; - // now we have the method name - // match on that to determine what fn to call - let response = match v.method.as_ref() { - // NO AUTH - "account_create" => Rpc::account_create(data, &mut tx, client), - "account_login" => Rpc::account_login(data, &mut tx, client), - "item_info" => Ok(RpcResponse { method: "item_info".to_string(), params: RpcResult::ItemInfo(item_info()) }), + // now we have the method name + // 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, client), + "account_login" => handle_account_login(data, &mut tx, client), + "item_info" => Ok(RpcResponse { method: "item_info".to_string(), params: RpcResult::ItemInfo(item_info()) }), - // AUTH METHODS - "account_constructs" => Rpc::account_constructs(data, &mut tx, account.unwrap(), client), - "account_instances" => Rpc::account_instances(data, &mut tx, account.unwrap(), client), + // AUTH METHODS + "account_constructs" => handle_account_constructs(data, &mut tx, account.unwrap(), client), + "account_instances" => handle_account_instances(data, &mut tx, account.unwrap(), client), - "construct_spawn" => Rpc::construct_spawn(data, &mut tx, account.unwrap(), client), - "construct_delete" => Rpc::construct_delete(data, &mut tx, account.unwrap(), client), + "construct_spawn" => handle_construct_spawn(data, &mut tx, account.unwrap(), client), + "construct_delete" => handle_construct_delete(data, &mut tx, account.unwrap(), client), - "game_state" => Rpc::game_state(data, &mut tx, account.unwrap(), client), - "game_skill" => Rpc::game_skill(data, &mut tx, account.unwrap(), client), - "game_ready" => Rpc::game_ready(data, &mut tx, account.unwrap(), client), + "game_state" => handle_game_state(data, &mut tx, account.unwrap(), client), + "game_skill" => handle_game_skill(data, &mut tx, account.unwrap(), client), + "game_ready" => handle_game_ready(data, &mut tx, account.unwrap(), client), - "instance_list" => Rpc::instance_list(data, &mut tx, account.unwrap(), client), - "instance_join" => Rpc::instance_join(data, &mut tx, account.unwrap(), client), - "instance_ready" => Rpc::instance_ready(data, &mut tx, account.unwrap(), client), - "instance_new" => Rpc::instance_new(data, &mut tx, account.unwrap(), client), - "instance_state" => Rpc::instance_state(data, &mut tx, account.unwrap(), client), + "instance_list" => handle_instance_list(data, &mut tx, account.unwrap(), client), + "instance_join" => handle_instance_join(data, &mut tx, account.unwrap(), client), + "instance_ready" => handle_instance_ready(data, &mut tx, account.unwrap(), client), + "instance_new" => handle_instance_new(data, &mut tx, account.unwrap(), client), + "instance_state" => handle_instance_state(data, &mut tx, account.unwrap(), client), - "vbox_accept" => Rpc::vbox_accept(data, &mut tx, account.unwrap(), client), - "vbox_apply" => Rpc::vbox_apply(data, &mut tx, account.unwrap(), client), - "vbox_combine" => Rpc::vbox_combine(data, &mut tx, account.unwrap(), client), - "vbox_discard" => Rpc::vbox_discard(data, &mut tx, account.unwrap(), client), - "vbox_reclaim" => Rpc::vbox_reclaim(data, &mut tx, account.unwrap(), client), - "vbox_unequip" => Rpc::vbox_unequip(data, &mut tx, account.unwrap(), client), + "vbox_accept" => handle_vbox_accept(data, &mut tx, account.unwrap(), client), + "vbox_apply" => handle_vbox_apply(data, &mut tx, account.unwrap(), client), + "vbox_combine" => handle_vbox_combine(data, &mut tx, account.unwrap(), client), + "vbox_discard" => handle_vbox_discard(data, &mut tx, account.unwrap(), client), + "vbox_reclaim" => handle_vbox_reclaim(data, &mut tx, account.unwrap(), client), + "vbox_unequip" => handle_vbox_unequip(data, &mut tx, account.unwrap(), client), - _ => Err(format_err!("unknown method - {:?}", v.method)), - }; + _ => Err(format_err!("unknown method - {:?}", v.method)), + }; - tx.commit()?; + tx.commit()?; - info!("method={:?} account={:?} duration={:?}", v.method, account_name, begin.elapsed()); - - return response; - }, - Err(e) => { - info!("{:?}", e); - Err(err_msg("unknown error")) - }, - } + return response; + }, + Err(e) => { + info!("{:?}", e); + Err(err_msg("unknown error")) + }, } +} - fn send_msg(client: &mut WebSocket, msg: RpcResponse) -> Result<(), Error> { - let bytes = to_vec(&msg)?; - match client.write_message(Binary(bytes)) { - Ok(()) => Ok(()), - Err(e) => Err(err_msg(e)) - } +fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let game_response = RpcResponse { + method: "game_state".to_string(), + params: RpcResult::GameState(game_state(msg.params, tx, &account)?) + }; + + return Ok(game_response); +} + +// fn handle_game_pve(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +// let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + +// let game_response = RpcResponse { +// method: "game_state".to_string(), +// params: RpcResult::GameState(game_pve(msg.params, tx, &account)?) +// }; + +// return Ok(game_response); +// } + +fn handle_game_skill(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let game_response = RpcResponse { + method: "game_state".to_string(), + params: RpcResult::GameState(game_skill(msg.params, tx, &account)?) + }; + + return Ok(game_response); +} + +fn handle_game_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let game_response = RpcResponse { + method: "game_state".to_string(), + params: RpcResult::GameState(game_ready(msg.params, tx, &account)?) + }; + + return Ok(game_response); +} + + +fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: Account, client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let construct_list = RpcResponse { + method: "account_constructs".to_string(), + params: RpcResult::ConstructList(account_constructs(tx, &account)?) + }; + + Ok(construct_list) +} + +fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + construct_delete(tx, msg.params.id, account.id)?; + + let construct_list = RpcResponse { + method: "account_constructs".to_string(), + params: RpcResult::ConstructList(account_constructs(tx, &account)?) + }; + + Ok(construct_list) +} + + +fn handle_account_create(data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let account = account_create(msg.params, tx)?; + + Ok(RpcResponse { + method: "account_create".to_string(), + params: RpcResult::Account(account) + }) +} + +fn handle_account_login(data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { + match from_slice::(&data) { + Ok(v) => Ok(RpcResponse { + method: v.method, + params: RpcResult::Account(account_login(v.params, tx)?) + }), + Err(_e) => Err(err_msg("invalid params")), } +} - fn game_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +// fn handle_account_demo(_data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { +// let mut rng = thread_rng(); - let game_response = RpcResponse { +// let acc_name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); + +// let account = account_create(AccountCreateParams { name: acc_name, password: "grepgrepgrep".to_string() }, tx)?; + +// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); +// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; + +// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); +// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; + +// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); +// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; + +// let res = RpcResponse { +// method: "account_create".to_string(), +// params: RpcResult::Account(account), +// }; + +// return Ok(res); +// } + + +fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + Ok(RpcResponse { + method: "account_constructs".to_string(), + params: RpcResult::ConstructList(account_constructs(tx, &account)?) + }) +} + +fn handle_account_instances(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + Ok(RpcResponse { + method: "account_instances".to_string(), + params: RpcResult::InstanceList(account_instances(tx, &account)?) + }) +} + +fn handle_instance_new(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(instance_new(msg.params, tx, &account)?) + }; + + return Ok(response); +} + +fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(instance_ready(msg.params, tx, &account)?) + }; + + return Ok(response); +} + +fn handle_instance_join(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(instance_join(msg.params, tx, &account)?) + }; + + return Ok(response); +} + +fn handle_instance_list(_data: Vec, tx: &mut Transaction, _account: Account, _client: &mut MnmlWs) -> Result { + let response = RpcResponse { + method: "instance_list".to_string(), + params: RpcResult::InstanceList(instance_list(tx)?) + }; + + return Ok(response); +} + + +// fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +// let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + +// let response = RpcResponse { +// method: "game_state".to_string(), +// params: RpcResult::GameState(instance_ready(msg.params, tx, &account)?) +// }; + +// return Ok(response); +// } + +fn handle_instance_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + match instance_state(msg.params, tx, &account)? { + RpcResult::GameState(p) => Ok(RpcResponse { method: "game_state".to_string(), - params: RpcResult::GameState(game_state(msg.params, tx, &account)?) - }; - - return Ok(game_response); - } - - // fn game_pve(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - // let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - // let game_response = RpcResponse { - // method: "game_state".to_string(), - // params: RpcResult::GameState(game_pve(msg.params, tx, &account)?) - // }; - - // return Ok(game_response); - // } - - fn game_skill(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let game_response = RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(game_skill(msg.params, tx, &account)?) - }; - - return Ok(game_response); - } - - fn game_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let game_response = RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(game_ready(msg.params, tx, &account)?) - }; - - return Ok(game_response); - } - - - fn construct_spawn(data: Vec, tx: &mut Transaction, account: Account, client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - Rpc::send_msg(client, RpcResponse { - method: "construct_spawn".to_string(), - params: RpcResult::ConstructSpawn(construct_spawn(msg.params, tx, &account)?) - })?; - - let construct_list = RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }; - - Ok(construct_list) - } - - fn construct_delete(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - construct_delete(tx, msg.params.id, account.id)?; - - let construct_list = RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }; - - Ok(construct_list) - } - - - fn account_create(data: Vec, tx: &mut Transaction, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let account = account_create(msg.params, tx)?; - - Ok(RpcResponse { - method: "account_create".to_string(), - params: RpcResult::Account(account) - }) - } - - fn account_login(data: Vec, tx: &mut Transaction, _client: &mut WebSocket) -> Result { - match from_slice::(&data) { - Ok(v) => Ok(RpcResponse { - method: v.method, - params: RpcResult::Account(account_login(v.params, tx)?) - }), - Err(_e) => Err(err_msg("invalid params")), - } - } - - // fn account_demo(_data: Vec, tx: &mut Transaction, _client: &mut WebSocket) -> Result { - // let mut rng = thread_rng(); - - // let acc_name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); - - // let account = account_create(AccountCreateParams { name: acc_name, password: "grepgrepgrep".to_string() }, tx)?; - - // let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); - // construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - - // let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); - // construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - - // let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); - // construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - - // let res = RpcResponse { - // method: "account_create".to_string(), - // params: RpcResult::Account(account), - // }; - - // return Ok(res); - // } - - - fn account_constructs(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - Ok(RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }) - } - - fn account_instances(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - Ok(RpcResponse { - method: "account_instances".to_string(), - params: RpcResult::InstanceList(account_instances(tx, &account)?) - }) - } - - fn instance_new(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { + params: RpcResult::GameState(p), + }), + RpcResult::InstanceState(p) => Ok(RpcResponse { method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_new(msg.params, tx, &account)?) - }; - - return Ok(response); + params: RpcResult::InstanceState(p), + }), + _ => Err(err_msg("unhandled instance state")) } +} - fn instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +fn handle_vbox_accept(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_ready(msg.params, tx, &account)?) - }; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_accept(msg.params, tx, &account)?) + }; - return Ok(response); - } + return Ok(response); +} - fn instance_join(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +fn handle_vbox_discard(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_join(msg.params, tx, &account)?) - }; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_discard(msg.params, tx, &account)?) + }; - return Ok(response); - } - - fn instance_list(_data: Vec, tx: &mut Transaction, _account: Account, _client: &mut WebSocket) -> Result { - let response = RpcResponse { - method: "instance_list".to_string(), - params: RpcResult::InstanceList(instance_list(tx)?) - }; - - return Ok(response); - } + return Ok(response); +} - // fn instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - // let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; +fn handle_vbox_combine(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - // let response = RpcResponse { - // method: "game_state".to_string(), - // params: RpcResult::GameState(instance_ready(msg.params, tx, &account)?) - // }; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_combine(msg.params, tx, &account)?) + }; - // return Ok(response); - // } + return Ok(response); +} - fn instance_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - match instance_state(msg.params, tx, &account)? { - RpcResult::GameState(p) => Ok(RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(p), - }), - RpcResult::InstanceState(p) => Ok(RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(p), - }), - _ => Err(err_msg("unhandled instance state")) - } - } +fn handle_vbox_apply(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - fn vbox_accept(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_apply(msg.params, tx, &account)?) + }; - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_accept(msg.params, tx, &account)?) - }; + return Ok(response); +} - return Ok(response); - } +fn handle_vbox_reclaim(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - fn vbox_discard(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_reclaim(msg.params, tx, &account)?) + }; - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_discard(msg.params, tx, &account)?) - }; + return Ok(response); +} - return Ok(response); - } +fn handle_vbox_unequip(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { + let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + let response = RpcResponse { + method: "instance_state".to_string(), + params: RpcResult::InstanceState(vbox_unequip(msg.params, tx, &account)?) + }; - fn vbox_combine(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + return Ok(response); +} - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_combine(msg.params, tx, &account)?) - }; - - return Ok(response); - } - - fn vbox_apply(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_apply(msg.params, tx, &account)?) - }; - - return Ok(response); - } - - fn vbox_reclaim(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_reclaim(msg.params, tx, &account)?) - }; - - return Ok(response); - } - - fn vbox_unequip(data: Vec, tx: &mut Transaction, account: Account, _client: &mut WebSocket) -> Result { - let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_unequip(msg.params, tx, &account)?) - }; - - return Ok(response); - } +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct RpcErrorResponse { + pub err: String } #[derive(Debug,Clone,Serialize,Deserialize)] From abe8a445ae5c61cdcb1a56ad6c9852cd9fb9c0d0 Mon Sep 17 00:00:00 2001 From: ntr Date: Wed, 12 Jun 2019 23:40:28 +1000 Subject: [PATCH 02/24] times --- server/src/net.rs | 3 ++- server/src/rpc.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/net.rs b/server/src/net.rs index 07cb03fc..ace6beeb 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -44,6 +44,7 @@ impl Actor for MnmlSocket { impl StreamHandler for MnmlSocket { fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { // process websocket messages + let begin = Instant::now(); println!("msg: {:?}", msg); match msg { ws::Message::Ping(msg) => { @@ -60,7 +61,7 @@ impl StreamHandler for MnmlSocket { ws::Message::Nop => (), ws::Message::Binary(bin) => { let db_connection = self.pool.get().expect("unable to get db connection"); - match receive(bin.to_vec(), &db_connection, ctx) { + match receive(bin.to_vec(), &db_connection, ctx, begin) { Ok(reply) => { let response = to_vec(&reply) .expect("failed to serialize response"); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 210befe7..1633c5ea 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,7 +1,9 @@ +use std::time::{Instant}; + use actix_web_actors::ws; use postgres::transaction::Transaction; -use serde_cbor::{from_slice, to_vec}; +use serde_cbor::{from_slice}; use uuid::Uuid; use failure::Error; use failure::err_msg; @@ -18,7 +20,7 @@ use item::{Item, ItemInfoCtr, item_info}; type MnmlWs = ws::WebsocketContext; -pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs) -> Result { +pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs, begin: Instant) -> Result { // cast the data to this type to receive method name match from_slice::(&data) { Ok(v) => { @@ -87,6 +89,8 @@ pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs) -> Result { @@ -144,6 +148,8 @@ fn handle_game_ready(data: Vec, tx: &mut Transaction, account: Account, _cli fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: Account, client: &mut MnmlWs) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; + construct_spawn(msg.params, tx, &account)?; + let construct_list = RpcResponse { method: "account_constructs".to_string(), params: RpcResult::ConstructList(account_constructs(tx, &account)?) From 859ff36771f8c1582691df478661583c00ce0c81 Mon Sep 17 00:00:00 2001 From: ntr Date: Thu, 13 Jun 2019 23:39:21 +1000 Subject: [PATCH 03/24] remove comments --- server/src/net.rs | 92 +---------------------------------------------- 1 file changed, 1 insertion(+), 91 deletions(-) diff --git a/server/src/net.rs b/server/src/net.rs index ace6beeb..0543f566 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -54,7 +54,7 @@ impl StreamHandler for MnmlSocket { ws::Message::Pong(_) => { self.hb = Instant::now(); } - ws::Message::Text(text) => (), + ws::Message::Text(_text) => (), ws::Message::Close(_) => { ctx.stop(); } @@ -127,7 +127,6 @@ fn create_pool(url: String) -> Pool { .expect("Failed to create pool.") } -// This struct represents state struct State { pool: PgPool, } @@ -149,92 +148,3 @@ pub fn start() { .bind("127.0.0.1:40000").expect("could not bind to port") .run().expect("could not start http server"); } - -// #[derive(Debug,Clone,Serialize,Deserialize)] -// struct RpcErrorResponse { -// err: String -// } - -// pub fn db_connection(url: String) -> Pool { -// let manager = PostgresConnectionManager::new(url, TlsMode::None) -// .expect("could not instantiate pg manager"); - -// Pool::builder() -// .max_size(DB_POOL_SIZE) -// .build(manager) -// .expect("Failed to create pool.") -// } - -// // fn print_panic_payload(ctx: &str, payload: &(Any + Send + 'static)) { -// // let d = format!("{:?}", payload); -// // let s = if let Some(s) = payload.downcast_ref::() { -// // &s -// // } else if let Some(s) = payload.downcast_ref::<&str>() { -// // s -// // } else { -// // // "PAYLOAD IS NOT A STRING" -// // d.as_str() -// // }; -// // info!("{}: PANIC OCCURRED: {}", ctx, s); -// // } - -// pub fn start() { -// // panic::set_hook(Box::new(|panic_info| { -// // print_panic_payload("set_hook", panic_info.payload()); -// // if let Some(location) = panic_info.location() { -// // info!("LOCATION: {}:{}", location.file(), location.line()); -// // } else { -// // info!("NO LOCATION INFORMATION"); -// // } -// // })); - -// let database_url = env::var("DATABASE_URL") -// .expect("DATABASE_URL must be set"); - -// let pool = db_connection(database_url); - -// // { -// // let startup_connection = pool.get().expect("unable to get db connection"); -// // startup(startup_connection).unwrap(); -// // } - -// let server = TcpListener::bind("0.0.0.0:40000").unwrap(); - -// let warden_pool = pool.clone(); -// spawn(move || { -// loop { -// let db_connection = warden_pool.get().expect("unable to get db connection"); -// if let Err(e) = warden(db_connection) { -// info!("{:?}", e); -// } -// sleep(Duration::new(1, 0)); -// } - -// }); - -// for stream in server.incoming() { -// let db = pool.clone(); -// spawn(move || { -// let mut websocket = accept(stream.unwrap()).unwrap(); -// let rpc = Rpc {}; - -// loop { -// match websocket.read_message() { -// Ok(msg) => { -// let begin = Instant::now(); -// let db_connection = db.get().expect("unable to get db connection"); -// match receive(db_connection, begin, &rpc, msg, &mut websocket) { -// Ok(_) => (), -// Err(e) => warn!("{:?}", e), -// } -// }, -// // connection is closed -// Err(e) => { -// debug!("{:?}", e); -// return; -// } -// }; -// } -// }); -// } -// } From 5c6e587044eca1c70c1af5f1435d17b4693e85e5 Mon Sep 17 00:00:00 2001 From: ntr Date: Fri, 14 Jun 2019 13:05:19 +1000 Subject: [PATCH 04/24] simpify rpc responses --- client/src/socket.jsx | 62 ++++----- server/src/rpc.rs | 299 ++++++++++-------------------------------- 2 files changed, 92 insertions(+), 269 deletions(-) diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 427e320c..d3e84d3b 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -143,9 +143,7 @@ function createSocket(events) { // ------------- // Incoming // ------------- - function accountLogin(res) { - const [struct, login] = res; - + function onAccount(login) { account = login; localStorage.setItem('account', JSON.stringify(login)); events.setAccount(login); @@ -153,19 +151,16 @@ function createSocket(events) { sendAccountInstances(); } - function accountInstanceList(res) { - const [struct, playerList] = res; - events.setAccountInstances(playerList); + function onAccountInstances(list) { + events.setAccountInstances(list); setTimeout(sendAccountInstances, 5000); } - function accountConstructs(response) { - const [structName, constructs] = response; + function onAccountConstructs(constructs) { events.setConstructList(constructs); } - function gameState(response) { - const [structName, game] = response; + function onGameState(game) { events.setGame(game); } @@ -180,15 +175,6 @@ function createSocket(events) { clearTimeout(gameStateTimeout); } - function constructSpawn(response) { - const [structName, construct] = response; - } - - function zoneState(response) { - const [structName, zone] = response; - events.setZone(zone); - } - let instanceStateTimeout; function startInstanceStateTimeout(id) { clearTimeout(instanceStateTimeout); @@ -196,13 +182,12 @@ function createSocket(events) { return true; } - function instanceState(response) { - const [structName, i] = response; - events.setInstance(i); + function onInstanceState(instance) { + events.setInstance(instance); return true; } - function instanceList([, list]) { + function onOpenInstances(list) { events.setInstanceList(list); return true; } @@ -211,12 +196,11 @@ function createSocket(events) { clearTimeout(instanceStateTimeout); } - function itemInfo(response) { - const [structName, info] = response; + function onItemInfo(info) { events.setItemInfo(info); } - function pong() { + function onPong() { events.setPing(Date.now() - ping); setTimeout(sendPing, 1000); } @@ -228,16 +212,14 @@ function createSocket(events) { // when the server sends a reply it will have one of these message types // this object wraps the reply types to a function const handlers = { - construct_spawn: constructSpawn, - game_state: gameState, - account_login: accountLogin, - account_create: accountLogin, - account_constructs: accountConstructs, - account_instances: accountInstanceList, - instance_list: instanceList, - instance_state: instanceState, - item_info: itemInfo, - pong, + Account: onAccount, + AccountConstructs: onAccountConstructs, + AccountInstances: onAccountInstances, + GameState: onGameState, + InstanceState: onInstanceState, + ItemInfo: onItemInfo, + OpenInstances: onOpenInstances, + Pong: onPong, }; function logout() { @@ -265,14 +247,14 @@ function createSocket(events) { // decode binary msg from server const blob = new Uint8Array(event.data); const res = cbor.decode(blob); - const { method, params } = res; + const [msgType, params] = res; - if (method !== '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); - if (!handlers[method]) return errorToast(`${method} handler missing`); - return handlers[method](params); + if (!handlers[msgType]) return errorToast(`${msgType} handler missing`); + return handlers[msgType](params); } function connect() { diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 1633c5ea..d2271225 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -17,15 +17,14 @@ use instance::{Instance, instance_state, instance_list, instance_new, instance_r use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip}; use item::{Item, ItemInfoCtr, item_info}; - type MnmlWs = ws::WebsocketContext; -pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs, begin: Instant) -> Result { - // cast the data to this type to receive method name +pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> Result { + // cast the msg to this type to receive method name match from_slice::(&data) { Ok(v) => { if v.method == "ping" { - return Ok(RpcResponse { method: "pong".to_string(), params: RpcResult::Pong(()) }); + return Ok(RpcResult::Pong(())); } let mut tx = db.transaction()?; @@ -56,33 +55,33 @@ pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs, begin: Instant) -> R // 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, client), - "account_login" => handle_account_login(data, &mut tx, client), - "item_info" => Ok(RpcResponse { method: "item_info".to_string(), params: RpcResult::ItemInfo(item_info()) }), + "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 - "account_constructs" => handle_account_constructs(data, &mut tx, account.unwrap(), client), - "account_instances" => handle_account_instances(data, &mut tx, account.unwrap(), client), + "account_constructs" => handle_account_constructs(data, &mut tx, account.unwrap()), + "account_instances" => handle_account_instances(data, &mut tx, account.unwrap()), - "construct_spawn" => handle_construct_spawn(data, &mut tx, account.unwrap(), client), - "construct_delete" => handle_construct_delete(data, &mut tx, account.unwrap(), client), + "construct_spawn" => handle_construct_spawn(data, &mut tx, account.unwrap()), + "construct_delete" => handle_construct_delete(data, &mut tx, account.unwrap()), - "game_state" => handle_game_state(data, &mut tx, account.unwrap(), client), - "game_skill" => handle_game_skill(data, &mut tx, account.unwrap(), client), - "game_ready" => handle_game_ready(data, &mut tx, account.unwrap(), client), + "game_state" => handle_game_state(data, &mut tx, account.unwrap()), + "game_skill" => handle_game_skill(data, &mut tx, account.unwrap()), + "game_ready" => handle_game_ready(data, &mut tx, account.unwrap()), - "instance_list" => handle_instance_list(data, &mut tx, account.unwrap(), client), - "instance_join" => handle_instance_join(data, &mut tx, account.unwrap(), client), - "instance_ready" => handle_instance_ready(data, &mut tx, account.unwrap(), client), - "instance_new" => handle_instance_new(data, &mut tx, account.unwrap(), client), - "instance_state" => handle_instance_state(data, &mut tx, account.unwrap(), client), + "instance_list" => handle_instance_list(data, &mut tx, account.unwrap()), + "instance_join" => handle_instance_join(data, &mut tx, account.unwrap()), + "instance_ready" => handle_instance_ready(data, &mut tx, account.unwrap()), + "instance_new" => handle_instance_new(data, &mut tx, account.unwrap()), + "instance_state" => handle_instance_state(data, &mut tx, account.unwrap()), - "vbox_accept" => handle_vbox_accept(data, &mut tx, account.unwrap(), client), - "vbox_apply" => handle_vbox_apply(data, &mut tx, account.unwrap(), client), - "vbox_combine" => handle_vbox_combine(data, &mut tx, account.unwrap(), client), - "vbox_discard" => handle_vbox_discard(data, &mut tx, account.unwrap(), client), - "vbox_reclaim" => handle_vbox_reclaim(data, &mut tx, account.unwrap(), client), - "vbox_unequip" => handle_vbox_unequip(data, &mut tx, account.unwrap(), client), + "vbox_accept" => handle_vbox_accept(data, &mut tx, account.unwrap()), + "vbox_apply" => handle_vbox_apply(data, &mut tx, account.unwrap()), + "vbox_combine" => handle_vbox_combine(data, &mut tx, account.unwrap()), + "vbox_discard" => handle_vbox_discard(data, &mut tx, account.unwrap()), + "vbox_reclaim" => handle_vbox_reclaim(data, &mut tx, account.unwrap()), + "vbox_unequip" => handle_vbox_unequip(data, &mut tx, account.unwrap()), _ => Err(format_err!("unknown method - {:?}", v.method)), }; @@ -100,18 +99,12 @@ pub fn receive(data: Vec, db: &Db, client: &mut MnmlWs, begin: Instant) -> R } } -fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let game_response = RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(game_state(msg.params, tx, &account)?) - }; - - return Ok(game_response); + return Ok(RpcResult::GameState(game_state(msg.params, tx, &account)?)); } -// fn handle_game_pve(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +// fn handle_game_pve(data: Vec, tx: &mut Transaction, account: Account) -> Result { // let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; // let game_response = RpcResponse { @@ -122,250 +115,107 @@ fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account, _cli // return Ok(game_response); // } -fn handle_game_skill(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_game_skill(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let game_response = RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(game_skill(msg.params, tx, &account)?) - }; - - return Ok(game_response); + Ok(RpcResult::GameState(game_skill(msg.params, tx, &account)?)) } -fn handle_game_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_game_ready(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let game_response = RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(game_ready(msg.params, tx, &account)?) - }; - - return Ok(game_response); + Ok(RpcResult::GameState(game_ready(msg.params, tx, &account)?)) } -fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: Account, client: &mut MnmlWs) -> Result { +fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - construct_spawn(msg.params, tx, &account)?; - - let construct_list = RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }; - - Ok(construct_list) + Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) } -fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - construct_delete(tx, msg.params.id, account.id)?; - - let construct_list = RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }; - - Ok(construct_list) + Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) } -fn handle_account_create(data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { +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(RpcResponse { - method: "account_create".to_string(), - params: RpcResult::Account(account) - }) + Ok(RpcResult::Account(account)) } -fn handle_account_login(data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { - match from_slice::(&data) { - Ok(v) => Ok(RpcResponse { - method: v.method, - params: RpcResult::Account(account_login(v.params, tx)?) - }), - Err(_e) => Err(err_msg("invalid params")), - } +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_demo(_data: Vec, tx: &mut Transaction, _client: &mut MnmlWs) -> Result { -// let mut rng = thread_rng(); - -// let acc_name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); - -// let account = account_create(AccountCreateParams { name: acc_name, password: "grepgrepgrep".to_string() }, tx)?; - -// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); -// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - -// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); -// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - -// let name: String = iter::repeat(()).map(|()| rng.sample(Alphanumeric)).take(8).collect(); -// construct_spawn(ConstructSpawnParams { name }, tx, &account)?; - -// let res = RpcResponse { -// method: "account_create".to_string(), -// params: RpcResult::Account(account), -// }; - -// return Ok(res); -// } - - -fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { - Ok(RpcResponse { - method: "account_constructs".to_string(), - params: RpcResult::ConstructList(account_constructs(tx, &account)?) - }) +fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: Account) -> Result { + Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) } -fn handle_account_instances(_data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { - Ok(RpcResponse { - method: "account_instances".to_string(), - params: RpcResult::InstanceList(account_instances(tx, &account)?) - }) +fn handle_account_instances(_data: Vec, tx: &mut Transaction, account: Account) -> Result { + Ok(RpcResult::AccountInstances(account_instances(tx, &account)?)) } -fn handle_instance_new(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_instance_new(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_new(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(instance_new(msg.params, tx, &account)?)) } -fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_ready(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(instance_ready(msg.params, tx, &account)?)) } -fn handle_instance_join(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_instance_join(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(instance_join(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(instance_join(msg.params, tx, &account)?)) } -fn handle_instance_list(_data: Vec, tx: &mut Transaction, _account: Account, _client: &mut MnmlWs) -> Result { - let response = RpcResponse { - method: "instance_list".to_string(), - params: RpcResult::InstanceList(instance_list(tx)?) - }; - - return Ok(response); +fn handle_instance_list(_data: Vec, tx: &mut Transaction, _account: Account) -> Result { + Ok(RpcResult::OpenInstances(instance_list(tx)?)) } -// fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { -// let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - -// let response = RpcResponse { -// method: "game_state".to_string(), -// params: RpcResult::GameState(instance_ready(msg.params, tx, &account)?) -// }; - -// return Ok(response); -// } - -fn handle_instance_state(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_instance_state(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; match instance_state(msg.params, tx, &account)? { - RpcResult::GameState(p) => Ok(RpcResponse { - method: "game_state".to_string(), - params: RpcResult::GameState(p), - }), - RpcResult::InstanceState(p) => Ok(RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(p), - }), + RpcResult::GameState(p) => Ok(RpcResult::GameState(p)), + RpcResult::InstanceState(p) => Ok(RpcResult::InstanceState(p)), _ => Err(err_msg("unhandled instance state")) } } -fn handle_vbox_accept(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_accept(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_accept(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_accept(msg.params, tx, &account)?)) } -fn handle_vbox_discard(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_discard(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_discard(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_discard(msg.params, tx, &account)?)) } -fn handle_vbox_combine(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_combine(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_combine(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_combine(msg.params, tx, &account)?)) } -fn handle_vbox_apply(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_apply(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_apply(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_apply(msg.params, tx, &account)?)) } -fn handle_vbox_reclaim(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_reclaim(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_reclaim(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_reclaim(msg.params, tx, &account)?)) } -fn handle_vbox_unequip(data: Vec, tx: &mut Transaction, account: Account, _client: &mut MnmlWs) -> Result { +fn handle_vbox_unequip(data: Vec, tx: &mut Transaction, account: Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; - - let response = RpcResponse { - method: "instance_state".to_string(), - params: RpcResult::InstanceState(vbox_unequip(msg.params, tx, &account)?) - }; - - return Ok(response); + Ok(RpcResult::InstanceState(vbox_unequip(msg.params, tx, &account)?)) } #[derive(Debug,Clone,Serialize,Deserialize)] @@ -373,24 +223,15 @@ pub struct RpcErrorResponse { pub err: String } -#[derive(Debug,Clone,Serialize,Deserialize)] -pub struct RpcResponse { - pub method: String, - params: RpcResult, -} - #[derive(Debug,Clone,Serialize,Deserialize)] pub enum RpcResult { - ConstructSpawn(Construct), - ConstructForget(Construct), - ConstructLearn(Construct), - ConstructUnspec(Construct), Account(Account), - ConstructList(Vec), + AccountConstructs(Vec), + AccountInstances(Vec), GameState(Game), ItemInfo(ItemInfoCtr), - InstanceList(Vec), + OpenInstances(Vec), InstanceState(Instance), Pong(()), From 66462ac670c85840fd16e3af95b65c8449de1fab Mon Sep 17 00:00:00 2001 From: ntr Date: Sat, 15 Jun 2019 00:00:29 +1000 Subject: [PATCH 05/24] a mighty battle against an evil opponent --- client/src/components/login.jsx | 39 +++++++++-- client/src/socket.jsx | 29 +++----- server/.env | 1 + server/src/account.rs | 92 ++++++++++++------------- server/src/net.rs | 115 +++++++++++++++++++++++++++----- server/src/rpc.rs | 26 ++++---- 6 files changed, 198 insertions(+), 104 deletions(-) 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)?)) From 2b06c83ea0494e84a14f80a2aff92ae1c61ecd6c Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 14:24:05 +1000 Subject: [PATCH 06/24] accounts are back --- client/src/socket.jsx | 2 +- server/src/account.rs | 2 - server/src/net.rs | 93 +++++++++++++++++++++++++++---------------- server/src/rpc.rs | 92 ++++++++++++++++++++---------------------- 4 files changed, 103 insertions(+), 86 deletions(-) diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 6d3b6849..523faccf 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -209,7 +209,7 @@ function createSocket(events) { // when the server sends a reply it will have one of these message types // this object wraps the reply types to a function const handlers = { - Account: onAccount, + AccountState: onAccount, AccountConstructs: onAccountConstructs, AccountInstances: onAccountInstances, GameState: onGameState, diff --git a/server/src/account.rs b/server/src/account.rs index 5cc62349..e084ac4a 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -19,7 +19,6 @@ static PASSWORD_MIN_LEN: usize = 11; pub struct Account { pub id: Uuid, pub name: String, - token: String, } #[derive(Debug,Clone,Serialize,Deserialize)] @@ -50,7 +49,6 @@ pub fn account_from_token(token: String, tx: &mut Transaction) -> Result; type PgPool = Pool; @@ -36,6 +33,7 @@ pub struct MnmlSocket { /// otherwise we drop connection. hb: Instant, pool: PgPool, + account: Option, } impl Actor for MnmlSocket { @@ -50,10 +48,22 @@ impl Actor for MnmlSocket { /// Handler for `ws::Message` impl StreamHandler for MnmlSocket { + fn started(&mut self, ctx: &mut Self::Context) { + match self.account.as_ref() { + Some(a) => { + info!("user connected {:?}", a); + let account_state = to_vec(&RpcResult::AccountState(a.clone())) + .expect("could not serialize account state"); + ctx.binary(account_state) + }, + None => info!("new connection"), + } + } + fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { // process websocket messages let begin = Instant::now(); - println!("msg: {:?}", msg); + debug!("msg: {:?}", msg); match msg { ws::Message::Ping(msg) => { self.hb = Instant::now(); @@ -64,12 +74,16 @@ impl StreamHandler for MnmlSocket { } ws::Message::Text(_text) => (), ws::Message::Close(_) => { + match self.account.as_ref() { + Some(a) => info!("disconnected {:?}", a), + None => info!("disconnected"), + } ctx.stop(); } ws::Message::Nop => (), ws::Message::Binary(bin) => { let db_connection = self.pool.get().expect("unable to get db connection"); - match receive(bin.to_vec(), &db_connection, ctx, begin) { + match receive(bin.to_vec(), &db_connection, ctx, begin, self.account.as_ref()) { Ok(reply) => { let response = to_vec(&reply) .expect("failed to serialize response"); @@ -87,10 +101,10 @@ impl StreamHandler for MnmlSocket { } impl MnmlSocket { - fn new(state: web::Data) -> MnmlSocket { + fn new(state: web::Data, account: Option) -> MnmlSocket { // idk why this has to be cloned again // i guess because each socket is added as a new thread? - MnmlSocket { hb: Instant::now(), pool: state.pool.clone() } + MnmlSocket { hb: Instant::now(), pool: state.pool.clone(), account } } // starts the keepalive interval once actor started @@ -137,8 +151,32 @@ impl ResponseError for MnmlError { // the req obj itself which we need for cookies // the application state // and the websocket stream -fn ws(r: HttpRequest, state: web::Data, stream: web::Payload) -> Result { - ws::start(MnmlSocket::new(state), &r, stream) +fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Result { + let account: Option = match r.cookie("x-auth-token") { + Some(t) => { + let db = state.pool.get().or(Err(MnmlError::ServerError))?; + let mut tx = db.transaction().or(Err(MnmlError::ServerError))?; + match account_from_token(t.value().to_string(), &mut tx) { + Ok(a) => Some(a), + Err(_) => None, + } + }, + None => None, + }; + + ws::start(MnmlSocket::new(state, account), &r, stream) +} + +fn token_res(token: String, secure: bool) -> HttpResponse { + HttpResponse::Ok() + .cookie(Cookie::build("x-auth-token", token) + // .path("/") + .secure(secure) // needs to be enabled for prod + .http_only(true) + .same_site(SameSite::Strict) + .max_age(60 * 60 * 24 * 7) // 1 week + .finish()) + .finish() } fn login(state: web::Data, params: web::Json::) -> Result { @@ -148,14 +186,7 @@ fn login(state: web::Data, params: web::Json::) -> Re 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()) + Ok(token_res(token, state.secure)) }, Err(e) => { info!("{:?}", e); @@ -171,18 +202,11 @@ fn register(state: web::Data, params: web::Json::) - 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()) + Ok(token_res(token, state.secure)) }, Err(e) => { info!("{:?}", e); - Err(MnmlError::Unauthorized) + Err(MnmlError::BadRequest) } } } @@ -199,6 +223,7 @@ fn create_pool(url: String) -> Pool { struct State { pool: PgPool, + secure: bool, } pub fn start() { @@ -211,22 +236,22 @@ pub fn start() { Ok(_) => { warn!("enabling dev CORS middleware"); HttpServer::new(move || App::new() - .data(State { pool: pool.clone() }) + .data(State { pool: pool.clone(), secure: false }) .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)))) + .service(web::resource("/ws/").route(web::get().to(connect)))) .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() }) + .data(State { pool: pool.clone(), secure: true }) .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)))) + .service(web::resource("/ws/").route(web::get().to(connect)))) .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 75fcfa5d..e60bb198 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_from_token, account_constructs, account_instances}; +use account::{Account, 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}; @@ -19,7 +19,7 @@ use item::{Item, ItemInfoCtr, item_info}; type MnmlWs = ws::WebsocketContext; -pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> Result { +pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, account: Option<&Account>) -> Result { // cast the msg to this type to receive method name match from_slice::(&data) { Ok(v) => { @@ -29,11 +29,6 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> let mut tx = db.transaction()?; - let account: Option = match v.token { - Some(t) => Some(account_from_token(t, &mut tx)?), - None => None, - }; - let account_name = match &account { Some(a) => a.name.clone(), None => "none".to_string(), @@ -51,37 +46,36 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> }, }; + let account = account.unwrap(); + // now we have the method name // 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), "item_info" => Ok(RpcResult::ItemInfo(item_info())), + "account_state" => Ok(RpcResult::AccountState(account.clone())), - // AUTH METHODS - "account_constructs" => handle_account_constructs(data, &mut tx, account.unwrap()), - "account_instances" => handle_account_instances(data, &mut tx, account.unwrap()), + "account_constructs" => handle_account_constructs(data, &mut tx, account), + "account_instances" => handle_account_instances(data, &mut tx, account), - "construct_spawn" => handle_construct_spawn(data, &mut tx, account.unwrap()), - "construct_delete" => handle_construct_delete(data, &mut tx, account.unwrap()), + "construct_spawn" => handle_construct_spawn(data, &mut tx, account), + "construct_delete" => handle_construct_delete(data, &mut tx, account), - "game_state" => handle_game_state(data, &mut tx, account.unwrap()), - "game_skill" => handle_game_skill(data, &mut tx, account.unwrap()), - "game_ready" => handle_game_ready(data, &mut tx, account.unwrap()), + "game_state" => handle_game_state(data, &mut tx, account), + "game_skill" => handle_game_skill(data, &mut tx, account), + "game_ready" => handle_game_ready(data, &mut tx, account), - "instance_list" => handle_instance_list(data, &mut tx, account.unwrap()), - "instance_join" => handle_instance_join(data, &mut tx, account.unwrap()), - "instance_ready" => handle_instance_ready(data, &mut tx, account.unwrap()), - "instance_new" => handle_instance_new(data, &mut tx, account.unwrap()), - "instance_state" => handle_instance_state(data, &mut tx, account.unwrap()), + "instance_list" => handle_instance_list(data, &mut tx, account), + "instance_join" => handle_instance_join(data, &mut tx, account), + "instance_ready" => handle_instance_ready(data, &mut tx, account), + "instance_new" => handle_instance_new(data, &mut tx, account), + "instance_state" => handle_instance_state(data, &mut tx, account), - "vbox_accept" => handle_vbox_accept(data, &mut tx, account.unwrap()), - "vbox_apply" => handle_vbox_apply(data, &mut tx, account.unwrap()), - "vbox_combine" => handle_vbox_combine(data, &mut tx, account.unwrap()), - "vbox_discard" => handle_vbox_discard(data, &mut tx, account.unwrap()), - "vbox_reclaim" => handle_vbox_reclaim(data, &mut tx, account.unwrap()), - "vbox_unequip" => handle_vbox_unequip(data, &mut tx, account.unwrap()), + "vbox_accept" => handle_vbox_accept(data, &mut tx, account), + "vbox_apply" => handle_vbox_apply(data, &mut tx, account), + "vbox_combine" => handle_vbox_combine(data, &mut tx, account), + "vbox_discard" => handle_vbox_discard(data, &mut tx, account), + "vbox_reclaim" => handle_vbox_reclaim(data, &mut tx, account), + "vbox_unequip" => handle_vbox_unequip(data, &mut tx, account), _ => Err(format_err!("unknown method - {:?}", v.method)), }; @@ -99,12 +93,12 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant) -> } } -fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_game_state(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; return Ok(RpcResult::GameState(game_state(msg.params, tx, &account)?)); } -// fn handle_game_pve(data: Vec, tx: &mut Transaction, account: Account) -> Result { +// fn handle_game_pve(data: Vec, tx: &mut Transaction, account: &Account) -> Result { // let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; // let game_response = RpcResponse { @@ -115,24 +109,24 @@ fn handle_game_state(data: Vec, tx: &mut Transaction, account: Account) -> R // return Ok(game_response); // } -fn handle_game_skill(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_game_skill(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::GameState(game_skill(msg.params, tx, &account)?)) } -fn handle_game_ready(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_game_ready(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::GameState(game_ready(msg.params, tx, &account)?)) } -fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_construct_spawn(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; construct_spawn(msg.params, tx, &account)?; Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) } -fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; construct_delete(tx, msg.params.id, account.id)?; Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) @@ -150,35 +144,35 @@ fn handle_construct_delete(data: Vec, tx: &mut Transaction, account: Account // Ok(RpcResult::Account(account_login(msg.params, tx)?)) // } -fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_account_constructs(_data: Vec, tx: &mut Transaction, account: &Account) -> Result { Ok(RpcResult::AccountConstructs(account_constructs(tx, &account)?)) } -fn handle_account_instances(_data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_account_instances(_data: Vec, tx: &mut Transaction, account: &Account) -> Result { Ok(RpcResult::AccountInstances(account_instances(tx, &account)?)) } -fn handle_instance_new(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_instance_new(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(instance_new(msg.params, tx, &account)?)) } -fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_instance_ready(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(instance_ready(msg.params, tx, &account)?)) } -fn handle_instance_join(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_instance_join(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(instance_join(msg.params, tx, &account)?)) } -fn handle_instance_list(_data: Vec, tx: &mut Transaction, _account: Account) -> Result { +fn handle_instance_list(_data: Vec, tx: &mut Transaction, _account: &Account) -> Result { Ok(RpcResult::OpenInstances(instance_list(tx)?)) } -fn handle_instance_state(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_instance_state(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; match instance_state(msg.params, tx, &account)? { RpcResult::GameState(p) => Ok(RpcResult::GameState(p)), @@ -187,33 +181,33 @@ fn handle_instance_state(data: Vec, tx: &mut Transaction, account: Account) } } -fn handle_vbox_accept(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_accept(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_accept(msg.params, tx, &account)?)) } -fn handle_vbox_discard(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_discard(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_discard(msg.params, tx, &account)?)) } -fn handle_vbox_combine(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_combine(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_combine(msg.params, tx, &account)?)) } -fn handle_vbox_apply(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_apply(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_apply(msg.params, tx, &account)?)) } -fn handle_vbox_reclaim(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_reclaim(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_reclaim(msg.params, tx, &account)?)) } -fn handle_vbox_unequip(data: Vec, tx: &mut Transaction, account: Account) -> Result { +fn handle_vbox_unequip(data: Vec, tx: &mut Transaction, account: &Account) -> Result { let msg = from_slice::(&data).or(Err(err_msg("invalid params")))?; Ok(RpcResult::InstanceState(vbox_unequip(msg.params, tx, &account)?)) } @@ -225,7 +219,7 @@ pub struct RpcErrorResponse { #[derive(Debug,Clone,Serialize,Deserialize)] pub enum RpcResult { - Account(Account), + AccountState(Account), AccountConstructs(Vec), AccountInstances(Vec), GameState(Game), From 9fcdbeb3702048f4ebfa4a756bc65b3276e9c4f8 Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 15:06:43 +1000 Subject: [PATCH 07/24] token expiry" " --- client/src/components/main.jsx | 7 +- .../20180913000513_create_accounts.js | 3 +- server/src/account.rs | 78 +++++++++++-------- server/src/net.rs | 9 +-- server/src/rpc.rs | 10 +-- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/client/src/components/main.jsx b/client/src/components/main.jsx index 59747677..235ecf4a 100644 --- a/client/src/components/main.jsx +++ b/client/src/components/main.jsx @@ -10,8 +10,8 @@ const List = require('./list'); const addState = connect( state => { - const { game, instance, account, nav, team } = state; - return { game, instance, account, nav, team }; + const { game, instance, account, nav, team, constructs } = state; + return { game, instance, account, nav, team, constructs }; } ); @@ -22,6 +22,7 @@ function Main(props) { account, nav, team, + constructs, } = props; if (!account) { @@ -36,8 +37,8 @@ function Main(props) { return ; } + if (nav === 'team' || !team.some(t => t) || constructs.length < 3) return ; if (nav === 'list') return ; - if (nav === 'team' || !team.some(t => t)) return ; return (
diff --git a/ops/migrations/20180913000513_create_accounts.js b/ops/migrations/20180913000513_create_accounts.js index 35683160..675bfc7c 100755 --- a/ops/migrations/20180913000513_create_accounts.js +++ b/ops/migrations/20180913000513_create_accounts.js @@ -4,7 +4,8 @@ exports.up = async knex => { table.timestamps(true, true); table.string('name', 42).notNullable().unique(); table.string('password').notNullable(); - table.string('token', 64).notNullable(); + table.string('token', 64); + table.timestamp('token_expiry'); table.index('name'); table.index('id'); diff --git a/server/src/account.rs b/server/src/account.rs index e084ac4a..3b85e4da 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -29,13 +29,12 @@ struct AccountEntry { token: String, } -// MAYBE -// hash tokens with a secret pub fn account_from_token(token: String, tx: &mut Transaction) -> Result { let query = " SELECT id, name, token FROM accounts - WHERE token = $1; + WHERE token = $1 + AND token_expiry > now(); "; let result = tx @@ -71,78 +70,93 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut let rounds = 8; let password = hash(&password, rounds)?; - let mut rng = thread_rng(); - let token: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(64) - .collect(); - let query = " - INSERT INTO accounts (id, name, password, token) - VALUES ($1, $2, $3, $4) - RETURNING id, name, token; + INSERT INTO accounts (id, name, password) + VALUES ($1, $2, $3) + RETURNING id, name; "; let result = tx - .query(query, &[&id, &name, &password, &token])?; + .query(query, &[&id, &name, &password])?; - if result.is_empty() { - return Err(err_msg("no row returned")); - } + let returned = match result.iter().next() { + Some(row) => row, + None => return Err(err_msg("account not created")), + }; info!("registration account={:?}", name); - return Ok(token); + let account = Account { + id: returned.get(0), + name: returned.get(1), + }; + + account_set_token(tx, &account) } pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result { let query = " - SELECT id, password + SELECT id, password, name FROM accounts - WHERE name = $1; + WHERE name = $1 + RETURNING id, name; "; let result = tx .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, None => { + let mut rng = thread_rng(); + let garbage: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); + // verify garbage to prevent timing attacks - verify(token.clone(), &token).ok(); + verify(garbage.clone(), &garbage).ok(); return Err(err_msg("account not found")); }, }; - let id: Uuid = returned.get(0); + let account = Account { + id: returned.get(0), + name: returned.get(2), + }; + let hash: String = returned.get(1); if !verify(password, &hash)? { return Err(err_msg("password does not match")); } + account_set_token(tx, &account) +} + +fn account_set_token(tx: &mut Transaction, account: &Account) -> Result { + let mut rng = thread_rng(); + let token: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); + // update token let query = " UPDATE accounts - SET token = $1, updated_at = now() + SET token = $1, updated_at = now(), token_expiry = now() + interval '1 week' WHERE id = $2 RETURNING id; "; let result = tx - .query(query, &[&token, &id])?; + .query(query, &[&token, &account.id])?; - result.iter().next().ok_or(format_err!("user {:?} could not be updated", id))?; + result.iter().next().ok_or(format_err!("user {:?} could not be updated", account.id))?; - info!("login account={:?}", name); + info!("login account={:?}", account.name); - return Ok(token); + 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 d3b9635e..0f4973df 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -26,11 +26,7 @@ const DB_POOL_SIZE: u32 = 20; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); -/// websocket connection is long running connection, it easier -/// to handle with an actor pub struct MnmlSocket { - /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), - /// otherwise we drop connection. hb: Instant, pool: PgPool, account: Option, @@ -170,11 +166,10 @@ fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Res fn token_res(token: String, secure: bool) -> HttpResponse { HttpResponse::Ok() .cookie(Cookie::build("x-auth-token", token) - // .path("/") - .secure(secure) // needs to be enabled for prod + .secure(secure) .http_only(true) .same_site(SameSite::Strict) - .max_age(60 * 60 * 24 * 7) // 1 week + .max_age(60 * 60 * 24 * 7) // 1 week aligns with db set .finish()) .finish() } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index e60bb198..ad359bc2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -29,7 +29,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc let mut tx = db.transaction()?; - let account_name = match &account { + let account_name = match account { Some(a) => a.name.clone(), None => "none".to_string(), }; @@ -37,9 +37,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc // check the method // if no auth required match v.method.as_ref() { - "account_create" => (), - // "account_login" => (), - "item_info" => (), + "item_info" => return Ok(RpcResult::ItemInfo(item_info())), _ => match account { Some(_) => (), None => return Err(err_msg("auth required")), @@ -51,9 +49,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc // now we have the method name // match on that to determine what fn to call let response = match v.method.as_ref() { - "item_info" => Ok(RpcResult::ItemInfo(item_info())), - "account_state" => Ok(RpcResult::AccountState(account.clone())), - + "account_state" => return Ok(RpcResult::AccountState(account.clone())), "account_constructs" => handle_account_constructs(data, &mut tx, account), "account_instances" => handle_account_instances(data, &mut tx, account), From 102a8e9817a2fd3f5ca41e99dff5975046c772d0 Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 15:45:03 +1000 Subject: [PATCH 08/24] logout --- client/src/components/login.jsx | 34 +++++-------------- client/src/components/nav.jsx | 10 +++++- client/src/utils.jsx | 20 +++++++++++ .../20180913000513_create_accounts.js | 2 +- server/src/account.rs | 3 +- server/src/net.rs | 33 +++++++++++++++++- 6 files changed, 72 insertions(+), 30 deletions(-) diff --git a/client/src/components/login.jsx b/client/src/components/login.jsx index 2d76d3ad..34737640 100644 --- a/client/src/components/login.jsx +++ b/client/src/components/login.jsx @@ -3,38 +3,22 @@ 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 { postData } = require('../utils'); const addState = connect( - null, - (dispatch) => { + (state) => { + const { + ws + } = state; function submitLogin(name, password) { - postData(`${SERVER}/login`, { name, password }) - .then(data => console.log(JSON.stringify(data))) + postData('/login', { name, password }) + .then(data => ws.connect()) .catch(error => console.error(error)); } function submitRegister(name, password, code) { - postData(`${SERVER}/register`, { name, password, code }) - .then(data => console.log(JSON.stringify(data))) + postData('/register', { name, password, code }) + .then(data => ws.connect()) .catch(error => console.error(error)); } diff --git a/client/src/components/nav.jsx b/client/src/components/nav.jsx index 931fa328..57d1665e 100644 --- a/client/src/components/nav.jsx +++ b/client/src/components/nav.jsx @@ -1,8 +1,9 @@ const { connect } = require('preact-redux'); const preact = require('preact'); const { Fragment } = require('preact'); -const actions = require('../actions'); +const { postData } = require('../utils'); +const actions = require('../actions'); const { saw } = require('./shapes'); const testGame = process.env.NODE_ENV === 'development' && require('./../test.game'); @@ -38,6 +39,10 @@ const addState = connect( return ws.sendInstanceList(); } + function logout() { + postData('/logout').then(() => window.location.reload(true)); + } + return { account, instances, @@ -47,6 +52,7 @@ const addState = connect( sendInstanceState, sendAccountInstances, sendInstanceList, + logout, }; }, function receiveDispatch(dispatch) { @@ -97,6 +103,7 @@ function Nav(args) { sendInstanceState, sendAccountInstances, sendInstanceList, + logout, setTestGame, setTestInstance, @@ -133,6 +140,7 @@ function Nav(args) {

Hax

+ ) : null; diff --git a/client/src/utils.jsx b/client/src/utils.jsx index 3b506704..5c328a34 100644 --- a/client/src/utils.jsx +++ b/client/src/utils.jsx @@ -345,6 +345,24 @@ const TARGET_COLOURS = { BROWN: '#583108', }; +const SERVER = process.env.NODE_ENV === 'production' ? '/' : 'http://localhost:40000'; +function postData(url = '/', data = {}) { + // Default options are marked with * + return fetch(`${SERVER}${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 + }) +} + module.exports = { stringSort, convertItem, @@ -352,6 +370,8 @@ module.exports = { eventClasses, getCombatSequence, getCombatText, + postData, + SERVER, NULL_UUID, STATS, COLOURS, diff --git a/ops/migrations/20180913000513_create_accounts.js b/ops/migrations/20180913000513_create_accounts.js index 675bfc7c..d0699f89 100755 --- a/ops/migrations/20180913000513_create_accounts.js +++ b/ops/migrations/20180913000513_create_accounts.js @@ -4,7 +4,7 @@ exports.up = async knex => { table.timestamps(true, true); table.string('name', 42).notNullable().unique(); table.string('password').notNullable(); - table.string('token', 64); + table.string('token', 64).notNullable(); table.timestamp('token_expiry'); table.index('name'); diff --git a/server/src/account.rs b/server/src/account.rs index 3b85e4da..3a01285a 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -99,7 +99,6 @@ pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> SELECT id, password, name FROM accounts WHERE name = $1 - RETURNING id, name; "; let result = tx @@ -134,7 +133,7 @@ pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> account_set_token(tx, &account) } -fn account_set_token(tx: &mut Transaction, account: &Account) -> Result { +pub fn account_set_token(tx: &mut Transaction, account: &Account) -> Result { let mut rng = thread_rng(); let token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) diff --git a/server/src/net.rs b/server/src/net.rs index 0f4973df..dd9905e0 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -17,7 +17,7 @@ use r2d2_postgres::{TlsMode, PostgresConnectionManager}; use rpc::{receive, RpcResult, RpcErrorResponse, AccountLoginParams, AccountCreateParams}; use warden::{warden}; -use account::{Account, account_login, account_create, account_from_token}; +use account::{Account, account_login, account_create, account_from_token, account_set_token}; pub type Db = PooledConnection; type PgPool = Pool; @@ -174,6 +174,17 @@ fn token_res(token: String, secure: bool) -> HttpResponse { .finish() } +fn token_clear() -> HttpResponse { + HttpResponse::Ok() + .cookie(Cookie::build("x-auth-token", "") + // .secure(secure) + .http_only(true) + .same_site(SameSite::Strict) + .max_age(-1) // 1 week aligns with db set + .finish()) + .finish() +} + 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))?; @@ -190,6 +201,24 @@ fn login(state: web::Data, params: web::Json::) -> Re } } +fn logout(r: HttpRequest, state: web::Data) -> Result { + match r.cookie("x-auth-token") { + Some(t) => { + let db = state.pool.get().or(Err(MnmlError::ServerError))?; + let mut tx = db.transaction().or(Err(MnmlError::ServerError))?; + match account_from_token(t.value().to_string(), &mut tx) { + Ok(a) => { + account_set_token(&mut tx, &a).or(Err(MnmlError::Unauthorized))?; + tx.commit().or(Err(MnmlError::ServerError))?; + return Ok(token_clear()); + }, + Err(_) => Err(MnmlError::Unauthorized), + } + }, + None => 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))?; @@ -235,6 +264,7 @@ pub fn start() { .wrap(middleware::Logger::default()) .wrap(Cors::new().supports_credentials()) .service(web::resource("/login").route(web::post().to(login))) + .service(web::resource("/logout").route(web::post().to(logout))) .service(web::resource("/register").route(web::post().to(register))) .service(web::resource("/ws/").route(web::get().to(connect)))) .bind("127.0.0.1:40000").expect("could not bind to port") @@ -245,6 +275,7 @@ pub fn start() { .data(State { pool: pool.clone(), secure: true }) .wrap(middleware::Logger::default()) .service(web::resource("/login").route(web::post().to(login))) + .service(web::resource("/logout").route(web::post().to(logout))) .service(web::resource("/register").route(web::post().to(register))) .service(web::resource("/ws/").route(web::get().to(connect)))) .bind("127.0.0.1:40000").expect("could not bind to port") From 7dcbe067663047cf7d75a5fae07ab673d872e4db Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 17:47:45 +1000 Subject: [PATCH 09/24] notifications init --- client/src/socket.jsx | 72 +++++++++++-------- .../20180913000513_create_accounts.js | 2 +- .../20190616170750_notifications.js | 41 +++++++++++ server/src/account.rs | 21 +++--- server/src/net.rs | 35 ++++++--- server/src/pubsub.rs | 0 6 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 ops/migrations/20190616170750_notifications.js create mode 100644 server/src/pubsub.rs diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 523faccf..919c90cb 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -197,9 +197,10 @@ function createSocket(events) { events.setItemInfo(info); } + let pongTimeout; function onPong() { events.setPing(Date.now() - ping); - setTimeout(sendPing, 1000); + pongTimeout = setTimeout(sendPing, 1000); } // ------------- @@ -253,41 +254,52 @@ function createSocket(events) { return handlers[msgType](params); } + // Connection opened + function onOpen() { + toast.info({ + message: 'connected', + position: 'topRight', + }); + + sendPing(); + sendItemInfo(); + + return true; + } + + function onError(event) { + console.error('WebSocket error', event); + } + + function onClose(event) { + console.error('WebSocket closed', event); + toast.warning({ + message: 'disconnected', + position: 'topRight', + }); + return setTimeout(connect, 5000); + } + function connect() { + if (ws) { + clearGameStateTimeout(); + clearInstanceStateTimeout(); + clearTimeout(pongTimeout); + ws.removeEventListener('open', onOpen); + ws.removeEventListener('message', onMessage); + ws.removeEventListener('error', onError); + ws.removeEventListener('close', onClose); + ws = null; + } + ws = new WebSocket(SOCKET_URL); ws.binaryType = 'arraybuffer'; - // Connection opened - ws.addEventListener('open', () => { - toast.info({ - message: 'connected', - position: 'topRight', - }); - - sendPing(); - sendItemInfo(); - - return true; - }); - // Listen for messages + ws.addEventListener('open', onOpen); ws.addEventListener('message', onMessage); - - ws.addEventListener('error', (event) => { - console.error('WebSocket error', event); - // account = null; - // return setTimeout(connect, 5000); - }); - - ws.addEventListener('close', (event) => { - console.error('WebSocket closed', event); - toast.warning({ - message: 'disconnected', - position: 'topRight', - }); - return setTimeout(connect, 5000); - }); - + ws.addEventListener('error', onError); + ws.addEventListener('close', onClose); return ws; } diff --git a/ops/migrations/20180913000513_create_accounts.js b/ops/migrations/20180913000513_create_accounts.js index d0699f89..78fb2381 100755 --- a/ops/migrations/20180913000513_create_accounts.js +++ b/ops/migrations/20180913000513_create_accounts.js @@ -5,7 +5,7 @@ exports.up = async knex => { table.string('name', 42).notNullable().unique(); table.string('password').notNullable(); table.string('token', 64).notNullable(); - table.timestamp('token_expiry'); + table.timestamp('token_expiry').notNullable(); table.index('name'); table.index('id'); diff --git a/ops/migrations/20190616170750_notifications.js b/ops/migrations/20190616170750_notifications.js new file mode 100644 index 00000000..8480fa5f --- /dev/null +++ b/ops/migrations/20190616170750_notifications.js @@ -0,0 +1,41 @@ +const notify = ` +CREATE OR REPLACE FUNCTION notify_event() RETURNS TRIGGER AS $$ + DECLARE + record RECORD; + id UUID; + payload JSON; + BEGIN + IF (TG_OP = 'DELETE') THEN + id = OLD.id; + ELSE + id = NEW.id; + END IF; + + payload = json_build_object( + 'table', TG_TABLE_NAME, + 'action', TG_OP, + 'id', id + ); + + PERFORM pg_notify('events', payload::text); + + RETURN NULL; + END; + +$$ LANGUAGE plpgsql; +`; + +const trigger = table => ` +CREATE TRIGGER notify_${table}_event +AFTER INSERT OR UPDATE OR DELETE ON ${table} + FOR EACH ROW EXECUTE PROCEDURE notify_event(); +`; + +exports.up = async knex => { + await knex.raw(notify); + await knex.raw(trigger('accounts')); + await knex.raw(trigger('games')); + await knex.raw(trigger('instances')); +}; + +exports.down = async () => {}; \ No newline at end of file diff --git a/server/src/account.rs b/server/src/account.rs index 3a01285a..e0b4b88b 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -70,28 +70,29 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut let rounds = 8; let password = hash(&password, rounds)?; + let mut rng = thread_rng(); + let token: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); + let query = " - INSERT INTO accounts (id, name, password) - VALUES ($1, $2, $3) + INSERT INTO accounts (id, name, password, token, token_expiry) + VALUES ($1, $2, $3, $4, now() + interval '1 week') RETURNING id, name; "; let result = tx - .query(query, &[&id, &name, &password])?; + .query(query, &[&id, &name, &password, &token])?; - let returned = match result.iter().next() { + match result.iter().next() { Some(row) => row, None => return Err(err_msg("account not created")), }; info!("registration account={:?}", name); - let account = Account { - id: returned.get(0), - name: returned.get(1), - }; - - account_set_token(tx, &account) + Ok(token) } pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result { diff --git a/server/src/net.rs b/server/src/net.rs index dd9905e0..2f53251f 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -3,13 +3,15 @@ use std::env; use serde_cbor::{to_vec}; -use actix::prelude::*; -use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder}; use actix_web::middleware::cors::Cors; use actix_web::error::ResponseError; use actix_web::http::{StatusCode, Cookie}; use actix_web::cookie::{SameSite}; use actix_web_actors::ws; +use actix::prelude::*; +use actix::fut::ok; +use actix::fut::FutureResult; use r2d2::{Pool}; use r2d2::{PooledConnection}; @@ -107,7 +109,7 @@ impl MnmlSocket { fn hb(&self, ctx: &mut ::Context) { ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { - info!("Websocket Client heartbeat failed, disconnecting!"); + info!("idle connection terminated"); // stop actor ctx.stop(); @@ -134,9 +136,20 @@ enum MnmlError { 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), + MnmlError::ServerError => HttpResponse::InternalServerError() + .json(RpcErrorResponse { err: "server error".to_string() }), + + MnmlError::BadRequest => HttpResponse::BadRequest() + .json(RpcErrorResponse { err: "bad request ".to_string() }), + + MnmlError::Unauthorized => HttpResponse::Unauthorized() + .cookie(Cookie::build("x-auth-token", "") + // .secure(secure) + .http_only(true) + .same_site(SameSite::Strict) + .max_age(-1) // 1 week aligns with db set + .finish()) + .json(RpcErrorResponse { err: "unauthorized ".to_string() }), } } } @@ -163,7 +176,7 @@ fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Res ws::start(MnmlSocket::new(state, account), &r, stream) } -fn token_res(token: String, secure: bool) -> HttpResponse { +fn login_res(token: String, secure: bool) -> HttpResponse { HttpResponse::Ok() .cookie(Cookie::build("x-auth-token", token) .secure(secure) @@ -174,7 +187,7 @@ fn token_res(token: String, secure: bool) -> HttpResponse { .finish() } -fn token_clear() -> HttpResponse { +fn logout_res() -> HttpResponse { HttpResponse::Ok() .cookie(Cookie::build("x-auth-token", "") // .secure(secure) @@ -192,7 +205,7 @@ fn login(state: web::Data, params: web::Json::) -> Re match account_login(¶ms.name, ¶ms.password, &mut tx) { Ok(token) => { tx.commit().or(Err(MnmlError::ServerError))?; - Ok(token_res(token, state.secure)) + Ok(login_res(token, state.secure)) }, Err(e) => { info!("{:?}", e); @@ -210,7 +223,7 @@ fn logout(r: HttpRequest, state: web::Data) -> Result { account_set_token(&mut tx, &a).or(Err(MnmlError::Unauthorized))?; tx.commit().or(Err(MnmlError::ServerError))?; - return Ok(token_clear()); + return Ok(logout_res()); }, Err(_) => Err(MnmlError::Unauthorized), } @@ -226,7 +239,7 @@ fn register(state: web::Data, params: web::Json::) - match account_create(¶ms.name, ¶ms.password, ¶ms.code, &mut tx) { Ok(token) => { tx.commit().or(Err(MnmlError::ServerError))?; - Ok(token_res(token, state.secure)) + Ok(login_res(token, state.secure)) }, Err(e) => { info!("{:?}", e); diff --git a/server/src/pubsub.rs b/server/src/pubsub.rs new file mode 100644 index 00000000..e69de29b From 5c081f8d9f950555eba8323a5bfcc794eb76ccac Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 19:47:16 +1000 Subject: [PATCH 10/24] events --- server/Cargo.toml | 1 + server/src/main.rs | 1 + server/src/net.rs | 47 ++++++++++++++++--------------- server/src/warden.rs | 66 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 84 insertions(+), 31 deletions(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 4cfd6849..cabe80d7 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -19,6 +19,7 @@ dotenv = "0.9.0" postgres = { version = "0.15", features = ["with-uuid", "with-chrono"] } r2d2 = "*" r2d2_postgres = "*" +fallible-iterator = "0.1" failure = "0.1" diff --git a/server/src/main.rs b/server/src/main.rs index eaffe954..5437670e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,6 +8,7 @@ extern crate dotenv; extern crate postgres; extern crate r2d2; extern crate r2d2_postgres; +extern crate fallible_iterator; extern crate actix; extern crate actix_web; diff --git a/server/src/net.rs b/server/src/net.rs index 2f53251f..1afee3ac 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -1,28 +1,27 @@ use std::time::{Instant, Duration}; use std::env; +use std::thread::spawn; use serde_cbor::{to_vec}; -use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder}; +use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer}; use actix_web::middleware::cors::Cors; use actix_web::error::ResponseError; -use actix_web::http::{StatusCode, Cookie}; +use actix_web::http::{Cookie}; use actix_web::cookie::{SameSite}; use actix_web_actors::ws; use actix::prelude::*; -use actix::fut::ok; -use actix::fut::FutureResult; use r2d2::{Pool}; use r2d2::{PooledConnection}; use r2d2_postgres::{TlsMode, PostgresConnectionManager}; use rpc::{receive, RpcResult, RpcErrorResponse, AccountLoginParams, AccountCreateParams}; -use warden::{warden}; +use warden::{Warden}; use account::{Account, account_login, account_create, account_from_token, account_set_token}; pub type Db = PooledConnection; -type PgPool = Pool; +pub type PgPool = Pool; const DB_POOL_SIZE: u32 = 20; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); @@ -177,25 +176,25 @@ fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Res } fn login_res(token: String, secure: bool) -> HttpResponse { - HttpResponse::Ok() - .cookie(Cookie::build("x-auth-token", token) - .secure(secure) - .http_only(true) - .same_site(SameSite::Strict) - .max_age(60 * 60 * 24 * 7) // 1 week aligns with db set - .finish()) - .finish() + HttpResponse::Ok() + .cookie(Cookie::build("x-auth-token", token) + .secure(secure) + .http_only(true) + .same_site(SameSite::Strict) + .max_age(60 * 60 * 24 * 7) // 1 week aligns with db set + .finish()) + .finish() } fn logout_res() -> HttpResponse { - HttpResponse::Ok() - .cookie(Cookie::build("x-auth-token", "") - // .secure(secure) - .http_only(true) - .same_site(SameSite::Strict) - .max_age(-1) // 1 week aligns with db set - .finish()) - .finish() + HttpResponse::Ok() + .cookie(Cookie::build("x-auth-token", "") + // .secure(secure) + .http_only(true) + .same_site(SameSite::Strict) + .max_age(-1) // 1 week aligns with db set + .finish()) + .finish() } fn login(state: web::Data, params: web::Json::) -> Result { @@ -266,9 +265,11 @@ struct State { pub fn start() { let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); - let pool = create_pool(database_url); + let sys = System::new("warden"); + let warden_addr = Warden::new(pool.clone()).start(); + match env::var("DEV_CORS") { Ok(_) => { warn!("enabling dev CORS middleware"); diff --git a/server/src/warden.rs b/server/src/warden.rs index 1b361b37..07b179fd 100644 --- a/server/src/warden.rs +++ b/server/src/warden.rs @@ -1,11 +1,15 @@ +use std::time::{Instant, Duration}; // Db Commons +use fallible_iterator::{FallibleIterator, IntoFallibleIterator}; use postgres::transaction::Transaction; use failure::Error; +use actix::prelude::*; + use game::{games_need_upkeep, game_update, game_write, game_delete}; use instance::{instances_need_upkeep, instances_idle, instance_update, instance_delete}; -use net::{Db}; +use net::{Db, PgPool}; fn fetch_games(mut tx: Transaction) -> Result { let games = games_need_upkeep(&mut tx)?; @@ -40,12 +44,58 @@ fn fetch_instances(mut tx: Transaction) -> Result { Ok(tx) } -pub fn warden(db: Db) -> Result<(), Error> { - fetch_games(db.transaction()?)? - .commit()?; +pub struct Warden { + pool: PgPool, +} - fetch_instances(db.transaction()?)? - .commit()?; +impl Actor for Warden { + type Context = Context; - Ok(()) -} \ No newline at end of file + fn started(&mut self, ctx: &mut Self::Context) { + self.upkeep(ctx); + self.subscribe(ctx); + } +} + +const UPKEEP_INTERVAL: Duration = Duration::from_secs(1); + +impl Warden { + pub fn new(pool: PgPool) -> Warden { + Warden { pool } + } + + // once the actor has been started this fn runs + // it starts the heartbeat interval and keepalive + fn upkeep(&mut self, ctx: &mut ::Context) { + ctx.run_interval(UPKEEP_INTERVAL, |act, _ctx| { + let db = act.pool.get().expect("warden could not get db connection"); + + fetch_games(db.transaction().expect("warden tx failure")) + .expect("upkeep games failure") + .commit().expect("warden games commit failure"); + + fetch_instances(db.transaction().expect("warden tx failure")) + .expect("instances games failure") + .commit().expect("warden instances commit failure"); + }); + } + + fn subscribe(&mut self, ctx: &mut ::Context) { + let db = self.pool.get().expect("warden could not get db connection"); + + db.execute("LISTEN events;", &[]).expect("unable to subscribe to pg events"); + let notifications = db.notifications(); + let mut n_iter = notifications.iter(); + + loop { + match n_iter.next() { + Ok(r) => { + if let Some(n) = r { + info!("{:?}", n); + } + } + Err(e) => warn!("{:?}", e), + } + } + } +} From 50487422367cc61def89d952887f9ada61312afd Mon Sep 17 00:00:00 2001 From: ntr Date: Sun, 16 Jun 2019 22:40:15 +1000 Subject: [PATCH 11/24] pubsub --- server/src/main.rs | 1 + server/src/net.rs | 12 ++++++---- server/src/pubsub.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++ server/src/warden.rs | 22 +------------------ 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 5437670e..e35a6777 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -32,6 +32,7 @@ mod names; mod net; mod player; mod rpc; +mod pubsub; mod skill; mod effect; mod spec; diff --git a/server/src/net.rs b/server/src/net.rs index 1afee3ac..a24ff8be 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -1,6 +1,5 @@ use std::time::{Instant, Duration}; use std::env; -use std::thread::spawn; use serde_cbor::{to_vec}; @@ -18,6 +17,7 @@ use r2d2_postgres::{TlsMode, PostgresConnectionManager}; use rpc::{receive, RpcResult, RpcErrorResponse, AccountLoginParams, AccountCreateParams}; use warden::{Warden}; +use pubsub::PubSub; use account::{Account, account_login, account_create, account_from_token, account_set_token}; pub type Db = PooledConnection; @@ -267,8 +267,12 @@ pub fn start() { .expect("DATABASE_URL must be set"); let pool = create_pool(database_url); - let sys = System::new("warden"); - let warden_addr = Warden::new(pool.clone()).start(); + let sys = System::new("mnml"); + + Warden::new(pool.clone()).start(); + + let pubsub_conn = pool.get().expect("could not get pubsub pg connection"); + let pubsub_addr = Supervisor::start(move |_| PubSub::new(pubsub_conn)); match env::var("DEV_CORS") { Ok(_) => { @@ -294,5 +298,5 @@ pub fn start() { .service(web::resource("/ws/").route(web::get().to(connect)))) .bind("127.0.0.1:40000").expect("could not bind to port") .run().expect("could not start http server"), - } + }; } diff --git a/server/src/pubsub.rs b/server/src/pubsub.rs index e69de29b..d812fc4a 100644 --- a/server/src/pubsub.rs +++ b/server/src/pubsub.rs @@ -0,0 +1,52 @@ +// Db Commons +use fallible_iterator::{FallibleIterator}; + +use actix::prelude::*; + +use net::{Db}; + +pub struct PubSub { + connection: Db, +} + +impl Actor for PubSub { + type Context = Context; + + fn started(&mut self, ctx: &mut Self::Context) { + self.pg_listen(ctx); + self.check_events(ctx); + } +} + +impl Supervised for PubSub { + fn restarting(&mut self, ctx: &mut Context) { + warn!("pubsub restarting"); + } +} + +impl PubSub { + pub fn new(connection: Db) -> PubSub { + PubSub { connection } + } + + fn pg_listen(&mut self, ctx: &mut ::Context) { + self.connection.execute("LISTEN events;", &[]).expect("unable to subscribe to pg events"); + info!("pubsub listening"); + } + + fn check_events(&mut self, ctx: &mut ::Context) { + let notifications = self.connection.notifications(); + let mut n_iter = notifications.iter(); + loop { + match n_iter.next() { + Ok(r) => { + if let Some(n) = r { + info!("{:?}", n); + } + } + Err(e) => warn!("{:?}", e), + } + } + } + +} diff --git a/server/src/warden.rs b/server/src/warden.rs index 07b179fd..8fa4a886 100644 --- a/server/src/warden.rs +++ b/server/src/warden.rs @@ -1,7 +1,6 @@ use std::time::{Instant, Duration}; // Db Commons -use fallible_iterator::{FallibleIterator, IntoFallibleIterator}; use postgres::transaction::Transaction; use failure::Error; @@ -53,7 +52,6 @@ impl Actor for Warden { fn started(&mut self, ctx: &mut Self::Context) { self.upkeep(ctx); - self.subscribe(ctx); } } @@ -68,6 +66,7 @@ impl Warden { // it starts the heartbeat interval and keepalive fn upkeep(&mut self, ctx: &mut ::Context) { ctx.run_interval(UPKEEP_INTERVAL, |act, _ctx| { + debug!("upkeep starting"); let db = act.pool.get().expect("warden could not get db connection"); fetch_games(db.transaction().expect("warden tx failure")) @@ -79,23 +78,4 @@ impl Warden { .commit().expect("warden instances commit failure"); }); } - - fn subscribe(&mut self, ctx: &mut ::Context) { - let db = self.pool.get().expect("warden could not get db connection"); - - db.execute("LISTEN events;", &[]).expect("unable to subscribe to pg events"); - let notifications = db.notifications(); - let mut n_iter = notifications.iter(); - - loop { - match n_iter.next() { - Ok(r) => { - if let Some(n) = r { - info!("{:?}", n); - } - } - Err(e) => warn!("{:?}", e), - } - } - } } From 463241f1e486fd27404c11798b9fa6b7caf9cb51 Mon Sep 17 00:00:00 2001 From: ntr Date: Mon, 17 Jun 2019 13:59:51 +1000 Subject: [PATCH 12/24] remove tungstenite --- server/Cargo.toml | 1 - server/src/main.rs | 2 -- 2 files changed, 3 deletions(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index cabe80d7..26d72aef 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,7 +12,6 @@ serde_cbor = "0.9" chrono = { version = "0.4", features = ["serde"] } -tungstenite = "0.6" bcrypt = "0.2" dotenv = "0.9.0" diff --git a/server/src/main.rs b/server/src/main.rs index e35a6777..c408c99c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,5 @@ extern crate rand; extern crate uuid; -extern crate tungstenite; extern crate bcrypt; extern crate chrono; @@ -55,7 +54,6 @@ fn setup_logger() -> Result<(), fern::InitError> { )) }) .level_for("postgres", log::LevelFilter::Info) - .level_for("tungstenite", log::LevelFilter::Info) .level(log::LevelFilter::Info) .chain(std::io::stdout()) .chain(fern::log_file("log/mnml.log")?) From 6f7c2cf57163ed25bc3a4d206e8e9819e8c94bd8 Mon Sep 17 00:00:00 2001 From: ntr Date: Mon, 17 Jun 2019 16:03:36 +1000 Subject: [PATCH 13/24] buy btn --- client/assets/styles/styles.css | 12 ++++ client/index.html | 1 + client/package.json | 6 ++ client/src/app.jsx | 5 +- client/src/components/account.status.jsx | 82 ++++++++++++++++++++++++ client/src/components/nav.jsx | 24 ++----- 6 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 client/src/components/account.status.jsx diff --git a/client/assets/styles/styles.css b/client/assets/styles/styles.css index 50dadc62..41de24c7 100644 --- a/client/assets/styles/styles.css +++ b/client/assets/styles/styles.css @@ -462,6 +462,18 @@ header { font-size: 1.2em; } +.stripe-btn { + padding: 0 0.5em; + background: whitesmoke; + color: black; + border-radius: 2px; +} + +.stripe-btn:hover { + color: black; +} + + .refresh-btn { border: 1px solid #222; float: right; diff --git a/client/index.html b/client/index.html index 3101999c..bfb399e6 100644 --- a/client/index.html +++ b/client/index.html @@ -17,6 +17,7 @@ +