mirror of
https://github.com/Ale32bit/Capy64.git
synced 2025-12-14 18:15:44 +00:00
392 lines
10 KiB
C#
392 lines
10 KiB
C#
using Capy64.API;
|
|
using Capy64.Runtime.Extensions;
|
|
using Capy64.Runtime.Objects;
|
|
using KeraLua;
|
|
using Microsoft.Extensions.Configuration;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace Capy64.Runtime.Libraries;
|
|
#nullable enable
|
|
public class HTTP : IPlugin
|
|
{
|
|
private static IGame _game;
|
|
private static HttpClient _httpClient;
|
|
private static long _requestId;
|
|
public static readonly HashSet<WebSocketClient.Client> WebSocketConnections = new();
|
|
|
|
public static readonly string UserAgent = $"Capy64/{Capy64.Version}";
|
|
|
|
private static IConfiguration _configuration;
|
|
private readonly LuaRegister[] HttpLib = new LuaRegister[]
|
|
{
|
|
new()
|
|
{
|
|
name = "checkURL",
|
|
function = L_CheckUrl,
|
|
},
|
|
new()
|
|
{
|
|
name = "requestAsync",
|
|
function = L_Request,
|
|
},
|
|
new()
|
|
{
|
|
name = "websocketAsync",
|
|
function = L_WebsocketAsync,
|
|
},
|
|
new(),
|
|
};
|
|
public HTTP(IGame game, IConfiguration configuration)
|
|
{
|
|
_game = game;
|
|
_requestId = 0;
|
|
_httpClient = new();
|
|
_httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
|
|
_configuration = configuration;
|
|
}
|
|
|
|
public void LuaInit(Lua L)
|
|
{
|
|
if (_configuration.GetValue<bool>("HTTP:Enable"))
|
|
L.RequireF("http", Open, false);
|
|
}
|
|
|
|
private int Open(IntPtr state)
|
|
{
|
|
var L = Lua.FromIntPtr(state);
|
|
L.NewLib(HttpLib);
|
|
return 1;
|
|
}
|
|
|
|
private static readonly string[] _allowedSchemes = new[]
|
|
{
|
|
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)
|
|
{
|
|
var L = Lua.FromIntPtr(state);
|
|
|
|
var request = new HttpRequestMessage();
|
|
|
|
var url = L.CheckString(1);
|
|
if (!TryGetUri(url, out Uri? uri) || uri is null)
|
|
{
|
|
L.ArgumentError(1, "invalid request url");
|
|
return 0;
|
|
}
|
|
request.RequestUri = uri;
|
|
|
|
if (L.IsTable(3)) // headers
|
|
{
|
|
L.PushCopy(3);
|
|
L.PushNil();
|
|
|
|
while (L.Next(-2))
|
|
{
|
|
L.PushCopy(-2);
|
|
|
|
var k = L.CheckString(-1);
|
|
if (L.IsStringOrNumber(-2))
|
|
{
|
|
var v = L.ToString(-2);
|
|
|
|
request.Headers.Add(k, v);
|
|
}
|
|
else if (L.IsNil(-2))
|
|
{
|
|
request.Headers.Remove(k);
|
|
}
|
|
else
|
|
{
|
|
L.ArgumentError(3, "string, number or nil expected, got " + L.TypeName(L.Type(-2)) + " in field " + k);
|
|
}
|
|
|
|
L.Pop(2);
|
|
}
|
|
|
|
L.Pop(1);
|
|
}
|
|
|
|
var options = new Dictionary<string, object>
|
|
{
|
|
["binary"] = false,
|
|
};
|
|
|
|
if (L.IsTable(4)) // other options?
|
|
{
|
|
L.PushCopy(4);
|
|
L.PushNil();
|
|
|
|
while (L.Next(-2))
|
|
{
|
|
L.PushCopy(-2);
|
|
var k = L.CheckString(-1);
|
|
|
|
switch (k)
|
|
{
|
|
case "method":
|
|
options["method"] = L.CheckString(-2);
|
|
break;
|
|
case "binary":
|
|
options["binary"] = L.IsBoolean(-2) ? L.ToBoolean(-2) : false;
|
|
break;
|
|
}
|
|
|
|
L.Pop(2);
|
|
}
|
|
|
|
L.Pop(1);
|
|
}
|
|
|
|
if (!L.IsNoneOrNil(2))
|
|
{
|
|
if ((bool)options["binary"])
|
|
{
|
|
request.Content = new ByteArrayContent(L.CheckBuffer(2));
|
|
}
|
|
else
|
|
{
|
|
request.Content = new StringContent(L.CheckString(2));
|
|
}
|
|
}
|
|
|
|
request.Method = options.TryGetValue("method", out var value)
|
|
? new HttpMethod((string)value)
|
|
: request.Content is not null ? HttpMethod.Post : HttpMethod.Get;
|
|
|
|
var requestId = _requestId++;
|
|
|
|
var reqTask = _httpClient.SendAsync(request);
|
|
reqTask.ContinueWith(async (task) =>
|
|
{
|
|
|
|
if (task.IsFaulted || task.IsCanceled)
|
|
{
|
|
_game.LuaRuntime.QueueEvent("http_failure", LK =>
|
|
{
|
|
LK.PushInteger(requestId);
|
|
LK.PushString(task.Exception?.Message);
|
|
|
|
return 2;
|
|
});
|
|
return;
|
|
}
|
|
|
|
var response = await task;
|
|
|
|
var stream = await response.Content.ReadAsStreamAsync();
|
|
|
|
_game.LuaRuntime.QueueEvent("http_response", LK =>
|
|
{
|
|
// arg 1, request id
|
|
LK.PushInteger(requestId);
|
|
|
|
// arg 2, response data
|
|
ObjectManager.PushObject(L, stream);
|
|
//L.PushObject(stream);
|
|
L.SetMetaTable(FileHandle.ObjectType);
|
|
/*if ((bool)options["binary"])
|
|
BinaryReadHandle.Push(LK, new(stream));
|
|
else
|
|
ReadHandle.Push(LK, new(stream));*/
|
|
|
|
// arg 3, response info
|
|
LK.NewTable();
|
|
|
|
LK.PushString("success");
|
|
LK.PushBoolean(response.IsSuccessStatusCode);
|
|
LK.SetTable(-3);
|
|
|
|
LK.PushString("statusCode");
|
|
LK.PushNumber((int)response.StatusCode);
|
|
LK.SetTable(-3);
|
|
|
|
LK.PushString("reasonPhrase");
|
|
LK.PushString(response.ReasonPhrase);
|
|
LK.SetTable(-3);
|
|
|
|
LK.PushString("headers");
|
|
LK.NewTable();
|
|
|
|
foreach (var header in response.Headers)
|
|
{
|
|
LK.PushString(header.Key);
|
|
LK.PushArray(header.Value.ToArray());
|
|
LK.SetTable(-3);
|
|
}
|
|
|
|
LK.SetTable(-3);
|
|
|
|
return 3;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
L.PushInteger(requestId);
|
|
|
|
return 1;
|
|
}
|
|
|
|
private static int L_WebsocketAsync(IntPtr state)
|
|
{
|
|
var L = Lua.FromIntPtr(state);
|
|
|
|
var wsSettings = _configuration.GetSection("HTTP:WebSockets");
|
|
|
|
if (!wsSettings.GetValue<bool>("Enable"))
|
|
{
|
|
L.Error("WebSockets are disabled");
|
|
return 0;
|
|
}
|
|
|
|
if (WebSocketConnections.Count >= wsSettings.GetValue<int>("MaxActiveConnections"))
|
|
{
|
|
L.Error("Max connections reached");
|
|
return 0;
|
|
}
|
|
|
|
var url = L.CheckString(1);
|
|
if (!TryGetUri(url, out var uri))
|
|
{
|
|
L.ArgumentError(1, "invalid request url");
|
|
return 0;
|
|
}
|
|
|
|
var requestId = _requestId++;
|
|
|
|
var wsClient = new ClientWebSocket();
|
|
|
|
wsClient.Options.SetRequestHeader("User-Agent", UserAgent);
|
|
|
|
if (L.IsTable(2)) // headers
|
|
{
|
|
L.PushCopy(2);
|
|
L.PushNil();
|
|
|
|
while (L.Next(-2))
|
|
{
|
|
L.PushCopy(-2);
|
|
|
|
var k = L.CheckString(-1);
|
|
if (L.IsStringOrNumber(-2))
|
|
{
|
|
var v = L.ToString(-2);
|
|
|
|
wsClient.Options.SetRequestHeader(k, v);
|
|
}
|
|
else if (L.IsNil(-2))
|
|
{
|
|
wsClient.Options.SetRequestHeader(k, null);
|
|
}
|
|
else
|
|
{
|
|
L.ArgumentError(3, "string, number or nil expected, got " + L.TypeName(L.Type(-2)) + " in field " + k);
|
|
}
|
|
|
|
L.Pop(2);
|
|
}
|
|
|
|
L.Pop(1);
|
|
}
|
|
|
|
|
|
var connectTask = wsClient.ConnectAsync(uri, CancellationToken.None);
|
|
connectTask.ContinueWith(async task =>
|
|
{
|
|
if (task.IsFaulted || task.IsCanceled)
|
|
{
|
|
_game.LuaRuntime.QueueEvent("websocket_failure", LK =>
|
|
{
|
|
LK.PushInteger(requestId);
|
|
LK.PushString(task.Exception?.Message);
|
|
|
|
return 2;
|
|
});
|
|
return;
|
|
}
|
|
|
|
await task;
|
|
|
|
var handle = new WebSocketClient.Client(wsClient, requestId);
|
|
WebSocketConnections.Add(handle);
|
|
|
|
_game.LuaRuntime.QueueEvent("websocket_connect", LK =>
|
|
{
|
|
LK.PushInteger(requestId);
|
|
|
|
ObjectManager.PushObject(LK, handle);
|
|
LK.SetMetaTable(WebSocketClient.ObjectType);
|
|
|
|
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)
|
|
{
|
|
await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
|
|
_game.LuaRuntime.QueueEvent("websocket_close", LK =>
|
|
{
|
|
LK.PushInteger(requestId);
|
|
|
|
return 1;
|
|
});
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
var data = Encoding.ASCII.GetString(buffer, 0, result.Count);
|
|
builder.Append(data);
|
|
}
|
|
|
|
if (result.EndOfMessage)
|
|
{
|
|
var payload = builder.ToString();
|
|
_game.LuaRuntime.QueueEvent("websocket_message", LK =>
|
|
{
|
|
LK.PushInteger(requestId);
|
|
LK.PushString(payload);
|
|
|
|
return 2;
|
|
});
|
|
builder.Clear();
|
|
}
|
|
}
|
|
});
|
|
|
|
L.PushInteger(requestId);
|
|
|
|
return 1;
|
|
}
|
|
}
|