From b06ab003e07a64fa2f4a4c1912dea55a20eb3b4b Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Mon, 16 Jan 2023 23:02:58 +0100 Subject: [PATCH] Added websocket support --- Capy64/LuaRuntime/Handlers/WebSocketHandle.cs | 89 ++++++++++++++ Capy64/LuaRuntime/Libraries/HTTP.cs | 113 +++++++++++++++--- 2 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 Capy64/LuaRuntime/Handlers/WebSocketHandle.cs diff --git a/Capy64/LuaRuntime/Handlers/WebSocketHandle.cs b/Capy64/LuaRuntime/Handlers/WebSocketHandle.cs new file mode 100644 index 0000000..8307105 --- /dev/null +++ b/Capy64/LuaRuntime/Handlers/WebSocketHandle.cs @@ -0,0 +1,89 @@ +using KeraLua; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Capy64.LuaRuntime.Handlers; + +class WebSocketHandle : IHandle +{ + private ClientWebSocket _client; + private long _requestId; + private static IGame _game; + public WebSocketHandle(ClientWebSocket client, long requestId, IGame game) + { + _client = client; + _requestId = requestId; + _game = game; + } + + private static readonly Dictionary functions = new() + { + ["send"] = L_Send, + ["closeAsync"] = L_CloseAsync, + }; + + public void Push(Lua L, bool newTable = true) + { + if (newTable) + L.NewTable(); + + foreach (var pair in functions) + { + L.PushString(pair.Key); + L.PushCFunction(pair.Value); + L.SetTable(-3); + } + + L.PushString("_handle"); + L.PushObject(this); + L.SetTable(-3); + } + + private static WebSocketHandle GetHandle(Lua L, bool gc = true) + { + L.CheckType(1, LuaType.Table); + L.PushString("_handle"); + L.GetTable(1); + return L.ToObject(-1, gc); + } + + private static int L_Send(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var data = L.CheckString(2); + + var h = GetHandle(L, false); + + if (h is null || h._client.State == WebSocketState.Closed) + L.Error("connection is closed"); + + h._client.SendAsync(Encoding.ASCII.GetBytes(data), WebSocketMessageType.Text, true, CancellationToken.None); + + return 0; + } + + private static int L_CloseAsync(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var h = GetHandle(L, true); + + if (h is null || h._client.State == WebSocketState.Closed) + return 0; + + h._client.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None) + .ContinueWith(async task => + { + await task; + _game.LuaRuntime.PushEvent("websocket_close", h._requestId); + }); + + return 0; + } +} diff --git a/Capy64/LuaRuntime/Libraries/HTTP.cs b/Capy64/LuaRuntime/Libraries/HTTP.cs index 323fa64..844434c 100644 --- a/Capy64/LuaRuntime/Libraries/HTTP.cs +++ b/Capy64/LuaRuntime/Libraries/HTTP.cs @@ -7,18 +7,28 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Capy64.LuaRuntime.Libraries; #nullable enable public class HTTP : IPlugin { private static IGame _game; - private static HttpClient _client; - private static long RequestId; + private static HttpClient _httpClient; + private static long _requestId; private readonly IConfiguration _configuration; private readonly LuaRegister[] HttpLib = new LuaRegister[] { + new() + { + name = "checkURL", + function = L_CheckUrl, + }, new() { name = "requestAsync", @@ -26,17 +36,17 @@ public class HTTP : IPlugin }, new() { - name = "checkURL", - function = L_CheckUrl, + name = "websocketAsync", + function = L_WebsocketAsync, }, new(), }; public HTTP(IGame game, IConfiguration configuration) { _game = game; - RequestId = 0; - _client = new(); - _client.DefaultRequestHeaders.Add("User-Agent", $"Capy64/{Capy64.Version}"); + _requestId = 0; + _httpClient = new(); + _httpClient.DefaultRequestHeaders.Add("User-Agent", $"Capy64/{Capy64.Version}"); _configuration = configuration; } @@ -53,10 +63,29 @@ public class HTTP : IPlugin return 1; } - public static bool TryGetUri(string url, out Uri? uri) + private static readonly string[] _allowedSchemes = new[] { - return (Uri.TryCreate(url, UriKind.Absolute, out uri) - && uri?.Scheme == Uri.UriSchemeHttp) || uri?.Scheme == Uri.UriSchemeHttps; + Uri.UriSchemeHttp, + Uri.UriSchemeHttps, + Uri.UriSchemeWs, + Uri.UriSchemeWss, + }; + public static bool TryGetUri(string url, out Uri uri) + { + return Uri.TryCreate(url, UriKind.Absolute, out uri!) && _allowedSchemes.Contains(uri.Scheme); + } + + private static int L_CheckUrl(IntPtr state) + { + var L = Lua.FromIntPtr(state); + + var url = L.CheckString(1); + + var isValid = TryGetUri(url, out _); + + L.PushBoolean(isValid); + + return 1; } private static int L_Request(IntPtr state) @@ -151,9 +180,9 @@ public class HTTP : IPlugin ? new HttpMethod((string)value) : request.Content is not null ? HttpMethod.Post : HttpMethod.Get; - var requestId = RequestId++; + var requestId = _requestId++; - var reqTask = _client.SendAsync(request); + var reqTask = _httpClient.SendAsync(request); reqTask.ContinueWith(async (task) => { @@ -218,15 +247,69 @@ public class HTTP : IPlugin return 1; } - private static int L_CheckUrl(IntPtr state) + private static int L_WebsocketAsync(IntPtr state) { var L = Lua.FromIntPtr(state); var url = L.CheckString(1); + if (!TryGetUri(url, out var uri)) + { + L.ArgumentError(1, "invalid request url"); + return 0; + } - var isValid = TryGetUri(url, out _); + var requestId = _requestId++; - L.PushBoolean(isValid); + var wsClient = new ClientWebSocket(); + var connectTask = wsClient.ConnectAsync(uri, CancellationToken.None); + connectTask.ContinueWith(async task => + { + if (task.IsFaulted || task.IsCanceled) + { + _game.LuaRuntime.PushEvent("websocket_failure", requestId, task.Exception?.Message); + return; + } + + await task; + + var handle = new WebSocketHandle(wsClient, requestId, _game); + + _game.LuaRuntime.PushEvent("websocket_connect", L => + { + L.PushInteger(requestId); + + handle.Push(L, true); + + return 2; + }); + + var buffer = new byte[4096]; + var builder = new StringBuilder(); + while (wsClient.State == WebSocketState.Open) + { + var result = await wsClient.ReceiveAsync(buffer, CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Close) + { + Console.WriteLine("Closing"); + await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + _game.LuaRuntime.PushEvent("websocket_close", requestId); + return; + } + else + { + var data = Encoding.ASCII.GetString(buffer, 0, result.Count); + builder.Append(data); + } + + if(result.EndOfMessage) + { + _game.LuaRuntime.PushEvent("websocket_message", requestId, builder.ToString()); + builder.Clear(); + } + } + }); + + L.PushInteger(requestId); return 1; }