From 7bae892c2e1d6f9f511b8f2843535ead13fa6ac9 Mon Sep 17 00:00:00 2001 From: yukirij Date: Thu, 8 Aug 2024 23:16:22 -0700 Subject: [PATCH] Add registration and authentication. --- game/src/game/mod.rs | 15 +- game/src/lib.rs | 1 - game/src/protocol/code.rs | 18 - game/src/protocol/mod.rs | 2 - game/src/protocol/packet/mod.rs | 10 - game/src/util/mod.rs | 1 - server/Cargo.toml | 9 +- server/docs/protocol/requests.md | 36 + server/src/app/authentication.rs | 18 + server/src/app/connection.rs | 15 + server/src/app/contest.rs | 9 - server/src/app/mod.rs | 34 +- server/src/app/session.rs | 25 +- server/src/app/user.rs | 7 +- server/src/main.rs | 141 +--- server/src/manager/data.rs | 199 +++++ server/src/manager/mod.rs | 2 + server/src/manager/ws.rs | 127 +++ server/src/protocol/code.rs | 24 + server/src/protocol/mod.rs | 34 +- .../src/protocol/packet/authenticate.rs | 27 +- server/src/protocol/packet/connect.rs | 4 + server/src/protocol/packet/mod.rs | 12 + server/src/protocol/packet/register.rs | 83 ++ server/src/system/net/certstore/mod.rs | 8 +- server/src/util/mod.rs | 1 + {game => server}/src/util/pack.rs | 0 www/.css | 120 ++- www/.js | 778 +++++++++++++----- 29 files changed, 1330 insertions(+), 430 deletions(-) delete mode 100644 game/src/protocol/code.rs delete mode 100644 game/src/protocol/mod.rs delete mode 100644 game/src/protocol/packet/mod.rs create mode 100644 server/docs/protocol/requests.md create mode 100644 server/src/app/authentication.rs create mode 100644 server/src/app/connection.rs delete mode 100644 server/src/app/contest.rs create mode 100644 server/src/manager/data.rs create mode 100644 server/src/manager/mod.rs create mode 100644 server/src/manager/ws.rs create mode 100644 server/src/protocol/code.rs rename {game => server}/src/protocol/packet/authenticate.rs (79%) create mode 100644 server/src/protocol/packet/connect.rs create mode 100644 server/src/protocol/packet/mod.rs create mode 100644 server/src/protocol/packet/register.rs rename {game => server}/src/util/pack.rs (100%) diff --git a/game/src/game/mod.rs b/game/src/game/mod.rs index c5d521c..80941bc 100644 --- a/game/src/game/mod.rs +++ b/game/src/game/mod.rs @@ -1,9 +1,20 @@ +use crate::board::Board; + +pub enum GameState { + Active, + Complete, +} + pub struct Game { - + pub state:GameState, + pub board:Board, } impl Game { pub fn new() -> Self { - Self { } + Self { + state:GameState::Active, + board:Board::new(), + } } } diff --git a/game/src/lib.rs b/game/src/lib.rs index b1cb4bd..4eff8de 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -4,5 +4,4 @@ pub mod util; pub mod piece; pub mod board; pub mod history; -pub mod protocol; mod game; pub use game::Game; diff --git a/game/src/protocol/code.rs b/game/src/protocol/code.rs deleted file mode 100644 index 0e7586f..0000000 --- a/game/src/protocol/code.rs +++ /dev/null @@ -1,18 +0,0 @@ -#![allow(dead_code)] - -pub const STATUS_OK :u16 = 0x0000; -pub const STATUS_BAD :u16 = 0x0001; -pub const STATUS_NOT_IMPL :u16 = 0x0002; - -pub const CODE_AUTH :u16 = 0x0000; -pub const CODE_REGISTER :u16 = 0x0001; -pub const CODE_RESUME :u16 = 0x0002; -pub const CODE_EXIT :u16 = 0x0003; -pub const CODE_LIST_GAME :u16 = 0x0010; -pub const CODE_LIST_OPEN :u16 = 0x0011; -pub const CODE_LIST_LIVE :u16 = 0x0012; -pub const CODE_JOIN :u16 = 0x0020; -pub const CODE_SPECTATE :u16 = 0x0021; -pub const CODE_LEAVE :u16 = 0x0022; -pub const CODE_RETIRE :u16 = 0x0023; -pub const CODE_PLAY :u16 = 0x0030; diff --git a/game/src/protocol/mod.rs b/game/src/protocol/mod.rs deleted file mode 100644 index 4cdb2d4..0000000 --- a/game/src/protocol/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod code; -pub mod packet; diff --git a/game/src/protocol/packet/mod.rs b/game/src/protocol/packet/mod.rs deleted file mode 100644 index b77a506..0000000 --- a/game/src/protocol/packet/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod authenticate; pub use authenticate::*; - -mod prelude { - pub trait Packet { - type Data; - - fn encode(&self) -> Vec; - fn decode(data:&Vec, index:&mut usize) -> Result; - } -} pub use prelude::*; diff --git a/game/src/util/mod.rs b/game/src/util/mod.rs index 7c193fc..44b6bc9 100644 --- a/game/src/util/mod.rs +++ b/game/src/util/mod.rs @@ -1,2 +1 @@ mod binary; pub use binary::*; -mod pack; pub use pack::*; diff --git a/server/Cargo.toml b/server/Cargo.toml index 516f769..1799fc5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] tokio = { version = "1.39.2", features = ["full"] } -tokio-stream = "0.1.15" tokio-tungstenite = "0.23.1" tokio-rustls = "0.26.0" tokio-util = { version = "0.7.11", features = ["compat"] } @@ -17,9 +16,13 @@ opaque-ke = "2.0.0" hyper = { version = "1.4.1", features = ["full"] } hyper-util = { version = "0.1.7", features = ["tokio"] } http-body-util = "0.1.2" +futures = "0.3.30" +rust-argon2 = "2.1.0" +ring = "0.17.8" game = { path = "../game" } bus = { git = "https://git.tsukiyo.org/Utility/bus" } -trie = { git = "https://git.tsukiyo.org/Utility/trie" } -pool = { git = "https://git.tsukiyo.org/Utility/pool" } +sparse = { git = "https://git.tsukiyo.org/Utility/sparse" } +trie = { git = "https://git.tsukiyo.org/Utility/trie" } +pool = { git = "https://git.tsukiyo.org/Utility/pool" } diff --git a/server/docs/protocol/requests.md b/server/docs/protocol/requests.md new file mode 100644 index 0000000..af9830f --- /dev/null +++ b/server/docs/protocol/requests.md @@ -0,0 +1,36 @@ +# Table + +## User Authentication + + - Register—create a new account. + - Handle + - Secret + - Invite_Code + - Authenticate—log into an account and create a new client session. + - Handle + - Secret + - Deauthenticate—revoke the current client session. + +## Sessions + + - List_Sessions— + - Page + - Ongoing or Complete + - Joinable + - Live + - Update_Sessions— + +## Game + + - Join— + - Session_Id + - Spectate— + - Session_Id + - Leave— + - Retire— + - Play— + - Move_Source [ Board, Pool ] + - Move_From + - Piece_From + - Move_To + - Piece_To diff --git a/server/src/app/authentication.rs b/server/src/app/authentication.rs new file mode 100644 index 0000000..be568ff --- /dev/null +++ b/server/src/app/authentication.rs @@ -0,0 +1,18 @@ +pub type AuthToken = [u8; 8]; + +#[derive(Clone, Copy)] +pub struct Authentication { + pub key:AuthToken, + pub secret:[u8; 16], + pub user:u32, +} +impl Authentication { + pub fn new() -> Self + { + Self { + key:[0; 8], + secret:[0; 16], + user:0, + } + } +} diff --git a/server/src/app/connection.rs b/server/src/app/connection.rs new file mode 100644 index 0000000..c130944 --- /dev/null +++ b/server/src/app/connection.rs @@ -0,0 +1,15 @@ +use crate::app::authentication::AuthToken; + +pub struct Connection { + pub bus:u32, + pub auth:Option, +} +impl Connection { + pub fn new() -> Self + { + Self { + bus:0, + auth:None, + } + } +} diff --git a/server/src/app/contest.rs b/server/src/app/contest.rs deleted file mode 100644 index b6869b8..0000000 --- a/server/src/app/contest.rs +++ /dev/null @@ -1,9 +0,0 @@ -use game::Game; - -pub struct Contest { - game:Game, - - p_dawn:u64, - p_dusk:u64, - specators:Vec, -} diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index ae15e7a..03f7b95 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -1,28 +1,42 @@ -use game::util::pack_u32; +#![allow(dead_code)] + +use sparse::Sparse; use pool::Pool; use trie::Trie; +use crate::util::pack::pack_u32; +pub mod connection; use connection::Connection; pub mod user; use user::User; +pub mod authentication; use authentication::Authentication; pub mod session; use session::Session; -pub mod contest; use contest::Contest; pub mod context; pub struct App { - users:Pool, - handles:Trie, - salts:Vec<[u8; 16]>, - sessions:Pool, - contests:Pool, + pub connections:Pool, + + pub users:Pool, + pub user_next:u32, + pub user_id:Sparse, + pub user_handle:Trie, + pub salts:Vec<[u8; 16]>, + + pub auths:Trie, + pub sessions:Trie, } impl App { pub fn new() -> Self { Self { + connections:Pool::new(), + users:Pool::new(), - handles:Trie::new(), + user_next:0, + user_id:Sparse::new(), + user_handle:Trie::new(), salts:Vec::new(), - sessions:Pool::new(), - contests:Pool::new(), + + auths:Trie::new(), + sessions:Trie::new(), } } diff --git a/server/src/app/session.rs b/server/src/app/session.rs index b4f1148..d20bdc1 100644 --- a/server/src/app/session.rs +++ b/server/src/app/session.rs @@ -1,19 +1,12 @@ -use crate::app::context::Context; +use game::Game; pub struct Session { - key:u64, - secret:[u8; 16], - user:Option, - context:Context, -} -impl Session { - pub fn new() -> Self - { - Self { - key:0, - secret:[0; 16], - user:None, - context:Context::None, - } - } + key:[u8; 8], + secret:[u8; 8], + + game:Game, + + p_dawn:Option, + p_dusk:Option, + specators:Vec, } diff --git a/server/src/app/user.rs b/server/src/app/user.rs index de8676a..328c302 100644 --- a/server/src/app/user.rs +++ b/server/src/app/user.rs @@ -1,5 +1,6 @@ pub struct User { - handle:String, - secret:String, - na_key:usize, + pub id:u32, + pub handle:String, + pub secret:Vec, + pub na_key:usize, } diff --git a/server/src/main.rs b/server/src/main.rs index ac475ac..5959408 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,44 +6,12 @@ mod util; mod app; mod system; mod protocol; +mod manager; use app::App; -use hyper::{body::Bytes, upgrade::Upgraded}; -use system::{cache::WebCache, net::Stream}; -use tokio_stream::StreamExt; -use tokio_tungstenite::WebSocketStream; +use hyper::body::Bytes; use hyper_util::rt::TokioIo; -//use tokio_rustls::server::TlsStream; -//use tokio::net::TcpStream; - -async fn thread_datasystem(mut _app:App, bus:Bus) -{ - use protocol::QRPacket; - use game::protocol::packet::*; - - while match bus.receive_wait() { - Some(packet) => { - match packet.data { - QRPacket::QAuth(_request) => { - let response = PacketAuthResponse::new(); - - // get user id from handle - - // get user salt - - // hash request secret - - // compare hash to user secret - - // return response - bus.send(packet.from, QRPacket::RAuth(response)).is_ok() - } - _ => { true } - } - } - None => false, - } { } -} +use system::{cache::WebCache, net::Stream}; #[derive(Clone)] struct HttpServiceArgs { @@ -51,66 +19,12 @@ struct HttpServiceArgs { cache:WebCache, } -async fn handle_ws(mut ws_stream:WebSocketStream>, args:HttpServiceArgs) -{ - use tokio_tungstenite::tungstenite::protocol::Message; - use game::util::unpack_u16; - use protocol::QRPacket; - use game::protocol::{ - code::*, - packet::*, - }; - - println!("ws ready"); - - let bus_ds = args.bus.mailbox(1).unwrap_or(1); - - while match ws_stream.try_next().await { - Ok(msg) => match msg { - Some(Message::Binary(data)) => { - - let mut index :usize = 0; - let code: u16 = unpack_u16(&data, &mut index); - - match code { - CODE_AUTH => { - match PacketAuth::decode(&data, &mut index) { - Ok(packet) => { - if args.bus.send(bus_ds, QRPacket::QAuth(packet)).is_ok() { - match args.bus.receive_wait() { - Some(resp) => match resp.data { - QRPacket::RAuth(_resp) => { - - } - _ => { } - } - None => { } - } - } - } - Err(_) => { } - } - } - _ => { } - } - - true - } - Some(_) => true, - None => false - } - Err(_) => false, - } { } - - ws_stream.close(None).await.ok(); -} - async fn service_http(mut request:hyper::Request, args:HttpServiceArgs) -> Result>, std::convert::Infallible> +// Serve cached files and upgrade websocket connections. +// { - use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL}}; //SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE + use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL}}; use http_body_util::Full; - //use tokio_tungstenite::accept_async; - //use tokio_tungstenite::tungstenite::handshake::derive_accept_key; println!("Serving: {}", request.uri().path()); @@ -131,37 +45,12 @@ async fn service_http(mut request:hyper::Request, args:Ht _ => { if hyper_tungstenite::is_upgrade_request(&request) { - //if request.headers().get(UPGRADE).map(|h| h == "websocket").unwrap_or(false) { - //let response = create_response_with_body(&request, || Full::new(Bytes::new())).unwrap(); - - //let key = request.headers().get(SEC_WEBSOCKET_KEY) - // .and_then(|v| v.to_str().ok()) - // .map(|k| derive_accept_key(k.as_bytes())) - // .unwrap_or_default(); - if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) { tokio::task::spawn(async move { match websocket.await { - Ok(websocket) => handle_ws(websocket, args).await, - Err(_) => { } - } - //match hyper::upgrade::on(request).await { - /*Ok(upgraded) => { - match upgraded.downcast::>>() { - Ok(parts) => { - match accept_async(parts.io.into_inner()).await { - Ok(ws_stream) => { - println!("here"); - handle_ws(ws_stream, args).await - } - Err(e) => { println!("ws not accepted: {}", e.to_string()); } - } - } - Err(_) => { println!("transfer error"); } - } - } - Er(e) => { println!("upgrade error: {}", e.to_string()); }*/ - //} + Ok(websocket) => manager::handle_ws(websocket, args).await, + Err(_) => Err(()), + }.ok() }); Ok(response) @@ -171,14 +60,6 @@ async fn service_http(mut request:hyper::Request, args:Ht .body(Full::new(Bytes::new())) .unwrap()) } - - //Ok(Response::builder() - // .status(101) - // .header(CONNECTION, "Upgrade") - // .header(UPGRADE, "websocket") - // .header(SEC_WEBSOCKET_ACCEPT, key) - // .body(Full::new(Bytes::new())) - // .unwrap()) } else { Ok(Response::builder() .header(CONTENT_TYPE, "text/html") @@ -190,6 +71,8 @@ async fn service_http(mut request:hyper::Request, args:Ht } async fn handle_http(stream:system::net::tls::TlsStream, addr:SocketAddr, args:HttpServiceArgs) -> Result<(),()> +// Hand off socket connection to Hyper server. +// { use hyper::server::conn::http1; use hyper::service::service_fn; @@ -229,7 +112,7 @@ async fn main() match b_main.connect() { Ok(bus) => { tokio::spawn(async move { - thread_datasystem(app, bus).await; + manager::thread_system(app, bus).await; }); } Err(_) => { diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs new file mode 100644 index 0000000..92ee856 --- /dev/null +++ b/server/src/manager/data.rs @@ -0,0 +1,199 @@ +use bus::Bus; +use crate::{ + app::{ + App, + authentication::Authentication, + user::User, + connection::Connection, + }, + protocol, +}; + +pub async fn thread_system(mut app:App, bus:Bus) +{ + use protocol::*; + use protocol::QRPacketData::*; + use ring::rand::{SecureRandom, SystemRandom}; + + let rng = SystemRandom::new(); + let argon_config = argon2::Config::default(); + + while match bus.receive_wait() { + Some(packet) => { + let qr = &packet.data; + match &qr.data { + QConn(request) => { + let id = app.connections.add(Connection { + bus: request.bus_id, + auth: None, + }); + + println!("Connect: {}", id); + + bus.send(packet.from, QRPacket::new(id as u32, RConn)).is_ok() + } + + QDisconn => { + app.connections.remove(qr.id as usize).ok(); + + println!("Disconnect: {}", qr.id); + true + } + + QRegister(request) => { + let mut response = PacketRegisterResponse::new(); + response.status = STATUS_ERROR; + + println!("Request: Register"); + + let mut is_valid = true; + if request.code != "abc".as_bytes() { response.status = STATUS_BAD_CODE; is_valid = false; } + if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; } + if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; } + + if is_valid { + match app.user_handle.get(request.handle.as_bytes()) { + None => { + let mut salt = [0u8; 16]; + match rng.fill(&mut salt) { + Ok(_) => { + let salt_id = app.salts.len(); + app.salts.push(salt); + + if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) { + let user_id = app.user_next; + app.user_next += 1; + + // Create user entry + let user_pos = app.users.add(User { + id:user_id, + handle:request.handle.clone(), + secret:hash, + na_key:salt_id, + }); + + // Register user pool id and handle + app.user_id.set(user_id as isize, user_pos); + app.user_handle.set(request.handle.as_bytes(), user_id); + + println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id); + + // Generate authentication token and secret + response.status = STATUS_OK; + rng.fill(&mut response.secret).ok(); + loop { + rng.fill(&mut response.token).ok(); + + if app.auths.get(&response.token).is_none() { + app.auths.set(&response.token, Authentication { + key:response.token, + secret:response.secret, + user:user_id, + }); + break; + } + } + + // Attach authentication to connection + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + conn.auth = Some(response.token); + } + } + } + Err(_) => { println!("error: failed to generate salt.") } + } + } + Some(_) => { + response.status = STATUS_BAD_HANDLE; + println!("notice: attempt to register existing handle: '{}'", request.handle); + } + } + } + + bus.send(packet.from, QRPacket::new(qr.id, RRegister(response))).is_ok() + } + + QAuth(request) => { + let mut response = PacketAuthResponse::new(); + response.status = STATUS_ERROR; + + println!("Request: Auth"); + + let mut is_valid = true; + if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; } + if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; } + + if is_valid { + + // Get user data from handle + match app.user_handle.get(request.handle.as_bytes()) { + Some(uid) => { + if let Some(tuid) = app.user_id.get(*uid as isize) { + if let Some(user) = app.users.get(*tuid) { + // Get user salt + if let Some(salt) = app.salts.get(user.na_key) { + + // Verify salted secret against user data + if argon2::verify_raw(&request.secret.as_bytes(), salt, &user.secret, &argon_config).unwrap_or(false) { + println!("Authenticated user '{}' id {}", request.handle, *uid); + + // Generate authentication token and secret + response.status = STATUS_OK; + rng.fill(&mut response.secret).ok(); + loop { + rng.fill(&mut response.token).ok(); + + if app.auths.get(&response.token).is_none() { + app.auths.set(&response.token, Authentication { + key:response.token, + secret:response.secret, + user:*uid, + }); + break; + } + } + + // Attach authentication to connection + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + conn.auth = Some(response.token); + } + } else { + println!("notice: password verification failed."); + } + } else { + println!("error: user salt id '{}' not found.", user.na_key); + } + } else { + println!("error: user with id '{}' not found.", uid); + } + } else { + println!("error: user with id '{}' not found.", uid); + } + } + None => { } + } + } + + bus.send(packet.from, QRPacket::new(qr.id, RAuth(response))).is_ok() + } + + QDeauth => { + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + match conn.auth { + Some(auth) => { + println!("Deauthenticated connection: {}", qr.id); + app.auths.unset(&auth); + } + None => { } + } + conn.auth = None; + } + true + } + + _ => { true } + } + } + None => false, + } { } +} diff --git a/server/src/manager/mod.rs b/server/src/manager/mod.rs new file mode 100644 index 0000000..25ae846 --- /dev/null +++ b/server/src/manager/mod.rs @@ -0,0 +1,2 @@ +mod data; pub use data::*; +mod ws; pub use ws::*; diff --git a/server/src/manager/ws.rs b/server/src/manager/ws.rs new file mode 100644 index 0000000..23f71ae --- /dev/null +++ b/server/src/manager/ws.rs @@ -0,0 +1,127 @@ +use hyper::upgrade::Upgraded; +use hyper_util::rt::TokioIo; +use tokio_tungstenite::WebSocketStream; +use futures::{SinkExt, StreamExt}; + +use crate::{ + protocol, + util::pack::{pack_u16, unpack_u16}, + HttpServiceArgs, +}; + +fn encode_response(code:u16, data:Vec) -> Vec +{ + [ + pack_u16(code), + data, + ].concat() +} + +pub async fn handle_ws(mut ws:WebSocketStream>, args:HttpServiceArgs) -> Result<(),()> +// Handle websocket connection. +// +{ + use tokio_tungstenite::tungstenite::protocol::Message; + use protocol::{QRPacket, QRPacketData::*, code::*, packet::*}; + + let conn_id :u32; + + let bus_ds = args.bus.mailbox(1).unwrap_or(1); + + // Perform connection handshake with data system. + // - Provide system with connection/bus pairing. + // - Acquire connection id. + // + args.bus.send(bus_ds, QRPacket::new(0, QConn(LocalPacketConnect { + bus_id:args.bus.id(), + })))?; + match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match qr.data { + RConn => { conn_id = qr.id; } + _ => { return Err(()); } + } + } + None => { return Err(()); } + } + + // Decode client requests from websocket, + // pass requests to data system, + // and return responses to client. + while match ws.next().await { + Some(msg) => match msg { + Ok(msg) => match msg { + Message::Binary(data) => { + let mut index :usize = 0; + let code: u16 = unpack_u16(&data, &mut index); + match code { + + CODE_REGISTER => match PacketRegister::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QRegister(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RRegister(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { } + } + + CODE_AUTH => match PacketAuth::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QAuth(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RAuth(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { } + } + + CODE_DEAUTH => { + args.bus.send(bus_ds, QRPacket::new(conn_id, QDeauth)).ok(); + } + + _ => { } + } + true + } + _ => { + println!("notice: received unexpected websocket data."); + true + } + } + Err(_) => false, + } + None => false, + } { } + + args.bus.send(bus_ds, QRPacket::new(conn_id, QDisconn)).ok(); + ws.close(None).await.ok(); + Ok(()) +} diff --git a/server/src/protocol/code.rs b/server/src/protocol/code.rs new file mode 100644 index 0000000..66982b7 --- /dev/null +++ b/server/src/protocol/code.rs @@ -0,0 +1,24 @@ +#![allow(dead_code)] + +pub const STATUS_OK :u16 = 0x0000; +pub const STATUS_ERROR :u16 = 0x0001; + +pub const STATUS_BAD_HANDLE :u16 = 0x0001; +pub const STATUS_BAD_SECRET :u16 = 0x0002; +pub const STATUS_BAD_CODE :u16 = 0x0003; + +pub const STATUS_NOT_IMPL :u16 = 0x00FF; + +pub const CODE_REGISTER :u16 = 0x0010; +pub const CODE_AUTH :u16 = 0x0011; +pub const CODE_DEAUTH :u16 = 0x0013; +pub const CODE_AUTH_RESUME :u16 = 0x0012; + +pub const CODE_LIST_SESSION :u16 = 0x0010; + +pub const CODE_SESSION_JOIN :u16 = 0x0020; +pub const CODE_SESSION_SPECTATE :u16 = 0x0021; +pub const CODE_SESSION_LEAVE :u16 = 0x0022; +pub const CODE_SESSION_RETIRE :u16 = 0x0023; + +pub const CODE_GAME_PLAY :u16 = 0x0030; diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index 018a8f8..ed98afa 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -1,7 +1,37 @@ -use game::protocol::packet::*; +#![allow(dead_code)] + +pub mod code; pub use code::*; +pub mod packet; pub use packet::*; #[derive(Clone)] -pub enum QRPacket { +pub enum QRPacketData { + ROk, + + QConn(LocalPacketConnect), + RConn, + + QDisconn, + + QRegister(PacketRegister), + RRegister(PacketRegisterResponse), + QAuth(PacketAuth), RAuth(PacketAuthResponse), + + QDeauth, + + QAuthResume, + RAuthResume, +} + +#[derive(Clone)] +pub struct QRPacket { + pub id:u32, + pub data:QRPacketData, +} +impl QRPacket { + pub fn new(id:u32, data:QRPacketData) -> Self + { + Self { id, data } + } } diff --git a/game/src/protocol/packet/authenticate.rs b/server/src/protocol/packet/authenticate.rs similarity index 79% rename from game/src/protocol/packet/authenticate.rs rename to server/src/protocol/packet/authenticate.rs index 1025aac..aeb972f 100644 --- a/game/src/protocol/packet/authenticate.rs +++ b/server/src/protocol/packet/authenticate.rs @@ -1,4 +1,4 @@ -use crate::util::{pack_u16, unpack_u16}; +use crate::util::pack::{pack_u16, unpack_u16}; use super::Packet; @@ -19,16 +19,6 @@ impl PacketAuth { impl Packet for PacketAuth { type Data = Self; - fn encode(&self) -> Vec - { - [ - pack_u16(self.handle.len() as u16), - self.handle.as_bytes().to_vec(), - pack_u16(self.secret.len() as u16), - self.secret.as_bytes().to_vec(), - ].concat() - } - fn decode(data:&Vec, index:&mut usize) -> Result { let mut result = Self::new(); @@ -65,7 +55,7 @@ impl Packet for PacketAuth { #[derive(Clone)] pub struct PacketAuthResponse { - pub status:bool, + pub status:u16, pub token:[u8; 8], pub secret:[u8; 16], } @@ -73,7 +63,7 @@ impl PacketAuthResponse { pub fn new() -> Self { Self { - status:false, + status:0, token:[0; 8], secret:[0; 16], } @@ -84,11 +74,10 @@ impl Packet for PacketAuthResponse { fn encode(&self) -> Vec { - vec![] - } - - fn decode(_data:&Vec, _index:&mut usize) -> Result - { - Err(()) + [ + pack_u16(self.status), + self.token.to_vec(), + self.secret.to_vec(), + ].concat() } } diff --git a/server/src/protocol/packet/connect.rs b/server/src/protocol/packet/connect.rs new file mode 100644 index 0000000..c1ea047 --- /dev/null +++ b/server/src/protocol/packet/connect.rs @@ -0,0 +1,4 @@ +#[derive(Clone, Copy)] +pub struct LocalPacketConnect { + pub bus_id:u32, +} diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs new file mode 100644 index 0000000..8b3100e --- /dev/null +++ b/server/src/protocol/packet/mod.rs @@ -0,0 +1,12 @@ +mod connect; pub use connect::*; +mod register; pub use register::*; +mod authenticate; pub use authenticate::*; + +mod prelude { + pub trait Packet { + type Data; + + fn encode(&self) -> Vec { Vec::new() } + fn decode(_data:&Vec, _index:&mut usize) -> Result { Err(()) } + } +} pub use prelude::*; diff --git a/server/src/protocol/packet/register.rs b/server/src/protocol/packet/register.rs new file mode 100644 index 0000000..bff9989 --- /dev/null +++ b/server/src/protocol/packet/register.rs @@ -0,0 +1,83 @@ +use crate::util::pack::{pack_u16, unpack_u16}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketRegister { + pub handle:String, + pub secret:Vec, + pub code:Vec, +} +impl PacketRegister { + pub fn new() -> Self + { + Self { + handle:String::new(), + secret:Vec::new(), + code:Vec::new(), + } + } +} +impl Packet for PacketRegister { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + let mut length = unpack_u16(data, index) as usize; + if length > 0 && data.len() >= *index + length { + match String::from_utf8(data[*index..*index+length].to_vec()) { + Ok(text) => { + result.handle = text; + *index += length; + } + Err(_) => { return Err(()) } + } + } else { return Err(()); } + + length = unpack_u16(data, index) as usize; + if length > 0 && data.len() >= *index + length { + result.secret = data[*index..*index+length].to_vec(); + *index += length; + } else { return Err(()); } + + length = unpack_u16(data, index) as usize; + if length > 0 && data.len() >= *index + length { + result.code = data[*index..*index+length].to_vec(); + *index += length; + } else { return Err(()); } + + Ok(result) + } +} + + +#[derive(Clone)] +pub struct PacketRegisterResponse { + pub status:u16, + pub token:[u8; 8], + pub secret:[u8; 16], +} +impl PacketRegisterResponse { + pub fn new() -> Self + { + Self { + status:0, + token:[0; 8], + secret:[0; 16], + } + } +} +impl Packet for PacketRegisterResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + self.token.to_vec(), + self.secret.to_vec(), + ].concat() + } +} diff --git a/server/src/system/net/certstore/mod.rs b/server/src/system/net/certstore/mod.rs index 4b2997e..afd3953 100644 --- a/server/src/system/net/certstore/mod.rs +++ b/server/src/system/net/certstore/mod.rs @@ -58,7 +58,7 @@ impl CertificateStore { }; if certs.is_ok() && key.is_ok() { - self.certs.set(domain, Arc::new(Certificate { + self.certs.set(domain.as_bytes(), Arc::new(Certificate { certs:certs.unwrap(), key:key.unwrap(), })); @@ -70,7 +70,7 @@ impl CertificateStore { pub fn remove(&mut self, domain:&str) -> Result<(),()> { - if self.certs.unset(domain) { + if self.certs.unset(domain.as_bytes()) { Ok(()) } else { Err(()) @@ -79,8 +79,8 @@ impl CertificateStore { pub fn get(&self, domain:&str) -> Result, ()> { - match self.certs.get(domain) { - Some(certs) => Ok(certs), + match self.certs.get(domain.as_bytes()) { + Some(certs) => Ok(certs.clone()), None => Err(()) } } diff --git a/server/src/util/mod.rs b/server/src/util/mod.rs index cca2ae2..bdd920d 100644 --- a/server/src/util/mod.rs +++ b/server/src/util/mod.rs @@ -1,3 +1,4 @@ #![allow(dead_code)] pub mod color; pub mod string; +pub mod pack; diff --git a/game/src/util/pack.rs b/server/src/util/pack.rs similarity index 100% rename from game/src/util/pack.rs rename to server/src/util/pack.rs diff --git a/www/.css b/www/.css index ec562b1..5b10137 100644 --- a/www/.css +++ b/www/.css @@ -47,8 +47,9 @@ body>nav>button{ cursor:pointer; background-color:#282828; - border:0; color:#c0c0c0; + border:0; + outline:0; } body>nav>button:hover{ background-color:#383838; color:#e0e0e0; @@ -71,8 +72,11 @@ body>nav>header{ } main{ - display:block; + display:flex; position:relative; + flex-flow:column nowrap; + align-items:flex-start; + justify-content:flex-start; width:100%; height:100%; @@ -124,8 +128,9 @@ main>nav>section>button{ cursor:pointer; background-color:#282828; - border:0; color:#c0c0c0; + border:0; + outline:0; } main>nav>section>button:hover{ background-color:#383838; @@ -146,14 +151,14 @@ main>nav>section>div{ color:#e0e0e0; } -main table{ +main.list table{ width:100%; border-collapse:collapse; } -main table tr{ +main.list table tr{ height:2.5rem; } -main table th{ +main.list table th{ padding:0 1rem 0 1rem; text-align:center; @@ -164,7 +169,7 @@ main table th{ color:#f0f0f0; border-bottom:1px solid #404040; } -main table td{ +main.list table td{ height:100%; padding:0 1rem 0 1rem; @@ -173,7 +178,7 @@ main table td{ background-color:#303030; color:#f0f0f0; } -main table td:last-child{ +main.list table td:last-child{ display:flex; flex-flow:row nowrap; align-items:flex-start; @@ -181,12 +186,7 @@ main table td:last-child{ flex-grow:1; } -main table td>img{ - height:2rem; - vertical-align:middle; -} - -main table td:last-child>button{ +main.list table td:last-child>button{ display:block; position:relative; width:auto; @@ -198,14 +198,15 @@ main table td:last-child>button{ cursor:pointer; background-color:#303030; - border:0; color:#e0e0e0; + border:0; + outline:0; } -main table td:last-child>button:hover{ +main.list table td:last-child>button:hover{ background-color:#343434; } -main>canvas{ +main.game>canvas{ display:block; position:relative; width:100%; @@ -242,6 +243,7 @@ main.game>div.sidemenu>button{ background-color:#202020; color:#F0F0F0; border:0; + outline:0; font-family:sans-serif; font-size:1rem; @@ -255,4 +257,88 @@ main.game>div.sidemenu>button.warn:hover{ background-color:#602020; } +main.form>section{ + display:flex; + position:relative; + flex-flow:column nowrap; + align-items:center; + justify-content:center; + width:100%; + height:auto; + flex-grow:1; +} + +main.form>section>div{ + display:block; + position:relative; + width:100%; + max-width:30rem; + padding:0.5rem; + + background-color:#303030; +} + +main.form>section>div>table{ + width:100%; +} + +main.form>section>div>table label{ + display:block; + position:relative; + width:auto; + height:auto; + padding:0 0.5rem 0 0.5rem; + + text-align:right; + font-size:1.2rem; + + color:#e0e0e0; +} + +main.form>section>div>table input{ + display:block; + position:relative; + width:100%; + height:2.5rem; + padding:0 1rem 0 1rem; + + text-align:left; + font-size:1.15rem; + + background-color:#202020; + color:#e0e0e0; + border:1px solid #303030; + outline:0; +} +main.form>section>div>table input.error{ + border:1px solid #603030; +} + +main.form>section>div>button{ + display:block; + position:relative; + width:100%; + height:2.5rem; + padding:0.5rem 1rem 0.5rem 1rem; + + font-size:1.2rem; + cursor:pointer; + + background-color:#303030; + color:#e0e0e0; + border:0; + outline:0; +} +main.form>section>div>button.error{ + border:1px solid #603030; +} +main.form>section>div>button:hover{ + background-color:#383838; +} +main.form>section>div>button:disabled{ + background-color:#2c2c2c; + color:#606060; + cursor:pointer; +} + span.text-system{color:#909090;} diff --git a/www/.js b/www/.js index 916f3ff..8626fea 100644 --- a/www/.js +++ b/www/.js @@ -4,28 +4,94 @@ let SCENE = null; let CONNECTED = false; let SOCKET = null; -let USER = null; -let CONTEXT = null; +let CONTEXT = { + Scene: null, + Auth: null, + Data: null, +}; -let UI = { - text:function(value) { +const Status = { + Ok: 0, + Error: 1, + NotImplement: 2, + + BadHandle: 1, + BadSecret: 2, + BadCode: 3, +}; + +const OpCode = { + Register :0x0010, + Authenticate :0x0011, + Resume :0x0012, + Deauthenticate :0x0013, +}; + +class Message { + constructor(code, data) { + this.code = code; + this.data = data; + } +} + +const PACK = { + u8:(value) => { + return new Uint8Array([ value & 0xFF ]); + }, + u16:(value) => { + return new Uint8Array([ (value >> 8) & 0xFF, value & 0xFF ]); + }, + u32:(value) => { + return new Uint8Array([ + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + ]); + }, +}; + +const UI = { + text:(value) => { return document.createTextNode(value); }, - button:function(text, callback) { - let b = document.createElement("button"); - b.innerText = text; - if(callback !== null) { b.addEventListener("click", callback); } - return b; + button:(text, callback) => { + let button = document.createElement("button"); + button.innerText = text; + if(callback !== null) { button.addEventListener("click", callback); } + return button; }, - div:function(children) { + textbox:(id, placeholder) => { + let input = document.createElement("input"); + input.setAttribute("type", "text"); + if(id !== null) { input.setAttribute("id", id); } + input.setAttribute("placeholder", placeholder); + return input; + }, + + password:(id) => { + let input = document.createElement("input"); + input.setAttribute("type", "password"); + if(id !== null) { input.setAttribute("id", id); } + return input; + }, + + label:(name, id) => { + let label = document.createElement("label"); + label.setAttribute("for", id); + label.innerText = name; + return label; + }, + + div:(children) => { let div = document.createElement("div"); for(child of children) { div.appendChild(child); } return div; }, - table:function(header, rows) { + table:(header, rows) => { let table = document.createElement("table"); let tbody = document.createElement("tbody"); @@ -54,9 +120,15 @@ let UI = { return table; }, - mainnav:function(left_children, right_children) { + mainnav:(left_children, right_children) => { let header = document.createElement("nav"); let left = document.createElement("section"); + + if(CONTEXT.Auth === null) { + left.appendChild(UI.button("Register", () => { LOAD(SCENES.Register) })); + left.appendChild(UI.button("Log In", () => { LOAD(SCENES.Authenticate) })); + } + for(child of left_children) { left.appendChild(child); } let right = document.createElement("section"); @@ -64,27 +136,423 @@ let UI = { header.appendChild(left); header.appendChild(right); - return header; + + MAIN.appendChild(header); }, - mainmenu:function() { + mainmenu:() => { if(SOCKET !== null) { - MENU.appendChild(UI.button("Online", function() { LOAD(SCENE.Online); })); - MENU.appendChild(UI.button("Continue", function() { LOAD(SCENE.Continue); })); - MENU.appendChild(UI.button("Join", function() { LOAD(SCENE.Continue); })); - MENU.appendChild(UI.button("Live", function() { LOAD(SCENE.Continue); })); - MENU.appendChild(UI.button("History", function() { LOAD(SCENE.Continue); })); - MENU.appendChild(UI.button("Guide", function() { LOAD(SCENE.Continue); })); - MENU.appendChild(UI.button("About", function() { LOAD(SCENE.Continue); })); + MENU.appendChild(UI.button("Online", () => { LOAD(SCENES.Online); })); + if(CONTEXT.Auth !== null) { + MENU.appendChild(UI.button("Continue", () => { LOAD(SCENES.Continue); })); + MENU.appendChild(UI.button("Join", () => { LOAD(SCENES.Join); })); + } + MENU.appendChild(UI.button("Live", () => { LOAD(SCENES.Live); })); + MENU.appendChild(UI.button("History", () => { LOAD(SCENES.History); })); + MENU.appendChild(UI.button("Guide", () => { LOAD(SCENES.Guide); })); + MENU.appendChild(UI.button("About", () => { LOAD(SCENES.About); })); + + if(CONTEXT.Auth !== null) { + MENU.appendChild(UI.button("Logout", () => { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.Deauthenticate), + ]); + CONTEXT.Auth = null; + LOAD(SCENE); + })); + } } }, }; +const SCENES = { + Init:{ + load:() => { + LOAD_OFFLINE(); + CONTEXT.Scene = SCENES.Online; + RECONNECT(); + return true; + }, + }, + + Offline:{ + load:() => { + MENU.appendChild(UI.button("Reconnect", () => { RECONNECT(); })) + return true; + }, + }, + + Register:{ + load:() => { + if(CONTEXT.Auth !== null) return false; + UI.mainmenu(); + UI.mainnav([], []); + + let container = document.createElement("section"); + let form = document.createElement("div"); + form.appendChild(UI.table(null, [ + [ UI.label("Handle", "handle"), UI.textbox("handle", "") ], + [ UI.label("Secret", "secret"), UI.password("secret") ], + [ UI.label("Code", "code"), UI.password("code") ], + ])); + + let button = UI.button("Register", (event) => { + let handle = document.getElementById("handle"); + let secret = document.getElementById("secret"); + let code = document.getElementById("code"); + + handle.removeAttribute("class"); + secret.removeAttribute("class"); + code.removeAttribute("class"); + event.target.removeAttribute("class"); + + if(handle.value.length > 0 && secret.value.length > 0 && code.value.length > 0) { + event.target.setAttribute("disabled", ""); + + let enc = new TextEncoder(); + let enc_handle = enc.encode(handle.value); + let enc_secret = enc.encode(secret.value); + let enc_code = enc.encode(code.value); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.Register), + PACK.u16(enc_handle.length), + enc_handle, + PACK.u16(enc_secret.length), + enc_secret, + PACK.u16(enc_code.length), + enc_code, + ]); + } else { + if(handle.value.length == 0) { handle.setAttribute("class", "error"); } + if(secret.value.length == 0) { secret.setAttribute("class", "error"); } + if(code.value.length == 0) { code.setAttribute("class", "error"); } + } + }); + button.setAttribute("id", "submit"); + form.appendChild(button); + + container.appendChild(form); + MAIN.appendChild(container); + MAIN.setAttribute("class", "form"); + + return true; + }, + message:(code, data) => { + if(code == OpCode.Register && data !== null) { + let submit = document.getElementById("submit"); + switch(data.status) { + case Status.Ok: { + CONTEXT.Auth = data; + LOAD(SCENES.Online); + } break; + default: { + submit.removeAttribute("disabled"); + switch(data.status) { + case Status.BadHandle: { + document.getElementById("handle").setAttribute("class", "error"); + submit.setAttribute("class", "error"); + } break; + case Status.BadSecret: { + document.getElementById("secret").setAttribute("class", "error"); + submit.setAttribute("class", "error"); + } break; + case Status.BadCode: { + document.getElementById("code").setAttribute("class", "error"); + submit.setAttribute("class", "error"); + } break; + } + } + } + } + }, + }, + + Authenticate:{ + load:() => { + if(CONTEXT.Auth !== null) return false; + UI.mainmenu(); + UI.mainnav([], []); + + let container = document.createElement("section"); + let form = document.createElement("div"); + form.appendChild(UI.table(null, [ + [ UI.label("Handle", "handle"), UI.textbox("handle", "") ], + [ UI.label("Secret", "secret"), UI.password("secret") ], + ])); + + let button = UI.button("Register", (event) => { + let handle = document.getElementById("handle"); + let secret = document.getElementById("secret"); + + handle.removeAttribute("class"); + secret.removeAttribute("class"); + event.target.removeAttribute("class"); + + if(handle.value.length > 0 && secret.value.length > 0) { + event.target.setAttribute("disabled", ""); + + let enc = new TextEncoder(); + let enc_handle = enc.encode(handle.value); + let enc_secret = enc.encode(secret.value); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.Authenticate), + PACK.u16(enc_handle.length), + enc_handle, + PACK.u16(enc_secret.length), + enc_secret, + ]); + } else { + if(handle.value.length == 0) { handle.setAttribute("class", "error"); } + if(secret.value.length == 0) { secret.setAttribute("class", "error"); } + } + }); + button.setAttribute("id", "submit"); + form.appendChild(button); + + container.appendChild(form); + MAIN.appendChild(container); + MAIN.setAttribute("class", "form"); + + return true; + }, + message:(code, data) => { + if(code == OpCode.Authenticate && data !== null) { + let submit = document.getElementById("submit"); + switch(data.status) { + case Status.Ok: { + CONTEXT.Auth = data; + LOAD(SCENES.Online); + } break; + case Status.Error: { + submit.removeAttribute("disabled"); + + document.getElementById("handle").setAttribute("class", "error"); + document.getElementById("secret").setAttribute("class", "error"); + submit.setAttribute("class", "error"); + } + } + } + }, + }, + + Online:{ + load:() => { + UI.mainmenu(); + + let left_buttons = [ ]; + if(CONTEXT.Auth !== null) { + left_buttons.push(UI.button("Start", null)); + } + + UI.mainnav( + left_buttons, + [ + UI.div([UI.text("0 - 0 of 0")]), + UI.button("◀", null), + UI.button("▶", null), + ] + ); + + let table = document.createElement("table"); + table.setAttribute("id", "content"); + MAIN.appendChild(table); + + MAIN.setAttribute("class", "list"); + + SCENE.refresh(); + return true; + }, + refresh:() => { + let request = new Uint8Array(); + + //SERVER.send() + + SCENE.message(0, 0, null); + }, + message:(code, data) => { + let table = document.getElementById("content"); + MAIN.removeChild(table); + + let rows = [ + [ UI.text("Player1"), UI.text("Player2"), UI.text("0"), UI.text("Ha1-D ◈ Ba1-M"), UI.text("0"), [ UI.button("Join", null), UI.button("Spectate", null) ] ], + ]; + + MAIN.appendChild(UI.table( + [ "Dawn", "Dusk", "Turn", "Move", "Spectators", "" ], + rows, + )); + } + }, + + Continue:{ + load:() => { + if(CONTEXT.Auth === null) return false; + UI.mainmenu(); + + let left_buttons = [ ]; + if(CONTEXT.Auth !== null) { + left_buttons.push(UI.button("Start", null)); + } + + UI.mainnav( + left_buttons, + [ + UI.div([UI.text("0 - 0 of 0")]), + UI.button("◀", null), + UI.button("▶", null), + ] + ); + + let table = document.createElement("table"); + table.setAttribute("id", "content"); + MAIN.appendChild(table); + + MAIN.setAttribute("class", "list"); + + SCENE.refresh(); + return true; + }, + refresh:() => { + + }, + }, + + Join:{ + load:() => { + if(CONTEXT.Auth === null) return false; + UI.mainmenu(); + + let left_buttons = [ ]; + if(CONTEXT.Auth !== null) { + left_buttons.push(UI.button("Start", null)); + } + + UI.mainnav( + left_buttons, + [ + UI.div([UI.text("0 - 0 of 0")]), + UI.button("◀", null), + UI.button("▶", null), + ] + ); + + let table = document.createElement("table"); + table.setAttribute("id", "content"); + MAIN.appendChild(table); + + MAIN.setAttribute("class", "list"); + + SCENE.refresh(); + return true; + }, + refresh:() => { + + }, + }, + + Live:{ + load:() => { + UI.mainmenu(); + + let left_buttons = [ ]; + if(CONTEXT.Auth !== null) { + left_buttons.push(UI.button("Start", null)); + } + + UI.mainnav( + left_buttons, + [ + UI.div([UI.text("0 - 0 of 0")]), + UI.button("◀", null), + UI.button("▶", null), + ] + ); + + let table = document.createElement("table"); + table.setAttribute("id", "content"); + MAIN.appendChild(table); + + MAIN.setAttribute("class", "list"); + + SCENE.refresh(); + return true; + }, + refresh:() => { + + }, + }, + + History:{ + load:() => { + UI.mainmenu(); + + UI.mainnav( + [ ], + [ + UI.div([UI.text("0 - 0 of 0")]), + UI.button("◀", null), + UI.button("▶", null), + ] + ); + + let table = document.createElement("table"); + table.setAttribute("id", "content"); + MAIN.appendChild(table); + + MAIN.setAttribute("class", "list"); + + SCENE.refresh(); + return true; + }, + refresh:() => { + + }, + }, + + Guide:{ + load:() => { + UI.mainmenu(); + UI.mainnav([], []); + return true; + }, + refresh:() => { + + }, + }, + + About:{ + load:() => { + UI.mainmenu(); + UI.mainnav([], []); + return true; + }, + refresh:() => { + + }, + }, + + Game:{ + load:() => { + MENU.appendChild(UI.button("Back", () => { LOAD(SCENES.Online) })); + MENU.appendChild(UI.button("Retire", () => { })); + return true; + }, + unload:() => { + + }, + refresh:() => { + + }, + }, +}; + function RECONNECT() { if(SOCKET === null) { console.log("Websocket connecting.."); SOCKET = new WebSocket("wss://omen.kirisame.com:38612"); - SOCKET.binaryType = 'blob'; + SOCKET.binaryType = "arraybuffer"; SOCKET.addEventListener("error", (event) => { SOCKET = null; LOAD(SCENES.Offline) @@ -93,9 +561,7 @@ function RECONNECT() { if(SOCKET.readyState === WebSocket.OPEN) { console.log("Websocket connected."); - SOCKET.addEventListener("message", (event) => { - MESSAGE(event.data); - }); + SOCKET.addEventListener("message", MESSAGE); SOCKET.addEventListener("close", (event) => { console.log("Websocket closed."); @@ -104,171 +570,15 @@ function RECONNECT() { }); RESUME(); - SOCKET.send(new Uint8Array([ 0 ])); } }); } } function RESUME() { - LOAD(SCENES.Online); + LOAD(CONTEXT.Scene); } -const SCENES = { - Init:{ - load:function() { - LOAD(SCENES.Offline); - RECONNECT(); - return true; - }, - }, - - Offline:{ - load:function() { - MENU.appendChild(UI.button("Reconnect", function() { RECONNECT(); })) - return true; - }, - }, - - Online:{ - load:function() { - UI.mainmenu(); - MAIN.appendChild(UI.mainnav([ - UI.button("Start", null), - ], [ - UI.div([UI.text("0 - 0 of 0")]), - UI.button("◀", null), - UI.button("▶", null), - ])); - - let table = document.createElement("table"); - table.setAttribute("id", "content"); - MAIN.appendChild(table); - - this.refresh(); - return true; - }, - refresh:function() { - let request = new Uint8Array(); - - //SERVER.send() - - let cb = function() { - let table = document.getElementById("content"); - MAIN.removeChild(table); - - let data = [ - [ UI.text("Player1"), UI.text("Player2"), UI.text("0"), UI.text("Ha1-D ◈ Ba1-M"), UI.text("0"), [ UI.button("Join", null), UI.button("Spectate", null) ] ], - ]; - - MAIN.appendChild(UI.table( - [ "Dawn", "Dusk", "Turn", "Move", "Spectators", "" ], - data, - )); - }; - cb(); - }, - }, - - Continue:{ - load:function() { - if(USER === null) return false; - UI.mainmenu(); - MAIN.appendChild(UI.mainnav([ - UI.button("Start", null), - ], [ - UI.button("◀", null), - UI.button("▶", null), - ])); - return true; - }, - refresh:function() { - - }, - }, - - Join:{ - load:function() { - if(USER === null) return false; - UI.mainmenu(); - MAIN.appendChild(UI.mainnav([ - UI.button("Start", null), - ], [ - UI.button("◀", null), - UI.button("▶", null), - ])); - return true; - }, - refresh:function() { - - }, - }, - - Live:{ - load:function() { - UI.mainmenu(); - MAIN.appendChild(UI.mainnav([ - UI.button("Start", null), - ], [ - UI.button("◀", null), - UI.button("▶", null), - ])); - return true; - }, - refresh:function() { - - }, - }, - - History:{ - load:function() { - UI.mainmenu(); - MAIN.appendChild(UI.mainnav([ ], [ - UI.button("◀", null), - UI.button("▶", null), - ])); - return true; - }, - refresh:function() { - - }, - }, - - Guide:{ - load:function() { - UI.mainmenu(); - return true; - }, - refresh:function() { - - }, - }, - - About:{ - load:function() { - UI.mainmenu(); - return true; - }, - refresh:function() { - - }, - }, - - Game:{ - load:function() { - MENU.appendChild(UI.button("Back", function() { LOAD(SCENES.Online) })); - MENU.appendChild(UI.button("Retire", function() { })); - return true; - }, - unload:function() { - - }, - refresh:function() { - - }, - }, -}; - function REBUILD() { MENU = document.createElement("nav"); let title = document.createElement("header"); @@ -281,8 +591,99 @@ function REBUILD() { document.body.appendChild(MAIN); } -function MESSAGE() { +function MESSAGE(event) { + console.log("Message received."); + if(SCENE.message !== undefined) { + let bytes = new Uint8Array(event.data); + let code = 0; + let index = 2; + let data = null; + + if(bytes.length >= 2) { + code = (bytes[0] << 8) + bytes[1]; + } + + switch(code) { + case OpCode.Register: { + console.log("Register response."); + + if(bytes.length == 28) { + console.log("Good size"); + data = { + status:(bytes[2] << 8) + bytes[3], + token:new Uint8Array(), + secret:new Uint8Array(), + }; + index += 2; + + for(let i = 0; i < 8; ++i) { + data.token += bytes[index++]; + } + for(let i = 0; i < 16; ++i) { + data.secret += bytes[index++]; + } + } else { + console.log("Register bad length:" + bytes.length); + return; + } + } break; + + case OpCode.Authenticate: { + console.log("Authenticate response."); + + if(bytes.length == 28) { + console.log("Good size"); + data = { + status:(bytes[2] << 8) + bytes[3], + token:new Uint8Array(), + secret:new Uint8Array(), + }; + index += 2; + + for(let i = 0; i < 8; ++i) { + data.token += bytes[index++]; + } + for(let i = 0; i < 16; ++i) { + data.secret += bytes[index++]; + } + } else { + console.log("Authenticate bad length:" + bytes.length); + return; + } + } break; + + case OpCode.Resume: { + + } break; + + case OpCode.Deauthenticate: { + + } break; + default: + return; + } + + SCENE.message(code, data); + } +} + +function MESSAGE_COMPOSE(data) { + if(SOCKET !== null) { + let length = 0; + for(let i = 0; i < data.length; ++i) { + length += data[i].length; + } + + let raw = new Uint8Array(length); + length = 0; + for(let i = 0; i < data.length; ++i) { + raw.set(data[i], length); + length += data[i].length; + } + + SOCKET.send(raw); + } } function LOAD(scene) { @@ -290,10 +691,19 @@ function LOAD(scene) { while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } REBUILD(); SCENE = scene; + CONTEXT.Scene = SCENE; if(!SCENE.load()) { LOAD(SCENES.Online); } } -document.addEventListener("DOMContentLoaded", function() { +function LOAD_OFFLINE() { + if(SCENE.unload !== undefined) { SCENE.unload(); } + while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } + REBUILD(); + SCENE = SCENES.Offline; + if(!SCENE.load()) { LOAD(SCENES.Online); } +} + +document.addEventListener("DOMContentLoaded", () => { SCENE = SCENES.Offline; LOAD(SCENES.Init); });