From cd2d8093225a438fbcf7b12e945d0089f6b756fc Mon Sep 17 00:00:00 2001 From: RuKira Date: Mon, 25 May 2026 01:06:19 -0400 Subject: [PATCH] Update: Seems everything is working now? --- RelayClient/MainPage.xaml.cs | 4 +- RelayClient/Services/RelaySocketClient.cs | 97 ++- RelayClient/Services/RtcBridgeService.cs | 4 +- .../Services/Chat/ChatSocketBehavior.cs | 816 ++++++++---------- .../Services/Chat/ConnectedClientService.cs | 63 ++ RelayShared/Services/WsControlMessage.cs | 34 + 6 files changed, 532 insertions(+), 486 deletions(-) create mode 100644 RelayServer/Services/Chat/ConnectedClientService.cs create mode 100644 RelayShared/Services/WsControlMessage.cs diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index 02902e2..8b22af0 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -137,7 +137,7 @@ public partial class MainPage : ContentPage await _rtc.PushRtcContextToJsAsync(); }); - _socket.SendRaw($"GET_HISTORY|{_username}|{_currentChannelId}"); + _socket.SendGetHistory(_currentChannelId); } private void HandleEncryptedChat(SocketEncryptedMessage payload) { @@ -229,7 +229,7 @@ public partial class MainPage : ContentPage RenderCurrentChannelMessages(); if (!_messagesByChannel.ContainsKey(channel.ChannelId)) - _socket.SendRaw($"GET_HISTORY|{_username}|{channel.ChannelId}"); + _socket.SendGetHistory(channel.ChannelId); }; SidebarList.Children.Add(button); diff --git a/RelayClient/Services/RelaySocketClient.cs b/RelayClient/Services/RelaySocketClient.cs index 75a12fc..642dee1 100644 --- a/RelayClient/Services/RelaySocketClient.cs +++ b/RelayClient/Services/RelaySocketClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using RelayClient.Crypto; using RelayShared.Services; using WebSocketSharp; @@ -12,7 +12,6 @@ public sealed class RelaySocketClient public string? ServerPublicKey { get; private set; } - public event Action? RawMessageReceived; public event Action? ChannelListReceived; public event Action? EncryptedChatReceived; public event Action? EncryptedRtcSignalReceived; @@ -32,21 +31,65 @@ public sealed class RelaySocketClient var publicKey = KeyStorage.LoadPublicKey(_username); - SendRaw($"AUTHENTICATE_USER|{_username}|{MainPage._userToken}"); - SendRaw($"REGISTER_KEY|{_username}|{publicKey}"); - SendRaw("GET_SERVER_KEY"); - SendRaw("GET_CHANNELS"); + SendControlMessage(new WsControlMessage + { + Action = WsAction.Authenticate, + Username = _username, + Token = MainPage._userToken + }); + + SendControlMessage(new WsControlMessage + { + Action = WsAction.RegisterKey, + Username = _username, + PublicKey = publicKey + }); + + SendControlMessage(new WsControlMessage { Action = WsAction.GetServerKey }); + SendControlMessage(new WsControlMessage { Action = WsAction.GetChannels }); } - public void SendRaw(string message) + public void SendGetHistory(string channelId) { - if (_socket.ReadyState == WebSocketState.Open) - _socket.Send(message); + SendControlMessage(new WsControlMessage + { + Action = WsAction.GetHistory, + Username = _username, + ChannelId = channelId + }); + } + + public void SendRtcJoinChannel(string channelId) + { + SendControlMessage(new WsControlMessage + { + Action = WsAction.RtcJoin, + Username = _username, + ChannelId = channelId + }); + } + + public void SendRtcLeaveChannel(string channelId) + { + SendControlMessage(new WsControlMessage + { + Action = WsAction.RtcLeave, + Username = _username, + ChannelId = channelId + }); + } + + private void SendControlMessage(WsControlMessage msg) + { + SendJson(msg); } public void SendJson(T payload) { - SendRaw(JsonSerializer.Serialize(payload)); + var json = JsonSerializer.Serialize(payload); + + if (_socket.ReadyState == WebSocketState.Open) + _socket.Send(json); } public void Disconnect() @@ -59,13 +102,6 @@ public sealed class RelaySocketClient private void OnMessage(object? sender, MessageEventArgs e) { - if (e.Data.StartsWith("SERVER:REGISTERED_KEY:")) - { - Log?.Invoke(e.Data); - return; - } - - RawMessageReceived?.Invoke(e.Data); Log?.Invoke($"[{_username}] RAW WS DATA: {e.Data}"); try @@ -73,6 +109,27 @@ public sealed class RelaySocketClient using var doc = JsonDocument.Parse(e.Data); var root = doc.RootElement; + if (root.TryGetProperty("Event", out var eventProp)) + { + var wsEvent = (WsEvent)eventProp.GetInt32(); + + switch (wsEvent) + { + case WsEvent.Authenticated: + Log?.Invoke($"[{_username}] Authenticated."); + return; + case WsEvent.KeyRegistered: + Log?.Invoke($"[{_username}] Key registered."); + return; + case WsEvent.Error: + var detail = root.TryGetProperty("Detail", out var d) ? d.GetString() : null; + Log?.Invoke($"[{_username}] Server error: {detail}"); + return; + } + + return; + } + if (!root.TryGetProperty("Type", out var typeElement)) return; @@ -85,7 +142,6 @@ public sealed class RelaySocketClient var channelList = JsonSerializer.Deserialize(e.Data); if (channelList is not null) ChannelListReceived?.Invoke(channelList); - return; } @@ -97,7 +153,6 @@ public sealed class RelaySocketClient ServerPublicKey = serverKeyMessage.PublicKey; ServerPublicKeyReceived?.Invoke(serverKeyMessage.PublicKey); } - return; } @@ -106,7 +161,6 @@ public sealed class RelaySocketClient var payload = JsonSerializer.Deserialize(e.Data); if (payload is not null) EncryptedRtcSignalReceived?.Invoke(payload); - return; } @@ -115,7 +169,6 @@ public sealed class RelaySocketClient var payload = JsonSerializer.Deserialize(e.Data); if (payload is not null) EncryptedChatReceived?.Invoke(payload); - return; } } @@ -125,4 +178,4 @@ public sealed class RelaySocketClient Log?.Invoke($"[{_username}] failed to process websocket message: {ex.Message}"); } } -} \ No newline at end of file +} diff --git a/RelayClient/Services/RtcBridgeService.cs b/RelayClient/Services/RtcBridgeService.cs index aa8d7d3..2a4687f 100644 --- a/RelayClient/Services/RtcBridgeService.cs +++ b/RelayClient/Services/RtcBridgeService.cs @@ -31,7 +31,7 @@ public sealed class RtcBridgeService if (string.IsNullOrWhiteSpace(channelId)) return Task.CompletedTask; - _socket.SendRaw($"RTC_JOIN_CHANNEL|{_username}|{channelId}"); + _socket.SendRtcJoinChannel(channelId); return Task.CompletedTask; } @@ -42,7 +42,7 @@ public sealed class RtcBridgeService if (string.IsNullOrWhiteSpace(channelId)) return; - _socket.SendRaw($"RTC_LEAVE_CHANNEL|{_username}|{channelId}"); + _socket.SendRtcLeaveChannel(channelId); } public void SendRtcSignal(string json) diff --git a/RelayServer/Services/Chat/ChatSocketBehavior.cs b/RelayServer/Services/Chat/ChatSocketBehavior.cs index ccfbf3b..0756a8c 100644 --- a/RelayServer/Services/Chat/ChatSocketBehavior.cs +++ b/RelayServer/Services/Chat/ChatSocketBehavior.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Text.Json; using RelayServer.Models; using RelayServer.Services.Crypto; @@ -11,11 +11,6 @@ using RelayShared.Services; namespace RelayServer.Services.Chat; -/// -/// Handles websocket-based chat operations including client key registration, -/// server key retrieval, channel listing, channel history loading, and encrypted -/// channel message relay. -/// public class ChatSocketBehavior : WebSocketBehavior { public static ClientKeyService? ClientKeyService { get; set; } @@ -25,114 +20,378 @@ public class ChatSocketBehavior : WebSocketBehavior public static ChannelCryptoService? ChannelCryptoService { get; set; } public static SurrealDb.Net.SurrealDbClient? Db { get; set; } - /// - /// Routes incoming websocket messages to the appropriate chat handler. - /// - /// The websocket message event arguments. protected override void OnMessage(MessageEventArgs e) { var msg = e.Data; - Console.WriteLine(msg); - if (msg.StartsWith("REGISTER_KEY|")) - { - HandleRegisterKey(msg); - return; - } - - if (msg.StartsWith("AUTHENTICATE_USER")) - { - HandleAuth(msg); - return; - } - - if (msg == "GET_SERVER_KEY") - { - HandleGetServerKey(); - return; - } - - if (msg == "GET_CHANNELS") - { - HandleGetChannels(); - return; - } - - if (msg.StartsWith("GET_HISTORY|")) - { - HandleGetHistory(msg); - return; - } - - if (msg.StartsWith("RTC_JOIN_CHANNEL|")) - { - HandleRtcJoinChannel(msg); - return; - } - - if (msg.StartsWith("RTC_LEAVE_CHANNEL|")) - { - HandleRtcLeaveChannel(msg); - return; - } - - if (IsEncryptedRtcSignal(msg)) - { - HandleEncryptedRtcSignal(msg); - return; - } - - HandleEncryptedChatMessage(msg); - } - - private static bool IsEncryptedRtcSignal(string msg) - { try { using var doc = JsonDocument.Parse(msg); var root = doc.RootElement; - if (!root.TryGetProperty("Type", out var typeProp)) - return false; + if (root.TryGetProperty("Action", out var actionProp)) + { + var action = (WsAction)actionProp.GetInt32(); + var control = JsonSerializer.Deserialize(msg)!; + DispatchControl(action, control); + return; + } - var type = (SignalType)typeProp.GetInt32(); + if (root.TryGetProperty("Type", out var typeProp)) + { + var type = (SignalType)typeProp.GetInt32(); - return type == SignalType.EncryptedSignal; + switch (type) + { + case SignalType.EncryptedSignal: + HandleEncryptedRtcSignal(msg); + return; + case SignalType.ClientEncryptedChat: + HandleEncryptedChatMessage(msg); + return; + } + } } - catch + catch (Exception ex) { - return false; + Console.WriteLine($"WebSocket message error: session={ID}, error={ex.Message}"); } } - private async void HandleAuth(string msg) + private void DispatchControl(WsAction action, WsControlMessage control) { - var parts = msg.Split('|', 3); - - if (parts.Length < 3) + switch (action) { - Console.WriteLine("Invalid AUTHENTICATE_USERS payload."); + case WsAction.Authenticate: + HandleAuthenticate(control); + break; + case WsAction.RegisterKey: + HandleRegisterKey(control); + break; + case WsAction.GetServerKey: + HandleGetServerKey(); + break; + case WsAction.GetChannels: + HandleGetChannels(); + break; + case WsAction.GetHistory: + HandleGetHistory(control); + break; + case WsAction.RtcJoin: + HandleRtcJoinChannel(control); + break; + case WsAction.RtcLeave: + HandleRtcLeaveChannel(control); + break; + } + } + + protected override void OnClose(CloseEventArgs e) + { + ConnectedClientService.Unregister(ID); + RtcChannelPresenceService.RemoveSession(ID); + Console.WriteLine($"WebSocket closed: session={ID}, code={e.Code}, reason={e.Reason}"); + base.OnClose(e); + } + + protected override void OnError(ErrorEventArgs e) + { + Console.WriteLine($"WebSocket error: session={ID}, message={e.Message}"); + base.OnError(e); + } + + private async void HandleAuthenticate(WsControlMessage control) + { + if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.Token)) + { + Console.WriteLine("Invalid Authenticate payload."); return; } - var username = parts[1]; - var token = parts[2]; - - // HttpClient core = new HttpClient{BaseAddress = new Uri("http://127.0.0.1:1337")}; - HttpClient core = new HttpClient{BaseAddress = new Uri("http://192.168.1.85:1337")}; + using var core = new HttpClient { BaseAddress = new Uri("http://192.168.1.85:1337") }; core.DefaultRequestHeaders.Accept.Clear(); core.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); core.DefaultRequestHeaders.Add("User-Agent", "RelayServer"); - HttpResponseMessage response = await core.PostAsJsonAsync("/server/verify/user", new AuthUserVerify + var response = await core.PostAsJsonAsync("/server/verify/user", new AuthUserVerify { - Username = username, - Token = token + Username = control.Username, + Token = control.Token }); - - Console.WriteLine(response.Content.ReadAsStringAsync().Result); + Console.WriteLine($"Auth response for {control.Username}: {await response.Content.ReadAsStringAsync()}"); + + var result = new WsEventMessage { Event = WsEvent.Authenticated, Detail = control.Username }; + Send(JsonSerializer.Serialize(result)); } + + private void HandleRegisterKey(WsControlMessage control) + { + if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.PublicKey)) + { + Console.WriteLine("Invalid RegisterKey payload."); + return; + } + + if (ClientKeyService is null) + { + Console.WriteLine("ClientKeyService is not initialized."); + return; + } + + RegisterOrUpdateClientKeySync(control.Username, control.PublicKey); + ConnectedClientService.Register(ID, control.Username); + + var response = new WsEventMessage { Event = WsEvent.KeyRegistered, Detail = control.Username }; + Send(JsonSerializer.Serialize(response)); + } + + private void HandleGetServerKey() + { + if (string.IsNullOrWhiteSpace(ServerPublicKey)) + { + Console.WriteLine("Server public key is not initialized."); + return; + } + + var payload = new ServerPublicKeyMessage + { + Type = SignalType.ServerPublicKey, + PublicKey = ServerPublicKey + }; + + Send(JsonSerializer.Serialize(payload)); + } + + private void HandleGetChannels() + { + if (Db is null) + { + Console.WriteLine("Db is not initialized."); + return; + } + + var channels = GetChannelsSync() + .OrderBy(c => c.CreatedAt) + .Select(c => new ChannelItem + { + ChannelId = GetRecordId(c.Id), + Name = c.Name, + CreatedAt = c.CreatedAt + }) + .ToList(); + + var payload = new SocketChannelList + { + Type = SignalType.ChannelList, + Channels = channels + }; + + Send(JsonSerializer.Serialize(payload)); + } + + private void HandleGetHistory(WsControlMessage control) + { + if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.ChannelId)) + { + Console.WriteLine("Invalid GetHistory payload."); + return; + } + + if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey)) + { + Console.WriteLine("History dependencies are not initialized."); + return; + } + + var targetClient = GetClientPublicKeyByUsernameSync(control.Username); + + if (targetClient is null) + { + Console.WriteLine($"No public key found for history request user {control.Username}"); + return; + } + + var channelMessages = GetChannelMessagesSync() + .Where(m => m.ChannelId == control.ChannelId) + .OrderBy(m => m.CreatedAt) + .ToList(); + + Console.WriteLine($"Sending {channelMessages.Count} history messages to {control.Username}"); + + foreach (var dbMessage in channelMessages) + { + string plainText; + + try + { + plainText = ChannelCryptoService.Decrypt( + dbMessage.CipherText, + dbMessage.Nonce, + dbMessage.Tag, + ChannelDbKey + ); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to decrypt DB history row {dbMessage.Id}: {ex.Message}"); + continue; + } + + var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey); + + var outbound = new SocketEncryptedMessage + { + Type = SignalType.EncryptedChat, + SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId), + RecipientUsername = control.Username, + ChannelId = control.ChannelId, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + Send(JsonSerializer.Serialize(outbound)); + } + } + + private void HandleRtcJoinChannel(WsControlMessage control) + { + if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.ChannelId)) + { + Console.WriteLine("Invalid RtcJoin payload."); + return; + } + + RtcChannelPresenceService.SetUser(ID, control.Username); + RtcChannelPresenceService.JoinChannel(ID, control.ChannelId); + + Console.WriteLine($"RTC presence joined: session={ID}, user={control.Username}, channel={control.ChannelId}"); + } + + private void HandleRtcLeaveChannel(WsControlMessage control) + { + if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.ChannelId)) + { + Console.WriteLine("Invalid RtcLeave payload."); + return; + } + + if (RtcChannelPresenceService.IsInChannel(ID, control.ChannelId)) + RtcChannelPresenceService.LeaveChannel(ID); + + Console.WriteLine($"RTC presence left: session={ID}, user={control.Username}, channel={control.ChannelId}"); + } + + private void HandleEncryptedChatMessage(string msg) + { + SocketEncryptedMessage? clientPayload; + + try + { + clientPayload = JsonSerializer.Deserialize(msg); + } + catch + { + Console.WriteLine("Failed to parse encrypted client payload."); + return; + } + + if (clientPayload is null || clientPayload.Type != SignalType.ClientEncryptedChat) + return; + + if (!EnsureCoreReady() || !EnsureCryptoReady()) + return; + + string plainText; + + try + { + plainText = E2EeHelper.DecryptForRecipient( + new EncryptedPayload + { + CipherText = clientPayload.CipherText, + Nonce = clientPayload.Nonce, + Tag = clientPayload.Tag, + EncryptedKey = clientPayload.EncryptedKey + }, + ServerPrivateKey + ); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to decrypt client payload: {ex.Message}"); + return; + } + + Console.WriteLine($"Server decrypted message from {clientPayload.SenderUsername}: {plainText}"); + + try + { + var dbEncrypted = ChannelCryptoService!.Encrypt(plainText, ChannelDbKey); + + var savedMessage = CreateChannelMessageSync(new ChannelMessages + { + ChannelId = clientPayload.ChannelId, + SenderUserId = $"users:{clientPayload.SenderUsername.ToLower()}", + CipherText = dbEncrypted.cipherText, + Nonce = dbEncrypted.nonce, + Tag = dbEncrypted.tag, + CreatedAt = DateTime.UtcNow + }); + + Console.WriteLine($"Live message saved to DB: {JsonSerializer.Serialize(savedMessage)}"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to save live message to DB: {ex.Message}"); + return; + } + + var members = GetServerMembersSync(); + + foreach (var member in members) + { + var username = ExtractUsernameFromUserId(member.UserId); + var sessionIds = ConnectedClientService.GetSessionsForUser(username); + + if (!sessionIds.Any()) + continue; + + // Preserve the exact casing the client registered with + var properUsername = sessionIds + .Select(ConnectedClientService.GetUsernameForSession) + .FirstOrDefault(u => u is not null) ?? username; + + var clientKey = GetClientPublicKeyByUsernameSync(properUsername); + + if (clientKey is null) + continue; + + var encrypted = E2EeHelper.EncryptForRecipient(plainText, clientKey.PublicKey); + + Console.WriteLine($"Routing message from {clientPayload.SenderUsername} to {properUsername}"); + + var outbound = new SocketEncryptedMessage + { + Type = SignalType.EncryptedChat, + SenderUsername = clientPayload.SenderUsername, + RecipientUsername = properUsername, + ChannelId = clientPayload.ChannelId, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + var json = JsonSerializer.Serialize(outbound); + + foreach (var sessionId in sessionIds) + Sessions.SendTo(json, sessionId); + } + } + private void HandleEncryptedRtcSignal(string msg) { Console.WriteLine("RTC SIGNAL HIT"); @@ -211,31 +470,7 @@ public class ChatSocketBehavior : WebSocketBehavior Console.WriteLine($"Forwarded encrypted RTC signal from {clientPayload.SenderUsername} to channel {clientPayload.ChannelId}"); } - - /// - /// - /// - /// - protected override void OnClose(CloseEventArgs e) - { - RtcChannelPresenceService.RemoveSession(ID); - Console.WriteLine($"WebSocket closed: session={ID}, code={e.Code}, reason={e.Reason}"); - base.OnClose(e); - } - - protected override void OnError(ErrorEventArgs e) - { - Console.WriteLine($"WebSocket error: session={ID}, message={e.Message}"); - base.OnError(e); - } - /// - /// Extracts a display username from a stored user record id value. - /// - /// The stored sender user id. - /// - /// The extracted username when possible; otherwise, a fallback value. - /// private static string ExtractUsernameFromUserId(string senderUserId) { if (string.IsNullOrWhiteSpace(senderUserId)) @@ -245,262 +480,6 @@ public class ChatSocketBehavior : WebSocketBehavior return parts.Length == 2 ? parts[1] : senderUserId; } - /// - /// Registers or updates a client's public key from a websocket registration payload. - /// - /// The raw websocket registration message. - private void HandleRegisterKey(string msg) - { - var parts = msg.Split('|', 3); - - if (parts.Length < 3) - { - Console.WriteLine("Invalid REGISTER_KEY payload."); - return; - } - - var username = parts[1]; - var publicKey = parts[2]; - - if (ClientKeyService is null) - { - Console.WriteLine("ClientKeyService is not initialized."); - return; - } - - RegisterOrUpdateClientKeySync(username, publicKey); - - Send($"SERVER:REGISTERED_KEY:{username}"); - } - - /// - /// Sends the current list of channels to the connected websocket client. - /// - private void HandleGetChannels() - { - if (Db is null) - { - Console.WriteLine("Db is not initialized."); - return; - } - //TODO: Update to include ChannelType and Group String on channels - var channels = GetChannelsSync() - .OrderBy(c => c.CreatedAt) - .Select(c => new ChannelItem() - { - ChannelId = GetRecordId(c.Id), - Name = c.Name, - CreatedAt = c.CreatedAt - }) - .ToList(); - - var payload = new SocketChannelList - { - Type = SignalType.ChannelList, - Channels = channels - }; - - Send(JsonSerializer.Serialize(payload)); - } - - /// - /// Sends the server's public key to the connected websocket client. - /// - private void HandleGetServerKey() - { - if (string.IsNullOrWhiteSpace(ServerPublicKey)) - { - Console.WriteLine("Server public key is not initialized."); - return; - } - - var payload = new ServerPublicKeyMessage - { - Type = SignalType.ServerPublicKey, - PublicKey = ServerPublicKey - }; - - Send(JsonSerializer.Serialize(payload)); - } - - /// - /// Decrypts an incoming encrypted chat payload, stores it in the database, - /// and rebroadcasts it to connected clients encrypted with each client's public key. - /// - /// The raw encrypted chat websocket message. - private void HandleEncryptedChatMessage(string msg) - { - SocketEncryptedMessage? clientPayload; - - try - { - clientPayload = JsonSerializer.Deserialize(msg); - } - catch - { - Console.WriteLine("Failed to parse encrypted client payload."); - return; - } - - if (clientPayload is null || clientPayload.Type != SignalType.ClientEncryptedChat) - return; - - if (!EnsureCoreReady() || !EnsureCryptoReady()) - return; - - string plainText; - - try - { - plainText = E2EeHelper.DecryptForRecipient( - new EncryptedPayload - { - CipherText = clientPayload.CipherText, - Nonce = clientPayload.Nonce, - Tag = clientPayload.Tag, - EncryptedKey = clientPayload.EncryptedKey - }, - ServerPrivateKey - ); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to decrypt client payload: {ex.Message}"); - return; - } - - Console.WriteLine($"Server decrypted message from {clientPayload.SenderUsername}: {plainText}"); - try - { - var dbEncrypted = ChannelCryptoService.Encrypt(plainText, ChannelDbKey); - - var savedMessage = CreateChannelMessageSync(new ChannelMessages - { - ChannelId = clientPayload.ChannelId, - SenderUserId = $"users:{clientPayload.SenderUsername.ToLower()}", - CipherText = dbEncrypted.cipherText, - Nonce = dbEncrypted.nonce, - Tag = dbEncrypted.tag, - CreatedAt = DateTime.UtcNow - }); - - Console.WriteLine($"Live message saved to DB: {JsonSerializer.Serialize(savedMessage)}"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to save live message to DB: {ex.Message}"); - return; - } - - var allKeys = GetAllClientPublicKeysSync(); - - foreach (var client in allKeys) - { - var encrypted = E2EeHelper.EncryptForRecipient(plainText, client.PublicKey); - - Console.WriteLine($"Encrypting outbound message from {clientPayload.SenderUsername} for {client.Username}"); - - var outbound = new SocketEncryptedMessage - { - Type = SignalType.EncryptedChat, - SenderUsername = clientPayload.SenderUsername, - RecipientUsername = client.Username, - ChannelId = clientPayload.ChannelId, - CipherText = encrypted.CipherText, - Nonce = encrypted.Nonce, - Tag = encrypted.Tag, - EncryptedKey = encrypted.EncryptedKey - }; - - Sessions.Broadcast(JsonSerializer.Serialize(outbound)); - } - } - - /// - /// Loads stored channel history for a specific user and channel, decrypts it from - /// database storage format, and sends it back encrypted for the requesting client. - /// - /// The raw history request websocket message. - private void HandleGetHistory(string msg) - { - var parts = msg.Split('|', 3); - - if (parts.Length < 3) - { - Console.WriteLine("Invalid GET_HISTORY payload."); - return; - } - - var username = parts[1]; - var channelId = parts[2]; - - if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey)) - { - Console.WriteLine("History dependencies are not initialized."); - return; - } - - var targetClient = GetClientPublicKeyByUsernameSync(username); - - if (targetClient is null) - { - Console.WriteLine($"No public key found for history request user {username}"); - return; - } - - var allMessages = GetChannelMessagesSync(); - - var channelMessages = allMessages - .Where(m => m.ChannelId == channelId) - .OrderBy(m => m.CreatedAt) - .ToList(); - - Console.WriteLine($"Sending {channelMessages.Count} history messages to {username}"); - - foreach (var dbMessage in channelMessages) - { - string plainText; - - try - { - plainText = ChannelCryptoService.Decrypt( - dbMessage.CipherText, - dbMessage.Nonce, - dbMessage.Tag, - ChannelDbKey - ); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to decrypt DB history row {dbMessage.Id}: {ex.Message}"); - continue; - } - - var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey); - - var outbound = new SocketEncryptedMessage - { - Type = SignalType.EncryptedChat, - SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId), - RecipientUsername = username, - ChannelId = channelId, - CipherText = encrypted.CipherText, - Nonce = encrypted.Nonce, - Tag = encrypted.Tag, - EncryptedKey = encrypted.EncryptedKey - }; - - Send(JsonSerializer.Serialize(outbound)); - } - } - - /// - /// Converts a SurrealDB record id object into a table:id string representation. - /// - /// The raw record id object. - /// - /// A formatted record id string, or an empty string if the input is null. - /// private static string GetRecordId(object? id) { if (id is null) @@ -509,7 +488,6 @@ public class ChatSocketBehavior : WebSocketBehavior var json = JsonSerializer.Serialize(id); using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; var recordId = root.GetProperty("Id").GetString() ?? string.Empty; @@ -517,12 +495,7 @@ public class ChatSocketBehavior : WebSocketBehavior return $"{table}:{recordId}"; } - - /// - /// Synchronously registers or updates a stored client public key using the async key service. - /// - /// The client username. - /// The client's public key. + private void RegisterOrUpdateClientKeySync(string username, string publicKey) { Task.Run(async () => await ClientKeyService!.RegisterOrUpdateKeyAsync(username, publicKey)) @@ -530,10 +503,6 @@ public class ChatSocketBehavior : WebSocketBehavior .GetResult(); } - /// - /// Synchronously loads all channels from the database. - /// - /// A list of channel records. private List GetChannelsSync() { return Task.Run(async () => await Db!.Select("channels")) @@ -542,13 +511,6 @@ public class ChatSocketBehavior : WebSocketBehavior .ToList(); } - /// - /// Synchronously gets the stored public key record for the specified user. - /// - /// The username to look up. - /// - /// The matching client public key record, or null if none exists. - /// private ClientPublicKeys? GetClientPublicKeyByUsernameSync(string username) { return Task.Run(async () => await ClientKeyService!.GetByUsernameAsync(username)) @@ -556,21 +518,6 @@ public class ChatSocketBehavior : WebSocketBehavior .GetResult(); } - /// - /// Synchronously loads all stored client public key records. - /// - /// A list of all client public key records. - private List GetAllClientPublicKeysSync() - { - return Task.Run(async () => await ClientKeyService!.GetAllAsync()) - .GetAwaiter() - .GetResult(); - } - - /// - /// Synchronously loads all stored channel messages from the database. - /// - /// A list of channel message records. private List GetChannelMessagesSync() { return Task.Run(async () => await Db!.Select("channel_messages")) @@ -579,22 +526,21 @@ public class ChatSocketBehavior : WebSocketBehavior .ToList(); } - /// - /// Synchronously creates a new channel message record in the database. - /// - /// The message record to create. - /// The created channel message record. private ChannelMessages CreateChannelMessageSync(ChannelMessages message) { return Task.Run(async () => await Db!.Create("channel_messages", message)) .GetAwaiter() .GetResult(); } - - /// - /// - /// - /// + + private List GetServerMembersSync() + { + return Task.Run(async () => await Db!.Select("server_members")) + .GetAwaiter() + .GetResult() + .ToList(); + } + private bool EnsureCoreReady() { if (ClientKeyService is null || Db is null) @@ -605,11 +551,7 @@ public class ChatSocketBehavior : WebSocketBehavior return true; } - - /// - /// - /// - /// + private bool EnsureCryptoReady() { if (string.IsNullOrWhiteSpace(ServerPrivateKey) || string.IsNullOrWhiteSpace(ChannelDbKey)) @@ -626,50 +568,4 @@ public class ChatSocketBehavior : WebSocketBehavior return true; } - - /// - /// - /// - /// - private void HandleRtcJoinChannel(string msg) - { - var parts = msg.Split('|', 3); - if (parts.Length < 3) - { - Console.WriteLine("Invalid RTC_JOIN_CHANNEL payload."); - return; - } - - var username = parts[1]; - var channelId = parts[2]; - - RtcChannelPresenceService.SetUser(ID, username); - RtcChannelPresenceService.JoinChannel(ID, channelId); - - Console.WriteLine($"RTC presence joined: session={ID}, user={username}, channel={channelId}"); - } - - /// - /// - /// - /// - private void HandleRtcLeaveChannel(string msg) - { - var parts = msg.Split('|', 3); - if (parts.Length < 3) - { - Console.WriteLine("Invalid RTC_LEAVE_CHANNEL payload."); - return; - } - - var username = parts[1]; - var channelId = parts[2]; - - if (RtcChannelPresenceService.IsInChannel(ID, channelId)) - { - RtcChannelPresenceService.LeaveChannel(ID); - } - - Console.WriteLine($"RTC presence left: session={ID}, user={username}, channel={channelId}"); - } -} \ No newline at end of file +} diff --git a/RelayServer/Services/Chat/ConnectedClientService.cs b/RelayServer/Services/Chat/ConnectedClientService.cs new file mode 100644 index 0000000..7cdf639 --- /dev/null +++ b/RelayServer/Services/Chat/ConnectedClientService.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; + +namespace RelayServer.Services.Chat; + +public static class ConnectedClientService +{ + private static readonly ConcurrentDictionary SessionToUsername = new(); + private static readonly ConcurrentDictionary> UsernameToSessions = + new(StringComparer.OrdinalIgnoreCase); + + public static void Register(string sessionId, string username) + { + if (SessionToUsername.TryGetValue(sessionId, out var oldUsername) && + !string.Equals(oldUsername, username, StringComparison.OrdinalIgnoreCase)) + { + RemoveSessionFromUsername(sessionId, oldUsername); + } + + SessionToUsername[sessionId] = username; + + var sessions = UsernameToSessions.GetOrAdd( + username, + _ => new HashSet(StringComparer.Ordinal)); + + lock (sessions) + sessions.Add(sessionId); + } + + public static void Unregister(string sessionId) + { + if (SessionToUsername.TryRemove(sessionId, out var username)) + RemoveSessionFromUsername(sessionId, username); + } + + public static IReadOnlyCollection GetSessionsForUser(string username) + { + if (UsernameToSessions.TryGetValue(username, out var sessions)) + { + lock (sessions) + return sessions.ToList(); + } + + return Array.Empty(); + } + + public static string? GetUsernameForSession(string sessionId) + { + return SessionToUsername.TryGetValue(sessionId, out var u) ? u : null; + } + + private static void RemoveSessionFromUsername(string sessionId, string username) + { + if (!UsernameToSessions.TryGetValue(username, out var sessions)) + return; + + lock (sessions) + { + sessions.Remove(sessionId); + if (sessions.Count == 0) + UsernameToSessions.TryRemove(username, out _); + } + } +} diff --git a/RelayShared/Services/WsControlMessage.cs b/RelayShared/Services/WsControlMessage.cs new file mode 100644 index 0000000..dea2ba6 --- /dev/null +++ b/RelayShared/Services/WsControlMessage.cs @@ -0,0 +1,34 @@ +namespace RelayShared.Services; + +public enum WsAction +{ + Authenticate, + RegisterKey, + GetServerKey, + GetChannels, + GetHistory, + RtcJoin, + RtcLeave +} + +public enum WsEvent +{ + Authenticated, + KeyRegistered, + Error +} + +public sealed class WsControlMessage +{ + public WsAction Action { get; set; } + public string? Username { get; set; } + public string? Token { get; set; } + public string? ChannelId { get; set; } + public string? PublicKey { get; set; } +} + +public sealed class WsEventMessage +{ + public WsEvent Event { get; set; } + public string? Detail { get; set; } +}