From a561a6fe94070bad4ac62080b0b11281b2841918 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Fri, 7 Aug 2026 15:10:20 +0800 Subject: [PATCH] server notified of client profile updates --- src-common/src/lib.rs | 9 ++++ src-server/src/network/mod.rs | 87 +++++++++++++++++++++++++++++++---- src-tauri/src/network/mod.rs | 44 +++++++++++++----- src-tauri/src/profile/mod.rs | 3 ++ 4 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src-common/src/lib.rs b/src-common/src/lib.rs index ba72281..4fd4cca 100644 --- a/src-common/src/lib.rs +++ b/src-common/src/lib.rs @@ -13,6 +13,7 @@ pub struct Profile { #[serde(tag = "type", rename_all = "camelCase")] pub enum ClientMessage { Register { profile: Profile, signature: String }, + ProfileUpdated { profile: Profile, signature: String }, Signed { payload: String, signature: String }, } @@ -34,3 +35,11 @@ pub fn register_bytes(challenge: &str, profile: &Profile) -> Vec { pub fn message_bytes(payload: &str) -> Vec { format!("wyd-message-v{VERSION}\n{payload}").into_bytes() } + +pub fn profile_bytes(profile: &Profile) -> Vec { + format!( + "wyd-profile-v{VERSION}\n{}\n{}", + profile.id, profile.display_name + ) + .into_bytes() +} diff --git a/src-server/src/network/mod.rs b/src-server/src/network/mod.rs index e24a237..2e04288 100644 --- a/src-server/src/network/mod.rs +++ b/src-server/src/network/mod.rs @@ -13,7 +13,9 @@ use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use futures_util::{SinkExt, StreamExt}; use tokio::sync::{Mutex, mpsc}; use uuid::Uuid; -use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes}; +use wyd_common::{ + ClientMessage, Profile, ServerMessage, message_bytes, profile_bytes, register_bytes, +}; type Clients = Arc>>; @@ -91,14 +93,21 @@ async fn connected(mut socket: WebSocket, clients: Clients) { if writer.send(Message::Ping(Vec::new().into())).await.is_err() { break; } } message = reader.next() => match message { - Some(Ok(Message::Text(text))) => { - let Ok(ClientMessage::Signed { payload, signature }) = serde_json::from_str(&text) else { break }; - let registered_key = clients.lock().await.get(&public_key) - .filter(|client| client.connection_id == connection_id) - .map(|client| client.key); - let Some(registered_key) = registered_key else { break }; - if !verify(®istered_key, &message_bytes(&payload), &signature) { break; } - // The message is authenticated. Domain routing comes next. + Some(Ok(Message::Text(text))) => match serde_json::from_str(&text) { + Ok(ClientMessage::Signed { payload, signature }) => { + let registered_key = clients.lock().await.get(&public_key) + .filter(|client| client.connection_id == connection_id) + .map(|client| client.key); + let Some(registered_key) = registered_key else { break }; + if !verify(®istered_key, &message_bytes(&payload), &signature) { break; } + // The message is authenticated. Domain routing comes next. + } + Ok(ClientMessage::ProfileUpdated { profile, signature }) => { + if !update_profile(&clients, &public_key, connection_id, profile, &signature).await { + break; + } + } + _ => break, } Some(Ok(Message::Ping(data))) => { if writer.send(Message::Pong(data)).await.is_err() { break; } @@ -112,6 +121,27 @@ async fn connected(mut socket: WebSocket, clients: Clients) { remove(&clients, &public_key, connection_id).await; } +async fn update_profile( + clients: &Clients, + public_key: &str, + connection_id: Uuid, + profile: Profile, + signature: &str, +) -> bool { + let mut clients = clients.lock().await; + let Some(client) = clients + .get_mut(public_key) + .filter(|client| client.connection_id == connection_id) + else { + return false; + }; + if profile.id != public_key || !verify(&client.key, &profile_bytes(&profile), signature) { + return false; + } + client.profile = profile; + true +} + async fn remove(clients: &Clients, public_key: &str, connection_id: Uuid) { let mut clients = clients.lock().await; if clients @@ -182,4 +212,43 @@ mod tests { &signature, )); } + + #[tokio::test] + async fn authenticated_profile_update_changes_the_registered_client() { + let signing_key = SigningKey::from_bytes(&[7; 32]); + let public_key = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes()); + let connection_id = Uuid::new_v4(); + let (sender, _receiver) = mpsc::channel(1); + let clients = Clients::default(); + clients.lock().await.insert( + public_key.clone(), + Client { + connection_id, + key: signing_key.verifying_key(), + profile: Profile { + id: public_key.clone(), + display_name: "Old".to_owned(), + }, + sender, + }, + ); + let profile = Profile { + id: public_key.clone(), + display_name: "New".to_owned(), + }; + let signature = + URL_SAFE_NO_PAD.encode(signing_key.sign(&profile_bytes(&profile)).to_bytes()); + + assert!(update_profile(&clients, &public_key, connection_id, profile, &signature).await); + assert_eq!( + clients + .lock() + .await + .get(&public_key) + .unwrap() + .profile + .display_name, + "New" + ); + } } diff --git a/src-tauri/src/network/mod.rs b/src-tauri/src/network/mod.rs index 10a82fb..dbca543 100644 --- a/src-tauri/src/network/mod.rs +++ b/src-tauri/src/network/mod.rs @@ -7,9 +7,11 @@ use serde::{Deserialize, Serialize}; use specta::Type; use tauri::{AppHandle, Manager, State}; use tauri_specta::Event; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, watch}; use tokio_tungstenite::tungstenite::Message; -use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes}; +use wyd_common::{ + ClientMessage, Profile, ServerMessage, message_bytes, profile_bytes, register_bytes, +}; use crate::db::AppDatabase; use crate::keypair::AppKeypair; @@ -43,6 +45,7 @@ pub struct NetworkStatusChanged { pub struct Network { senders: Mutex>>, statuses: Statuses, + profile: watch::Sender, } impl Network { @@ -56,12 +59,17 @@ impl Network { .try_send(payload) .map_err(|error| format!("remote is not ready: {error}")) } + + pub fn update_profile(&self, profile: crate::user::User) { + self.profile.send_replace(profile); + } } pub async fn init(handle: &AppHandle) -> Result<(), Box> { let database = handle.state::(); let keypair = handle.state::().inner().clone(); let profile = crate::profile::get(&database, keypair.public_key()).await?; + let (profile_sender, profile_receiver) = watch::channel(profile); let remotes = remotes::all(&database).await?; let mut senders = HashMap::new(); let statuses = Statuses::default(); @@ -74,7 +82,7 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box> handle.clone(), statuses.clone(), remote, - profile.clone(), + profile_receiver.clone(), keypair.clone(), receiver, )); @@ -83,6 +91,7 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box> handle.manage(Network { senders: Mutex::new(senders), statuses, + profile: profile_sender, }); Ok(()) } @@ -91,7 +100,7 @@ async fn run( handle: AppHandle, statuses: Statuses, remote: Remote, - profile: crate::user::User, + mut profiles: watch::Receiver, keypair: AppKeypair, mut outgoing: mpsc::Receiver, ) { @@ -101,7 +110,7 @@ async fn run( &handle, &statuses, &remote, - &profile, + &mut profiles, &keypair, &mut outgoing, ) @@ -118,7 +127,7 @@ async fn connect( handle: &AppHandle, statuses: &Statuses, remote: &Remote, - profile: &crate::user::User, + profiles: &mut watch::Receiver, keypair: &AppKeypair, outgoing: &mut mpsc::Receiver, ) -> Result<(), Box> { @@ -131,15 +140,16 @@ async fn connect( } _ => return Err("server did not send a compatible challenge".into()), }; - let profile = Profile { - id: profile.id.clone(), - display_name: profile.display_name.clone(), + let current = profiles.borrow_and_update().clone(); + let registration_profile = Profile { + id: current.id, + display_name: current.display_name, }; send( &mut writer, &ClientMessage::Register { - signature: keypair.sign(®ister_bytes(&challenge, &profile)), - profile, + signature: keypair.sign(®ister_bytes(&challenge, ®istration_profile)), + profile: registration_profile, }, ) .await?; @@ -158,6 +168,18 @@ async fn connect( payload, }).await?; } + changed = profiles.changed() => { + changed.map_err(|_| "profile sender closed")?; + let current = profiles.borrow_and_update().clone(); + let profile = Profile { + id: current.id, + display_name: current.display_name, + }; + send(&mut writer, &ClientMessage::ProfileUpdated { + signature: keypair.sign(&profile_bytes(&profile)), + profile, + }).await?; + } message = reader.next() => match message.ok_or("server closed the socket")?? { Message::Ping(data) => writer.send(Message::Pong(data)).await?, Message::Close(_) => return Ok(()), diff --git a/src-tauri/src/profile/mod.rs b/src-tauri/src/profile/mod.rs index 19e4136..168e74d 100644 --- a/src-tauri/src/profile/mod.rs +++ b/src-tauri/src/profile/mod.rs @@ -1,5 +1,6 @@ use crate::db::{self, AppDatabase}; use crate::keypair::AppKeypair; +use crate::network::Network; use crate::user::User; use serde::{Deserialize, Serialize}; use specta::Type; @@ -71,11 +72,13 @@ pub async fn update_profile( handle: AppHandle, database: State<'_, AppDatabase>, keypair: State<'_, AppKeypair>, + network: State<'_, Network>, display_name: String, ) -> Result { let profile = update(&database, keypair.public_key(), display_name) .await .map_err(db::command_error)?; + network.update_profile(profile.clone()); emit_changed(&handle, profile.clone())?; Ok(profile) }