From 185b231e7d61a011e3400e4c6884be1e1c9afd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E9=9B=A8=E8=90=BD?= Date: Wed, 13 Sep 2023 16:12:27 +0800 Subject: [PATCH] :sparkles: TestClient, but wait, what are we actually need? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 秋雨落 --- ConnectorTest/ConnectorTest.csproj | 16 ++ ConnectorTest/Program.cs | 156 ++++++++++++++++++ CrystalConnector.sln | 6 + CrystalConnector/Handlers/HandlerConstants.cs | 8 + CrystalConnector/Handlers/MessageException.cs | 9 + CrystalConnector/Handlers/WebSocketHandler.cs | 19 ++- .../Utilities/WebSocketExtension.cs | 14 +- .../WebSockets/WebSocketManager.cs | 33 +++- CrystalConnector/config.json | 3 +- README.md | 64 ++++++- 10 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 ConnectorTest/ConnectorTest.csproj create mode 100644 ConnectorTest/Program.cs create mode 100644 CrystalConnector/Handlers/HandlerConstants.cs create mode 100644 CrystalConnector/Handlers/MessageException.cs diff --git a/ConnectorTest/ConnectorTest.csproj b/ConnectorTest/ConnectorTest.csproj new file mode 100644 index 0000000..b90ebde --- /dev/null +++ b/ConnectorTest/ConnectorTest.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/ConnectorTest/Program.cs b/ConnectorTest/Program.cs new file mode 100644 index 0000000..f72da2c --- /dev/null +++ b/ConnectorTest/Program.cs @@ -0,0 +1,156 @@ +using System.Formats.Cbor; +using System.Net.WebSockets; + +var webSocket = new ClientWebSocket(); +var cborWriter = new CborWriter(); + +while (true) +{ + try + { + await Process(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } +} + +async Task Receive() +{ + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + var reader = new CborReader(new ReadOnlyMemory(buffer, 0, result.Count)); + Console.WriteLine(reader.BytesRemaining); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } +} + +async Task Process() +{ + Console.Write("> "); + var operation = Console.ReadLine(); + + if (operation == "") + { + return; + } + + if (operation == "connect") + { + var uri = Console.ReadLine(); + await webSocket.ConnectAsync(new Uri(uri!), CancellationToken.None); + Receive(); + } + else if (operation == "message") + { + cborWriter.Reset(); + cborWriter.WriteStartArray(null); + } + else if (operation == "send") + { + cborWriter.WriteEndArray(); + var bytes = cborWriter.Encode(); + await webSocket.SendAsync(bytes, WebSocketMessageType.Binary, true, CancellationToken.None); + } + else if (operation == "bye") + { + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + webSocket = new ClientWebSocket(); + } + else if (operation == "help") + { + Console.WriteLine("Every space should use enter."); + Console.WriteLine("Connect to a server: connect "); + Console.WriteLine("New message: message"); + Console.WriteLine("Send message: send"); + Console.WriteLine("Close websocket: bye"); + Console.WriteLine("Insert data: data "); + } + else if (operation == "data") + { + var type = Console.ReadLine(); + var valueStr = Console.ReadLine(); + + if (type == "string") + { + cborWriter.WriteTextString(valueStr!); + } + else if (type == "int") + { + var result = int.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = int.TryParse(valueStr, out value); + } + cborWriter.WriteInt32(value); + } + else if (type == "long") + { + var result = long.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = long.TryParse(valueStr, out value); + } + cborWriter.WriteInt64(value); + } + else if (type == "bool") + { + var result = bool.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = bool.TryParse(valueStr, out value); + } + cborWriter.WriteBoolean(value); + } + else if (type == "float") + { + var result = float.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = float.TryParse(valueStr, out value); + } + cborWriter.WriteSingle(value); + } + else if (type == "double") + { + var result = double.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = double.TryParse(valueStr, out value); + } + cborWriter.WriteDouble(value); + } + else if (type == "decimal") + { + var result = decimal.TryParse(valueStr, out var value); + while (!result) + { + valueStr = Console.ReadLine(); + + result = decimal.TryParse(valueStr, out value); + } + cborWriter.WriteDecimal(value); + } + else if (type == "null") + { + cborWriter.WriteNull(); + } + } + + Console.WriteLine("Done!"); +} diff --git a/CrystalConnector.sln b/CrystalConnector.sln index 49543a6..c0f31b2 100644 --- a/CrystalConnector.sln +++ b/CrystalConnector.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector", "CrystalConnector\CrystalConnector.csproj", "{9DA4D124-899F-4946-8BFC-510654340357}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorTest", "ConnectorTest\ConnectorTest.csproj", "{7DA8728D-146A-4E85-AEB8-A48130836CF7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {9DA4D124-899F-4946-8BFC-510654340357}.Debug|Any CPU.Build.0 = Debug|Any CPU {9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.ActiveCfg = Release|Any CPU {9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.Build.0 = Release|Any CPU + {7DA8728D-146A-4E85-AEB8-A48130836CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA8728D-146A-4E85-AEB8-A48130836CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA8728D-146A-4E85-AEB8-A48130836CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA8728D-146A-4E85-AEB8-A48130836CF7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/CrystalConnector/Handlers/HandlerConstants.cs b/CrystalConnector/Handlers/HandlerConstants.cs new file mode 100644 index 0000000..24b8bd5 --- /dev/null +++ b/CrystalConnector/Handlers/HandlerConstants.cs @@ -0,0 +1,8 @@ +namespace CrystalConnector.Handlers; + +public class HandlerConstants +{ + public static readonly string MessageTypeAuthenticate = "Authenticate"; + public static readonly string MessageTypeRegisterHandler = "Register"; + public static readonly string MessageTypeEmit = "Emit"; +} \ No newline at end of file diff --git a/CrystalConnector/Handlers/MessageException.cs b/CrystalConnector/Handlers/MessageException.cs new file mode 100644 index 0000000..b751f21 --- /dev/null +++ b/CrystalConnector/Handlers/MessageException.cs @@ -0,0 +1,9 @@ +namespace CrystalConnector.Handlers; + +public class MessageException : Exception +{ + public MessageException(string message) + : base(message) + { + } +} \ No newline at end of file diff --git a/CrystalConnector/Handlers/WebSocketHandler.cs b/CrystalConnector/Handlers/WebSocketHandler.cs index 09034fe..0f3ef15 100644 --- a/CrystalConnector/Handlers/WebSocketHandler.cs +++ b/CrystalConnector/Handlers/WebSocketHandler.cs @@ -20,17 +20,34 @@ public class WebSocketHandler public void Handle(WebSocket webSocket, CborReader reader) { + reader.ReadStartArray(); + var type = reader.ReadTextString(); - if (type == "Authenticate") + if (type == HandlerConstants.MessageTypeAuthenticate) { if (!webSocket.GetConnectionInfo().Authorized) { var secret = reader.ReadTextString(); if (secret == Config.GetValue("Connector:Auth:Secret")) { + Logger.LogInformation("Client {Id} authorized", webSocket.GetConnectionInfo().Id); webSocket.GetConnectionInfo().Authorized = true; } + else + { + throw new MessageException("Auth failed!"); + } + } + else + { + throw new MessageException("You are already authorized."); } } + else if (type == "") + { + + } + + reader.ReadEndArray(); } } \ No newline at end of file diff --git a/CrystalConnector/Utilities/WebSocketExtension.cs b/CrystalConnector/Utilities/WebSocketExtension.cs index 5cef381..cda35c5 100644 --- a/CrystalConnector/Utilities/WebSocketExtension.cs +++ b/CrystalConnector/Utilities/WebSocketExtension.cs @@ -24,11 +24,21 @@ public static class WebSocketExtension public static void Disconnect(this WebSocket webSocket) { - var data = GetConnectionInfo(webSocket); - data.CompletionSource.SetResult(WebSocketHandleResult.Successful); + var info = GetConnectionInfo(webSocket); + info.CompletionSource.SetResult(WebSocketHandleResult.Successful); webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); WebSocketConnectionManager.Info.Remove(webSocket); } + + public static async Task Send(this WebSocket webSocket, byte[] data) + { + await webSocket.Send(data, 0, data.Length); + } + + public static async Task Send(this WebSocket webSocket, byte[] data, int startIndex, int length) + { + await webSocket.SendAsync(new ReadOnlyMemory(data, startIndex, length), WebSocketMessageType.Binary, true, CancellationToken.None); + } } \ No newline at end of file diff --git a/CrystalConnector/WebSockets/WebSocketManager.cs b/CrystalConnector/WebSockets/WebSocketManager.cs index e163bbb..a847a19 100644 --- a/CrystalConnector/WebSockets/WebSocketManager.cs +++ b/CrystalConnector/WebSockets/WebSocketManager.cs @@ -4,6 +4,7 @@ using System.Net.WebSockets; using CrystalConnector.Handlers; using CrystalConnector.Utilities; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace CrystalConnector.WebSockets; @@ -11,12 +12,15 @@ namespace CrystalConnector.WebSockets; public class WebSocketManager : IWebSocketManager { private ILogger Logger { get; } + + private IConfiguration Config { get; } private WebSocketHandler Handler { get; } - public WebSocketManager(ILogger logger, WebSocketHandler handler) + public WebSocketManager(ILogger logger, IConfiguration config, WebSocketHandler handler) { Logger = logger; + Config = config; Handler = handler; } @@ -36,7 +40,32 @@ public class WebSocketManager : IWebSocketManager { var reader = new CborReader(new ReadOnlyMemory(buffer, 0, receiveResult.Count)); - Handler.Handle(webSocket, reader); + try + { + Handler.Handle(webSocket, reader); + } + catch (CborContentException ex) + { + if (Config.GetValue("Connector:Debug")) + { + Logger.LogWarning(ex, "Malformed message!"); + } + + Disconnect(webSocket); + } + catch (InvalidOperationException ex) + { + if (Config.GetValue("Connector:Debug")) + { + Logger.LogWarning(ex, "Bad message!"); + } + Disconnect(webSocket); + } + catch (MessageException ex) + { + Logger.LogWarning(ex, "Why do that?"); + Disconnect(webSocket); + } receiveResult = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); } diff --git a/CrystalConnector/config.json b/CrystalConnector/config.json index c4f45d7..a6229bc 100644 --- a/CrystalConnector/config.json +++ b/CrystalConnector/config.json @@ -14,8 +14,9 @@ ] }, "Connector": { + "Debug": false, "Auth": { - "Secret": "qyl27IsHere!" + "Secret": "" } } } \ No newline at end of file diff --git a/README.md b/README.md index 756f7d8..78b65ce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,62 @@ -# CrystalConnector -【水晶互联】猫猫之间的三次握爪,四次摇尾巴! +# CrystalConnector / 水晶互联 +猫猫之间的三次握爪,四次摇尾巴! + + + +## 传输协议 + +水晶互联采用 WebSocket 进行消息传输,使用 CBOR 进行消息编码。 + +为了贴近 Guava 中 `ByteArrayDataInput` / `ByteArrayDataOutput` 的使用习惯,规定以单层基本类型的数组形式使用 CBOR 。(Todo:支持更复杂的结构。) + +参考代码: + +```c# +var writer = new CborWriter(); +writer.WriteStartArray(null); // 不定长数组 +// 消息序列化 +writer.WriteEndArray(); + +var reader = new CborReader(buffer); +reader.ReadStartArray(); +// 消息反序列化 +reader.ReadEndArray(); +``` + +### 文档约定 + +文档中的代码块,以行首的 `>` 代表客户端向服务端传输的消息;以行首的 `<` 代表服务端向客户端传输的消息。其后的消息以纯文本形式发送。如需某一端进行操作,在尖括号后以 `!` 标志体现。 + +服务端是指水晶互联的服务端,一个 C# 程序用来完成消息转发操作。 + +客户端是指所有通过 WebSocket 连接服务端的消息提供或消费者。 + +文档中使用尖括号对 `` 代表必填参数,方括号对 `[type:Name]` 代表可选参数。 `type` 是参数类型,如 `string` 、 `int` 等。 `Name` 是参数名,不在具体传输过程中体现,是方便开发人员记忆的名称。 + +### 客户端 to 服务端 + +#### 认证客户端 + +为了防止未授权的传输,水晶互联需要一个 SecretKey 来信任客户端,它由服务端配置文件指定。(这个设计没有任何安全性的保证,请保证不在公网中使用它。) + +认证请求: + +``` +> Authenticate +> +``` + +认证成功: + +``` +< Authenticated +``` + +认证失败: + +``` +