Compare commits
2 Commits
be797c55c2
...
5b10afcec2
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b10afcec2 | |||
| 1220654656 |
@@ -1,18 +1,17 @@
|
||||
using RelayClient.Crypto;
|
||||
using WebSocketSharp;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using RelayClient.Crypto;
|
||||
using RelayClient.Services;
|
||||
using RelayShared.Rtc;
|
||||
using RelayShared.Services;
|
||||
|
||||
namespace RelayClient;
|
||||
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private readonly string _username;
|
||||
private readonly WebSocket _wsc;
|
||||
private string? _serverPublicKey;
|
||||
private readonly RelaySocketClient _socket;
|
||||
private readonly RtcBridgeService _rtc;
|
||||
|
||||
private string? _currentChannelId;
|
||||
private string? _currentChannelName;
|
||||
|
||||
@@ -33,19 +32,30 @@ public partial class MainPage : ContentPage
|
||||
KeyStorage.SavePublicKey(_username, keys.publicKey);
|
||||
}
|
||||
|
||||
_wsc = new WebSocket("ws://localhost:1337/");
|
||||
|
||||
_wsc.OnMessage += WscOnMessage;
|
||||
_wsc.Connect();
|
||||
|
||||
var publicKey = KeyStorage.LoadPublicKey(_username);
|
||||
_wsc.Send($"REGISTER_KEY|{_username}|{publicKey}");
|
||||
_wsc.Send("GET_SERVER_KEY");
|
||||
_wsc.Send("GET_CHANNELS");
|
||||
|
||||
hybridWebView.SetInvokeJavaScriptTarget(this);
|
||||
ServerAPI.setupClient();
|
||||
|
||||
_socket = new RelaySocketClient(_username);
|
||||
_rtc = new RtcBridgeService(
|
||||
_username,
|
||||
_socket,
|
||||
hybridWebView,
|
||||
() => _currentChannelId,
|
||||
SafeSendRawToWebView
|
||||
);
|
||||
|
||||
_socket.Log += Console.WriteLine;
|
||||
_socket.ChannelListReceived += HandleChannelList;
|
||||
_socket.EncryptedChatReceived += HandleEncryptedChat;
|
||||
_socket.EncryptedRtcSignalReceived += payload =>
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await _rtc.HandleIncomingRtcSignalAsync(payload);
|
||||
});
|
||||
};
|
||||
|
||||
_socket.Connect();
|
||||
}
|
||||
|
||||
private void SendButton_OnClicked(object? sender, EventArgs e)
|
||||
@@ -65,7 +75,7 @@ public partial class MainPage : ContentPage
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serverPublicKey))
|
||||
if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey))
|
||||
{
|
||||
Console.WriteLine("Server public key not loaded yet.");
|
||||
return;
|
||||
@@ -77,7 +87,7 @@ public partial class MainPage : ContentPage
|
||||
return;
|
||||
}
|
||||
|
||||
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
|
||||
var encrypted = E2EeHelper.EncryptForRecipient(text, _socket.ServerPublicKey);
|
||||
|
||||
var payload = new SocketEncryptedMessage
|
||||
{
|
||||
@@ -90,8 +100,7 @@ public partial class MainPage : ContentPage
|
||||
EncryptedKey = encrypted.EncryptedKey
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
_wsc.Send(json);
|
||||
_socket.SendJson(payload);
|
||||
|
||||
Console.WriteLine($"[{_username}] sent encrypted message.");
|
||||
|
||||
@@ -99,34 +108,8 @@ public partial class MainPage : ContentPage
|
||||
MessageEntry.Focus();
|
||||
}
|
||||
|
||||
private void WscOnMessage(object? sender, MessageEventArgs e)
|
||||
private void HandleChannelList(SocketChannelList channelList)
|
||||
{
|
||||
if (e.Data.StartsWith("SERVER:REGISTERED_KEY:"))
|
||||
{
|
||||
Console.WriteLine(e.Data);
|
||||
return;
|
||||
}
|
||||
|
||||
// SafeSendRawToWebView($"[{_username}] RAW WS DATA: {e.Data}");
|
||||
|
||||
Console.WriteLine($"[{_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();
|
||||
|
||||
if (type == SignalType.ChannelList)
|
||||
{
|
||||
var channelList = JsonSerializer.Deserialize<SocketChannelList>(e.Data);
|
||||
if (channelList is null)
|
||||
return;
|
||||
|
||||
_channels.Clear();
|
||||
_channels.AddRange(channelList.Channels.OrderBy(c => c.CreatedAt));
|
||||
|
||||
@@ -136,8 +119,8 @@ public partial class MainPage : ContentPage
|
||||
.FirstOrDefault()
|
||||
?? _channels.OrderBy(c => c.CreatedAt).FirstOrDefault();
|
||||
|
||||
if (defaultChannel is not null)
|
||||
{
|
||||
if (defaultChannel is null) return;
|
||||
|
||||
_currentChannelId = defaultChannel.ChannelId;
|
||||
_currentChannelName = defaultChannel.Name;
|
||||
|
||||
@@ -145,42 +128,23 @@ public partial class MainPage : ContentPage
|
||||
{
|
||||
ChannelLabel.Text = $"#{_currentChannelName}";
|
||||
RenderChannelList();
|
||||
await PushRtcContextToJsAsync();
|
||||
await _rtc.PushRtcContextToJsAsync();
|
||||
});
|
||||
|
||||
_wsc.Send($"GET_HISTORY|{_username}|{_currentChannelId}");
|
||||
_socket.SendRaw($"GET_HISTORY|{_username}|{_currentChannelId}");
|
||||
}
|
||||
|
||||
private void HandleEncryptedChat(SocketEncryptedMessage payload) {
|
||||
if (payload.RecipientUsername != _username)
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == SignalType.ServerPublicKey)
|
||||
string decryptedText;
|
||||
|
||||
try
|
||||
{
|
||||
var serverKeyMessage = JsonSerializer.Deserialize<ServerPublicKeyMessage>(e.Data);
|
||||
if (serverKeyMessage is not null)
|
||||
{
|
||||
_serverPublicKey = serverKeyMessage.PublicKey;
|
||||
Console.WriteLine($"[{_username}] loaded server public key.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == SignalType.EncryptedSignal)
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(e.Data);
|
||||
if (payload is null)
|
||||
return;
|
||||
|
||||
if (payload.ChannelId != _currentChannelId)
|
||||
return;
|
||||
|
||||
if (payload.SenderUsername == _username)
|
||||
return;
|
||||
|
||||
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
||||
|
||||
var decryptedJson = E2EeHelper.DecryptForRecipient(
|
||||
decryptedText = E2EeHelper.DecryptForRecipient(
|
||||
new EncryptedPayload
|
||||
{
|
||||
CipherText = payload.CipherText,
|
||||
@@ -190,71 +154,26 @@ public partial class MainPage : ContentPage
|
||||
},
|
||||
privateKey
|
||||
);
|
||||
|
||||
var rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(decryptedJson);
|
||||
|
||||
if (rtcSignal is null)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rtcSignal.To) &&
|
||||
!string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SafeSendRawToWebView($"Ignoring RTC signal meant for {rtcSignal.To}");
|
||||
Console.WriteLine($"[{_username}] failed to decrypt chat: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
SafeSendRawToWebView("Received encrypted RTC signal: " + decryptedJson);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await SendRtcSignalToJsAsync(decryptedJson);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != SignalType.EncryptedChat)
|
||||
return;
|
||||
|
||||
var pyload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||
if (pyload is null)
|
||||
return;
|
||||
|
||||
if (pyload.RecipientUsername != _username)
|
||||
return;
|
||||
|
||||
Console.WriteLine($"[{_username}] received encrypted payload for {pyload.RecipientUsername}");
|
||||
|
||||
var privKey = KeyStorage.LoadPrivateKey(_username);
|
||||
|
||||
var decryptedText = E2EeHelper.DecryptForRecipient(
|
||||
new EncryptedPayload
|
||||
{
|
||||
CipherText = pyload.CipherText,
|
||||
Nonce = pyload.Nonce,
|
||||
Tag = pyload.Tag,
|
||||
EncryptedKey = pyload.EncryptedKey
|
||||
},
|
||||
privKey
|
||||
);
|
||||
|
||||
Console.WriteLine($"[{_username}] decrypted message from {pyload.SenderUsername}: {decryptedText}");
|
||||
|
||||
var message = new ChatMessage
|
||||
{
|
||||
SenderUsername = pyload.SenderUsername,
|
||||
SenderUsername = payload.SenderUsername,
|
||||
Text = decryptedText,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
|
||||
if (!_messagesByChannel.ContainsKey(pyload.ChannelId))
|
||||
{
|
||||
_messagesByChannel[pyload.ChannelId] = [];
|
||||
}
|
||||
if (!_messagesByChannel.ContainsKey(payload.ChannelId))
|
||||
_messagesByChannel[payload.ChannelId] = [];
|
||||
|
||||
_messagesByChannel[pyload.ChannelId].Add(message);
|
||||
_messagesByChannel[payload.ChannelId].Add(message);
|
||||
|
||||
if (pyload.ChannelId == _currentChannelId)
|
||||
if (payload.ChannelId == _currentChannelId)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
@@ -262,16 +181,10 @@ public partial class MainPage : ContentPage
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[{_username}] failed to process websocket message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_wsc.OnMessage -= WscOnMessage;
|
||||
_wsc.Close();
|
||||
_socket.Disconnect();
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
@@ -295,11 +208,14 @@ public partial class MainPage : ContentPage
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await PushRtcContextToJsAsync();
|
||||
await _rtc.PushRtcContextToJsAsync();
|
||||
|
||||
if (channel.Type == ChannelType.Voice)
|
||||
{
|
||||
if (!RtcView.IsVisible)
|
||||
SwapView();
|
||||
_ = JoinRtcChannel(); //TODO: Join voice calls when clicking channel rather than a separate button
|
||||
|
||||
_ = _rtc.JoinRtcChannel();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -307,9 +223,7 @@ public partial class MainPage : ContentPage
|
||||
RenderCurrentChannelMessages();
|
||||
|
||||
if (!_messagesByChannel.ContainsKey(channel.ChannelId))
|
||||
{
|
||||
_wsc.Send($"GET_HISTORY|{_username}|{channel.ChannelId}");
|
||||
}
|
||||
_socket.SendRaw($"GET_HISTORY|{_username}|{channel.ChannelId}");
|
||||
};
|
||||
|
||||
SidebarList.Children.Add(button);
|
||||
@@ -334,7 +248,7 @@ public partial class MainPage : ContentPage
|
||||
|
||||
private async void RenderSingleMessage(ChatMessage message)
|
||||
{
|
||||
bool isOwnMessage = message.SenderUsername == _username;
|
||||
var isOwnMessage = message.SenderUsername == _username;
|
||||
|
||||
var bubble = new Border
|
||||
{
|
||||
@@ -369,7 +283,6 @@ public partial class MainPage : ContentPage
|
||||
MessagesScrollView.IsVisible = true;
|
||||
RtcView.IsVisible = false;
|
||||
ViewSwapped.Text = "Swap to Web View";
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -384,148 +297,31 @@ public partial class MainPage : ContentPage
|
||||
SwapView();
|
||||
}
|
||||
|
||||
#region RTC Functions
|
||||
|
||||
public Task JoinRtcChannel()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_currentChannelId))
|
||||
return Task.CompletedTask;
|
||||
|
||||
_wsc.Send($"RTC_JOIN_CHANNEL|{_username}|{_currentChannelId}");
|
||||
return Task.CompletedTask;
|
||||
return _rtc.JoinRtcChannel();
|
||||
}
|
||||
|
||||
public void LeaveRtcChannel()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_currentChannelId))
|
||||
return;
|
||||
|
||||
_wsc.Send($"RTC_LEAVE_CHANNEL|{_username}|{_currentChannelId}");
|
||||
_rtc.LeaveRtcChannel();
|
||||
}
|
||||
|
||||
public void SendRtcSignal(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_serverPublicKey))
|
||||
{
|
||||
SafeSendRawToWebView("SendRtcSignal failed: server public key not loaded.");
|
||||
return;
|
||||
_rtc.SendRtcSignal(json);
|
||||
}
|
||||
|
||||
RtcSignalMessage? rtcSignal;
|
||||
|
||||
try
|
||||
public Task<string> GetRtcParticipants()
|
||||
{
|
||||
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(json);
|
||||
return _rtc.GetRtcParticipants();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SafeSendRawToWebView("SendRtcSignal failed to parse RTC signal: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rtcSignal is null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId))
|
||||
rtcSignal.ChannelId = _currentChannelId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rtcSignal.From))
|
||||
rtcSignal.From = _username;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId))
|
||||
{
|
||||
SafeSendRawToWebView("SendRtcSignal failed: missing channel id.");
|
||||
return;
|
||||
}
|
||||
|
||||
var outgoingJson = JsonSerializer.Serialize(rtcSignal);
|
||||
|
||||
try
|
||||
{
|
||||
var encrypted = E2EeHelper.EncryptForRecipient(outgoingJson, _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
|
||||
};
|
||||
|
||||
_wsc.Send(JsonSerializer.Serialize(payload));
|
||||
|
||||
SafeSendRawToWebView($"SendRtcSignal sent: {rtcSignal.Type} -> {rtcSignal.To}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SafeSendRawToWebView("SendRtcSignal failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetRtcParticipants()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_currentChannelId))
|
||||
return "[]";
|
||||
|
||||
var participants = await ServerAPI.GetRtcParticipantsAsync(_currentChannelId);
|
||||
return JsonSerializer.Serialize(participants ?? []);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
SafeSendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PushRtcContextToJsAsync()
|
||||
{
|
||||
var usernameJson = JsonSerializer.Serialize(_username);
|
||||
var channelIdJson = JsonSerializer.Serialize(_currentChannelId);
|
||||
|
||||
await hybridWebView.EvaluateJavaScriptAsync($"window.setUsername({usernameJson})");
|
||||
await hybridWebView.EvaluateJavaScriptAsync($"window.setChannelId({channelIdJson})");
|
||||
|
||||
Console.WriteLine($"[{_username}] pushed RTC context into HybridWebView.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
|
||||
{
|
||||
if (e.Message == "rtc_page_ready")
|
||||
{
|
||||
await PushRtcContextToJsAsync();
|
||||
await _rtc.PushRtcContextToJsAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,7 +346,7 @@ public partial class MainPage : ContentPage
|
||||
public class ChannelButton : Button
|
||||
{
|
||||
public ChannelType Type { get; set; }
|
||||
public string Group { get; set; }
|
||||
public string Group { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = false)]
|
||||
@@ -561,8 +357,5 @@ public partial class MainPage : ContentPage
|
||||
[JsonSerializable(typeof(string))]
|
||||
internal partial class HybridJSType : JsonSerializerContext
|
||||
{
|
||||
// This type's attributes specify JSON serialization info to preserve type structure
|
||||
// for trimmed builds.
|
||||
}
|
||||
|
||||
}
|
||||
127
RelayClient/Services/RelaySocketClient.cs
Normal file
127
RelayClient/Services/RelaySocketClient.cs
Normal file
@@ -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<string>? RawMessageReceived;
|
||||
public event Action<SocketChannelList>? ChannelListReceived;
|
||||
public event Action<SocketEncryptedMessage>? EncryptedChatReceived;
|
||||
public event Action<SocketRtcSignalMessage>? EncryptedRtcSignalReceived;
|
||||
public event Action<string>? ServerPublicKeyReceived;
|
||||
public event Action<string>? 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>(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<SocketChannelList>(e.Data);
|
||||
if (channelList is not null)
|
||||
ChannelListReceived?.Invoke(channelList);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case SignalType.ServerPublicKey:
|
||||
{
|
||||
var serverKeyMessage = JsonSerializer.Deserialize<ServerPublicKeyMessage>(e.Data);
|
||||
if (serverKeyMessage is not null)
|
||||
{
|
||||
ServerPublicKey = serverKeyMessage.PublicKey;
|
||||
ServerPublicKeyReceived?.Invoke(serverKeyMessage.PublicKey);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case SignalType.EncryptedSignal:
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(e.Data);
|
||||
if (payload is not null)
|
||||
EncryptedRtcSignalReceived?.Invoke(payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case SignalType.EncryptedChat:
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||
if (payload is not null)
|
||||
EncryptedChatReceived?.Invoke(payload);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke($"[{_username}] failed to process websocket message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
228
RelayClient/Services/RtcBridgeService.cs
Normal file
228
RelayClient/Services/RtcBridgeService.cs
Normal file
@@ -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<string?> _getCurrentChannelId;
|
||||
private readonly Action<string> _sendRawToWebView;
|
||||
|
||||
public RtcBridgeService(
|
||||
string username,
|
||||
RelaySocketClient socket,
|
||||
HybridWebView hybridWebView,
|
||||
Func<string?> getCurrentChannelId,
|
||||
Action<string> 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<RtcSignalMessage>(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<string> 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<RtcSignalMessage>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user