diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index 2f8d1c6..5cef8f1 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -189,6 +189,20 @@ public partial class MainPage : ContentPage }, privateKey ); + + var rtcSignal = JsonSerializer.Deserialize(decryptedJson); + + if (rtcSignal is null) + return; + + if (!string.IsNullOrWhiteSpace(rtcSignal.To) && + !string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase)) + { + SafeSendRawToWebView($"Ignoring RTC signal meant for {rtcSignal.To}"); + return; + } + + SafeSendRawToWebView("Received encrypted RTC signal: " + decryptedJson); MainThread.BeginInvokeOnMainThread(async () => { @@ -438,20 +452,20 @@ public partial class MainPage : ContentPage } #region RTC Functions - public async Task JoinRtcChannel() + public async Task JoinRtcChannel() { if (string.IsNullOrWhiteSpace(_currentChannelId)) - return false; + return; //false; _wsc.Send($"RTC_JOIN_CHANNEL|{_username}|{_currentChannelId}"); // SafeSendRawToWebView($"Attempting to join RTC Channel {_currentChannelName} | {_currentChannelId} "); - bool active = await ServerAPI.GetIsChannelActiveAsync(_currentChannelId); + //bool active = await ServerAPI.GetIsChannelActiveAsync(_currentChannelId); - SafeSendRawToWebView($"Rtc Channel {_currentChannelName} | {_currentChannelId} is active: {active}"); + //SafeSendRawToWebView($"Rtc Channel {_currentChannelName} | {_currentChannelId} is active: {active}"); - return active; + return; //active; } public void LeaveRtcChannel() @@ -566,10 +580,36 @@ public partial class MainPage : ContentPage } } - private async Task SendRtcSignalToJsAsync(string rawJson) + private Task SendRtcSignalToJsAsync(string rawJson) { - var jsArg = JsonSerializer.Serialize(rawJson); - await hybridWebView.EvaluateJavaScriptAsync($"window.handleRtcSignal({jsArg})"); + MainThread.BeginInvokeOnMainThread(async () => + { + try + { + SafeSendRawToWebView("Dispatching RTC signal to JS"); + + var jsArg = JsonSerializer.Serialize(rawJson); + + await hybridWebView.EvaluateJavaScriptAsync($@" + window.HybridWebView.SendRawMessage('JS dispatch wrapper hit'); + const fn = window.handleRtcSignal || window.dispatchRtcSignal; + if (!fn) {{ + window.HybridWebView.SendRawMessage('No RTC signal handler found on window'); + }} else {{ + window.HybridWebView.SendRawMessage('Calling RTC signal handler'); + fn({jsArg}); + }} + "); + + SafeSendRawToWebView("RTC signal dispatched to JS"); + } + catch (Exception ex) + { + SafeSendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message); + } + }); + + return Task.CompletedTask; } //Remove? private async Task PushRtcContextToJsAsync() @@ -585,41 +625,63 @@ public partial class MainPage : ContentPage public void SendRtcSignal(string json) { + SafeSendRawToWebView("SendRtcSignal entered: " + json); + if (string.IsNullOrWhiteSpace(_serverPublicKey)) { - Console.WriteLine("Server public key not loaded yet."); + SafeSendRawToWebView("SendRtcSignal failed: server public key not loaded."); return; } RtcSignalMessage? rtcSignal; + try { rtcSignal = JsonSerializer.Deserialize(json); } catch (Exception ex) { - Console.WriteLine($"Failed to parse RTC signal from JS: {ex.Message}"); + SafeSendRawToWebView("SendRtcSignal failed to parse RTC signal: " + ex.Message); return; } if (rtcSignal is null) - return; - - var encrypted = E2EeHelper.EncryptForRecipient(json, _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 - }; + SafeSendRawToWebView("SendRtcSignal failed: rtcSignal was null."); + return; + } - _wsc.Send(JsonSerializer.Serialize(payload)); - Console.WriteLine($"[{_username}] sent RTC signal: {rtcSignal.Type} -> {rtcSignal.ChannelId}"); + if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId)) + { + SafeSendRawToWebView("SendRtcSignal failed: channelId was empty."); + return; + } + + try + { + var encrypted = E2EeHelper.EncryptForRecipient(json, _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 + }; + + var socketJson = JsonSerializer.Serialize(payload); + _wsc.Send(socketJson); + + SafeSendRawToWebView($"SendRtcSignal sent: {rtcSignal.Type} -> {rtcSignal.ChannelId}"); + Console.WriteLine($"[{_username}] sent RTC signal: {rtcSignal.Type} -> {rtcSignal.ChannelId}"); + } + catch (Exception ex) + { + SafeSendRawToWebView("SendRtcSignal websocket/encrypt failed: " + ex.Message); + } } //Remove? public async Task GetRtcParticipants() diff --git a/RelayClient/Resources/Raw/wwwroot/index.js b/RelayClient/Resources/Raw/wwwroot/index.js index d720498..4374da7 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.js +++ b/RelayClient/Resources/Raw/wwwroot/index.js @@ -164,58 +164,43 @@ async function joinChannelCall() { LogMessage("Current username: " + currentUsername); LogMessage("Current channel: " + currentChannelId); - const isActive = await window.HybridWebView.InvokeDotNet("JoinRtcChannel"); - const peerConnection = await ensurePeerConnectionForUser(currentUsername); + await window.HybridWebView.InvokeDotNet("JoinRtcChannel"); await ensureLocalMedia(); - if (isActive) { - - const rawJson = await window.HybridWebView.InvokeDotNet("GetRtcOffer"); - const offer = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson; - await peerConnection.setRemoteDescription(offer); - const answer = await peerConnection.createAnswer(); - await peerConnection.setRemoteDescription(answer); - await window.HybridWebView.InvokeDotNet("WriteRtcAnswer", [JSON.stringify(answer)]) + const rawParticipants = await window.HybridWebView.InvokeDotNet("GetRtcParticipants"); + const participants = typeof rawParticipants === "string" + ? JSON.parse(rawParticipants) + : rawParticipants; - const rawParticipants = await window.HybridWebView.InvokeDotNet("GetRtcParticipants"); - const participants = typeof rawParticipants === "string" ? JSON.parse(rawParticipants) : rawParticipants; + LogMessage("Participants: " + JSON.stringify(participants)); - LogMessage("Participants: " + JSON.stringify(participants)); // TODO: Remove + const otherUsers = participants.filter(username => username !== currentUsername); - for (const username of participants) { - if (username === currentUsername) continue; - - const pc = await ensurePeerConnectionForUser(username); - - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - - const payload = { - type: "rtc_offer", - from: currentUsername, - to: username, - channelId: currentChannelId, - sdp: offer.sdp - }; - - await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); - LogMessage(`Sent offer to ${username}`); - } + if (otherUsers.length === 0) { + LogMessage("Joined call as first participant. Waiting for others..."); + return; } - else - { - try { - LogMessage(currentUsername + " attempted to join inactive channel. Making new call.") - const offer = await peerConnection.createOffer(); - await peerConnection.setLocalDescription(offer); - await window.HybridWebView.InvokeDotNet("WriteRtcOffer", [JSON.stringify(offer)]); - LogMessage(`Joining call with media offer: ${JSON.stringify(offer)}`); - } - catch (error) { - LogMessage(error) - } + + for (const username of otherUsers) { + const pc = await ensurePeerConnectionForUser(username); + + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + + const payload = { + type: "rtc_offer", + from: currentUsername, + to: username, + channelId: currentChannelId, + sdp: offer.sdp, + isInitiator: true + }; + + await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); + LogMessage(`Sent offer to ${username}`); } } + async function channelCallJoin(activeCall) { // LogMessage("Active call: " + activeCall); @@ -337,6 +322,11 @@ async function handleRtcSignal(rawJson) { if (!msg.from || msg.from === currentUsername) return; + if (msg.to && msg.to !== currentUsername) { + LogMessage(`Ignoring signal meant for ${msg.to}`); + return; + } + const pc = await ensurePeerConnectionForUser(msg.from); if (msg.type === "rtc_offer") { @@ -390,6 +380,8 @@ async function handleRtcSignal(rawJson) { } } +window.handleRtcSignal = handleRtcSignal; + async function loadDevices() { try { const devices = await navigator.mediaDevices.enumerateDevices(); @@ -510,8 +502,6 @@ function removeRemoteTile(username) { } } -window.handleRtcSignal = handleRtcSignal; - window.addEventListener("HybridWebViewMessageReceived", function (e) { LogMessage("Raw message: " + e.detail.message); }); diff --git a/RelayServer/Services/Chat/ChatSocketBehavior.cs b/RelayServer/Services/Chat/ChatSocketBehavior.cs index bdb4abb..659e4ac 100644 --- a/RelayServer/Services/Chat/ChatSocketBehavior.cs +++ b/RelayServer/Services/Chat/ChatSocketBehavior.cs @@ -122,6 +122,27 @@ public class ChatSocketBehavior : WebSocketBehavior 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 RTC signal: {ex.Message}"); + return; + } + var sessionIds = RtcChannelPresenceService.GetSessionsInChannel(clientPayload.ChannelId); foreach (var sessionId in sessionIds) @@ -129,7 +150,28 @@ public class ChatSocketBehavior : WebSocketBehavior if (sessionId == ID) continue; - Sessions.SendTo(JsonSerializer.Serialize(clientPayload), sessionId); + var username = RtcChannelPresenceService.GetUsernameForSession(sessionId); + if (string.IsNullOrWhiteSpace(username)) + continue; + + var clientKey = GetClientPublicKeyByUsernameSync(username); + if (clientKey is null) + continue; + + var encrypted = E2EeHelper.EncryptForRecipient(plainText, clientKey.PublicKey); + + var outbound = new SocketRtcSignalMessage + { + Type = SignalType.EncryptedSignal, + SenderUsername = clientPayload.SenderUsername, + ChannelId = clientPayload.ChannelId, + CipherText = encrypted.CipherText, + Nonce = encrypted.Nonce, + Tag = encrypted.Tag, + EncryptedKey = encrypted.EncryptedKey + }; + + Sessions.SendTo(JsonSerializer.Serialize(outbound), sessionId); } Console.WriteLine($"Forwarded encrypted RTC signal from {clientPayload.SenderUsername} to channel {clientPayload.ChannelId}"); diff --git a/RelayServer/Services/Rtc/RtcChannelPresenceService.cs b/RelayServer/Services/Rtc/RtcChannelPresenceService.cs index 1786e17..4b72605 100644 --- a/RelayServer/Services/Rtc/RtcChannelPresenceService.cs +++ b/RelayServer/Services/Rtc/RtcChannelPresenceService.cs @@ -57,4 +57,11 @@ public static class RtcChannelPresenceService return SessionToChannel.TryGetValue(sessionId, out var currentChannel) && string.Equals(currentChannel, channelId, StringComparison.Ordinal); } + + public static string? GetUsernameForSession(string sessionId) + { + return SessionToUsername.TryGetValue(sessionId, out var username) + ? username + : null; + } } \ No newline at end of file diff --git a/RelayShared/Rtc/RtcModels.cs b/RelayShared/Rtc/RtcModels.cs index 6f2de64..e167c08 100644 --- a/RelayShared/Rtc/RtcModels.cs +++ b/RelayShared/Rtc/RtcModels.cs @@ -37,7 +37,7 @@ public enum ChannelType public sealed class RtcSignalMessage { [JsonPropertyName("type")] - public SignalType Type { get; set; } + public string Type { get; set; } = string.Empty; [JsonPropertyName("from")] public string From { get; set; } = string.Empty; @@ -59,7 +59,7 @@ public sealed class RtcSignalMessage [JsonPropertyName("sdpMLineIndex")] public int? SdpMLineIndex { get; set; } - + [JsonPropertyName("isInitiator")] public bool IsInitiator { get; set; } }