diff --git a/server/src/app/invitation.rs b/server/src/app/invitation.rs new file mode 100644 index 0000000..2f1c0b6 --- /dev/null +++ b/server/src/app/invitation.rs @@ -0,0 +1,7 @@ +pub type InviteToken = [u8; 12]; + +#[derive(Clone, Copy)] +pub struct Invitation { + pub token:InviteToken, + pub parent:Option, +} diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index a55251d..3bb0412 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -16,6 +16,7 @@ pub mod connection; use connection::Connection; pub mod user; use user::User; pub mod authentication; use authentication::Authentication; pub mod session; use session::{Session, SessionToken}; +pub mod invitation; pub use invitation::{Invitation, InviteToken}; pub mod context; pub struct App { @@ -32,6 +33,9 @@ pub struct App { pub auths:Trie, pub sessions:Trie, + pub invite_tokens:Trie, + pub invites:Pool, + pub contests:Vec, pub session_time:Chain, @@ -124,6 +128,9 @@ impl App { auths:Trie::new(), sessions, + invite_tokens:Trie::new(), + invites:Pool::new(), + contests, session_time, @@ -230,6 +237,18 @@ impl App { )).await.ok(); } + QRPacketData::RInviteList(response) => { + socket.send(Message::Binary( + encode_response(CODE_INVITE_LIST, response.encode()) + )).await.ok(); + } + + QRPacketData::RInviteAcquire(response) => { + socket.send(Message::Binary( + encode_response(CODE_INVITE_ACQUIRE, response.encode()) + )).await.ok(); + } + QRPacketData::TestResult(response) => { socket.send(Message::Binary( encode_response(CODE_TEST_RESULT, response.encode()) diff --git a/server/src/config.rs b/server/src/config.rs index de32930..fb4156c 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -1,2 +1,2 @@ pub const VERSION :&str = env!("GIT_HASH"); -pub const REGISTER_CODE :&str = "mountain"; +//pub const REGISTER_CODE :&str = "mountain"; diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index 9f69d50..695dac4 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -142,7 +142,8 @@ pub async fn thread_system(mut app:App, bus:Bus) app.log.log("Request: Register"); let mut is_valid = true; - if request.code != crate::config::REGISTER_CODE.as_bytes() { response.status = STATUS_BAD_CODE; is_valid = false; } + let invite = app.invite_tokens.get(&request.code).cloned(); + if is_valid && invite.is_none() { response.status = STATUS_BAD_CODE; is_valid = false; } if is_valid && request.handle.len() == 0 && request.handle.chars().count() <= 24 { response.status = STATUS_BAD_HANDLE; is_valid = false; } if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; } @@ -207,6 +208,14 @@ pub async fn thread_system(mut app:App, bus:Bus) if let Some(conn) = app.connections.get_mut(qr.id as usize) { conn.auth = Some(response.token); } + + // Remove invitation from store. + if let Some(invite) = invite { + if let Some(invite) = app.invites.get(invite as usize).cloned() { + app.invite_tokens.unset(&invite.token); + } + app.invites.remove(invite as usize).ok(); + } } } Err(_) => { app.log.log("error: failed to generate salt.") } @@ -1000,6 +1009,95 @@ pub async fn thread_system(mut app:App, bus:Bus) Some(QRPacket::new(qr.id, QRPacketData::RUserList(response))) } + // InviteAcquire + QRPacketData::QInviteAcquire => { + use crate::app::{Invitation, InviteToken}; + app.log.log("Request: Invite Acquire"); + + let mut response = PacketInviteAcquireResponse::new(); + + if let Some(user_id) = user_id { + + // Count number of invites acquired by user. + let mut count_invites = 0; + let invites = app.invites.list(); + for id in invites { + if let Some(invite) = app.invites.get(id) { + if let Some(parent) = invite.parent { + if parent == user_id { + count_invites += 1; + } + } + } + } + + // Create new invite if below threshold. + const MAX_INVITES :usize = 10; + if count_invites < MAX_INVITES { + let mut token = InviteToken::default(); + loop { + rng.fill(&mut token).ok(); + for byte in &mut token { + *byte = *byte % 62; + *byte += if *byte < 10 { + '0' as u8 + } else if *byte < 36 { + 'a' as u8 - 10 + } else { + 'A' as u8 - 36 + }; + } + + if app.invite_tokens.get(&token).is_none() { break } + } + + let invite = Invitation { + token, + parent:Some(user_id), + }; + + let id = app.invites.add(invite); + app.invite_tokens.set(&token, id as u32); + + response.status = STATUS_OK; + } else { + response.status = STATUS_ERROR; + } + } else { + response.status = STATUS_NOAUTH; + } + + Some(QRPacket::new(qr.id, QRPacketData::RInviteAcquire(response))) + } + + // InviteList + QRPacketData::QInviteList => { + app.log.log("Request: Invite List"); + + let mut response = PacketInviteListResponse::new(); + + if let Some(user_id) = user_id { + + // Get user's invitations. + let invites = app.invites.list(); + for id in invites { + if let Some(invite) = app.invites.get(id) { + if let Some(parent) = invite.parent { + if parent == user_id { + response.records.push(invite.clone()); + } + } + } + } + + response.status = STATUS_OK; + } else { + response.status = STATUS_NOAUTH; + } + + Some(QRPacket::new(qr.id, QRPacketData::RInviteList(response))) + } + _ => { Some(QRPacket::new(0, QRPacketData::None)) } } { Some(response) => { diff --git a/server/src/manager/ws.rs b/server/src/manager/ws.rs index 012b53d..afcd178 100644 --- a/server/src/manager/ws.rs +++ b/server/src/manager/ws.rs @@ -198,6 +198,20 @@ pub async fn handle_ws(ws:WebSocketStream>, args:HttpServiceAr ).ok(); } + CODE_INVITE_LIST => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QInviteList) + ).ok(); + } + + CODE_INVITE_ACQUIRE => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QInviteAcquire) + ).ok(); + } + _ => { } } true diff --git a/server/src/protocol/code.rs b/server/src/protocol/code.rs index d4bd554..f67a2a9 100644 --- a/server/src/protocol/code.rs +++ b/server/src/protocol/code.rs @@ -1,7 +1,6 @@ /* ** Status Codes */ - pub const STATUS_OK :u16 = 0x0000; pub const STATUS_ERROR :u16 = 0x0001; @@ -21,7 +20,6 @@ pub const STATUS_NOT_IMPL :u16 = 0x00FF; /* ** Operation Codes */ - pub const CODE_HELLO :u16 = 0x0001; pub const CODE_REGISTER :u16 = 0x0010; @@ -46,15 +44,16 @@ pub const CODE_CHALLENGE_LIST :u16 = 0x0062; pub const CODE_USER_LIST :u16 = 0x0100; pub const CODE_USER_INFO :u16 = 0x0101; -//pub const CODE_USER_AWAIT_GET :u16 = 0x0110; -//pub const CODE_USER_AWAIT_SET :u16 = 0x0111; + +pub const CODE_INVITE_LIST :u16 = 0x0180; +pub const CODE_INVITE_ACQUIRE :u16 = 0x0181; pub const CODE_TEST_RESULT :u16 = 0xFFFF; + /* ** Game Messages */ - pub const GMSG_ERROR :u8 = 0x00; pub const GMSG_PLAY_MOVE :u8 = 0x01; pub const GMSG_PLAY_DROP :u8 = 0x02; diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index d7ac356..70d126b 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -55,6 +55,12 @@ pub enum QRPacketData { QChallengeList, RChallengeList(PacketChallengeListResponse), + QInviteAcquire, + RInviteAcquire(PacketInviteAcquireResponse), + + QInviteList, + RInviteList(PacketInviteListResponse), + TestResult(PacketTestResult), } diff --git a/server/src/protocol/packet/invite_acquire.rs b/server/src/protocol/packet/invite_acquire.rs new file mode 100644 index 0000000..7127902 --- /dev/null +++ b/server/src/protocol/packet/invite_acquire.rs @@ -0,0 +1,32 @@ +use crate::{ + util::pack::pack_u16, + app::InviteToken, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketInviteAcquireResponse { + pub status:u16, + pub token:InviteToken, +} +impl PacketInviteAcquireResponse { + pub fn new() -> Self + { + Self { + status:0, + token:InviteToken::default(), + } + } +} +impl Packet for PacketInviteAcquireResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + self.token.to_vec(), + ].concat() + } +} diff --git a/server/src/protocol/packet/invite_list.rs b/server/src/protocol/packet/invite_list.rs new file mode 100644 index 0000000..ddc6526 --- /dev/null +++ b/server/src/protocol/packet/invite_list.rs @@ -0,0 +1,40 @@ +use crate::{ + util::pack::pack_u16, + app::Invitation, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketInviteListResponse { + pub status:u16, + pub records:Vec, +} +impl PacketInviteListResponse { + pub fn new() -> Self + { + Self { + status:0, + records:Vec::new(), + } + } +} +impl Packet for PacketInviteListResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + let mut output = [ + pack_u16(self.status), + pack_u16(self.records.len() as u16), + ].concat(); + + for record in &self.records { + output.append(&mut [ + record.token.to_vec(), + ].concat()); + } + + output + } +} diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index e27b6a0..fdfe025 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -8,13 +8,11 @@ mod resume; pub use resume::*; mod summary; pub use summary::*; mod session_list; pub use session_list::*; -//mod session_create; pub use session_create::*; mod session_view; pub use session_view::*; mod session_retire; pub use session_retire::*; mod game_state; pub use game_state::*; mod game_message; pub use game_message::*; -//mod game_history; pub use game_history::*; mod challenge; pub use challenge::*; mod challenge_answer; pub use challenge_answer::*; @@ -23,6 +21,9 @@ mod challenge_list; pub use challenge_list::*; mod user_list; pub use user_list::*; mod user_info; pub use user_info::*; +mod invite_acquire; pub use invite_acquire::*; +mod invite_list; pub use invite_list::*; + mod test_result; pub use test_result::*; mod prelude { diff --git a/server/src/protocol/packet/session_create.rs b/server/src/protocol/packet/session_create.rs deleted file mode 100644 index 45e667d..0000000 --- a/server/src/protocol/packet/session_create.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{ - app::session::SessionToken, - util::pack::pack_u16, -}; - -use super::Packet; - -#[derive(Clone)] -pub struct PacketSessionCreate { - -} -impl PacketSessionCreate { - pub fn new() -> Self - { - Self { } - } -} -impl Packet for PacketSessionCreate { - type Data = Self; - - fn decode(_data:&Vec, _index:&mut usize) -> Result - { - Ok(Self::new()) - } -} - - -#[derive(Clone)] -pub struct PacketSessionCreateResponse { - pub status:u16, - pub token:SessionToken, -} -impl PacketSessionCreateResponse { - pub fn new() -> Self - { - Self { - status:0, - token:SessionToken::default(), - } - } -} -impl Packet for PacketSessionCreateResponse { - type Data = Self; - - fn encode(&self) -> Vec - { - [ - pack_u16(self.status), - self.token.to_vec(), - ].concat() - } -} diff --git a/server/src/system/log/mod.rs b/server/src/system/log/mod.rs index 2ca4114..de8e1ef 100644 --- a/server/src/system/log/mod.rs +++ b/server/src/system/log/mod.rs @@ -1,4 +1,4 @@ -const DEBUG_PRINT :bool = false; +const DEBUG_PRINT :bool = true; pub struct Log { diff --git a/www/css/util.css b/www/css/util.css index b6cbe6a..60042ed 100644 --- a/www/css/util.css +++ b/www/css/util.css @@ -7,3 +7,5 @@ button.warn { background-color:#471414; color:#e0e0e0; } + +span.monospace {font-family:monospace;} diff --git a/www/js/const.js b/www/js/const.js index 9f84545..de92466 100644 --- a/www/js/const.js +++ b/www/js/const.js @@ -56,6 +56,9 @@ const OpCode = { UserList :0x0100, UserInfo :0x0101, + InviteList :0x0180, + InviteAcquire :0x0181, + AccountManage :0x1000, AccountCommit :0x1001, diff --git a/www/js/scene.js b/www/js/scene.js index 3c5f69c..f4639f2 100644 --- a/www/js/scene.js +++ b/www/js/scene.js @@ -43,13 +43,13 @@ const SCENES = { let container = document.createElement("section"); let form = document.createElement("form"); - let tb_handle = UI.textbox("handle", ""); + let tb_handle = UI.textbox("handle", "user"); tb_handle.setAttribute("maxlength", 24); form.appendChild(UI.table(null, [ [ UI.label(LANG("handle"), "handle"), tb_handle ], [ UI.label(LANG("secret"), "secret"), UI.password("secret") ], - [ UI.label(LANG("invitation"), "code"), UI.password("code") ], + [ UI.label(LANG("invitation"), "code"), UI.textbox("code", "####-####-####") ], ])); let button = UI.submit(LANG("register")); @@ -68,13 +68,16 @@ const SCENES = { code.removeAttribute("class"); event.target.removeAttribute("class"); - if(handle.value.length > 0 && handle.value.length < 24 && secret.value.length > 0 && code.value.length > 0) { + let invitation = code.value; + invitation = invitation.replace(/\-/g, ''); + + if(handle.value.length > 0 && handle.value.length < 24 && secret.value.length > 0 && invitation.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); + let enc_code = enc.encode(invitation); MESSAGE_COMPOSE([ PACK.u16(OpCode.Register), @@ -147,7 +150,7 @@ const SCENES = { let container = document.createElement("section"); let form = document.createElement("form"); - let tb_handle = UI.textbox("handle", ""); + let tb_handle = UI.textbox("handle", "user"); tb_handle.setAttribute("maxlength", 24); form.appendChild(UI.table(null, [ @@ -677,7 +680,13 @@ const SCENES = { UI.mainmenu_account(null, "invitations"); // Left Buttons - let buttons_left = [ ]; + let buttons_left = [ + UI.button("Acquire", () => { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.InviteAcquire), + ]) + }), + ]; // Right Buttons let buttons_right = [ ]; @@ -685,10 +694,36 @@ const SCENES = { UI.mainnav(buttons_left, buttons_right); // Main Content + let table = document.createElement("table"); + table.setAttribute("id", "content"); + table.setAttribute("class", "list session"); + UI.maincontent(table); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.InviteList), + ]); history.pushState(null, "Dzura - About", "/u/" + CONTEXT.Auth.handle); return true; } + message(code, data) { + switch(code) { + case OpCode.InviteAcquire: { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.InviteList), + ]); + } break; + + case OpCode.InviteList: { + let table = document.getElementById("content"); + UI.clear(table); + + if(data !== null) { + table.appendChild(UI.invite_table(data.records)); + } + } break; + } + } }, GameLoad:class{ diff --git a/www/js/system.js b/www/js/system.js index 0b5c3c4..4bdea89 100644 --- a/www/js/system.js +++ b/www/js/system.js @@ -490,6 +490,62 @@ function MESSAGE(event) { } } break; + case OpCode.InviteList: { + console.log("RECV InviteList"); + + data = { + status:0, + records:[ ], + }; + + // Status + result = UNPACK.u16(bytes, index); + index = result.index; + data.status = result.data; + + // Records + result = UNPACK.u16(bytes, index); + index = result.index; + let length = result.data; + + for(let i = 0; i < length; ++i) { + let token = ""; + + for(let c = 0; c < 4; ++c) { + token += String.fromCharCode(bytes[index++]); + } token += "-"; + + for(let c = 0; c < 4; ++c) { + token += String.fromCharCode(bytes[index++]); + } token += "-"; + + for(let c = 0; c < 4; ++c) { + token += String.fromCharCode(bytes[index++]); + } + + data.records.push(token); + } + } break; + + case OpCode.InviteAcquire: { + console.log("RECV InviteAcquire"); + + data = { + status:0, + token:"", + }; + + // Status + result = UNPACK.u16(bytes, index); + index = result.index; + data.status = result.data; + + // Token + for(let c = 0; c < 12; ++c) { + data.token += String.fromCharCode(bytes[index++]); + } + } break; + case OpCode.TestResult: { result = UNPACK.u8(bytes, index); index = result.index; diff --git a/www/js/ui.js b/www/js/ui.js index f180fff..01859ff 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -55,6 +55,7 @@ const UI = { password(id) { let input = document.createElement("input"); input.setAttribute("type", "password"); + input.setAttribute("placeholder", "••••••••••••"); if(id !== null) { input.setAttribute("id", id); } return input; }, @@ -411,6 +412,26 @@ const UI = { return tbody; }, + invite_table(records) { + let rows = [ ]; + + for(let r = 0; r < records.length; ++r) { + let record = records[r]; + + rows.push([ + UI.span([UI.text(record)], "monospace"), + UI.text(""), + ]); + } + + let tbody = UI.table_content( + [ LANG("invitation"), "" ], + rows, + ); + + return tbody; + }, + page_indicator(first, last, total) { let ind = document.getElementById("indicator-page"); UI.clear(ind);