Implement initial version for websocket module (#1433)
This commit is contained in:
parent
3c55e8c026
commit
16051d51d6
|
@ -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 \
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -53,6 +53,7 @@ INCLUDES += -I ../smart
|
|||
INCLUDES += -I ../cjson
|
||||
INCLUDES += -I ../dhtlib
|
||||
INCLUDES += -I ../http
|
||||
INCLUDES += -I ../websocket
|
||||
PDIR := ../$(PDIR)
|
||||
sinclude $(PDIR)Makefile
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
@ -0,0 +1,819 @@
|
|||
/* Websocket client implementation
|
||||
*
|
||||
* Copyright (c) 2016 Luís Fonseca <miguelluisfonseca@gmail.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/* Websocket client implementation
|
||||
*
|
||||
* Copyright (c) 2016 Luís Fonseca <miguelluisfonseca@gmail.com>
|
||||
*
|
||||
* 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_
|
|
@ -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')
|
||||
```
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue