]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-lua: Add unit test for dovecot.http
authorStephan Bosch <stephan.bosch@open-xchange.com>
Sat, 25 Nov 2023 16:08:56 +0000 (17:08 +0100)
committerStephan Bosch <stephan.bosch@open-xchange.com>
Fri, 23 Feb 2024 11:19:26 +0000 (12:19 +0100)
src/lib-lua/Makefile.am
src/lib-lua/test-lua-http-client.c [new file with mode: 0644]
src/lib-lua/test-lua-http-client.lua [new file with mode: 0644]

index 64b862b1eb2d179b727c60c7570821032f909ba5..7fee906c79b34ca96b2035fde847fa615d72a2ed 100644 (file)
@@ -22,7 +22,7 @@ libdovecot_lua_la_SOURCES = \
        dlua-table.c \
        dlua-thread.c
 
-test_programs = test-lua
+test_programs = test-lua test-lua-http-client
 
 WITH_YIELDS_LUA=
 if DLUA_WITH_YIELDS
@@ -54,12 +54,17 @@ headers = \
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
 
+EXTRA_DIST = \
+       test-lua-http-client.lua
+
 noinst_PROGRAMS = $(test_programs)
 
 test_libs =\
  libdovecot-lua.la \
  ../lib-dovecot/libdovecot.la
 
+test_libs_ssl = ../lib-ssl-iostream/libssl_iostream_openssl.la
+
 test_lua_SOURCES = test-lua.c
 test_lua_CFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
 test_lua_LDFLAGS = $(BINARY_LDFLAGS)
@@ -74,6 +79,14 @@ test_dns_lua_SOURCES = test-dns-lua.c
 test_dns_lua_LDADD = $(test_libs) $(LUA_LIBS)
 test_dns_lua_DEPENDENCIES = $(test_libs)
 
+test_lua_http_client_SOURCES = test-lua-http-client.c
+test_lua_http_client_LDADD = $(test_libs) $(test_libs_ssl) $(LUA_LIBS)
+test_lua_http_client_DEPENDENCIES = $(test_libs)
+test_lua_http_client_CFLAGS = \
+       $(AM_CPPFLAGS) \
+       $(DOVECOT_BINARY_CFLAGS) \
+       -DTEST_LUA_SCRIPT_DIR=\"$(abs_top_srcdir)/src/lib-lua\"
+
 check-local:
        for bin in $(test_programs); do \
          if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
diff --git a/src/lib-lua/test-lua-http-client.c b/src/lib-lua/test-lua-http-client.c
new file mode 100644 (file)
index 0000000..f2a9c0f
--- /dev/null
@@ -0,0 +1,763 @@
+/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+#include "iostream-openssl.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "dlua-script-private.h"
+
+#include <unistd.h>
+#include <sys/signal.h>
+
+#define CLIENT_PROGRESS_TIMEOUT     10
+#define SERVER_KILL_TIMEOUT_SECS    20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+       struct connection conn;
+       void *context;
+
+       struct ssl_iostream *ssl_iostream;
+       struct istream *real_input;
+       struct ostream *real_output;
+
+       pool_t pool;
+       bool version_sent:1;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool (*test_client_init_t)(void);
+typedef void (*test_dns_init_t)(void);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+static struct event *test_event;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static struct connection_list *server_conn_list;
+static unsigned int server_index;
+struct ssl_iostream_context *server_ssl_ctx = NULL;
+bool test_server_ssl = FALSE;
+static int (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+static void (*test_server_input)(struct server_connection *conn);
+
+/* client */
+static struct timeout *to_client_progress = NULL;
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_deinit(void);
+
+/* test*/
+static void
+test_run_client_server(test_client_init_t client_test,
+                      test_server_init_t server_test,
+                      unsigned int server_tests_count,
+                      test_dns_init_t dns_test) ATTR_NULL(3);
+
+/*
+ * Simple post
+ */
+
+/* dns */
+
+static void
+test_dns_simple_post_input(struct server_connection *conn)
+{
+       const char *line;
+
+       if (!conn->version_sent) {
+               conn->version_sent = TRUE;
+               o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+       }
+
+       while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+               if (str_begins_with(line, "VERSION"))
+                       continue;
+               e_debug(test_event, "DNS REQUEST: %s", line);
+
+               if (strcmp(line, "IP\thosta") == 0) {
+                       o_stream_nsend_str(conn->conn.output,
+                                          "0\t127.0.0.1\n");
+               } else {
+                       i_sleep_msecs(300);
+                       o_stream_nsend_str(
+                               conn->conn.output,
+                               t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+               }
+       }
+}
+
+static void test_dns_simple_post(void)
+{
+       test_server_input = test_dns_simple_post_input;
+       test_server_run(0);
+}
+
+/* server */
+
+struct _simple_post_sctx {
+       bool eoh;
+       struct timeout *to;
+       bool serviced;
+};
+
+static int test_server_simple_post_init(struct server_connection *conn)
+{
+       struct _simple_post_sctx *ctx;
+
+       ctx = p_new(conn->pool, struct _simple_post_sctx, 1);
+       conn->context = ctx;
+       return 0;
+}
+
+static void
+test_server_simple_post_disconnect(struct server_connection *conn)
+{
+       struct _simple_post_sctx *ctx = conn->context;
+
+       timeout_remove(&ctx->to);
+       server_connection_deinit(&conn);
+}
+
+static void test_server_simple_post_input(struct server_connection *conn)
+{
+       struct _simple_post_sctx *ctx = conn->context;
+       const char *line;
+
+       if (ctx->serviced) {
+               /* Wait for disconnect or beginning of next request */
+               ssize_t sret = i_stream_read(conn->conn.input);
+               if (sret > 0 || conn->conn.input->eof)
+                       server_connection_deinit(&conn);
+               return;
+       }
+
+       while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+               if (*line == '\0') {
+                       ctx->eoh = TRUE;
+                       break;
+               }
+       }
+
+       if (conn->conn.input->stream_errno != 0) {
+               i_fatal("server: Stream error: %s",
+                       i_stream_get_error(conn->conn.input));
+       }
+       if (line == NULL) {
+               if (conn->conn.input->eof)
+                       server_connection_deinit(&conn);
+               return;
+       }
+
+       i_assert(ctx->eoh);
+       ctx->eoh = FALSE;
+
+       static const char json_response[] =
+       "{\n"
+       "\"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
+       "\"token_type\":\"Bearer\",\n"
+       "\"expires_in\":3600,\n"
+       "\"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
+       "\"scope\":\"create\"\n"
+       "}";
+
+       string_t *resp = t_str_new(512);
+       str_printfa(resp,
+                   "HTTP/1.1 100 OK\r\n"
+                   "\r\n"
+                   "HTTP/1.1 200 OK\r\n"
+                   "Content-Length: %zu\r\n"
+                   "\r\n"
+                   "%s", sizeof(json_response) - 1, json_response);
+       o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+       if (o_stream_flush(conn->conn.output) < 0) {
+               i_fatal("server: Flush error: %s",
+                       o_stream_get_error(conn->conn.output));
+       }
+
+       ctx->serviced = TRUE;
+       ctx->to = timeout_add(
+               5000, test_server_simple_post_disconnect, conn);
+}
+
+static void test_server_simple_post_deinit(struct server_connection *conn)
+{
+       struct _simple_post_sctx *ctx = conn->context;
+
+       timeout_remove(&ctx->to);
+}
+
+static void test_server_simple_post(unsigned int index)
+{
+       test_server_init = test_server_simple_post_init;
+       test_server_input = test_server_simple_post_input;
+       test_server_deinit = test_server_simple_post_deinit;
+       test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_simple_post_run_post(struct dlua_script *script, const char *url)
+{
+       const char *error;
+       int ret;
+
+       lua_pushstring(script->L, url);
+       if (dlua_pcall(script->L, "http_request_post", 1, 1, &error) < 0)
+               i_fatal("dlua_pcall() failed: %s", error);
+
+       test_assert(lua_isinteger(script->L, -1));
+       if (lua_isinteger(script->L, -1)) {
+               ret = lua_tointeger(script->L, -1);
+               /* not guaranteed to fail, but it will happen often */
+               e_debug(test_event, "http_request_post() returned %d", ret);
+       }
+
+       lua_pop(script->L, 1);
+       i_assert(lua_gettop(script->L) == 0);
+}
+
+static bool test_client_simple_post(void)
+{
+       struct dlua_script *script;
+       const char *error;
+
+       if (event_want_debug(test_event))
+               test_expect_errors(4);
+
+       if (dlua_script_create_file(
+               TEST_LUA_SCRIPT_DIR "/test-lua-http-client.lua",
+               &script, test_event, &error) < 0)
+               i_fatal("dlua_script_create_file() failed: %s", error);
+
+       dlua_dovecot_register(script);
+       if (dlua_script_init(script, &error) < 0)
+               i_fatal("dlua_script_init() failed: %s", error);
+
+       /* First POST */
+       test_client_simple_post_run_post(
+               script, t_strdup_printf("https://hosta:%u/first-post",
+                                       bind_ports[0]));
+
+       /* Second POST */
+       test_client_simple_post_run_post(
+               script, t_strdup_printf("https://hosta:%u/second-post",
+                                       bind_ports[0]));
+
+       dlua_script_unref(&script);
+
+       return TRUE;
+}
+
+static bool test_client_second_post(void)
+{
+       struct dlua_script *script;
+       const char *error;
+
+       if (event_want_debug(test_event))
+               test_expect_errors(4);
+
+       if (dlua_script_create_file(
+               TEST_LUA_SCRIPT_DIR "/test-lua-http-client.lua",
+               &script, test_event, &error) < 0)
+               i_fatal("dlua_script_create_file() failed: %s", error);
+
+       dlua_dovecot_register(script);
+       if (dlua_script_init(script, &error) < 0)
+               i_fatal("dlua_script_init() failed: %s", error);
+
+       /* First POST */
+       test_client_simple_post_run_post(
+               script, t_strdup_printf("http%s://hosta:%u/first-post",
+                                       (test_server_ssl ? "s" : ""),
+                                       bind_ports[0]));
+
+       /* Second POST */
+       test_client_simple_post_run_post(
+               script, t_strdup_printf("http%s://hosta:%u/second-post",
+                                       (test_server_ssl ? "s" : ""),
+                                       bind_ports[0]));
+
+       dlua_script_unref(&script);
+
+       return TRUE;
+}
+
+/* test */
+
+static void test_simple_post(void)
+{
+       test_begin("simple post");
+       test_server_ssl = FALSE;
+       test_run_client_server(test_client_simple_post,
+                              test_server_simple_post, 1,
+                              test_dns_simple_post);
+       test_end();
+
+       test_begin("simple post (ssl)");
+       test_server_ssl = TRUE;
+       test_run_client_server(test_client_simple_post,
+                              test_server_simple_post, 1,
+                              test_dns_simple_post);
+       test_end();
+}
+
+static void test_second_post(void)
+{
+       test_begin("second post");
+       test_server_ssl = FALSE;
+       test_run_client_server(test_client_second_post,
+                              test_server_simple_post, 1,
+                              test_dns_simple_post);
+       test_end();
+
+       test_begin("second post (ssl)");
+       test_server_ssl = TRUE;
+       test_run_client_server(test_client_second_post,
+                              test_server_simple_post, 1,
+                              test_dns_simple_post);
+       test_end();
+
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+       test_simple_post,
+       test_second_post,
+       NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+       /* Terminate test due to lack of progress */
+       test_assert(FALSE);
+       timeout_remove(&to_client_progress);
+       io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_init(test_client_init_t client_test)
+{
+       i_assert(client_test != NULL);
+       if (!client_test())
+               return FALSE;
+
+       to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000,
+                                        test_client_progress_timeout, NULL);
+       return TRUE;
+}
+
+static void test_client_deinit(void)
+{
+       timeout_remove(&to_client_progress);
+}
+
+static void test_client_run(test_client_init_t client_test)
+{
+       test_client_init(client_test);
+       test_client_deinit();
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static int
+server_connection_init_ssl(struct server_connection *conn)
+{
+       struct ssl_iostream_settings ssl_set;
+       const char *error;
+
+       if (!test_server_ssl)
+               return 0;
+
+       connection_input_halt(&conn->conn);
+
+       ssl_iostream_test_settings_server(&ssl_set);
+       ssl_set.verbose = debug;
+
+       if (server_ssl_ctx == NULL &&
+           ssl_iostream_context_init_server(&ssl_set, &server_ssl_ctx,
+                                            &error) < 0) {
+               i_error("SSL context initialization failed: %s", error);
+               return -1;
+       }
+
+       if (io_stream_create_ssl_server(server_ssl_ctx, &ssl_set, conn->conn.event,
+                                       &conn->conn.input, &conn->conn.output,
+                                       &conn->ssl_iostream, &error) < 0) {
+               i_error("SSL init failed: %s", error);
+               return -1;
+       }
+       if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+               i_error("SSL handshake failed: %s",
+                       ssl_iostream_get_last_error(conn->ssl_iostream));
+               return -1;
+       }
+
+       connection_input_resume(&conn->conn);
+       return 0;
+}
+
+static void server_connection_input(struct connection *_conn)
+{
+       struct server_connection *conn = (struct server_connection *)_conn;
+
+       test_server_input(conn);
+}
+
+static void server_connection_init(int fd)
+{
+       struct server_connection *conn;
+       pool_t pool;
+
+       net_set_nonblock(fd, TRUE);
+
+       pool = pool_alloconly_create("server connection", 512);
+       conn = p_new(pool, struct server_connection, 1);
+       conn->pool = pool;
+
+       connection_init_server(server_conn_list, &conn->conn,
+                              "server connection", fd, fd);
+
+       conn->real_input = conn->conn.input;
+       conn->real_output = conn->conn.output;
+       if (server_connection_init_ssl(conn) < 0) {
+               server_connection_deinit(&conn);
+               return;
+       }
+
+       if (test_server_init != NULL) {
+               if (test_server_init(conn) != 0)
+                       return;
+       }
+}
+
+static void server_connection_deinit(struct server_connection **_conn)
+{
+       struct server_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       if (test_server_deinit != NULL)
+               test_server_deinit(conn);
+
+       ssl_iostream_destroy(&conn->ssl_iostream);
+       connection_deinit(&conn->conn);
+       pool_unref(&conn->pool);
+}
+
+static void server_connection_destroy(struct connection *_conn)
+{
+       struct server_connection *conn = (struct server_connection *)_conn;
+
+       server_connection_deinit(&conn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+       int fd;
+
+       /* accept new client */
+       fd = net_accept(fd_listen, NULL, NULL);
+       if (fd == -1)
+               return;
+       if (fd == -2)
+               i_fatal("test server: accept() failed: %m");
+
+       server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+       .input_max_size = SIZE_MAX,
+       .output_max_size = SIZE_MAX,
+       .client = FALSE
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+       .destroy = server_connection_destroy,
+       .input = server_connection_input
+};
+
+static void test_server_run(unsigned int index)
+{
+       server_index = index;
+
+       /* open server socket */
+       io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+       server_conn_list = connection_list_init(&server_connection_set,
+                                               &server_connection_vfuncs);
+
+       io_loop_run(ioloop);
+
+       /* close server socket */
+       io_remove(&io_listen);
+
+       connection_list_deinit(&server_conn_list);
+
+       ssl_iostream_context_unref(&server_ssl_ctx);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+       unsigned int index;
+       test_server_init_t server_test;
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+       int fd = net_listen(&bind_ip, bind_port, 128);
+
+       e_debug(test_event, "server listening on %u", *bind_port);
+       if (fd == -1) {
+               i_fatal("listen(%s:%u) failed: %m",
+                       net_ip2addr(&bind_ip), *bind_port);
+       }
+       return fd;
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+       master_service_deinit_forked(&master_service);
+
+       i_set_failure_prefix("SERVER[%u]: ", data->index + 1);
+
+       e_debug(test_event, "PID=%s", my_pid);
+
+       server_ssl_ctx = NULL;
+
+       test_subprocess_notify_signal_send_parent(SIGHUP);
+       ioloop = io_loop_create();
+       data->server_test(data->index);
+       io_loop_destroy(&ioloop);
+
+       e_debug(test_event, "Terminated");
+
+       i_close_fd(&fd_listen);
+       i_free(bind_ports);
+       event_unref(&test_event);
+       main_deinit();
+       return 0;
+}
+
+static int test_run_dns(test_dns_init_t dns_test)
+{
+       master_service_deinit_forked(&master_service);
+
+       test_server_ssl = FALSE;
+
+       i_set_failure_prefix("DNS: ");
+
+       e_debug(test_event, "PID=%s", my_pid);
+
+       test_subprocess_notify_signal_send_parent(SIGHUP);
+       ioloop = io_loop_create();
+       dns_test();
+       io_loop_destroy(&ioloop);
+
+       e_debug(test_event, "Terminated");
+
+       i_close_fd(&fd_listen);
+       i_free(bind_ports);
+       event_unref(&test_event);
+       main_deinit();
+       return 0;
+}
+
+static void test_run_client(test_client_init_t client_test)
+{
+       i_set_failure_prefix("CLIENT: ");
+
+       e_debug(test_event, "PID=%s", my_pid);
+
+       ioloop = io_loop_create();
+       test_client_run(client_test);
+       io_loop_destroy(&ioloop);
+
+       e_debug(test_event, "Terminated");
+}
+
+static void
+test_run_client_server(test_client_init_t client_test,
+                      test_server_init_t server_test,
+                      unsigned int server_tests_count,
+                      test_dns_init_t dns_test)
+{
+       unsigned int i;
+
+       test_server_init = NULL;
+       test_server_deinit = NULL;
+       test_server_input = NULL;
+
+       if (server_tests_count > 0) {
+               int fds[server_tests_count];
+
+               bind_ports = i_new(in_port_t, server_tests_count);
+               for (i = 0; i < server_tests_count; i++)
+                       fds[i] = test_open_server_fd(&bind_ports[i]);
+
+               for (i = 0; i < server_tests_count; i++) {
+                       struct test_server_data data;
+
+                       i_zero(&data);
+                       data.index = i;
+                       data.server_test = server_test;
+
+                       /* Fork server */
+                       fd_listen = fds[i];
+                       test_subprocess_notify_signal_reset(SIGHUP);
+                       test_subprocess_fork(test_run_server, &data, FALSE);
+                       test_subprocess_notify_signal_wait(
+                               SIGHUP, TEST_SIGNALS_DEFAULT_TIMEOUT_MS);
+                       i_close_fd(&fd_listen);
+               }
+       }
+
+       if (dns_test != NULL) {
+               int fd;
+
+               i_unlink_if_exists("./dns-test");
+               fd = net_listen_unix("./dns-test", 128);
+               if (fd == -1) {
+                       i_fatal("listen(./dns-test) failed: %m");
+               }
+
+               /* Fork DNS service */
+               fd_listen = fd;
+               test_subprocess_notify_signal_reset(SIGHUP);
+               test_subprocess_fork(test_run_dns, dns_test, FALSE);
+               test_subprocess_notify_signal_wait(
+                       SIGHUP, TEST_SIGNALS_DEFAULT_TIMEOUT_MS);
+               i_close_fd(&fd_listen);
+       }
+
+       /* Run client */
+       test_run_client(client_test);
+
+       i_unset_failure_prefix();
+       test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+       i_free(bind_ports);
+
+       i_unlink_if_exists("./dns-test");
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+       ssl_iostream_openssl_init();
+}
+
+static void main_deinit(void)
+{
+       ssl_iostream_context_cache_free();
+       ssl_iostream_openssl_deinit();
+}
+
+int main(int argc, char *argv[])
+{
+       const enum master_service_flags service_flags =
+               MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS |
+               MASTER_SERVICE_FLAG_STANDALONE |
+               MASTER_SERVICE_FLAG_STD_CLIENT |
+               MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+       const char *error;
+       int c;
+       int ret;
+
+       while ((c = getopt(argc, argv, "D")) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+
+       master_service = master_service_init("test-lua-http-client",
+                                            service_flags, &argc, &argv, "");
+       master_service_parse_option(
+               master_service, 'o', "ssl_client_require_valid_cert=no");
+       if (master_service_settings_read_simple(master_service, &error) < 0)
+               i_fatal("Error reading configuration: %s", error);
+
+       main_init();
+
+       master_service_init_finish(master_service);
+
+       test_subprocesses_init(debug);
+
+       /* listen on localhost */
+       i_zero(&bind_ip);
+       bind_ip.family = AF_INET;
+       bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+       test_event = event_create(NULL);
+       event_set_forced_debug(test_event, debug);
+
+       ret = test_run(test_functions);
+
+       event_unref(&test_event);
+
+       test_subprocesses_deinit();
+       main_deinit();
+       master_service_deinit(&master_service);
+
+       return ret;
+}
diff --git a/src/lib-lua/test-lua-http-client.lua b/src/lib-lua/test-lua-http-client.lua
new file mode 100644 (file)
index 0000000..1316688
--- /dev/null
@@ -0,0 +1,45 @@
+local json = require 'json'
+local http_client
+
+function http_request_post(url)
+  local request = http_client:request {
+    url = url,
+    method = "POST"
+  }
+
+  request:add_header("Cache-Control", "no-cache")
+  request:add_header("Content-Type", "application/x-www-form-urlencoded")
+  request:set_payload("some+foolish+payload+for+funsies")
+  local response = request:submit()
+
+  e = dovecot.event()
+
+  local status = response:status()
+  if status ~= 200 then
+    e:log_debug("HTTP error: " .. status .. " " .. response:reason())
+    e:log_debug("HTTP error response: " .. response:payload())
+    return -1
+  end
+
+  local payload = response:payload()
+  local ok, result = pcall(json.decode, payload)
+  if not ok then
+    e:log_error("Could not parse JSON response: " .. result)
+    e:log_debug("Server response: " .. payload)
+    return -2
+  end
+
+  e:log_debug("Server response: " .. json.encode(result))
+  return 0
+end
+
+function script_init()
+  http_client = dovecot.http.client({
+    max_attempts = 3,
+    connect_timeout_msecs = 2000,
+    request_timeout_msecs = 5000,
+    request_absolute_timeout_msecs = 45000,
+    dns_client_socket_path = "./dns-test"
+  })
+  return 0
+end