From ec604d9c702cfdd08da86eaf03eb6dee831d6183 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Thu, 10 Mar 2022 21:52:23 -0500 Subject: [PATCH 1/8] Mostly working version of the websocket support --- components/modules/Kconfig | 8 + components/modules/httpd.c | 478 +++++++++++++++++++++++++++++++------ docs/modules/httpd.md | 38 +++ 3 files changed, 450 insertions(+), 74 deletions(-) diff --git a/components/modules/Kconfig b/components/modules/Kconfig index b07b6d0e4..a44d58503 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -126,6 +126,14 @@ menu "NodeMCU modules" Includes the HTTPD module. This module uses the regular IDF http server component internally. + config NODEMCU_CMODULE_HTTPD_WS + bool "Include websocket support." if NODEMCU_CMODULE_HTTPD + default "n" + select HTTPD_WS_SUPPORT + help + Includes the websocket support. This module uses the regular IDF + http server component internally. + config NODEMCU_CMODULE_HTTPD_MAX_RESPONSE_HEADERS int "Max response header fields" if NODEMCU_CMODULE_HTTPD default 5 diff --git a/components/modules/httpd.c b/components/modules/httpd.c index 6fcbc299d..ee666faf3 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -73,6 +73,9 @@ typedef enum { READ_BODY_CHUNK, SEND_RESPONSE, SEND_PARTIAL_RESPONSE, + SEND_ERROR, + SEND_OK, + FREE_WS_OBJECT, } request_type_t; typedef struct { @@ -99,6 +102,11 @@ typedef struct { const char *query_str; int method; size_t body_len; + httpd_req_t *req; +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + httpd_ws_frame_t ws_pkt; + int reference; +#endif } request_data_t; @@ -107,6 +115,21 @@ typedef struct { uint32_t guard; } req_udata_t; +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +#define WS_METATABLE "httpd.ws" +#define HTTP_WEBSOCKET 1234 +#define HTTP_WEBSOCKET_GET 1235 + +typedef struct { + bool closed; + httpd_handle_t handle; + int fd; + int self_ref; + int text_fn_ref; + int binary_fn_ref; + int close_fn_ref; +} ws_connection_t; +#endif typedef enum { INDEX_NONE, INDEX_ROOT, INDEX_ALL } index_mode_t; @@ -240,10 +263,9 @@ static esp_err_t auto_index_handler(httpd_req_t *req) return ESP_OK; } - static esp_err_t dynamic_handler_httpd(httpd_req_t *req) { - size_t query_len = httpd_req_get_url_query_len(req); + size_t query_len = req->method != HTTP_WEBSOCKET ? httpd_req_get_url_query_len(req) : 0; char *query = query_len ? malloc(query_len + 1) : NULL; if (query_len) httpd_req_get_url_query_str(req, query, query_len + 1); @@ -254,7 +276,34 @@ static esp_err_t dynamic_handler_httpd(httpd_req_t *req) .query_str = query, .method = req->method, .body_len = req->content_len, + .req = req, }; + +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + memset(&req_data.ws_pkt, 0, sizeof(httpd_ws_frame_t)); + if (req->method == HTTP_WEBSOCKET) { + printf("Handling callback for websocket\n"); + req_data.ws_pkt.type = HTTPD_WS_TYPE_TEXT; + /* Set max_len = 0 to get the frame len */ + esp_err_t ret = httpd_ws_recv_frame(req, &req_data.ws_pkt, 0); + if (ret != ESP_OK) { + return ret; + } + + printf("About to allocate %d bytes for buffer\n", req_data.ws_pkt.len); + char *buf = malloc(req_data.ws_pkt.len); + if (!buf) { + return ESP_ERR_NO_MEM; + } + req_data.ws_pkt.payload = (void *) buf; + ret = httpd_ws_recv_frame(req, &req_data.ws_pkt, req_data.ws_pkt.len); + if (ret != ESP_OK) { + free(buf); + return ret; + } + } + #endif + // Pass the req info over to the LVM thread task_post_medium(dynamic_task, (task_param_t)&req_data); @@ -319,13 +368,50 @@ static esp_err_t dynamic_handler_httpd(httpd_req_t *req) } } +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + if (req_data.ws_pkt.payload) { + free(req_data.ws_pkt.payload); + req_data.ws_pkt.payload = 0; + } +#endif + // Request processed, release LVM thread xSemaphoreGive(done); - } while(tr.request_type != SEND_RESPONSE); // done + } while(tr.request_type != SEND_RESPONSE && tr.request_type != SEND_ERROR && tr.request_type != SEND_OK); // done + + if (tr.request_type == SEND_ERROR) { + return ESP_FAIL; + } return ESP_OK; } +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +static esp_err_t websocket_handler_httpd(httpd_req_t *req) +{ + if (req->method == HTTP_GET) { + req->method = HTTP_WEBSOCKET_GET; + } else { + req->method = HTTP_WEBSOCKET; + } + return dynamic_handler_httpd(req); +} + +static void free_sess_ctx(void *ctx) { + int ref = (int) ctx; + + request_data_t req_data = { + .method = FREE_WS_OBJECT, + .reference = ref, + }; + task_post_medium(dynamic_task, (task_param_t)&req_data); + thread_request_t tr; + xQueueReceive(queue, &tr, portMAX_DELAY); // Receive the reply + xSemaphoreGive(done); +} + +#endif + // ---- helper functions ---------------------------------------------- @@ -434,7 +520,6 @@ static int lhttpd_req_index(lua_State *L) #undef KEY_IS } - static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) { UNUSED(prio); @@ -452,84 +537,162 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) .response = &resp, }; - lua_rawgeti(L, LUA_REGISTRYINDEX, dynamic_handlers_table_ref); // +1 - lua_getfield(L, -1, req_info->key); // +1 - if (lua_isfunction(L, -1)) - { - // push req - req_udata_t *ud = - (req_udata_t *)lua_newuserdata(L, sizeof(req_udata_t)); // +1 - ud->req_info = req_info; - ud->guard = guard; - luaL_getmetatable(L, REQUEST_METATABLE); // +1 - lua_setmetatable(L, -2); // -1 +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + if (req_info->method == FREE_WS_OBJECT) { + printf("Freeing WS Object %d\n", req_info->reference); + lua_rawgeti(L, LUA_REGISTRYINDEX, req_info->reference); + ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); + + if (!ws->closed) { + printf("FIrst close\n"); + ws->closed = true; + if (ws->close_fn_ref > 0) { + printf("Calling close handler\n"); + lua_rawgeti(L, LUA_REGISTRYINDEX, ws->close_fn_ref); + luaL_pcallx(L, 0, 0); + } + } - int err = luaL_pcallx(L, 1, 1); // -1 +1 - if (!err && lua_istable(L, -1)) + luaL_unref(L, LUA_REGISTRYINDEX, ws->self_ref); + ws->self_ref = LUA_NOREF; + tr.request_type = SEND_OK; + } else if (req_info->method == HTTP_WEBSOCKET) { + printf("Handling websocket callbacks\n"); + // Just handle the callbacks here + if (req_info->req->sess_ctx) { + // Websocket event arrived + printf("Sess_ctx = %d\n", (int) req_info->req->sess_ctx); + lua_rawgeti(L, LUA_REGISTRYINDEX, (int) req_info->req->sess_ctx); + ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); + int fn = 0; + + if (req_info->ws_pkt.type == HTTPD_WS_TYPE_TEXT) { + fn = ws->text_fn_ref; + } else if (req_info->ws_pkt.type == HTTPD_WS_TYPE_BINARY) { + fn = ws->binary_fn_ref; + } + + if (fn) { + lua_rawgeti(L, LUA_REGISTRYINDEX, fn); + + lua_pushlstring(L, (const char *) req_info->ws_pkt.payload, (size_t) req_info->ws_pkt.len); + + luaL_pcallx(L, 1, 0); + } + } + tr.request_type = SEND_OK; + } else +#endif + { + lua_rawgeti(L, LUA_REGISTRYINDEX, dynamic_handlers_table_ref); // +1 + lua_getfield(L, -1, req_info->key); // +1 + if (lua_isfunction(L, -1)) { - // pull out response data - int t = lua_gettop(L); // response table index - lua_getfield(L, t, "status"); // +1 - resp.status_str = luaL_optstring(L, -1, "200 OK"); - lua_getfield(L, t, "type"); // +1 - resp.content_type = luaL_optstring(L, -1, NULL); - lua_getfield(L, t, "body"); // +1 - resp.body_data = luaL_optlstring(L, -1, NULL, &resp.body_len); - if (!resp.body_data) - resp.body_len = 0; - lua_getfield(L, t, "headers"); // +1 - if (lua_istable(L, -1)) - { - lua_pushnil(L); // +1 - for (unsigned i = 0; lua_next(L, -2); ++i) // +1 - { - if (i >= MAX_RESPONSE_HEADERS) - { - printf("Warning - too many response headers, ignoring some!\n"); - break; + // push req + req_udata_t *ud = + (req_udata_t *)lua_newuserdata(L, sizeof(req_udata_t)); // +1 + ud->req_info = req_info; + ud->guard = guard; + luaL_getmetatable(L, REQUEST_METATABLE); // +1 + lua_setmetatable(L, -2); // -1 + +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + if (strncmp(req_info->key, "[WS]", 4) == 0) { + if (req_info->method == HTTP_WEBSOCKET_GET) { + // web socket + ws_connection_t *ws = (ws_connection_t *) lua_newuserdata(L, sizeof(*ws)); + memset(ws, 0, sizeof(*ws)); + luaL_getmetatable(L, WS_METATABLE); + lua_setmetatable(L, -2); + lua_pushvalue(L, -1); + ws->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + ws->handle = req_info->req->handle; + ws->fd = httpd_req_to_sockfd(req_info->req); + + // Set the session context so we know what is going on. + req_info->req->sess_ctx = (void *) ws->self_ref; + req_info->req->free_ctx = free_sess_ctx; + + int err = luaL_pcallx(L, 2, 0); + if (err) { + tr.request_type = SEND_ERROR; + luaL_unref(L, LUA_REGISTRYINDEX, ws->self_ref); + ws->self_ref = LUA_NOREF; + } else { + tr.request_type = SEND_OK; } - resp.headers[i].key = lua_tostring(L, -2); - resp.headers[i].value = lua_tostring(L, -1); - lua_pop(L, 1); // drop value, keep key for lua_next() - } - } - lua_getfield(L, t, "getbody"); // +1 - if (lua_isfunction(L, -1)) + } + } else +#endif { - // Okay, we're doing a chunked body send, so we have to repeatedly - // call the provided getbody() function until it returns nil - bool headers_cleared = false; - tr.request_type = SEND_PARTIAL_RESPONSE; -next_chunk: - resp.body_data = NULL; - resp.body_len = 0; - err = luaL_pcallx(L, 0, 1); // -1 +1 - resp.body_data = - err ? NULL : luaL_optlstring(L, -1, NULL, &resp.body_len); - if (resp.body_data) + int err = luaL_pcallx(L, 1, 1); // -1 +1 + if (!err && lua_istable(L, -1)) { - // Toss this bit of response data over to the httpd thread - xQueueSend(queue, &tr, portMAX_DELAY); - // ...and wait until it's done sending it - xSemaphoreTake(done, portMAX_DELAY); - - lua_pop(L, 1); // -1 - - if (!headers_cleared) + // pull out response data + int t = lua_gettop(L); // response table index + lua_getfield(L, t, "status"); // +1 + resp.status_str = luaL_optstring(L, -1, "200 OK"); + lua_getfield(L, t, "type"); // +1 + resp.content_type = luaL_optstring(L, -1, NULL); + lua_getfield(L, t, "body"); // +1 + resp.body_data = luaL_optlstring(L, -1, NULL, &resp.body_len); + if (!resp.body_data) + resp.body_len = 0; + lua_getfield(L, t, "headers"); // +1 + if (lua_istable(L, -1)) { - // Clear the header data; it's only used for the first chunk - resp.status_str = NULL; - resp.content_type = NULL; - for (unsigned i = 0; i < MAX_RESPONSE_HEADERS; ++i) - resp.headers[i].key = resp.headers[i].value = NULL; - - headers_cleared = true; + lua_pushnil(L); // +1 + for (unsigned i = 0; lua_next(L, -2); ++i) // +1 + { + if (i >= MAX_RESPONSE_HEADERS) + { + printf("Warning - too many response headers, ignoring some!\n"); + break; + } + resp.headers[i].key = lua_tostring(L, -2); + resp.headers[i].value = lua_tostring(L, -1); + lua_pop(L, 1); // drop value, keep key for lua_next() + } } lua_getfield(L, t, "getbody"); // +1 - goto next_chunk; + if (lua_isfunction(L, -1)) + { + // Okay, we're doing a chunked body send, so we have to repeatedly + // call the provided getbody() function until it returns nil + bool headers_cleared = false; + tr.request_type = SEND_PARTIAL_RESPONSE; + next_chunk: + resp.body_data = NULL; + resp.body_len = 0; + err = luaL_pcallx(L, 0, 1); // -1 +1 + resp.body_data = + err ? NULL : luaL_optlstring(L, -1, NULL, &resp.body_len); + if (resp.body_data) + { + // Toss this bit of response data over to the httpd thread + xQueueSend(queue, &tr, portMAX_DELAY); + // ...and wait until it's done sending it + xSemaphoreTake(done, portMAX_DELAY); + + lua_pop(L, 1); // -1 + + if (!headers_cleared) + { + // Clear the header data; it's only used for the first chunk + resp.status_str = NULL; + resp.content_type = NULL; + for (unsigned i = 0; i < MAX_RESPONSE_HEADERS; ++i) + resp.headers[i].key = resp.headers[i].value = NULL; + + headers_cleared = true; + } + lua_getfield(L, t, "getbody"); // +1 + goto next_chunk; + } + // else, getbody() returned nil, so let the normal exit path + // toss the final SEND_PARTIAL_RESPONSE request over to the httpd + } } - // else, getbody() returned nil, so let the normal exit path - // toss the final SEND_PARTIAL_RESPONSE request over to the httpd } } } @@ -584,6 +747,46 @@ static int lhttpd_static(lua_State *L) return 1; } +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +// add websocket route: httpd.websocket(uri, handler) +static int lhttpd_websocket(lua_State *L) +{ + if (!server) + return luaL_error(L, "Server not started"); + + const char *match = luaL_checkstring(L, 1); + luaL_checkfunction(L, 2); + lua_settop(L, 2); + + if (!match[0]) + return luaL_error(L, "Null route not supported"); + + // Create a key for this entry + const char *key = lua_pushfstring(L, "[WS]%s", match); + + // Store this in our dynamic handlers table, so the ref lives on + // on, but so that we can also free it after server shutdown. + lua_rawgeti(L, LUA_REGISTRYINDEX, dynamic_handlers_table_ref); + lua_pushvalue(L, -2); // key + lua_pushvalue(L, 2); // handler + lua_settable(L, -3); + + httpd_uri_t websocket_handler = { + .uri = match, + .method = HTTP_GET, + .handler = websocket_handler_httpd, + .user_ctx = (void *)key, + .is_websocket = true, + .handle_ws_control_frames = false, + }; + if (httpd_register_uri_handler(server, &websocket_handler) == 1) + lua_pushinteger(L, 1); + else + lua_pushnil(L); + + return 1; +} +#endif // add dynamic route: httpd.dynamic(method, uri, handler) static int lhttpd_dynamic(lua_State *L) @@ -715,17 +918,140 @@ static int lhttpd_stop(lua_State *L) return 0; } +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +// Websocket functions + +typedef struct { + httpd_handle_t hd; + int fd; + int type; + size_t len; + char *data; +} async_send_t; + +/* + * async send function, which we put into the httpd work queue + */ +static void ws_async_send(void *arg) +{ + async_send_t *async_send = arg; + httpd_handle_t hd = async_send->hd; + int fd = async_send->fd; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = (uint8_t*)async_send->data; + ws_pkt.len = async_send->len; + ws_pkt.type = async_send->type; + + httpd_ws_send_frame_async(hd, fd, &ws_pkt); + free(async_send); +} + +static esp_err_t trigger_async_send(ws_connection_t *ws, int type, const char *data, size_t len) { + async_send_t *async_send = malloc(sizeof(async_send_t) + len); + async_send->hd = ws->handle; + async_send->fd = ws->fd; + async_send->type = type; + async_send->len = len; + async_send->data = (char *) (async_send + 1); + memcpy(async_send->data, data, len); + return httpd_queue_work(ws->handle, ws_async_send, async_send); +} + +static void ws_async_close(void *arg) { + async_send_t *async_close = arg; + + httpd_sess_trigger_close(async_close->hd, async_close->fd); + free(async_close); +} + +static int ws_close(lua_State *L) { + ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); + + if (!ws->closed) { + ws->closed = true; + async_send_t *async_close = malloc(sizeof(async_send_t)); + async_close->hd = ws->handle; + async_close->fd = ws->fd; + httpd_queue_work(ws->handle, ws_async_close, async_close); + } + return 0; +} + +// event types: text, binary, close +static int ws_on(lua_State *L) { + ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); + const char *event = lua_tostring(L, 2); + + int *slot = NULL; + if (strcmp(event, "text") == 0) { + slot = &ws->text_fn_ref; + } else if (strcmp(event, "binary") == 0) { + slot = &ws->binary_fn_ref; + } else if (strcmp(event, "close") == 0) { + slot = &ws->close_fn_ref; + } else { + return luaL_error(L, "Incorrect event argument"); + } + + if (*slot) { + luaL_unref(L, LUA_REGISTRYINDEX, *slot); + *slot = 0; + } + + if (!lua_isnil(L, 3)) { + luaL_checkfunction(L, 3); + lua_pushvalue(L, 3); + *slot = luaL_ref(L, LUA_REGISTRYINDEX); + } + + return 0; +} + +static int ws_textbinary(lua_State *L, int type) { + ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); + + size_t len; + const char *data = lua_tolstring(L, 2, &len); + + esp_err_t rc = trigger_async_send(ws, type, data, len); + if (rc) { + return luaL_error(L, "Failed to send to websocket"); + } + + return 0; +} + +static int ws_text(lua_State *L) { + return ws_textbinary(L, HTTPD_WS_TYPE_TEXT); +} + +static int ws_binary(lua_State *L) { + return ws_textbinary(L, HTTPD_WS_TYPE_BINARY); +} + +LROT_BEGIN(httpd_ws_mt, NULL, LROT_MASK_GC_INDEX) + LROT_TABENTRY( __index, httpd_ws_mt ) + LROT_FUNCENTRY( __gc, ws_close ) + LROT_FUNCENTRY( close, ws_close ) + LROT_FUNCENTRY( on, ws_on ) + LROT_FUNCENTRY( text, ws_text ) + LROT_FUNCENTRY( binary, ws_binary ) +LROT_END(httpd_ws_mt, NULL, LROT_MASK_GC_INDEX) +#endif LROT_BEGIN(httpd_req_mt, NULL, LROT_MASK_INDEX) LROT_FUNCENTRY( __index, lhttpd_req_index ) LROT_END(httpd_req_mt, NULL, LROT_MASK_INDEX) - LROT_BEGIN(httpd, NULL, 0) LROT_FUNCENTRY( start, lhttpd_start ) LROT_FUNCENTRY( stop, lhttpd_stop ) LROT_FUNCENTRY( static, lhttpd_static ) LROT_FUNCENTRY( dynamic, lhttpd_dynamic ) +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + LROT_FUNCENTRY( websocket, lhttpd_websocket ) +#endif LROT_FUNCENTRY( unregister, lhttpd_unregister ) LROT_NUMENTRY( GET, HTTP_GET ) @@ -748,6 +1074,10 @@ static int lhttpd_init(lua_State *L) luaL_rometatable(L, REQUEST_METATABLE, LROT_TABLEREF(httpd_req_mt)); +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS + luaL_rometatable(L, WS_METATABLE, LROT_TABLEREF(httpd_ws_mt)); +#endif + return 0; } diff --git a/docs/modules/httpd.md b/docs/modules/httpd.md index f651f5c37..c41f754d2 100644 --- a/docs/modules/httpd.md +++ b/docs/modules/httpd.md @@ -256,6 +256,44 @@ end httpd.dynamic(httpd.PUT, "/foo", put_foo) ``` +## httpd.websocket() + +Registers a websocket route handler. + +#### Syntax +```lua +httpd.websocket(route, handler) +``` + +#### Parameters +- `route` The route prefix. Be mindful of any trailing "/" as that may interact +with the `auto_index` functionality. +- `handler` The route handler function - `handler(req, ws)`. The `req` object is +the same as for a regular dynamic route. The provided websocket +object `ws` has the following fields/functions: + - `on` This allows registration of handlers when data is received. This is invoked with + two arguments -- the name of the event and the handler for that event. The allowable names are: + - `text` The handler is called with a single string argument whenever a text message is received. + - `binary` The handler is called with a single string argument whenever a binary message is received. + - `close` The handler is called when the client wants to close the connection. + - `text` This can be called with a string argument and it sends a text message. + - `binary` This can be called with a string argument and it sends a binary message. + - `close` The connection to the client is closed. + +#### Returns +nil + +#### Example +```lua +httpd.start({ webroot = "web" }) + +function echo_ws(req, ws) + ws:on('text', function(data) ws:text(data) end) +end + +httpd.websocket("/echo", echo_ws) +``` + ## httpd.unregister() Unregisters a previously registered handler. The default handlers may be From e0748a4b9e74f50cfe93cb750f40957fa0cbe80a Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sat, 30 Apr 2022 19:48:31 -0400 Subject: [PATCH 2/8] Improved the documentation --- docs/modules/httpd.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/modules/httpd.md b/docs/modules/httpd.md index c41f754d2..b64433d02 100644 --- a/docs/modules/httpd.md +++ b/docs/modules/httpd.md @@ -258,7 +258,7 @@ httpd.dynamic(httpd.PUT, "/foo", put_foo) ## httpd.websocket() -Registers a websocket route handler. +Registers a websocket route handler. This is optional, and must be selected explicitly in the configuration. #### Syntax ```lua @@ -285,13 +285,35 @@ nil #### Example ```lua -httpd.start({ webroot = "web" }) +httpd.start({ + webroot = "", + max_handlers = 20, + auto_index = httpd.INDEX_NONE + httpd.INDEX_ROOT + httpd.INDEX_ALL, +}) function echo_ws(req, ws) - ws:on('text', function(data) ws:text(data) end) + ws:on('text', function(data) print(data) ws:text(data) end) end httpd.websocket("/echo", echo_ws) + +function tick(ws, n) + ws:text("tick: " .. n) + n = n + 1 + if n < 6 then + tmr.create():alarm(1000, tmr.ALARM_SINGLE, function () + tick(ws ,n) + end) + else + ws:close() + end +end + +function heartbeat(req, ws) + tick(ws, 0) +end + +httpd.websocket("/beat", heartbeat) ``` ## httpd.unregister() From 75fdcd9747a9b1966051d0ae939b69fc7cd0a801 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Sat, 30 Apr 2022 19:51:42 -0400 Subject: [PATCH 3/8] Make it compile without websocket support --- components/modules/httpd.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/modules/httpd.c b/components/modules/httpd.c index ee666faf3..d012a427a 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -265,7 +265,12 @@ static esp_err_t auto_index_handler(httpd_req_t *req) static esp_err_t dynamic_handler_httpd(httpd_req_t *req) { +#ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS size_t query_len = req->method != HTTP_WEBSOCKET ? httpd_req_get_url_query_len(req) : 0; +#else + size_t query_len = httpd_req_get_url_query_len(req); +#endif + char *query = query_len ? malloc(query_len + 1) : NULL; if (query_len) httpd_req_get_url_query_str(req, query, query_len + 1); From ff116b760a54aad5437ff7f9aacf0623811fe1ad Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 2 May 2022 19:29:27 -0400 Subject: [PATCH 4/8] Minor cleanup --- components/modules/httpd.c | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/components/modules/httpd.c b/components/modules/httpd.c index d012a427a..a29437c13 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -415,6 +415,20 @@ static void free_sess_ctx(void *ctx) { xSemaphoreGive(done); } +static void ws_clear(lua_State *L, ws_connection_t *ws) +{ + luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_ref); + if (ws->text_fn_ref > 0) { + luaL_unref2(L, LUA_REGISTRYINDEX, ws->text_fn_ref); + } + if (ws->binary_fn_ref > 0) { + luaL_unref2(L, LUA_REGISTRYINDEX, ws->binary_fn_ref); + } + if (ws->close_fn_ref > 0) { + luaL_unref2(L, LUA_REGISTRYINDEX, ws->close_fn_ref); + } +} + #endif @@ -558,8 +572,8 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) } } - luaL_unref(L, LUA_REGISTRYINDEX, ws->self_ref); - ws->self_ref = LUA_NOREF; + ws_clear(L, ws); + tr.request_type = SEND_OK; } else if (req_info->method == HTTP_WEBSOCKET) { printf("Handling websocket callbacks\n"); @@ -621,8 +635,7 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) int err = luaL_pcallx(L, 2, 0); if (err) { tr.request_type = SEND_ERROR; - luaL_unref(L, LUA_REGISTRYINDEX, ws->self_ref); - ws->self_ref = LUA_NOREF; + ws_clear(L, ws); } else { tr.request_type = SEND_OK; } @@ -966,6 +979,8 @@ static esp_err_t trigger_async_send(ws_connection_t *ws, int type, const char *d static void ws_async_close(void *arg) { async_send_t *async_close = arg; + printf("About to trigger close on %d\n", async_close->fd); + httpd_sess_trigger_close(async_close->hd, async_close->fd); free(async_close); } @@ -979,6 +994,8 @@ static int ws_close(lua_State *L) { async_close->hd = ws->handle; async_close->fd = ws->fd; httpd_queue_work(ws->handle, ws_async_close, async_close); + } else { + printf("ws_close called when already closed\n"); } return 0; } From 6899d8ed0cf3ec2244b6d67a3db7cc7c8f59474c Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 2 May 2022 20:24:02 -0400 Subject: [PATCH 5/8] Better handling of lifetime of WS object --- components/modules/httpd.c | 132 ++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 37 deletions(-) diff --git a/components/modules/httpd.c b/components/modules/httpd.c index a29437c13..350cb8efe 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -124,7 +124,8 @@ typedef struct { bool closed; httpd_handle_t handle; int fd; - int self_ref; + int self_ref; // only present if at least one callback registered + int self_weak_ref; int text_fn_ref; int binary_fn_ref; int close_fn_ref; @@ -392,6 +393,48 @@ static esp_err_t dynamic_handler_httpd(httpd_req_t *req) } #ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +// returns a reference to a table with the [1] value being a weak +// ref to the top of the stack +static int register_weak_ref(lua_State *L) +{ + lua_newtable(L); + + lua_newtable(L); // metatable={} + + lua_pushliteral(L, "__mode"); + lua_pushliteral(L, "v"); + lua_rawset(L, -3); // metatable._mode='v' + + lua_setmetatable(L, -2); // setmetatable(new_table,metatable) + + lua_pushvalue(L, -2); // push the previous top of stack + lua_rawseti(L, -2, 1); // new_table[1]=original value on top of the stack + + lua_remove(L, -2); // Remove the original ivalue + + return luaL_ref(L, LUA_REGISTRYINDEX); // this pops the new_table +} + +// Returns the value of the weak ref on the stack +// but returns false with nothing on the stack has been GC'ed +static bool deref_weak_ref(lua_State *L, int ref) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return false; + } + lua_rawgeti(L, -1, 1); + + // Either we have nil, or we have the underlying object + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return false; + } + + return true; +} + static esp_err_t websocket_handler_httpd(httpd_req_t *req) { if (req->method == HTTP_GET) { @@ -415,9 +458,9 @@ static void free_sess_ctx(void *ctx) { xSemaphoreGive(done); } -static void ws_clear(lua_State *L, ws_connection_t *ws) +static void ws_clear(lua_State *L, ws_connection_t *ws) { - luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_ref); + luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_weak_ref); if (ws->text_fn_ref > 0) { luaL_unref2(L, LUA_REGISTRYINDEX, ws->text_fn_ref); } @@ -559,20 +602,21 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) #ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS if (req_info->method == FREE_WS_OBJECT) { printf("Freeing WS Object %d\n", req_info->reference); - lua_rawgeti(L, LUA_REGISTRYINDEX, req_info->reference); - ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); - - if (!ws->closed) { - printf("FIrst close\n"); - ws->closed = true; - if (ws->close_fn_ref > 0) { - printf("Calling close handler\n"); - lua_rawgeti(L, LUA_REGISTRYINDEX, ws->close_fn_ref); - luaL_pcallx(L, 0, 0); + if (deref_weak_ref(L, req_info->reference)) { + ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); + + if (!ws->closed) { + printf("First close\n"); + ws->closed = true; + if (ws->close_fn_ref > 0) { + printf("Calling close handler\n"); + lua_rawgeti(L, LUA_REGISTRYINDEX, ws->close_fn_ref); + luaL_pcallx(L, 0, 0); + } } - } - ws_clear(L, ws); + ws_clear(L, ws); + } tr.request_type = SEND_OK; } else if (req_info->method == HTTP_WEBSOCKET) { @@ -581,26 +625,27 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) if (req_info->req->sess_ctx) { // Websocket event arrived printf("Sess_ctx = %d\n", (int) req_info->req->sess_ctx); - lua_rawgeti(L, LUA_REGISTRYINDEX, (int) req_info->req->sess_ctx); - ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); - int fn = 0; - - if (req_info->ws_pkt.type == HTTPD_WS_TYPE_TEXT) { - fn = ws->text_fn_ref; - } else if (req_info->ws_pkt.type == HTTPD_WS_TYPE_BINARY) { - fn = ws->binary_fn_ref; - } - - if (fn) { - lua_rawgeti(L, LUA_REGISTRYINDEX, fn); + if (deref_weak_ref(L, (int) req_info->req->sess_ctx)) { + ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); + int fn = 0; + + if (req_info->ws_pkt.type == HTTPD_WS_TYPE_TEXT) { + fn = ws->text_fn_ref; + } else if (req_info->ws_pkt.type == HTTPD_WS_TYPE_BINARY) { + fn = ws->binary_fn_ref; + } + + if (fn) { + lua_rawgeti(L, LUA_REGISTRYINDEX, fn); - lua_pushlstring(L, (const char *) req_info->ws_pkt.payload, (size_t) req_info->ws_pkt.len); + lua_pushlstring(L, (const char *) req_info->ws_pkt.payload, (size_t) req_info->ws_pkt.len); - luaL_pcallx(L, 1, 0); + luaL_pcallx(L, 1, 0); + } } - } + } tr.request_type = SEND_OK; - } else + } else #endif { lua_rawgeti(L, LUA_REGISTRYINDEX, dynamic_handlers_table_ref); // +1 @@ -624,12 +669,13 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) luaL_getmetatable(L, WS_METATABLE); lua_setmetatable(L, -2); lua_pushvalue(L, -1); - ws->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + ws->self_weak_ref = register_weak_ref(L); ws->handle = req_info->req->handle; ws->fd = httpd_req_to_sockfd(req_info->req); + ws->self_ref = LUA_NOREF; // Set the session context so we know what is going on. - req_info->req->sess_ctx = (void *) ws->self_ref; + req_info->req->sess_ctx = (void *) ws->self_weak_ref; req_info->req->free_ctx = free_sess_ctx; int err = luaL_pcallx(L, 2, 0); @@ -639,8 +685,8 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) } else { tr.request_type = SEND_OK; } - } - } else + } + } else #endif { int err = luaL_pcallx(L, 1, 1); // -1 +1 @@ -981,7 +1027,9 @@ static void ws_async_close(void *arg) { printf("About to trigger close on %d\n", async_close->fd); - httpd_sess_trigger_close(async_close->hd, async_close->fd); + if (httpd_sess_trigger_close(async_close->hd, async_close->fd) != ESP_OK) { + printf("Failed to trigger close\n"); + } free(async_close); } @@ -1000,7 +1048,7 @@ static int ws_close(lua_State *L) { return 0; } -// event types: text, binary, close +// event types: text, binary, close static int ws_on(lua_State *L) { ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); const char *event = lua_tostring(L, 2); @@ -1027,6 +1075,16 @@ static int ws_on(lua_State *L) { *slot = luaL_ref(L, LUA_REGISTRYINDEX); } + if (ws->text_fn_ref || ws->binary_fn_ref || ws->close_fn_ref) { + // We need a self_ref + if (ws->self_ref <= 0) { + lua_pushvalue(L, 1); + ws->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } + } else { + luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_ref); + } + return 0; } From 22809538219c4a316577c10eb9820ec6471df433 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Tue, 3 May 2022 20:01:21 -0400 Subject: [PATCH 6/8] Remove the printf debugging --- components/modules/httpd.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/components/modules/httpd.c b/components/modules/httpd.c index 350cb8efe..33b85f5fa 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -116,6 +116,7 @@ typedef struct { } req_udata_t; #ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS +#define WS_DEBUG(...) #define WS_METATABLE "httpd.ws" #define HTTP_WEBSOCKET 1234 #define HTTP_WEBSOCKET_GET 1235 @@ -288,7 +289,7 @@ static esp_err_t dynamic_handler_httpd(httpd_req_t *req) #ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS memset(&req_data.ws_pkt, 0, sizeof(httpd_ws_frame_t)); if (req->method == HTTP_WEBSOCKET) { - printf("Handling callback for websocket\n"); + WS_DEBUG("Handling callback for websocket\n"); req_data.ws_pkt.type = HTTPD_WS_TYPE_TEXT; /* Set max_len = 0 to get the frame len */ esp_err_t ret = httpd_ws_recv_frame(req, &req_data.ws_pkt, 0); @@ -296,7 +297,7 @@ static esp_err_t dynamic_handler_httpd(httpd_req_t *req) return ret; } - printf("About to allocate %d bytes for buffer\n", req_data.ws_pkt.len); + WS_DEBUG("About to allocate %d bytes for buffer\n", req_data.ws_pkt.len); char *buf = malloc(req_data.ws_pkt.len); if (!buf) { return ESP_ERR_NO_MEM; @@ -601,15 +602,15 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) #ifdef CONFIG_NODEMCU_CMODULE_HTTPD_WS if (req_info->method == FREE_WS_OBJECT) { - printf("Freeing WS Object %d\n", req_info->reference); + WS_DEBUG("Freeing WS Object %d\n", req_info->reference); if (deref_weak_ref(L, req_info->reference)) { ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); if (!ws->closed) { - printf("First close\n"); + WS_DEBUG("First close\n"); ws->closed = true; if (ws->close_fn_ref > 0) { - printf("Calling close handler\n"); + WS_DEBUG("Calling close handler\n"); lua_rawgeti(L, LUA_REGISTRYINDEX, ws->close_fn_ref); luaL_pcallx(L, 0, 0); } @@ -620,11 +621,11 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) tr.request_type = SEND_OK; } else if (req_info->method == HTTP_WEBSOCKET) { - printf("Handling websocket callbacks\n"); + WS_DEBUG("Handling websocket callbacks\n"); // Just handle the callbacks here if (req_info->req->sess_ctx) { // Websocket event arrived - printf("Sess_ctx = %d\n", (int) req_info->req->sess_ctx); + WS_DEBUG("Sess_ctx = %d\n", (int) req_info->req->sess_ctx); if (deref_weak_ref(L, (int) req_info->req->sess_ctx)) { ws_connection_t *ws = (ws_connection_t *) luaL_checkudata(L, -1, WS_METATABLE); int fn = 0; @@ -1025,10 +1026,10 @@ static esp_err_t trigger_async_send(ws_connection_t *ws, int type, const char *d static void ws_async_close(void *arg) { async_send_t *async_close = arg; - printf("About to trigger close on %d\n", async_close->fd); + WS_DEBUG("About to trigger close on %d\n", async_close->fd); if (httpd_sess_trigger_close(async_close->hd, async_close->fd) != ESP_OK) { - printf("Failed to trigger close\n"); + WS_DEBUG("Failed to trigger close\n"); } free(async_close); } @@ -1043,7 +1044,7 @@ static int ws_close(lua_State *L) { async_close->fd = ws->fd; httpd_queue_work(ws->handle, ws_async_close, async_close); } else { - printf("ws_close called when already closed\n"); + WS_DEBUG("ws_close called when already closed\n"); } return 0; } From e88b23ebd897868bed8c65dfa3d089802aca1488 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Thu, 1 Feb 2024 20:30:17 -0500 Subject: [PATCH 7/8] Clean up and add more debug (normally compiled out) --- components/modules/httpd.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/components/modules/httpd.c b/components/modules/httpd.c index 33b85f5fa..8fe96c881 100644 --- a/components/modules/httpd.c +++ b/components/modules/httpd.c @@ -413,7 +413,9 @@ static int register_weak_ref(lua_State *L) lua_remove(L, -2); // Remove the original ivalue - return luaL_ref(L, LUA_REGISTRYINDEX); // this pops the new_table + int result = luaL_ref(L, LUA_REGISTRYINDEX); // this pops the new_table + WS_DEBUG("register_weak_ref returning %d\n", result); + return result; } // Returns the value of the weak ref on the stack @@ -423,16 +425,20 @@ static bool deref_weak_ref(lua_State *L, int ref) lua_rawgeti(L, LUA_REGISTRYINDEX, ref); if (lua_isnil(L, -1)) { lua_pop(L, 1); + WS_DEBUG("deref_weak_ref(%d) has nothing in registry\n", ref); return false; } lua_rawgeti(L, -1, 1); // Either we have nil, or we have the underlying object if (lua_isnil(L, -1)) { - lua_pop(L, 1); + lua_pop(L, 2); + WS_DEBUG("deref_weak_ref(%d) has nothing in weak table\n", ref); return false; } + lua_remove(L, -2); // remove the table + return true; } @@ -462,6 +468,7 @@ static void free_sess_ctx(void *ctx) { static void ws_clear(lua_State *L, ws_connection_t *ws) { luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_weak_ref); + luaL_unref2(L, LUA_REGISTRYINDEX, ws->self_ref); if (ws->text_fn_ref > 0) { luaL_unref2(L, LUA_REGISTRYINDEX, ws->text_fn_ref); } @@ -666,6 +673,7 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) if (req_info->method == HTTP_WEBSOCKET_GET) { // web socket ws_connection_t *ws = (ws_connection_t *) lua_newuserdata(L, sizeof(*ws)); + WS_DEBUG("Created new WS object\n"); memset(ws, 0, sizeof(*ws)); luaL_getmetatable(L, WS_METATABLE); lua_setmetatable(L, -2); @@ -675,16 +683,15 @@ static void dynamic_handler_lvm(task_param_t param, task_prio_t prio) ws->fd = httpd_req_to_sockfd(req_info->req); ws->self_ref = LUA_NOREF; - // Set the session context so we know what is going on. - req_info->req->sess_ctx = (void *) ws->self_weak_ref; - req_info->req->free_ctx = free_sess_ctx; - int err = luaL_pcallx(L, 2, 0); if (err) { tr.request_type = SEND_ERROR; ws_clear(L, ws); } else { tr.request_type = SEND_OK; + // Set the session context so we know what is going on. + req_info->req->sess_ctx = (void *)ws->self_weak_ref; + req_info->req->free_ctx = free_sess_ctx; } } } else @@ -1035,13 +1042,14 @@ static void ws_async_close(void *arg) { } static int ws_close(lua_State *L) { - ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); + ws_connection_t *ws = (ws_connection_t *)luaL_checkudata(L, 1, WS_METATABLE); if (!ws->closed) { ws->closed = true; async_send_t *async_close = malloc(sizeof(async_send_t)); async_close->hd = ws->handle; async_close->fd = ws->fd; + WS_DEBUG("ws_close called and now marked closed\n"); httpd_queue_work(ws->handle, ws_async_close, async_close); } else { WS_DEBUG("ws_close called when already closed\n"); @@ -1049,6 +1057,11 @@ static int ws_close(lua_State *L) { return 0; } +static int ws_gcclose(lua_State *L) { + WS_DEBUG("gc cleaning up block\n"); + return ws_close(L); +} + // event types: text, binary, close static int ws_on(lua_State *L) { ws_connection_t *ws = (ws_connection_t*)luaL_checkudata(L, 1, WS_METATABLE); @@ -1113,7 +1126,7 @@ static int ws_binary(lua_State *L) { LROT_BEGIN(httpd_ws_mt, NULL, LROT_MASK_GC_INDEX) LROT_TABENTRY( __index, httpd_ws_mt ) - LROT_FUNCENTRY( __gc, ws_close ) + LROT_FUNCENTRY( __gc, ws_gcclose ) LROT_FUNCENTRY( close, ws_close ) LROT_FUNCENTRY( on, ws_on ) LROT_FUNCENTRY( text, ws_text ) From affed3f802c2c9a61cde3e48f646add6ac4863a6 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Fri, 2 Feb 2024 17:50:32 -0500 Subject: [PATCH 8/8] FIx the example to use a correct value --- docs/modules/httpd.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/httpd.md b/docs/modules/httpd.md index b64433d02..1ac118d33 100644 --- a/docs/modules/httpd.md +++ b/docs/modules/httpd.md @@ -76,7 +76,7 @@ configured. httpd.start({ webroot = "", max_handlers = 20, - auto_index = httpd.INDEX_NONE || httpd.INDEX_ROOT || httpd.INDEX_ALL, + auto_index = httpd.INDEX_ALL, }) ``` @@ -288,7 +288,7 @@ nil httpd.start({ webroot = "", max_handlers = 20, - auto_index = httpd.INDEX_NONE + httpd.INDEX_ROOT + httpd.INDEX_ALL, + auto_index = httpd.INDEX_ALL, }) function echo_ws(req, ws)