accounts are now real

This commit is contained in:
2025-12-03 22:51:57 -05:00
parent 4aa22de61b
commit 24bb65bf7b
15 changed files with 679 additions and 163 deletions

143
Cargo.lock generated
View File

@@ -244,6 +244,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "2.0.1" version = "2.0.1"
@@ -312,6 +318,15 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 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]] [[package]]
name = "block2" name = "block2"
version = "0.5.1" version = "0.5.1"
@@ -437,6 +452,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "clap" name = "clap"
version = "4.5.53" version = "4.5.53"
@@ -622,6 +647,15 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -662,6 +696,16 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 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]] [[package]]
name = "cursor-icon" name = "cursor-icon"
version = "1.2.0" version = "1.2.0"
@@ -677,6 +721,17 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "directories-next" name = "directories-next"
version = "2.0.0" version = "2.0.0"
@@ -966,6 +1021,16 @@ dependencies = [
"byteorder", "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]] [[package]]
name = "gethostname" name = "gethostname"
version = "1.1.0" version = "1.1.0"
@@ -1157,6 +1222,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.8" version = "0.25.8"
@@ -1207,6 +1281,15 @@ dependencies = [
"hashbrown 0.16.0", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.13" version = "0.1.13"
@@ -1981,6 +2064,7 @@ dependencies = [
"quinn", "quinn",
"rcgen", "rcgen",
"ron", "ron",
"scrypt",
"sled", "sled",
"tokio", "tokio",
"tracing", "tracing",
@@ -2074,12 +2158,33 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "pem" name = "pem"
version = "3.0.6" version = "3.0.6"
@@ -2722,6 +2827,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -2752,6 +2866,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "sctk-adwaita" name = "sctk-adwaita"
version = "0.10.1" version = "0.10.1"
@@ -2833,6 +2959,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -3329,6 +3466,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"

View File

@@ -22,6 +22,7 @@ zstd = "0.13.3"
ron = "0.12.0" ron = "0.12.0"
sled = "0.34.7" sled = "0.34.7"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
scrypt = "0.11.0"
[[bin]] [[bin]]
name = "openworm-client" name = "openworm-client"

2
iris

Submodule iris updated: d6a9711ceb...84c460a91f

View File

@@ -2,8 +2,9 @@
use crate::{ use crate::{
app::App, app::App,
net::{NetCtrlMsg, NetHandle, NetSender, NetState}, net::{NetHandle, NetSender},
rsc::{CLIENT_DATA, ClientData}, rsc::{CLIENT_DATA, ClientData},
state::{ClientState, LoggedIn, Login},
ui::*, ui::*,
}; };
pub use app::AppHandle; pub use app::AppHandle;
@@ -11,7 +12,7 @@ use arboard::Clipboard;
use input::Input; use input::Input;
use iris::prelude::*; use iris::prelude::*;
use openworm::{ use openworm::{
net::{ClientMsg, NetMsg, ServerMsg, install_crypto_provider}, net::{ClientMsg, ServerMsg, install_crypto_provider},
rsc::DataDir, rsc::DataDir,
}; };
use render::Renderer; use render::Renderer;
@@ -28,6 +29,7 @@ mod input;
mod net; mod net;
mod render; mod render;
mod rsc; mod rsc;
mod state;
mod ui; mod ui;
fn main() { fn main() {
@@ -35,28 +37,26 @@ fn main() {
App::run(); App::run();
} }
pub enum ClientEvent {
Connect { send: NetSender, username: String },
ServerMsg(ServerMsg),
Err(String),
}
pub struct Client { pub struct Client {
renderer: Renderer, renderer: Renderer,
input: Input, input: Input,
ui: Ui, ui: Ui,
focus: Option<WidgetId<TextEdit>>, focus: Option<WidgetId<TextEdit>>,
channel: Option<WidgetId<Span>>,
username: String,
clipboard: Clipboard, clipboard: Clipboard,
dir: DataDir, dir: DataDir,
data: ClientData, data: ClientData,
handle: AppHandle, handle: AppHandle,
error: Option<WidgetId<WidgetPtr>>, state: ClientState,
net: NetState,
msgs: Vec<NetMsg>,
ime: usize, ime: usize,
last_click: Instant, last_click: Instant,
main_ui: WidgetId<WidgetPtr>,
notif: WidgetId<WidgetPtr>,
}
pub enum ClientEvent {
Connect { send: NetSender },
ServerMsg(ServerMsg),
Err(String),
} }
impl Client { impl Client {
@@ -70,62 +70,90 @@ impl Client {
let dir = DataDir::default(); let dir = DataDir::default();
let handle = AppHandle { proxy, window }; 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 { let mut s = Self {
handle, handle,
renderer, renderer,
input: Input::default(), input: Input::default(),
ui: Ui::new(), ui,
data: dir.load(CLIENT_DATA), data: dir.load(CLIENT_DATA),
state: Default::default(),
dir, dir,
channel: None,
focus: None, focus: None,
net: Default::default(),
username: "<unknown>".to_string(),
clipboard: Clipboard::new().unwrap(), clipboard: Clipboard::new().unwrap(),
error: None,
ime: 0, ime: 0,
last_click: Instant::now(), 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 s
} }
pub fn event(&mut self, event: ClientEvent, _: &ActiveEventLoop) { pub fn event(&mut self, event: ClientEvent, _: &ActiveEventLoop) {
match event { match event {
ClientEvent::Connect { send, username } => { ClientEvent::Connect { send } => {
self.username = username; let ClientState::Connect(state) = self.state.take() else {
send.send(ClientMsg::RequestMsgs);
let NetState::Connecting(th) = self.net.take() else {
panic!("invalid state"); panic!("invalid state");
}; };
self.net = NetState::Connected(NetHandle { let th = state.handle.unwrap();
self.state = ClientState::Login(Login {
handle: NetHandle {
send: send.clone(), send: send.clone(),
thread: th, 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 { ClientEvent::ServerMsg(msg) => match msg {
ServerMsg::SendMsg(msg) => { ServerMsg::SendMsg(msg) => {
if let Some(msg_area) = &self.channel { if let ClientState::LoggedIn(state) = &mut self.state
let msg = msg_widget(msg).add(&mut self.ui); && 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()); self.ui[msg_area].children.push(msg.any());
} }
} }
ServerMsg::LoadMsgs(msgs) => { 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 { for msg in msgs {
self.msgs.push(msg.clone()); state.msgs.push(msg.clone());
let msg = msg_widget(msg).add(&mut self.ui); let msg = msg_widget(&msg.user, &msg.content).add(&mut self.ui);
self.ui[msg_area].children.push(msg.any()); 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) => { ClientEvent::Err(msg) => {
if let Some(err) = &self.error { self.ui[&self.notif].inner = Some(werror(&mut self.ui, &msg));
self.ui[err].inner = Some(ui::error(&mut self.ui, &msg));
}
} }
} }
} }
@@ -222,10 +250,7 @@ impl Client {
} }
pub fn exit(&mut self) { pub fn exit(&mut self) {
if let Some(handle) = self.net.take_connection() { self.state.exit();
handle.send.send(NetCtrlMsg::Exit);
let _ = handle.thread.join();
}
self.dir.save(CLIENT_DATA, &self.data); self.dir.save(CLIENT_DATA, &self.data);
} }
} }

View File

@@ -19,7 +19,6 @@ pub const CLIENT_SOCKET: SocketAddr =
pub struct ConnectInfo { pub struct ConnectInfo {
pub ip: String, pub ip: String,
pub username: String,
} }
pub struct NetHandle { pub struct NetHandle {
@@ -27,27 +26,6 @@ pub struct NetHandle {
pub thread: JoinHandle<()>, pub thread: JoinHandle<()>,
} }
#[derive(Default)]
pub enum NetState {
#[default]
None,
Connecting(JoinHandle<()>),
Connected(NetHandle),
}
impl NetState {
pub fn take_connection(&mut self) -> Option<NetHandle> {
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<()> { pub fn connect(handle: AppHandle, info: ConnectInfo) -> JoinHandle<()> {
std::thread::spawn(move || { std::thread::spawn(move || {
if let Err(msg) = connect_the(handle.clone(), info) { if let Err(msg) = connect_the(handle.clone(), info) {
@@ -80,6 +58,17 @@ impl NetSender {
} }
} }
impl NetHandle {
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
self.send.send(msg.into());
}
pub fn exit(self) {
self.send(NetCtrlMsg::Exit);
let _ = self.thread.join();
}
}
// async fn connection_cert(addr: SocketAddr) -> NetResult<Connection> { // async fn connection_cert(addr: SocketAddr) -> NetResult<Connection> {
// let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap(); // let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap();
// let mut roots = quinn::rustls::RootCertStore::empty(); // let mut roots = quinn::rustls::RootCertStore::empty();
@@ -151,7 +140,6 @@ async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
let conn_ = conn.clone(); let conn_ = conn.clone();
handle.send(ClientEvent::Connect { handle.send(ClientEvent::Connect {
username: info.username,
send: NetSender { send }, send: NetSender { send },
}); });

View File

@@ -1,7 +1,11 @@
pub const CLIENT_DATA: &str = "client_data"; pub const CLIENT_DATA: &str = "client_data";
#[derive(Default, bincode::Encode, bincode::Decode)] #[derive(Debug, Default, bincode::Encode, bincode::Decode)]
pub struct ClientData { pub struct ClientData {
pub ip: String, pub ip: String,
pub username: String, pub username: String,
/// TODO: not store this as plain string?
/// need to figure out crypto stuff
/// or store session token
pub password: String,
} }

51
src/bin/client/state.rs Normal file
View File

@@ -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<JoinHandle<()>>,
}
pub struct Login {
pub handle: NetHandle,
}
pub struct LoggedIn {
pub network: NetHandle,
pub msgs: Vec<NetServerMsg>,
pub channel: Option<WidgetId<Span>>,
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();
}
}
}
}

View File

@@ -1,53 +1,65 @@
use crate::net::NetState; use openworm::net::ClientMsg;
use crate::state::ClientState;
use super::*; use super::*;
pub fn login_screen(client: &mut Client) -> WidgetId { pub fn field_widget(name: &str, hint_text: &str, ui: &mut Ui) -> WidgetId<TextEdit> {
let Client { wtext(name)
ui, handle, data, .. .editable(true)
} = client; .size(20)
.hint(hint(hint_text))
.add(ui)
}
let mut field = |name, hint_| text(name).editable(true).size(20).hint(hint(hint_)).add(ui); pub fn field_box(field: WidgetId<TextEdit>, ui: &mut Ui) -> WidgetId {
let ip = field(&data.ip, "ip");
let username = field(&data.username, "username");
// let password = field("password");
let fbx = |field: WidgetId<TextEdit>| {
field field
.clone() .clone()
.pad(10) .pad(10)
.background(rect(Color::BLACK.brighter(0.1)).radius(15)) .background(rect(Color::BLACK.brighter(0.1)).radius(15))
.attr::<Selector>(field) .attr::<Selector>(field)
}; .add(ui)
.any()
}
// I LAV NOT HAVING ERGONOMIC CLONES pub fn submit_button(text: &str, on_submit: impl Fn(&mut Client) + 'static) -> impl WidgetRet {
let handle = handle.clone();
let ip_ = ip.clone();
let username_ = username.clone();
let color = Color::GREEN; let color = Color::GREEN;
let submit = rect(color) rect(color)
.radius(15) .radius(15)
.id_on(CursorSense::click(), move |id, client: &mut Client, _| { .id_on(CursorSense::click(), move |id, client: &mut Client, _| {
client.ui[id].color = color.darker(0.3); client.ui[id].color = color.darker(0.3);
let ip = client.ui[&ip_].content(); on_submit(client);
let username = client.ui[&username_].content();
let th = connect(handle.clone(), ConnectInfo { ip, username });
client.net = NetState::Connecting(th);
}) })
.height(40); .height(40)
let modal = ( .foreground(wtext(text).size(20).text_align(Align::CENTER))
text("login").text_align(Align::CENTER).size(30), .to_any()
fbx(ip }
.id_on(Edited, |id, client: &mut Client, _| {
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(); client.data.ip = client.ui[id].content();
}) })
.add(ui)), .add(ui),
fbx(username ui,
.id_on(Edited, |id, client: &mut Client, _| { ),
client.data.username = client.ui[id].content();
})
.add(ui)),
// fbx(password),
submit, submit,
) )
.span(Dir::DOWN) .span(Dir::DOWN)
@@ -55,10 +67,64 @@ pub fn login_screen(client: &mut Client) -> WidgetId {
.pad(15) .pad(15)
.background(rect(Color::BLACK.brighter(0.2)).radius(15)) .background(rect(Color::BLACK.brighter(0.2)).radius(15))
.width(400) .width(400)
.align(Align::CENTER); .align(Align::CENTER)
.add(ui)
let err = WidgetPtr::default().add(ui); .any()
client.error = Some(err.clone()); }
(modal, err.align(Align::TOP_CENTER)).stack().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()
} }

View File

@@ -1,9 +1,15 @@
use super::*; use super::*;
use crate::state::{ClientState, LoggedIn};
use iris::layout::len_fns::*;
use openworm::net::{ClientMsg, NetClientMsg, NetServerMsg};
pub const SIZE: u32 = 20; pub const SIZE: u32 = 20;
pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId { pub fn main_view(client: &mut Client) -> WidgetId {
let msg_panel = msg_panel(client, network); 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 side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
let bg = ( let bg = (
@@ -18,13 +24,13 @@ pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId {
.any() .any()
} }
pub fn msg_widget(msg: NetMsg) -> impl WidgetLike<FnTag> { pub fn msg_widget(username: &str, content: &str) -> impl WidgetRet {
let content = text(msg.content) let content = wtext(content)
.editable(false) .editable(false)
.size(SIZE) .size(SIZE)
.wrap(true) .wrap(true)
.attr::<Selectable>(()); .attr::<Selectable>(());
let header = text(msg.user).size(SIZE); let header = wtext(username).size(SIZE);
( (
image(include_bytes!("../assets/sungals.png")) image(include_bytes!("../assets/sungals.png"))
.sized((70, 70)) .sized((70, 70))
@@ -37,14 +43,14 @@ pub fn msg_widget(msg: NetMsg) -> impl WidgetLike<FnTag> {
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.gap(10) .gap(10)
.to_any()
} }
pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn<Sized> + use<> { pub fn msg_panel(ui: &mut Ui, state: &mut LoggedIn) -> impl WidgetRet + use<> {
let Client { ui, channel, .. } = client;
let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui); 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) .editable(false)
.size(SIZE) .size(SIZE)
.wrap(true) .wrap(true)
@@ -60,13 +66,15 @@ pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn<Sized
send_text send_text
.clone() .clone()
.id_on(Submit, move |id, client: &mut Client, _| { .id_on(Submit, move |id, client: &mut Client, _| {
let content = client.ui.text(id).take(); let ClientState::LoggedIn(state) = &mut client.state else {
let msg = NetMsg { panic!("we ain't logged in buh");
content: content.clone(),
user: client.username.clone(),
}; };
network.send(ClientMsg::SendMsg(msg.clone())); let content = client.ui.text(id).take();
let msg = msg_widget(msg).add(&mut client.ui); let msg = NetClientMsg {
content: content.clone(),
};
state.network.send(ClientMsg::SendMsg(msg.clone()));
let msg = msg_widget(&client.data.username, &content).add(&mut client.ui);
client.ui[&msg_area].children.push(msg.any()); client.ui[&msg_area].children.push(msg.any());
}) })
.pad(15) .pad(15)
@@ -80,4 +88,5 @@ pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn<Sized
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.width(rest(1)) .width(rest(1))
.to_any()
} }

View File

@@ -2,11 +2,11 @@ use std::time::{Duration, Instant};
use super::*; use super::*;
pub fn error(ui: &mut Ui, msg: &str) -> WidgetId { pub fn werror(ui: &mut Ui, msg: &str) -> WidgetId {
text(msg) wtext(msg)
.size(20) .size(20)
.color(Color::RED.brighter(0.3))
.pad(10) .pad(10)
.background(rect(Color::RED).radius(10))
.add(ui) .add(ui)
.any() .any()
} }
@@ -69,5 +69,5 @@ fn select(id: WidgetId<TextEdit>, client: &mut Client, data: CursorData) {
} }
pub fn hint(msg: impl Into<String>) -> TextBuilder { pub fn hint(msg: impl Into<String>) -> TextBuilder {
text(msg).size(20).color(Color::GRAY) wtext(msg).size(20).color(Color::GRAY)
} }

View File

@@ -1,16 +1,14 @@
use crate::{ use crate::{
Client, Client,
net::{ConnectInfo, NetSender, connect}, net::{ConnectInfo, connect},
ui::login::login_screen,
}; };
use iris::prelude::*; use iris::prelude::*;
use len_fns::*;
use openworm::net::{ClientMsg, NetMsg};
use winit::dpi::{LogicalPosition, LogicalSize}; use winit::dpi::{LogicalPosition, LogicalSize};
mod login; mod login;
mod main; mod main;
mod misc; mod misc;
pub use login::*;
pub use main::*; pub use main::*;
pub use misc::*; pub use misc::*;
@@ -27,7 +25,3 @@ impl DefaultEvent for Submit {
impl DefaultEvent for Edited { impl DefaultEvent for Edited {
type Data = (); type Data = ();
} }
pub fn init(client: &mut Client) {
login_screen(client).set_root(&mut client.ui);
}

View File

@@ -1,11 +1,90 @@
use std::path::Path; use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
path::Path,
};
use bincode::{Decode, Encode}; use bincode::{Decode, Encode};
use openworm::net::BINCODE_CONFIG; use openworm::net::BINCODE_CONFIG;
use sled::{Db, Tree}; use sled::Tree;
pub const DB_VERSION: u64 = 0; 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<u64, Msg>,
pub users: DbMap<u64, User>,
pub usernames: DbMap<String, u64>,
}
pub struct DbMap<K, V> {
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<K: Key, V: Encode + Decode<()>> DbMap<K, V> {
pub fn insert(&self, k: &K, v: &V) {
self.tree.insert_(k, v);
}
pub fn get(&self, k: &K) -> Option<V> {
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<Item = V> {
self.tree.iter_all()
}
}
pub fn open_db(path: impl AsRef<Path>) -> Db { pub fn open_db(path: impl AsRef<Path>) -> Db {
let db = sled::open(path).expect("failed to open database"); let db = sled::open(path).expect("failed to open database");
if !db.was_recovered() { if !db.was_recovered() {
@@ -19,23 +98,28 @@ pub fn open_db(path: impl AsRef<Path>) -> Db {
panic!("non matching db version! (auto update in the future)"); 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 { trait DbUtil {
fn insert_<K: AsRef<[u8]>, V: Encode>(&self, k: K, v: V); fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V);
fn get_<K: AsRef<[u8]>, V: Decode<()>>(&self, k: K) -> Option<V>; fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V>;
fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V>; fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V>;
} }
impl DbUtil for Tree { impl DbUtil for Tree {
fn insert_<K: AsRef<[u8]>, V: Encode>(&self, k: K, v: V) { fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V) {
let bytes = bincode::encode_to_vec(v, BINCODE_CONFIG).unwrap(); let bytes = bincode::encode_to_vec(v, BINCODE_CONFIG).unwrap();
self.insert(k, bytes).unwrap(); self.insert(k.bytes(), bytes).unwrap();
} }
fn get_<K: AsRef<[u8]>, V: Decode<()>>(&self, k: K) -> Option<V> { fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V> {
let bytes = self.get(k).unwrap()?; let bytes = self.get(k.bytes()).unwrap()?;
Some( Some(
bincode::decode_from_slice(&bytes, BINCODE_CONFIG) bincode::decode_from_slice(&bytes, BINCODE_CONFIG)
.unwrap() .unwrap()
@@ -51,3 +135,33 @@ impl DbUtil for Tree {
}) })
} }
} }
pub fn open_tree<K, V>(name: &str, db: &sled::Db) -> DbMap<K, V> {
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<K, V> Clone for DbMap<K, V> {
fn clone(&self) -> Self {
Self {
tree: self.tree.clone(),
_pd: self._pd.clone(),
}
}
}

View File

@@ -2,14 +2,20 @@
mod db; mod db;
mod net; mod net;
use crate::db::{DbUtil, open_db}; use crate::db::{Db, Msg, User, open_db};
use clap::Parser; use clap::Parser;
use net::{ClientSender, ConAccepter, listen}; use net::{ClientSender, ConAccepter, listen};
use openworm::{ use openworm::{
net::{ClientMsg, DisconnectReason, RecvHandler, ServerMsg, install_crypto_provider}, net::{
ClientMsg, DisconnectReason, NetServerMsg, RecvHandler, ServerError, ServerMsg,
install_crypto_provider,
},
rsc::DataDir, rsc::DataDir,
}; };
use sled::{Db, Tree}; use scrypt::{
Scrypt,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{ sync::{
@@ -39,7 +45,6 @@ pub async fn run_server(port: u16) {
let path = dir.get(); let path = dir.get();
let db: Db = open_db(path.join("server.db")); let db: Db = open_db(path.join("server.db"));
let handler = ServerListener { let handler = ServerListener {
msgs: db.open_tree("msgs").unwrap(),
senders: Default::default(), senders: Default::default(),
count: 0.into(), count: 0.into(),
db: db.clone(), db: db.clone(),
@@ -60,19 +65,24 @@ type ClientId = u64;
struct ServerListener { struct ServerListener {
db: Db, db: Db,
msgs: Tree,
senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>, senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
count: AtomicU64, count: AtomicU64,
} }
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum ClientState {
Login,
Authed(u64),
}
impl ConAccepter for ServerListener { impl ConAccepter for ServerListener {
async fn accept(&self, send: ClientSender) -> impl RecvHandler<ClientMsg> { async fn accept(&self, send: ClientSender) -> impl RecvHandler<ClientMsg> {
let id = self.count.fetch_add(1, Ordering::Release); let id = self.count.fetch_add(1, Ordering::Release);
self.senders.write().await.insert(id, send.clone()); self.senders.write().await.insert(id, send.clone());
ClientHandler { ClientHandler {
db: self.db.clone(), db: self.db.clone(),
msgs: self.msgs.clone(),
senders: self.senders.clone(), senders: self.senders.clone(),
state: Arc::new(RwLock::new(ClientState::Login)),
send, send,
id, id,
} }
@@ -81,22 +91,35 @@ impl ConAccepter for ServerListener {
struct ClientHandler { struct ClientHandler {
db: Db, db: Db,
msgs: Tree,
send: ClientSender, send: ClientSender,
senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>, senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
id: ClientId, id: ClientId,
state: Arc<RwLock<ClientState>>,
} }
impl RecvHandler<ClientMsg> for ClientHandler { impl RecvHandler<ClientMsg> for ClientHandler {
async fn connect(&self) -> () { async fn connect(&self) -> () {
println!("connected: {:?}", self.send.remote()); println!("connected: {:?}", self.send.remote().ip());
} }
async fn msg(&self, msg: ClientMsg) { async fn msg(&self, msg: ClientMsg) {
match msg { match msg {
ClientMsg::SendMsg(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(); 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 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() { for (&id, send) in self.senders.read().await.iter() {
if id == self.id { if id == self.id {
continue; continue;
@@ -104,7 +127,7 @@ impl RecvHandler<ClientMsg> for ClientHandler {
let send = send.clone(); let send = send.clone();
let msg = msg.clone(); let msg = msg.clone();
let fut = async move { let fut = async move {
let _ = send.send(ServerMsg::SendMsg(msg)).await; let _ = send.send(msg).await;
}; };
handles.push(tokio::spawn(fut)); handles.push(tokio::spawn(fut));
} }
@@ -113,17 +136,85 @@ impl RecvHandler<ClientMsg> for ClientHandler {
} }
} }
ClientMsg::RequestMsgs => { 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; 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) -> () { async fn disconnect(&self, reason: DisconnectReason) -> () {
println!("disconnected: {:?}", self.send.remote()); println!("disconnected: {:?}", self.send.remote().ip());
match reason { match reason {
DisconnectReason::Closed | DisconnectReason::Timeout => (), DisconnectReason::Closed | DisconnectReason::Timeout => (),
DisconnectReason::Other(e) => println!("connection issue: {e}"), DisconnectReason::Other(e) => println!("connection issue: {e}"),
} }
} }
} }
impl ClientState {
pub fn is_authed(&self) -> bool {
matches!(self, Self::Authed(_))
}
}

View File

@@ -62,7 +62,8 @@ impl ClientSender {
pub fn remote(&self) -> SocketAddr { pub fn remote(&self) -> SocketAddr {
self.conn.remote_address() self.conn.remote_address()
} }
pub async fn send(&self, msg: ServerMsg) -> SendResult { pub async fn send(&self, msg: impl Into<ServerMsg>) -> SendResult {
let msg = msg.into();
send_uni(&self.conn, msg).await send_uni(&self.conn, msg).await
} }
} }

View File

@@ -11,24 +11,53 @@ pub const BINCODE_CONFIG: Configuration = bincode::config::standard();
#[derive(Debug, bincode::Encode, bincode::Decode)] #[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ClientMsg { pub enum ClientMsg {
SendMsg(NetMsg), SendMsg(NetClientMsg),
RequestMsgs, RequestMsgs,
CreateAccount { username: String, password: String },
Login { username: String, password: String },
} }
#[derive(Debug, bincode::Encode, bincode::Decode)] #[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ServerMsg { pub enum ServerMsg {
SendMsg(NetMsg), SendMsg(NetServerMsg),
LoadMsgs(Vec<NetMsg>), LoadMsgs(Vec<NetServerMsg>),
Login { username: String },
Error(ServerError),
}
#[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ServerError {
NotLoggedIn,
UnknownUsername,
InvalidPassword,
UsernameTaken,
}
impl From<ServerError> for ServerMsg {
fn from(value: ServerError) -> Self {
Self::Error(value)
}
} }
pub type ServerResp<T> = Result<T, String>; pub type ServerResp<T> = Result<T, String>;
#[derive(Debug, Clone, bincode::Encode, bincode::Decode)] #[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 content: String,
pub user: String, pub user: String,
} }
impl From<NetServerMsg> for ServerMsg {
fn from(value: NetServerMsg) -> Self {
Self::SendMsg(value)
}
}
pub fn install_crypto_provider() { pub fn install_crypto_provider() {
quinn::rustls::crypto::ring::default_provider() quinn::rustls::crypto::ring::default_provider()
.install_default() .install_default()