diff --git a/CrystalConnector/Configs/ConfigAccessor.cs b/CrystalConnector/Configs/ConfigAccessor.cs index b048812..407ada2 100644 --- a/CrystalConnector/Configs/ConfigAccessor.cs +++ b/CrystalConnector/Configs/ConfigAccessor.cs @@ -13,7 +13,13 @@ public class ConfigAccessor public bool IsDebug() { - return Config.GetValue("Connector:Debug"); + return HasDevEnvironmentVariable() || Config.GetValue("Connector:Debug"); + } + + private bool HasDevEnvironmentVariable() + { + var dev = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); + return dev != null && dev.Equals("Development", StringComparison.OrdinalIgnoreCase); } public string GetSecretKey() diff --git a/CrystalConnector/Connector/MessageChannel.cs b/CrystalConnector/Connector/MessageChannel.cs index 0643dcd..957082b 100644 --- a/CrystalConnector/Connector/MessageChannel.cs +++ b/CrystalConnector/Connector/MessageChannel.cs @@ -2,5 +2,7 @@ public class MessageChannel { + public required string Id { get; set; } + public required MessageDirection Direction { get; set; } } \ No newline at end of file diff --git a/CrystalConnector/Connector/MessageDirection.cs b/CrystalConnector/Connector/MessageDirection.cs new file mode 100644 index 0000000..32fab28 --- /dev/null +++ b/CrystalConnector/Connector/MessageDirection.cs @@ -0,0 +1,10 @@ +namespace CrystalConnector.Connector; + +[Flags] +public enum MessageDirection +{ + None = 0, + Incoming = 1, + Outgoing = 2, + All = Incoming | Outgoing +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/PacketS2C.cs b/CrystalConnector/Connector/Packet/S2C/PacketS2C.cs new file mode 100644 index 0000000..8158bc8 --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/PacketS2C.cs @@ -0,0 +1,15 @@ +using System.Formats.Cbor; + +namespace CrystalConnector.Connector.Packet.S2C; + +public abstract class PacketS2C : IPacket +{ + public void Write(CborWriter writer) + { + writer.WriteStartMap(null); + WriteData(writer); + writer.WriteEndMap(); + } + + protected abstract void WriteData(CborWriter writer); +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CAuthenticatedPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CAuthenticatedPacket.cs new file mode 100644 index 0000000..5166007 --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CAuthenticatedPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CAuthenticatedPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseAuthenticated); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CForwardMessagePacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CForwardMessagePacket.cs new file mode 100644 index 0000000..1058a1a --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CForwardMessagePacket.cs @@ -0,0 +1,24 @@ +using System.Formats.Cbor; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CForwardMessagePacket : PacketS2C +{ + private string Origin { get; } + private string ChannelId { get; } + private byte[] Payload { get; } + + public S2CForwardMessagePacket(string origin, string channelId, byte[] payload) + { + Origin = origin; + ChannelId = channelId; + Payload = payload; + } + + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(Origin); + writer.WriteTextString(ChannelId); + writer.WriteEncodedValue(Payload); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CSuccessfulPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CSuccessfulPacket.cs new file mode 100644 index 0000000..358856b --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CSuccessfulPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CSuccessfulPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseSuccessful); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CUnauthenticatedPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CUnauthenticatedPacket.cs new file mode 100644 index 0000000..8525ea6 --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CUnauthenticatedPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CUnauthenticatedPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseErrorUnauthenticated); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CUndefinedDirectionPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CUndefinedDirectionPacket.cs new file mode 100644 index 0000000..1f3dff7 --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CUndefinedDirectionPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CUndefinedDirectionPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseErrorUndefinedDirection); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CUnknownPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CUnknownPacket.cs new file mode 100644 index 0000000..9f818ae --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CUnknownPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CUnknownPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseErrorUnknownPacket); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2C/S2CUnregisteredDirectionPacket.cs b/CrystalConnector/Connector/Packet/S2C/S2CUnregisteredDirectionPacket.cs new file mode 100644 index 0000000..3c4bbad --- /dev/null +++ b/CrystalConnector/Connector/Packet/S2C/S2CUnregisteredDirectionPacket.cs @@ -0,0 +1,12 @@ +using System.Formats.Cbor; +using CrystalConnector.Handlers; + +namespace CrystalConnector.Connector.Packet.S2C; + +public class S2CUnregisteredDirectionPacket : PacketS2C +{ + protected override void WriteData(CborWriter writer) + { + writer.WriteTextString(HandlerConstants.ResponseErrorUnregisteredDirection); + } +} \ No newline at end of file diff --git a/CrystalConnector/Connector/Packet/S2CAuthenticatedPacket.cs b/CrystalConnector/Connector/Packet/S2CAuthenticatedPacket.cs deleted file mode 100644 index ce27749..0000000 --- a/CrystalConnector/Connector/Packet/S2CAuthenticatedPacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Formats.Cbor; -using CrystalConnector.Handlers; - -namespace CrystalConnector.Connector.Packet; - -public class S2CAuthenticatedPacket : IPacket -{ - public void Write(CborWriter writer) - { - writer.WriteTextString(HandlerConstants.ResponseAuthenticated); - } -} \ No newline at end of file diff --git a/CrystalConnector/Connector/WebSocketConnectionManager.cs b/CrystalConnector/Connector/WebSocketConnectionManager.cs index c4269fa..7adb3db 100644 --- a/CrystalConnector/Connector/WebSocketConnectionManager.cs +++ b/CrystalConnector/Connector/WebSocketConnectionManager.cs @@ -1,4 +1,5 @@ using System.Net.WebSockets; +using CrystalConnector.Connector.Packet.S2C; using CrystalConnector.Utilities; namespace CrystalConnector.Connector; @@ -14,4 +15,18 @@ public class WebSocketConnectionManager webSocket.Disconnect(); } } + + public static async Task Broadcast(string origin, string channelId, byte[] payload, bool allowUnauthenticated = false) + { + var packet = new S2CForwardMessagePacket(origin, channelId, payload); + + foreach (var (webSocket, info) in Info) + { + if (info.RegisteredChannels.TryGetValue(channelId, out var value) + && value.Direction.HasFlag(MessageDirection.Incoming)) + { + await webSocket.Send(packet); + } + } + } } \ No newline at end of file diff --git a/CrystalConnector/CrystalConnector.csproj b/CrystalConnector/CrystalConnector.csproj index 3508238..408cc6c 100644 --- a/CrystalConnector/CrystalConnector.csproj +++ b/CrystalConnector/CrystalConnector.csproj @@ -32,4 +32,5 @@ + diff --git a/CrystalConnector/Handlers/HandlerConstants.cs b/CrystalConnector/Handlers/HandlerConstants.cs index e0bf396..7d948f6 100644 --- a/CrystalConnector/Handlers/HandlerConstants.cs +++ b/CrystalConnector/Handlers/HandlerConstants.cs @@ -5,6 +5,12 @@ public class HandlerConstants public static readonly string OperationAuthenticate = "Authenticate"; public static readonly string OperationRegisterChannel = "Register"; public static readonly string OperationPublish = "Publish"; - + + public static readonly string ResponseSuccessful = "Successful"; public static readonly string ResponseAuthenticated = "Authenticated"; + + public static readonly string ResponseErrorUnknownPacket = "Unknown"; + public static readonly string ResponseErrorUnauthenticated = "Unauthenticated"; + public static readonly string ResponseErrorUndefinedDirection = "UndefinedDirection"; + public static readonly string ResponseErrorUnregisteredDirection = "UnregisteredDirection"; } \ No newline at end of file diff --git a/CrystalConnector/Handlers/WebSocketHandler.cs b/CrystalConnector/Handlers/WebSocketHandler.cs index c7f470c..1172ae3 100644 --- a/CrystalConnector/Handlers/WebSocketHandler.cs +++ b/CrystalConnector/Handlers/WebSocketHandler.cs @@ -1,7 +1,9 @@ using System.Formats.Cbor; using System.Net.WebSockets; using CrystalConnector.Configs; +using CrystalConnector.Connector; using CrystalConnector.Connector.Packet; +using CrystalConnector.Connector.Packet.S2C; using CrystalConnector.Utilities; using Microsoft.Extensions.Logging; @@ -19,10 +21,11 @@ public class WebSocketHandler Logger = logger; } - public async Task Handle(WebSocket webSocket, CborReader reader) + public async Task Handle(WebSocket webSocket, CborReader reader) { reader.ReadStartArray(); + // Todo: qyl27: Use C2SPacketReader. var type = reader.ReadTextString(); if (type == HandlerConstants.OperationAuthenticate) { @@ -35,23 +38,79 @@ public class WebSocketHandler Logger.LogInformation("Client {Name}({Id}) authorized", name, webSocket.GetConnectionInfo().Id); webSocket.GetConnectionInfo().Authenticated = true; webSocket.GetConnectionInfo().Name = name; - await webSocket.Send(new S2CAuthenticatedPacket().Write); - } - else - { - throw new MessageException("Auth failed!"); + return new S2CSuccessfulPacket(); } + + Logger.LogWarning("Client {Name}({Id}) auth failed with wrong key!", name, webSocket.GetConnectionInfo().Id); + return new S2CUnauthenticatedPacket(); } - else - { - throw new MessageException("You are already authorized."); - } + + return new S2CAuthenticatedPacket(); } - else if (type == "") + + if (type == HandlerConstants.OperationRegisterChannel) { + if (!webSocket.GetConnectionInfo().Authenticated) + { + Logger.LogWarning("Client {Id} is unauthenticated!", webSocket.GetConnectionInfo().Id); + return new S2CUnauthenticatedPacket(); + } + + var channelId = reader.ReadTextString(); + var directionValue = reader.ReadInt32(); + + if (!Enum.IsDefined(typeof(MessageDirection), directionValue)) + { + Logger.LogWarning("Client {Name}({Id}) tried to register channel {Channel} with undefined direction {Direction}!", + webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id, channelId, directionValue); + return new S2CUndefinedDirectionPacket(); + } + var direction = (MessageDirection)directionValue; + + webSocket.GetConnectionInfo().RegisteredChannels.Add(channelId, new MessageChannel + { + Id = channelId, + Direction = direction + }); + Logger.LogInformation("Client {Name}({Id}) registered channel {Channel}(Direction: {Direction})", + webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id, channelId, direction.ToString()); + return new S2CSuccessfulPacket(); } - + + if (type == HandlerConstants.OperationPublish) + { + var origin = reader.ReadTextString(); + var channelId = reader.ReadTextString(); + var payload = reader.ReadEncodedValue(); + + if (!webSocket.GetConnectionInfo().RegisteredChannels.TryGetValue(channelId, out var value) + || !value.Direction.HasFlag(MessageDirection.Outgoing)) + { + Logger.LogError("Channel {Channel}(Direction: {Direction}) have not been registered by Client {Name}({Id})", + channelId, MessageDirection.Outgoing, webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id); + return new S2CUnregisteredDirectionPacket(); + } + + try + { + await WebSocketConnectionManager.Broadcast(origin, channelId, payload.ToArray()); + + if (Config.IsDebug()) + { + Logger.LogDebug("Client {Name}({Id}) send data in {Channel}: {Data}", + origin, webSocket.GetConnectionInfo().Id, channelId, Convert.ToBase64String(payload.Span)); + return new S2CSuccessfulPacket(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Send error!"); + } + } + reader.ReadEndArray(); + + return new S2CUnknownPacket(); } } \ No newline at end of file diff --git a/CrystalConnector/Utilities/WebSocketConnectionInfo.cs b/CrystalConnector/Utilities/WebSocketConnectionInfo.cs index 11ef6b6..8f2d9bb 100644 --- a/CrystalConnector/Utilities/WebSocketConnectionInfo.cs +++ b/CrystalConnector/Utilities/WebSocketConnectionInfo.cs @@ -15,5 +15,5 @@ public class WebSocketConnectionInfo public bool Authenticated { get; set; } = false; - public List RegisteredChannels { get; } = new(); + public Dictionary RegisteredChannels { get; } = new(); } \ No newline at end of file diff --git a/CrystalConnector/Utilities/WebSocketExtension.cs b/CrystalConnector/Utilities/WebSocketExtension.cs index 1a2cd97..200158a 100644 --- a/CrystalConnector/Utilities/WebSocketExtension.cs +++ b/CrystalConnector/Utilities/WebSocketExtension.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.WebSockets; using CrystalConnector.Connector; +using CrystalConnector.Connector.Packet.S2C; namespace CrystalConnector.Utilities; @@ -33,12 +34,10 @@ public static class WebSocketExtension WebSocketConnectionManager.Info.Remove(webSocket); } - public static async Task Send(this WebSocket webSocket, Action write) + public static async Task Send(this WebSocket webSocket, PacketS2C packet) { var writer = new CborWriter(); - writer.WriteStartArray(null); - write(writer); - writer.WriteEndArray(); + packet.Write(writer); await webSocket.Send(writer.Encode()); } diff --git a/CrystalConnector/WebSockets/WebSocketManager.cs b/CrystalConnector/WebSockets/WebSocketManager.cs index b686046..42e22c6 100644 --- a/CrystalConnector/WebSockets/WebSocketManager.cs +++ b/CrystalConnector/WebSockets/WebSocketManager.cs @@ -42,7 +42,8 @@ public class WebSocketManager : IWebSocketManager try { - await Handler.Handle(webSocket, reader); + var resultPacket = await Handler.Handle(webSocket, reader); + await webSocket.Send(resultPacket); } catch (CborContentException ex) {