Commit 16051d51 authored by Luís Fonseca's avatar Luís Fonseca Committed by Arnim Läuger
Browse files

Implement initial version for websocket module (#1433)

parent 3c55e8c0
...@@ -42,7 +42,8 @@ SUBDIRS= \ ...@@ -42,7 +42,8 @@ SUBDIRS= \
dhtlib \ dhtlib \
tsl2561 \ tsl2561 \
net \ net \
http http \
websocket
endif # } PDIR endif # } PDIR
...@@ -87,6 +88,7 @@ COMPONENTS_eagle.app.v6 = \ ...@@ -87,6 +88,7 @@ COMPONENTS_eagle.app.v6 = \
dhtlib/libdhtlib.a \ dhtlib/libdhtlib.a \
tsl2561/tsl2561lib.a \ tsl2561/tsl2561lib.a \
http/libhttp.a \ http/libhttp.a \
websocket/libwebsocket.a \
net/libnodemcu_net.a \ net/libnodemcu_net.a \
modules/libmodules.a \ modules/libmodules.a \
......
...@@ -61,6 +61,7 @@ ...@@ -61,6 +61,7 @@
//#define LUA_USE_MODULES_U8G //#define LUA_USE_MODULES_U8G
#define LUA_USE_MODULES_UART #define LUA_USE_MODULES_UART
//#define LUA_USE_MODULES_UCG //#define LUA_USE_MODULES_UCG
//#define LUA_USE_MODULES_WEBSOCKET
#define LUA_USE_MODULES_WIFI #define LUA_USE_MODULES_WIFI
//#define LUA_USE_MODULES_WS2801 //#define LUA_USE_MODULES_WS2801
//#define LUA_USE_MODULES_WS2812 //#define LUA_USE_MODULES_WS2812
......
...@@ -53,6 +53,7 @@ INCLUDES += -I ../smart ...@@ -53,6 +53,7 @@ INCLUDES += -I ../smart
INCLUDES += -I ../cjson INCLUDES += -I ../cjson
INCLUDES += -I ../dhtlib INCLUDES += -I ../dhtlib
INCLUDES += -I ../http INCLUDES += -I ../http
INCLUDES += -I ../websocket
PDIR := ../$(PDIR) PDIR := ../$(PDIR)
sinclude $(PDIR)Makefile sinclude $(PDIR)Makefile
// 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);
#############################################################
# 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
This diff is collapsed.
/* 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_
# 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: ...@@ -78,6 +78,7 @@ pages:
- 'u8g': 'en/modules/u8g.md' - 'u8g': 'en/modules/u8g.md'
- 'uart': 'en/modules/uart.md' - 'uart': 'en/modules/uart.md'
- 'ucg': 'en/modules/ucg.md' - 'ucg': 'en/modules/ucg.md'
- 'websocket': 'en/modules/websocket.md'
- 'wifi': 'en/modules/wifi.md' - 'wifi': 'en/modules/wifi.md'
- 'ws2801': 'en/modules/ws2801.md' - 'ws2801': 'en/modules/ws2801.md'
- 'ws2812': 'en/modules/ws2812.md' - 'ws2812': 'en/modules/ws2812.md'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment