Compare commits
15 Commits
RTC-Rewrit
...
b62ceb1949
| Author | SHA1 | Date | |
|---|---|---|---|
| b62ceb1949 | |||
| 1ed3efcc68 | |||
| 9fbe795660 | |||
| 63d3806936 | |||
| a9d2fd64de | |||
| f8b595f609 | |||
| 885db41ba9 | |||
| 3460ce6b04 | |||
| 4974663128 | |||
| ec6a8c446a | |||
| 3901542141 | |||
| 33eee17c43 | |||
| dd1aa45f6e | |||
| 38662f6655 | |||
| 777328caed |
@@ -15,7 +15,8 @@ public partial class App : Application
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username))
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
{
|
{
|
||||||
throw new Exception("Missing required --user argument. Example: --user Keeper317");
|
username = "Test";
|
||||||
|
// throw new Exception("Missing required --user argument. Example: --user Keeper317");
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSession.Username = username;
|
ClientSession.Username = username;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:local="clr-namespace:RelayClient"
|
xmlns:local="clr-namespace:RelayClient"
|
||||||
Title="RelayClient">
|
Title="RelayClient"
|
||||||
|
FlyoutBehavior="Flyout">
|
||||||
|
|
||||||
<ShellContent
|
<ShellContent
|
||||||
Title="Home"
|
Title="Home"
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ namespace RelayClient;
|
|||||||
|
|
||||||
public partial class MainPage : ContentPage
|
public partial class MainPage : ContentPage
|
||||||
{
|
{
|
||||||
private readonly string _username;
|
public static string _username;
|
||||||
private readonly RelaySocketClient _socket;
|
private readonly RelaySocketClient _socket;
|
||||||
private readonly RtcBridgeService _rtc;
|
private readonly RtcBridgeService _rtc;
|
||||||
|
|
||||||
|
public static string? _userToken;
|
||||||
|
|
||||||
private string? _currentChannelId;
|
private string? _currentChannelId;
|
||||||
private string? _currentChannelName;
|
private string? _currentChannelName;
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ public partial class MainPage : ContentPage
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_username = username;
|
_username = username;
|
||||||
|
|
||||||
UserLabel.Text = $"Logged in as: {_username}";
|
UserLabel.Text = $"Logged in as: {_username}";
|
||||||
|
|
||||||
if (!KeyStorage.HasKeys(_username))
|
if (!KeyStorage.HasKeys(_username))
|
||||||
@@ -32,7 +35,7 @@ public partial class MainPage : ContentPage
|
|||||||
KeyStorage.SavePublicKey(_username, keys.publicKey);
|
KeyStorage.SavePublicKey(_username, keys.publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerAPI.setupClient();
|
var waitFor = ServerAPI.setupClient();
|
||||||
|
|
||||||
_socket = new RelaySocketClient(_username);
|
_socket = new RelaySocketClient(_username);
|
||||||
_rtc = new RtcBridgeService(
|
_rtc = new RtcBridgeService(
|
||||||
@@ -56,6 +59,8 @@ public partial class MainPage : ContentPage
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// while(!waitFor.IsCompleted){}
|
||||||
|
|
||||||
_socket.Connect();
|
_socket.Connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +137,11 @@ public partial class MainPage : ContentPage
|
|||||||
await _rtc.PushRtcContextToJsAsync();
|
await _rtc.PushRtcContextToJsAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket.SendRaw($"GET_HISTORY|{_username}|{_currentChannelId}");
|
_socket.SendGetHistory(_currentChannelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleEncryptedChat(SocketEncryptedMessage payload) {
|
private void HandleEncryptedChat(SocketEncryptedMessage payload) {
|
||||||
if (payload.RecipientUsername != _username)
|
if (!payload.RecipientUsername.Equals(_username, StringComparison.OrdinalIgnoreCase))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string decryptedText;
|
string decryptedText;
|
||||||
@@ -224,7 +229,7 @@ public partial class MainPage : ContentPage
|
|||||||
RenderCurrentChannelMessages();
|
RenderCurrentChannelMessages();
|
||||||
|
|
||||||
if (!_messagesByChannel.ContainsKey(channel.ChannelId))
|
if (!_messagesByChannel.ContainsKey(channel.ChannelId))
|
||||||
_socket.SendRaw($"GET_HISTORY|{_username}|{channel.ChannelId}");
|
_socket.SendGetHistory(channel.ChannelId);
|
||||||
};
|
};
|
||||||
|
|
||||||
SidebarList.Children.Add(button);
|
SidebarList.Children.Add(button);
|
||||||
|
|||||||
@@ -49,4 +49,25 @@ window.addEventListener("load", async () => {
|
|||||||
Media.wireDeviceSelectors();
|
Media.wireDeviceSelectors();
|
||||||
await Media.loadDevices();
|
await Media.loadDevices();
|
||||||
await Media.ensureLocalMedia();
|
await Media.ensureLocalMedia();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function testIndex(rawJson)
|
||||||
|
{
|
||||||
|
const data = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson;
|
||||||
|
if (data.sdp) {
|
||||||
|
data.sdp = data.sdp.replaceAll("(rn)", "\r\n");
|
||||||
|
}
|
||||||
|
handleRtcSignal(JSON.stringify(data));
|
||||||
|
// if (data.type === "rtc_offer") {
|
||||||
|
// handleOffer(data)
|
||||||
|
// }
|
||||||
|
// if (data.type === "rtc_answer") {
|
||||||
|
// data.sdp = data.sdp.replaceAll("(rn)", "\r\n");
|
||||||
|
// handleAnswer(data)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
function noDataTest()
|
||||||
|
{
|
||||||
|
LogMessage("No Data Called!!");
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const peerConnections = {};
|
const peerConnections = {};
|
||||||
|
|
||||||
async function joinChannelCall() {
|
async function joinChannelCall() {
|
||||||
LogMessage("Current username: " + currentUsername);
|
LogMessage("Current username: " + currentUsername);
|
||||||
@@ -24,7 +24,7 @@ async function joinChannelCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const username of existingUsers) {
|
for (const username of existingUsers) {
|
||||||
await sendOffer(username);
|
await sendOffer(username); //Creates an offer to each person in call for MESH RTC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ async function sendOffer(username) {
|
|||||||
await Media.applyLocalStreamToPeerConnection(pc, username);
|
await Media.applyLocalStreamToPeerConnection(pc, username);
|
||||||
|
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
|
// LogMessage(`Offer created: ${JSON.stringify(offer)}`);
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
|
|
||||||
await RelaySocket.sendRtcSignal({
|
await RelaySocket.sendRtcSignal({
|
||||||
@@ -88,11 +89,12 @@ async function handleRtcSignal(rawJson) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleOffer(msg) {
|
async function handleOffer(msg) {
|
||||||
|
LogMessage(`Offer handler: ${msg}`);
|
||||||
const pc = await ensurePeerConnectionForUser(msg.from);
|
const pc = await ensurePeerConnectionForUser(msg.from);
|
||||||
|
|
||||||
await Media.ensureLocalMedia();
|
await Media.ensureLocalMedia();
|
||||||
await Media.applyLocalStreamToPeerConnection(pc, msg.from);
|
await Media.applyLocalStreamToPeerConnection(pc, msg.from);
|
||||||
|
// const offer = JSON.parse(msg.offer);
|
||||||
await pc.setRemoteDescription({
|
await pc.setRemoteDescription({
|
||||||
type: "offer",
|
type: "offer",
|
||||||
sdp: msg.sdp
|
sdp: msg.sdp
|
||||||
@@ -137,8 +139,14 @@ async function handleIce(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!msg.candidate) return;
|
if (!msg.candidate) return;
|
||||||
|
|
||||||
|
const candidateInit = {
|
||||||
|
candidate: msg.candidate,
|
||||||
|
sdpMid: msg.sdpMid,
|
||||||
|
sdpMLineIndex: msg.sdpMLineIndex
|
||||||
|
};
|
||||||
|
|
||||||
await pc.addIceCandidate(msg.candidate);
|
await pc.addIceCandidate(candidateInit);
|
||||||
|
|
||||||
LogMessage(`Applied ICE from ${msg.from}`);
|
LogMessage(`Applied ICE from ${msg.from}`);
|
||||||
}
|
}
|
||||||
@@ -159,7 +167,9 @@ async function ensurePeerConnectionForUser(username) {
|
|||||||
channelId: currentChannelId,
|
channelId: currentChannelId,
|
||||||
from: currentUsername,
|
from: currentUsername,
|
||||||
to: username,
|
to: username,
|
||||||
candidate: JSON.stringify(event.candidate)
|
candidate: event.candidate.candidate,
|
||||||
|
sdpMid: event.candidate.sdpMid,
|
||||||
|
sdpMLineIndex: event.candidate.sdpMLineIndex
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,51 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using RelayShared.Services;
|
||||||
|
|
||||||
namespace RelayClient;
|
namespace RelayClient;
|
||||||
|
|
||||||
public class ServerAPI
|
public class ServerAPI
|
||||||
{
|
{
|
||||||
static HttpClient client = new HttpClient { BaseAddress = new Uri("http://localhost:5000/") };
|
static HttpClient client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5000/") };
|
||||||
|
static HttpClient core = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:1337/") };
|
||||||
|
// static HttpClient client = new HttpClient { BaseAddress = new Uri("http://192.168.1.92:5000/") };
|
||||||
|
// static HttpClient core = new HttpClient { BaseAddress = new Uri("http://192.168.1.92:1337/") };
|
||||||
|
|
||||||
public static void setupClient()
|
public static async Task setupClient()
|
||||||
{
|
{
|
||||||
client.DefaultRequestHeaders.Accept.Clear();
|
client.DefaultRequestHeaders.Accept.Clear();
|
||||||
client.DefaultRequestHeaders.Accept.Add(
|
client.DefaultRequestHeaders.Accept.Add(
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
core.DefaultRequestHeaders.Accept.Clear();
|
||||||
|
core.DefaultRequestHeaders.Accept.Add(
|
||||||
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
core.DefaultRequestHeaders.Add("User-Agent", "RelayClient");
|
||||||
|
MainPage._userToken = await CoreUserSignin(new AuthSignin
|
||||||
|
{
|
||||||
|
UserName = MainPage._username,
|
||||||
|
Password = "password"
|
||||||
|
});
|
||||||
|
|
||||||
|
await CoreUserAlive(new AuthSignin
|
||||||
|
{
|
||||||
|
UserName = MainPage._username,
|
||||||
|
Password = MainPage._userToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Uri> CoreUserAlive(AuthSignin data)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await core.PostAsJsonAsync("user/isAlive", data);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return response.Headers.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> CoreUserSignin(AuthSignin data)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await core.PostAsJsonAsync("user/signin", data);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Uri> PostOfferAsync(DBOffer offer)
|
public static async Task<Uri> PostOfferAsync(DBOffer offer)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using RelayClient.Crypto;
|
using RelayClient.Crypto;
|
||||||
using RelayShared.Services;
|
using RelayShared.Services;
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
@@ -19,7 +19,7 @@ public sealed class RelaySocketClient
|
|||||||
public event Action<string>? ServerPublicKeyReceived;
|
public event Action<string>? ServerPublicKeyReceived;
|
||||||
public event Action<string>? Log;
|
public event Action<string>? Log;
|
||||||
|
|
||||||
public RelaySocketClient(string username, string url = "ws://localhost:1337/")
|
public RelaySocketClient(string username, string url = "ws://127.0.0.1:5001/")
|
||||||
{
|
{
|
||||||
_username = username;
|
_username = username;
|
||||||
_socket = new WebSocket(url);
|
_socket = new WebSocket(url);
|
||||||
@@ -32,9 +32,57 @@ public sealed class RelaySocketClient
|
|||||||
|
|
||||||
var publicKey = KeyStorage.LoadPublicKey(_username);
|
var publicKey = KeyStorage.LoadPublicKey(_username);
|
||||||
|
|
||||||
SendRaw($"REGISTER_KEY|{_username}|{publicKey}");
|
SendControlMessage(new WsControlMessage
|
||||||
SendRaw("GET_SERVER_KEY");
|
{
|
||||||
SendRaw("GET_CHANNELS");
|
Action = WsAction.Authenticate,
|
||||||
|
Username = _username,
|
||||||
|
Token = MainPage._userToken
|
||||||
|
});
|
||||||
|
|
||||||
|
SendControlMessage(new WsControlMessage
|
||||||
|
{
|
||||||
|
Action = WsAction.RegisterKey,
|
||||||
|
Username = _username,
|
||||||
|
PublicKey = publicKey
|
||||||
|
});
|
||||||
|
|
||||||
|
SendControlMessage(new WsControlMessage { Action = WsAction.GetServerKey });
|
||||||
|
SendControlMessage(new WsControlMessage { Action = WsAction.GetChannels });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendControlMessage(WsControlMessage message)
|
||||||
|
{
|
||||||
|
SendRaw(JsonSerializer.Serialize(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendGetHistory(string channelId)
|
||||||
|
{
|
||||||
|
SendControlMessage(new WsControlMessage
|
||||||
|
{
|
||||||
|
Action = WsAction.GetHistory,
|
||||||
|
Username = _username,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendRtcJoinChannel(string channelId)
|
||||||
|
{
|
||||||
|
SendControlMessage(new WsControlMessage
|
||||||
|
{
|
||||||
|
Action = WsAction.RtcJoin,
|
||||||
|
Username = _username,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendRtcLeaveChannel(string channelId)
|
||||||
|
{
|
||||||
|
SendControlMessage(new WsControlMessage
|
||||||
|
{
|
||||||
|
Action = WsAction.RtcLeave,
|
||||||
|
Username = _username,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendRaw(string message)
|
public void SendRaw(string message)
|
||||||
@@ -58,12 +106,6 @@ public sealed class RelaySocketClient
|
|||||||
|
|
||||||
private void OnMessage(object? sender, MessageEventArgs e)
|
private void OnMessage(object? sender, MessageEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Data.StartsWith("SERVER:REGISTERED_KEY:"))
|
|
||||||
{
|
|
||||||
Log?.Invoke(e.Data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RawMessageReceived?.Invoke(e.Data);
|
RawMessageReceived?.Invoke(e.Data);
|
||||||
Log?.Invoke($"[{_username}] RAW WS DATA: {e.Data}");
|
Log?.Invoke($"[{_username}] RAW WS DATA: {e.Data}");
|
||||||
|
|
||||||
@@ -72,6 +114,31 @@ public sealed class RelaySocketClient
|
|||||||
using var doc = JsonDocument.Parse(e.Data);
|
using var doc = JsonDocument.Parse(e.Data);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Control event responses (WsEvent)
|
||||||
|
if (root.TryGetProperty("Event", out var eventElement))
|
||||||
|
{
|
||||||
|
var wsEvent = (WsEvent)eventElement.GetInt32();
|
||||||
|
|
||||||
|
switch (wsEvent)
|
||||||
|
{
|
||||||
|
case WsEvent.KeyRegistered:
|
||||||
|
Log?.Invoke($"[{_username}] Key registered on server.");
|
||||||
|
return;
|
||||||
|
|
||||||
|
case WsEvent.Authenticated:
|
||||||
|
Log?.Invoke($"[{_username}] Authenticated with server.");
|
||||||
|
return;
|
||||||
|
|
||||||
|
case WsEvent.Error:
|
||||||
|
var detail = root.TryGetProperty("Detail", out var d) ? d.GetString() : null;
|
||||||
|
Log?.Invoke($"[{_username}] Server error: {detail}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data messages (SignalType)
|
||||||
if (!root.TryGetProperty("Type", out var typeElement))
|
if (!root.TryGetProperty("Type", out var typeElement))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -124,4 +191,4 @@ public sealed class RelaySocketClient
|
|||||||
Log?.Invoke($"[{_username}] failed to process websocket message: {ex.Message}");
|
Log?.Invoke($"[{_username}] failed to process websocket message: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using RelayClient.Crypto;
|
using RelayClient.Crypto;
|
||||||
using RelayShared.Rtc;
|
using RelayShared.Rtc;
|
||||||
using RelayShared.Services;
|
using RelayShared.Services;
|
||||||
@@ -30,7 +31,7 @@ public sealed class RtcBridgeService
|
|||||||
if (string.IsNullOrWhiteSpace(channelId))
|
if (string.IsNullOrWhiteSpace(channelId))
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_socket.SendRaw($"RTC_JOIN_CHANNEL|{_username}|{channelId}");
|
_socket.SendRtcJoinChannel(channelId);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ public sealed class RtcBridgeService
|
|||||||
if (string.IsNullOrWhiteSpace(channelId))
|
if (string.IsNullOrWhiteSpace(channelId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_socket.SendRaw($"RTC_LEAVE_CHANNEL|{_username}|{channelId}");
|
_socket.SendRtcLeaveChannel(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendRtcSignal(string json)
|
public void SendRtcSignal(string json)
|
||||||
@@ -70,6 +71,7 @@ public sealed class RtcBridgeService
|
|||||||
rtcSignal.ChannelId ??= _getCurrentChannelId();
|
rtcSignal.ChannelId ??= _getCurrentChannelId();
|
||||||
rtcSignal.From ??= _username;
|
rtcSignal.From ??= _username;
|
||||||
|
|
||||||
|
// _sendRawToWebView($"RTC_SIGNAL file: {JsonSerializer.Serialize(rtcSignal)}");
|
||||||
if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId))
|
if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId))
|
||||||
{
|
{
|
||||||
_sendRawToWebView("SendRtcSignal failed: missing channel id.");
|
_sendRawToWebView("SendRtcSignal failed: missing channel id.");
|
||||||
@@ -116,16 +118,23 @@ public sealed class RtcBridgeService
|
|||||||
|
|
||||||
public async Task HandleIncomingRtcSignalAsync(SocketRtcSignalMessage payload)
|
public async Task HandleIncomingRtcSignalAsync(SocketRtcSignalMessage payload)
|
||||||
{
|
{
|
||||||
|
// _sendRawToWebView("HandleIncomingRtcSignal called");
|
||||||
var currentChannelId = _getCurrentChannelId();
|
var currentChannelId = _getCurrentChannelId();
|
||||||
|
|
||||||
if (payload.ChannelId != currentChannelId)
|
if (payload.ChannelId != currentChannelId)
|
||||||
|
{
|
||||||
|
_sendRawToWebView("Channel id does not match");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.SenderUsername == _username)
|
if (payload.SenderUsername == _username)
|
||||||
|
{
|
||||||
|
_sendRawToWebView("Received own message");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string decryptedJson;
|
string decryptedJson;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
||||||
@@ -152,6 +161,7 @@ public sealed class RtcBridgeService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(decryptedJson);
|
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(decryptedJson);
|
||||||
|
// _sendRawToWebView($"Received Encrypted Signal: [{rtcSignal.From}]: {rtcSignal.Offer}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -160,7 +170,10 @@ public sealed class RtcBridgeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rtcSignal is null)
|
if (rtcSignal is null)
|
||||||
|
{
|
||||||
|
_sendRawToWebView("rtcSignal is null");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(rtcSignal.To) &&
|
if (!string.IsNullOrWhiteSpace(rtcSignal.To) &&
|
||||||
!string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -169,9 +182,9 @@ public sealed class RtcBridgeService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendRawToWebView("Received encrypted RTC signal: " + decryptedJson);
|
// _sendRawToWebView("Received encrypted RTC signal: " + decryptedJson);
|
||||||
|
|
||||||
await SendRtcSignalToJsAsync(decryptedJson);
|
await SendRtcSignalToJsAsync(rtcSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task PushRtcContextToJsAsync()
|
public Task PushRtcContextToJsAsync()
|
||||||
@@ -188,37 +201,55 @@ public sealed class RtcBridgeService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SendRtcSignalToJsAsync(string rawJson)
|
private Task SendRtcSignalToJsAsync(RtcSignalMessage data)
|
||||||
{
|
{
|
||||||
|
if (data.Type == "rtc_offer" || data.Type == "rtc_answer")
|
||||||
|
{
|
||||||
|
data.Sdp = data.Sdp.Replace("\r\n", "(rn)");
|
||||||
|
}
|
||||||
MainThread.BeginInvokeOnMainThread(async () =>
|
MainThread.BeginInvokeOnMainThread(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var jsArg = JsonSerializer.Serialize(rawJson);
|
// await _hybridWebView.InvokeJavaScriptAsync("testIndex", [JsonSerializer.Serialize(data)], [RtcJsType.Default.String]);
|
||||||
|
await _hybridWebView.InvokeJavaScriptAsync("testIndex", [data], [RtcJsType.Default.RtcSignalMessage]);
|
||||||
await _hybridWebView.EvaluateJavaScriptAsync($@"
|
#region OldDebugger
|
||||||
try {{
|
// var jsArg = JsonSerializer.Serialize(data);
|
||||||
window.HybridWebView.SendRawMessage('C# eval entered');
|
//
|
||||||
|
// await _hybridWebView.EvaluateJavaScriptAsync($@"
|
||||||
if (!window.RelaySocket) {{
|
// try {{
|
||||||
window.HybridWebView.SendRawMessage('window.RelaySocket missing');
|
// window.HybridWebView.SendRawMessage('C# eval entered');
|
||||||
}} else if (typeof window.RelaySocket.receiveRtcSignal !== 'function') {{
|
//
|
||||||
window.HybridWebView.SendRawMessage('RelaySocket.receiveRtcSignal missing');
|
// if (!window.RelaySocket) {{
|
||||||
}} else {{
|
// window.HybridWebView.SendRawMessage('window.RelaySocket missing');
|
||||||
window.HybridWebView.SendRawMessage('Calling RelaySocket.receiveRtcSignal');
|
// }} else if (typeof window.RelaySocket.receiveRtcSignal !== 'function') {{
|
||||||
window.RelaySocket.receiveRtcSignal({jsArg});
|
// window.HybridWebView.SendRawMessage('RelaySocket.receiveRtcSignal missing');
|
||||||
}}
|
// }} else {{
|
||||||
}} catch (err) {{
|
// window.HybridWebView.SendRawMessage('Calling RelaySocket.receiveRtcSignal');
|
||||||
window.HybridWebView.SendRawMessage('RTC JS dispatch failed: ' + err);
|
// window.RelaySocket.receiveRtcSignal({jsArg});
|
||||||
}}
|
// }}
|
||||||
");
|
// }} catch (err) {{
|
||||||
|
// window.HybridWebView.SendRawMessage('RTC JS dispatch failed: ' + err);
|
||||||
|
// }}
|
||||||
|
// ");
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_sendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message);
|
_sendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(WriteIndented = false)]
|
||||||
|
[JsonSerializable(typeof(RtcDescription))]
|
||||||
|
[JsonSerializable(typeof(List<RtcSignalMessage>))]
|
||||||
|
[JsonSerializable(typeof(RtcSignalMessage))]
|
||||||
|
[JsonSerializable(typeof(IceCandidate))]
|
||||||
|
[JsonSerializable(typeof(List<IceCandidate>))]
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
internal partial class RtcJsType : JsonSerializerContext
|
||||||
|
{
|
||||||
}
|
}
|
||||||
63
RelayCore/Endpoints/AuthEndpoints.cs
Normal file
63
RelayCore/Endpoints/AuthEndpoints.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using RelayCore.Services;
|
||||||
|
using RelayShared.Services;
|
||||||
|
|
||||||
|
namespace RelayCore.Endpoints;
|
||||||
|
|
||||||
|
public static class AuthEndpoints
|
||||||
|
{
|
||||||
|
public static void MapAuthEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.MapPost("/user/signin", async (AuthSignin request, APIAuthService service, HttpContext context) =>
|
||||||
|
{
|
||||||
|
string ip = "";
|
||||||
|
StringValues userAgent = "";
|
||||||
|
if (context != null)
|
||||||
|
{
|
||||||
|
ip = context.Connection.RemoteIpAddress?.MapToIPv4().ToString();
|
||||||
|
context.Request.Headers.TryGetValue("User-Agent", out userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = await service.UserSigninAsync(request, ip, userAgent.ToString());
|
||||||
|
|
||||||
|
return token != null ? Results.Ok(token) : Results.Unauthorized();
|
||||||
|
});
|
||||||
|
app.MapGet("/users", async (APIAuthService service) =>
|
||||||
|
{
|
||||||
|
return Results.Ok(await service.GetUsersAsync());
|
||||||
|
});
|
||||||
|
app.MapPost("/user/register", async (AuthRegister request, APIAuthService service, HttpContext context) =>
|
||||||
|
{
|
||||||
|
var ip = context.Connection.RemoteIpAddress?.MapToIPv4().ToString();
|
||||||
|
context.Request.Headers.TryGetValue("User-Agent", out var userAgent);
|
||||||
|
|
||||||
|
var token = await service.UserRegisterAsync(request, ip, userAgent);
|
||||||
|
return token != null ? Results.Ok(token) : Results.Ok("Username or Email already exists!");
|
||||||
|
});
|
||||||
|
app.MapPost("/user/isAlive", async (AuthSignin request, HttpContext context) =>
|
||||||
|
{
|
||||||
|
var ip = context.Connection.RemoteIpAddress?.MapToIPv4().ToString();
|
||||||
|
context.Request.Headers.TryGetValue("User-Agent", out var userAgent);
|
||||||
|
|
||||||
|
Console.WriteLine($"UN: {request.UserName}\nToken: {request.Password}\nIP: {ip}\nUserAgent: {userAgent}");
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
app.MapPost("/server/verify/user", async (AuthUserVerify request, APIAuthService service) =>
|
||||||
|
{
|
||||||
|
bool valid = await service.ServerVerifyUser(request);
|
||||||
|
Console.WriteLine($"UN: {request.Username}\nToken: {request.Token}");
|
||||||
|
return Results.Ok(valid);
|
||||||
|
});
|
||||||
|
app.MapPost("/server/license/generate", async (AuthServerLicenseGenerate request, APIAuthService service) =>
|
||||||
|
{
|
||||||
|
var license = await service.ServerLicenseGenerate(request);
|
||||||
|
|
||||||
|
return license != null ? Results.Ok(license) : Results.BadRequest();
|
||||||
|
});
|
||||||
|
app.MapPost("/server/license/verify", async (AuthServerLicenseVerify request, APIAuthService service) =>
|
||||||
|
{
|
||||||
|
bool valid = await service.ServerVerifyLicense(request);
|
||||||
|
return Results.Ok(valid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ namespace RelayCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of threads to use for parallel computation
|
/// Number of threads to use for parallel computation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int DegreeOfParallelism = 1;
|
private const int DegreeOfParallelism = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of iterations for the Argon2id algorithm
|
/// Number of iterations for the Argon2id algorithm
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace RelayCore.Models;
|
|||||||
|
|
||||||
public class Sessions : Record
|
public class Sessions : Record
|
||||||
{
|
{
|
||||||
public required string UserId { get; set; }
|
public required RecordId UserId { get; set; }
|
||||||
public required string TokenHash { get; set; }
|
public required string TokenHash { get; set; }
|
||||||
public required DateTime IssuedAt { get; set; }
|
public required DateTime IssuedAt { get; set; }
|
||||||
public required DateTime ExpiresAt { get; set; }
|
public required DateTime ExpiresAt { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using SurrealDb.Net.Models;
|
using SurrealDb.Net.Models;
|
||||||
|
|
||||||
namespace RelayCore.Models;
|
namespace RelayCore.Models;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
using SurrealDb.Net;
|
using SurrealDb.Net;
|
||||||
using SurrealDb.Net.Models.Auth;
|
using SurrealDb.Net.Models.Auth;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
using RelayCore.Enums;
|
using RelayCore.Enums;
|
||||||
using RelayCore.Models;
|
using RelayCore.Models;
|
||||||
|
using RelayCore.Endpoints;
|
||||||
|
using RelayCore.Services;
|
||||||
|
|
||||||
|
|
||||||
await using var db = new SurrealDbClient("ws://127.0.0.1:8000/rpc");
|
await using var db = new SurrealDbClient("ws://127.0.0.1:8000/rpc");
|
||||||
@@ -25,8 +24,26 @@ Console.WriteLine($"Keeper created: {ToJsonString(keeper)}");
|
|||||||
Console.WriteLine($"Kira created: {ToJsonString(kira)}");
|
Console.WriteLine($"Kira created: {ToJsonString(kira)}");
|
||||||
Console.WriteLine($"Test created: {ToJsonString(test)}");
|
Console.WriteLine($"Test created: {ToJsonString(test)}");
|
||||||
|
|
||||||
await server.Main(db);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.WebHost.UseUrls("http://127.0.0.1:1337/");
|
||||||
|
// builder.WebHost.UseUrls("http://192.168.1.92:1337");
|
||||||
|
builder.Services.AddSingleton(db);
|
||||||
|
builder.Services.AddScoped<APIAuthService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapGet("/", () => "Auth Server Running!");
|
||||||
|
app.MapAuthEndpoints();
|
||||||
|
|
||||||
|
// await server.Main(db);
|
||||||
|
|
||||||
|
await app.StartAsync();
|
||||||
|
Console.WriteLine("API Started");
|
||||||
|
Console.WriteLine("\n\n\n");
|
||||||
|
|
||||||
|
Console.Write("Press any key to stop.");
|
||||||
Console.ReadKey(true);
|
Console.ReadKey(true);
|
||||||
|
|
||||||
|
await app.StopAsync();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
static string ToJsonString(object? o)
|
static string ToJsonString(object? o)
|
||||||
@@ -51,7 +68,7 @@ static async Task<Users> CreateUserAsync(SurrealDbClient db, string username, st
|
|||||||
OnlineStatus = (int)OnlineStatuses.Online,
|
OnlineStatus = (int)OnlineStatuses.Online,
|
||||||
};
|
};
|
||||||
|
|
||||||
var created = await db.Create("users", user);
|
var created = await db.Create("auth_users", user);
|
||||||
|
|
||||||
var hasher = new PasswordHasher();
|
var hasher = new PasswordHasher();
|
||||||
var passwordHash = hasher.HashPassword(created.Id.ToString() + rawPassword);
|
var passwordHash = hasher.HashPassword(created.Id.ToString() + rawPassword);
|
||||||
@@ -65,16 +82,15 @@ static async Task<Users> CreateUserAsync(SurrealDbClient db, string username, st
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
partial class Program
|
partial class Program
|
||||||
{
|
{
|
||||||
public async Task Main(SurrealDbClient db)
|
public async Task Main(SurrealDbClient db)
|
||||||
{
|
{
|
||||||
// Set up listener
|
// Set up listener
|
||||||
using var listener = new HttpListener();
|
using var listener = new HttpListener();
|
||||||
listener.Prefixes.Add("http://localhost:8080/");
|
listener.Prefixes.Add("http://127.0.0.1:8080/");
|
||||||
listener.Start();
|
listener.Start();
|
||||||
Console.WriteLine("API Started: http://localhost:8080/");
|
Console.WriteLine("API Started: http://127.0.0.1:8080/");
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
|
||||||
<PackageReference Include="SurrealDb.Net" Version="0.9.0" />
|
<PackageReference Include="SurrealDb.Net" Version="0.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Services\" />
|
<ProjectReference Include="..\RelayShared\RelayShared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
126
RelayCore/Services/APIAuthService.cs
Normal file
126
RelayCore/Services/APIAuthService.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using RelayCore.Endpoints;
|
||||||
|
using RelayCore.Enums;
|
||||||
|
using RelayCore.Models;
|
||||||
|
using RelayShared.Services;
|
||||||
|
using SurrealDb.Net;
|
||||||
|
using SurrealDb.Net.Models;
|
||||||
|
|
||||||
|
namespace RelayCore.Services;
|
||||||
|
|
||||||
|
public class APIAuthService(SurrealDbClient _db)
|
||||||
|
{
|
||||||
|
public async Task<List<Users>> GetUsersAsync()
|
||||||
|
{
|
||||||
|
var users = await _db.Select<Users>("auth_users");
|
||||||
|
return users.Where(x => x.Username is not null).OrderByDescending(x=>x.CreatedAt).ToList();
|
||||||
|
}
|
||||||
|
public async Task<string?> UserSigninAsync(AuthSignin request, string ip, string userAgent)
|
||||||
|
{
|
||||||
|
var hasher = new PasswordHasher();
|
||||||
|
var users = await _db.Select<Users>("auth_users");
|
||||||
|
var user = users.FirstOrDefault(x => (x.Username.ToLower() == request.UserName.ToLower() ||
|
||||||
|
x.Email.ToLower() == request.UserName.ToLower()) &&
|
||||||
|
hasher.VerifyPassword(x.Id + request.Password, x.Password));
|
||||||
|
if (user == null)
|
||||||
|
return null;
|
||||||
|
var tokens = await _db.Select<Sessions>("auth_sessions");
|
||||||
|
var token = tokens.Where(x => x.UserId == user.Id && x.IpAddress == ip && x.UserAgent == userAgent && !x.Revoked)
|
||||||
|
.OrderByDescending(x => x.ExpiresAt).FirstOrDefault();
|
||||||
|
if (token != null)
|
||||||
|
if (token.ExpiresAt > DateTime.UtcNow)
|
||||||
|
return token.TokenHash;
|
||||||
|
|
||||||
|
//TODO: Generate TOKEN
|
||||||
|
var newToken = hasher.HashPassword($"{request.UserName}{userAgent}");
|
||||||
|
//TODO: Store TOKEN and Username for verification
|
||||||
|
var sessionId = await _db.Create("auth_sessions", new Sessions
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
TokenHash = newToken,
|
||||||
|
IssuedAt = DateTime.UtcNow,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(30),
|
||||||
|
DeviceName = "",
|
||||||
|
Revoked = false,
|
||||||
|
IpAddress = ip,
|
||||||
|
UserAgent = userAgent
|
||||||
|
});
|
||||||
|
//TODO: Add invalidation to TOKENs
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
public async Task<string?> UserRegisterAsync(AuthRegister request, string ip, string userAgent)
|
||||||
|
{
|
||||||
|
var hasher = new PasswordHasher();
|
||||||
|
var users = await _db.Select<Users>("auth_users");
|
||||||
|
var user = users.FirstOrDefault(x => x.Username.ToLower() == request.Username.ToLower() || x.Email.ToLower() == request.Email.ToLower());
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var created = await _db.Create("auth_users", new Users
|
||||||
|
{
|
||||||
|
Username = request.Username,
|
||||||
|
Email = request.Email,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now,
|
||||||
|
LastLogin = now,
|
||||||
|
TwoFactorEnabled = false,
|
||||||
|
EmailVerified = false,
|
||||||
|
AccountStatus = (int)AccountStatuses.Active,
|
||||||
|
OnlineStatus = (int)OnlineStatuses.Online,
|
||||||
|
|
||||||
|
});
|
||||||
|
var passwordHash = hasher.HashPassword(created.Id + request.Password);
|
||||||
|
await _db.Merge<PasswordHash, Users>(new PasswordHash
|
||||||
|
{
|
||||||
|
Id = created.Id,
|
||||||
|
Password = passwordHash
|
||||||
|
});
|
||||||
|
|
||||||
|
return await UserSigninAsync(new AuthSignin{UserName=request.Username, Password = request.Password}, ip, userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ServerVerifyUser(AuthUserVerify request)
|
||||||
|
{
|
||||||
|
var users = await _db.Select<Users>("auth_users");
|
||||||
|
var user = users.FirstOrDefault(x => x.Username == request.Username);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var sessions = await _db.Select<Sessions>("auth_sessions");
|
||||||
|
var session = sessions.FirstOrDefault(x => x.TokenHash == request.Token && x.UserId == user.Id);
|
||||||
|
if (session == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> ServerLicenseGenerate(AuthServerLicenseGenerate request)
|
||||||
|
{
|
||||||
|
var hasher = new PasswordHasher();
|
||||||
|
string token = null;
|
||||||
|
token = hasher.HashPassword(DateTime.Now.ToString("yyyyMMddHHmmss"));
|
||||||
|
var created = await _db.Create("auth_licenses", new DBLicense
|
||||||
|
{
|
||||||
|
Token = token,
|
||||||
|
IsClient = false,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(365),
|
||||||
|
IsExpired = false,
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ServerVerifyLicense(AuthServerLicenseVerify request)
|
||||||
|
{
|
||||||
|
var tokens = await _db.Select<DBLicense>("auth_licenses");
|
||||||
|
var token = tokens.FirstOrDefault(x => x.Token == request.License);
|
||||||
|
if (token != null)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ var bootstrapService = new ServerBootstrapService(db, coreClient, cryptoService)
|
|||||||
await bootstrapService.InitializeAsync();
|
await bootstrapService.InitializeAsync();
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.WebHost.UseUrls("http://127.0.0.1:5000/");
|
||||||
|
// builder.WebHost.UseUrls("http://192.168.1.92:5000/");
|
||||||
|
|
||||||
builder.Services.AddSingleton(db);
|
builder.Services.AddSingleton(db);
|
||||||
builder.Services.AddScoped<RtcCallService>();
|
builder.Services.AddScoped<RtcCallService>();
|
||||||
@@ -30,7 +32,8 @@ var app = builder.Build();
|
|||||||
app.MapGet("/", () => "Server Running!");
|
app.MapGet("/", () => "Server Running!");
|
||||||
app.MapRtcEndpoints();
|
app.MapRtcEndpoints();
|
||||||
|
|
||||||
var wssv = new WebSocketServer("ws://localhost:1337");
|
var wssv = new WebSocketServer("ws://127.0.0.1:5001");
|
||||||
|
// var wssv = new WebSocketServer("ws://192.168.1.92:5001");
|
||||||
wssv.AddWebSocketService<ChatSocketBehavior>("/");
|
wssv.AddWebSocketService<ChatSocketBehavior>("/");
|
||||||
RtcNotificationService.Server = wssv;
|
RtcNotificationService.Server = wssv;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
using RelayServer.Models;
|
using RelayServer.Models;
|
||||||
using RelayServer.Services.Crypto;
|
using RelayServer.Services.Crypto;
|
||||||
using RelayServer.Services.Data;
|
using RelayServer.Services.Data;
|
||||||
@@ -11,9 +12,9 @@ using RelayShared.Services;
|
|||||||
namespace RelayServer.Services.Chat;
|
namespace RelayServer.Services.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles websocket-based chat operations including client key registration,
|
/// Handles websocket-based chat operations including authentication, client key
|
||||||
/// server key retrieval, channel listing, channel history loading, and encrypted
|
/// registration, server key retrieval, channel listing, channel history loading,
|
||||||
/// channel message relay.
|
/// and encrypted channel message relay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChatSocketBehavior : WebSocketBehavior
|
public class ChatSocketBehavior : WebSocketBehavior
|
||||||
{
|
{
|
||||||
@@ -25,82 +26,328 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
public static SurrealDb.Net.SurrealDbClient? Db { get; set; }
|
public static SurrealDb.Net.SurrealDbClient? Db { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Routes incoming websocket messages to the appropriate chat handler.
|
/// Routes incoming websocket messages to the appropriate handler via JSON dispatch.
|
||||||
|
/// Control messages carry an <c>Action</c> property; data messages carry a <c>Type</c> property.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="e">The websocket message event arguments.</param>
|
|
||||||
protected override void OnMessage(MessageEventArgs e)
|
protected override void OnMessage(MessageEventArgs e)
|
||||||
{
|
{
|
||||||
var msg = e.Data;
|
var msg = e.Data;
|
||||||
Console.WriteLine(msg);
|
|
||||||
|
|
||||||
if (msg.StartsWith("REGISTER_KEY|"))
|
|
||||||
{
|
|
||||||
HandleRegisterKey(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg == "GET_SERVER_KEY")
|
|
||||||
{
|
|
||||||
HandleGetServerKey();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg == "GET_CHANNELS")
|
|
||||||
{
|
|
||||||
HandleGetChannels();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.StartsWith("GET_HISTORY|"))
|
|
||||||
{
|
|
||||||
HandleGetHistory(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.StartsWith("RTC_JOIN_CHANNEL|"))
|
|
||||||
{
|
|
||||||
HandleRtcJoinChannel(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.StartsWith("RTC_LEAVE_CHANNEL|"))
|
|
||||||
{
|
|
||||||
HandleRtcLeaveChannel(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsEncryptedRtcSignal(msg))
|
|
||||||
{
|
|
||||||
HandleEncryptedRtcSignal(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleEncryptedChatMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEncryptedRtcSignal(string msg)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(msg);
|
using var doc = JsonDocument.Parse(msg);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
if (!root.TryGetProperty("Type", out var typeProp))
|
if (root.TryGetProperty("Action", out var actionProp))
|
||||||
return false;
|
{
|
||||||
|
var action = (WsAction)actionProp.GetInt32();
|
||||||
|
var control = JsonSerializer.Deserialize<WsControlMessage>(msg)!;
|
||||||
|
DispatchControl(action, control);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var type = (SignalType)typeProp.GetInt32();
|
if (root.TryGetProperty("Type", out var typeProp))
|
||||||
|
{
|
||||||
|
var type = (SignalType)typeProp.GetInt32();
|
||||||
|
|
||||||
return type == SignalType.EncryptedSignal;
|
switch (type)
|
||||||
|
{
|
||||||
|
case SignalType.EncryptedSignal:
|
||||||
|
HandleEncryptedRtcSignal(msg);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case SignalType.ClientEncryptedChat:
|
||||||
|
HandleEncryptedChatMessage(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Unrecognised WebSocket message from session={ID}: {msg[..Math.Min(200, msg.Length)]}");
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return false;
|
Console.WriteLine($"WebSocket message error: session={ID}, error={ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispatches a control message to the correct handler based on its action.
|
||||||
|
/// </summary>
|
||||||
|
private void DispatchControl(WsAction action, WsControlMessage control)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case WsAction.Authenticate:
|
||||||
|
HandleAuthenticate(control);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.RegisterKey:
|
||||||
|
HandleRegisterKey(control);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.GetServerKey:
|
||||||
|
HandleGetServerKey();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.GetChannels:
|
||||||
|
HandleGetChannels();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.GetHistory:
|
||||||
|
HandleGetHistory(control);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.RtcJoin:
|
||||||
|
HandleRtcJoinChannel(control);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WsAction.RtcLeave:
|
||||||
|
HandleRtcLeaveChannel(control);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Console.WriteLine($"Unknown WsAction {action} from session={ID}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Control handlers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies a user token with the Core service. The HTTP call is wrapped in
|
||||||
|
/// a try-catch so that a network failure never crashes the WebSocket session.
|
||||||
|
/// </summary>
|
||||||
|
private async void HandleAuthenticate(WsControlMessage control)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.Token))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid Authenticate payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var core = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:1337") };
|
||||||
|
core.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
core.DefaultRequestHeaders.Add("User-Agent", "RelayServer");
|
||||||
|
|
||||||
|
var response = await core.PostAsJsonAsync("/server/verify/user", new AuthUserVerify
|
||||||
|
{
|
||||||
|
Username = control.Username,
|
||||||
|
Token = control.Token
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"Auth response for {control.Username}: {await response.Content.ReadAsStringAsync()}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Auth verification failed for {control.Username}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new WsEventMessage { Event = WsEvent.Authenticated, Detail = control.Username };
|
||||||
|
Send(JsonSerializer.Serialize(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores (or updates) the client's public key and registers the session in
|
||||||
|
/// <see cref="ConnectedClientService"/> so targeted delivery can resolve session ids.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleRegisterKey(WsControlMessage control)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(control.Username) || string.IsNullOrWhiteSpace(control.PublicKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid RegisterKey payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientKeyService is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ClientKeyService is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterOrUpdateClientKeySync(control.Username, control.PublicKey);
|
||||||
|
ConnectedClientService.Register(ID, control.Username);
|
||||||
|
|
||||||
|
Console.WriteLine($"Registered key and session for {control.Username} (session={ID})");
|
||||||
|
|
||||||
|
var result = new WsEventMessage { Event = WsEvent.KeyRegistered, Detail = control.Username };
|
||||||
|
Send(JsonSerializer.Serialize(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the server's public key to the requesting client.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleGetServerKey()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ServerPublicKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server public key is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new ServerPublicKeyMessage
|
||||||
|
{
|
||||||
|
Type = SignalType.ServerPublicKey,
|
||||||
|
PublicKey = ServerPublicKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Send(JsonSerializer.Serialize(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the current list of channels to the connected client.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleGetChannels()
|
||||||
|
{
|
||||||
|
if (Db is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Db is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channels = GetChannelsSync()
|
||||||
|
.OrderBy(c => c.CreatedAt)
|
||||||
|
.Select(c => new ChannelItem
|
||||||
|
{
|
||||||
|
ChannelId = GetRecordId(c.Id),
|
||||||
|
Name = c.Name,
|
||||||
|
CreatedAt = c.CreatedAt
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var payload = new SocketChannelList
|
||||||
|
{
|
||||||
|
Type = SignalType.ChannelList,
|
||||||
|
Channels = channels
|
||||||
|
};
|
||||||
|
|
||||||
|
Send(JsonSerializer.Serialize(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads stored channel history for a specific channel, decrypts it from
|
||||||
|
/// database storage format, and sends it back encrypted for the requesting client.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleGetHistory(WsControlMessage control)
|
||||||
|
{
|
||||||
|
var username = control.Username;
|
||||||
|
var channelId = control.ChannelId;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(channelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid GetHistory payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("History dependencies are not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetClient = GetClientPublicKeyByUsernameSync(username);
|
||||||
|
|
||||||
|
if (targetClient is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No public key found for history request user {username}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allMessages = GetChannelMessagesSync();
|
||||||
|
|
||||||
|
var channelMessages = allMessages
|
||||||
|
.Where(m => m.ChannelId == channelId)
|
||||||
|
.OrderBy(m => m.CreatedAt)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Sending {channelMessages.Count} history messages to {username}");
|
||||||
|
|
||||||
|
foreach (var dbMessage in channelMessages)
|
||||||
|
{
|
||||||
|
string plainText;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
plainText = ChannelCryptoService.Decrypt(
|
||||||
|
dbMessage.CipherText,
|
||||||
|
dbMessage.Nonce,
|
||||||
|
dbMessage.Tag,
|
||||||
|
ChannelDbKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to decrypt DB history row {dbMessage.Id}: {ex.Message}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey);
|
||||||
|
|
||||||
|
var outbound = new SocketEncryptedMessage
|
||||||
|
{
|
||||||
|
Type = SignalType.EncryptedChat,
|
||||||
|
SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId),
|
||||||
|
RecipientUsername = username,
|
||||||
|
ChannelId = channelId,
|
||||||
|
CipherText = encrypted.CipherText,
|
||||||
|
Nonce = encrypted.Nonce,
|
||||||
|
Tag = encrypted.Tag,
|
||||||
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
|
};
|
||||||
|
|
||||||
|
Send(JsonSerializer.Serialize(outbound));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRtcJoinChannel(WsControlMessage control)
|
||||||
|
{
|
||||||
|
var username = control.Username;
|
||||||
|
var channelId = control.ChannelId;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(channelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid RtcJoin payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RtcChannelPresenceService.SetUser(ID, username);
|
||||||
|
RtcChannelPresenceService.JoinChannel(ID, channelId);
|
||||||
|
|
||||||
|
Console.WriteLine($"RTC presence joined: session={ID}, user={username}, channel={channelId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRtcLeaveChannel(WsControlMessage control)
|
||||||
|
{
|
||||||
|
var username = control.Username;
|
||||||
|
var channelId = control.ChannelId;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(channelId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid RtcLeave payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RtcChannelPresenceService.IsInChannel(ID, channelId))
|
||||||
|
RtcChannelPresenceService.LeaveChannel(ID);
|
||||||
|
|
||||||
|
Console.WriteLine($"RTC presence left: session={ID}, user={username}, channel={channelId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Data message handlers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrypts an incoming encrypted RTC signal and re-encrypts it for every
|
||||||
|
/// other session in the same RTC channel.
|
||||||
|
/// </summary>
|
||||||
private void HandleEncryptedRtcSignal(string msg)
|
private void HandleEncryptedRtcSignal(string msg)
|
||||||
{
|
{
|
||||||
Console.WriteLine("RTC SIGNAL HIT");
|
Console.WriteLine("RTC SIGNAL HIT");
|
||||||
|
|
||||||
SocketRtcSignalMessage? clientPayload;
|
SocketRtcSignalMessage? clientPayload;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -176,123 +423,12 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
|
|
||||||
Console.WriteLine($"Forwarded encrypted RTC signal from {clientPayload.SenderUsername} to channel {clientPayload.ChannelId}");
|
Console.WriteLine($"Forwarded encrypted RTC signal from {clientPayload.SenderUsername} to channel {clientPayload.ChannelId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="e"></param>
|
|
||||||
protected override void OnClose(CloseEventArgs e)
|
|
||||||
{
|
|
||||||
RtcChannelPresenceService.RemoveSession(ID);
|
|
||||||
Console.WriteLine($"WebSocket closed: session={ID}, code={e.Code}, reason={e.Reason}");
|
|
||||||
base.OnClose(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnError(ErrorEventArgs e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"WebSocket error: session={ID}, message={e.Message}");
|
|
||||||
base.OnError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts a display username from a stored user record id value.
|
/// Decrypts an incoming encrypted chat message, stores it in the database,
|
||||||
|
/// then re-encrypts and delivers it individually to every connected server member.
|
||||||
|
/// Messages are never broadcast — each recipient receives their own encrypted copy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="senderUserId">The stored sender user id.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The extracted username when possible; otherwise, a fallback value.
|
|
||||||
/// </returns>
|
|
||||||
private static string ExtractUsernameFromUserId(string senderUserId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(senderUserId))
|
|
||||||
return "Unknown";
|
|
||||||
|
|
||||||
var parts = senderUserId.Split(':', 2);
|
|
||||||
return parts.Length == 2 ? parts[1] : senderUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers or updates a client's public key from a websocket registration payload.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="msg">The raw websocket registration message.</param>
|
|
||||||
private void HandleRegisterKey(string msg)
|
|
||||||
{
|
|
||||||
var parts = msg.Split('|', 3);
|
|
||||||
|
|
||||||
if (parts.Length < 3)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Invalid REGISTER_KEY payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = parts[1];
|
|
||||||
var publicKey = parts[2];
|
|
||||||
|
|
||||||
if (ClientKeyService is null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("ClientKeyService is not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterOrUpdateClientKeySync(username, publicKey);
|
|
||||||
|
|
||||||
Send($"SERVER:REGISTERED_KEY:{username}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the current list of channels to the connected websocket client.
|
|
||||||
/// </summary>
|
|
||||||
private void HandleGetChannels()
|
|
||||||
{
|
|
||||||
if (Db is null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Db is not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//TODO: Update to include ChannelType and Group String on channels
|
|
||||||
var channels = GetChannelsSync()
|
|
||||||
.OrderBy(c => c.CreatedAt)
|
|
||||||
.Select(c => new ChannelItem()
|
|
||||||
{
|
|
||||||
ChannelId = GetRecordId(c.Id),
|
|
||||||
Name = c.Name,
|
|
||||||
CreatedAt = c.CreatedAt
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var payload = new SocketChannelList
|
|
||||||
{
|
|
||||||
Type = SignalType.ChannelList,
|
|
||||||
Channels = channels
|
|
||||||
};
|
|
||||||
|
|
||||||
Send(JsonSerializer.Serialize(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the server's public key to the connected websocket client.
|
|
||||||
/// </summary>
|
|
||||||
private void HandleGetServerKey()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(ServerPublicKey))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Server public key is not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = new ServerPublicKeyMessage
|
|
||||||
{
|
|
||||||
Type = SignalType.ServerPublicKey,
|
|
||||||
PublicKey = ServerPublicKey
|
|
||||||
};
|
|
||||||
|
|
||||||
Send(JsonSerializer.Serialize(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decrypts an incoming encrypted chat payload, stores it in the database,
|
|
||||||
/// and rebroadcasts it to connected clients encrypted with each client's public key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="msg">The raw encrypted chat websocket message.</param>
|
|
||||||
private void HandleEncryptedChatMessage(string msg)
|
private void HandleEncryptedChatMessage(string msg)
|
||||||
{
|
{
|
||||||
SocketEncryptedMessage? clientPayload;
|
SocketEncryptedMessage? clientPayload;
|
||||||
@@ -335,9 +471,10 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Server decrypted message from {clientPayload.SenderUsername}: {plainText}");
|
Console.WriteLine($"Server decrypted message from {clientPayload.SenderUsername}: {plainText}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dbEncrypted = ChannelCryptoService.Encrypt(plainText, ChannelDbKey);
|
var dbEncrypted = ChannelCryptoService!.Encrypt(plainText, ChannelDbKey);
|
||||||
|
|
||||||
var savedMessage = CreateChannelMessageSync(new ChannelMessages
|
var savedMessage = CreateChannelMessageSync(new ChannelMessages
|
||||||
{
|
{
|
||||||
@@ -357,19 +494,40 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var allKeys = GetAllClientPublicKeysSync();
|
// Deliver to every connected server member individually.
|
||||||
|
var members = GetServerMembersSync();
|
||||||
|
|
||||||
foreach (var client in allKeys)
|
foreach (var member in members)
|
||||||
{
|
{
|
||||||
var encrypted = E2EeHelper.EncryptForRecipient(plainText, client.PublicKey);
|
// Derive the lowercase username from the stored record id (e.g. "users:keeper317").
|
||||||
|
var rawUsername = ExtractUsernameFromUserId(member.UserId);
|
||||||
|
|
||||||
Console.WriteLine($"Encrypting outbound message from {clientPayload.SenderUsername} for {client.Username}");
|
// Find all active sessions for this member (supports multi-device).
|
||||||
|
var sessionIds = ConnectedClientService.GetSessionsForUser(rawUsername);
|
||||||
|
if (sessionIds.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Resolve the correctly-cased username as the client registered it.
|
||||||
|
var properUsername = sessionIds
|
||||||
|
.Select(ConnectedClientService.GetUsernameForSession)
|
||||||
|
.FirstOrDefault(u => u is not null) ?? rawUsername;
|
||||||
|
|
||||||
|
var clientKey = GetClientPublicKeyByUsernameSync(properUsername);
|
||||||
|
if (clientKey is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No public key for {properUsername}, skipping.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, clientKey.PublicKey);
|
||||||
|
|
||||||
|
Console.WriteLine($"Encrypting outbound message from {clientPayload.SenderUsername} for {properUsername}");
|
||||||
|
|
||||||
var outbound = new SocketEncryptedMessage
|
var outbound = new SocketEncryptedMessage
|
||||||
{
|
{
|
||||||
Type = SignalType.EncryptedChat,
|
Type = SignalType.EncryptedChat,
|
||||||
SenderUsername = clientPayload.SenderUsername,
|
SenderUsername = clientPayload.SenderUsername,
|
||||||
RecipientUsername = client.Username,
|
RecipientUsername = properUsername,
|
||||||
ChannelId = clientPayload.ChannelId,
|
ChannelId = clientPayload.ChannelId,
|
||||||
CipherText = encrypted.CipherText,
|
CipherText = encrypted.CipherText,
|
||||||
Nonce = encrypted.Nonce,
|
Nonce = encrypted.Nonce,
|
||||||
@@ -377,117 +535,35 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
EncryptedKey = encrypted.EncryptedKey
|
EncryptedKey = encrypted.EncryptedKey
|
||||||
};
|
};
|
||||||
|
|
||||||
Sessions.Broadcast(JsonSerializer.Serialize(outbound));
|
var json = JsonSerializer.Serialize(outbound);
|
||||||
|
|
||||||
|
foreach (var sessionId in sessionIds)
|
||||||
|
Sessions.SendTo(json, sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// -------------------------------------------------------------------------
|
||||||
/// Loads stored channel history for a specific user and channel, decrypts it from
|
// Lifecycle
|
||||||
/// database storage format, and sends it back encrypted for the requesting client.
|
// -------------------------------------------------------------------------
|
||||||
/// </summary>
|
|
||||||
/// <param name="msg">The raw history request websocket message.</param>
|
protected override void OnClose(CloseEventArgs e)
|
||||||
private void HandleGetHistory(string msg)
|
|
||||||
{
|
{
|
||||||
var parts = msg.Split('|', 3);
|
ConnectedClientService.Unregister(ID);
|
||||||
|
RtcChannelPresenceService.RemoveSession(ID);
|
||||||
if (parts.Length < 3)
|
Console.WriteLine($"WebSocket closed: session={ID}, code={e.Code}, reason={e.Reason}");
|
||||||
{
|
base.OnClose(e);
|
||||||
Console.WriteLine("Invalid GET_HISTORY payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = parts[1];
|
|
||||||
var channelId = parts[2];
|
|
||||||
|
|
||||||
if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey))
|
|
||||||
{
|
|
||||||
Console.WriteLine("History dependencies are not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetClient = GetClientPublicKeyByUsernameSync(username);
|
|
||||||
|
|
||||||
if (targetClient is null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"No public key found for history request user {username}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allMessages = GetChannelMessagesSync();
|
|
||||||
|
|
||||||
var channelMessages = allMessages
|
|
||||||
.Where(m => m.ChannelId == channelId)
|
|
||||||
.OrderBy(m => m.CreatedAt)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Console.WriteLine($"Sending {channelMessages.Count} history messages to {username}");
|
|
||||||
|
|
||||||
foreach (var dbMessage in channelMessages)
|
|
||||||
{
|
|
||||||
string plainText;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
plainText = ChannelCryptoService.Decrypt(
|
|
||||||
dbMessage.CipherText,
|
|
||||||
dbMessage.Nonce,
|
|
||||||
dbMessage.Tag,
|
|
||||||
ChannelDbKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to decrypt DB history row {dbMessage.Id}: {ex.Message}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey);
|
|
||||||
|
|
||||||
var outbound = new SocketEncryptedMessage
|
|
||||||
{
|
|
||||||
Type = SignalType.EncryptedChat,
|
|
||||||
SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId),
|
|
||||||
RecipientUsername = username,
|
|
||||||
ChannelId = channelId,
|
|
||||||
CipherText = encrypted.CipherText,
|
|
||||||
Nonce = encrypted.Nonce,
|
|
||||||
Tag = encrypted.Tag,
|
|
||||||
EncryptedKey = encrypted.EncryptedKey
|
|
||||||
};
|
|
||||||
|
|
||||||
Send(JsonSerializer.Serialize(outbound));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void OnError(ErrorEventArgs e)
|
||||||
/// Converts a SurrealDB record id object into a table:id string representation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The raw record id object.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A formatted record id string, or an empty string if the input is null.
|
|
||||||
/// </returns>
|
|
||||||
private static string GetRecordId(object? id)
|
|
||||||
{
|
{
|
||||||
if (id is null)
|
Console.WriteLine($"WebSocket error: session={ID}, message={e.Message}");
|
||||||
return string.Empty;
|
base.OnError(e);
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(id);
|
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
|
|
||||||
var root = doc.RootElement;
|
|
||||||
|
|
||||||
var recordId = root.GetProperty("Id").GetString() ?? string.Empty;
|
|
||||||
var table = root.GetProperty("Table").GetString() ?? string.Empty;
|
|
||||||
|
|
||||||
return $"{table}:{recordId}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// -------------------------------------------------------------------------
|
||||||
/// Synchronously registers or updates a stored client public key using the async key service.
|
// Sync DB helpers
|
||||||
/// </summary>
|
// -------------------------------------------------------------------------
|
||||||
/// <param name="username">The client username.</param>
|
|
||||||
/// <param name="publicKey">The client's public key.</param>
|
|
||||||
private void RegisterOrUpdateClientKeySync(string username, string publicKey)
|
private void RegisterOrUpdateClientKeySync(string username, string publicKey)
|
||||||
{
|
{
|
||||||
Task.Run(async () => await ClientKeyService!.RegisterOrUpdateKeyAsync(username, publicKey))
|
Task.Run(async () => await ClientKeyService!.RegisterOrUpdateKeyAsync(username, publicKey))
|
||||||
@@ -495,10 +571,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Synchronously loads all channels from the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A list of channel records.</returns>
|
|
||||||
private List<Channels> GetChannelsSync()
|
private List<Channels> GetChannelsSync()
|
||||||
{
|
{
|
||||||
return Task.Run(async () => await Db!.Select<Channels>("channels"))
|
return Task.Run(async () => await Db!.Select<Channels>("channels"))
|
||||||
@@ -507,13 +579,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Synchronously gets the stored public key record for the specified user.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="username">The username to look up.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The matching client public key record, or null if none exists.
|
|
||||||
/// </returns>
|
|
||||||
private ClientPublicKeys? GetClientPublicKeyByUsernameSync(string username)
|
private ClientPublicKeys? GetClientPublicKeyByUsernameSync(string username)
|
||||||
{
|
{
|
||||||
return Task.Run(async () => await ClientKeyService!.GetByUsernameAsync(username))
|
return Task.Run(async () => await ClientKeyService!.GetByUsernameAsync(username))
|
||||||
@@ -521,21 +586,6 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Synchronously loads all stored client public key records.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A list of all client public key records.</returns>
|
|
||||||
private List<ClientPublicKeys> GetAllClientPublicKeysSync()
|
|
||||||
{
|
|
||||||
return Task.Run(async () => await ClientKeyService!.GetAllAsync())
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Synchronously loads all stored channel messages from the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A list of channel message records.</returns>
|
|
||||||
private List<ChannelMessages> GetChannelMessagesSync()
|
private List<ChannelMessages> GetChannelMessagesSync()
|
||||||
{
|
{
|
||||||
return Task.Run(async () => await Db!.Select<ChannelMessages>("channel_messages"))
|
return Task.Run(async () => await Db!.Select<ChannelMessages>("channel_messages"))
|
||||||
@@ -544,22 +594,56 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Synchronously creates a new channel message record in the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message record to create.</param>
|
|
||||||
/// <returns>The created channel message record.</returns>
|
|
||||||
private ChannelMessages CreateChannelMessageSync(ChannelMessages message)
|
private ChannelMessages CreateChannelMessageSync(ChannelMessages message)
|
||||||
{
|
{
|
||||||
return Task.Run(async () => await Db!.Create("channel_messages", message))
|
return Task.Run(async () => await Db!.Create("channel_messages", message))
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ServerMembers> GetServerMembersSync()
|
||||||
|
{
|
||||||
|
return Task.Run(async () => await Db!.Select<ServerMembers>("server_members"))
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Utilities
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Extracts a display username from a stored user record id value
|
||||||
|
/// (e.g. "users:keeper317" → "keeper317").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
private static string ExtractUsernameFromUserId(string senderUserId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(senderUserId))
|
||||||
|
return "Unknown";
|
||||||
|
|
||||||
|
var parts = senderUserId.Split(':', 2);
|
||||||
|
return parts.Length == 2 ? parts[1] : senderUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a SurrealDB record id object into a "table:id" string.
|
||||||
|
/// </summary>
|
||||||
|
private static string GetRecordId(object? id)
|
||||||
|
{
|
||||||
|
if (id is null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(id);
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var recordId = root.GetProperty("Id").GetString() ?? string.Empty;
|
||||||
|
var table = root.GetProperty("Table").GetString() ?? string.Empty;
|
||||||
|
|
||||||
|
return $"{table}:{recordId}";
|
||||||
|
}
|
||||||
|
|
||||||
private bool EnsureCoreReady()
|
private bool EnsureCoreReady()
|
||||||
{
|
{
|
||||||
if (ClientKeyService is null || Db is null)
|
if (ClientKeyService is null || Db is null)
|
||||||
@@ -570,11 +654,7 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private bool EnsureCryptoReady()
|
private bool EnsureCryptoReady()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(ServerPrivateKey) || string.IsNullOrWhiteSpace(ChannelDbKey))
|
if (string.IsNullOrWhiteSpace(ServerPrivateKey) || string.IsNullOrWhiteSpace(ChannelDbKey))
|
||||||
@@ -591,50 +671,4 @@ public class ChatSocketBehavior : WebSocketBehavior
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="msg"></param>
|
|
||||||
private void HandleRtcJoinChannel(string msg)
|
|
||||||
{
|
|
||||||
var parts = msg.Split('|', 3);
|
|
||||||
if (parts.Length < 3)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Invalid RTC_JOIN_CHANNEL payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = parts[1];
|
|
||||||
var channelId = parts[2];
|
|
||||||
|
|
||||||
RtcChannelPresenceService.SetUser(ID, username);
|
|
||||||
RtcChannelPresenceService.JoinChannel(ID, channelId);
|
|
||||||
|
|
||||||
Console.WriteLine($"RTC presence joined: session={ID}, user={username}, channel={channelId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="msg"></param>
|
|
||||||
private void HandleRtcLeaveChannel(string msg)
|
|
||||||
{
|
|
||||||
var parts = msg.Split('|', 3);
|
|
||||||
if (parts.Length < 3)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Invalid RTC_LEAVE_CHANNEL payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = parts[1];
|
|
||||||
var channelId = parts[2];
|
|
||||||
|
|
||||||
if (RtcChannelPresenceService.IsInChannel(ID, channelId))
|
|
||||||
{
|
|
||||||
RtcChannelPresenceService.LeaveChannel(ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"RTC presence left: session={ID}, user={username}, channel={channelId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
56
RelayServer/Services/Chat/ConnectedClientService.cs
Normal file
56
RelayServer/Services/Chat/ConnectedClientService.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace RelayServer.Services.Chat;
|
||||||
|
|
||||||
|
public static class ConnectedClientService
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, string> SessionToUsername = new();
|
||||||
|
private static readonly ConcurrentDictionary<string, HashSet<string>> UsernameToSessions =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static void Register(string sessionId, string username)
|
||||||
|
{
|
||||||
|
if (SessionToUsername.TryGetValue(sessionId, out var oldUsername) &&
|
||||||
|
!string.Equals(oldUsername, username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
RemoveSessionFromUsername(sessionId, oldUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionToUsername[sessionId] = username;
|
||||||
|
|
||||||
|
var sessions = UsernameToSessions.GetOrAdd(username, _ => new HashSet<string>(StringComparer.Ordinal));
|
||||||
|
lock (sessions)
|
||||||
|
sessions.Add(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Unregister(string sessionId)
|
||||||
|
{
|
||||||
|
if (SessionToUsername.TryRemove(sessionId, out var username))
|
||||||
|
RemoveSessionFromUsername(sessionId, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<string> GetSessionsForUser(string username)
|
||||||
|
{
|
||||||
|
if (UsernameToSessions.TryGetValue(username, out var sessions))
|
||||||
|
lock (sessions)
|
||||||
|
return sessions.ToList();
|
||||||
|
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetUsernameForSession(string sessionId) =>
|
||||||
|
SessionToUsername.TryGetValue(sessionId, out var u) ? u : null;
|
||||||
|
|
||||||
|
private static void RemoveSessionFromUsername(string sessionId, string username)
|
||||||
|
{
|
||||||
|
if (!UsernameToSessions.TryGetValue(username, out var sessions))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (sessions)
|
||||||
|
{
|
||||||
|
sessions.Remove(sessionId);
|
||||||
|
if (sessions.Count == 0)
|
||||||
|
UsernameToSessions.TryRemove(username, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
RelayShared/Services/Authentication.cs
Normal file
40
RelayShared/Services/Authentication.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
namespace RelayShared.Services;
|
||||||
|
|
||||||
|
public class AuthSignin
|
||||||
|
{
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthRegister
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthUserVerify
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthServerLicenseVerify
|
||||||
|
{
|
||||||
|
public string License { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthServerLicenseGenerate
|
||||||
|
{
|
||||||
|
public string Server { get; set; }
|
||||||
|
public string Length {get; set;} //TODO: Convert to Enum
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DBLicense
|
||||||
|
{
|
||||||
|
public string Token {get; set;}
|
||||||
|
public bool IsClient {get; set;}
|
||||||
|
public DateTime CreatedAt {get; set;}
|
||||||
|
public DateTime ExpiresAt {get; set;}
|
||||||
|
public bool IsExpired {get; set;}
|
||||||
|
}
|
||||||
34
RelayShared/Services/WsControlMessage.cs
Normal file
34
RelayShared/Services/WsControlMessage.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace RelayShared.Services;
|
||||||
|
|
||||||
|
public enum WsAction
|
||||||
|
{
|
||||||
|
Authenticate,
|
||||||
|
RegisterKey,
|
||||||
|
GetServerKey,
|
||||||
|
GetChannels,
|
||||||
|
GetHistory,
|
||||||
|
RtcJoin,
|
||||||
|
RtcLeave
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WsEvent
|
||||||
|
{
|
||||||
|
Authenticated,
|
||||||
|
KeyRegistered,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WsControlMessage
|
||||||
|
{
|
||||||
|
public WsAction Action { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public string? ChannelId { get; set; }
|
||||||
|
public string? PublicKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WsEventMessage
|
||||||
|
{
|
||||||
|
public WsEvent Event { get; set; }
|
||||||
|
public string? Detail { get; set; }
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ Start-Sleep -Seconds 5
|
|||||||
|
|
||||||
$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 5
|
||||||
& '$clientExe' --user Test
|
& '$clientExe' --user Test
|
||||||
"@
|
"@
|
||||||
|
|
||||||
|
|||||||
63
start-servers.ps1
Normal file
63
start-servers.ps1
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
$dockerExe = (Get-Command docker.exe).Source
|
||||||
|
$dotnetExe = (Get-Command dotnet.exe).Source
|
||||||
|
$ps = (Get-Command powershell.exe).Source
|
||||||
|
|
||||||
|
Write-Host "Building RelayCore..."
|
||||||
|
& $dotnetExe build .\RelayCore\RelayCore.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayCore build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayServer..."
|
||||||
|
& $dotnetExe build .\RelayServer\RelayServer.csproj
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayServer build failed." }
|
||||||
|
|
||||||
|
Write-Host "Building RelayClient (Windows only)..."
|
||||||
|
& $dotnetExe build .\RelayClient\RelayClient.csproj -f net10.0-windows10.0.19041.0
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "RelayClient build failed." }
|
||||||
|
|
||||||
|
$coreDll = Join-Path $root "RelayCore\bin\Debug\net9.0\RelayCore.dll"
|
||||||
|
$serverDll = Join-Path $root "RelayServer\bin\Debug\net10.0\RelayServer.dll"
|
||||||
|
|
||||||
|
$tempDir = Join-Path $env:TEMP "RelayTabs"
|
||||||
|
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
|
||||||
|
|
||||||
|
function New-TabScript {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Content
|
||||||
|
)
|
||||||
|
|
||||||
|
$path = Join-Path $tempDir "$Name.ps1"
|
||||||
|
Set-Content -Path $path -Value $Content -Encoding UTF8
|
||||||
|
return $path
|
||||||
|
}
|
||||||
|
|
||||||
|
$dockerScript = New-TabScript -Name "SurrealDB" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
& '$dockerExe' run --rm -p 8000:8000 -v /mydata:/mydata surrealdb/surrealdb:v2.2.1 start --user root --pass secret
|
||||||
|
"@
|
||||||
|
|
||||||
|
$coreScript = New-TabScript -Name "RelayCore" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
& '$dotnetExe' '$coreDll'
|
||||||
|
"@
|
||||||
|
|
||||||
|
$serverScript = New-TabScript -Name "RelayServer" -Content @"
|
||||||
|
Set-Location '$root'
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
& '$dotnetExe' '$serverDll'
|
||||||
|
"@
|
||||||
|
|
||||||
|
$wtArgs = @(
|
||||||
|
"new-tab --title `"SurrealDB`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$dockerScript`"",
|
||||||
|
"new-tab --title `"RelayCore`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$coreScript`"",
|
||||||
|
"new-tab --title `"RelayServer`" `"$ps`" -NoExit -ExecutionPolicy Bypass -File `"$serverScript`""
|
||||||
|
) -join " ; "
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Everything started."
|
||||||
|
Write-Host "Close out terminal to end all applications."
|
||||||
|
Start-Process wt.exe -ArgumentList $wtArgs
|
||||||
Reference in New Issue
Block a user