From 1220654656c9ea84dd9a6d21717e871465e6e23e Mon Sep 17 00:00:00 2001 From: RuKira Date: Mon, 27 Apr 2026 10:01:02 -0400 Subject: [PATCH] Setup new services required for change --- RelayClient/Services/RelaySocketClient.cs | 127 ++++++++++++ RelayClient/Services/RtcBridgeService.cs | 228 ++++++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 RelayClient/Services/RelaySocketClient.cs create mode 100644 RelayClient/Services/RtcBridgeService.cs diff --git a/RelayClient/Services/RelaySocketClient.cs b/RelayClient/Services/RelaySocketClient.cs new file mode 100644 index 0000000..49c0f99 --- /dev/null +++ b/RelayClient/Services/RelaySocketClient.cs @@ -0,0 +1,127 @@ +using System.Text.Json; +using RelayClient.Crypto; +using RelayShared.Services; +using WebSocketSharp; + +namespace RelayClient.Services; + +public sealed class RelaySocketClient +{ + private readonly string _username; + private readonly WebSocket _socket; + + public string? ServerPublicKey { get; private set; } + + public event Action? RawMessageReceived; + public event Action? ChannelListReceived; + public event Action? EncryptedChatReceived; + public event Action? EncryptedRtcSignalReceived; + public event Action? ServerPublicKeyReceived; + public event Action? Log; + + public RelaySocketClient(string username, string url = "ws://localhost:1337/") + { + _username = username; + _socket = new WebSocket(url); + _socket.OnMessage += OnMessage; + } + + public void Connect() + { + _socket.Connect(); + + var publicKey = KeyStorage.LoadPublicKey(_username); + + SendRaw($"REGISTER_KEY|{_username}|{publicKey}"); + SendRaw("GET_SERVER_KEY"); + SendRaw("GET_CHANNELS"); + } + + public void SendRaw(string message) + { + if (_socket.ReadyState == WebSocketState.Open) + _socket.Send(message); + } + + public void SendJson(T payload) + { + SendRaw(JsonSerializer.Serialize(payload)); + } + + public void Disconnect() + { + _socket.OnMessage -= OnMessage; + + if (_socket.ReadyState == WebSocketState.Open) + _socket.Close(); + } + + 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 + { + using var doc = JsonDocument.Parse(e.Data); + var root = doc.RootElement; + + if (!root.TryGetProperty("Type", out var typeElement)) + return; + + var type = (SignalType)typeElement.GetInt32(); + + switch (type) + { + case SignalType.ChannelList: + { + var channelList = JsonSerializer.Deserialize(e.Data); + if (channelList is not null) + ChannelListReceived?.Invoke(channelList); + + return; + } + + case SignalType.ServerPublicKey: + { + var serverKeyMessage = JsonSerializer.Deserialize(e.Data); + if (serverKeyMessage is not null) + { + ServerPublicKey = serverKeyMessage.PublicKey; + ServerPublicKeyReceived?.Invoke(serverKeyMessage.PublicKey); + } + + return; + } + + case SignalType.EncryptedSignal: + { + var payload = JsonSerializer.Deserialize(e.Data); + if (payload is not null) + EncryptedRtcSignalReceived?.Invoke(payload); + + return; + } + + case SignalType.EncryptedChat: + { + var payload = JsonSerializer.Deserialize(e.Data); + if (payload is not null) + EncryptedChatReceived?.Invoke(payload); + + return; + } + } + } + catch (Exception ex) + { + 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 new file mode 100644 index 0000000..7c3f3e2 --- /dev/null +++ b/RelayClient/Services/RtcBridgeService.cs @@ -0,0 +1,228 @@ +using System.Text.Json; +using RelayClient.Crypto; +using RelayShared.Rtc; +using RelayShared.Services; + +namespace RelayClient.Services; + +public sealed class RtcBridgeService +{ + private readonly string _username; + private readonly RelaySocketClient _socket; + private readonly HybridWebView _hybridWebView; + private readonly Func _getCurrentChannelId; + private readonly Action _sendRawToWebView; + + public RtcBridgeService( + string username, + RelaySocketClient socket, + HybridWebView hybridWebView, + Func getCurrentChannelId, + Action sendRawToWebView) + { + _username = username; + _socket = socket; + _hybridWebView = hybridWebView; + _getCurrentChannelId = getCurrentChannelId; + _sendRawToWebView = sendRawToWebView; + } + + public Task JoinRtcChannel() + { + var channelId = _getCurrentChannelId(); + + if (string.IsNullOrWhiteSpace(channelId)) + return Task.CompletedTask; + + _socket.SendRaw($"RTC_JOIN_CHANNEL|{_username}|{channelId}"); + return Task.CompletedTask; + } + + public void LeaveRtcChannel() + { + var channelId = _getCurrentChannelId(); + + if (string.IsNullOrWhiteSpace(channelId)) + return; + + _socket.SendRaw($"RTC_LEAVE_CHANNEL|{_username}|{channelId}"); + } + + public void SendRtcSignal(string json) + { + if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey)) + { + _sendRawToWebView("SendRtcSignal failed: server public key not loaded."); + return; + } + + RtcSignalMessage? rtcSignal; + + try + { + rtcSignal = JsonSerializer.Deserialize(json); + } + catch (Exception ex) + { + _sendRawToWebView("SendRtcSignal failed to parse RTC signal: " + ex.Message); + return; + } + + if (rtcSignal is null) + return; + + rtcSignal.ChannelId ??= _getCurrentChannelId(); + rtcSignal.From ??= _username; + + if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId)) + { + _sendRawToWebView("SendRtcSignal failed: missing channel id."); + return; + } + + var outgoingJson = JsonSerializer.Serialize(rtcSignal); + + try + { + var encrypted = E2EeHelper.EncryptForRecipient(outgoingJson, _socket.ServerPublicKey); + + var payload = new SocketRtcSignalMessage + { + Type = SignalType.EncryptedSignal, + SenderUsername = _username, + ChannelId = rtcSignal.ChannelId, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + _socket.SendJson(payload); + + _sendRawToWebView($"SendRtcSignal sent: {rtcSignal.Type} -> {rtcSignal.To}"); + } + catch (Exception ex) + { + _sendRawToWebView("SendRtcSignal failed: " + ex.Message); + } + } + + public async Task GetRtcParticipants() + { + var channelId = _getCurrentChannelId(); + + if (string.IsNullOrWhiteSpace(channelId)) + return "[]"; + + var participants = await ServerAPI.GetRtcParticipantsAsync(channelId); + return JsonSerializer.Serialize(participants ?? []); + } + + public async Task HandleIncomingRtcSignalAsync(SocketRtcSignalMessage payload) + { + var currentChannelId = _getCurrentChannelId(); + + if (payload.ChannelId != currentChannelId) + return; + + if (payload.SenderUsername == _username) + return; + + string decryptedJson; + + try + { + var privateKey = KeyStorage.LoadPrivateKey(_username); + + decryptedJson = E2EeHelper.DecryptForRecipient( + new EncryptedPayload + { + CipherText = payload.CipherText, + Nonce = payload.Nonce, + Tag = payload.Tag, + EncryptedKey = payload.EncryptedKey + }, + privateKey + ); + } + catch (Exception ex) + { + _sendRawToWebView("RTC decrypt failed: " + ex.Message); + return; + } + + RtcSignalMessage? rtcSignal; + + try + { + rtcSignal = JsonSerializer.Deserialize(decryptedJson); + } + catch (Exception ex) + { + _sendRawToWebView("RTC signal parse failed: " + ex.Message); + return; + } + + if (rtcSignal is null) + return; + + if (!string.IsNullOrWhiteSpace(rtcSignal.To) && + !string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase)) + { + _sendRawToWebView($"Ignoring RTC signal meant for {rtcSignal.To}"); + return; + } + + _sendRawToWebView("Received encrypted RTC signal: " + decryptedJson); + + await SendRtcSignalToJsAsync(decryptedJson); + } + + public Task PushRtcContextToJsAsync() + { + MainThread.BeginInvokeOnMainThread(async () => + { + var usernameJson = JsonSerializer.Serialize(_username); + var channelIdJson = JsonSerializer.Serialize(_getCurrentChannelId()); + + await _hybridWebView.EvaluateJavaScriptAsync($"window.setUsername({usernameJson})"); + await _hybridWebView.EvaluateJavaScriptAsync($"window.setChannelId({channelIdJson})"); + }); + + return Task.CompletedTask; + } + + private Task SendRtcSignalToJsAsync(string rawJson) + { + MainThread.BeginInvokeOnMainThread(async () => + { + try + { + var jsArg = JsonSerializer.Serialize(rawJson); + + await _hybridWebView.EvaluateJavaScriptAsync($@" + try {{ + window.HybridWebView.SendRawMessage('C# eval entered'); + + if (!window.RelaySocket) {{ + window.HybridWebView.SendRawMessage('window.RelaySocket missing'); + }} else if (typeof window.RelaySocket.receiveRtcSignal !== 'function') {{ + window.HybridWebView.SendRawMessage('RelaySocket.receiveRtcSignal missing'); + }} else {{ + window.HybridWebView.SendRawMessage('Calling RelaySocket.receiveRtcSignal'); + window.RelaySocket.receiveRtcSignal({jsArg}); + }} + }} catch (err) {{ + window.HybridWebView.SendRawMessage('RTC JS dispatch failed: ' + err); + }} + "); + } + catch (Exception ex) + { + _sendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message); + } + }); + + return Task.CompletedTask; + } +} \ No newline at end of file