Add NETWORK:WebSocket()

This commit is contained in:
Martin Natano
2022-05-27 20:33:14 +02:00
parent cb037494fc
commit d8586b02b2
4 changed files with 492 additions and 11 deletions
+11
View File
@@ -1118,6 +1118,7 @@
<Class name='NetworkManager'>
<Function name='IsUrlAllowed'/>
<Function name='HttpRequest'/>
<Function name='WebSocket'/>
<Function name='UrlEncode'/>
<Function name='EncodeQueryParameters'/>
</Class>
@@ -2147,6 +2148,10 @@
<Function name='UnlockEntryID'/>
<Function name='UnlockEntryIndex'/>
</Class>
<Class name='WebSocketHandle'>
<Function name='Close'/>
<Function name='Send'/>
</Class>
<Class base='ActorFrame' name='WheelBase'>
<Function name='GetCurrentIndex'/>
<Function name='GetNumItems'/>
@@ -2955,6 +2960,12 @@
<EnumValue name='&apos;VertAlign_Middle&apos;' value='1'/>
<EnumValue name='&apos;VertAlign_Bottom&apos;' value='2'/>
</Enum>
<Enum name='WebSocketMessageType'>
<EnumValue name='&apos;WebSocketMessageType_Message&apos;' value='0'/>
<EnumValue name='&apos;WebSocketMessageType_Open&apos;' value='1'/>
<EnumValue name='&apos;WebSocketMessageType_Close&apos;' value='2'/>
<EnumValue name='&apos;WebSocketMessageType_Error&apos;' value='3'/>
</Enum>
<Enum name='WheelItemDataType'>
<EnumValue name='&apos;WheelItemDataType_Generic&apos;' value='0'/>
<EnumValue name='&apos;WheelItemDataType_Section&apos;' value='1'/>
+76 -9
View File
@@ -3583,20 +3583,20 @@ end
<pre><code>
NETWORK:HttpRequest{
url="https://api.example.com",
method="GET", -- default: "GET"
body="", -- default: ""
multipartBoundary="", -- default: ""
headers={ -- default: {}
method="GET", -- default: "GET"
body="", -- default: ""
multipartBoundary="", -- default: ""
headers={ -- default: {}
["Accept-Language"]="en-US",
["Cookie"]="sessionId=42",
},
connectTimeout=3, -- default: 60
transferTimeout=10, -- default: 1800
downloadFile="", -- default: no download file
onProgress=function(currentBytes, totalBytes) -- default: no callback
connectTimeout=3, -- default: 60
transferTimeout=10, -- default: 1800
downloadFile="", -- default: no download file
onProgress=function(currentBytes, totalBytes) -- default: no callback
...
end,
onResponse=function(response) -- default: no callback
onResponse=function(response) -- default: no callback
...
end,
}
@@ -3627,6 +3627,63 @@ NETWORK:HttpRequest{
The file is available in the <code>onResponse</code> callback where it can be unzipped/copied to another location using <code>FILEMAN:Unzip()</code>/<code>FILEMAN:Copy()</code> respectively.
The file is deleted once the callback returns.
</Function>
<Function name='WebSocket' return='WebSocketHandle' arguments='table params' since='ITGmania 0.5.1'>
Open a WebSocket connection.<br />
Usage example:
<pre><code>
NETWORK:WebSocket{
url="wss://api.example.com/chat",
headers={ -- default: {}
["Accept-Language"]="en-US",
["Cookie"]="sessionId=42",
},
handshakeTimeout=3, -- default: 60 seconds
pingInterval=10, -- default: disabled
automaticReconnect=false, -- default: true
onMessage=function(message) -- default: no callback
...
end,
}
</code></pre>
Everything but <code>url</code> is optional.<br />
Messages look like this:
<pre><code>
-- Data
{
type="WebSocketMessageType_Message",
data="some data",
binary=false,
}
-- Open
{
type="WebSocketMessageType_Open",
uri="/chat",
headers={
["Date"]="Fri, 27 May 2022 18:50:47 GMT",
},
protocol="",
}
-- Close
{
type="WebSocketMessageType_Close",
reason="Normal closure",
remote=false,
}
-- Error
{
type="WebSocketMessageType_Error",
retries=1,
waitTime=100,
httpStatusCode=404,
reason="Expecting status 101 (Switching Protocol), got 404",
decompressionError=false,
}
</code></pre>
</Function>
<Function name='UrlEncode' return='string' arguments='string value' since='ITGmania 0.5.1'>
Returns the URL encoded representation of <code>value</code>.
</Function>
@@ -6617,6 +6674,16 @@ local bpms_and_times = timing_data:GetBPMsAndTimes(true)
how the song is locked.
</Function>
</Class>
<Class name='WebSocketHandle'>
<Function name='Close' return='void' arguments='' since='ITGmania 0.5.1'>
Closes the WebSocket connection. No further reconnections will be attempted.
</Function>
<Function name='Send' return='bool' arguments='string data, bool binary' since='ITGmania 0.5.1'>
Sends a message containing <code>data</code>.
If the optional <code>binary</code> argument is set to <code>true</code>, the message is marked as binary.<br />
Returns whether the message was sent successfully.
</Function>
</Class>
<Class name='WheelBase'>
<Function name='GetCurrentIndex' return='int' arguments=''>
Returns the wheel's current index.
+362 -2
View File
@@ -50,6 +50,16 @@ XToString(HttpErrorCode);
StringToX(HttpErrorCode);
LuaXType(HttpErrorCode);
static const char *WebSocketMessageTypeNames[] = {
"Message",
"Open",
"Close",
"Error",
};
XToString(WebSocketMessageType);
StringToX(WebSocketMessageType);
LuaXType(WebSocketMessageType);
NetworkManager::NetworkManager() : httpClient(true), downloadClient(true)
{
ix::initNetSystem();
@@ -93,7 +103,7 @@ bool NetworkManager::IsUrlAllowed(const std::string& url)
return false;
}
if (protocol != "http" && protocol != "https")
if (protocol != "http" && protocol != "https" && protocol != "ws" && protocol != "wss")
{
return false;
}
@@ -198,6 +208,43 @@ HttpRequestFuturePtr NetworkManager::HttpRequest(const HttpRequestArgs& args)
return std::make_shared<HttpRequestFuture>(req);
}
WebSocketHandlePtr NetworkManager::WebSocket(const WebSocketArgs& args)
{
auto handle = std::make_shared<WebSocketHandle>();
handle->onClose = args.onClose;
handle->webSocket.setUrl(args.url);
ix::WebSocketHttpHeaders headers;
headers["User-Agent"] = this->GetUserAgent();
for (const auto& entry : args.headers)
{
headers[entry.first] = entry.second;
}
handle->webSocket.setExtraHeaders(headers);
if (args.handshakeTimeout > -1)
handle->webSocket.setHandshakeTimeout(args.handshakeTimeout);
if (args.pingInterval > -1)
handle->webSocket.setPingInterval(args.pingInterval);
if (args.automaticReconnect)
{
handle->webSocket.enableAutomaticReconnection();
}
else
{
handle->webSocket.disableAutomaticReconnection();
}
handle->webSocket.setOnMessageCallback(args.onMessage);
handle->webSocket.start();
return handle;
}
std::string NetworkManager::UrlEncode(const std::string& value)
{
return this->httpClient.urlEncode(value);
@@ -223,9 +270,13 @@ void NetworkManager::ClearDownloads()
for (const auto& file : files)
{
if (FILEMAN->IsADirectory(file))
{
FILEMAN->DeleteRecursive(file + "/");
}
else
{
FILEMAN->Remove(file);
}
}
}
@@ -245,6 +296,42 @@ int HttpRequestFuture::Cancel(lua_State *L)
return 0;
}
int WebSocketHandle::Collect(lua_State *L)
{
void *udata = luaL_checkudata(L, 1, "WebSocketHandle");
auto handleptr = static_cast<WebSocketHandlePtr*>(udata);
handleptr->~shared_ptr();
return 0;
}
int WebSocketHandle::Close(lua_State *L)
{
void *udata = luaL_checkudata(L, 1, "WebSocketHandle");
auto handle = *static_cast<WebSocketHandlePtr*>(udata);
LUA->YieldLua();
handle->webSocket.stop();
handle->onClose();
LUA->UnyieldLua();
return 0;
}
int WebSocketHandle::Send(lua_State *L)
{
void *udata = luaL_checkudata(L, 1, "WebSocketHandle");
auto handle = *static_cast<WebSocketHandlePtr*>(udata);
size_t len;
const char *s = luaL_checklstring(L, 2, &len);
std::string data(s, len);
bool binary = lua_toboolean(L, 3);
auto info = handle->webSocket.send(data, binary);
lua_pushboolean(L, info.success);
return 1;
}
// lua start
#include "LuaBinding.h"
@@ -266,6 +353,24 @@ static void registerHttpRequestMetatable(lua_State *L)
REGISTER_WITH_LUA_FUNCTION(registerHttpRequestMetatable)
static void registerWebSocketMetatable(lua_State *L)
{
const luaL_Reg WebSocket_meta[] = {
{"__gc", WebSocketHandle::Collect},
{"Close", WebSocketHandle::Close},
{"Send", WebSocketHandle::Send},
{NULL, NULL},
};
luaL_newmetatable(L, "WebSocketHandle");
luaL_register(L, NULL, WebSocket_meta);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}
REGISTER_WITH_LUA_FUNCTION(registerWebSocketMetatable)
/** @brief Allow Lua to have access to the NetworkManager. */
class LunaNetworkManager: public Luna<NetworkManager>
{
@@ -316,7 +421,8 @@ public:
args.method = method;
}
else {
else
{
luaL_error(L, "method must be a string");
}
}
@@ -513,6 +619,157 @@ public:
}
}
static int WebSocket(T* p, lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
WebSocketArgs args;
int onMessageRef = LUA_NOREF;
lua_getfield(L, 1, "url");
if (lua_isnil(L, -1))
{
luaL_error(L, "url is required");
}
else if (lua_isstring(L, -1))
{
args.url = lua_tostring(L, -1);
}
else
{
luaL_error(L, "url must be a string");
}
lua_pop(L, 1);
lua_getfield(L, 1, "headers");
if (!lua_isnil(L, -1)) {
if (lua_istable(L, -1))
{
lua_pushnil(L);
while(lua_next(L, -2) != 0)
{
if (!lua_isstring(L, -2))
{
luaL_error(L, "header keys must be strings");
}
if (!lua_isstring(L, -1))
{
luaL_error(L, "header values must be strings");
}
std::string key = lua_tostring(L, -2);
std::string value = lua_tostring(L, -1);
args.headers[key] = value;
lua_pop(L, 1);
}
}
else
{
luaL_error(L, "headers must be a table");
}
}
lua_pop(L, 1);
lua_getfield(L, 1, "handshakeTimeout");
if (!lua_isnil(L, -1)) {
if (lua_isnumber(L, -1))
{
args.handshakeTimeout = lua_tointeger(L, -1);
}
else
{
luaL_error(L, "handshakeTimeout must be an integer");
}
}
lua_pop(L, 1);
lua_getfield(L, 1, "pingInterval");
if (!lua_isnil(L, -1))
{
if (lua_isnumber(L, -1))
{
args.pingInterval = lua_tointeger(L, -1);
}
else
{
luaL_error(L, "pingInterval must be an integer");
}
}
lua_pop(L, 1);
lua_getfield(L, 1, "automaticReconnect");
if (!lua_isnil(L, -1))
{
if (lua_isboolean(L, -1))
{
args.automaticReconnect = lua_toboolean(L, -1);
}
else
{
luaL_error(L, "automaticReconnect must be a boolean");
}
}
lua_pop(L, 1);
lua_getfield(L, 1, "onMessage");
if (!lua_isnil(L, -1))
{
if (lua_isfunction(L, -1))
{
lua_pushvalue(L, -1);
onMessageRef = luaL_ref(L, LUA_REGISTRYINDEX);
}
else
{
luaL_error(L, "onMessage must be a function");
}
}
lua_pop(L, 1);
args.onMessage = [onMessageRef](const ix::WebSocketMessagePtr& msg)
{
Lua *L = LUA->Get();
if (onMessageRef != LUA_NOREF)
handleMessage(L, msg, onMessageRef);
LUA->Release(L);
};
args.onClose = [onMessageRef]()
{
Lua *L = LUA->Get();
if (onMessageRef != LUA_NOREF)
luaL_unref(L, LUA_REGISTRYINDEX, onMessageRef);
LUA->Release(L);
};
if (p->IsUrlAllowed(args.url))
{
auto handle = p->WebSocket(args);
void *vp = lua_newuserdata(L, sizeof(std::shared_ptr<WebSocketHandle>));
new(vp) std::shared_ptr<::WebSocketHandle>(handle);
luaL_getmetatable(L, "WebSocketHandle");
lua_setmetatable(L, -2);
return 1;
}
else
{
LOG->Warn("blocked access to %s", args.url.c_str());
if (onMessageRef != LUA_NOREF)
{
handleWebSocketUrlForbidden(L, args.url, onMessageRef);
}
return 0;
}
}
static int UrlEncode(T* p, lua_State *L)
{
std::string url = SArg(1);
@@ -565,6 +822,7 @@ public:
{
ADD_METHOD(IsUrlAllowed);
ADD_METHOD(HttpRequest);
ADD_METHOD(WebSocket);
ADD_METHOD(UrlEncode);
ADD_METHOD(EncodeQueryParameters);
}
@@ -676,6 +934,108 @@ private:
RString error = "Lua error in HTTP progress handler: ";
LuaHelpers::RunScriptOnStack(L, error, 2, 0, true);
}
static void handleWebSocketUrlForbidden(Lua *L, std::string& url, int onMessageRef)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, onMessageRef);
luaL_unref(L, LUA_REGISTRYINDEX, onMessageRef);
lua_newtable(L);
LuaHelpers::Push(L, WebSocketMessageType_Error);
lua_setfield(L, -2, "type");
lua_pushfstring(L, "access to %s is not allowed", url.c_str());
lua_setfield(L, -2, "reason");
RString error = "Lua error in WebSocket message handler: ";
LuaHelpers::RunScriptOnStack(L, error, 1, 0, true);
}
static void handleMessage(Lua *L, const ix::WebSocketMessagePtr& msg, int onMessageRef)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, onMessageRef);
lua_newtable(L);
switch (msg->type)
{
case ix::WebSocketMessageType::Message:
LuaHelpers::Push(L, WebSocketMessageType_Message);
lua_setfield(L, -2, "type");
lua_pushlstring(L, msg->str.c_str(), msg->str.length());
lua_setfield(L, -2, "data");
lua_pushboolean(L, msg->binary);
lua_setfield(L, -2, "binary");
break;
case ix::WebSocketMessageType::Open:
LuaHelpers::Push(L, WebSocketMessageType_Open);
lua_setfield(L, -2, "type");
lua_pushstring(L, msg->openInfo.uri.c_str());
lua_setfield(L, -2, "uri");
lua_newtable(L);
for (const auto& entry : msg->openInfo.headers)
{
lua_pushstring(L, entry.second.c_str());
lua_setfield(L, -2, entry.first.c_str());
}
lua_setfield(L, -2, "headers");
lua_pushstring(L, msg->openInfo.protocol.c_str());
lua_setfield(L, -2, "protocol");
break;
case ix::WebSocketMessageType::Close:
LuaHelpers::Push(L, WebSocketMessageType_Close);
lua_setfield(L, -2, "type");
lua_pushstring(L, msg->closeInfo.reason.c_str());
lua_setfield(L, -2, "reason");
lua_pushboolean(L, msg->closeInfo.remote);
lua_setfield(L, -2, "remote");
break;
case ix::WebSocketMessageType::Error:
LuaHelpers::Push(L, WebSocketMessageType_Error);
lua_setfield(L, -2, "type");
lua_pushinteger(L, msg->errorInfo.retries);
lua_setfield(L, -2, "retries");
lua_pushnumber(L, msg->errorInfo.wait_time);
lua_setfield(L, -2, "waitTime");
if (msg->errorInfo.http_status > 0)
{
lua_pushinteger(L, msg->errorInfo.http_status);
}
else
{
lua_pushnil(L);
}
lua_setfield(L, -2, "httpStatusCode");
lua_pushstring(L, msg->errorInfo.reason.c_str());
lua_setfield(L, -2, "reason");
lua_pushboolean(L, msg->errorInfo.decompressionError);
lua_setfield(L, -2, "decompressionError");
break;
case ix::WebSocketMessageType::Ping:
case ix::WebSocketMessageType::Pong:
case ix::WebSocketMessageType::Fragment:
default:
lua_pop(L, 2);
return;
}
RString error = "Lua error in WebSocket message handler: ";
LuaHelpers::RunScriptOnStack(L, error, 1, 0, true);
}
};
LUA_REGISTER_CLASS(NetworkManager)
+43
View File
@@ -13,6 +13,7 @@
#include <ixwebsocket/IXHttp.h>
#include <ixwebsocket/IXHttpClient.h>
#include <ixwebsocket/IXWebSocket.h>
#include "EnumHelper.h"
#include "LuaManager.h"
@@ -49,6 +50,21 @@ const RString& HttpErrorCodeToString(HttpErrorCode dc);
HttpErrorCode StringToHttpErrorCode(const RString& sDC);
LuaDeclareType(HttpErrorCode);
enum WebSocketMessageType
{
// from IXWebSocket
WebSocketMessageType_Message,
WebSocketMessageType_Open,
WebSocketMessageType_Close,
WebSocketMessageType_Error,
NUM_WebSocketMessageType,
WebSocketMessageType_Invalid,
};
const RString& WebSocketMessageTypeToString(WebSocketMessageType dc);
WebSocketMessageType StringToWebSocketMessageType(const RString& sDC);
LuaDeclareType(WebSocketMessageType);
struct HttpRequestArgs
{
std::string url;
@@ -78,6 +94,32 @@ private:
typedef std::shared_ptr<HttpRequestFuture> HttpRequestFuturePtr;
struct WebSocketArgs
{
std::string url;
std::unordered_map<std::string, std::string> headers;
int handshakeTimeout = -1;
int pingInterval = -1;
bool automaticReconnect = true;
std::function<void(const ix::WebSocketMessagePtr& response)> onMessage;
std::function<void()> onClose;
};
class WebSocketHandle
{
public:
WebSocketHandle() {};
static int Collect(lua_State *L);
static int Close(lua_State *L);
static int Send(lua_State *L);
ix::WebSocket webSocket;
std::function<void()> onClose;
};
typedef std::shared_ptr<WebSocketHandle> WebSocketHandlePtr;
class NetworkManager
{
public:
@@ -86,6 +128,7 @@ public:
bool IsUrlAllowed(const std::string& url);
HttpRequestFuturePtr HttpRequest(const HttpRequestArgs& args);
WebSocketHandlePtr WebSocket(const WebSocketArgs& args);
std::string UrlEncode(const std::string& value);
std::string EncodeQueryParameters(const std::unordered_map<std::string, std::string>& query);