From 6febca9158e443edd2b93e9f181b759f2cb33cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Fonseca?= Date: Mon, 1 Aug 2016 15:37:18 +0100 Subject: [PATCH] Implement initial version for websocket module --- app/Makefile | 4 +- app/include/user_modules.h | 1 + app/modules/Makefile | 1 + app/modules/websocket.c | 282 +++++++++++ app/websocket/Makefile | 49 ++ app/websocket/websocketclient.c | 819 ++++++++++++++++++++++++++++++++ app/websocket/websocketclient.h | 89 ++++ docs/en/modules/websocket.md | 174 +++++++ mkdocs.yml | 1 + 9 files changed, 1419 insertions(+), 1 deletion(-) create mode 100644 app/modules/websocket.c create mode 100644 app/websocket/Makefile create mode 100644 app/websocket/websocketclient.c create mode 100644 app/websocket/websocketclient.h create mode 100644 docs/en/modules/websocket.md diff --git a/app/Makefile b/app/Makefile index 1051145172..c86efb207f 100644 --- a/app/Makefile +++ b/app/Makefile @@ -42,7 +42,8 @@ SUBDIRS= \ dhtlib \ tsl2561 \ net \ - http + http \ + websocket endif # } PDIR @@ -87,6 +88,7 @@ COMPONENTS_eagle.app.v6 = \ dhtlib/libdhtlib.a \ tsl2561/tsl2561lib.a \ http/libhttp.a \ + websocket/libwebsocket.a \ net/libnodemcu_net.a \ modules/libmodules.a \ diff --git a/app/include/user_modules.h b/app/include/user_modules.h index cbb794c120..0073d562da 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -61,6 +61,7 @@ //#define LUA_USE_MODULES_U8G #define LUA_USE_MODULES_UART //#define LUA_USE_MODULES_UCG +//#define LUA_USE_MODULES_WEBSOCKET #define LUA_USE_MODULES_WIFI //#define LUA_USE_MODULES_WS2801 //#define LUA_USE_MODULES_WS2812 diff --git a/app/modules/Makefile b/app/modules/Makefile index ef6d6dae06..ac712a5aea 100644 --- a/app/modules/Makefile +++ b/app/modules/Makefile @@ -53,6 +53,7 @@ INCLUDES += -I ../smart INCLUDES += -I ../cjson INCLUDES += -I ../dhtlib INCLUDES += -I ../http +INCLUDES += -I ../websocket PDIR := ../$(PDIR) sinclude $(PDIR)Makefile diff --git a/app/modules/websocket.c b/app/modules/websocket.c new file mode 100644 index 0000000000..899d04dc21 --- /dev/null +++ b/app/modules/websocket.c @@ -0,0 +1,282 @@ +// Module for websockets + +// Example usage: +// ws = websocket.createClient() +// ws:on("connection", function() ws:send('hi') end) +// ws:on("receive", function(_, data, opcode) print(data) end) +// ws:on("close", function(_, reasonCode) print('ws closed', reasonCode) end) +// ws:connect('ws://echo.websocket.org') + +#include "lmem.h" +#include "lualib.h" +#include "lauxlib.h" +#include "platform.h" +#include "module.h" + +#include "c_types.h" +#include "c_string.h" + +#include "websocketclient.h" + +#define METATABLE_WSCLIENT "websocket.client" + +typedef struct ws_data { + int self_ref; + int onConnection; + int onReceive; + int onClose; +} ws_data; + +static void websocketclient_onConnectionCallback(ws_info *ws) { + NODE_DBG("websocketclient_onConnectionCallback\n"); + + lua_State *L = lua_getstate(); + + if (ws == NULL || ws->reservedData == NULL) { + luaL_error(L, "Client websocket is nil.\n"); + return; + } + ws_data *data = (ws_data *) ws->reservedData; + + if (data->onConnection != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, data->onConnection); // load the callback function + lua_rawgeti(L, LUA_REGISTRYINDEX, data->self_ref); // pass itself, #1 callback argument + lua_call(L, 1, 0); + } +} + +static void websocketclient_onReceiveCallback(ws_info *ws, char *message, int opCode) { + NODE_DBG("websocketclient_onReceiveCallback\n"); + + lua_State *L = lua_getstate(); + + if (ws == NULL || ws->reservedData == NULL) { + luaL_error(L, "Client websocket is nil.\n"); + return; + } + ws_data *data = (ws_data *) ws->reservedData; + + if (data->onReceive != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, data->onReceive); // load the callback function + lua_rawgeti(L, LUA_REGISTRYINDEX, data->self_ref); // pass itself, #1 callback argument + lua_pushstring(L, message); // #2 callback argument + lua_pushnumber(L, opCode); // #3 callback argument + lua_call(L, 3, 0); + } +} + +static void websocketclient_onCloseCallback(ws_info *ws, int errorCode) { + NODE_DBG("websocketclient_onCloseCallback\n"); + + lua_State *L = lua_getstate(); + + if (ws == NULL || ws->reservedData == NULL) { + luaL_error(L, "Client websocket is nil.\n"); + return; + } + ws_data *data = (ws_data *) ws->reservedData; + + if (data->onClose != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, data->onClose); // load the callback function + lua_rawgeti(L, LUA_REGISTRYINDEX, data->self_ref); // pass itself, #1 callback argument + lua_pushnumber(L, errorCode); // pass the error code, #2 callback argument + lua_call(L, 2, 0); + } + + // free self-reference to allow gc (no futher callback will be called until next ws:connect()) + lua_gc(L, LUA_GCSTOP, 0); // required to avoid freeing ws_data + luaL_unref(L, LUA_REGISTRYINDEX, data->self_ref); + data->self_ref = LUA_NOREF; + lua_gc(L, LUA_GCRESTART, 0); +} + +static int websocket_createClient(lua_State *L) { + NODE_DBG("websocket_createClient\n"); + + // create user data + ws_data *data = (ws_data *) luaM_malloc(L, sizeof(ws_data)); + data->onConnection = LUA_NOREF; + data->onReceive = LUA_NOREF; + data->onClose = LUA_NOREF; + data->self_ref = LUA_NOREF; // only set when ws:connect is called + + ws_info *ws = (ws_info *) lua_newuserdata(L, sizeof(ws_info)); + ws->connectionState = 0; + ws->onConnection = &websocketclient_onConnectionCallback; + ws->onReceive = &websocketclient_onReceiveCallback; + ws->onFailure = &websocketclient_onCloseCallback; + ws->reservedData = data; + + // set its metatable + luaL_getmetatable(L, METATABLE_WSCLIENT); + lua_setmetatable(L, -2); + + return 1; +} + +static int websocketclient_on(lua_State *L) { + NODE_DBG("websocketclient_on\n"); + + ws_info *ws = (ws_info *) luaL_checkudata(L, 1, METATABLE_WSCLIENT); + luaL_argcheck(L, ws, 1, "Client websocket expected"); + + ws_data *data = (ws_data *) ws->reservedData; + + int handle = luaL_checkoption(L, 2, NULL, (const char * const[]){ "connection", "receive", "close", NULL }); + if (lua_type(L, 3) != LUA_TNIL && lua_type(L, 3) != LUA_TFUNCTION && lua_type(L, 3) != LUA_TLIGHTFUNCTION) { + return luaL_typerror(L, 3, "function or nil"); + } + + switch (handle) { + case 0: + NODE_DBG("connection\n"); + + luaL_unref(L, LUA_REGISTRYINDEX, data->onConnection); + data->onConnection = LUA_NOREF; + + if (lua_type(L, 3) != LUA_TNIL) { + lua_pushvalue(L, 3); // copy argument (func) to the top of stack + data->onConnection = luaL_ref(L, LUA_REGISTRYINDEX); + } + break; + case 1: + NODE_DBG("receive\n"); + + luaL_unref(L, LUA_REGISTRYINDEX, data->onReceive); + data->onReceive = LUA_NOREF; + + if (lua_type(L, 3) != LUA_TNIL) { + lua_pushvalue(L, 3); // copy argument (func) to the top of stack + data->onReceive = luaL_ref(L, LUA_REGISTRYINDEX); + } + break; + case 2: + NODE_DBG("close\n"); + + luaL_unref(L, LUA_REGISTRYINDEX, data->onClose); + data->onClose = LUA_NOREF; + + if (lua_type(L, 3) != LUA_TNIL) { + lua_pushvalue(L, 3); // copy argument (func) to the top of stack + data->onClose = luaL_ref(L, LUA_REGISTRYINDEX); + } + break; + } + + return 0; +} + +static int websocketclient_connect(lua_State *L) { + NODE_DBG("websocketclient_connect is called.\n"); + + ws_info *ws = (ws_info *) luaL_checkudata(L, 1, METATABLE_WSCLIENT); + luaL_argcheck(L, ws, 1, "Client websocket expected"); + + ws_data *data = (ws_data *) ws->reservedData; + + if (ws->connectionState != 0 && ws->connectionState != 4) { + return luaL_error(L, "Websocket already connecting or connected.\n"); + } + ws->connectionState = 0; + + lua_pushvalue(L, 1); // copy userdata to the top of stack to allow ref + data->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + const char *url = luaL_checkstring(L, 2); + ws_connect(ws, url); + + return 0; +} + +static int websocketclient_send(lua_State *L) { + NODE_DBG("websocketclient_send is called.\n"); + + ws_info *ws = (ws_info *) luaL_checkudata(L, 1, METATABLE_WSCLIENT); + luaL_argcheck(L, ws, 1, "Client websocket expected"); + + ws_data *data = (ws_data *) ws->reservedData; + + if (ws->connectionState != 3) { + // should this be an onFailure callback instead? + return luaL_error(L, "Websocket isn't connected.\n"); + } + + int msgLength; + const char *msg = luaL_checklstring(L, 2, &msgLength); + + int opCode = 1; // default: text message + if (lua_gettop(L) == 3) { + opCode = luaL_checkint(L, 3); + } + + ws_send(ws, opCode, msg, (unsigned short) msgLength); + return 0; +} + +static int websocketclient_close(lua_State *L) { + NODE_DBG("websocketclient_close.\n"); + ws_info *ws = (ws_info *) luaL_checkudata(L, 1, METATABLE_WSCLIENT); + + ws_close(ws); + return 0; +} + +static int websocketclient_gc(lua_State *L) { + NODE_DBG("websocketclient_gc\n"); + + ws_info *ws = (ws_info *) luaL_checkudata(L, 1, METATABLE_WSCLIENT); + luaL_argcheck(L, ws, 1, "Client websocket expected"); + + ws_data *data = (ws_data *) ws->reservedData; + + luaL_unref(L, LUA_REGISTRYINDEX, data->onConnection); + luaL_unref(L, LUA_REGISTRYINDEX, data->onReceive); + + if (data->onClose != LUA_NOREF) { + if (ws->connectionState != 4) { // only call if connection open + lua_rawgeti(L, LUA_REGISTRYINDEX, data->onClose); + + lua_pushnumber(L, -100); + lua_call(L, 1, 0); + } + luaL_unref(L, LUA_REGISTRYINDEX, data->onClose); + } + + if (data->self_ref != LUA_NOREF) { + lua_gc(L, LUA_GCSTOP, 0); // required to avoid freeing ws_data + luaL_unref(L, LUA_REGISTRYINDEX, data->self_ref); + data->self_ref = LUA_NOREF; + lua_gc(L, LUA_GCRESTART, 0); + } + + NODE_DBG("freeing lua data\n"); + luaM_free(L, data); + NODE_DBG("done freeing lua data\n"); + + return 0; +} + +static const LUA_REG_TYPE websocket_map[] = +{ + { LSTRKEY("createClient"), LFUNCVAL(websocket_createClient) }, + { LNILKEY, LNILVAL } +}; + +static const LUA_REG_TYPE websocketclient_map[] = +{ + { LSTRKEY("on"), LFUNCVAL(websocketclient_on) }, + { LSTRKEY("connect"), LFUNCVAL(websocketclient_connect) }, + { LSTRKEY("send"), LFUNCVAL(websocketclient_send) }, + { LSTRKEY("close"), LFUNCVAL(websocketclient_close) }, + { LSTRKEY("__gc" ), LFUNCVAL(websocketclient_gc) }, + { LSTRKEY("__index"), LROVAL(websocketclient_map) }, + { LNILKEY, LNILVAL } +}; + +int loadWebsocketModule(lua_State *L) { + luaL_rometatable(L, METATABLE_WSCLIENT, (void *) websocketclient_map); + + return 0; +} + +NODEMCU_MODULE(WEBSOCKET, "websocket", websocket_map, loadWebsocketModule); diff --git a/app/websocket/Makefile b/app/websocket/Makefile new file mode 100644 index 0000000000..3cc87d5f42 --- /dev/null +++ b/app/websocket/Makefile @@ -0,0 +1,49 @@ + +############################################################# +# Required variables for each makefile +# Discard this section from all parent makefiles +# Expected variables (with automatic defaults): +# CSRCS (all "C" files in the dir) +# SUBDIRS (all subdirs with a Makefile) +# GEN_LIBS - list of libs to be generated () +# GEN_IMAGES - list of images to be generated () +# COMPONENTS_xxx - a list of libs/objs in the form +# subdir/lib to be extracted and rolled up into +# a generated lib/image xxx.a () +# +ifndef PDIR +GEN_LIBS = libwebsocket.a +endif + +STD_CFLAGS=-std=gnu11 -Wimplicit + +############################################################# +# Configuration i.e. compile options etc. +# Target specific stuff (defines etc.) goes in here! +# Generally values applying to a tree are captured in the +# makefile at its root level - these are then overridden +# for a subtree within the makefile rooted therein +# +#DEFINES += + +############################################################# +# Recursion Magic - Don't touch this!! +# +# Each subtree potentially has an include directory +# corresponding to the common APIs applicable to modules +# rooted at that subtree. Accordingly, the INCLUDE PATH +# of a module can only contain the include directories up +# its parent path, and not its siblings +# +# Required for each makefile to inherit from the parent +# + +INCLUDES := $(INCLUDES) -I $(PDIR)include +INCLUDES += -I ./ +INCLUDES += -I ./include +INCLUDES += -I ../include +INCLUDES += -I ../libc +INCLUDES += -I ../../include +PDIR := ../$(PDIR) +sinclude $(PDIR)Makefile + diff --git a/app/websocket/websocketclient.c b/app/websocket/websocketclient.c new file mode 100644 index 0000000000..bc0f72094a --- /dev/null +++ b/app/websocket/websocketclient.c @@ -0,0 +1,819 @@ +/* Websocket client implementation + * + * Copyright (c) 2016 Luís Fonseca + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "osapi.h" +#include "user_interface.h" +#include "espconn.h" +#include "mem.h" +#include "limits.h" +#include "stdlib.h" + +#include "c_types.h" +#include "c_string.h" +#include "c_stdlib.h" +#include "c_stdio.h" + +#include "websocketclient.h" + +// Depends on 'crypto' module for sha1 +#include "../crypto/digests.h" +#include "../crypto/mech.h" + +#define PROTOCOL_SECURE "wss://" +#define PROTOCOL_INSECURE "ws://" + +#define PORT_SECURE 443 +#define PORT_INSECURE 80 +#define PORT_MAX_VALUE 65535 + +#define SSL_BUFFER_SIZE 5120 + +// TODO: user agent configurable +#define WS_INIT_HEADERS "GET %s HTTP/1.1\r\n"\ + "Host: %s:%d\r\n"\ + "Upgrade: websocket\r\n"\ + "Connection: Upgrade\r\n"\ + "User-Agent: ESP8266\r\n"\ + "Sec-Websocket-Key: %s\r\n"\ + "Sec-WebSocket-Protocol: chat\r\n"\ + "Sec-WebSocket-Version: 13\r\n"\ + "\r\n" + +#define WS_INIT_HEADERS_LENGTH 169 +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +#define WS_GUID_LENGTH 36 + +#define WS_HTTP_SWITCH_PROTOCOL_HEADER "HTTP/1.1 101" +#define WS_HTTP_SEC_WEBSOCKET_ACCEPT "Sec-WebSocket-Accept:" + +#define WS_CONNECT_TIMEOUT_MS 10 * 1000 +#define WS_PING_INTERVAL_MS 30 * 1000 +#define WS_FORCE_CLOSE_TIMEOUT_MS 5 * 1000 +#define WS_UNHEALTHY_THRESHOLD 2 + +#define WS_OPCODE_CONTINUATION 0x0 +#define WS_OPCODE_TEXT 0x1 +#define WS_OPCODE_BINARY 0x2 +#define WS_OPCODE_CLOSE 0x8 +#define WS_OPCODE_PING 0x9 +#define WS_OPCODE_PONG 0xA + +static char *cryptoSha1(char *data, unsigned int len) { + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, data, len); + + uint8_t *digest = (uint8_t *) c_zalloc(20); + SHA1Final(digest, &ctx); + return (char *) digest; // Requires free +} + +static const char *bytes64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static char *base64Encode(char *data, unsigned int len) { + int blen = (len + 2) / 3 * 4; + + char *out = (char *) c_zalloc(blen + 1); + out[blen] = '\0'; + int j = 0, i; + for (i = 0; i < len; i += 3) { + int a = data[i]; + int b = (i + 1 < len) ? data[i + 1] : 0; + int c = (i + 2 < len) ? data[i + 2] : 0; + out[j++] = bytes64[a >> 2]; + out[j++] = bytes64[((a & 3) << 4) | (b >> 4)]; + out[j++] = (i + 1 < len) ? bytes64[((b & 15) << 2) | (c >> 6)] : 61; + out[j++] = (i + 2 < len) ? bytes64[(c & 63)] : 61; + } + + return out; // Requires free +} + +static void generateSecKeys(char **key, char **expectedKey) { + char rndData[16]; + int i; + for (i = 0; i < 16; i++) { + rndData[i] = (char) os_random(); + } + + *key = base64Encode(rndData, 16); + + // expectedKey = b64(sha1(keyB64 + GUID)) + char keyWithGuid[24 + WS_GUID_LENGTH]; + memcpy(keyWithGuid, *key, 24); + memcpy(keyWithGuid + 24, WS_GUID, WS_GUID_LENGTH); + + char *keyEncrypted = cryptoSha1(keyWithGuid, 24 + WS_GUID_LENGTH); + *expectedKey = base64Encode(keyEncrypted, 20); + + os_free(keyEncrypted); +} + +static void ws_closeSentCallback(void *arg) { + NODE_DBG("ws_closeSentCallback \n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + if (ws == NULL) { + NODE_DBG("ws is unexpectly null\n"); + return; + } + + ws->knownFailureCode = -6; + + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); +} + +static void ws_sendFrame(struct espconn *conn, int opCode, const char *data, unsigned short len) { + NODE_DBG("ws_sendFrame %d %d\n", opCode, len); + ws_info *ws = (ws_info *) conn->reverse; + + if (ws->connectionState == 4) { + NODE_DBG("already in closing state\n"); + return; + } else if (ws->connectionState != 3) { + NODE_DBG("can't send message while not in a connected state\n"); + return; + } + + char *b = c_zalloc(10 + len); // 10 bytes = worst case scenario for framming + if (b == NULL) { + NODE_DBG("Out of memory when receiving message, disconnecting...\n"); + + ws->knownFailureCode = -16; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + + b[0] = 1 << 7; // has fin + b[0] += opCode; + b[1] = 1 << 7; // has mask + int bufOffset; + if (len < 126) { + b[1] += len; + bufOffset = 2; + } else if (len < 0x10000) { + b[1] += 126; + b[2] = len >> 8; + b[3] = len; + bufOffset = 4; + } else { + b[1] += 127; + b[2] = len >> 24; + b[3] = len >> 16; + b[4] = len >> 8; + b[5] = len; + bufOffset = 6; + } + + // Random mask: + b[bufOffset] = (char) os_random(); + b[bufOffset + 1] = (char) os_random(); + b[bufOffset + 2] = (char) os_random(); + b[bufOffset + 3] = (char) os_random(); + bufOffset += 4; + + // Copy data to buffer + memcpy(b + bufOffset, data, len); + + // Apply mask to encode payload + int i; + for (i = 0; i < len; i++) { + b[bufOffset + i] ^= b[bufOffset - 4 + i % 4]; + } + bufOffset += len; + + NODE_DBG("b[0] = %d \n", b[0]); + NODE_DBG("b[1] = %d \n", b[1]); + NODE_DBG("b[2] = %d \n", b[2]); + NODE_DBG("b[3] = %d \n", b[3]); + NODE_DBG("b[4] = %d \n", b[4]); + NODE_DBG("b[5] = %d \n", b[5]); + NODE_DBG("b[6] = %d \n", b[6]); + NODE_DBG("b[7] = %d \n", b[7]); + NODE_DBG("b[8] = %d \n", b[8]); + NODE_DBG("b[9] = %d \n", b[9]); + + NODE_DBG("sending message\n"); + if (ws->isSecure) + espconn_secure_send(conn, (uint8_t *) b, bufOffset); + else + espconn_send(conn, (uint8_t *) b, bufOffset); + + os_free(b); +} + +static void ws_sendPingTimeout(void *arg) { + NODE_DBG("ws_sendPingTimeout \n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + if (ws->unhealthyPoints == WS_UNHEALTHY_THRESHOLD) { + // several pings were sent but no pongs nor messages + ws->knownFailureCode = -19; + + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + + ws_sendFrame(conn, WS_OPCODE_PING, NULL, 0); + ws->unhealthyPoints += 1; +} + +static void ws_receiveCallback(void *arg, char *buf, unsigned short len) { + NODE_DBG("ws_receiveCallback %d \n", len); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + ws->unhealthyPoints = 0; // received data, connection is healthy + os_timer_disarm(&ws->timeoutTimer); // reset ping check + os_timer_arm(&ws->timeoutTimer, WS_PING_INTERVAL_MS, true); + + + char *b = buf; + if (ws->frameBuffer != NULL) { // Append previous frameBuffer with new content + NODE_DBG("Appending new frameBuffer to old one \n"); + + ws->frameBuffer = c_realloc(ws->frameBuffer, ws->frameBufferLen + len); + if (ws->frameBuffer == NULL) { + NODE_DBG("Failed to allocate new framebuffer, disconnecting...\n"); + + ws->knownFailureCode = -8; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(ws->frameBuffer + ws->frameBufferLen, b, len); + + ws->frameBufferLen += len; + + len = ws->frameBufferLen; + b = ws->frameBuffer; + NODE_DBG("New frameBufferLen: %d\n", len); + } + + while (b != NULL) { // several frames can be present, b pointer will be moved to the next frame + NODE_DBG("b[0] = %d \n", b[0]); + NODE_DBG("b[1] = %d \n", b[1]); + NODE_DBG("b[2] = %d \n", b[2]); + NODE_DBG("b[3] = %d \n", b[3]); + NODE_DBG("b[4] = %d \n", b[4]); + NODE_DBG("b[5] = %d \n", b[5]); + NODE_DBG("b[6] = %d \n", b[6]); + NODE_DBG("b[7] = %d \n", b[7]); + + int isFin = b[0] & 0x80 ? 1 : 0; + int opCode = b[0] & 0x0f; + int hasMask = b[1] & 0x80 ? 1 : 0; + uint64_t payloadLength = b[1] & 0x7f; + int bufOffset = 2; + if (payloadLength == 126) { + payloadLength = (b[2] << 8) + b[3]; + bufOffset = 4; + } else if (payloadLength == 127) { // this will clearly not hold in heap, abort?? + payloadLength = (b[2] << 24) + (b[3] << 16) + (b[4] << 8) + b[5]; + bufOffset = 6; + } + + if (hasMask) { + int maskOffset = bufOffset; + bufOffset += 4; + + int i; + for (i = 0; i < payloadLength; i++) { + b[bufOffset + i] ^= b[maskOffset + i % 4]; // apply mask to decode payload + } + } + + if (payloadLength > len - bufOffset) { + NODE_DBG("INCOMPLETE Frame \n"); + if (ws->frameBuffer == NULL) { + NODE_DBG("Allocing new frameBuffer \n"); + ws->frameBuffer = c_zalloc(len); + if (ws->frameBuffer == NULL) { + NODE_DBG("Failed to allocate framebuffer, disconnecting... \n"); + + ws->knownFailureCode = -9; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(ws->frameBuffer, b, len); + ws->frameBufferLen = len; + } + break; // since the buffer were already concat'ed, wait for the next receive + } + + if (!isFin) { + NODE_DBG("PARTIAL frame! Should concat payload and later restore opcode\n"); + if(ws->payloadBuffer == NULL) { + NODE_DBG("Allocing new payloadBuffer \n"); + ws->payloadBuffer = c_zalloc(payloadLength); + if (ws->payloadBuffer == NULL) { + NODE_DBG("Failed to allocate payloadBuffer, disconnecting...\n"); + + ws->knownFailureCode = -10; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(ws->payloadBuffer, b + bufOffset, payloadLength); + ws->frameBufferLen = payloadLength; + ws->payloadOriginalOpCode = opCode; + } else { + NODE_DBG("Appending new payloadBuffer to old one \n"); + ws->payloadBuffer = c_realloc(ws->payloadBuffer, ws->payloadBufferLen + payloadLength); + if (ws->payloadBuffer == NULL) { + NODE_DBG("Failed to allocate new framebuffer, disconnecting...\n"); + + ws->knownFailureCode = -11; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(ws->payloadBuffer + ws->payloadBufferLen, b + bufOffset, payloadLength); + + ws->payloadBufferLen += payloadLength; + } + } else { + char *payload; + if (opCode == WS_OPCODE_CONTINUATION) { + NODE_DBG("restoring original opcode\n"); + if (ws->payloadBuffer == NULL) { + NODE_DBG("Got FIN continuation frame but didn't receive any beforehand, disconnecting...\n"); + + ws->knownFailureCode = -15; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + // concat buffer with payload + payload = c_zalloc(ws->payloadBufferLen + payloadLength); + + if (payload == NULL) { + NODE_DBG("Failed to allocate new framebuffer, disconnecting...\n"); + + ws->knownFailureCode = -12; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(payload, ws->payloadBuffer, ws->payloadBufferLen); + memcpy(payload + ws->payloadBufferLen, b + bufOffset, payloadLength); + + os_free(ws->payloadBuffer); // free previous buffer + ws->payloadBuffer = NULL; + + payloadLength += ws->payloadBufferLen; + ws->payloadBufferLen = 0; + + opCode = ws->payloadOriginalOpCode; + ws->payloadOriginalOpCode = 0; + } else { + int extensionDataOffset = 0; + + if (opCode == WS_OPCODE_CLOSE && payloadLength > 0) { + unsigned int reasonCode = b[bufOffset] << 8 + b[bufOffset + 1]; + NODE_DBG("Closing due to: %d\n", reasonCode); // Must not be shown to client as per spec + extensionDataOffset += 2; + } + + payload = c_zalloc(payloadLength - extensionDataOffset + 1); + if (payload == NULL) { + NODE_DBG("Failed to allocate payload, disconnecting...\n"); + + ws->knownFailureCode = -13; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + + memcpy(payload, b + bufOffset + extensionDataOffset, payloadLength - extensionDataOffset); + payload[payloadLength - extensionDataOffset] = '\0'; + } + + NODE_DBG("isFin %d \n", isFin); + NODE_DBG("opCode %d \n", opCode); + NODE_DBG("hasMask %d \n", hasMask); + NODE_DBG("payloadLength %d \n", payloadLength); + NODE_DBG("len %d \n", len); + NODE_DBG("bufOffset %d \n", bufOffset); + + if (opCode == WS_OPCODE_CLOSE) { + NODE_DBG("Closing message: %s\n", payload); // Must not be shown to client as per spec + + espconn_regist_sentcb(conn, ws_closeSentCallback); + ws_sendFrame(conn, WS_OPCODE_CLOSE, (const char *) (b + bufOffset), (unsigned short) payloadLength); + ws->connectionState = 4; + } else if (opCode == WS_OPCODE_PING) { + ws_sendFrame(conn, WS_OPCODE_PONG, (const char *) (b + bufOffset), (unsigned short) payloadLength); + } else if (opCode == WS_OPCODE_PONG) { + // ping alarm was already reset... + } else { + if (ws->onReceive) ws->onReceive(ws, payload, opCode); + } + os_free(payload); + } + + bufOffset += payloadLength; + NODE_DBG("bufOffset %d \n", bufOffset); + if (bufOffset == len) { // (bufOffset > len) won't happen here because it's being checked earlier + b = NULL; + if (ws->frameBuffer != NULL) { // the last frame inside buffer was processed + os_free(ws->frameBuffer); + ws->frameBuffer = NULL; + ws->frameBufferLen = 0; + } + } else { + len -= bufOffset; + b += bufOffset; // move b to next frame + if (ws->frameBuffer != NULL) { + NODE_DBG("Reallocing frameBuffer to remove consumed frame\n"); + + ws->frameBuffer = c_realloc(ws->frameBuffer, ws->frameBufferLen + len); + if (ws->frameBuffer == NULL) { + NODE_DBG("Failed to allocate new frame buffer, disconnecting...\n"); + + ws->knownFailureCode = -14; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + memcpy(ws->frameBuffer + ws->frameBufferLen, b, len); + + ws->frameBufferLen += len; + b = ws->frameBuffer; + } + } + } +} + +static void ws_initReceiveCallback(void *arg, char *buf, unsigned short len) { + NODE_DBG("ws_initReceiveCallback %d \n", len); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + // Check server is switch protocols + if (strstr(buf, WS_HTTP_SWITCH_PROTOCOL_HEADER) == NULL) { + NODE_DBG("Server is not switching protocols\n"); + ws->knownFailureCode = -17; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + + // Check server has valid sec key + if (strstr(buf, WS_HTTP_SEC_WEBSOCKET_ACCEPT) == NULL || strstr(buf, ws->expectedSecKey) == NULL) { + NODE_DBG("Server has invalid response\n"); + ws->knownFailureCode = -7; + if (ws->isSecure) + espconn_secure_disconnect(conn); + else + espconn_disconnect(conn); + return; + } + + NODE_DBG("Server response is valid, it's now a websocket!\n"); + + os_timer_disarm(&ws->timeoutTimer); + os_timer_setfn(&ws->timeoutTimer, (os_timer_func_t *) ws_sendPingTimeout, conn); + os_timer_arm(&ws->timeoutTimer, WS_PING_INTERVAL_MS, true); + + espconn_regist_recvcb(conn, ws_receiveCallback); + + if (ws->onConnection) ws->onConnection(ws); + + char *data = strstr(buf, "\r\n\r\n"); + unsigned short dataLength = len - (data - buf) - 4; + + NODE_DBG("dataLength = %d\n", len - (data - buf) - 4); + + if (data != NULL && dataLength > 0) { // handshake already contained a frame + ws_receiveCallback(arg, data + 4, dataLength); + } +} + +static void connect_callback(void *arg) { + NODE_DBG("Connected\n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + ws->connectionState = 3; + + espconn_regist_recvcb(conn, ws_initReceiveCallback); + + char *key; + generateSecKeys(&key, &ws->expectedSecKey); + + char buf[WS_INIT_HEADERS_LENGTH + strlen(ws->path) + strlen(ws->hostname) + strlen(key)]; + int len = os_sprintf(buf, WS_INIT_HEADERS, ws->path, ws->hostname, ws->port, key); + + os_free(key); + + NODE_DBG("connecting\n"); + if (ws->isSecure) + espconn_secure_send(conn, (uint8_t *) buf, len); + else + espconn_send(conn, (uint8_t *) buf, len); +} + +static void disconnect_callback(void *arg) { + NODE_DBG("disconnect_callback\n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + ws->connectionState = 4; + + os_timer_disarm(&ws->timeoutTimer); + + NODE_DBG("ws->hostname %d\n", ws->hostname); + os_free(ws->hostname); + NODE_DBG("ws->path %d\n ", ws->path); + os_free(ws->path); + + if (ws->expectedSecKey != NULL) { + os_free(ws->expectedSecKey); + } + + if (ws->frameBuffer != NULL) { + os_free(ws->frameBuffer); + } + + if (ws->payloadBuffer != NULL) { + os_free(ws->payloadBuffer); + } + + if (conn->proto.tcp != NULL) { + os_free(conn->proto.tcp); + } + + NODE_DBG("conn %d\n", conn); + espconn_delete(conn); + + NODE_DBG("freeing conn1 \n"); + + os_free(conn); + ws->conn = NULL; + + if (ws->onFailure) { + if (ws->knownFailureCode) ws->onFailure(ws, ws->knownFailureCode); + else ws->onFailure(ws, -99); + } +} + +static void ws_connectTimeout(void *arg) { + NODE_DBG("ws_connectTimeout\n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + ws->knownFailureCode = -18; + disconnect_callback(arg); +} + +static void error_callback(void * arg, sint8 errType) { + NODE_DBG("error_callback %d\n", errType); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + ws->knownFailureCode = ((int) errType) - 100; + disconnect_callback(arg); +} + +static void dns_callback(const char *hostname, ip_addr_t *addr, void *arg) { + NODE_DBG("dns_callback\n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + if (ws->conn == NULL || ws->connectionState == 4) { + return; + } + + if (addr == NULL) { + ws->knownFailureCode = -5; + disconnect_callback(arg); + return; + } + + ws->connectionState = 2; + + os_memcpy(conn->proto.tcp->remote_ip, addr, 4); + + espconn_regist_connectcb(conn, connect_callback); + espconn_regist_disconcb(conn, disconnect_callback); + espconn_regist_reconcb(conn, error_callback); + + // Set connection timeout timer + os_timer_disarm(&ws->timeoutTimer); + os_timer_setfn(&ws->timeoutTimer, (os_timer_func_t *) ws_connectTimeout, conn); + os_timer_arm(&ws->timeoutTimer, WS_CONNECT_TIMEOUT_MS, false); + + if (ws->isSecure) { + NODE_DBG("secure connecting \n"); + espconn_secure_set_size(ESPCONN_CLIENT, SSL_BUFFER_SIZE); + espconn_secure_connect(conn); + } + else { + NODE_DBG("insecure connecting \n"); + espconn_connect(conn); + } + + NODE_DBG("DNS found %s " IPSTR " \n", hostname, IP2STR(addr)); +} + +void ws_connect(ws_info *ws, const char *url) { + NODE_DBG("ws_connect called\n"); + + if (ws == NULL) { + NODE_DBG("ws_connect ws_info argument is null!"); + return; + } + + if (url == NULL) { + NODE_DBG("url is null!"); + return; + } + + // Extract protocol - either ws or wss + bool isSecure = c_strncasecmp(url, PROTOCOL_SECURE, strlen(PROTOCOL_SECURE)) == 0; + + if (isSecure) { + url += strlen(PROTOCOL_SECURE); + } else { + if (c_strncasecmp(url, PROTOCOL_INSECURE, strlen(PROTOCOL_INSECURE)) != 0) { + NODE_DBG("Failed to extract protocol from: %s\n", url); + if (ws->onFailure) ws->onFailure(ws, -1); + return; + } + url += strlen(PROTOCOL_INSECURE); + } + + // Extract path - it should start with '/' + char *path = c_strchr(url, '/'); + + // Extract hostname, possibly including port + char hostname[256]; + if (path) { + if (path - url >= sizeof(hostname)) { + NODE_DBG("Hostname too large"); + if (ws->onFailure) ws->onFailure(ws, -2); + return; + } + memcpy(hostname, url, path - url); + hostname[path - url] = '\0'; + } else { + // no path found, assuming the url only refers to the hostname and possibly the port + memcpy(hostname, url, strlen(url)); + hostname[strlen(url)] = '\0'; + path = "/"; + } + + // Extract port from hostname, if available + char *portInHostname = strchr(hostname, ':'); + int port; + if (portInHostname) { + port = atoi(portInHostname + 1); + if (port <= 0 || port > PORT_MAX_VALUE) { + NODE_DBG("Invalid port number\n"); + if (ws->onFailure) ws->onFailure(ws, -3); + return; + } + hostname[strlen(hostname) - strlen(portInHostname)] = '\0'; // remove port from hostname + } else { + port = isSecure ? PORT_SECURE : PORT_INSECURE; + } + + if (strlen(hostname) == 0) { + NODE_DBG("Failed to extract hostname\n"); + if (ws->onFailure) ws->onFailure(ws, -4); + return; + } + + NODE_DBG("secure protocol = %d\n", isSecure); + NODE_DBG("hostname = %s\n", hostname); + NODE_DBG("port = %d\n", port); + NODE_DBG("path = %s\n", path); + + // Prepare internal ws_info + ws->connectionState = 1; + ws->isSecure = isSecure; + ws->hostname = c_strdup(hostname); + ws->port = port; + ws->path = c_strdup(path); + ws->expectedSecKey = NULL; + ws->knownFailureCode = 0; + ws->frameBuffer = NULL; + ws->frameBufferLen = 0; + ws->payloadBuffer = NULL; + ws->payloadBufferLen = 0; + ws->payloadOriginalOpCode = 0; + ws->unhealthyPoints = 0; + + // Prepare espconn + struct espconn *conn = (struct espconn *) c_zalloc(sizeof(struct espconn)); + conn->type = ESPCONN_TCP; + conn->state = ESPCONN_NONE; + conn->proto.tcp = (esp_tcp *) c_zalloc(sizeof(esp_tcp)); + conn->proto.tcp->local_port = espconn_port(); + conn->proto.tcp->remote_port = ws->port; + + conn->reverse = ws; + ws->conn = conn; + + // Attempt to resolve hostname address + ip_addr_t addr; + err_t result = espconn_gethostbyname(conn, hostname, &addr, dns_callback); + + if (result == ESPCONN_INPROGRESS) { + NODE_DBG("DNS pending\n"); + } else { + dns_callback(hostname, &addr, conn); + } + + return; +} + +void ws_send(ws_info *ws, int opCode, const char *message, unsigned short length) { + NODE_DBG("ws_send\n"); + ws_sendFrame(ws->conn, opCode, message, length); +} + +static void ws_forceCloseTimeout(void *arg) { + NODE_DBG("ws_forceCloseTimeout\n"); + struct espconn *conn = (struct espconn *) arg; + ws_info *ws = (ws_info *) conn->reverse; + + if (ws->connectionState == 0 || ws->connectionState == 4) { + return; + } + + if (ws->isSecure) + espconn_secure_disconnect(ws->conn); + else + espconn_disconnect(ws->conn); +} + +void ws_close(ws_info *ws) { + NODE_DBG("ws_close\n"); + + if (ws->connectionState == 0 || ws->connectionState == 4) { + return; + } + + ws->knownFailureCode = 0; // no error as user requested to close + if (ws->connectionState == 1) { + disconnect_callback(ws->conn); + } else { + ws_sendFrame(ws->conn, WS_OPCODE_CLOSE, NULL, 0); + + os_timer_disarm(&ws->timeoutTimer); + os_timer_setfn(&ws->timeoutTimer, (os_timer_func_t *) ws_forceCloseTimeout, ws->conn); + os_timer_arm(&ws->timeoutTimer, WS_FORCE_CLOSE_TIMEOUT_MS, false); + } +} diff --git a/app/websocket/websocketclient.h b/app/websocket/websocketclient.h new file mode 100644 index 0000000000..35746b48f2 --- /dev/null +++ b/app/websocket/websocketclient.h @@ -0,0 +1,89 @@ +/* Websocket client implementation + * + * Copyright (c) 2016 Luís Fonseca + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef _WEBSOCKET_H_ +#define _WEBSOCKET_H_ + +#include "osapi.h" +#include "user_interface.h" +#include "espconn.h" +#include "mem.h" +#include "limits.h" +#include "stdlib.h" + +#if defined(USES_SDK_BEFORE_V140) +#define espconn_send espconn_sent +#define espconn_secure_send espconn_secure_sent +#endif + +struct ws_info; + +typedef void (*ws_onConnectionCallback)(struct ws_info *wsInfo); +typedef void (*ws_onReceiveCallback)(struct ws_info *wsInfo, char *message, int opCode); +typedef void (*ws_onFailureCallback)(struct ws_info *wsInfo, int errorCode); + +typedef struct ws_info { + int connectionState; + + bool isSecure; + char *hostname; + int port; + char *path; + char *expectedSecKey; + + struct espconn *conn; + void *reservedData; + int knownFailureCode; + + char *frameBuffer; + int frameBufferLen; + + char *payloadBuffer; + int payloadBufferLen; + int payloadOriginalOpCode; + + os_timer_t timeoutTimer; + int unhealthyPoints; + + ws_onConnectionCallback onConnection; + ws_onReceiveCallback onReceive; + ws_onFailureCallback onFailure; +} ws_info; + +/* + * Attempts to estabilish a websocket connection to the given url. + */ +void ws_connect(ws_info *wsInfo, const char *url); + +/* + * Sends a message with a given opcode. + */ +void ws_send(ws_info *wsInfo, int opCode, const char *message, unsigned short length); + +/* + * Disconnects existing conection and frees memory. + */ +void ws_close(ws_info *wsInfo); + +#endif // _WEBSOCKET_H_ diff --git a/docs/en/modules/websocket.md b/docs/en/modules/websocket.md new file mode 100644 index 0000000000..177c9abec2 --- /dev/null +++ b/docs/en/modules/websocket.md @@ -0,0 +1,174 @@ +# Websocket Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2016-08-02 | [Luís Fonseca](https://github.com/luismfonseca) | [Luís Fonseca](https://github.com/luismfonseca) | [websocket.c](../../../app/modules/websocket.c)| + +A websocket *client* module that implements [RFC6455](https://tools.ietf.org/html/rfc6455) (version 13) and provides a simple interface to send and receive messages. + +The implementation supports fragmented messages, automatically respondes to ping requests and periodically pings if the server isn't communicating. + +!!! note + + Currently, it is **not** possible to change the request headers, most notably the user agent. + +**SSL/TLS support** + +Take note of constraints documented in the [net module](net.md). + + +## websocket.createClient() + +Creates a new websocket client. This client should be stored in a variable and will provide all the functions to handle a connection. + +When the connection becomes closed, the same client can still be reused - the callback functions are kept - and you can connect again to any server. + +Before disposing the client, make sure to call `ws:close()`. + +#### Syntax +`websocket.createClient()` + +#### Parameters +none + +#### Returns +`websocketclient` + +#### Example +```lua +local ws = websocket.createClient() +-- ... +ws:close() +ws = nil +``` + + +## websocket.client:close() + +Closes a websocket connection. The client issues a close frame and attemtps to gracefully close the websocket. +If server doesn't reply, the connection is terminated after a small timeout. + +This function can be called even if the websocket isn't connected. + +This function must *always* be called before disposing the reference to the websocket client. + +#### Syntax +`websocket:close()` + +#### Parameters +none + +#### Returns +`nil` + +#### Example +```lua +ws = websocket.createClient() +ws:close() +ws:close() -- nothing will happen + +ws = nil -- fully dispose the client as lua will now gc it +``` + + +## websocket.client:connect() + +Attempts to estabilish a websocket connection to the given URL. + +#### Syntax +`websocket:connect(url)` + +#### Parameters +- `url` the URL for the websocket. + +#### Returns +`nil` + +#### Example +```lua +ws = websocket.createClient() +ws:connect('ws://echo.websocket.org') +``` + +If it fails, an error will be delivered via `websocket:on("close", handler)`. + + +## websocket.client:on() + +Registers the callback function to handle websockets events (there can be only one handler function registered per event type). + +#### Syntax +`websocket:on(eventName, function(ws, ...))` + +#### Parameters +- `eventName` the type of websocket event to register the callback function. Those events are: `connection`, `receive` and `close`. +- `function(ws, ...)` callback function. +The function first parameter is always the websocketclient. +Other arguments are required depending on the event type. See example for more details. +If `nil`, any previously configured callback is unregistered. + +#### Returns +`nil` + +#### Example +```lua +local ws = websocket.createClient() +ws:on("connection", function(ws) + print('got ws connection') +end) +ws:on("receive", function(_, msg, opcode) + print('got message:', msg, opcode) -- opcode is 1 for text message, 2 for binary +end) +ws:on("close", function(_, status) + print('connection closed', status) + ws = nil -- required to lua gc the websocket client +end) + +ws:connect('ws://echo.websocket.org') +``` + +Note that the close callback is also triggered if any error occurs. + +The status code for the close, if not 0 then it represents an error, as described in the following table. + + +| Status Code | Explanation | +| :----------- | :----------- | +| 0 | User requested close or the connection was terminated gracefully | +| -1 | Failed to extract protocol from URL | +| -2 | Hostname is too large (>256 chars) | +| -3 | Invalid port number (must be >0 and <= 65535) | +| -4 | Failed to extract hostname | +| -5 | DNS failed to lookup hostname | +| -6 | Server requested termination | +| -7 | Server sent invalid handshake HTTP response (i.e. server sent a bad key) | +| -8 to -14 | Failed to allocate memory to receive message | +| -15 | Server not following FIN bit protocol correctly | +| -16 | Failed to allocate memory to send message | +| -17 | Server is not switching protocols | +| -18 | Connect timeout | +| -19 | Server is not responding to health checks nor communicating | +| -99 to -999 | Well, something bad has happenned | + + +## websocket.client:send() + +Sends a message through the websocket connection. + +#### Syntax +`websocket:send(message, opcode)` + +#### Parameters +- `message` the data to send. +- `opcode` optionally set the opcode (default: 1, text message) + +#### Returns +`nil` or an error if socket is not connected + +#### Example +```lua +ws = websocket.createClient() +ws:on("connection", function() + ws:send('hello!') +end) +ws:connect('ws://echo.websocket.org') +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7c05db1180..b1abadae88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,6 +78,7 @@ pages: - 'u8g': 'en/modules/u8g.md' - 'uart': 'en/modules/uart.md' - 'ucg': 'en/modules/ucg.md' + - 'websocket': 'en/modules/websocket.md' - 'wifi': 'en/modules/wifi.md' - 'ws2801': 'en/modules/ws2801.md' - 'ws2812': 'en/modules/ws2812.md'