Add registration and authentication.

This commit is contained in:
yukirij 2024-08-08 23:16:22 -07:00
parent e47558d6d4
commit 7bae892c2e
29 changed files with 1330 additions and 430 deletions

View File

@ -1,9 +1,20 @@
pub struct Game {
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(),
}
}
}

View File

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

View File

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

View File

@ -1,2 +0,0 @@
pub mod code;
pub mod packet;

View File

@ -1,10 +0,0 @@
mod authenticate; pub use authenticate::*;
mod prelude {
pub trait Packet {
type Data;
fn encode(&self) -> Vec<u8>;
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>;
}
} pub use prelude::*;

View File

@ -1,2 +1 @@
mod binary; pub use binary::*;
mod pack; pub use pack::*;

View File

@ -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" }
sparse = { git = "https://git.tsukiyo.org/Utility/sparse" }
trie = { git = "https://git.tsukiyo.org/Utility/trie" }
pool = { git = "https://git.tsukiyo.org/Utility/pool" }

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -0,0 +1,15 @@
use crate::app::authentication::AuthToken;
pub struct Connection {
pub bus:u32,
pub auth:Option<AuthToken>,
}
impl Connection {
pub fn new() -> Self
{
Self {
bus:0,
auth:None,
}
}
}

View File

@ -1,9 +0,0 @@
use game::Game;
pub struct Contest {
game:Game,
p_dawn:u64,
p_dusk:u64,
specators:Vec<u64>,
}

View File

@ -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<User>,
handles:Trie<u32>,
salts:Vec<[u8; 16]>,
sessions:Pool<Session>,
contests:Pool<Contest>,
pub connections:Pool<Connection>,
pub users:Pool<User>,
pub user_next:u32,
pub user_id:Sparse<usize>,
pub user_handle:Trie<u32>,
pub salts:Vec<[u8; 16]>,
pub auths:Trie<Authentication>,
pub sessions:Trie<Session>,
}
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(),
}
}

View File

@ -1,19 +1,12 @@
use crate::app::context::Context;
use game::Game;
pub struct Session {
key:u64,
secret:[u8; 16],
user:Option<usize>,
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<u32>,
p_dusk:Option<u32>,
specators:Vec<u32>,
}

View File

@ -1,5 +1,6 @@
pub struct User {
handle:String,
secret:String,
na_key:usize,
pub id:u32,
pub handle:String,
pub secret:Vec<u8>,
pub na_key:usize,
}

View File

@ -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<protocol::QRPacket>)
{
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<TokioIo<Upgraded>>, 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<hyper::body::Incoming>, args:HttpServiceArgs) -> Result<hyper::Response<http_body_util::Full<Bytes>>, 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<hyper::body::Incoming>, 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::<TokioIo<TlsStream<TcpStream>>>() {
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<hyper::body::Incoming>, 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<hyper::body::Incoming>, 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(_) => {

199
server/src/manager/data.rs Normal file
View File

@ -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<protocol::QRPacket>)
{
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,
} { }
}

View File

@ -0,0 +1,2 @@
mod data; pub use data::*;
mod ws; pub use ws::*;

127
server/src/manager/ws.rs Normal file
View File

@ -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<u8>) -> Vec<u8>
{
[
pack_u16(code),
data,
].concat()
}
pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, 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(())
}

View File

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

View File

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

View File

@ -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<u8>
{
[
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<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
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<u8>
{
vec![]
}
fn decode(_data:&Vec<u8>, _index:&mut usize) -> Result<Self::Data, ()>
{
Err(())
[
pack_u16(self.status),
self.token.to_vec(),
self.secret.to_vec(),
].concat()
}
}

View File

@ -0,0 +1,4 @@
#[derive(Clone, Copy)]
pub struct LocalPacketConnect {
pub bus_id:u32,
}

View File

@ -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<u8> { Vec::new() }
fn decode(_data:&Vec<u8>, _index:&mut usize) -> Result<Self::Data, ()> { Err(()) }
}
} pub use prelude::*;

View File

@ -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<u8>,
pub code:Vec<u8>,
}
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<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
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<u8>
{
[
pack_u16(self.status),
self.token.to_vec(),
self.secret.to_vec(),
].concat()
}
}

View File

@ -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<Arc<Certificate>, ()>
{
match self.certs.get(domain) {
Some(certs) => Ok(certs),
match self.certs.get(domain.as_bytes()) {
Some(certs) => Ok(certs.clone()),
None => Err(())
}
}

View File

@ -1,3 +1,4 @@
#![allow(dead_code)]
pub mod color;
pub mod string;
pub mod pack;

120
www/.css
View File

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

778
www/.js
View File

@ -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,19 +136,415 @@ 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:() => {
},
},
};
@ -84,7 +552,7 @@ 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);
});