From 5de32250c3790bce4d10e2c7d6647c3ede9bfab8 Mon Sep 17 00:00:00 2001 From: JCash Date: Wed, 2 Sep 2020 10:44:02 +0200 Subject: [PATCH] Websocket now supports html5 as well --- .gitignore | 1 + examples/websocket.gui_script | 24 +--- websocket/src/handshake.cpp | 158 ++++++++++++++------- websocket/src/socket.cpp | 8 ++ websocket/src/websocket.cpp | 224 ++++++++++++++++++++---------- websocket/src/websocket.h | 28 +++- websocket/src/wslay_callbacks.cpp | 75 ++++++++++ 7 files changed, 371 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index 2b43067..7d5711e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.internal /build +/bundle* .externalToolBuilders .DS_Store Thumbs.db diff --git a/examples/websocket.gui_script b/examples/websocket.gui_script index 6300753..2dd49f8 100644 --- a/examples/websocket.gui_script +++ b/examples/websocket.gui_script @@ -1,15 +1,9 @@ local URL="://echo.websocket.org" local function click_button(node, action) - --print("mouse", action.x, action.y) - return gui.is_enabled(node) and action.pressed and gui.pick_node(node, action.x, action.y) end -local function http_handle_response(self, id, response) - print(response.status, response.response) -end - local function update_gui(self) if self.connection then gui.set_enabled(self.connect_ws_node, false) @@ -47,11 +41,8 @@ function init(self) self.send_node = gui.get_node("send/button") self.close_node = gui.get_node("close/button") self.connection_text = gui.get_node("connection_text") - update_gui(self) - - --http.request("https://www.google.com", "GET", http_handle_response) - self.connection = nil + update_gui(self) end function final(self) @@ -78,19 +69,17 @@ local function websocket_callback(self, conn, data) self.connection = nil end update_gui(self) + elseif data.event == websocket.EVENT_ERROR then + if data.error then + log("Error:", data.error) + end elseif data.event == websocket.EVENT_MESSAGE then log("Receiving: '" .. tostring(data.message) .. "'") end end local function connect(self, scheme) - local params = { - mode = "client" --only supported mode currently - --mode = "client", - --protocol = "tlsv1_2", - --verify = "none", - --options = "all", - } + local params = {} self.url = scheme .. URL log("Connecting to " .. self.url) @@ -109,7 +98,6 @@ end function on_input(self, action_id, action) - --print("MAWE", action_id) if click_button(self.connect_ws_node, action) then connect(self, "ws") elseif click_button(self.connect_wss_node, action) then diff --git a/websocket/src/handshake.cpp b/websocket/src/handshake.cpp index cc83d1f..0b81930 100644 --- a/websocket/src/handshake.cpp +++ b/websocket/src/handshake.cpp @@ -16,13 +16,13 @@ static void CreateKey(uint8_t* key, size_t len) } #define WS_SENDALL(s) \ - sock_res = Send(conn, s, strlen(s), 0);\ - if (sock_res != dmSocket::RESULT_OK)\ + sr = Send(conn, s, strlen(s), 0);\ + if (sr != dmSocket::RESULT_OK)\ {\ goto bail;\ }\ -Result SendClientHandshake(WebsocketConnection* conn) +static Result SendClientHandshakeHeaders(WebsocketConnection* conn) { CreateKey(conn->m_Key, sizeof(conn->m_Key)); @@ -38,7 +38,7 @@ Result SendClientHandshake(WebsocketConnection* conn) if (!(conn->m_Url.m_Port == 80 || conn->m_Url.m_Port == 443)) dmSnPrintf(port, sizeof(port), ":%d", conn->m_Url.m_Port); - dmSocket::Result sock_res = dmSocket::RESULT_OK; + dmSocket::Result sr; WS_SENDALL("GET /"); WS_SENDALL(conn->m_Url.m_Path); WS_SENDALL(" HTTP/1.1\r\n"); @@ -60,9 +60,9 @@ Result SendClientHandshake(WebsocketConnection* conn) WS_SENDALL("\r\n"); bail: - if (sock_res != dmSocket::RESULT_OK) + if (sr != dmSocket::RESULT_OK) { - return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "SendClientHandshake failed: %s", dmSocket::ResultToString(sock_res)); + return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "SendClientHandshake failed: %s", dmSocket::ResultToString(sr)); } return RESULT_OK; @@ -70,68 +70,114 @@ bail: #undef WS_SENDALL - -// Currently blocking! -Result ReceiveHeaders(WebsocketConnection* conn) +Result SendClientHandshake(WebsocketConnection* conn) { - while (1) + dmSocket::Result sr = WaitForSocket(conn, dmSocket::SELECTOR_KIND_WRITE, SOCKET_WAIT_TIMEOUT); + if (dmSocket::RESULT_WOULDBLOCK == sr) { - int max_to_recv = (int)(conn->m_BufferCapacity - 1) - conn->m_BufferSize; // allow for a terminating null character - - if (max_to_recv <= 0) - { - return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive buffer full: %u bytes", conn->m_BufferCapacity); - } - - int recv_bytes = 0; - dmSocket::Result r = Receive(conn, conn->m_Buffer + conn->m_BufferSize, max_to_recv, &recv_bytes); - - if( r == dmSocket::RESULT_WOULDBLOCK ) - { - r = dmSocket::RESULT_TRY_AGAIN; - } - - if (r == dmSocket::RESULT_TRY_AGAIN) - continue; - - if (r != dmSocket::RESULT_OK) - { - return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive error: %s", dmSocket::ResultToString(r)); - } - - conn->m_BufferSize += recv_bytes; - - // NOTE: We have an extra byte for null-termination so no buffer overrun here. - conn->m_Buffer[conn->m_BufferSize] = '\0'; - - // Check if the end of the response has arrived - if (conn->m_BufferSize >= 4 && strcmp(conn->m_Buffer + conn->m_BufferSize - 4, "\r\n\r\n") == 0) - { - return RESULT_OK; - } - - if (r == 0) - { - return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed to parse headers:\n%s", conn->m_Buffer); - } + return RESULT_WOULDBLOCK; } + if (dmSocket::RESULT_OK != sr) + { + return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Connection not ready for sending data: %s", dmSocket::ResultToString(sr)); + } + +// In emscripten, the sockets are actually already websockets, so no handshake necessary +#if defined(__EMSCRIPTEN__) + return RESULT_OK; +#else + return SendClientHandshakeHeaders(conn); +#endif } + +#if defined(__EMSCRIPTEN__) +Result ReceiveHeaders(WebsocketConnection* conn) +{ + return RESULT_OK; +} + +#else +Result ReceiveHeaders(WebsocketConnection* conn) +{ + dmSocket::Selector selector; + dmSocket::SelectorZero(&selector); + dmSocket::SelectorSet(&selector, dmSocket::SELECTOR_KIND_READ, conn->m_Socket); + + dmSocket::Result sr = dmSocket::Select(&selector, 200*1000); + + if (dmSocket::RESULT_OK != sr) + { + if (dmSocket::RESULT_WOULDBLOCK) + { + dmLogWarning("Waiting for socket to be available for reading"); + return RESULT_WOULDBLOCK; + } + + return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed waiting for more handshake headers: %s", dmSocket::ResultToString(sr)); + } + + int max_to_recv = (int)(conn->m_BufferCapacity - 1) - conn->m_BufferSize; // allow for a terminating null character + + if (max_to_recv <= 0) + { + return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive buffer full: %u bytes", conn->m_BufferCapacity); + } + + int recv_bytes = 0; + sr = Receive(conn, conn->m_Buffer + conn->m_BufferSize, max_to_recv, &recv_bytes); + + if( sr == dmSocket::RESULT_WOULDBLOCK ) + { + sr = dmSocket::RESULT_TRY_AGAIN; + } + + if (sr == dmSocket::RESULT_TRY_AGAIN) + return RESULT_WOULDBLOCK; + + if (sr != dmSocket::RESULT_OK) + { + return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive error: %s", dmSocket::ResultToString(sr)); + } + + conn->m_BufferSize += recv_bytes; + + // NOTE: We have an extra byte for null-termination so no buffer overrun here. + conn->m_Buffer[conn->m_BufferSize] = '\0'; + + // Check if the end of the response has arrived + if (conn->m_BufferSize >= 4 && strcmp(conn->m_Buffer + conn->m_BufferSize - 4, "\r\n\r\n") == 0) + { + return RESULT_OK; + } + + return RESULT_WOULDBLOCK; +} +#endif + +#if defined(__EMSCRIPTEN__) +Result VerifyHeaders(WebsocketConnection* conn) +{ + return RESULT_OK; +} +#else Result VerifyHeaders(WebsocketConnection* conn) { char* r = conn->m_Buffer; - const char* http_version_and_status_protocol = "HTTP/1.1 101"; // optionally "Web Socket Protocol Handshake" + // According to protocol, the response should start with "HTTP/1.1 " + const char* http_version_and_status_protocol = "HTTP/1.1 101"; if (strstr(r, http_version_and_status_protocol) != r) { return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Missing: '%s' in header", http_version_and_status_protocol); } + r = strstr(r, "\r\n") + 2; bool upgraded = false; bool valid_key = false; const char* protocol = ""; - // Sec-WebSocket-Protocol + // TODO: Perhaps also support the Sec-WebSocket-Protocol // parse the headers in place while (r) @@ -155,7 +201,6 @@ Result VerifyHeaders(WebsocketConnection* conn) uint8_t client_key[32 + 40]; uint32_t client_key_len = sizeof(client_key); - //mbedtls_base64_encode((unsigned char*)client_key, sizeof(client_key), &client_key_len, (const unsigned char*)conn->m_Key, sizeof(conn->m_Key)); dmCrypt::Base64Encode(conn->m_Key, sizeof(conn->m_Key), client_key, &client_key_len); client_key[client_key_len] = 0; @@ -166,7 +211,6 @@ Result VerifyHeaders(WebsocketConnection* conn) uint8_t client_key_sha1[20]; dmCrypt::HashSha1(client_key, client_key_len, client_key_sha1); - //mbedtls_base64_encode((unsigned char*)client_key, sizeof(client_key), &client_key_len, client_key_sha1, sizeof(client_key_sha1)); client_key_len = sizeof(client_key); dmCrypt::Base64Encode(client_key_sha1, sizeof(client_key_sha1), client_key, &client_key_len); client_key[client_key_len] = 0; @@ -179,7 +223,17 @@ Result VerifyHeaders(WebsocketConnection* conn) break; } + if (!upgraded) + dmLogError("Failed to find the Upgrade keyword in the response headers"); + if (!valid_key) + dmLogError("Failed to find valid key in the response headers"); + + if (!(upgraded && valid_key)) { + dmLogError("Response:\n\"%s\"\n", conn->m_Buffer); + } + return (upgraded && valid_key) ? RESULT_OK : RESULT_HANDSHAKE_FAILED; } +#endif } // namespace \ No newline at end of file diff --git a/websocket/src/socket.cpp b/websocket/src/socket.cpp index af11717..2935c45 100644 --- a/websocket/src/socket.cpp +++ b/websocket/src/socket.cpp @@ -5,6 +5,14 @@ namespace dmWebsocket { +dmSocket::Result WaitForSocket(WebsocketConnection* conn, dmSocket::SelectorKind kind, int timeout) +{ + dmSocket::Selector selector; + dmSocket::SelectorZero(&selector); + dmSocket::SelectorSet(&selector, kind, conn->m_Socket); + return dmSocket::Select(&selector, timeout); +} + dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes) { int total_sent_bytes = 0; diff --git a/websocket/src/websocket.cpp b/websocket/src/websocket.cpp index 0f98331..996d9f6 100644 --- a/websocket/src/websocket.cpp +++ b/websocket/src/websocket.cpp @@ -8,6 +8,7 @@ #include "script_util.h" #include #include +#include namespace dmWebsocket { @@ -37,18 +38,44 @@ Result SetStatus(WebsocketConnection* conn, Result status, const char* format, . return status; } +static void HandleCallback(WebsocketConnection* conn, int event, const uint8_t* msg, size_t msg_len); + + +#define STRING_CASE(_X) case _X: return #_X; + +const char* ResultToString(Result err) +{ + switch(err) { + STRING_CASE(RESULT_OK); + STRING_CASE(RESULT_ERROR); + STRING_CASE(RESULT_FAIL_WSLAY_INIT); + STRING_CASE(RESULT_NOT_CONNECTED); + STRING_CASE(RESULT_HANDSHAKE_FAILED); + STRING_CASE(RESULT_WOULDBLOCK); + default: return "Unknown result"; + }; +} + +const char* StateToString(State err) +{ + switch(err) { + STRING_CASE(STATE_CONNECTING); + STRING_CASE(STATE_HANDSHAKE_WRITE); + STRING_CASE(STATE_HANDSHAKE_READ); + STRING_CASE(STATE_CONNECTED); + STRING_CASE(STATE_DISCONNECTED); + default: return "Unknown error"; + }; +} + +#undef STRING_CASE + +#define WS_DEBUG(...) +//#define WS_DEBUG dmLogWarning + // *************************************************************************************************** // LUA functions -const struct wslay_event_callbacks g_WslCallbacks = { - WSL_RecvCallback, - WSL_SendCallback, - WSL_GenmaskCallback, - NULL, - NULL, - NULL, - WSL_OnMsgRecvCallback -}; static WebsocketConnection* CreateConnection(const char* url) @@ -72,28 +99,36 @@ static WebsocketConnection* CreateConnection(const char* url) static void DestroyConnection(WebsocketConnection* conn) { +#if defined(HAVE_WSLAY) if (conn->m_State == STATE_CONNECTED) - wslay_event_context_free(conn->m_Ctx); + WSL_Exit(conn->m_Ctx); +#endif if (conn->m_Callback) dmScript::DestroyCallback(conn->m_Callback); if (conn->m_Connection) - dmConnectionPool::Close(g_Websocket.m_Pool, conn->m_Connection); + dmConnectionPool::Return(g_Websocket.m_Pool, conn->m_Connection); free((void*)conn->m_Buffer); free((void*)conn); } + static void CloseConnection(WebsocketConnection* conn) { + State prev_state = conn->m_State; + // we want it to send this message in the polling if (conn->m_State == STATE_CONNECTED) { - const char* reason = "Client wants to close"; - wslay_event_queue_close(conn->m_Ctx, 0, (const uint8_t*)reason, strlen(reason)); +#if defined(HAVE_WSLAY) + WSL_Close(conn->m_Ctx); +#endif } - else - conn->m_State = STATE_DISCONNECTED; + + conn->m_State = STATE_DISCONNECTED; + + WS_DEBUG("%s -> %s", StateToString(prev_state), StateToString(conn->m_State)); } static int FindConnection(WebsocketConnection* conn) @@ -177,6 +212,7 @@ static int LuaSend(lua_State* L) size_t string_length = 0; const char* string = luaL_checklstring(L, 2, &string_length); +#if defined(HAVE_WSLAY) int write_mode = WSLAY_BINARY_FRAME; // WSLAY_TEXT_FRAME struct wslay_event_msg msg; @@ -185,6 +221,17 @@ static int LuaSend(lua_State* L) msg.msg_length = string_length; wslay_event_queue_msg(conn->m_Ctx, &msg); // it makes a copy of the data +#else + + dmSocket::Result sr = Send(conn, string, string_length, 0); + if (dmSocket::RESULT_OK != sr) + { + conn->m_Status = RESULT_ERROR; + dmSnPrintf(conn->m_Buffer, conn->m_BufferCapacity, "Failed to send on websocket"); + HandleCallback(conn, EVENT_ERROR, 0, 0); + CloseConnection(conn); + } +#endif return 0; } @@ -229,25 +276,6 @@ static void HandleCallback(WebsocketConnection* conn, int event, const uint8_t* dmScript::TeardownCallback(conn->m_Callback); } -#define WSLAY_CASE(_X) case _X: return #_X; - -static const char* WSL_ResultToString(int err) -{ - switch(err) { - WSLAY_CASE(WSLAY_ERR_WANT_READ); - WSLAY_CASE(WSLAY_ERR_WANT_WRITE); - WSLAY_CASE(WSLAY_ERR_PROTO); - WSLAY_CASE(WSLAY_ERR_INVALID_ARGUMENT); - WSLAY_CASE(WSLAY_ERR_INVALID_CALLBACK); - WSLAY_CASE(WSLAY_ERR_NO_MORE_MSG); - WSLAY_CASE(WSLAY_ERR_CALLBACK_FAILURE); - WSLAY_CASE(WSLAY_ERR_WOULDBLOCK); - WSLAY_CASE(WSLAY_ERR_NOMEM); - default: return "Unknown error"; - }; -} - -#undef WSLAY_CASE // *************************************************************************************************** // Life cycle functions @@ -268,13 +296,14 @@ static void LuaInit(lua_State* L) // Register lua names luaL_register(L, MODULE_NAME, Websocket_module_methods); -#define SETCONSTANT(name, val) \ - lua_pushnumber(L, (lua_Number) val); \ - lua_setfield(L, -2, #name); +#define SETCONSTANT(_X) \ + lua_pushnumber(L, (lua_Number) _X); \ + lua_setfield(L, -2, #_X); - SETCONSTANT(EVENT_CONNECTED, EVENT_CONNECTED); - SETCONSTANT(EVENT_DISCONNECTED, EVENT_DISCONNECTED); - SETCONSTANT(EVENT_MESSAGE, EVENT_MESSAGE); + SETCONSTANT(EVENT_CONNECTED); + SETCONSTANT(EVENT_DISCONNECTED); + SETCONSTANT(EVENT_MESSAGE); + SETCONSTANT(EVENT_ERROR); #undef SETCONSTANT @@ -299,21 +328,26 @@ static dmExtension::Result WebsocketAppInitialize(dmExtension::AppParams* params dmLogError("Failed to create connection pool: %d", result); } +// We can do without the channel, it will then fallback to the dmSocket::GetHostname (as opposed to dmDNS::GetHostname) +#if defined(HAVE_WSLAY) dmDNS::Result dns_result = dmDNS::NewChannel(&g_Websocket.m_Channel); if (dmDNS::RESULT_OK != dns_result) { dmLogError("Failed to create connection pool: %d", dns_result); } +#endif g_Websocket.m_Initialized = 1; - if (g_Websocket.m_Channel == 0 || g_Websocket.m_Pool == 0) + if (!g_Websocket.m_Pool) { - if (g_Websocket.m_Channel) - dmDNS::DeleteChannel(g_Websocket.m_Channel); - if (g_Websocket.m_Pool) + if (!g_Websocket.m_Pool) + { + dmLogInfo("pool is null!"); dmConnectionPool::Delete(g_Websocket.m_Pool); + } + dmLogInfo("%s extension not initialized", MODULE_NAME); g_Websocket.m_Initialized = 0; } @@ -326,7 +360,7 @@ static dmExtension::Result WebsocketInitialize(dmExtension::Params* params) return dmExtension::RESULT_OK; LuaInit(params->m_L); - dmLogInfo("Registered %s extension\n", MODULE_NAME); + dmLogInfo("Registered %s extension", MODULE_NAME); return dmExtension::RESULT_OK; } @@ -366,37 +400,55 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params) } else if (STATE_CONNECTED == conn->m_State) { - // Do we need to loop here? - int err = 0; - if ((err = wslay_event_recv(conn->m_Ctx)) != 0 || (err = wslay_event_send(conn->m_Ctx)) != 0) { - dmLogError("Websocket poll error: %s from %s", WSL_ResultToString(err), conn->m_Url.m_Hostname); +#if defined(HAVE_WSLAY) + int r = WSL_Poll(conn->m_Ctx); + if (0 != r) + { + CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r)); + continue; + } + r = WSL_WantsExit(conn->m_Ctx); + if (0 != r) + { + CLOSE_CONN("Websocket received close event for %s", conn->m_Url.m_Hostname); + continue; + } +#else + int recv_bytes = 0; + dmSocket::Result sr = Receive(conn, conn->m_Buffer, conn->m_BufferCapacity, &recv_bytes); + if( sr == dmSocket::RESULT_WOULDBLOCK ) + { + continue; } - if ((wslay_event_get_close_sent(conn->m_Ctx) && wslay_event_get_close_received(conn->m_Ctx))) { - CLOSE_CONN("Websocket received close event for %s", conn->m_Url.m_Hostname); + if (dmSocket::RESULT_OK == sr) + { + conn->m_BufferSize += recv_bytes; + conn->m_HasMessage = 1; + } + else + { + CLOSE_CONN("Websocket failed to receive data %s", dmSocket::ResultToString(sr)); conn->m_State = STATE_DISCONNECTED; continue; } +#endif if (conn->m_HasMessage) { HandleCallback(conn, EVENT_MESSAGE, (uint8_t*)conn->m_Buffer, conn->m_BufferSize); conn->m_HasMessage = 0; + conn->m_BufferSize = 0; } } - else if (STATE_HANDSHAKE == conn->m_State) + else if (STATE_HANDSHAKE_READ == conn->m_State) { - // TODO: Split up this state into three? - // e.g. STATE_HANDSHAKE_SEND, STATE_HANDSHAKE_RECEIVE, STATE_HANDSHAKE_VERIFY - - Result result = SendClientHandshake(conn); - if (RESULT_OK != result) + Result result = ReceiveHeaders(conn); + if (RESULT_WOULDBLOCK == result) { - CLOSE_CONN("Failed sending handshake: %d", result); continue; } - result = ReceiveHeaders(conn); if (RESULT_OK != result) { CLOSE_CONN("Failed receiving handshake headers. %d", result); @@ -410,35 +462,55 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params) continue; } - // Currently only supports client implementation - int ret = -1; - ret = wslay_event_context_client_init(&conn->m_Ctx, &g_WslCallbacks, conn); - if (ret == 0) - wslay_event_config_set_max_recv_msg_length(conn->m_Ctx, g_Websocket.m_BufferSize); - if (ret != 0) +#if defined(HAVE_WSLAY) + int r = WSL_Init(&conn->m_Ctx, g_Websocket.m_BufferSize, (void*)conn); + if (0 != r) { - CLOSE_CONN("Failed initializing wslay: %s", WSL_ResultToString(ret)); - SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed initializing wslay: %s", WSL_ResultToString(ret)); + SetStatus(conn, RESULT_FAIL_WSLAY_INIT, "Failed initializing wslay: %s", WSL_ResultToString(r)); + CLOSE_CONN("Failed initializing wslay: %s", WSL_ResultToString(r)); continue; } - if (conn->m_Socket) { - dmSocket::SetNoDelay(conn->m_Socket, true); - dmSocket::SetBlocking(conn->m_Socket, false); - dmSocket::SetReceiveTimeout(conn->m_Socket, 500); - } + dmSocket::SetNoDelay(conn->m_Socket, true); + // Don't go lower than 1000 since some platforms might not have that good precision + dmSocket::SetReceiveTimeout(conn->m_Socket, 1000); + if (conn->m_SSLSocket) + dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000); +#endif + dmSocket::SetBlocking(conn->m_Socket, false); conn->m_Buffer[0] = 0; conn->m_BufferSize = 0; conn->m_State = STATE_CONNECTED; + WS_DEBUG("STATE_HANDSHAKE -> STATE_CONNECTED"); + HandleCallback(conn, EVENT_CONNECTED, 0, 0); } + else if (STATE_HANDSHAKE_WRITE == conn->m_State) + { + Result result = SendClientHandshake(conn); + if (RESULT_WOULDBLOCK == result) + { + continue; + } + if (RESULT_OK != result) + { + CLOSE_CONN("Failed sending handshake: %d", result); + continue; + } + + conn->m_State = STATE_HANDSHAKE_READ; + WS_DEBUG("STATE_HANDSHAKE_WRITE -> STATE_HANDSHAKE_READ"); + } else if (STATE_CONNECTING == conn->m_State) { - // wait for it to finish dmSocket::Result socket_result; - dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, g_Websocket.m_Channel, conn->m_SSL, g_Websocket.m_Timeout, &conn->m_Connection, &socket_result); + int timeout = g_Websocket.m_Timeout; +#if defined(__EMSCRIPTEN__) + timeout = 0; +#endif + dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, g_Websocket.m_Channel, conn->m_SSL, timeout, &conn->m_Connection, &socket_result); if (dmConnectionPool::RESULT_OK != pool_result) { CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(socket_result)); @@ -447,7 +519,9 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params) conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection); conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection); - conn->m_State = STATE_HANDSHAKE; + conn->m_State = STATE_HANDSHAKE_WRITE; + + WS_DEBUG("STATE_CONNECTING -> STATE_HANDSHAKE"); } } diff --git a/websocket/src/websocket.h b/websocket/src/websocket.h index c879d30..fe29684 100644 --- a/websocket/src/websocket.h +++ b/websocket/src/websocket.h @@ -3,7 +3,13 @@ // include the Defold SDK #include -#include +#if !defined(__EMSCRIPTEN__) + #define HAVE_WSLAY 1 +#endif + +#if defined(HAVE_WSLAY) + #include +#endif #include #include @@ -19,10 +25,14 @@ namespace dmCrypt namespace dmWebsocket { + // Maximum time to wait for a socket + static const int SOCKET_WAIT_TIMEOUT = 4*1000; + enum State { STATE_CONNECTING, - STATE_HANDSHAKE, + STATE_HANDSHAKE_WRITE, + STATE_HANDSHAKE_READ, STATE_CONNECTED, STATE_DISCONNECTED, }; @@ -30,9 +40,11 @@ namespace dmWebsocket enum Result { RESULT_OK, + RESULT_ERROR, RESULT_FAIL_WSLAY_INIT, RESULT_NOT_CONNECTED, RESULT_HANDSHAKE_FAILED, + RESULT_WOULDBLOCK, }; enum Event @@ -40,12 +52,15 @@ namespace dmWebsocket EVENT_CONNECTED, EVENT_DISCONNECTED, EVENT_MESSAGE, + EVENT_ERROR, }; struct WebsocketConnection { dmScript::LuaCallbackInfo* m_Callback; +#if defined(HAVE_WSLAY) wslay_event_context_ptr m_Ctx; +#endif dmURI::Parts m_Url; dmConnectionPool::HConnection m_Connection; dmSocket::Socket m_Socket; @@ -70,17 +85,26 @@ namespace dmWebsocket // Communication dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes); dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes); + dmSocket::Result WaitForSocket(WebsocketConnection* conn, dmSocket::SelectorKind kind, int timeout); // Handshake Result SendClientHandshake(WebsocketConnection* conn); Result ReceiveHeaders(WebsocketConnection* conn); Result VerifyHeaders(WebsocketConnection* conn); +#if defined(HAVE_WSLAY) // Wslay callbacks + int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx); + void WSL_Exit(wslay_event_context_ptr ctx); + int WSL_Close(wslay_event_context_ptr ctx); + int WSL_Poll(wslay_event_context_ptr ctx); + int WSL_WantsExit(wslay_event_context_ptr ctx); ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data); ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data); void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data); + const char* WSL_ResultToString(int err); +#endif // Random numbers (PCG) typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t; diff --git a/websocket/src/wslay_callbacks.cpp b/websocket/src/wslay_callbacks.cpp index 7a018b7..fd7739f 100644 --- a/websocket/src/wslay_callbacks.cpp +++ b/websocket/src/wslay_callbacks.cpp @@ -1,8 +1,81 @@ #include "websocket.h" +#if defined(HAVE_WSLAY) + namespace dmWebsocket { +const struct wslay_event_callbacks g_WslCallbacks = { + WSL_RecvCallback, + WSL_SendCallback, + WSL_GenmaskCallback, + NULL, + NULL, + NULL, + WSL_OnMsgRecvCallback +}; + +#define WSLAY_CASE(_X) case _X: return #_X; + +const char* WSL_ResultToString(int err) +{ + switch(err) { + WSLAY_CASE(WSLAY_ERR_WANT_READ); + WSLAY_CASE(WSLAY_ERR_WANT_WRITE); + WSLAY_CASE(WSLAY_ERR_PROTO); + WSLAY_CASE(WSLAY_ERR_INVALID_ARGUMENT); + WSLAY_CASE(WSLAY_ERR_INVALID_CALLBACK); + WSLAY_CASE(WSLAY_ERR_NO_MORE_MSG); + WSLAY_CASE(WSLAY_ERR_CALLBACK_FAILURE); + WSLAY_CASE(WSLAY_ERR_WOULDBLOCK); + WSLAY_CASE(WSLAY_ERR_NOMEM); + default: return "Unknown error"; + }; +} + +#undef WSLAY_CASE + + +int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx) +{ + // Currently only supports client implementation + int ret = -1; + ret = wslay_event_context_client_init(ctx, &g_WslCallbacks, userctx); + if (ret == 0) + wslay_event_config_set_max_recv_msg_length(*ctx, buffer_size); + return ret; +} + + +void WSL_Exit(wslay_event_context_ptr ctx) +{ + wslay_event_context_free(ctx); +} + +int WSL_Close(wslay_event_context_ptr ctx) +{ + const char* reason = "Client wants to close"; + wslay_event_queue_close(ctx, 0, (const uint8_t*)reason, strlen(reason)); + return 0; +} + +int WSL_Poll(wslay_event_context_ptr ctx) +{ + int r = 0; + if ((r = wslay_event_recv(ctx)) != 0 || (r = wslay_event_send(ctx)) != 0) { + dmLogError("Websocket poll error: %s", WSL_ResultToString(r)); + } + return r; +} + +int WSL_WantsExit(wslay_event_context_ptr ctx) +{ + if ((wslay_event_get_close_sent(ctx) && wslay_event_get_close_received(ctx))) { + return 1; + } + return 0; +} + ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data) { WebsocketConnection* conn = (WebsocketConnection*)user_data; @@ -135,3 +208,5 @@ int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, v } } // namespace + +#endif // HAVE_WSLAY \ No newline at end of file