815 lines
25 KiB
C
815 lines
25 KiB
C
#include "module.h"
|
|
#include "common.h"
|
|
#include "lauxlib.h"
|
|
#include "lmem.h"
|
|
#include <string.h>
|
|
|
|
#include "esp_http_client.h"
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_task.h"
|
|
#include "task/task.h"
|
|
|
|
#include <esp_log.h>
|
|
#define TAG "http"
|
|
|
|
typedef struct list_item list_item;
|
|
|
|
enum {
|
|
ConnectionCallback = 0,
|
|
HeadersCallback,
|
|
DataCallback,
|
|
CompleteCallback,
|
|
|
|
// after this are other refs which aren't callbacks
|
|
|
|
ContextRef,
|
|
PostDataRef,
|
|
CertRef,
|
|
CountRefs // Must be last
|
|
};
|
|
|
|
static char const * const CALLBACK_NAME[] = {
|
|
"connect",
|
|
"headers",
|
|
"data",
|
|
"complete",
|
|
NULL
|
|
};
|
|
|
|
typedef enum {
|
|
Async = 0,
|
|
Connected = 1,
|
|
InCallback = 2,
|
|
AckPending = 3,
|
|
ShouldCloseInRtosTask = 4,
|
|
} Flag;
|
|
|
|
typedef struct
|
|
{
|
|
esp_http_client_handle_t client;
|
|
int refs[CountRefs];
|
|
list_item *headers;
|
|
int16_t status_code;
|
|
uint16_t flags;
|
|
TaskHandle_t perform_rtos_task; // NULL if we're not in the middle of an async request
|
|
} lhttp_context_t;
|
|
|
|
struct list_item
|
|
{
|
|
struct list_item *next;
|
|
uint32_t len;
|
|
char data[1];
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
int id;
|
|
lhttp_context_t *context;
|
|
union {
|
|
list_item *headers;
|
|
struct {
|
|
uint32_t data_len;
|
|
char data[1];
|
|
};
|
|
};
|
|
} lhttp_event;
|
|
|
|
static const char http_context_mt[] = "http.context";
|
|
static const char http_default_user_agent[] = "NodeMCU (ESP32)";
|
|
#define DELAY_ACK (-99) // Chosen not to conflict with any other esp_err_t
|
|
#define HTTP_REQUEST_COMPLETE (-1)
|
|
|
|
static task_handle_t lhttp_request_task_id, lhttp_event_task_id;
|
|
#define CHECK_CONNECTION_IDLE(ctx) \
|
|
if (ctx->perform_rtos_task || context_flag(ctx, InCallback)) { \
|
|
return luaL_error(L, "Cannot modify connection while a request is active"); \
|
|
}
|
|
|
|
static void context_setflag(lhttp_context_t *context, Flag flag)
|
|
{
|
|
context->flags |= 1 << flag;
|
|
}
|
|
|
|
static void context_clearflag(lhttp_context_t *context, Flag flag)
|
|
{
|
|
context->flags &= ~(1 << flag);
|
|
}
|
|
|
|
static bool context_flag(lhttp_context_t const *context, Flag flag)
|
|
{
|
|
return context->flags & (1 << flag);
|
|
}
|
|
|
|
static void context_setflagbool(lhttp_context_t *context, Flag flag, bool val)
|
|
{
|
|
if (val) {
|
|
context_setflag(context, flag);
|
|
} else {
|
|
context_clearflag(context, flag);
|
|
}
|
|
}
|
|
|
|
static void context_setref(lua_State *L, lhttp_context_t *context, int index)
|
|
{
|
|
assert(index >= 0 && index < CountRefs);
|
|
luaL_unref(L, LUA_REGISTRYINDEX, context->refs[index]);
|
|
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
if (ref == LUA_REFNIL) {
|
|
ref = LUA_NOREF;
|
|
}
|
|
context->refs[index] = ref;
|
|
}
|
|
|
|
static void context_unsetref(lua_State *L, lhttp_context_t *context, int index)
|
|
{
|
|
assert(index >= 0 && index < CountRefs);
|
|
int ref = context->refs[index];
|
|
context->refs[index] = LUA_NOREF;
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ref);
|
|
}
|
|
|
|
static int context_close(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
if (!context->client) {
|
|
// Nothing to do
|
|
} else if (context->perform_rtos_task) {
|
|
// Can only be closed from within a callback or while there's a delayed ack pending
|
|
if (context_flag(context, InCallback) || context_flag(context, AckPending)) {
|
|
context_setflag(context, ShouldCloseInRtosTask);
|
|
} else {
|
|
return luaL_error(L, "Cannot close an ongoing async request outside of a callback or pending ack");
|
|
}
|
|
} else {
|
|
esp_http_client_close(context->client);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int context_gc(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
assert(context->refs[ContextRef] == LUA_NOREF); // No way to get GC'd if we still hold a ref to ourselves
|
|
for (int i = 0; i < CountRefs; i++) {
|
|
context_unsetref(L, context, i);
|
|
}
|
|
list_item *hdr = context->headers;
|
|
while (hdr) {
|
|
list_item *next = hdr->next;
|
|
free(hdr);
|
|
hdr = next;
|
|
}
|
|
context->headers = NULL;
|
|
if (context->client) {
|
|
esp_http_client_cleanup(context->client);
|
|
context->client = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static lhttp_context_t *context_new(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)lua_newuserdata(L, sizeof(lhttp_context_t));
|
|
context->client = NULL;
|
|
context->status_code = 0;
|
|
context->headers = NULL;
|
|
for (int i = 0; i < CountRefs; i++) {
|
|
context->refs[i] = LUA_NOREF;
|
|
}
|
|
context->flags = 0;
|
|
context->perform_rtos_task = NULL;
|
|
luaL_getmetatable(L, http_context_mt);
|
|
lua_setmetatable(L, -2);
|
|
return context;
|
|
}
|
|
|
|
static void context_seterr(lhttp_context_t *context, esp_err_t err)
|
|
{
|
|
if (err > 0 && err <= INT16_MAX) {
|
|
context->status_code = -err;
|
|
} else if (err) {
|
|
context->status_code = -1;
|
|
}
|
|
}
|
|
|
|
static void perform_rtos_task(void *pvParameters)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)pvParameters;
|
|
|
|
ulTaskNotifyTake(pdTRUE, 0); // ensure that notification counter is reset
|
|
esp_err_t err = esp_http_client_perform(context->client);
|
|
if (err) {
|
|
ESP_LOGW(TAG, "esp_http_client_perform returned error %d", err);
|
|
context_seterr(context, err);
|
|
}
|
|
ESP_LOGD(TAG, "perform_rtos_task completed, remaining stack = %d bytes", uxTaskGetStackHighWaterMark(NULL));
|
|
task_post_low(lhttp_request_task_id, (task_param_t)context);
|
|
vTaskSuspend(NULL);
|
|
}
|
|
|
|
static int make_callback(lhttp_context_t *context, int id, void *data, size_t data_len);
|
|
|
|
static void lhttp_request_task(task_param_t param, task_prio_t prio)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)param;
|
|
|
|
// the rtos task for esp_http_client_perform suspended, reap it
|
|
vTaskDelete(context->perform_rtos_task);
|
|
context->perform_rtos_task = NULL;
|
|
|
|
make_callback(context, HTTP_REQUEST_COMPLETE, NULL, 0);
|
|
lua_State *L = lua_getstate();
|
|
context_unsetref(L, context, ContextRef);
|
|
}
|
|
|
|
// note: this function is called both in synchronous mode and in asynchronous mode
|
|
static esp_err_t http_event_cb(esp_http_client_event_t *evt)
|
|
{
|
|
// ESP_LOGI(TAG, "http_event_cb %d", evt->event_id);
|
|
lhttp_context_t *context = (lhttp_context_t *)evt->user_data;
|
|
switch (evt->event_id) {
|
|
case HTTP_EVENT_ON_CONNECTED: {
|
|
context_setflag(context, Connected);
|
|
return make_callback(context, HTTP_EVENT_ON_CONNECTED, NULL, 0);
|
|
}
|
|
case HTTP_EVENT_ON_HEADER: {
|
|
size_t keylen = strlen(evt->header_key);
|
|
size_t vallen = strlen(evt->header_value);
|
|
list_item *hdr = (list_item *)malloc(sizeof(list_item) + keylen + vallen + 1); // +1 for final null
|
|
if (!hdr) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
hdr->next = context->headers;
|
|
context->headers = hdr;
|
|
hdr->len = keylen;
|
|
memcpy(hdr->data, evt->header_key, keylen + 1);
|
|
memcpy(hdr->data + keylen + 1, evt->header_value, vallen + 1);
|
|
break;
|
|
}
|
|
case HTTP_EVENT_ON_DATA: {
|
|
if (context->headers) {
|
|
context->status_code = esp_http_client_get_status_code(evt->client);
|
|
int err = make_callback(context, HTTP_EVENT_ON_HEADER, NULL, 0);
|
|
if (err) return err;
|
|
}
|
|
return make_callback(context, evt->event_id, evt->data, evt->data_len);
|
|
}
|
|
case HTTP_EVENT_ON_FINISH: {
|
|
if (context->headers) {
|
|
// Might still be set, if there wasn't any data in the request
|
|
context->status_code = esp_http_client_get_status_code(evt->client);
|
|
int err = make_callback(context, HTTP_EVENT_ON_HEADER, NULL, 0);
|
|
if (err) return err;
|
|
}
|
|
// Given when HTTP_EVENT_ON_FINISH is dispatched (before the
|
|
// http_should_keep_alive check in esp_http_client_perform) I don't think
|
|
// there's any benefit to exposing this event
|
|
// int ret = make_callback(context, HTTP_EVENT_ON_FINISH, NULL, 0);
|
|
break;
|
|
}
|
|
case HTTP_EVENT_DISCONNECTED:
|
|
context_clearflag(context, Connected);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Task posted from http thread when there's an event
|
|
static void lhttp_event_task(task_param_t param, task_prio_t prio)
|
|
{
|
|
esp_http_client_event_t *evt = (esp_http_client_event_t *)param;
|
|
lhttp_context_t *context = (lhttp_context_t *)evt->user_data;
|
|
|
|
context_setflag(context, AckPending);
|
|
int result = http_event_cb(evt);
|
|
if (context->perform_rtos_task && result != DELAY_ACK) {
|
|
context_clearflag(context, AckPending);
|
|
xTaskNotifyGive(context->perform_rtos_task);
|
|
}
|
|
}
|
|
|
|
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)evt->user_data;
|
|
|
|
if (context->perform_rtos_task) {
|
|
// asynchronous mode: we're called from perform_rtos_task context
|
|
// 1. post to Lua task
|
|
task_post_high(lhttp_event_task_id, (task_param_t)evt);
|
|
// 2. wait for ack from Lua land
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
if (context_flag(context, ShouldCloseInRtosTask)) {
|
|
context_clearflag(context, ShouldCloseInRtosTask);
|
|
esp_http_client_close(context->client);
|
|
return ESP_FAIL;
|
|
} else {
|
|
return ESP_OK;
|
|
}
|
|
} else {
|
|
// Call directly
|
|
return http_event_cb(evt);
|
|
}
|
|
}
|
|
|
|
static int make_callback(lhttp_context_t *context, int id, void *data, size_t data_len)
|
|
{
|
|
lua_State *L = lua_getstate();
|
|
lua_settop(L, 0);
|
|
|
|
switch (id) {
|
|
case HTTP_EVENT_ON_CONNECTED:
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[ConnectionCallback]);
|
|
break;
|
|
case HTTP_EVENT_ON_HEADER: {
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[HeadersCallback]);
|
|
lua_pushinteger(L, context->status_code);
|
|
lua_newtable(L);
|
|
list_item *item = context->headers;
|
|
while (item) {
|
|
lua_pushlstring(L, item->data, item->len); // key
|
|
// Lowercase all header names
|
|
luaL_getmetafield(L, -1, "lower");
|
|
lua_insert(L, -2);
|
|
lua_call(L, 1, 1);
|
|
char *val = item->data + item->len + 1;
|
|
lua_pushstring(L, val);
|
|
lua_settable(L, -3);
|
|
list_item *next = item->next;
|
|
free(item);
|
|
item = next;
|
|
}
|
|
context->headers = NULL;
|
|
break;
|
|
}
|
|
case HTTP_EVENT_ON_DATA:
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[DataCallback]);
|
|
lua_pushinteger(L, context->status_code);
|
|
lua_pushlstring(L, data, data_len);
|
|
break;
|
|
case HTTP_REQUEST_COMPLETE:
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[CompleteCallback]);
|
|
lua_pushinteger(L, context->status_code);
|
|
lua_pushboolean(L, context_flag(context, Connected));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
int result = ESP_OK;
|
|
if (lua_type(L, 1) == LUA_TFUNCTION) {
|
|
// Don't set InCallback for complete callback, we use that to determine
|
|
// whether a connection is busy or not, and during a complete it's not
|
|
if (id != HTTP_REQUEST_COMPLETE) {
|
|
context_setflag(context, InCallback);
|
|
}
|
|
int err = lua_pcall(L, lua_gettop(L) - 1, 1, 0);
|
|
context_clearflag(context, InCallback);
|
|
if (err) {
|
|
const char *msg = lua_type(L, -1) == LUA_TSTRING ? lua_tostring(L, -1) : "<?>";
|
|
ESP_LOGW(TAG, "Error returned from callback for HTTP event %d: %s", id, msg);
|
|
result = ESP_FAIL;
|
|
} else {
|
|
bool delay_ack = (lua_tointeger(L, -1) == DELAY_ACK);
|
|
if (delay_ack) {
|
|
if (!context_flag(context, Async)) {
|
|
luaL_error(L, "Cannot delay acknowledgment of a callback when using synchronous callbacks");
|
|
} else if (id != HTTP_EVENT_ON_DATA) {
|
|
luaL_error(L, "Cannot delay acknowledgment of callbacks other than 'data'");
|
|
}
|
|
result = DELAY_ACK;
|
|
}
|
|
}
|
|
}
|
|
lua_settop(L, 0);
|
|
return result;
|
|
}
|
|
|
|
// headers_idx must be absolute idx
|
|
static void set_headers(lua_State *L, int headers_idx, esp_http_client_handle_t client)
|
|
{
|
|
if (lua_isnoneornil(L, headers_idx)) {
|
|
return;
|
|
}
|
|
lua_pushnil(L);
|
|
while (lua_next(L, headers_idx) != 0) {
|
|
/* uses 'key' (at index -2) and 'value' (at index -1) */
|
|
if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TSTRING) {
|
|
const char *key = lua_tostring(L, -2);
|
|
const char *val = lua_tostring(L, -1);
|
|
esp_http_client_set_header(client, key, val);
|
|
}
|
|
/* removes 'value'; keeps 'key' for next iteration */
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
|
|
// Options assumed to be on top of stack
|
|
static void parse_options(lua_State *L, lhttp_context_t *context, esp_http_client_config_t *config)
|
|
{
|
|
config->timeout_ms = opt_checkint(L, "timeout", 10*1000); // Same default as old http module
|
|
config->buffer_size = opt_checkint(L, "bufsz", DEFAULT_HTTP_BUF_SIZE);
|
|
int redirects = opt_checkint(L, "max_redirects", -1); // -1 means "not specified" here
|
|
if (redirects == 0) {
|
|
config->disable_auto_redirect = true;
|
|
} else if (redirects > 0) {
|
|
config->max_redirection_count = redirects;
|
|
}
|
|
// Note, config->is_async is always set to false regardless of what we set
|
|
// the Async flag to, because of how we configure the tasks we always want
|
|
// esp_http_client_perform to run in its 'is_async=false' mode.
|
|
context_setflagbool(context, Async, opt_checkbool(L, "async", false));
|
|
|
|
if (opt_get(L, "cert", LUA_TSTRING)) {
|
|
const char *cert = lua_tostring(L, -1);
|
|
context_setref(L, context, CertRef);
|
|
config->cert_pem = cert;
|
|
}
|
|
|
|
// This function doesn't set headers because we need the connection to be created first
|
|
}
|
|
|
|
// http.createConnection([url, [method,] [options])
|
|
static int http_lapi_createConnection(lua_State *L)
|
|
{
|
|
lua_settop(L, 3);
|
|
const char *url = NULL;
|
|
if (!lua_isnoneornil(L, 1)) {
|
|
url = luaL_checkstring(L, 1);
|
|
}
|
|
int method = HTTP_METHOD_GET;
|
|
if (lua_type(L, 2) == LUA_TNUMBER) {
|
|
method = luaL_checkint(L, 2);
|
|
if (method < 0 || method >= HTTP_METHOD_MAX) {
|
|
return luaL_error(L, "Bad HTTP method %d", method);
|
|
}
|
|
} else {
|
|
// No method, make sure options on top
|
|
lua_settop(L, 2);
|
|
}
|
|
lhttp_context_t *context = context_new(L); // context now on top of stack
|
|
lua_insert(L, -2); // Move context below options
|
|
|
|
esp_http_client_config_t config = {
|
|
.url = url,
|
|
.event_handler = http_event_handler,
|
|
.method = method,
|
|
.is_async = false,
|
|
.user_data = context,
|
|
};
|
|
parse_options(L, context, &config);
|
|
|
|
context->client = esp_http_client_init(&config);
|
|
if (!context->client) {
|
|
return luaL_error(L, "esp_http_client_init failed");
|
|
}
|
|
|
|
// override the default user agent with our own default
|
|
esp_http_client_set_header(context->client, "User-Agent", http_default_user_agent);
|
|
|
|
if (lua_istable(L, -1)) {
|
|
lua_getfield(L, -1, "headers");
|
|
set_headers(L, lua_gettop(L), context->client);
|
|
lua_pop(L, 1); // headers
|
|
}
|
|
lua_pop(L, 1); // options
|
|
|
|
// Note we do not take a ref to the context itself until http_lapi_request(),
|
|
// because otherwise we'd leak any object that was constructed but never used.
|
|
return 1;
|
|
}
|
|
|
|
// context:on(name, fn)
|
|
static int http_lapi_on(lua_State *L)
|
|
{
|
|
lua_settop(L, 3);
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
int callback_idx = luaL_checkoption(L, 2, NULL, CALLBACK_NAME);
|
|
context_setref(L, context, callback_idx);
|
|
lua_settop(L, 1);
|
|
return 1; // Return context, for chained calls
|
|
}
|
|
|
|
// context:request()
|
|
static int http_lapi_request(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
CHECK_CONNECTION_IDLE(context);
|
|
|
|
// context now owns itself for the duration of the request
|
|
assert(context->refs[ContextRef] == LUA_NOREF);
|
|
lua_pushvalue(L, 1);
|
|
context_setref(L, context, ContextRef);
|
|
|
|
if (context_flag(context, Async)) {
|
|
if (xTaskCreate(perform_rtos_task,
|
|
"http_task",
|
|
4096,
|
|
(void *)context,
|
|
ESP_TASK_MAIN_PRIO + 1,
|
|
&context->perform_rtos_task) != pdPASS) {
|
|
context_unsetref(L, context, ContextRef);
|
|
return luaL_error(L, "cannot create rtos task");
|
|
}
|
|
return 0;
|
|
} else {
|
|
esp_err_t err = esp_http_client_perform(context->client);
|
|
// Note, the above call invalidates the Lua stack
|
|
|
|
if (err) {
|
|
ESP_LOGW(TAG, "Error %d from esp_http_client_perform", err);
|
|
context_seterr(context, err);
|
|
}
|
|
make_callback(context, HTTP_REQUEST_COMPLETE, NULL, 0);
|
|
|
|
lua_pushinteger(L, context->status_code);
|
|
lua_pushboolean(L, context_flag(context, Connected));
|
|
context_unsetref(L, context, ContextRef);
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
// connection:seturl(url)
|
|
static int http_lapi_seturl(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
CHECK_CONNECTION_IDLE(context);
|
|
esp_err_t err = esp_http_client_set_url(context->client, luaL_checkstring(L, 2));
|
|
if (err) {
|
|
return luaL_error(L, "esp_http_client_set_url returned %d", err);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// connection:setmethod(method)
|
|
static int http_lapi_setmethod(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
CHECK_CONNECTION_IDLE(context);
|
|
int method = luaL_checkint(L, 2);
|
|
if (method < 0 || method >= HTTP_METHOD_MAX) {
|
|
return luaL_error(L, "Bad HTTP method %d", method);
|
|
}
|
|
esp_http_client_set_method(context->client, method);
|
|
return 0;
|
|
}
|
|
|
|
// connection:setheader(name, val)
|
|
static int http_lapi_setheader(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
CHECK_CONNECTION_IDLE(context);
|
|
const char *name = luaL_checkstring(L, 2);
|
|
const char *value = luaL_optstring(L, 3, NULL);
|
|
esp_http_client_set_header(context->client, name, value);
|
|
return 0;
|
|
}
|
|
|
|
// context:setpostdata(data)
|
|
static int http_lapi_setpostdata(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
CHECK_CONNECTION_IDLE(context);
|
|
|
|
size_t postdata_sz;
|
|
const char *postdata = luaL_optlstring(L, 2, NULL, &postdata_sz);
|
|
esp_http_client_set_method(context->client, HTTP_METHOD_POST);
|
|
esp_http_client_set_post_field(context->client, postdata, (int)postdata_sz);
|
|
context_setref(L, context, PostDataRef);
|
|
return 0;
|
|
}
|
|
|
|
// context:ack()
|
|
static int http_lapi_ack(lua_State *L)
|
|
{
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, 1, http_context_mt);
|
|
if (!context_flag(context, AckPending)) {
|
|
return luaL_error(L, "No asynchronous callback pending");
|
|
}
|
|
if (context_flag(context, InCallback)) {
|
|
// Unblocking the HTTP task to potentially trigger more callbacks while
|
|
// we're still processing this callback is just too complicated
|
|
return luaL_error(L, "Cannot call ack() from within the callback itself");
|
|
}
|
|
context_clearflag(context, AckPending);
|
|
xTaskNotifyGive(context->perform_rtos_task);
|
|
return 0;
|
|
}
|
|
|
|
//// One-shot functions http.get(), http.post() etc follow ////
|
|
|
|
// args: statusCode, headers
|
|
static int http_accumulate_headers(lua_State *L)
|
|
{
|
|
int cache_table = lua_upvalueindex(1);
|
|
lua_rawseti(L, cache_table, 2); // cache_table[2] = headers
|
|
lua_rawseti(L, cache_table, 1); // cache_table[1] = statusCode
|
|
lua_pushinteger(L, 2);
|
|
lua_rawseti(L, cache_table, 0); // Use zero for len
|
|
return 0;
|
|
}
|
|
|
|
// args: statusCode, data
|
|
static int http_accumulate_data(lua_State *L)
|
|
{
|
|
int cache_table = lua_upvalueindex(1);
|
|
lua_rawgeti(L, cache_table, 0);
|
|
int n = lua_tointeger(L, -1);
|
|
lua_pop(L, 1);
|
|
lua_rawseti(L, cache_table, n + 1); // top of stack is data
|
|
lua_pushinteger(L, n + 1);
|
|
lua_rawseti(L, cache_table, 0);
|
|
return 0;
|
|
}
|
|
|
|
static int http_accumulate_complete(lua_State *L)
|
|
{
|
|
lua_settop(L, 1); // Don't care about any of the args except status_code
|
|
int context_idx = lua_upvalueindex(1);
|
|
int cache_table = lua_upvalueindex(2);
|
|
lua_pushvalue(L, lua_upvalueindex(3)); // The callback fn
|
|
lua_insert(L, 1); // Put callback fn first, status_code second
|
|
|
|
// Now concat data
|
|
luaL_Buffer b;
|
|
luaL_buffinit(L, &b);
|
|
for (int i = 3; ; i++) {
|
|
lua_rawgeti(L, cache_table, i);
|
|
if lua_isnoneornil(L, -1) {
|
|
lua_pop(L, 1);
|
|
break;
|
|
}
|
|
luaL_addvalue(&b);
|
|
// Remove from table, don't need any more
|
|
lua_pushnil(L);
|
|
lua_rawseti(L, cache_table, i);
|
|
}
|
|
luaL_pushresult(&b); // data now pushed
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, context_idx, http_context_mt);
|
|
if (lua_isnoneornil(L, 1)) {
|
|
// No callback fn so must be sync, meaning just need to stash headers and data in the context
|
|
// steal some completion refs, nothing's going to need them again in a one-shot
|
|
context_setref(L, context, DataCallback); // pops data
|
|
lua_rawgeti(L, cache_table, 2); // headers
|
|
context_setref(L, context, HeadersCallback);
|
|
} else {
|
|
lua_rawgeti(L, cache_table, 2); // headers
|
|
lua_call(L, 3, 0);
|
|
}
|
|
// unset this since it contains a reference to the context and would prevent the context to be garbage collected
|
|
context_unsetref(L, context,CompleteCallback);
|
|
return 0;
|
|
}
|
|
|
|
static int make_oneshot_request(lua_State *L, int callback_idx)
|
|
{
|
|
// context must be on top of stack
|
|
lhttp_context_t *context = (lhttp_context_t *)luaL_checkudata(L, -1, http_context_mt);
|
|
const bool async = context_flag(context, Async);
|
|
lua_pushvalue(L, -1); // Need this later
|
|
|
|
// Make sure we always send Connection: close for oneshots
|
|
esp_http_client_set_header(context->client, "Connection", "close");
|
|
|
|
lua_newtable(L); // cache table
|
|
|
|
lua_pushvalue(L, -1); // dup cache table
|
|
lua_pushcclosure(L, http_accumulate_headers, 1);
|
|
context_setref(L, context, HeadersCallback);
|
|
|
|
lua_pushvalue(L, -1); // dup cache table
|
|
lua_pushcclosure(L, http_accumulate_data, 1);
|
|
context_setref(L, context, DataCallback);
|
|
|
|
// Don't dup cache table, it's in the right place on the stack and we don't need it again
|
|
lua_pushvalue(L, callback_idx);
|
|
lua_pushcclosure(L, http_accumulate_complete, 3); // context, cache table, callback
|
|
context_setref(L, context, CompleteCallback);
|
|
|
|
// Finally, call request
|
|
lua_pushcfunction(L, http_lapi_request);
|
|
lua_pushvalue(L, -2); // context
|
|
lua_call(L, 1, 0);
|
|
|
|
if (async) {
|
|
return 0;
|
|
} else {
|
|
// Have to return the data here. context is guaranteed still valid because
|
|
// we made sure to keep a reference to it on our stack
|
|
lua_pushinteger(L, context->status_code);
|
|
// Retrieve the data we stashed in context in http_accumulate_complete
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[DataCallback]);
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, context->refs[HeadersCallback]);
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
// http.get(url, [options,] [callback])
|
|
static int http_lapi_get(lua_State *L)
|
|
{
|
|
luaL_checkstring(L, 1);
|
|
if (lua_isfunction(L, 2)) {
|
|
lua_pushnil(L);
|
|
lua_insert(L, 2);
|
|
}
|
|
lua_settop(L, 3);
|
|
if (lua_isnil(L, 2)) {
|
|
lua_newtable(L);
|
|
lua_replace(L, 2);
|
|
}
|
|
// Now 1 = url, 2 = non-nil options, 3 = [callback]
|
|
|
|
luaL_argcheck(L, lua_istable(L, 2), 2, "options must be nil or a table");
|
|
bool async = lua_isfunction(L, 3);
|
|
luaL_argcheck(L, lua_isnil(L, 3) || async, 3, "callback must be nil or a function");
|
|
|
|
// Override options.async based on whether callback present
|
|
lua_pushboolean(L, async);
|
|
lua_setfield(L, 2, "async");
|
|
|
|
// Setup call to createConnection
|
|
lua_pushcfunction(L, http_lapi_createConnection);
|
|
lua_pushvalue(L, 1); // url
|
|
lua_pushinteger(L, HTTP_METHOD_GET);
|
|
lua_pushvalue(L, 2); // options
|
|
|
|
lua_call(L, 3, 1); // returns context
|
|
|
|
return make_oneshot_request(L, 3);
|
|
}
|
|
|
|
// http.post(url, options, body[, callback])
|
|
static int http_lapi_post(lua_State *L)
|
|
{
|
|
lua_settop(L, 4);
|
|
|
|
luaL_checkstring(L, 1);
|
|
if (lua_isnil(L, 2)) {
|
|
lua_newtable(L);
|
|
lua_replace(L, 2);
|
|
}
|
|
// Now 1 = url, 2 = non-nil options, 3 = body, 4 = [callback]
|
|
|
|
luaL_argcheck(L, lua_istable(L, 2), 2, "options must be nil or a table");
|
|
luaL_checkstring(L, 3);
|
|
bool async = lua_isfunction(L, 4);
|
|
luaL_argcheck(L, lua_isnil(L, 4) || async, 4, "callback must be nil or a function");
|
|
|
|
// Override options.async based on whether callback present
|
|
lua_pushboolean(L, async);
|
|
lua_setfield(L, 2, "async");
|
|
|
|
// Setup call to createConnection
|
|
lua_pushcfunction(L, http_lapi_createConnection);
|
|
lua_pushvalue(L, 1); // url
|
|
lua_pushinteger(L, HTTP_METHOD_POST);
|
|
lua_pushvalue(L, 2); // options
|
|
|
|
lua_call(L, 3, 1); // returns context
|
|
|
|
lua_pushcfunction(L, http_lapi_setpostdata);
|
|
lua_pushvalue(L, -2); // context
|
|
lua_pushvalue(L, 3); // body
|
|
lua_call(L, 2, 0);
|
|
|
|
return make_oneshot_request(L, 4); // 4 = callback idx
|
|
}
|
|
|
|
LROT_BEGIN(http, NULL, 0)
|
|
LROT_FUNCENTRY(createConnection, http_lapi_createConnection)
|
|
LROT_NUMENTRY (GET, HTTP_METHOD_GET)
|
|
LROT_NUMENTRY (POST, HTTP_METHOD_POST)
|
|
LROT_NUMENTRY (DELETE, HTTP_METHOD_DELETE)
|
|
LROT_NUMENTRY (HEAD, HTTP_METHOD_HEAD)
|
|
LROT_NUMENTRY (DELAYACK, DELAY_ACK)
|
|
LROT_NUMENTRY (ACKNOW, 0) // Doesn't really matter what this is
|
|
LROT_FUNCENTRY(get, http_lapi_get)
|
|
LROT_FUNCENTRY(post, http_lapi_post)
|
|
LROT_END(http, NULL, 0)
|
|
|
|
LROT_BEGIN(http_context, NULL, 0)
|
|
LROT_FUNCENTRY(on, http_lapi_on)
|
|
LROT_FUNCENTRY(request, http_lapi_request)
|
|
LROT_FUNCENTRY(setmethod, http_lapi_setmethod)
|
|
LROT_FUNCENTRY(setheader, http_lapi_setheader)
|
|
LROT_FUNCENTRY(seturl, http_lapi_seturl)
|
|
LROT_FUNCENTRY(setpostdata, http_lapi_setpostdata)
|
|
LROT_FUNCENTRY(close, context_close)
|
|
LROT_FUNCENTRY(ack, http_lapi_ack)
|
|
LROT_FUNCENTRY(__gc, context_gc)
|
|
LROT_TABENTRY (__index, http_context)
|
|
LROT_END(http_context, NULL, 0)
|
|
|
|
static int luaopen_http(lua_State *L)
|
|
{
|
|
luaL_rometatable(L, http_context_mt, LROT_TABLEREF(http_context));
|
|
lhttp_request_task_id = task_get_id(lhttp_request_task);
|
|
lhttp_event_task_id = task_get_id(lhttp_event_task);
|
|
return 0;
|
|
}
|
|
|
|
NODEMCU_MODULE(HTTP, "http", http, luaopen_http);
|