From 6287f4d19bf1a14096a74ae993eaf61e413e92a3 Mon Sep 17 00:00:00 2001 From: RuKira Date: Wed, 1 Apr 2026 14:32:23 -0400 Subject: [PATCH] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --- RelayClient/Resources/Raw/wwwroot/index.js | 70 ++++++++++----- RelayServer/Services/ChatTest.cs | 99 ++++++++++++++++++++-- 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/RelayClient/Resources/Raw/wwwroot/index.js b/RelayClient/Resources/Raw/wwwroot/index.js index 70ce3f3..83dfcbe 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.js +++ b/RelayClient/Resources/Raw/wwwroot/index.js @@ -36,20 +36,10 @@ async function ensurePeerConnection() { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }); - peerConnection.onicecandidate = async (event) => { - if (!event.candidate || !currentChannelId || !currentUsername) return; - - const payload = { - type: "rtc_ice_candidate", - from: currentUsername, - channelId: currentChannelId, - candidate: event.candidate.candidate, - sdpMid: event.candidate.sdpMid, - sdpMLineIndex: event.candidate.sdpMLineIndex - }; - - LogMessage("Sending ICE candidate"); - await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); + peerConnection.onicecandidate = (event) => { + if (event.candidate) { + LogMessage("ICE candidate gathered"); + } }; peerConnection.ontrack = (event) => { @@ -180,17 +170,13 @@ async function joinChannelCall() { LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); - const offer = await peerConnection.createOffer(); - await peerConnection.setLocalDescription(offer); - const payload = { - type: "rtc_offer", + type: "rtc_join", from: currentUsername, - channelId: currentChannelId, - sdp: offer.sdp + channelId: currentChannelId }; - LogMessage("Sending offer to channel " + currentChannelId); + LogMessage("Requesting join for channel " + currentChannelId); await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); } catch (err) { LogMessage("joinChannelCall failed: " + err); @@ -254,6 +240,30 @@ async function handleRtcSignal(rawJson) { await ensurePeerConnection(); + if (msg.type === "rtc_join_state") { + if (msg.isInitiator) { + LogMessage("No active call found. Becoming initiator."); + + const offer = await peerConnection.createOffer(); + await peerConnection.setLocalDescription(offer); + await waitForIceGatheringComplete(peerConnection); + + const payload = { + type: "rtc_offer", + from: currentUsername, + channelId: currentChannelId, + sdp: peerConnection.localDescription.sdp + }; + + LogMessage("Sending offer to channel " + currentChannelId); + await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); + } else { + LogMessage("Active call exists. Waiting for stored offer."); + } + + return; + } + if (msg.type === "rtc_offer") { LogMessage("Incoming channel call offer from " + msg.from); await ensureLocalMedia(); @@ -268,12 +278,13 @@ async function handleRtcSignal(rawJson) { const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); + await waitForIceGatheringComplete(peerConnection); const payload = { type: "rtc_answer", from: currentUsername, channelId: msg.channelId, - sdp: answer.sdp + sdp: peerConnection.localDescription.sdp }; LogMessage("Sending answer to channel " + msg.channelId); @@ -360,6 +371,21 @@ async function loadDevices() { } } +async function waitForIceGatheringComplete(pc) { + if (pc.iceGatheringState === "complete") return; + + await new Promise(resolve => { + function checkState() { + if (pc.iceGatheringState === "complete") { + pc.removeEventListener("icegatheringstatechange", checkState); + resolve(); + } + } + + pc.addEventListener("icegatheringstatechange", checkState); + }); +} + window.handleRtcSignal = handleRtcSignal; window.addEventListener("HybridWebViewMessageReceived", function (e) { diff --git a/RelayServer/Services/ChatTest.cs b/RelayServer/Services/ChatTest.cs index f90004e..26cade0 100644 --- a/RelayServer/Services/ChatTest.cs +++ b/RelayServer/Services/ChatTest.cs @@ -12,6 +12,8 @@ public class ChatTest : WebSocketBehavior public static string? ServerPrivateKey { get; set; } public static string? ChannelDbKey { get; set; } public static SurrealDb.Net.SurrealDbClient? Db { get; set; } + private static readonly Dictionary ActiveRtcOffersByChannel = new(); + private static readonly HashSet ActiveRtcChannels = new(); protected override void OnMessage(MessageEventArgs e) { @@ -41,7 +43,7 @@ public class ChatTest : WebSocketBehavior HandleGetHistory(msg); return; } - + SocketRtcSignalMessage? rtcProbe = null; try { @@ -60,7 +62,7 @@ public class ChatTest : WebSocketBehavior HandleEncryptedClientMessage(msg); } - + private static string ExtractUsernameFromUserId(string senderUserId) { if (string.IsNullOrWhiteSpace(senderUserId)) @@ -89,10 +91,8 @@ public class ChatTest : WebSocketBehavior return; } - Task.Run(async () => - { - await ClientKeyService.RegisterOrUpdateKeyAsync(username, publicKey); - }).GetAwaiter().GetResult(); + Task.Run(async () => { await ClientKeyService.RegisterOrUpdateKeyAsync(username, publicKey); }).GetAwaiter() + .GetResult(); Send($"SERVER:REGISTERED_KEY:{username}"); } @@ -125,7 +125,7 @@ public class ChatTest : WebSocketBehavior Send(JsonSerializer.Serialize(payload)); } - + private void HandleGetServerKey() { if (string.IsNullOrWhiteSpace(ServerPublicKey)) @@ -322,7 +322,7 @@ public class ChatTest : WebSocketBehavior Send(JsonSerializer.Serialize(outbound)); } } - + private static string GetRecordId(object? id) { if (id is null) @@ -339,7 +339,7 @@ public class ChatTest : WebSocketBehavior return $"{table}:{recordId}"; } - + private void HandleEncryptedRtcSignal(string msg) { SocketRtcSignalMessage? clientPayload; @@ -384,10 +384,91 @@ public class ChatTest : WebSocketBehavior return; } + RtcSignalMessage? rtcSignal; + + try + { + rtcSignal = JsonSerializer.Deserialize(plainJson); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to parse decrypted RTC signal JSON: {ex.Message}"); + return; + } + + if (rtcSignal is null) + return; + var allKeys = Task.Run(async () => await ClientKeyService.GetAllAsync()) .GetAwaiter() .GetResult(); + if (rtcSignal.Type == "rtc_join") + { + var joinState = new + { + type = "rtc_join_state", + from = "server", + channelId = rtcSignal.ChannelId, + isInitiator = !ActiveRtcOffersByChannel.ContainsKey(rtcSignal.ChannelId) + }; + + var senderClient = allKeys.FirstOrDefault(x => x.Username == clientPayload.SenderUsername); + if (senderClient is null) + { + Console.WriteLine($"No client key found for RTC join sender {clientPayload.SenderUsername}"); + return; + } + + var joinStateJson = JsonSerializer.Serialize(joinState); + var encryptedJoinState = E2EeHelper.EncryptForRecipient(joinStateJson, senderClient.PublicKey); + + var joinStateOutbound = new SocketRtcSignalMessage + { + Type = "encrypted_rtc_signal", + SenderUsername = "server", + ChannelId = clientPayload.ChannelId, + CipherText = encryptedJoinState.CipherText, + Nonce = encryptedJoinState.Nonce, + Tag = encryptedJoinState.Tag, + EncryptedKey = encryptedJoinState.EncryptedKey + }; + + Send(JsonSerializer.Serialize(joinStateOutbound)); + + if (ActiveRtcOffersByChannel.TryGetValue(rtcSignal.ChannelId, out var storedOfferJson)) + { + var encryptedStoredOffer = E2EeHelper.EncryptForRecipient(storedOfferJson, senderClient.PublicKey); + + var storedOfferOutbound = new SocketRtcSignalMessage + { + Type = "encrypted_rtc_signal", + SenderUsername = "server", + ChannelId = clientPayload.ChannelId, + CipherText = encryptedStoredOffer.CipherText, + Nonce = encryptedStoredOffer.Nonce, + Tag = encryptedStoredOffer.Tag, + EncryptedKey = encryptedStoredOffer.EncryptedKey + }; + + Send(JsonSerializer.Serialize(storedOfferOutbound)); + } + + return; + } + + if (rtcSignal.Type == "rtc_offer") + { + ActiveRtcOffersByChannel[rtcSignal.ChannelId] = plainJson; + ActiveRtcChannels.Add(rtcSignal.ChannelId); + } + + if (rtcSignal.Type == "rtc_leave") + { + ActiveRtcOffersByChannel.Remove(rtcSignal.ChannelId); + ActiveRtcChannels.Remove(rtcSignal.ChannelId); + } + foreach (var client in allKeys) { if (client.Username == clientPayload.SenderUsername)