Update: Seems everything is working now?
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<string>? RawMessageReceived;
|
||||
public event Action<SocketChannelList>? ChannelListReceived;
|
||||
public event Action<SocketEncryptedMessage>? EncryptedChatReceived;
|
||||
public event Action<SocketRtcSignalMessage>? 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>(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<SocketChannelList>(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<SocketRtcSignalMessage>(e.Data);
|
||||
if (payload is not null)
|
||||
EncryptedRtcSignalReceived?.Invoke(payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +169,6 @@ public sealed class RelaySocketClient
|
||||
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||
if (payload is not null)
|
||||
EncryptedChatReceived?.Invoke(payload);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Handles websocket-based chat operations including client key registration,
|
||||
/// server key retrieval, channel listing, channel history loading, and encrypted
|
||||
/// channel message relay.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Routes incoming websocket messages to the appropriate chat handler.
|
||||
/// </summary>
|
||||
/// <param name="e">The websocket message event arguments.</param>
|
||||
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;
|
||||
|
||||
var type = (SignalType)typeProp.GetInt32();
|
||||
|
||||
return type == SignalType.EncryptedSignal;
|
||||
}
|
||||
catch
|
||||
if (root.TryGetProperty("Action", out var actionProp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleAuth(string msg)
|
||||
{
|
||||
var parts = msg.Split('|', 3);
|
||||
|
||||
if (parts.Length < 3)
|
||||
{
|
||||
Console.WriteLine("Invalid AUTHENTICATE_USERS payload.");
|
||||
var action = (WsAction)actionProp.GetInt32();
|
||||
var control = JsonSerializer.Deserialize<WsControlMessage>(msg)!;
|
||||
DispatchControl(action, control);
|
||||
return;
|
||||
}
|
||||
|
||||
var username = parts[1];
|
||||
var token = parts[2];
|
||||
if (root.TryGetProperty("Type", out var typeProp))
|
||||
{
|
||||
var type = (SignalType)typeProp.GetInt32();
|
||||
|
||||
// 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")};
|
||||
switch (type)
|
||||
{
|
||||
case SignalType.EncryptedSignal:
|
||||
HandleEncryptedRtcSignal(msg);
|
||||
return;
|
||||
case SignalType.ClientEncryptedChat:
|
||||
HandleEncryptedChatMessage(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"WebSocket message error: session={ID}, error={ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchControl(WsAction action, WsControlMessage control)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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<SocketEncryptedMessage>(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");
|
||||
@@ -212,30 +471,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
Console.WriteLine($"Forwarded encrypted RTC signal from {clientPayload.SenderUsername} to channel {clientPayload.ChannelId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a display username from a stored user record id value.
|
||||
/// </summary>
|
||||
/// <param name="senderUserId">The stored sender user id.</param>
|
||||
/// <returns>
|
||||
/// The extracted username when possible; otherwise, a fallback value.
|
||||
/// </returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers or updates a client's public key from a websocket registration payload.
|
||||
/// </summary>
|
||||
/// <param name="msg">The raw websocket registration message.</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the current list of channels to the connected websocket client.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server's public key to the connected websocket client.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts an incoming encrypted chat payload, stores it in the database,
|
||||
/// and rebroadcasts it to connected clients encrypted with each client's public key.
|
||||
/// </summary>
|
||||
/// <param name="msg">The raw encrypted chat websocket message.</param>
|
||||
private void HandleEncryptedChatMessage(string msg)
|
||||
{
|
||||
SocketEncryptedMessage? clientPayload;
|
||||
|
||||
try
|
||||
{
|
||||
clientPayload = JsonSerializer.Deserialize<SocketEncryptedMessage>(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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="msg">The raw history request websocket message.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a SurrealDB record id object into a table:id string representation.
|
||||
/// </summary>
|
||||
/// <param name="id">The raw record id object.</param>
|
||||
/// <returns>
|
||||
/// A formatted record id string, or an empty string if the input is null.
|
||||
/// </returns>
|
||||
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;
|
||||
@@ -518,11 +496,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
return $"{table}:{recordId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously registers or updates a stored client public key using the async key service.
|
||||
/// </summary>
|
||||
/// <param name="username">The client username.</param>
|
||||
/// <param name="publicKey">The client's public key.</param>
|
||||
private void RegisterOrUpdateClientKeySync(string username, string publicKey)
|
||||
{
|
||||
Task.Run(async () => await ClientKeyService!.RegisterOrUpdateKeyAsync(username, publicKey))
|
||||
@@ -530,10 +503,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously loads all channels from the database.
|
||||
/// </summary>
|
||||
/// <returns>A list of channel records.</returns>
|
||||
private List<Channels> GetChannelsSync()
|
||||
{
|
||||
return Task.Run(async () => await Db!.Select<Channels>("channels"))
|
||||
@@ -542,13 +511,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously gets the stored public key record for the specified user.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to look up.</param>
|
||||
/// <returns>
|
||||
/// The matching client public key record, or null if none exists.
|
||||
/// </returns>
|
||||
private ClientPublicKeys? GetClientPublicKeyByUsernameSync(string username)
|
||||
{
|
||||
return Task.Run(async () => await ClientKeyService!.GetByUsernameAsync(username))
|
||||
@@ -556,21 +518,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously loads all stored client public key records.
|
||||
/// </summary>
|
||||
/// <returns>A list of all client public key records.</returns>
|
||||
private List<ClientPublicKeys> GetAllClientPublicKeysSync()
|
||||
{
|
||||
return Task.Run(async () => await ClientKeyService!.GetAllAsync())
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously loads all stored channel messages from the database.
|
||||
/// </summary>
|
||||
/// <returns>A list of channel message records.</returns>
|
||||
private List<ChannelMessages> GetChannelMessagesSync()
|
||||
{
|
||||
return Task.Run(async () => await Db!.Select<ChannelMessages>("channel_messages"))
|
||||
@@ -579,11 +526,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously creates a new channel message record in the database.
|
||||
/// </summary>
|
||||
/// <param name="message">The message record to create.</param>
|
||||
/// <returns>The created channel message record.</returns>
|
||||
private ChannelMessages CreateChannelMessageSync(ChannelMessages message)
|
||||
{
|
||||
return Task.Run(async () => await Db!.Create("channel_messages", message))
|
||||
@@ -591,10 +533,14 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<ServerMembers> GetServerMembersSync()
|
||||
{
|
||||
return Task.Run(async () => await Db!.Select<ServerMembers>("server_members"))
|
||||
.GetAwaiter()
|
||||
.GetResult()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool EnsureCoreReady()
|
||||
{
|
||||
if (ClientKeyService is null || Db is null)
|
||||
@@ -606,10 +552,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool EnsureCryptoReady()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ServerPrivateKey) || string.IsNullOrWhiteSpace(ChannelDbKey))
|
||||
@@ -626,50 +568,4 @@ public class ChatSocketBehavior : WebSocketBehavior
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
63
RelayServer/Services/Chat/ConnectedClientService.cs
Normal file
63
RelayServer/Services/Chat/ConnectedClientService.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RelayServer.Services.Chat;
|
||||
|
||||
public static class ConnectedClientService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, string> SessionToUsername = new();
|
||||
private static readonly ConcurrentDictionary<string, HashSet<string>> 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<string>(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<string> GetSessionsForUser(string username)
|
||||
{
|
||||
if (UsernameToSessions.TryGetValue(username, out var sessions))
|
||||
{
|
||||
lock (sessions)
|
||||
return sessions.ToList();
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
RelayShared/Services/WsControlMessage.cs
Normal file
34
RelayShared/Services/WsControlMessage.cs
Normal file
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user