From 24bb65bf7b572651f754617ab6d19e89c1dfd9da Mon Sep 17 00:00:00 2001 From: shadow cat Date: Wed, 3 Dec 2025 22:51:57 -0500 Subject: [PATCH] accounts are now real --- Cargo.lock | 143 ++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + iris | 2 +- src/bin/client/main.rs | 105 +++++++++++++++---------- src/bin/client/net.rs | 34 +++----- src/bin/client/rsc.rs | 6 +- src/bin/client/state.rs | 51 ++++++++++++ src/bin/client/ui/login.rs | 154 ++++++++++++++++++++++++++----------- src/bin/client/ui/main.rs | 39 ++++++---- src/bin/client/ui/misc.rs | 8 +- src/bin/client/ui/mod.rs | 10 +-- src/bin/server/db.rs | 134 +++++++++++++++++++++++++++++--- src/bin/server/main.rs | 115 ++++++++++++++++++++++++--- src/bin/server/net.rs | 3 +- src/net/mod.rs | 37 ++++++++- 15 files changed, 679 insertions(+), 163 deletions(-) create mode 100644 src/bin/client/state.rs diff --git a/Cargo.lock b/Cargo.lock index 7ce2fec..6513635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bincode" version = "2.0.1" @@ -312,6 +318,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -437,6 +452,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.53" @@ -622,6 +647,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -662,6 +696,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -677,6 +721,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "directories-next" version = "2.0.0" @@ -966,6 +1021,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -1157,6 +1222,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "image" version = "0.25.8" @@ -1207,6 +1281,15 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1981,6 +2064,7 @@ dependencies = [ "quinn", "rcgen", "ron", + "scrypt", "sled", "tokio", "tracing", @@ -2074,12 +2158,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.6" @@ -2722,6 +2827,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2752,6 +2866,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -2833,6 +2959,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3329,6 +3466,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-bidi" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 7615981..5393a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ zstd = "0.13.3" ron = "0.12.0" sled = "0.34.7" clap = { version = "4.5.53", features = ["derive"] } +scrypt = "0.11.0" [[bin]] name = "openworm-client" diff --git a/iris b/iris index d6a9711..84c460a 160000 --- a/iris +++ b/iris @@ -1 +1 @@ -Subproject commit d6a9711ceb68ec1a11d9c9577381992d2beffd0e +Subproject commit 84c460a91f7d98d565f4b71a41420a3e4a8c8c31 diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs index 7d39dd3..ebe55ad 100644 --- a/src/bin/client/main.rs +++ b/src/bin/client/main.rs @@ -2,8 +2,9 @@ use crate::{ app::App, - net::{NetCtrlMsg, NetHandle, NetSender, NetState}, + net::{NetHandle, NetSender}, rsc::{CLIENT_DATA, ClientData}, + state::{ClientState, LoggedIn, Login}, ui::*, }; pub use app::AppHandle; @@ -11,7 +12,7 @@ use arboard::Clipboard; use input::Input; use iris::prelude::*; use openworm::{ - net::{ClientMsg, NetMsg, ServerMsg, install_crypto_provider}, + net::{ClientMsg, ServerMsg, install_crypto_provider}, rsc::DataDir, }; use render::Renderer; @@ -28,6 +29,7 @@ mod input; mod net; mod render; mod rsc; +mod state; mod ui; fn main() { @@ -35,28 +37,26 @@ fn main() { App::run(); } -pub enum ClientEvent { - Connect { send: NetSender, username: String }, - ServerMsg(ServerMsg), - Err(String), -} - pub struct Client { renderer: Renderer, input: Input, ui: Ui, focus: Option>, - channel: Option>, - username: String, clipboard: Clipboard, dir: DataDir, data: ClientData, handle: AppHandle, - error: Option>, - net: NetState, - msgs: Vec, + state: ClientState, ime: usize, last_click: Instant, + main_ui: WidgetId, + notif: WidgetId, +} + +pub enum ClientEvent { + Connect { send: NetSender }, + ServerMsg(ServerMsg), + Err(String), } impl Client { @@ -70,62 +70,90 @@ impl Client { let dir = DataDir::default(); let handle = AppHandle { proxy, window }; + let mut ui = Ui::new(); + let notif = WidgetPtr::default().add(&mut ui); + let main_ui = WidgetPtr::default().add(&mut ui); + ( + notif.clone().pad(Padding::top(10)).align(Align::TOP_CENTER), + main_ui.clone(), + ) + .stack() + .set_root(&mut ui); + let mut s = Self { handle, renderer, input: Input::default(), - ui: Ui::new(), + ui, data: dir.load(CLIENT_DATA), + state: Default::default(), dir, - channel: None, focus: None, - net: Default::default(), - username: "".to_string(), clipboard: Clipboard::new().unwrap(), - error: None, ime: 0, last_click: Instant::now(), - msgs: Vec::new(), + main_ui: main_ui.clone(), + notif, }; - ui::init(&mut s); + connect_screen(&mut s).set_ptr(&main_ui, &mut s.ui); s } pub fn event(&mut self, event: ClientEvent, _: &ActiveEventLoop) { match event { - ClientEvent::Connect { send, username } => { - self.username = username; - send.send(ClientMsg::RequestMsgs); - let NetState::Connecting(th) = self.net.take() else { + ClientEvent::Connect { send } => { + let ClientState::Connect(state) = self.state.take() else { panic!("invalid state"); }; - self.net = NetState::Connected(NetHandle { - send: send.clone(), - thread: th, + let th = state.handle.unwrap(); + self.state = ClientState::Login(Login { + handle: NetHandle { + send: send.clone(), + thread: th, + }, }); - main_view(self, send).set_root(&mut self.ui); + login_screen(self).set_ptr(&self.main_ui, &mut self.ui); } ClientEvent::ServerMsg(msg) => match msg { ServerMsg::SendMsg(msg) => { - if let Some(msg_area) = &self.channel { - let msg = msg_widget(msg).add(&mut self.ui); + if let ClientState::LoggedIn(state) = &mut self.state + && let Some(msg_area) = &state.channel + { + let msg = msg_widget(&msg.user, &msg.content).add(&mut self.ui); self.ui[msg_area].children.push(msg.any()); } } ServerMsg::LoadMsgs(msgs) => { - if let Some(msg_area) = &self.channel { + if let ClientState::LoggedIn(state) = &mut self.state + && let Some(msg_area) = &state.channel + { for msg in msgs { - self.msgs.push(msg.clone()); - let msg = msg_widget(msg).add(&mut self.ui); + state.msgs.push(msg.clone()); + let msg = msg_widget(&msg.user, &msg.content).add(&mut self.ui); self.ui[msg_area].children.push(msg.any()); } } } + ServerMsg::Login { username } => { + let ClientState::Login(state) = self.state.take() else { + panic!("invalid state"); + }; + state.handle.send(ClientMsg::RequestMsgs); + self.state = ClientState::LoggedIn(LoggedIn { + network: state.handle, + channel: None, + msgs: Vec::new(), + username, + }); + main_view(self).set_ptr(&self.main_ui, &mut self.ui); + } + ServerMsg::Error(error) => { + let msg = format!("{error:?}"); + self.ui[&self.notif].inner = Some(werror(&mut self.ui, &msg)); + } }, ClientEvent::Err(msg) => { - if let Some(err) = &self.error { - self.ui[err].inner = Some(ui::error(&mut self.ui, &msg)); - } + self.ui[&self.notif].inner = Some(werror(&mut self.ui, &msg)); } } } @@ -222,10 +250,7 @@ impl Client { } pub fn exit(&mut self) { - if let Some(handle) = self.net.take_connection() { - handle.send.send(NetCtrlMsg::Exit); - let _ = handle.thread.join(); - } + self.state.exit(); self.dir.save(CLIENT_DATA, &self.data); } } diff --git a/src/bin/client/net.rs b/src/bin/client/net.rs index fe1686a..c4f835c 100644 --- a/src/bin/client/net.rs +++ b/src/bin/client/net.rs @@ -19,7 +19,6 @@ pub const CLIENT_SOCKET: SocketAddr = pub struct ConnectInfo { pub ip: String, - pub username: String, } pub struct NetHandle { @@ -27,27 +26,6 @@ pub struct NetHandle { pub thread: JoinHandle<()>, } -#[derive(Default)] -pub enum NetState { - #[default] - None, - Connecting(JoinHandle<()>), - Connected(NetHandle), -} - -impl NetState { - pub fn take_connection(&mut self) -> Option { - match self.take() { - NetState::Connected(net_handle) => Some(net_handle), - _ => None, - } - } - - pub fn take(&mut self) -> Self { - std::mem::replace(self, Self::None) - } -} - pub fn connect(handle: AppHandle, info: ConnectInfo) -> JoinHandle<()> { std::thread::spawn(move || { if let Err(msg) = connect_the(handle.clone(), info) { @@ -80,6 +58,17 @@ impl NetSender { } } +impl NetHandle { + pub fn send(&self, msg: impl Into) { + self.send.send(msg.into()); + } + + pub fn exit(self) { + self.send(NetCtrlMsg::Exit); + let _ = self.thread.join(); + } +} + // async fn connection_cert(addr: SocketAddr) -> NetResult { // let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap(); // let mut roots = quinn::rustls::RootCertStore::empty(); @@ -151,7 +140,6 @@ async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> { let conn_ = conn.clone(); handle.send(ClientEvent::Connect { - username: info.username, send: NetSender { send }, }); diff --git a/src/bin/client/rsc.rs b/src/bin/client/rsc.rs index 0c8cf35..531d3fc 100644 --- a/src/bin/client/rsc.rs +++ b/src/bin/client/rsc.rs @@ -1,7 +1,11 @@ pub const CLIENT_DATA: &str = "client_data"; -#[derive(Default, bincode::Encode, bincode::Decode)] +#[derive(Debug, Default, bincode::Encode, bincode::Decode)] pub struct ClientData { pub ip: String, pub username: String, + /// TODO: not store this as plain string? + /// need to figure out crypto stuff + /// or store session token + pub password: String, } diff --git a/src/bin/client/state.rs b/src/bin/client/state.rs new file mode 100644 index 0000000..c5c5f97 --- /dev/null +++ b/src/bin/client/state.rs @@ -0,0 +1,51 @@ +use iris::prelude::*; +use openworm::net::NetServerMsg; +use std::thread::JoinHandle; + +use crate::net::NetHandle; + +#[derive(Default)] +pub struct Connect { + pub handle: Option>, +} + +pub struct Login { + pub handle: NetHandle, +} + +pub struct LoggedIn { + pub network: NetHandle, + pub msgs: Vec, + pub channel: Option>, + pub username: String, +} + +pub enum ClientState { + Connect(Connect), + Login(Login), + LoggedIn(LoggedIn), +} + +impl Default for ClientState { + fn default() -> Self { + Self::Connect(Default::default()) + } +} + +impl ClientState { + pub fn take(&mut self) -> Self { + std::mem::take(self) + } + pub fn exit(&mut self) { + let s = self.take(); + match s { + ClientState::Connect(_) => (), + ClientState::Login(Login { handle }) => { + handle.exit(); + } + ClientState::LoggedIn(state) => { + state.network.exit(); + } + } + } +} diff --git a/src/bin/client/ui/login.rs b/src/bin/client/ui/login.rs index 8399ce6..26de33e 100644 --- a/src/bin/client/ui/login.rs +++ b/src/bin/client/ui/login.rs @@ -1,53 +1,65 @@ -use crate::net::NetState; +use openworm::net::ClientMsg; + +use crate::state::ClientState; use super::*; -pub fn login_screen(client: &mut Client) -> WidgetId { - let Client { - ui, handle, data, .. - } = client; +pub fn field_widget(name: &str, hint_text: &str, ui: &mut Ui) -> WidgetId { + wtext(name) + .editable(true) + .size(20) + .hint(hint(hint_text)) + .add(ui) +} - let mut field = |name, hint_| text(name).editable(true).size(20).hint(hint(hint_)).add(ui); - let ip = field(&data.ip, "ip"); - let username = field(&data.username, "username"); - // let password = field("password"); +pub fn field_box(field: WidgetId, ui: &mut Ui) -> WidgetId { + field + .clone() + .pad(10) + .background(rect(Color::BLACK.brighter(0.1)).radius(15)) + .attr::(field) + .add(ui) + .any() +} - let fbx = |field: WidgetId| { - field - .clone() - .pad(10) - .background(rect(Color::BLACK.brighter(0.1)).radius(15)) - .attr::(field) - }; - - // I LAV NOT HAVING ERGONOMIC CLONES - let handle = handle.clone(); - let ip_ = ip.clone(); - let username_ = username.clone(); +pub fn submit_button(text: &str, on_submit: impl Fn(&mut Client) + 'static) -> impl WidgetRet { let color = Color::GREEN; - let submit = rect(color) + rect(color) .radius(15) .id_on(CursorSense::click(), move |id, client: &mut Client, _| { client.ui[id].color = color.darker(0.3); - let ip = client.ui[&ip_].content(); - let username = client.ui[&username_].content(); - let th = connect(handle.clone(), ConnectInfo { ip, username }); - client.net = NetState::Connecting(th); + on_submit(client); }) - .height(40); - let modal = ( - text("login").text_align(Align::CENTER).size(30), - fbx(ip - .id_on(Edited, |id, client: &mut Client, _| { + .height(40) + .foreground(wtext(text).size(20).text_align(Align::CENTER)) + .to_any() +} + +pub fn connect_screen(client: &mut Client) -> WidgetId { + let Client { + data, ui, handle, .. + } = client; + let ip = field_widget(&data.ip, "ip", ui); + let ip_ = ip.clone(); + let handle = handle.clone(); + let submit = submit_button("connect", move |client| { + let ClientState::Connect(state) = &mut client.state else { + return; + }; + let ip = client.ui[&ip_].content(); + state.handle = Some(connect(handle.clone(), ConnectInfo { ip })); + }); + ( + wtext("connect to a server") + .text_align(Align::CENTER) + .size(30), + field_box( + ip.id_on(Edited, |id, client: &mut Client, _| { client.data.ip = client.ui[id].content(); }) - .add(ui)), - fbx(username - .id_on(Edited, |id, client: &mut Client, _| { - client.data.username = client.ui[id].content(); - }) - .add(ui)), - // fbx(password), + .add(ui), + ui, + ), submit, ) .span(Dir::DOWN) @@ -55,10 +67,64 @@ pub fn login_screen(client: &mut Client) -> WidgetId { .pad(15) .background(rect(Color::BLACK.brighter(0.2)).radius(15)) .width(400) - .align(Align::CENTER); - - let err = WidgetPtr::default().add(ui); - client.error = Some(err.clone()); - - (modal, err.align(Align::TOP_CENTER)).stack().add(ui).any() + .align(Align::CENTER) + .add(ui) + .any() +} + +pub fn login_screen(client: &mut Client) -> WidgetId { + let Client { data, ui, .. } = client; + let username = field_widget(&data.username, "username", ui); + let password = field_widget(&data.password, "password", ui); + let username_ = username.clone(); + let password_ = password.clone(); + let submit = submit_button("login", move |client| { + let ClientState::Login(state) = &mut client.state else { + return; + }; + let username = client.ui[&username_].content(); + let password = client.ui[&password_].content(); + state.handle.send(ClientMsg::Login { username, password }); + }); + let username_ = username.clone(); + let password_ = password.clone(); + let create_button = submit_button("create account", move |client| { + let ClientState::Login(state) = &mut client.state else { + return; + }; + let username = client.ui[&username_].content(); + let password = client.ui[&password_].content(); + state + .handle + .send(ClientMsg::CreateAccount { username, password }); + }); + ( + wtext("login to server").text_align(Align::CENTER).size(30), + field_box( + username + .id_on(Edited, |id, client: &mut Client, _| { + client.data.username = client.ui[id].content(); + }) + .add(ui), + ui, + ), + field_box( + password + .id_on(Edited, |id, client: &mut Client, _| { + client.data.password = client.ui[id].content(); + }) + .add(ui), + ui, + ), + submit, + create_button, + ) + .span(Dir::DOWN) + .gap(10) + .pad(15) + .background(rect(Color::BLACK.brighter(0.2)).radius(15)) + .width(400) + .align(Align::CENTER) + .add(ui) + .any() } diff --git a/src/bin/client/ui/main.rs b/src/bin/client/ui/main.rs index 72bb45b..eee182a 100644 --- a/src/bin/client/ui/main.rs +++ b/src/bin/client/ui/main.rs @@ -1,9 +1,15 @@ use super::*; +use crate::state::{ClientState, LoggedIn}; +use iris::layout::len_fns::*; +use openworm::net::{ClientMsg, NetClientMsg, NetServerMsg}; pub const SIZE: u32 = 20; -pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId { - let msg_panel = msg_panel(client, network); +pub fn main_view(client: &mut Client) -> WidgetId { + let ClientState::LoggedIn(state) = &mut client.state else { + panic!("we ain't logged in buh"); + }; + let msg_panel = msg_panel(&mut client.ui, state); let side_bar = rect(Color::BLACK.brighter(0.05)).width(80); let bg = ( @@ -18,13 +24,13 @@ pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId { .any() } -pub fn msg_widget(msg: NetMsg) -> impl WidgetLike { - let content = text(msg.content) +pub fn msg_widget(username: &str, content: &str) -> impl WidgetRet { + let content = wtext(content) .editable(false) .size(SIZE) .wrap(true) .attr::(()); - let header = text(msg.user).size(SIZE); + let header = wtext(username).size(SIZE); ( image(include_bytes!("../assets/sungals.png")) .sized((70, 70)) @@ -37,14 +43,14 @@ pub fn msg_widget(msg: NetMsg) -> impl WidgetLike { ) .span(Dir::RIGHT) .gap(10) + .to_any() } -pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn + use<> { - let Client { ui, channel, .. } = client; +pub fn msg_panel(ui: &mut Ui, state: &mut LoggedIn) -> impl WidgetRet + use<> { let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui); - *channel = Some(msg_area.clone()); + state.channel = Some(msg_area.clone()); - let send_text = text("") + let send_text = wtext("") .editable(false) .size(SIZE) .wrap(true) @@ -60,13 +66,15 @@ pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn impl WidgetFn WidgetId { - text(msg) +pub fn werror(ui: &mut Ui, msg: &str) -> WidgetId { + wtext(msg) .size(20) - .color(Color::RED.brighter(0.3)) .pad(10) + .background(rect(Color::RED).radius(10)) .add(ui) .any() } @@ -69,5 +69,5 @@ fn select(id: WidgetId, client: &mut Client, data: CursorData) { } pub fn hint(msg: impl Into) -> TextBuilder { - text(msg).size(20).color(Color::GRAY) + wtext(msg).size(20).color(Color::GRAY) } diff --git a/src/bin/client/ui/mod.rs b/src/bin/client/ui/mod.rs index 7f068ba..15fdb22 100644 --- a/src/bin/client/ui/mod.rs +++ b/src/bin/client/ui/mod.rs @@ -1,16 +1,14 @@ use crate::{ Client, - net::{ConnectInfo, NetSender, connect}, - ui::login::login_screen, + net::{ConnectInfo, connect}, }; use iris::prelude::*; -use len_fns::*; -use openworm::net::{ClientMsg, NetMsg}; use winit::dpi::{LogicalPosition, LogicalSize}; mod login; mod main; mod misc; +pub use login::*; pub use main::*; pub use misc::*; @@ -27,7 +25,3 @@ impl DefaultEvent for Submit { impl DefaultEvent for Edited { type Data = (); } - -pub fn init(client: &mut Client) { - login_screen(client).set_root(&mut client.ui); -} diff --git a/src/bin/server/db.rs b/src/bin/server/db.rs index 5db81d7..a457d1a 100644 --- a/src/bin/server/db.rs +++ b/src/bin/server/db.rs @@ -1,11 +1,90 @@ -use std::path::Path; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + path::Path, +}; use bincode::{Decode, Encode}; use openworm::net::BINCODE_CONFIG; -use sled::{Db, Tree}; +use sled::Tree; pub const DB_VERSION: u64 = 0; +#[derive(Encode, Decode)] +pub struct User { + pub username: String, + pub password_hash: String, +} + +#[derive(Clone, Encode, Decode)] +pub struct Msg { + pub user: u64, + pub content: String, +} + +#[derive(Clone)] +pub struct Db { + pub db: sled::Db, + pub msgs: DbMap, + pub users: DbMap, + pub usernames: DbMap, +} + +pub struct DbMap { + tree: Tree, + _pd: PhantomData<(K, V)>, +} + +pub trait Key { + type Output<'a>: AsRef<[u8]> + where + Self: 'a; + fn bytes(&self) -> Self::Output<'_>; +} + +impl Key for String { + type Output<'a> = &'a Self; + fn bytes(&self) -> Self::Output<'_> { + self + } +} + +impl Key for str { + type Output<'a> = &'a Self; + fn bytes(&self) -> Self::Output<'_> { + self + } +} + +impl Key for u64 { + type Output<'a> = [u8; 8]; + + fn bytes(&self) -> Self::Output<'_> { + self.to_be_bytes() + } +} + +impl> DbMap { + pub fn insert(&self, k: &K, v: &V) { + self.tree.insert_(k, v); + } + + pub fn get(&self, k: &K) -> Option { + self.tree.get_(k) + } + + pub fn init_unique(&self, k: &K) -> bool { + self.tree + .compare_and_swap(k.bytes(), None as Option<&[u8]>, Some(&[0])) + .unwrap() + .is_ok() + } + + pub fn iter_all(&self) -> impl Iterator { + self.tree.iter_all() + } +} + pub fn open_db(path: impl AsRef) -> Db { let db = sled::open(path).expect("failed to open database"); if !db.was_recovered() { @@ -19,23 +98,28 @@ pub fn open_db(path: impl AsRef) -> Db { panic!("non matching db version! (auto update in the future)"); } } - db + Db { + msgs: open_tree("msg", &db), + users: open_tree("user", &db), + usernames: open_tree("username", &db), + db, + } } -pub trait DbUtil { - fn insert_, V: Encode>(&self, k: K, v: V); - fn get_, V: Decode<()>>(&self, k: K) -> Option; +trait DbUtil { + fn insert_(&self, k: &(impl Key + ?Sized), v: V); + fn get_>(&self, k: &(impl Key + ?Sized)) -> Option; fn iter_all>(&self) -> impl Iterator; } impl DbUtil for Tree { - fn insert_, V: Encode>(&self, k: K, v: V) { + fn insert_(&self, k: &(impl Key + ?Sized), v: V) { let bytes = bincode::encode_to_vec(v, BINCODE_CONFIG).unwrap(); - self.insert(k, bytes).unwrap(); + self.insert(k.bytes(), bytes).unwrap(); } - fn get_, V: Decode<()>>(&self, k: K) -> Option { - let bytes = self.get(k).unwrap()?; + fn get_>(&self, k: &(impl Key + ?Sized)) -> Option { + let bytes = self.get(k.bytes()).unwrap()?; Some( bincode::decode_from_slice(&bytes, BINCODE_CONFIG) .unwrap() @@ -51,3 +135,33 @@ impl DbUtil for Tree { }) } } + +pub fn open_tree(name: &str, db: &sled::Db) -> DbMap { + DbMap { + tree: db.open_tree(name).unwrap(), + _pd: PhantomData, + } +} + +impl Deref for Db { + type Target = sled::Db; + + fn deref(&self) -> &Self::Target { + &self.db + } +} + +impl DerefMut for Db { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.db + } +} + +impl Clone for DbMap { + fn clone(&self) -> Self { + Self { + tree: self.tree.clone(), + _pd: self._pd.clone(), + } + } +} diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs index 43157b9..f37a29d 100644 --- a/src/bin/server/main.rs +++ b/src/bin/server/main.rs @@ -2,14 +2,20 @@ mod db; mod net; -use crate::db::{DbUtil, open_db}; +use crate::db::{Db, Msg, User, open_db}; use clap::Parser; use net::{ClientSender, ConAccepter, listen}; use openworm::{ - net::{ClientMsg, DisconnectReason, RecvHandler, ServerMsg, install_crypto_provider}, + net::{ + ClientMsg, DisconnectReason, NetServerMsg, RecvHandler, ServerError, ServerMsg, + install_crypto_provider, + }, rsc::DataDir, }; -use sled::{Db, Tree}; +use scrypt::{ + Scrypt, + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}, +}; use std::{ collections::HashMap, sync::{ @@ -39,7 +45,6 @@ pub async fn run_server(port: u16) { let path = dir.get(); let db: Db = open_db(path.join("server.db")); let handler = ServerListener { - msgs: db.open_tree("msgs").unwrap(), senders: Default::default(), count: 0.into(), db: db.clone(), @@ -60,19 +65,24 @@ type ClientId = u64; struct ServerListener { db: Db, - msgs: Tree, senders: Arc>>, count: AtomicU64, } +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClientState { + Login, + Authed(u64), +} + impl ConAccepter for ServerListener { async fn accept(&self, send: ClientSender) -> impl RecvHandler { let id = self.count.fetch_add(1, Ordering::Release); self.senders.write().await.insert(id, send.clone()); ClientHandler { db: self.db.clone(), - msgs: self.msgs.clone(), senders: self.senders.clone(), + state: Arc::new(RwLock::new(ClientState::Login)), send, id, } @@ -81,22 +91,35 @@ impl ConAccepter for ServerListener { struct ClientHandler { db: Db, - msgs: Tree, send: ClientSender, senders: Arc>>, id: ClientId, + state: Arc>, } impl RecvHandler for ClientHandler { async fn connect(&self) -> () { - println!("connected: {:?}", self.send.remote()); + println!("connected: {:?}", self.send.remote().ip()); } async fn msg(&self, msg: ClientMsg) { match msg { ClientMsg::SendMsg(msg) => { + let ClientState::Authed(uid) = &*self.state.read().await else { + let _ = self.send.send(ServerError::NotLoggedIn).await; + return; + }; + let msg = Msg { + user: *uid, + content: msg.content, + }; let id = self.db.generate_id().unwrap(); - self.msgs.insert_(id.to_be_bytes(), &msg); + self.db.msgs.insert(&id, &msg); let mut handles = Vec::new(); + let user: User = self.db.users.get(uid).unwrap(); + let msg = NetServerMsg { + content: msg.content, + user: user.username, + }; for (&id, send) in self.senders.read().await.iter() { if id == self.id { continue; @@ -104,7 +127,7 @@ impl RecvHandler for ClientHandler { let send = send.clone(); let msg = msg.clone(); let fut = async move { - let _ = send.send(ServerMsg::SendMsg(msg)).await; + let _ = send.send(msg).await; }; handles.push(tokio::spawn(fut)); } @@ -113,17 +136,85 @@ impl RecvHandler for ClientHandler { } } ClientMsg::RequestMsgs => { - let msgs = self.msgs.iter_all().collect(); + let ClientState::Authed(_uid) = &*self.state.read().await else { + let _ = self.send.send(ServerError::NotLoggedIn).await; + return; + }; + let msgs = self + .db + .msgs + .iter_all() + .map(|msg| { + let user = self + .db + .users + .get(&msg.user) + .map(|user| user.username.to_string()) + .unwrap_or("deleted user".to_string()); + NetServerMsg { + content: msg.content, + user, + } + }) + .collect(); let _ = self.send.send(ServerMsg::LoadMsgs(msgs)).await; } + ClientMsg::CreateAccount { username, password } => { + if !self.db.usernames.init_unique(&username) { + let _ = self.send.send(ServerError::UsernameTaken).await; + return; + } + let id = self.db.generate_id().unwrap(); + let salt = SaltString::generate(&mut OsRng); + let params = scrypt::Params::new(11, 8, 1, 32).unwrap(); + let hash = Scrypt + .hash_password_customized(password.as_bytes(), None, None, params, &salt) + .unwrap() + .to_string(); + self.db.users.insert( + &id, + &User { + username: username.clone(), + password_hash: hash, + }, + ); + println!("account created: \"{username}\""); + self.db.usernames.insert(&username, &id); + *self.state.write().await = ClientState::Authed(id); + let _ = self.send.send(ServerMsg::Login { username }).await; + } + ClientMsg::Login { username, password } => { + let Some(id) = self.db.usernames.get(&username) else { + let _ = self.send.send(ServerError::UnknownUsername).await; + return; + }; + let Some(user) = self.db.users.get(&id) else { + panic!("invalid state! (should be a user)"); + }; + let hash = PasswordHash::new(&user.password_hash).unwrap(); + if Scrypt.verify_password(password.as_bytes(), &hash).is_err() { + println!("invalid password: \"{username}\""); + let _ = self.send.send(ServerError::InvalidPassword).await; + return; + } + println!("login: \"{username}\""); + *self.state.write().await = ClientState::Authed(id); + let _ = self.send.send(ServerMsg::Login { username }).await; + } } } async fn disconnect(&self, reason: DisconnectReason) -> () { - println!("disconnected: {:?}", self.send.remote()); + println!("disconnected: {:?}", self.send.remote().ip()); match reason { DisconnectReason::Closed | DisconnectReason::Timeout => (), DisconnectReason::Other(e) => println!("connection issue: {e}"), } } } + +impl ClientState { + pub fn is_authed(&self) -> bool { + matches!(self, Self::Authed(_)) + } +} diff --git a/src/bin/server/net.rs b/src/bin/server/net.rs index 258666b..8169db3 100644 --- a/src/bin/server/net.rs +++ b/src/bin/server/net.rs @@ -62,7 +62,8 @@ impl ClientSender { pub fn remote(&self) -> SocketAddr { self.conn.remote_address() } - pub async fn send(&self, msg: ServerMsg) -> SendResult { + pub async fn send(&self, msg: impl Into) -> SendResult { + let msg = msg.into(); send_uni(&self.conn, msg).await } } diff --git a/src/net/mod.rs b/src/net/mod.rs index da3b4bf..ed02b3c 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -11,24 +11,53 @@ pub const BINCODE_CONFIG: Configuration = bincode::config::standard(); #[derive(Debug, bincode::Encode, bincode::Decode)] pub enum ClientMsg { - SendMsg(NetMsg), + SendMsg(NetClientMsg), RequestMsgs, + CreateAccount { username: String, password: String }, + Login { username: String, password: String }, } #[derive(Debug, bincode::Encode, bincode::Decode)] pub enum ServerMsg { - SendMsg(NetMsg), - LoadMsgs(Vec), + SendMsg(NetServerMsg), + LoadMsgs(Vec), + Login { username: String }, + Error(ServerError), +} + +#[derive(Debug, bincode::Encode, bincode::Decode)] +pub enum ServerError { + NotLoggedIn, + UnknownUsername, + InvalidPassword, + UsernameTaken, +} + +impl From for ServerMsg { + fn from(value: ServerError) -> Self { + Self::Error(value) + } } pub type ServerResp = Result; #[derive(Debug, Clone, bincode::Encode, bincode::Decode)] -pub struct NetMsg { +pub struct NetClientMsg { + pub content: String, +} + +#[derive(Debug, Clone, bincode::Encode, bincode::Decode)] +pub struct NetServerMsg { pub content: String, pub user: String, } +impl From for ServerMsg { + fn from(value: NetServerMsg) -> Self { + Self::SendMsg(value) + } +} + pub fn install_crypto_provider() { quinn::rustls::crypto::ring::default_provider() .install_default()