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 {
if let Ok(mut filesystem) = FileSystem::init() {
// 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);
}
// 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);
}
Ok(Self {
filesystem:filesystem,
connections:Pool::new(),
users:Pool::new(),
user_next:0,
user_id:Sparse::new(),
user_handle:Trie::new(),
salts:Vec::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 => {
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
}
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 => 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();
}
_ => { }
}
}
}
}
None => false,
} { }
}

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,23 +1,68 @@
class GamePieceMove {
const GAME_CONST = {
PLAYER_DAWN: 0,
PLAYER_DUSK: 1,
}
SOURCE_BOARD: 0,
SOURCE_POOL: 1,
};
class GamePiece {
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;
}
}
let GAME_DATA = {
board: {
tiles: [ ],
},
pieces: [
new PieceDef("Militia「兵」", "♟︎", // ♟︎士
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)
@ -31,7 +76,7 @@ let GAME_DATA = {
.add(4)
.add(5)
),
new PieceDef("Knight「騎」", "♞", // ♞馬
new GAME_CLASS.GamePiece("Knight",
["asset/knight_dusk.svg", "asset/knight_dawn.svg"],
new Move()
.add(3)
@ -51,7 +96,7 @@ let GAME_DATA = {
.add(16)
.add(17)
),
new PieceDef("Lance「槍」", "♛", // ♛槍
new GAME_CLASS.GamePiece("Lance",
["asset/lance_dusk.svg", "asset/lance_dawn.svg"],
new Move()
.add(0, true)
@ -66,7 +111,7 @@ let GAME_DATA = {
.add(4, true)
.add(5, true)
),
new PieceDef("Tower「楼」", "♖", // ♖高
new GAME_CLASS.GamePiece("Tower",
["asset/tower_dusk.svg", "asset/tower_dawn.svg"],
new Move()
.add(0)
@ -88,7 +133,7 @@ let GAME_DATA = {
.add(9)
.add(11)
),
new PieceDef("Castle「城」", "♜", // ♜城
new GAME_CLASS.GamePiece("Castle",
["asset/castle_dusk.svg", "asset/castle_dawn.svg"],
new Move()
.add(0)
@ -109,7 +154,7 @@ let GAME_DATA = {
.add(7, true)
.add(10, true)
),
new PieceDef("Dragon「竜」", "♝", // ♝竜
new GAME_CLASS.GamePiece("Dragon",
["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"],
new Move()
.add(6, true)
@ -133,7 +178,7 @@ let GAME_DATA = {
.add(10, true)
.add(11, true)
),
new PieceDef("King「王」", "♚", // ♚王
new GAME_CLASS.GamePiece("Omen",
["asset/king_dusk.svg", "asset/king_dawn.svg"],
new Move()
.add(0)
@ -145,13 +190,23 @@ let GAME_DATA = {
.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,6 +210,7 @@ const SCENES = {
MESSAGE_SESSION_LIST(0, 0, false, false);
},
message(code, data) {
if(code == OpCode.SessionList) {
let table = document.getElementById("content");
UI.clear(table);
@ -217,6 +218,7 @@ const SCENES = {
table.appendChild(UI.session_table(data.records));
}
}
}
},
Continue:{
@ -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); }