# HG changeset patch # User David Demelier # Date 1646422264 -3600 # Node ID 47ed24577389758b2cc25079aee598031a4ea531 # Parent 832dbde9d4951abde9ae4a5c5b35bc3654624a87 irccd: add Irccd.Http API diff -r 832dbde9d495 -r 47ed24577389 CMakeLists.txt --- a/CMakeLists.txt Mon Feb 28 21:38:38 2022 +0100 +++ b/CMakeLists.txt Fri Mar 04 20:31:04 2022 +0100 @@ -55,6 +55,7 @@ include(CMakePackageConfigHelpers) include(GNUInstallDirs) +option(IRCCD_WITH_HTTP "Enable HTTP support (requires libcurl)" On) option(IRCCD_WITH_EXAMPLES "Enable example files" On) option(IRCCD_WITH_JS "Enable Javascript" On) option(IRCCD_WITH_MAN "Enable manual pages" On) diff -r 832dbde9d495 -r 47ed24577389 irccd/CMakeLists.txt --- a/irccd/CMakeLists.txt Mon Feb 28 21:38:38 2022 +0100 +++ b/irccd/CMakeLists.txt Fri Mar 04 20:31:04 2022 +0100 @@ -75,6 +75,15 @@ flex_target(lex ${irccd_SOURCE_DIR}/lex.l ${irccd_BINARY_DIR}/lex.c) add_flex_bison_dependency(lex yacc) +if (IRCCD_WITH_HTTP AND IRCCD_WITH_JS) + find_package(CURL REQUIRED) + list( + APPEND SOURCES + ${irccd_SOURCE_DIR}/jsapi-http.c + ${irccd_SOURCE_DIR}/jsapi-http.h + ) +endif () + # # libirccd-static # ------------------------------------------------------------------- @@ -98,6 +107,10 @@ ) set_target_properties(irccd-static PROPERTIES PREFIX "") +if (IRCCD_WITH_HTTP AND IRCCD_WITH_JS) + target_link_libraries(irccd-static CURL::libcurl) +endif () + # # irccd(1) # ------------------------------------------------------------------- diff -r 832dbde9d495 -r 47ed24577389 irccd/js-plugin.c --- a/irccd/js-plugin.c Mon Feb 28 21:38:38 2022 +0100 +++ b/irccd/js-plugin.c Fri Mar 04 20:31:04 2022 +0100 @@ -38,6 +38,7 @@ #include "jsapi-directory.h" #include "jsapi-file.h" #include "jsapi-hook.h" +#include "jsapi-http.h" #include "jsapi-irccd.h" #include "jsapi-logger.h" #include "jsapi-plugin.h" @@ -496,6 +497,9 @@ jsapi_directory_load(js->ctx); jsapi_file_load(js->ctx); jsapi_hook_load(js->ctx); +#if defined(IRCCD_WITH_HTTP) + jsapi_http_load(js->ctx); +#endif jsapi_logger_load(js->ctx); jsapi_plugin_load(js->ctx, &js->plugin); jsapi_rule_load(js->ctx); diff -r 832dbde9d495 -r 47ed24577389 irccd/jsapi-http.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/jsapi-http.c Fri Mar 04 20:31:04 2022 +0100 @@ -0,0 +1,405 @@ +/* + * jsapi-http.c -- Irccd.Http API + * + * Copyright (c) 2013-2022 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include "jsapi-http.h" +#include "jsapi-plugin.h" +#include "jsapi-system.h" + +#define SIGNATURE DUK_HIDDEN_SYMBOL("Irccd.Http") +#define CALLBACK DUK_HIDDEN_SYMBOL("Irccd.Http.callback") +#define TABLE DUK_HIDDEN_SYMBOL("Irccd.Http.table") + +struct op { + CURL *curl; + CURLM *multi; + struct irc_pollable pollable; + int errn; + long code; + char *out; + size_t outsz; + FILE *fp; + void *ptr; + duk_context *ctx; +}; + +static int pollable_fd(void *); +static void pollable_want(int *, int *, void *); +static int pollable_sync(int, int, void *); + +static void +op_free(struct op *op) +{ + if (op->curl) { + if (op->multi) + curl_multi_remove_handle(op->multi, op->curl); + + curl_easy_cleanup(op->curl); + } + if (op->multi) + curl_multi_cleanup(op->multi); + if (op->fp) + fclose(op->fp); + + free(op->out); + free(op); +} + +static size_t +op_write(char *ptr, size_t w, size_t n, void *data) +{ + struct op *op = data; + + if (fwrite(ptr, w, n, op->fp) != n) { + op->errn = errno; + return 0; + } + + return n; +} + +static struct op * +op_new(duk_context *ctx) +{ + struct op *op; + + op = irc_util_calloc(1, sizeof (*op)); + op->ctx = ctx; + + if (!(op->curl = curl_easy_init())) + goto enomem; + if (!(op->multi = curl_multi_init())) + goto enomem; + if (!(op->fp = open_memstream(&op->out, &op->outsz))) + goto enomem; + + curl_easy_setopt(op->curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(op->curl, CURLOPT_PROTOCOLS, + CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_easy_setopt(op->curl, CURLOPT_WRITEFUNCTION, op_write); + curl_easy_setopt(op->curl, CURLOPT_WRITEDATA, op); + + return op; + +enomem: + op_free(op); + errno = ENOMEM; + + return NULL; +} + +static inline void +op_start(struct op *op) +{ + int pending; + + curl_multi_add_handle(op->multi, op->curl); + curl_multi_perform(op->multi, &pending); +} + +static void +op_complete(struct op *op) +{ + struct irc_plugin *plg = jsapi_plugin_self(op->ctx); + + /* Close file pointer to retrieve data. */ + fclose(op->fp); + op->fp = NULL; + + /* + * Create a result object with the following properties: + * + * { + * status: 0 on success or errno value on error + * code: HTTP result code + * body: body content if any + * } + */ + duk_push_heapptr(op->ctx, op->ptr); + duk_push_object(op->ctx); + duk_push_int(op->ctx, op->errn); + duk_put_prop_string(op->ctx, -2, "status"); + duk_push_int(op->ctx, op->code); + duk_put_prop_string(op->ctx, -2, "code"); + duk_push_string(op->ctx, op->out); + duk_put_prop_string(op->ctx, -2, "body"); + + /* + * Remove already the data to avoid leaking too much memory until the + * function callback is finalized. + */ + free(op->out); + op->out = NULL; + op->outsz = 0; + + if (duk_pcall(op->ctx, 1) != 0) + irc_log_warn("plugin %s: %s", plg->name, duk_to_string(op->ctx, -1)); + + duk_pop(op->ctx); + + /* + * Now remove the function from the table so it gets deleted at some + * point. + */ + duk_push_global_stash(op->ctx); + duk_get_prop_string(op->ctx, -1, TABLE); + duk_del_prop_heapptr(op->ctx, -1, op->ptr); + duk_pop_n(op->ctx, 2); +} + +static inline void +parsestr(struct op *op, const char *key, int option) +{ + duk_get_prop_string(op->ctx, 0, key); + + if (duk_is_string(op->ctx, -1)) + curl_easy_setopt(op->curl, option, duk_get_string(op->ctx, -1)); + + duk_pop(op->ctx); +} + +static inline void +parselong(struct op *op, const char *key, int option) +{ + duk_get_prop_string(op->ctx, 0, key); + + if (duk_is_number(op->ctx, -1)) + curl_easy_setopt(op->curl, option, (long)duk_get_number(op->ctx, -1)); + + duk_pop(op->ctx); +} + +static inline void +parsemethod(struct op *op) +{ + duk_get_prop_string(op->ctx, 0, "method"); + + if (duk_is_string(op->ctx, -1)) + curl_easy_setopt(op->curl, CURLOPT_CUSTOMREQUEST, duk_get_string(op->ctx, -1)); + + duk_pop(op->ctx); +} + +static inline void +parsebody(struct op *op) +{ + duk_get_prop_string(op->ctx, 0, "body"); + + if (!duk_is_undefined(op->ctx, -1)) + curl_easy_setopt(op->curl, CURLOPT_POSTFIELDS, duk_to_string(op->ctx, -1)); + + duk_pop(op->ctx); +} + +static void +op_parse(struct op *op) +{ + parsestr(op, "url", CURLOPT_URL); + parselong(op, "timeout", CURLOPT_TIMEOUT); + parsemethod(op); + parsebody(op); +} + +static inline void +op_insert(struct op *op) +{ + op->pollable.data = op; + op->pollable.fd = pollable_fd; + op->pollable.want = pollable_want; + op->pollable.sync = pollable_sync; + + irc_bot_pollable_add(&op->pollable); +} + +static int +pollable_fd(void *data) +{ + struct op *op = data; + fd_set in, out, exc; + int fd = 0; + + FD_ZERO(&in); + FD_ZERO(&out); + FD_ZERO(&exc); + + curl_multi_fdset(op->multi, &in, &out, &exc, &fd); + + return fd; +} + +static void +pollable_want(int *frecv, int *fsend, void *data) +{ + struct op *op = data; + fd_set in, out, exc; + int maxfd = 0; + + FD_ZERO(&in); + FD_ZERO(&out); + FD_ZERO(&exc); + + curl_multi_fdset(op->multi, &in, &out, &exc, &maxfd); + + if (FD_ISSET(maxfd, &in)) + *frecv = 1; + if (FD_ISSET(maxfd, &out)) + *fsend = 1; +} + +static int +pollable_sync(int frecv, int fsend, void *data) +{ + (void)frecv; + (void)fsend; + + CURLMsg *msg; + struct op *op = data; + int pending, msgsz; + + /* + * CURL does its own job reading/sending without taking action on what + * have been found. + */ + if (curl_multi_perform(op->multi, &pending) < 0) { + op->errn = EINVAL; + op_complete(op); + return -1; + } + + /* We only have one handle so we can just assume 0 means complete. */ + if (pending) + return 0; + + while ((msg = curl_multi_info_read(op->multi, &msgsz))) { + if (msg->msg != CURLMSG_DONE) + continue; + + switch (msg->data.result) { + case CURLE_OPERATION_TIMEDOUT: + op->errn = ETIMEDOUT; + break; + case CURLE_OK: + op->errn = 0; + curl_easy_getinfo(op->curl, CURLINFO_RESPONSE_CODE, + &op->code); + break; + default: + break; + } + + op_complete(op); + return -1; + } + + op->errn = EINVAL; + op_complete(op); + + return 0; +} + +static int +Http_destructor(duk_context *ctx) +{ + struct op *op; + + duk_get_prop_string(ctx, 0, SIGNATURE); + + if ((op = duk_to_pointer(ctx, -1))) { + duk_push_global_stash(ctx); + duk_get_prop_string(ctx, -1, TABLE); + duk_del_prop_heapptr(ctx, -1, op->ptr); + duk_pop_n(ctx, 2); + op_free(duk_to_pointer(ctx, -1)); + } + + duk_del_prop_string(ctx, 0, SIGNATURE); + duk_pop(ctx); + + return 0; +} + +static int +Http_request(duk_context *ctx) +{ + struct op *op; + + duk_require_object(ctx, 0); + duk_require_callable(ctx, 1); + + if (!(op = op_new(ctx))) + jsapi_system_raise(ctx); + + op_parse(op); + op_start(op); + op_insert(op); + + /* + * We attach the HTTP operation to the function callback argument with + * a specific finalizer. + */ + duk_dup(ctx, 1); + duk_push_pointer(ctx, op); + duk_put_prop_string(ctx, -2, SIGNATURE); + duk_push_c_function(ctx, Http_destructor, 1); + duk_set_finalizer(ctx, -2); + duk_pop(ctx); + + /* Store the callback in a global table. */ + duk_push_global_stash(ctx); + duk_get_prop_string(ctx, -1, TABLE); + duk_dup(ctx, 1); + duk_put_prop_heapptr(ctx, -2, (op->ptr = duk_get_heapptr(ctx, 1))); + duk_pop_n(ctx, 2); + + return 0; +} + +static const duk_function_list_entry functions[] = { + { "request", Http_request, 2 }, + { NULL, NULL, 0 }, +}; + +void +jsapi_http_load(duk_context *ctx) +{ + assert(ctx); + + duk_get_global_string(ctx, "Irccd"); + duk_push_object(ctx); + duk_put_function_list(ctx, -1, functions); + duk_put_prop_string(ctx, -2, "Http"); + duk_pop(ctx); + + duk_push_global_stash(ctx); + duk_push_object(ctx); + duk_put_prop_string(ctx, -2, TABLE); + duk_pop(ctx); +} diff -r 832dbde9d495 -r 47ed24577389 irccd/jsapi-http.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccd/jsapi-http.h Fri Mar 04 20:31:04 2022 +0100 @@ -0,0 +1,27 @@ +/* + * jsapi-http.h -- Irccd.Http API + * + * Copyright (c) 2013-2022 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef IRCCD_JSAPI_HTTP_H +#define IRCCD_JSAPI_HTTP_H + +#include + +void +jsapi_http_load(duk_context *); + +#endif /* !IRCCD_JSAPI_HTTP_H */ diff -r 832dbde9d495 -r 47ed24577389 lib/irccd/config.h.in --- a/lib/irccd/config.h.in Mon Feb 28 21:38:38 2022 +0100 +++ b/lib/irccd/config.h.in Fri Mar 04 20:31:04 2022 +0100 @@ -31,5 +31,6 @@ #cmakedefine IRCCD_WITH_JS #cmakedefine IRCCD_WITH_SSL +#cmakedefine IRCCD_WITH_HTTP #endif /* !IRCCD_CONFIG_H */