Add persistent storage for salt and handle.

This commit is contained in:
yukirij 2024-08-10 15:04:34 -07:00
parent d5c0d74016
commit ee12d74c67
23 changed files with 901 additions and 418 deletions

View File

@ -19,6 +19,7 @@ http-body-util = "0.1.2"
futures = "0.3.30"
rust-argon2 = "2.1.0"
ring = "0.17.8"
const_format = "0.2.32"
game = { path = "../game" }

View File

@ -1,36 +0,0 @@
# 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,143 @@
### Status Codes ##
# General
0000 OK
0001 ERROR
0002 NOAUTH
00FE SERVER_ERROR
00FF NOT_IMPLEMENTED
# Context Specific
0010 BAD_HANDLE
0011 BAD_SECRET
0012 BAD_CODE
###
## Authentication ##
# 0010 REGISTER
Req_Register {
handle :sequence # username
secret :sequence # password
code :sequence # invitation code
}
Res_Register {
status :block<2>
# OK - registration accepted
# BAD_HANDLE - handle empty or not vacant
# BAD_SECRET - secret is empty
# BAD_CODE - code not accepted
token :block<8> # auth key
secret :block<16> # resume code
}
# 0011 AUTH
Req_Auth {
handle :sequence # username
secret :sequence # password
}
Res_Auth {
status :block<2>
# STATUS_OK - authentication successful
# ERROR - authentication failed
token :block<8> # auth key
secret :block<16> # resume code
}
# 0012 AUTH_RESUME
Req_AuthResume {
token :block<8> # auth key
secret :block<16> # resume code
}
Res_AuthResume {
status :block<2>
# OK - resume accepted
# ERROR - auth/code pair not accepted
}
# 0013 AUTH_REVOKE
Req_AuthRevoke { }
Res_AuthRevoke { }
## Session ##
# 0020 SESSION_LIST
Req_SessionList {
flags :block<2>
# 0[2] Game State
# 2[1] Is Player
# 3[1] Is Live
page :block<2>
}
Res_SessionList {
records :list<{
token :block<8> # session key
handle_dawn :sequence # username of dawn player
handle_dusk :sequence # username of dusk player
turn :block<2> # turn number
last_move :block<3> # most recent move code
viewers :block<4> # number of viewers
player :bool # user is player
}>
}
# 0021 SESSION_CREATE
Req_SessionCreate { }
Res_SessionCreate {
status :block<2>
token :block<8>
mode :block<1>
# 0: Player Dawn
# 1: Player Dusk
# 2: Spectator
}
# 0022 SESSION_JOIN
Res_SessionJoin {
token :block<8> # session key
}
Req_SessionJoin {
status :block<2>
# OK
# NOAUTH
token :block<8> # session key
mode :block<1>
# 0: Player Dawn
# 1: Player Dusk
# 2: Spectator
}
# 002E SESSION_LEAVE
Req_SessionLeave { }
Res_SessionLeave { }
# 002F SESSION_RETIRE
Req_SessionRetire {
token :block<8> # session key
}
Res_SessionRetire {
status:block<2>
}
## Game ##
# 0030 GAME_STATE
Req_GameState {
token :block<8> # session token
}
Res_GameState {
}
# 0031 GAME_PLAY
Req_GamePlay {
token :block<8> # session token
}
Res_GamePlay {
}

View File

@ -1,15 +1,16 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use futures::stream::SplitSink;
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
use crate::app::authentication::AuthToken;
type StreamType = Arc<RwLock<SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>>>;
pub struct Connection {
pub bus:u32,
pub stream:StreamType,
pub auth:Option<AuthToken>,
}
impl Connection {
pub fn new() -> Self
{
Self {
bus:0,
auth:None,
}
}
}

View File

@ -3,9 +3,9 @@
use sparse::Sparse;
use pool::Pool;
use trie::Trie;
use crate::util::{
Chain,
pack::pack_u32,
use crate::{
system::filesystem::FileSystem,
util::Chain,
};
pub mod connection; use connection::Connection;
@ -15,13 +15,15 @@ pub mod session; use session::{Session, SessionToken};
pub mod context;
pub struct App {
pub filesystem:FileSystem,
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 salts:Sparse<[u8; 16]>,
pub auths:Trie<Authentication>,
pub sessions:Trie<Session>,
@ -29,39 +31,45 @@ pub struct App {
pub session_time:Chain<SessionToken>,
}
impl App {
pub fn new() -> Self
pub fn init() -> Result<Self, ()>
{
Self {
connections:Pool::new(),
if let Ok(mut filesystem) = FileSystem::init() {
users:Pool::new(),
user_next:0,
user_id:Sparse::new(),
user_handle:Trie::new(),
salts:Vec::new(),
// Load salts
let mut salts = Sparse::new();
let salt_count = filesystem.salt_count()?;
for id in 0..salt_count {
let salt = filesystem.salt_fetch(id as u32).unwrap();
salts.set(id as isize, salt);
}
auths:Trie::new(),
sessions:Trie::new(),
// Load handles
let mut user_handle = Trie::new();
let handle_count = filesystem.handle_count()?;
for id in 0..handle_count {
let (handle, user_id) = filesystem.handle_fetch(id as u32).unwrap();
println!("got: {} = {}", handle, user_id);
user_handle.set(handle.as_bytes(), user_id);
}
session_time:Chain::new(),
Ok(Self {
filesystem:filesystem,
connections:Pool::new(),
users:Pool::new(),
user_next:0,
user_id:Sparse::new(),
user_handle:Trie::new(),
salts,
auths:Trie::new(),
sessions:Trie::new(),
session_time:Chain::new(),
})
} else {
Err(())
}
}
pub fn init(&mut self) -> Result<(),std::io::Error>
{ use std::{path::Path, fs};
let path_data = Path::new("data");
if !path_data.exists() {
fs::create_dir(path_data)?;
fs::create_dir(path_data.join("c"))?;
fs::create_dir(path_data.join("s"))?;
fs::create_dir(path_data.join("u"))?;
fs::write(path_data.join("c/.i"), pack_u32(0)).ok();
fs::write(path_data.join("s/.i"), pack_u32(0)).ok();
fs::write(path_data.join("u/.i"), pack_u32(0)).ok();
}
Ok(())
}
}

View File

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

View File

@ -100,13 +100,14 @@ async fn main()
};
// Initialize application data.
let mut app = App::new();
if app.init().is_err() {
let app;
if let Ok(result) = App::init() {
app = result;
} else {
println!("fatal: failed to initialize server.");
return;
}
// Initialize central bus and data serivce.
let b_main :Bus<protocol::QRPacket> = Bus::new_as(bus::Mode::Transmitter);
match b_main.connect() {

View File

@ -1,3 +1,6 @@
use tokio_tungstenite::tungstenite::Message;
use futures::SinkExt;
use bus::Bus;
use crate::{
app::{
@ -7,20 +10,28 @@ use crate::{
connection::Connection,
},
protocol,
util::pack::pack_u16,
};
fn encode_response(code:u16, data:Vec<u8>) -> Vec<u8>
{
[
pack_u16(code),
data,
].concat()
}
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() {
while let Some(response) = match bus.receive_wait() {
Some(packet) => {
let qr = &packet.data;
let qr = packet.data;
let mut user_id = None;
if let Some(conn) = app.connections.get(qr.id as usize) {
@ -31,28 +42,41 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
}
match &qr.data {
QConn(request) => {
match qr.data {
QRPacketData::QConn(request) => {
let id = app.connections.add(Connection {
bus: request.bus_id,
stream: request.stream,
auth: None,
});
println!("Connect: {}", id);
bus.send(packet.from, QRPacket::new(id as u32, RConn)).is_ok()
bus.send(
packet.from,
QRPacket::new(id as u32, QRPacketData::RConn)
).ok();
Some(QRPacket::new(0, QRPacketData::None))
}
QDisconn => {
app.connections.remove(qr.id as usize).ok();
QRPacketData::QDisconn => {
// Close socket and remove connection if valid
//
if if let Some(conn) = app.connections.get_mut(qr.id as usize) {
let mut socket = conn.stream.write().await;
socket.close().await.ok();
true
} else { false } {
app.connections.remove(qr.id as usize).ok();
println!("Disconnect: {}", qr.id);
true
println!("Disconnect: {}", qr.id);
}
Some(QRPacket::new(0, QRPacketData::None))
}
QRegister(request) => {
QRPacketData::QRegister(request) => {
let mut response = PacketRegisterResponse::new();
response.status = STATUS_ERROR;
response.status = STATUS_SERVER_ERROR;
println!("Request: Register");
@ -67,8 +91,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
let mut salt = [0u8; 16];
match rng.fill(&mut salt) {
Ok(_) => {
let salt_id = app.salts.len();
app.salts.push(salt);
let salt_id = app.filesystem.salt_store(salt).unwrap();
app.salts.set(salt_id as isize, salt);
if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
let user_id = app.user_next;
@ -83,6 +107,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
});
// Register user pool id and handle
app.filesystem.handle_store(&request.handle, user_id).ok();
app.user_id.set(user_id as isize, user_pos);
app.user_handle.set(request.handle.as_bytes(), user_id);
@ -120,10 +145,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
}
bus.send(packet.from, QRPacket::new(qr.id, RRegister(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RRegister(response)))
}
QAuth(request) => {
QRPacketData::QAuth(request) => {
let mut response = PacketAuthResponse::new();
response.status = STATUS_ERROR;
@ -141,7 +166,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
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) {
if let Some(salt) = app.salts.get(user.na_key as isize) {
// Verify salted secret against user data
if argon2::verify_raw(&request.secret.as_bytes(), salt, &user.secret, &argon_config).unwrap_or(false) {
@ -184,10 +209,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
}
bus.send(packet.from, QRPacket::new(qr.id, RAuth(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RAuth(response)))
}
QAuthResume(request) => {
QRPacketData::QAuthResume(request) => {
let mut response = PacketAuthResumeResponse::new();
response.status = STATUS_ERROR;
@ -209,10 +234,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
}
bus.send(packet.from, QRPacket::new(qr.id, RAuthResume(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RAuthResume(response)))
}
QDeauth => {
QRPacketData::QAuthRevoke => {
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
match conn.auth {
Some(auth) => {
@ -223,10 +248,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
conn.auth = None;
}
true
Some(QRPacket::new(0, QRPacketData::None))
}
QSessionList(request) => {
QRPacketData::QSessionList(request) => {
use game::game::GameState;
println!("Request: Session List");
@ -277,7 +302,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
dusk_handle,
],
turn:0,
last_move:[0; 5],
last_move:[0; 3],
viewers:0,
player:is_player,
});
@ -290,10 +315,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
next_id = app.session_time.next(id);
}
bus.send(packet.from, QRPacket::new(qr.id, RSessionList(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RSessionList(response)))
}
QSessionCreate(_request) => {
QRPacketData::QSessionCreate(_request) => {
use crate::app::session::*;
println!("Request: Session Create");
@ -336,10 +361,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
response.token = token;
}
bus.send(packet.from, QRPacket::new(qr.id, RSessionCreate(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RSessionCreate(response)))
}
QSessionJoin(request) => {
QRPacketData::QSessionJoin(request) => {
println!("Request: Session Join");
let mut response = PacketSessionJoinResponse::new();
@ -390,12 +415,58 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
}
bus.send(packet.from, QRPacket::new(qr.id, RSessionJoin(response))).is_ok()
Some(QRPacket::new(qr.id, QRPacketData::RSessionJoin(response)))
}
_ => { true }
_ => { Some(QRPacket::new(0, QRPacketData::None)) }
}
}
None => false,
} { }
None => None,
} {
if match response.data { QRPacketData::None => false, _ => true } {
if let Some(conn) = app.connections.get_mut(response.id as usize) {
let mut socket = conn.stream.write().await;
match response.data {
QRPacketData::RRegister(response) => {
socket.send(Message::Binary(
encode_response(CODE_REGISTER, response.encode())
)).await.ok();
}
QRPacketData::RAuth(response) => {
socket.send(Message::Binary(
encode_response(CODE_AUTH, response.encode())
)).await.ok();
}
QRPacketData::RAuthResume(response) => {
socket.send(Message::Binary(
encode_response(CODE_AUTH_RESUME, response.encode())
)).await.ok();
}
QRPacketData::RSessionList(response) => {
socket.send(Message::Binary(
encode_response(CODE_SESSION_LIST, response.encode())
)).await.ok();
}
QRPacketData::RSessionCreate(response) => {
socket.send(Message::Binary(
encode_response(CODE_SESSION_CREATE, response.encode())
)).await.ok();
}
QRPacketData::RSessionJoin(response) => {
socket.send(Message::Binary(
encode_response(CODE_SESSION_JOIN, response.encode())
)).await.ok();
}
_ => { }
}
}
}
}
}

View File

@ -1,45 +1,42 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
use tokio_tungstenite::WebSocketStream;
use futures::{SinkExt, StreamExt};
use futures::StreamExt;
use crate::{
protocol,
util::pack::{pack_u16, unpack_u16},
util::pack::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<(),()>
pub async fn handle_ws(ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServiceArgs) -> Result<(),()>
// Handle websocket connection.
//
{
use tokio_tungstenite::tungstenite::protocol::Message;
use protocol::{QRPacket, QRPacketData::*, code::*, packet::*};
use protocol::{QRPacket, QRPacketData, code::*, packet::*};
let conn_id :u32;
let (sink, mut stream) = ws.split();
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 {
args.bus.send(bus_ds, QRPacket::new(0, QRPacketData::QConn(LocalPacketConnect {
bus_id:args.bus.id(),
stream:Arc::new(RwLock::new(sink)),
})))?;
match args.bus.receive_wait() {
Some(resp) => {
let qr = &resp.data;
match qr.data {
RConn => { conn_id = qr.id; }
QRPacketData::RConn => { conn_id = qr.id; }
_ => { return Err(()); }
}
}
@ -49,7 +46,7 @@ pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServi
// Decode client requests from websocket,
// pass requests to data system,
// and return responses to client.
while match ws.next().await {
while match stream.next().await {
Some(msg) => match msg {
Ok(msg) => match msg {
Message::Binary(data) => {
@ -62,142 +59,67 @@ pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServi
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,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QRegister(packet))
).ok();
}
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,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QAuth(packet))
).ok();
}
Err(_) => { }
}
CODE_AUTH_RESUME => match PacketAuthResume::decode(&data, &mut index) {
Ok(packet) => {
if args.bus.send(bus_ds, QRPacket::new(conn_id, QAuthResume(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,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QAuthResume(packet))
).ok();
}
Err(_) => { }
}
CODE_DEAUTH => {
args.bus.send(bus_ds, QRPacket::new(conn_id, QDeauth)).ok();
CODE_AUTH_REVOKE => {
args.bus.send(
bus_ds, QRPacket::new(conn_id,
QRPacketData::QAuthRevoke)
).ok();
}
CODE_SESSION_LIST => match PacketSessionList::decode(&data, &mut index) {
Ok(packet) => {
if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionList(packet))).is_ok() {
while match args.bus.receive_wait() {
Some(resp) => {
let qr = &resp.data;
match &qr.data {
RSessionList(resp) => {
ws.send(Message::Binary(
encode_response(code, resp.encode())
)).await.ok();
false
}
_ => true,
}
}
None => true,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QSessionList(packet))
).ok();
}
Err(_) => { println!("error: packet decode failed."); }
}
CODE_SESSION_CREATE => match PacketSessionCreate::decode(&data, &mut index) {
Ok(packet) => {
if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionCreate(packet))).is_ok() {
while match args.bus.receive_wait() {
Some(resp) => {
let qr = &resp.data;
match &qr.data {
RSessionCreate(resp) => {
ws.send(Message::Binary(
encode_response(code, resp.encode())
)).await.ok();
false
}
_ => true,
}
}
None => true,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QSessionCreate(packet))
).ok();
}
Err(_) => { println!("error: packet decode failed."); }
}
CODE_SESSION_JOIN => match PacketSessionJoin::decode(&data, &mut index) {
Ok(packet) => {
if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionJoin(packet))).is_ok() {
while match args.bus.receive_wait() {
Some(resp) => {
let qr = &resp.data;
match &qr.data {
RSessionJoin(resp) => {
ws.send(Message::Binary(
encode_response(code, resp.encode())
)).await.ok();
false
}
_ => true,
}
}
None => true,
} { }
}
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QSessionJoin(packet))
).ok();
}
Err(_) => { println!("error: packet decode failed."); }
}
@ -216,7 +138,6 @@ pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServi
None => false,
} { }
args.bus.send(bus_ds, QRPacket::new(conn_id, QDisconn)).ok();
ws.close(None).await.ok();
args.bus.send(bus_ds, QRPacket::new(conn_id, QRPacketData::QDisconn)).ok();
Ok(())
}

View File

@ -20,8 +20,8 @@ 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_AUTH_REVOKE :u16 = 0x0013;
pub const CODE_SESSION_LIST :u16 = 0x0020;
pub const CODE_SESSION_CREATE :u16 = 0x0021;
@ -29,4 +29,5 @@ pub const CODE_SESSION_JOIN :u16 = 0x0022;
pub const CODE_SESSION_RETIRE :u16 = 0x002E;
pub const CODE_SESSION_LEAVE :u16 = 0x002F;
pub const CODE_GAME_PLAY :u16 = 0x0030;
pub const CODE_GAME_STATE :u16 = 0x0030;
pub const CODE_GAME_PLAY :u16 = 0x0031;

View File

@ -5,7 +5,7 @@ pub mod packet; pub use packet::*;
#[derive(Clone)]
pub enum QRPacketData {
ROk,
None,
QConn(LocalPacketConnect),
RConn,
@ -21,7 +21,7 @@ pub enum QRPacketData {
QAuthResume(PacketAuthResume),
RAuthResume(PacketAuthResumeResponse),
QDeauth,
QAuthRevoke,
QSessionList(PacketSessionList),
RSessionList(PacketSessionListResponse),

View File

@ -1,4 +1,12 @@
#[derive(Clone, Copy)]
use std::sync::Arc;
use tokio::sync::RwLock;
use futures::stream::SplitSink;
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
#[derive(Clone)]
pub struct LocalPacketConnect {
pub bus_id:u32,
pub stream:Arc<RwLock<SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>>>,
}

View File

@ -65,7 +65,7 @@ pub struct PacketSessionListResponseRecord {
pub token:SessionToken,
pub handles:[String; 2],
pub turn:u16,
pub last_move:[u8; 5],
pub last_move:[u8; 3],
pub viewers:u32,
pub player:bool,
}

View File

@ -0,0 +1,244 @@
use std::{
fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path
};
use crate::{
app::session::Session,
app::user::User,
util::pack::{pack_u32, unpack_u32}
};
const HANDLE_BUCKET_MASK :u32 = 0xFF;
const HANDLE_BUCKET_SIZE :u32 = HANDLE_BUCKET_MASK + 1;
const DIR_DATA :&str = "data";
const DIR_HANDLE :&str = const_format::formatcp!("{}/h", DIR_DATA);
const DIR_SESSION :&str = const_format::formatcp!("{}/s", DIR_DATA);
const DIR_USER :&str = const_format::formatcp!("{}/u", DIR_DATA);
const INDEX_HANDLE :&str = const_format::formatcp!("{}/i.bin", DIR_HANDLE);
const INDEX_SESSION :&str = const_format::formatcp!("{}/i.bin", DIR_SESSION);
const INDEX_USER :&str = const_format::formatcp!("{}/i.bin", DIR_USER);
pub const FILE_SALT :&str = const_format::formatcp!("{}/x.bin", DIR_DATA);
pub struct FileSystem {
index_handle:File,
index_session:File,
index_user:File,
table_salt:File,
}
impl FileSystem {
pub fn init() -> Result<Self, std::io::Error>
{
//
// TEMPORARY: REMOVE AFTER TESTING
//
//fs::remove_dir_all(DIR_DATA).ok();
// Initialize filesystem if does not exist.
//
// Note: does not currently check for corruption.
//
if !Path::new(DIR_DATA).exists() {
fs::create_dir(DIR_DATA)?;
fs::create_dir(DIR_HANDLE)?;
fs::create_dir(DIR_SESSION)?;
fs::create_dir(DIR_USER)?;
fs::write(INDEX_HANDLE, pack_u32(0))?;
fs::write(INDEX_SESSION, pack_u32(0))?;
fs::write(INDEX_USER, pack_u32(0))?;
fs::write(FILE_SALT, pack_u32(0))?;
}
let index_handle = File::options()
.read(true)
.write(true)
.open(INDEX_HANDLE)?;
let index_session = File::options()
.read(true)
.write(true)
.open(INDEX_SESSION)?;
let index_user = File::options()
.read(true)
.write(true)
.open(INDEX_USER)?;
let table_salt = File::options()
.read(true)
.write(true)
.open(FILE_SALT)?;
Ok(Self {
index_handle,
index_session,
index_user,
table_salt,
})
}
pub fn session_store(&mut self, _session:&Session) -> Result<(),()>
{
Err(())
}
pub fn user_store(&mut self, _user:&User) -> Result<(),()>
{
Err(())
}
pub fn handle_store(&mut self, handle:&String, user_id:u32) -> Result<u32,()>
// Add a salt to store.
//
{
let size = self.handle_count()? as u32;
let data = handle.as_bytes();
let length = data.len();
let bucket_index = size & !HANDLE_BUCKET_MASK;
let record_index = size & HANDLE_BUCKET_MASK;
let record_offset = 12 * record_index as u64;
// Update size record
self.index_handle.seek(SeekFrom::Start(0)).map_err(|_| ())?;
self.index_handle.write(&pack_u32(size + 1)).map_err(|_| ())?;
// Create bucket file if not exists
let bucket_path = Path::new(DIR_HANDLE).join(format!("{:x}.bin", bucket_index));
if !bucket_path.exists() {
fs::write(bucket_path.clone(), vec![0u8; 12 * HANDLE_BUCKET_SIZE as usize]).map_err(|_| ())?;
}
// Open bucket file for record
if let Ok(mut file) = File::options().read(true).write(true).open(bucket_path) {
let offset = file.seek(SeekFrom::End(0)).map_err(|_| ())?;
// Write record header to table
let buffer_header = [
pack_u32(offset as u32),
pack_u32(length as u32),
pack_u32(user_id),
].concat();
file.seek(SeekFrom::Start(record_offset)).map_err(|_| ())?;
file.write(&buffer_header).map_err(|_| ())?;
// Write handle data to end of file
file.seek(SeekFrom::End(0)).map_err(|_| ())?;
file.write(data).map_err(|_| ())?;
Ok(size)
} else { Err(()) }
}
pub fn handle_fetch(&mut self, id:u32) -> Result<(String, u32),()>
// Retrieve a salt from store.
//
{
let size = self.salt_count()? as u32;
let mut buffer = Vec::new();
// Get location of handle by index
if id < size {
let bucket_index = id & !HANDLE_BUCKET_MASK;
let record_index = id & HANDLE_BUCKET_MASK;
let record_offset = 12 * record_index as u64;
let path = Path::new(DIR_HANDLE)
.join(format!("{:x}.bin", bucket_index));
// Read bucket file for record
if let Ok(mut file) = File::open(path) {
// Get record offset, length, and user association
let mut buffer_header = [0u8; 12];
file.seek(SeekFrom::Start(record_offset)).map_err(|_| ())?;
if file.read_exact(&mut buffer_header).is_ok() {
let offset = unpack_u32(&buffer_header, &mut 0);
let length = unpack_u32(&buffer_header, &mut 4);
let user_id = unpack_u32(&buffer_header, &mut 8);
if offset != 0 {
buffer.resize(length as usize, 0);
// Read handle data from offset
file.seek(SeekFrom::Start(offset as u64)).map_err(|_| ())?;
match file.read_exact(&mut buffer) {
Ok(_) => {
Ok((String::from_utf8(buffer).map_err(|_| ())?, user_id))
}
Err(e) => { println!("e: {}", e.to_string()); Err(()) }
}
} else { Err(()) }
} else { Err(()) }
} else { Err(()) }
} else { Err(()) }
}
pub fn handle_count(&mut self) -> Result<usize, ()>
// Get number of salts in store.
//
{
Self::get_header_size(&mut self.index_handle)
}
pub fn salt_store(&mut self, salt:[u8; 16]) -> Result<u32,()>
// Add a salt to store.
//
{
let size = self.salt_count()? as u32;
// Update size record
self.table_salt.seek(SeekFrom::Start(0)).map_err(|_| ())?;
self.table_salt.write(&pack_u32(size + 1)).map_err(|_| ())?;
// Write salt to store
self.table_salt.seek(SeekFrom::End(0)).map_err(|_| ())?;
self.table_salt.write(&salt).map_err(|_| ())?;
Ok(size)
}
pub fn salt_fetch(&mut self, id:u32) -> Result<[u8; 16],()>
// Retrieve a salt from store.
//
{
let size = self.salt_count()? as u32;
let mut buffer = [0u8; 16];
if id < size {
let offset = 4 + (16 * id as u64);
self.table_salt.seek(SeekFrom::Start(offset)).map_err(|_| ())?;
if self.table_salt.read_exact(&mut buffer).is_ok() {
Ok(buffer)
} else { Err(()) }
} else { Err(()) }
}
pub fn salt_count(&mut self) -> Result<usize, ()>
// Get number of salts in store.
//
{
Self::get_header_size(&mut self.table_salt)
}
fn get_header_size(file:&mut File) -> Result<usize, ()>
{
let mut size_buffer = [0u8; 4];
file.seek(SeekFrom::Start(0)).map_err(|_| ())?;
if file.read_exact(&mut size_buffer).is_ok() {
Ok(unpack_u32(&size_buffer, &mut 0) as usize)
} else { Err(()) }
}
}

View File

@ -2,3 +2,4 @@
pub mod net;
pub mod cache;
pub mod filesystem;

View File

@ -3,7 +3,7 @@ pub fn pack_u8(value:u8) -> Vec<u8>
vec![value]
}
pub fn unpack_u8(data:&Vec<u8>, index:&mut usize) -> u8
pub fn unpack_u8(data:&[u8], index:&mut usize) -> u8
{
let mut result :u8 = 0;
if *index < data.len() {
@ -18,7 +18,7 @@ pub fn pack_u16(value:u16) -> Vec<u8>
vec![(value >> 8) as u8, (value & 0xFF) as u8]
}
pub fn unpack_u16(data:&Vec<u8>, index:&mut usize) -> u16
pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16
{
let mut result :u16 = 0;
if *index < data.len() {
@ -42,7 +42,7 @@ pub fn pack_u32(value:u32) -> Vec<u8>
]
}
pub fn unpack_u32(data:&Vec<u8>, index:&mut usize) -> u32
pub fn unpack_u32(data:&[u8], index:&mut usize) -> u32
{
let mut result :u32 = 0;
if *index < data.len() {

View File

@ -11,13 +11,13 @@ let CONTEXT = {
};
const Status = {
Ok: 0,
Error: 1,
NotImplement: 2,
Ok: 0x0000,
Error: 0x0001,
NotImplemented: 0x0002,
BadHandle: 1,
BadSecret: 2,
BadCode: 3,
BadHandle: 0x0010,
BadSecret: 0x0011,
BadCode: 0x0012,
};
const OpCode = {
@ -29,8 +29,11 @@ const OpCode = {
SessionList :0x0020,
SessionCreate :0x0021,
SessionJoin :0x0022,
SessionRetire :0x002E,
SessionLeave :0x002F,
GameState :0x0030,
GamePlay :0x0031,
};
const GameState = {

View File

@ -1,157 +1,212 @@
class GamePieceMove {
const GAME_CONST = {
PLAYER_DAWN: 0,
PLAYER_DUSK: 1,
}
class GamePiece {
constructor(name, assets, moves, promote_moves) {
this.name = name;
this.assets = assets;
this.moves = moves;
this.pmoves = promote_moves;
}
}
let GAME_DATA = {
board: {
tiles: [ ],
},
pieces: [
new PieceDef("Militia「兵」", "♟︎", // ♟︎士
["asset/militia_dusk.svg", "asset/militia_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(5),
new Move()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
),
new PieceDef("Knight「騎」", "♞", // ♞馬
["asset/knight_dusk.svg", "asset/knight_dawn.svg"],
new Move()
.add(3)
.add(6)
.add(11)
.add(13)
.add(17),
new Move()
.add(3)
.add(6)
.add(7)
.add(10)
.add(11)
.add(13)
.add(14)
.add(16)
.add(17)
),
new PieceDef("Lance「槍」", "♛", // ♛槍
["asset/lance_dusk.svg", "asset/lance_dawn.svg"],
new Move()
.add(0, true)
.add(1)
.add(5),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
),
new PieceDef("Tower「楼」", "♖", // ♖高
["asset/tower_dusk.svg", "asset/tower_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(3)
.add(5)
.add(6)
.add(11),
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(6)
.add(8)
.add(9)
.add(11)
),
new PieceDef("Castle「城」", "♜", // ♜城
["asset/castle_dusk.svg", "asset/castle_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
.add(7)
.add(10),
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7, true)
.add(10, true)
),
new PieceDef("Dragon「竜」", "♝", // ♝竜
["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"],
new Move()
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true)
),
new PieceDef("King「王」", "♚", // ♚王
["asset/king_dusk.svg", "asset/king_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7)
.add(10)
),
],
SOURCE_BOARD: 0,
SOURCE_POOL: 1,
};
const GAME_CLASS = {
Board: class {
constructor() {
this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME_CLASS.Tile()); }
this.dawn = new GAME_CLASS.Player();
}
},
Player: class {
constructor() {
this.handle = "";
this.pool = new GAME_CLASS.Pool();
}
},
Pool: class {
constructor() {
this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(0); }
}
},
Tile: class {
constructor() {
this.piece = 0;
}
},
Move: class {
constructor(source, from, to) {
this.source = source;
this.from = from;
this.to = to;
}
},
GamePiece: class {
constructor(name, assets, moves, promote_moves) {
this.name = name;
this.assets = assets;
this.moves = moves;
this.pmoves = promote_moves;
}
},
Piece: class {
constructor(piece, player) {
this.piece = piece;
this.player = player;
this.promoted = false;
}
},
Game: class {
constructor() {
this.board = new GAME_CLASS.Board();
this.pieces = [
new GAME_CLASS.GamePiece("Militia",
["asset/militia_dusk.svg", "asset/militia_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(5),
new Move()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
),
new GAME_CLASS.GamePiece("Knight",
["asset/knight_dusk.svg", "asset/knight_dawn.svg"],
new Move()
.add(3)
.add(6)
.add(11)
.add(13)
.add(17),
new Move()
.add(3)
.add(6)
.add(7)
.add(10)
.add(11)
.add(13)
.add(14)
.add(16)
.add(17)
),
new GAME_CLASS.GamePiece("Lance",
["asset/lance_dusk.svg", "asset/lance_dawn.svg"],
new Move()
.add(0, true)
.add(1)
.add(5),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
),
new GAME_CLASS.GamePiece("Tower",
["asset/tower_dusk.svg", "asset/tower_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(3)
.add(5)
.add(6)
.add(11),
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(6)
.add(8)
.add(9)
.add(11)
),
new GAME_CLASS.GamePiece("Castle",
["asset/castle_dusk.svg", "asset/castle_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
.add(7)
.add(10),
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7, true)
.add(10, true)
),
new GAME_CLASS.GamePiece("Dragon",
["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"],
new Move()
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true)
),
new GAME_CLASS.GamePiece("Omen",
["asset/king_dusk.svg", "asset/king_dawn.svg"],
new Move()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7)
.add(10)
),
];
}
}
};
let GAME_DATA = null;
const GAME = {
init() {
GAME_DATA.board.tiles
GAME_DATA = new GAME_CLASS.Game();
},
process(move) {
},
validate(move) {
},
};

View File

@ -53,7 +53,11 @@ const INTERFACE = {
},
init() {
GAME.init();
INTERFACE_DATA.canvas = document.getElementById("game");
let canvas = INTERFACE_DATA.canvas;
if(canvas !== undefined) {
INTERFACE_DATA.context = canvas.getContext("2d");
@ -68,4 +72,8 @@ const INTERFACE = {
INTERFACE_DATA.canvas = null;
INTERFACE_DATA.context = null;
},
message(data) {
},
};

View File

@ -1,4 +1,6 @@
document.addEventListener("DOMContentLoaded", () => {
SCENE = SCENES.Offline;
LOAD(SCENES.Init);
document.addEventListener("beforeunload", UNLOAD);
});

View File

@ -1,7 +1,7 @@
const SCENES = {
Init:{
load() {
LOAD_OFFLINE();
LOAD_STACK(SCENES.Offline);
CONTEXT.Scene = SCENES.Online;
RECONNECT();
return true;
@ -210,11 +210,13 @@ const SCENES = {
MESSAGE_SESSION_LIST(0, 0, false, false);
},
message(code, data) {
let table = document.getElementById("content");
UI.clear(table);
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.session_table(data.records));
if(data !== null) {
table.appendChild(UI.session_table(data.records));
}
}
}
},
@ -250,6 +252,16 @@ const SCENES = {
refresh() {
},
message(code, data) {
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.session_table(data.records));
}
}
}
},
Join:{
@ -283,6 +295,16 @@ const SCENES = {
refresh() {
},
message(code, data) {
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.session_table(data.records));
}
}
}
},
Live:{
@ -315,6 +337,16 @@ const SCENES = {
refresh() {
},
message(code, data) {
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.session_table(data.records));
}
}
}
},
History:{
@ -342,12 +374,25 @@ const SCENES = {
refresh() {
},
message(code, data) {
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.session_table(data.records));
}
}
}
},
Guide:{
load() {
UI.mainmenu();
UI.mainnav([], []);
return true;
},
refresh() {
@ -359,6 +404,9 @@ const SCENES = {
load() {
UI.mainmenu();
UI.mainnav([], []);
return true;
},
refresh() {
@ -377,7 +425,6 @@ const SCENES = {
let canvas = document.createElement("canvas");
canvas.setAttribute("id", "game");
MAIN.appendChild(canvas);
INTERFACE.init();
@ -385,29 +432,33 @@ const SCENES = {
return true;
},
unload() {
MESSAGE_COMPOSE([
PACK.u16(OpCode.SessionLeave),
]);
},
refresh() {
},
message() {
message(code, data) {
if(code == OpCode.GameState || code == OpCode.GamePlay) {
INTERFACE.message(code, data);
}
},
},
};
function LOAD(scene) {
if(SCENE.unload !== undefined) { SCENE.unload(); }
UNLOAD();
UI.rebuild();
SCENE = scene;
CONTEXT.Scene = SCENE;
if(!SCENE.load()) { LOAD(SCENES.Online); }
}
function LOAD_OFFLINE() {
if(SCENE.unload !== undefined) { SCENE.unload(); }
while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); }
function UNLOAD() {
if(SCENE !== null && SCENE.unload !== undefined) { SCENE.unload(); }
}
function LOAD_STACK(scene) {
UNLOAD();
UI.rebuild();
SCENE = SCENES.Offline;
SCENE = scene;
if(!SCENE.load()) { LOAD(SCENES.Online); }
}

View File

@ -5,7 +5,7 @@ function RECONNECT() {
SOCKET.binaryType = "arraybuffer";
SOCKET.addEventListener("error", (event) => {
SOCKET = null;
LOAD_OFFLINE()
LOAD_STACK(SCENES.Offline);
});
SOCKET.addEventListener("open", (event) => {
if(SOCKET.readyState === WebSocket.OPEN) {

View File

@ -84,8 +84,8 @@ const UI = {
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) }));
left.appendChild(UI.button("Register", () => { LOAD_STACK(SCENES.Register) }));
left.appendChild(UI.button("Log In", () => { LOAD_STACK(SCENES.Authenticate) }));
}
for(child of left_children) { left.appendChild(child); }