Added WebRTC stuff - Needs Testing.
This commit is contained in:
@@ -15,7 +15,7 @@ public partial class MainPage : ContentPage
|
|||||||
private string? _currentChannelName;
|
private string? _currentChannelName;
|
||||||
|
|
||||||
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
|
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
|
||||||
private readonly List<ChannelItem> _channels = new();
|
private readonly List<ChannelItem> _channels = [];
|
||||||
|
|
||||||
public MainPage(string username)
|
public MainPage(string username)
|
||||||
{
|
{
|
||||||
@@ -41,6 +41,11 @@ public partial class MainPage : ContentPage
|
|||||||
_wsc.Send("GET_SERVER_KEY");
|
_wsc.Send("GET_SERVER_KEY");
|
||||||
_wsc.Send("GET_CHANNELS");
|
_wsc.Send("GET_CHANNELS");
|
||||||
hybridWebView.SetInvokeJavaScriptTarget(this);
|
hybridWebView.SetInvokeJavaScriptTarget(this);
|
||||||
|
|
||||||
|
Loaded += async (_, _) =>
|
||||||
|
{
|
||||||
|
await InitializeRtcPageAsync();
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +71,12 @@ public partial class MainPage : ContentPage
|
|||||||
Console.WriteLine("Server public key not loaded yet.");
|
Console.WriteLine("Server public key not loaded yet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_currentChannelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("No channel selected yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
|
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
|
||||||
|
|
||||||
@@ -152,49 +163,79 @@ public partial class MainPage : ContentPage
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == "encrypted_rtc_signal")
|
||||||
|
{
|
||||||
|
var payload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(e.Data);
|
||||||
|
if (payload is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (payload.RecipientUsername != _username)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
||||||
|
|
||||||
|
var decryptedJson = E2EeHelper.DecryptForRecipient(
|
||||||
|
new EncryptedPayload
|
||||||
|
{
|
||||||
|
CipherText = payload.CipherText,
|
||||||
|
Nonce = payload.Nonce,
|
||||||
|
Tag = payload.Tag,
|
||||||
|
EncryptedKey = payload.EncryptedKey
|
||||||
|
},
|
||||||
|
privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
MainThread.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
await SendRtcSignalToJsAsync(decryptedJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type != "encrypted_chat")
|
if (type != "encrypted_chat")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
var pyload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||||
if (payload is null)
|
if (pyload is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (payload.RecipientUsername != _username)
|
if (pyload.RecipientUsername != _username)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Console.WriteLine($"[{_username}] received encrypted payload for {payload.RecipientUsername}");
|
Console.WriteLine($"[{_username}] received encrypted payload for {pyload.RecipientUsername}");
|
||||||
|
|
||||||
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
var privKey = KeyStorage.LoadPrivateKey(_username);
|
||||||
|
|
||||||
var decryptedText = E2EeHelper.DecryptForRecipient(
|
var decryptedText = E2EeHelper.DecryptForRecipient(
|
||||||
new EncryptedPayload
|
new EncryptedPayload
|
||||||
{
|
{
|
||||||
CipherText = payload.CipherText,
|
CipherText = pyload.CipherText,
|
||||||
Nonce = payload.Nonce,
|
Nonce = pyload.Nonce,
|
||||||
Tag = payload.Tag,
|
Tag = pyload.Tag,
|
||||||
EncryptedKey = payload.EncryptedKey
|
EncryptedKey = pyload.EncryptedKey
|
||||||
},
|
},
|
||||||
privateKey
|
privKey
|
||||||
);
|
);
|
||||||
|
|
||||||
Console.WriteLine($"[{_username}] decrypted message from {payload.SenderUsername}: {decryptedText}");
|
Console.WriteLine($"[{_username}] decrypted message from {pyload.SenderUsername}: {decryptedText}");
|
||||||
|
|
||||||
var message = new ChatMessage
|
var message = new ChatMessage
|
||||||
{
|
{
|
||||||
SenderUsername = payload.SenderUsername,
|
SenderUsername = pyload.SenderUsername,
|
||||||
Text = decryptedText,
|
Text = decryptedText,
|
||||||
Timestamp = DateTime.Now
|
Timestamp = DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!_messagesByChannel.ContainsKey(payload.ChannelId))
|
if (!_messagesByChannel.ContainsKey(pyload.ChannelId))
|
||||||
{
|
{
|
||||||
_messagesByChannel[payload.ChannelId] = [];
|
_messagesByChannel[pyload.ChannelId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_messagesByChannel[payload.ChannelId].Add(message);
|
_messagesByChannel[pyload.ChannelId].Add(message);
|
||||||
|
|
||||||
if (payload.ChannelId == _currentChannelId)
|
if (pyload.ChannelId == _currentChannelId)
|
||||||
{
|
{
|
||||||
MainThread.BeginInvokeOnMainThread(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
@@ -296,14 +337,14 @@ public partial class MainPage : ContentPage
|
|||||||
{
|
{
|
||||||
MessagesScrollView.IsVisible = true;
|
MessagesScrollView.IsVisible = true;
|
||||||
RtcView.IsVisible = false;
|
RtcView.IsVisible = false;
|
||||||
ViewSwapped.Text = "Swap to Message View";
|
ViewSwapped.Text = "Swap to Web View";
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MessagesScrollView.IsVisible = false;
|
MessagesScrollView.IsVisible = false;
|
||||||
RtcView.IsVisible = true;
|
RtcView.IsVisible = true;
|
||||||
ViewSwapped.Text = "Swap to Web View";
|
ViewSwapped.Text = "Swap to Message View";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,62 +357,56 @@ public partial class MainPage : ContentPage
|
|||||||
{
|
{
|
||||||
await DisplayAlertAsync("Raw Message Received", e.Message, "OK");
|
await DisplayAlertAsync("Raw Message Received", e.Message, "OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
#region syncs
|
|
||||||
public async void DoSyncWork()
|
|
||||||
{
|
|
||||||
await DisplayAlertAsync("Sync Work", "Sync Work", "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void DoSyncWorkParams(int i, string s)
|
|
||||||
{
|
|
||||||
await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DoSyncWorkReturn()
|
|
||||||
{
|
|
||||||
return "Hello from C#!";
|
|
||||||
}
|
|
||||||
|
|
||||||
public SyncReturn DoSyncWorkParamsReturn(int i, string s)
|
|
||||||
{
|
|
||||||
return new SyncReturn
|
|
||||||
{
|
|
||||||
Message = $"Hello from C#! {s}",
|
|
||||||
Value = i
|
|
||||||
};
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region asyncs
|
public void SendRtcSignal(string json)
|
||||||
|
|
||||||
public async Task DoAsyncWork()
|
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
if (string.IsNullOrWhiteSpace(_serverPublicKey))
|
||||||
}
|
|
||||||
public async Task DoAsyncWorkParams(int i, string s)
|
|
||||||
{
|
|
||||||
await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK");
|
|
||||||
}
|
|
||||||
public async Task<string> DoAsyncWorkReturn()
|
|
||||||
{
|
|
||||||
return "Hello from C#!";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SyncReturn> DoAsyncWorkParamsReturn(int i, string s)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
return new SyncReturn
|
|
||||||
{
|
{
|
||||||
Message = $"Hello from C# ASync! {s}",
|
Console.WriteLine("Server public key not loaded yet.");
|
||||||
Value = i
|
return;
|
||||||
};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
RtcSignalMessage? rtcSignal;
|
||||||
public class SyncReturn
|
try
|
||||||
|
{
|
||||||
|
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to parse RTC signal from JS: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rtcSignal is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(json, _serverPublicKey);
|
||||||
|
|
||||||
|
var payload = new SocketRtcSignalMessage
|
||||||
|
{
|
||||||
|
Type = "encrypted_rtc_signal",
|
||||||
|
SenderUsername = _username,
|
||||||
|
RecipientUsername = rtcSignal.To,
|
||||||
|
CipherText = encrypted.CipherText,
|
||||||
|
Nonce = encrypted.Nonce,
|
||||||
|
Tag = encrypted.Tag,
|
||||||
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
|
};
|
||||||
|
|
||||||
|
_wsc.Send(JsonSerializer.Serialize(payload));
|
||||||
|
Console.WriteLine($"[{_username}] sent RTC signal: {rtcSignal.Type} -> {rtcSignal.To}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendRtcSignalToJsAsync(string rawJson)
|
||||||
{
|
{
|
||||||
public string? Message { get; set; }
|
var jsArg = JsonSerializer.Serialize(rawJson);
|
||||||
public int Value { get; set; }
|
await hybridWebView.EvaluateJavaScriptAsync($"window.handleRtcSignal({jsArg})");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeRtcPageAsync()
|
||||||
|
{
|
||||||
|
var jsArg = JsonSerializer.Serialize(_username);
|
||||||
|
await hybridWebView.EvaluateJavaScriptAsync($"window.currentUsername = {jsArg};");
|
||||||
|
Console.WriteLine($"[{_username}] RTC page initialized.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
RelayClient/Models/RtcSignalMessage.cs
Normal file
13
RelayClient/Models/RtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RelayClient.Models;
|
||||||
|
|
||||||
|
public class RtcSignalMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; } // rtc_offer / rtc_answer / rtc_ice_candidate / rtc_call_request / rtc_call_accept / rtc_call_reject
|
||||||
|
public required string From { get; set; }
|
||||||
|
public required string To { get; set; }
|
||||||
|
|
||||||
|
public string? Sdp { get; set; }
|
||||||
|
public string? Candidate { get; set; }
|
||||||
|
public string? SdpMid { get; set; }
|
||||||
|
public int? SdpMLineIndex { get; set; }
|
||||||
|
}
|
||||||
13
RelayClient/Models/SocketRtcSignalMessage.cs
Normal file
13
RelayClient/Models/SocketRtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RelayClient.Models;
|
||||||
|
|
||||||
|
public class SocketRtcSignalMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; } // encrypted_rtc_signal
|
||||||
|
public required string SenderUsername { get; set; }
|
||||||
|
public required string RecipientUsername { get; set; }
|
||||||
|
|
||||||
|
public required string CipherText { get; set; }
|
||||||
|
public required string Nonce { get; set; }
|
||||||
|
public required string Tag { get; set; }
|
||||||
|
public required string EncryptedKey { get; set; }
|
||||||
|
}
|
||||||
@@ -9,116 +9,190 @@
|
|||||||
<link rel="stylesheet" href="index.css">
|
<link rel="stylesheet" href="index.css">
|
||||||
<script src="_framework/hybridwebview.js"></script>
|
<script src="_framework/hybridwebview.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
let peerConnection = null;
|
||||||
|
let localStream = null;
|
||||||
|
let currentTarget = null;
|
||||||
|
let currentUsername = null;
|
||||||
|
|
||||||
function LogMessage(msg) {
|
function LogMessage(msg) {
|
||||||
var messageLog = document.getElementById("messageLog");
|
const messageLog = document.getElementById("messageLog");
|
||||||
messageLog.value += '\r\n' + msg;
|
messageLog.value += '\r\n' + msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener(
|
async function ensurePeerConnection() {
|
||||||
"HybridWebViewMessageReceived",
|
if (peerConnection) return;
|
||||||
function (e) {
|
|
||||||
LogMessage("Raw message: " + e.detail.message);
|
peerConnection = new RTCPeerConnection({
|
||||||
|
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
||||||
});
|
});
|
||||||
|
|
||||||
function AddNumbers(a, b) {
|
peerConnection.onicecandidate = async (event) => {
|
||||||
var result = {
|
if (!event.candidate || !currentTarget || !currentUsername) return;
|
||||||
"result": a + b,
|
|
||||||
"operationName": "Addition"
|
const payload = {
|
||||||
|
type: "rtc_ice_candidate",
|
||||||
|
from: currentUsername,
|
||||||
|
to: currentTarget,
|
||||||
|
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.ontrack = (event) => {
|
||||||
|
LogMessage("Remote track received");
|
||||||
|
const remoteVideo = document.getElementById("remoteVideo");
|
||||||
|
remoteVideo.srcObject = event.streams[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onconnectionstatechange = () => {
|
||||||
|
LogMessage("Connection state: " + peerConnection.connectionState);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.oniceconnectionstatechange = () => {
|
||||||
|
LogMessage("ICE connection state: " + peerConnection.iceConnectionState);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onicegatheringstatechange = () => {
|
||||||
|
LogMessage("ICE gathering state: " + peerConnection.iceGatheringState);
|
||||||
};
|
};
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 0;
|
async function ensureLocalMedia() {
|
||||||
|
if (localStream) return;
|
||||||
|
|
||||||
async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
|
try {
|
||||||
const response = await fetch("/asyncdata.txt");
|
localStream = await navigator.mediaDevices.getUserMedia({
|
||||||
if (!response.ok) {
|
video: true,
|
||||||
throw new Error(`HTTP error: ${response.status}`);
|
audio: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const localVideo = document.getElementById("localVideo");
|
||||||
|
localVideo.srcObject = localStream;
|
||||||
|
|
||||||
|
for (const track of localStream.getTracks()) {
|
||||||
|
peerConnection.addTrack(track, localStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogMessage("Local media initialized");
|
||||||
|
} catch (err) {
|
||||||
|
LogMessage("getUserMedia failed: " + err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
var jsonData = await response.json();
|
|
||||||
|
|
||||||
jsonData[s1] = s2;
|
|
||||||
|
|
||||||
const msg = 'JSON data is available: ' + JSON.stringify(jsonData);
|
|
||||||
window.HybridWebView.SendRawMessage(msg)
|
|
||||||
|
|
||||||
return jsonData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function InvokeDoSyncWork() {
|
async function startCall() {
|
||||||
LogMessage("Invoking DoSyncWork");
|
try {
|
||||||
await window.HybridWebView.InvokeDotNet('DoSyncWork');
|
currentTarget = document.getElementById("targetUser").value;
|
||||||
LogMessage("Invoked DoSyncWork");
|
|
||||||
|
if (!currentTarget) {
|
||||||
|
LogMessage("No target user set.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensurePeerConnection();
|
||||||
|
await ensureLocalMedia();
|
||||||
|
|
||||||
|
const offer = await peerConnection.createOffer();
|
||||||
|
await peerConnection.setLocalDescription(offer);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: "rtc_offer",
|
||||||
|
from: currentUsername,
|
||||||
|
to: currentTarget,
|
||||||
|
sdp: offer.sdp
|
||||||
|
};
|
||||||
|
|
||||||
|
LogMessage("Sending offer to " + currentTarget);
|
||||||
|
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
|
||||||
|
} catch (err) {
|
||||||
|
LogMessage("startCall failed: " + err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function InvokeDoSyncWorkParams() {
|
async function handleRtcSignal(rawJson) {
|
||||||
LogMessage("Invoking DoSyncWorkParams");
|
try {
|
||||||
await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']);
|
const msg = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson;
|
||||||
LogMessage("Invoked DoSyncWorkParams");
|
|
||||||
|
LogMessage("Received signal: " + msg.type + " from " + msg.from);
|
||||||
|
|
||||||
|
await ensurePeerConnection();
|
||||||
|
|
||||||
|
if (msg.type === "rtc_offer") {
|
||||||
|
currentTarget = msg.from;
|
||||||
|
LogMessage("Incoming call from " + msg.from);
|
||||||
|
await ensureLocalMedia();
|
||||||
|
|
||||||
|
await peerConnection.setRemoteDescription({
|
||||||
|
type: "offer",
|
||||||
|
sdp: msg.sdp
|
||||||
|
});
|
||||||
|
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: "rtc_answer",
|
||||||
|
from: currentUsername,
|
||||||
|
to: msg.from,
|
||||||
|
sdp: answer.sdp
|
||||||
|
};
|
||||||
|
|
||||||
|
LogMessage("Sending answer to " + msg.from);
|
||||||
|
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "rtc_answer") {
|
||||||
|
await peerConnection.setRemoteDescription({
|
||||||
|
type: "answer",
|
||||||
|
sdp: msg.sdp
|
||||||
|
});
|
||||||
|
LogMessage("Remote answer applied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "rtc_ice_candidate") {
|
||||||
|
await peerConnection.addIceCandidate({
|
||||||
|
candidate: msg.candidate,
|
||||||
|
sdpMid: msg.sdpMid,
|
||||||
|
sdpMLineIndex: msg.sdpMLineIndex
|
||||||
|
});
|
||||||
|
LogMessage("Remote ICE candidate applied");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
LogMessage("handleRtcSignal failed: " + err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function InvokeDoSyncWorkReturn() {
|
window.handleRtcSignal = handleRtcSignal;
|
||||||
LogMessage("Invoking DoSyncWorkReturn");
|
|
||||||
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn');
|
|
||||||
LogMessage("Invoked DoSyncWorkReturn, return value: " + retValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InvokeDoSyncWorkParamsReturn() {
|
|
||||||
LogMessage("Invoking DoSyncWorkParamsReturn");
|
|
||||||
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']);
|
|
||||||
LogMessage("Invoked DoSyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InvokeDoAsyncWork() {
|
|
||||||
LogMessage("Invoking DoAsyncWork");
|
|
||||||
await window.HybridWebView.InvokeDotNet('DoAsyncWork');
|
|
||||||
LogMessage("Invoked DoAsyncWork");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InvokeDoAsyncWorkParams() {
|
|
||||||
LogMessage("Invoking DoAsyncWorkParams");
|
|
||||||
await window.HybridWebView.InvokeDotNet('DoAsyncWorkParams', [123, 'hello']);
|
|
||||||
LogMessage("Invoked DoAsyncWorkParams");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InvokeDoAsyncWorkReturn() {
|
|
||||||
LogMessage("Invoking DoAsyncWorkReturn");
|
|
||||||
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkReturn');
|
|
||||||
LogMessage("Invoked DoAsyncWorkReturn, return value: " + retValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InvokeDoAsyncWorkParamsReturn() {
|
|
||||||
LogMessage("Invoking DoAsyncWorkParamsReturn");
|
|
||||||
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkParamsReturn', [123, 'hello']);
|
|
||||||
LogMessage("Invoked DoAsyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
window.addEventListener("HybridWebViewMessageReceived", function (e) {
|
||||||
|
LogMessage("Raw message: " + e.detail.message);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
Hybrid sample!
|
<h3>Relay RTC Test</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button>
|
<div>
|
||||||
</div>
|
<label for="targetUser">Target User:</label>
|
||||||
<div>
|
<input id="targetUser" type="text" value="Ru_Kira" />
|
||||||
<button onclick="InvokeDoSyncWork()">Call C# sync method (no params)</button>
|
<button onclick="startCall()">Start Call</button>
|
||||||
<button onclick="InvokeDoSyncWorkParams()">Call C# sync method (params)</button>
|
</div>
|
||||||
<button onclick="InvokeDoSyncWorkReturn()">Call C# method (no params) and get simple return value</button>
|
|
||||||
<button onclick="InvokeDoSyncWorkParamsReturn()">Call C# method (params) and get complex return value</button>
|
<div style="margin-top: 10px;">
|
||||||
</div>
|
<video id="localVideo" autoplay playsinline muted style="width: 320px; height: 240px; background: #111;"></video>
|
||||||
<div>
|
<video id="remoteVideo" autoplay playsinline style="width: 320px; height: 240px; background: #111;"></video>
|
||||||
<button onclick="InvokeDoAsyncWork()">Call C# async method (no params)</button>
|
</div>
|
||||||
<button onclick="InvokeDoAsyncWorkParams()">Call C# async method (params)</button>
|
|
||||||
<button onclick="InvokeDoAsyncWorkReturn()">Call C# async method (no params) and get simple return value</button>
|
<div style="margin-top: 10px;">
|
||||||
<button onclick="InvokeDoAsyncWorkParamsReturn()">Call C# async method (params) and get complex return value</button>
|
<textarea readonly id="messageLog" style="width: 90%; height: 12em;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</body>
|
||||||
Log: <textarea readonly id="messageLog" style="width: 80%; height: 10em;"></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Consider checking out this PDF: <a href="docs/sample.pdf">sample.pdf</a>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
13
RelayServer/Models/RtcSignalMessage.cs
Normal file
13
RelayServer/Models/RtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RelayServer.Models;
|
||||||
|
|
||||||
|
public class RtcSignalMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; } // rtc_offer / rtc_answer / rtc_ice_candidate / rtc_call_request / rtc_call_accept / rtc_call_reject
|
||||||
|
public required string From { get; set; }
|
||||||
|
public required string To { get; set; }
|
||||||
|
|
||||||
|
public string? Sdp { get; set; }
|
||||||
|
public string? Candidate { get; set; }
|
||||||
|
public string? SdpMid { get; set; }
|
||||||
|
public int? SdpMLineIndex { get; set; }
|
||||||
|
}
|
||||||
13
RelayServer/Models/SocketRtcSignalMessage.cs
Normal file
13
RelayServer/Models/SocketRtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RelayServer.Models;
|
||||||
|
|
||||||
|
public class SocketRtcSignalMessage
|
||||||
|
{
|
||||||
|
public required string Type { get; set; } // encrypted_rtc_signal
|
||||||
|
public required string SenderUsername { get; set; }
|
||||||
|
public required string RecipientUsername { get; set; }
|
||||||
|
|
||||||
|
public required string CipherText { get; set; }
|
||||||
|
public required string Nonce { get; set; }
|
||||||
|
public required string Tag { get; set; }
|
||||||
|
public required string EncryptedKey { get; set; }
|
||||||
|
}
|
||||||
@@ -41,6 +41,22 @@ public class ChatTest : WebSocketBehavior
|
|||||||
HandleGetHistory(msg);
|
HandleGetHistory(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SocketRtcSignalMessage? rtcProbe = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rtcProbe = JsonSerializer.Deserialize<SocketRtcSignalMessage>(msg);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rtcProbe?.Type == "encrypted_rtc_signal")
|
||||||
|
{
|
||||||
|
HandleEncryptedRtcSignal(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
HandleEncryptedClientMessage(msg);
|
HandleEncryptedClientMessage(msg);
|
||||||
}
|
}
|
||||||
@@ -323,4 +339,74 @@ public class ChatTest : WebSocketBehavior
|
|||||||
|
|
||||||
return $"{table}:{recordId}";
|
return $"{table}:{recordId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleEncryptedRtcSignal(string msg)
|
||||||
|
{
|
||||||
|
SocketRtcSignalMessage? clientPayload;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientPayload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(msg);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to parse encrypted RTC signal payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientPayload is null || clientPayload.Type != "encrypted_rtc_signal")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ClientKeyService is null || string.IsNullOrWhiteSpace(ServerPrivateKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server RTC crypto dependencies are not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string plainJson;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
plainJson = 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 payload: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetClient = Task.Run(async () => await ClientKeyService.GetByUsernameAsync(clientPayload.RecipientUsername))
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
if (targetClient is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No target RTC client key found for {clientPayload.RecipientUsername}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(plainJson, targetClient.PublicKey);
|
||||||
|
|
||||||
|
var outbound = new SocketRtcSignalMessage
|
||||||
|
{
|
||||||
|
Type = "encrypted_rtc_signal",
|
||||||
|
SenderUsername = clientPayload.SenderUsername,
|
||||||
|
RecipientUsername = clientPayload.RecipientUsername,
|
||||||
|
CipherText = encrypted.CipherText,
|
||||||
|
Nonce = encrypted.Nonce,
|
||||||
|
Tag = encrypted.Tag,
|
||||||
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Sessions.Broadcast(JsonSerializer.Serialize(outbound));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -64,19 +64,19 @@ Start-Sleep -Seconds 5
|
|||||||
& '$clientExe' --user Ru_Kira
|
& '$clientExe' --user Ru_Kira
|
||||||
"@
|
"@
|
||||||
|
|
||||||
$testScript = New-TabScript -Name "Test" -Content @"
|
#$testScript = New-TabScript -Name "Test" -Content @"
|
||||||
Set-Location '$root'
|
#Set-Location '$root'
|
||||||
Start-Sleep -Seconds 25
|
#Start-Sleep -Seconds 25
|
||||||
& '$clientExe' --user Test
|
#& '$clientExe' --user Test
|
||||||
"@
|
#"@
|
||||||
|
|
||||||
$wtArgs = @(
|
$wtArgs = @(
|
||||||
"new-tab --title `"SurrealDB`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$dockerScript`"",
|
"new-tab --title `"SurrealDB`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$dockerScript`"",
|
||||||
"new-tab --title `"RelayCore`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$coreScript`"",
|
"new-tab --title `"RelayCore`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$coreScript`"",
|
||||||
"new-tab --title `"RelayServer`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$serverScript`"",
|
"new-tab --title `"RelayServer`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$serverScript`"",
|
||||||
"new-tab --title `"Keeper317`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$keeperScript`"",
|
"new-tab --title `"Keeper317`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$keeperScript`"",
|
||||||
"new-tab --title `"Ru_Kira`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$kiraScript`"",
|
"new-tab --title `"Ru_Kira`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$kiraScript`""
|
||||||
"new-tab --title `"Test`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$testScript`""
|
#"new-tab --title `"Test`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$testScript`""
|
||||||
) -join " ; "
|
) -join " ; "
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|||||||
Reference in New Issue
Block a user