/*
 * Copyright 2025 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "lua_common.h"
#include "lua_caseless_table.h"
#include "lua_thread_pool.h"
#include "libserver/http/http_private.h"
#include "libutil/upstream.h"
#include "ref.h"
#include "unix-std.h"
#include "zlib.h"
#include "utlist.h"
#include "libserver/protocol_internal.h"

/* HTTP-specific caseless table builder (defined in lua_caseless_table.c) */
extern void rspamd_lua_caseless_table_push_from_http(lua_State *L,
													 khash_t(rspamd_http_headers_hash) * headers);

/***
 * @module rspamd_http
 * Rspamd HTTP module represents HTTP asynchronous client available from LUA code.
 * This module hides all complexity: DNS resolving, sessions management, zero-copy
 * text transfers and so on under the hood.
 * @example
-- Basic GET request with callback
local rspamd_http = require "rspamd_http"

local function symbol_callback(task)
	local function http_callback(err_message, code, body, headers)
		task:insert_result('SYMBOL', 1) -- task is available via closure

		if err_message then
			-- Handle error
			return
		end

		-- Process response
		if code == 200 then
			-- Process body and headers (caseless table)
			-- Lookup is case-insensitive: headers['Content-Type'] == headers['content-type']
			for name, value in headers:each() do
				-- Original header case is preserved
			end
		end
	end

	rspamd_http.request({
		task=task,
		url='http://example.com/data',
		body=task:get_content(),
		callback=http_callback,
		headers={Header='Value', OtherHeader='Value', DuplicatedHeader={'Multiple', 'Values'}},
		mime_type='text/plain',
	})
end

-- POST request with JSON body
local function post_json_example(task)
	local ucl = require "ucl"
	local data = {
		id = task:get_queue_id(),
		sender = task:get_from()[1].addr
	}

	local json_data = ucl.to_json(data)

	rspamd_http.request({
		task = task,
		url = "http://example.com/api/submit",
		method = "POST",
		body = json_data,
		headers = {['Content-Type'] = 'application/json'},
		callback = function(err, code, body, headers)
			if not err and code == 200 then
				-- Success
			end
		end
	})
end

-- Synchronous HTTP request (using coroutines)
local function sync_http_example(task)
	-- No callback makes this a synchronous call
	local err, response = rspamd_http.request({
		task = task,
		url = "http://example.com/api/data",
		method = "GET",
		timeout = 10.0
	})

	if not err then
		-- Response is a table with code, content, and headers
		if response.code == 200 then
			-- Process response.content
			return true
		end
	end
	return false
end

-- Using authentication
local function auth_example(task)
	rspamd_http.request({
		task = task,
		url = "https://example.com/api/protected",
		method = "GET",
		user = "username",
		password = "secret",
		callback = function(err, code, body, headers)
			-- Process authenticated response
		end
	})
end

-- Using HTTPS with SSL options
local function https_example(task)
	rspamd_http.request({
		task = task,
		url = "https://example.com/api/secure",
		method = "GET",
		no_ssl_verify = false, -- Verify SSL (default)
		callback = function(err, code, body, headers)
			-- Process secure response
		end
	})
end

-- Using keep-alive and gzip
local function advanced_example(task)
	rspamd_http.request({
		task = task,
		url = "http://example.com/api/data",
		method = "POST",
		body = task:get_content(),
		gzip = true, -- Compress request body
		keepalive = true, -- Use keep-alive connection
		max_size = 1024 * 1024, -- Limit response to 1MB
		callback = function(err, code, body, headers)
			-- Process response
		end
	})
end
 */

#define MAX_HEADERS_SIZE 8192

static const char *M = "rspamd lua http";

LUA_FUNCTION_DEF(http, request);

static const struct luaL_reg httplib_m[] = {
	LUA_INTERFACE_DEF(http, request),
	{"__tostring", rspamd_lua_class_tostring},
	{NULL, NULL}};

#define RSPAMD_LUA_HTTP_FLAG_TEXT (1 << 0)
#define RSPAMD_LUA_HTTP_FLAG_NOVERIFY (1 << 1)
#define RSPAMD_LUA_HTTP_FLAG_RESOLVED (1 << 2)
#define RSPAMD_LUA_HTTP_FLAG_KEEP_ALIVE (1 << 3)
#define RSPAMD_LUA_HTTP_FLAG_YIELDED (1 << 4)
#define RSPAMD_LUA_HTTP_FLAG_NO_LOCAL (1 << 5)

struct lua_http_cbdata {
	struct rspamd_http_connection *conn;
	struct rspamd_async_session *session;
	struct rspamd_symcache_dynamic_item *item;
	struct rspamd_http_message *msg;
	struct ev_loop *event_loop;
	struct rspamd_config *cfg;
	struct rspamd_task *task;
	lua_State *L;
	ev_tstamp timeout;
	/* optional per-request tuning */
	double connect_timeout;
	double ssl_timeout;
	double write_timeout;
	double read_timeout;
	double connection_ttl;
	double idle_timeout;
	unsigned int max_reuse;
	struct rspamd_cryptobox_keypair *local_kp;
	struct rspamd_cryptobox_pubkey *peer_pk;
	rspamd_inet_addr_t *addr;
	char *mime_type;
	char *host;
	char *auth;
	struct upstream *up;
	const char *url;
	gsize max_size;
	int flags;
	int fd;
	int cbref;
	struct thread_entry *thread;
	guint64 thread_generation;
	ref_entry_t ref;
};

static const double default_http_timeout = 5.0;

static struct rspamd_dns_resolver *
lua_http_global_resolver(struct ev_loop *ev_base)
{
	static struct rspamd_dns_resolver *global_resolver;

	if (global_resolver == NULL) {
		global_resolver = rspamd_dns_resolver_init(NULL, ev_base, NULL);
	}

	return global_resolver;
}

static void
lua_http_fin(gpointer arg)
{
	struct lua_http_cbdata *cbd = (struct lua_http_cbdata *) arg;

	if (cbd->cbref != -1) {
		luaL_unref(cbd->cfg->lua_state, LUA_REGISTRYINDEX, cbd->cbref);
	}

	if (cbd->conn) {
		/* Here we already have a connection, so we need to unref it */
		rspamd_http_connection_unref(cbd->conn);
	}
	else if (cbd->msg != NULL) {
		/* We need to free message */
		rspamd_http_message_unref(cbd->msg);
	}

	if (cbd->fd != -1) {
		close(cbd->fd);
	}

	if (cbd->addr) {
		rspamd_inet_address_free(cbd->addr);
	}

	if (cbd->up) {
		rspamd_upstream_unref(cbd->up);
	}

	if (cbd->mime_type) {
		g_free(cbd->mime_type);
	}

	if (cbd->auth) {
		g_free(cbd->auth);
	}

	if (cbd->host) {
		g_free(cbd->host);
	}

	if (cbd->local_kp) {
		rspamd_keypair_unref(cbd->local_kp);
	}

	if (cbd->peer_pk) {
		rspamd_pubkey_unref(cbd->peer_pk);
	}

	g_free(cbd);
}

static void
lua_http_cbd_dtor(struct lua_http_cbdata *cbd)
{
	if (cbd->thread) {
		/* Balances the retain taken when the coroutine yielded on this request */
		lua_thread_pool_release_entry(cbd->thread);
		cbd->thread = NULL;
	}

	if (cbd->session) {

		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_RESOLVED) {
			/* Event is added merely for resolved events */
			if (cbd->item) {
				rspamd_symcache_item_async_dec_check(cbd->task, cbd->item, M);
			}

			rspamd_session_remove_event(cbd->session, lua_http_fin, cbd);
		}
	}
	else {
		lua_http_fin(cbd);
	}
}

static void
lua_http_push_error(struct lua_http_cbdata *cbd, const char *err)
{
	struct lua_callback_state lcbd;
	lua_State *L;

	if (cbd->cbref == -1) {
		/* Coroutine-style call: resume the yielded thread with (err, nil) */
		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_YIELDED) {
			cbd->flags &= ~RSPAMD_LUA_HTTP_FLAG_YIELDED;

			L = cbd->thread->lua_state;
			lua_pushstring(L, err);
			lua_pushnil(L);

			if (cbd->item) {
				rspamd_symcache_set_cur_item(cbd->task, cbd->item);
			}

			lua_thread_resume_checked(cbd->thread, cbd->thread_generation, 2);
		}
		else {
			msg_info("lost HTTP error for %s in coroutines mess: %s",
					 cbd->host ? cbd->host : "unknown host", err);
		}

		return;
	}

	lua_thread_pool_prepare_callback(cbd->cfg->lua_thread_pool, &lcbd);

	L = lcbd.L;

	lua_rawgeti(L, LUA_REGISTRYINDEX, cbd->cbref);
	lua_pushstring(L, err);


	if (cbd->item) {
		rspamd_symcache_set_cur_item(cbd->task, cbd->item);
	}

	if (lua_pcall(L, 1, 0, 0) != 0) {
		msg_info("callback call failed: %s", lua_tostring(L, -1));
		lua_pop(L, 1);
	}

	lua_thread_pool_restore_callback(&lcbd);
}

static void lua_http_resume_handler(struct rspamd_http_connection *conn,
									struct rspamd_http_message *msg, const char *err);

static void
lua_http_error_handler(struct rspamd_http_connection *conn, GError *err)
{
	struct lua_http_cbdata *cbd = (struct lua_http_cbdata *) conn->ud;

	if (cbd->up) {
		rspamd_upstream_fail(cbd->up, false, err ? err->message : "unknown error");
	}

	if (cbd->cbref == -1) {
		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_YIELDED) {
			cbd->flags &= ~RSPAMD_LUA_HTTP_FLAG_YIELDED;
			lua_http_resume_handler(conn, NULL, err->message);
		}
		else {
			/* TODO: kill me please */
			msg_info("lost HTTP error from %s in coroutines mess: %s",
					 rspamd_inet_address_to_string_pretty(cbd->addr),
					 err->message);
		}
	}
	else {
		lua_http_push_error(cbd, err->message);
	}

	REF_RELEASE(cbd);
}

static int
lua_http_finish_handler(struct rspamd_http_connection *conn,
						struct rspamd_http_message *msg)
{
	struct lua_http_cbdata *cbd = (struct lua_http_cbdata *) conn->ud;
	const char *body;
	gsize body_len;

	struct lua_callback_state lcbd;
	lua_State *L;

	if (cbd->cbref == -1) {
		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_YIELDED) {
			cbd->flags &= ~RSPAMD_LUA_HTTP_FLAG_YIELDED;
			lua_http_resume_handler(conn, msg, NULL);
		}
		else {
			/* TODO: kill me please */
			msg_err("lost HTTP data from %s in coroutines mess",
					rspamd_inet_address_to_string_pretty(cbd->addr));
		}

		REF_RELEASE(cbd);

		return 0;
	}
	lua_thread_pool_prepare_callback(cbd->cfg->lua_thread_pool, &lcbd);

	if (cbd->up) {
		rspamd_upstream_ok(cbd->up);
	}

	L = lcbd.L;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	int err_idx = lua_gettop(L);

	lua_rawgeti(L, LUA_REGISTRYINDEX, cbd->cbref);
	/* Error */
	lua_pushnil(L);
	/* Reply code */
	lua_pushinteger(L, msg->code);
	/* Body */
	body = rspamd_http_message_get_body(msg, &body_len);

	if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_TEXT) {
		struct rspamd_lua_text *t;

		t = lua_newuserdata(L, sizeof(*t));
		rspamd_lua_setclass(L, rspamd_text_classname, -1);
		t->start = body;
		t->len = body_len;
		t->flags = 0;
	}
	else {
		if (body_len > 0) {
			lua_pushlstring(L, body, body_len);
		}
		else {
			lua_pushnil(L);
		}
	}
	/* Headers as caseless table */
	rspamd_lua_caseless_table_push_from_http(L, msg->headers);

	if (cbd->item) {
		/* Replace watcher to deal with nested calls */
		rspamd_symcache_set_cur_item(cbd->task, cbd->item);
	}

	if (lua_pcall(L, 4, 0, err_idx) != 0) {
		msg_info("callback call failed: %s", lua_tostring(L, -1));
		lua_pop(L, 1);
	}

	lua_pop(L, 1); /* Remove traceback function */

	REF_RELEASE(cbd);

	lua_thread_pool_restore_callback(&lcbd);

	return 0;
}

/*
 * resumes yielded thread
 */
static void
lua_http_resume_handler(struct rspamd_http_connection *conn,
						struct rspamd_http_message *msg, const char *err)
{
	struct lua_http_cbdata *cbd = (struct lua_http_cbdata *) conn->ud;
	lua_State *L = cbd->thread->lua_state;
	const char *body;
	gsize body_len;

	if (err) {
		lua_pushstring(L, err);
		lua_pushnil(L);
	}
	else {
		/*
		 * 1 - nil (error)
		 * 2 - table:
		 *   code (int)
		 *   content (string)
		 *   headers (table: header -> value)
		 */
		lua_pushnil(L);// error code

		lua_createtable(L, 0, 3);

		/* code */
		lua_pushliteral(L, "code");
		lua_pushinteger(L, msg->code);
		lua_settable(L, -3);

		/* content */
		lua_pushliteral(L, "content");

		body = rspamd_http_message_get_body(msg, &body_len);
		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_TEXT) {
			struct rspamd_lua_text *t;

			t = lua_newuserdata(L, sizeof(*t));
			rspamd_lua_setclass(L, rspamd_text_classname, -1);
			t->start = body;
			t->len = body_len;
			t->flags = 0;
		}
		else {
			if (body_len > 0) {
				lua_pushlstring(L, body, body_len);
			}
			else {
				lua_pushnil(L);
			}
		}
		lua_settable(L, -3);

		/* headers as caseless table */
		lua_pushliteral(L, "headers");
		rspamd_lua_caseless_table_push_from_http(L, msg->headers);
		lua_settable(L, -3);
	}

	if (cbd->item) {
		/* Replace watcher to deal with nested calls */
		rspamd_symcache_set_cur_item(cbd->task, cbd->item);
	}

	lua_thread_resume_checked(cbd->thread, cbd->thread_generation, 2);
}

static gboolean
lua_http_make_connection(struct lua_http_cbdata *cbd)
{
	if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_NO_LOCAL) {
		struct rspamd_config *cfg = cbd->cfg;

		if (cfg == NULL && cbd->task) {
			cfg = cbd->task->cfg;
		}

		if (rspamd_ip_is_local_cfg(cfg, cbd->addr)) {
			msg_info("forbid HTTP request to local address %s (host: %s) as 'forbid_local' is set",
					 rspamd_inet_address_to_string(cbd->addr),
					 cbd->host ? cbd->host : "unknown");

			return FALSE;
		}
	}

	rspamd_inet_address_set_port(cbd->addr, cbd->msg->port);
	unsigned http_opts = RSPAMD_HTTP_CLIENT_SIMPLE;

	if (cbd->msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL) {
		http_opts |= RSPAMD_HTTP_CLIENT_SSL;
	}

	if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_KEEP_ALIVE) {
		cbd->fd = -1; /* FD is owned by keepalive connection */
		cbd->conn = rspamd_http_connection_new_client_keepalive(
			NULL, /* Default context */
			NULL,
			lua_http_error_handler,
			lua_http_finish_handler,
			http_opts,
			cbd->addr,
			cbd->host);
	}
	else {
		cbd->fd = -1;
		cbd->conn = rspamd_http_connection_new_client(
			NULL, /* Default context */
			NULL,
			lua_http_error_handler,
			lua_http_finish_handler,
			http_opts,
			cbd->addr);
	}

	if (cbd->conn) {
		if (cbd->local_kp) {
			rspamd_http_connection_set_key(cbd->conn, cbd->local_kp);
		}

		if (cbd->peer_pk) {
			rspamd_http_message_set_peer_key(cbd->msg, cbd->peer_pk);
		}

		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_NOVERIFY) {
			cbd->msg->flags |= RSPAMD_HTTP_FLAG_SSL_NOVERIFY;
		}

		if (cbd->max_size) {
			rspamd_http_connection_set_max_size(cbd->conn, cbd->max_size);
		}

		if (cbd->auth) {
			rspamd_http_message_add_header(cbd->msg, "Authorization",
										   cbd->auth);
		}

		if (cbd->session) {
			rspamd_session_add_event_full(cbd->session,
										  (event_finalizer_t) lua_http_fin, cbd, M,
										  cbd->host);
			cbd->flags |= RSPAMD_LUA_HTTP_FLAG_RESOLVED;
		}

		/* Optional per-request tuning from table (if present) */
		if (lua_type(cbd->L, 1) == LUA_TTABLE) {
			double connect_timeout = 0, ssl_timeout = 0, write_timeout = 0, read_timeout = 0;
			double connection_ttl = 0, idle_timeout = 0;
			unsigned int max_reuse = 0;
			lua_pushstring(cbd->L, "connect_timeout");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) connect_timeout = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			lua_pushstring(cbd->L, "ssl_timeout");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) ssl_timeout = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			lua_pushstring(cbd->L, "write_timeout");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) write_timeout = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			lua_pushstring(cbd->L, "read_timeout");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) read_timeout = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			rspamd_http_connection_set_timeouts(cbd->conn, connect_timeout, ssl_timeout, write_timeout, read_timeout);
			lua_pushstring(cbd->L, "connection_ttl");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) connection_ttl = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			lua_pushstring(cbd->L, "idle_timeout");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) idle_timeout = lua_tonumber(cbd->L, -1);
			lua_pop(cbd->L, 1);
			lua_pushstring(cbd->L, "max_reuse");
			lua_gettable(cbd->L, 1);
			if (lua_type(cbd->L, -1) == LUA_TNUMBER) max_reuse = lua_tointeger(cbd->L, -1);
			lua_pop(cbd->L, 1);
			rspamd_http_connection_set_keepalive_tuning(cbd->conn, connection_ttl, idle_timeout, max_reuse);
		}

		if (cbd->task) {
			cbd->conn->log_tag = cbd->task->task_pool->tag.uid;

			if (cbd->item) {
				rspamd_symcache_item_async_inc(cbd->task, cbd->item, M);
			}
		}
		else if (cbd->cfg) {
			cbd->conn->log_tag = cbd->cfg->cfg_pool->tag.uid;
		}

		struct rspamd_http_message *msg = cbd->msg;

		/* Message is now owned by a connection object */
		cbd->msg = NULL;

		return rspamd_http_connection_write_message(cbd->conn, msg,
													cbd->host, cbd->mime_type, cbd,
													cbd->timeout);
	}

	return FALSE;
}

static void
lua_http_dns_handler(struct rdns_reply *reply, gpointer ud)
{
	struct lua_http_cbdata *cbd = (struct lua_http_cbdata *) ud;
	struct rspamd_symcache_dynamic_item *item;
	struct rspamd_task *task;

	task = cbd->task;
	item = cbd->item;

	if (reply->code != RDNS_RC_NOERROR) {
		lua_http_push_error(cbd, "unable to resolve host");
		REF_RELEASE(cbd);
	}
	else {
		struct rdns_reply_entry *entry;

		DL_FOREACH(reply->entries, entry)
		{
			if (entry->type == RDNS_REQUEST_A) {
				cbd->addr = rspamd_inet_address_new(AF_INET,
													&entry->content.a.addr);
				break;
			}
			else if (entry->type == RDNS_REQUEST_AAAA) {
				cbd->addr = rspamd_inet_address_new(AF_INET6,
													&entry->content.aaa.addr);
				break;
			}
		}

		if (cbd->addr == NULL) {
			lua_http_push_error(cbd, "unable to resolve host: no records with such name");
			REF_RELEASE(cbd);
		}
		else {
			REF_RETAIN(cbd);
			if (!lua_http_make_connection(cbd)) {
				lua_http_push_error(cbd, "unable to make connection to the host");

				if (cbd->ref.refcount > 1) {
					REF_RELEASE(cbd);
				}

				REF_RELEASE(cbd);

				return;
			}
			REF_RELEASE(cbd);
		}
	}

	if (item) {
		rspamd_symcache_item_async_dec_check(task, item, M);
	}
}

static void
lua_http_push_headers(lua_State *L, struct rspamd_http_message *msg)
{
	const char *name, *value;
	int i, sz;
	int tbl = lua_gettop(L);

	/*
	 * Two accepted shapes for the headers table:
	 *  - map:  { ['Name'] = 'value', ['Name'] = {'v1', 'v2'} }  -- order undefined
	 *  - list: { {'Name', 'value'}, {'Name', {'v1', 'v2'}} }    -- order preserved
	 * The list shape sets RSPAMD_HTTP_FLAG_ORDERED_HEADERS so the HTTP client
	 * emits headers on the wire in exactly the order they are listed (used to
	 * mimic a real browser's header order). It is detected by a non-nil
	 * integer key 1 whose value is itself a table.
	 */
	lua_rawgeti(L, tbl, 1);
	if (lua_type(L, -1) == LUA_TTABLE) {
		lua_pop(L, 1);

		msg->flags |= RSPAMD_HTTP_FLAG_ORDERED_HEADERS;
		sz = rspamd_lua_table_size(L, tbl);

		for (i = 1; i <= sz; i++) {
			lua_rawgeti(L, tbl, i); /* pair { name, value } */

			if (lua_type(L, -1) == LUA_TTABLE) {
				int pair = lua_gettop(L);
				int vsz, j;

				lua_rawgeti(L, pair, 1);
				name = lua_tostring(L, -1);
				lua_pop(L, 1);

				lua_rawgeti(L, pair, 2);
				vsz = rspamd_lua_table_size(L, -1);

				if (name != NULL) {
					if (vsz != 0) {
						/* Duplicated header: value is a list of strings */
						for (j = 1; j <= vsz; j++) {
							lua_rawgeti(L, -1, j);
							value = lua_tostring(L, -1);
							if (value != NULL) {
								rspamd_http_message_add_header(msg, name, value);
							}
							lua_pop(L, 1);
						}
					}
					else {
						value = lua_tostring(L, -1);
						if (value != NULL) {
							rspamd_http_message_add_header(msg, name, value);
						}
					}
				}
				lua_pop(L, 1); /* value */
			}

			lua_pop(L, 1); /* pair */
		}
	}
	else {
		lua_pop(L, 1);

		lua_pushnil(L);
		while (lua_next(L, tbl) != 0) {

			lua_pushvalue(L, -2);
			name = lua_tostring(L, -1);
			sz = rspamd_lua_table_size(L, -2);
			if (sz != 0 && name != NULL) {
				for (i = 1; i <= sz; i++) {
					lua_rawgeti(L, -2, i);
					value = lua_tostring(L, -1);
					if (value != NULL) {
						rspamd_http_message_add_header(msg, name, value);
					}
					lua_pop(L, 1);
				}
			}
			else {
				value = lua_tostring(L, -2);
				if (name != NULL && value != NULL) {
					rspamd_http_message_add_header(msg, name, value);
				}
			}
			lua_pop(L, 2);
		}
	}
}

/***
 * @function rspamd_http.request({params...})
 * This function creates HTTP request and accepts several parameters as a table using key=value syntax.
 * Required params are:
 *
 * - `url`
 * - `task`
 *
 * In taskless mode, instead of `task` required are:
 *
 * - `ev_base`
 * - `config`
 *
 * @param {string} url specifies URL for a request in the standard URI form (e.g. 'http://example.com/path')
 * @param {function} callback specifies callback function in format  `function (err_message, code, body, headers)` that is called on HTTP request completion. if this parameter is missing, the function performs "pseudo-synchronous" call (see [Synchronous and Asynchronous API overview](/doc/developers/sync_async.html#API-example-http-module)
 * @param {task} task if called from symbol handler it is generally a good idea to use the common task objects: event base, DNS resolver and events session
 * @param {table} headers optional headers in form `[name='value']` or `[name=['value1', 'value2']]` to duplicate a header with multiple values. A list form `{{'name', 'value'}, {'name', {'value1', 'value2'}}}` is also accepted and preserves the header order on the wire
 * @param {string} mime_type MIME type of the HTTP content (for example, `text/html`)
 * @param {string/text} body full body content, can be opaque `rspamd{text}` to avoid data copying
 * @param {number} timeout floating point request timeout value in seconds (default is 5.0 seconds)
 * @param {number} max_size maximum size of the reply body in bytes (default is the `max_lua_http_response` global option, 256Mb; 0 disables the limit)
 * @param {resolver} resolver to perform DNS-requests. Usually got from either `task` or `config`
 * @param {boolean} gzip if true, body of the requests will be compressed
 * @param {boolean} no_ssl_verify disable SSL peer checks
 * @param {boolean} forbid_local refuse to connect when the destination resolves to a loopback, link-local or `local_addrs` address (SSRF protection for requests to untrusted URLs)
 * @param {boolean} keepalive enable keep-alive pool
 * @param {string} user for HTTP authentication
 * @param {string} password for HTTP authentication, only if "user" present
 * @param {number} connect_timeout optional TCP connect timeout (seconds)
 * @param {number} ssl_timeout optional SSL handshake timeout (seconds)
 * @param {number} write_timeout optional request write timeout (seconds)
 * @param {number} read_timeout optional response read timeout (seconds)
 * @param {number} connection_ttl optional absolute keep-alive connection TTL (seconds)
 * @param {number} idle_timeout optional keep-alive idle timeout override (seconds)
 * @param {number} max_reuse optional keep-alive max reuse count per connection
 * @return {boolean} `true`, in **async** mode, if a request has been successfully scheduled. If this value is `false` then some error occurred, the callback thus will not be called.
 * @return In **sync** mode `string|nil, nil|table` In sync mode  error message if any and response as table: `int` _code_, `string` _content_ and `table` _headers_ (header -> value)
 */
static int
lua_http_request(lua_State *L)
{
	LUA_TRACE_POINT;
	struct ev_loop *ev_base;
	struct rspamd_http_message *msg;
	struct lua_http_cbdata *cbd;
	struct rspamd_dns_resolver *resolver;
	struct rspamd_async_session *session = NULL;
	struct rspamd_lua_text *t;
	struct rspamd_task *task = NULL;
	struct rspamd_config *cfg = NULL;
	struct rspamd_cryptobox_pubkey *peer_key = NULL;
	struct rspamd_cryptobox_keypair *local_kp = NULL;
	struct upstream *up = NULL;
	const char *url, *lua_body;
	rspamd_fstring_t *body = NULL;
	int cbref = -1;
	gsize bodylen;
	double timeout = default_http_timeout;
	int flags = 0;
	char *mime_type = NULL;
	char *auth = NULL;
	gssize max_size = -1; /* -1 = use the global default; 0 = unlimited */
	gboolean gzip = FALSE;

	if (lua_gettop(L) >= 2) {
		/* url, callback and event_base format */
		url = luaL_checkstring(L, 1);

		if (url == NULL || lua_type(L, 2) != LUA_TFUNCTION) {
			msg_err("http request has bad params");
			lua_pushboolean(L, FALSE);
			return 1;
		}

		lua_pushvalue(L, 2);
		cbref = luaL_ref(L, LUA_REGISTRYINDEX);

		if (lua_gettop(L) >= 3 && rspamd_lua_check_udata_maybe(L, 3, rspamd_ev_base_classname)) {
			ev_base = *(struct ev_loop **) lua_touserdata(L, 3);
		}
		else {
			ev_base = NULL;
		}

		if (lua_gettop(L) >= 4 && rspamd_lua_check_udata_maybe(L, 4, rspamd_resolver_classname)) {
			resolver = *(struct rspamd_dns_resolver **) lua_touserdata(L, 4);
		}
		else {
			resolver = lua_http_global_resolver(ev_base);
		}

		if (lua_gettop(L) >= 5 && rspamd_lua_check_udata_maybe(L, 5, rspamd_session_classname)) {
			session = *(struct rspamd_async_session **) lua_touserdata(L, 5);
		}
		else {
			session = NULL;
		}

		msg = rspamd_http_message_from_url(url);

		if (msg == NULL) {
			luaL_unref(L, LUA_REGISTRYINDEX, cbref);
			msg_err("cannot create HTTP message from url %s", url);
			lua_pushboolean(L, FALSE);
			return 1;
		}
	}
	else if (lua_type(L, 1) == LUA_TTABLE) {
		lua_pushstring(L, "url");
		lua_gettable(L, 1);
		url = luaL_checkstring(L, -1);
		lua_pop(L, 1);

		if (url == NULL) {
			msg_err("cannot create HTTP message without url");
			lua_pushboolean(L, FALSE);
			return 1;
		}

		lua_pushstring(L, "callback");
		lua_gettable(L, 1);
		if (url == NULL || lua_type(L, -1) != LUA_TFUNCTION) {
			lua_pop(L, 1);
		}
		else {
			cbref = luaL_ref(L, LUA_REGISTRYINDEX);
		}

		lua_pushstring(L, "task");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TUSERDATA) {
			task = lua_check_task(L, -1);

			if (task) {
				ev_base = task->event_loop;
				resolver = task->resolver;
				session = task->s;
				cfg = task->cfg;
			}
		}
		lua_pop(L, 1);

		if (task == NULL) {
			lua_pushstring(L, "ev_base");
			lua_gettable(L, 1);
			if (rspamd_lua_check_udata_maybe(L, -1, rspamd_ev_base_classname)) {
				ev_base = *(struct ev_loop **) lua_touserdata(L, -1);
			}
			else {
				ev_base = NULL;
			}
			lua_pop(L, 1);


			lua_pushstring(L, "session");
			lua_gettable(L, 1);
			if (rspamd_lua_check_udata_maybe(L, -1, rspamd_session_classname)) {
				session = *(struct rspamd_async_session **) lua_touserdata(L, -1);
			}
			else {
				session = NULL;
			}
			lua_pop(L, 1);

			lua_pushstring(L, "config");
			lua_gettable(L, 1);
			if (rspamd_lua_check_udata_maybe(L, -1, rspamd_config_classname)) {
				cfg = *(struct rspamd_config **) lua_touserdata(L, -1);
			}
			else {
				cfg = NULL;
			}

			lua_pop(L, 1);

			lua_pushstring(L, "resolver");
			lua_gettable(L, 1);

			if (rspamd_lua_check_udata_maybe(L, -1, rspamd_resolver_classname)) {
				resolver = *(struct rspamd_dns_resolver **) lua_touserdata(L, -1);
			}
			else {
				if (cfg && cfg->dns_resolver) {
					resolver = cfg->dns_resolver;
				}
				else {
					resolver = lua_http_global_resolver(ev_base);
				}
			}
			lua_pop(L, 1);
		}

		msg = rspamd_http_message_from_url(url);
		if (msg == NULL) {
			msg_err_task_check("cannot create HTTP message from url %s", url);

			if (cbref == -1) {
				/* Coroutine callers expect (err, response), not a bare false */
				lua_pushstring(L, "cannot create HTTP message from url");
				lua_pushnil(L);
				return 2;
			}

			lua_pushboolean(L, FALSE);
			return 1;
		}

		lua_pushstring(L, "headers");
		lua_gettable(L, 1);
		if (lua_type(L, -1) == LUA_TTABLE) {
			lua_http_push_headers(L, msg);
		}
		lua_pop(L, 1);

		lua_pushstring(L, "timeout");
		lua_gettable(L, 1);
		if (lua_type(L, -1) == LUA_TNUMBER) {
			timeout = lua_tonumber(L, -1);
		}
		lua_pop(L, 1);

		lua_pushstring(L, "mime_type");
		lua_gettable(L, 1);
		if (lua_type(L, -1) == LUA_TSTRING) {
			mime_type = g_strdup(lua_tostring(L, -1));
		}
		lua_pop(L, 1);

		lua_pushstring(L, "body");
		lua_gettable(L, 1);
		if (lua_type(L, -1) == LUA_TSTRING) {
			lua_body = lua_tolstring(L, -1, &bodylen);
			body = rspamd_fstring_new_init(lua_body, bodylen);
		}
		else if (lua_type(L, -1) == LUA_TUSERDATA) {
			t = lua_check_text(L, -1);
			/* TODO: think about zero-copy possibilities */
			if (t) {
				body = rspamd_fstring_new_init(t->start, t->len);
			}
			else {
				rspamd_http_message_unref(msg);
				g_free(mime_type);

				return luaL_error(L, "invalid body argument type: %s",
								  lua_typename(L, lua_type(L, -1)));
			}
		}
		else if (lua_type(L, -1) == LUA_TTABLE) {
			gsize total_len = 0, nelts = rspamd_lua_table_size(L, -1);

			/* Calculate length and check types */
			for (gsize i = 0; i < nelts; i++) {
				lua_rawgeti(L, -1, i + 1);

				if (lua_type(L, -1) == LUA_TSTRING) {
#if LUA_VERSION_NUM >= 502
					total_len += lua_rawlen(L, -1);
#else
					total_len += lua_objlen(L, -1);
#endif
				}
				else if (lua_type(L, -1) == LUA_TUSERDATA) {
					t = lua_check_text(L, -1);

					if (t) {
						total_len += t->len;
					}
					else {
						rspamd_http_message_unref(msg);
						if (mime_type) {
							g_free(mime_type);
						}

						return luaL_error(L, "invalid body argument: %s",
										  lua_typename(L, lua_type(L, -1)));
					}
				}
				else {
					rspamd_http_message_unref(msg);
					if (mime_type) {
						g_free(mime_type);
					}

					return luaL_error(L, "invalid body argument type: %s",
									  lua_typename(L, lua_type(L, -1)));
				}

				lua_pop(L, 1);
			}

			/* Preallocate body */
			if (total_len > 0) {
				body = rspamd_fstring_sized_new(total_len);
			}
			else {
				rspamd_http_message_unref(msg);
				if (mime_type) {
					g_free(mime_type);
				}

				return luaL_error(L, "empty body specified");
			}

			/* Fill elements */
			for (gsize i = 0; i < nelts; i++) {
				lua_rawgeti(L, -1, i + 1);

				if (lua_type(L, -1) == LUA_TSTRING) {
					lua_body = lua_tolstring(L, -1, &bodylen);
					body = rspamd_fstring_append(body, lua_body, bodylen);
				}
				else {
					t = lua_check_text(L, -1);

					if (t) {
						body = rspamd_fstring_append(body, t->start, t->len);
					}
				}

				lua_pop(L, 1);
			}
		}
		else if (lua_type(L, -1) != LUA_TNONE && lua_type(L, -1) != LUA_TNIL) {
			rspamd_http_message_unref(msg);
			return luaL_error(L, "invalid body argument type: %s",
							  lua_typename(L, lua_type(L, -1)));
		}
		lua_pop(L, 1);

		lua_pushstring(L, "peer_key");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TSTRING) {
			const char *in;
			gsize inlen;

			in = lua_tolstring(L, -1, &inlen);
			peer_key = rspamd_pubkey_from_base32(in, inlen, RSPAMD_KEYPAIR_KEX);
		}

		lua_pop(L, 1);

		lua_pushstring(L, "keypair");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TTABLE) {
			ucl_object_t *kp_ucl = ucl_object_lua_import(L, -1);

			local_kp = rspamd_keypair_from_ucl(kp_ucl);
			ucl_object_unref(kp_ucl);
		}

		lua_pop(L, 1);

		lua_pushstring(L, "opaque_body");
		lua_gettable(L, 1);

		if (!!lua_toboolean(L, -1)) {
			flags |= RSPAMD_LUA_HTTP_FLAG_TEXT;
		}

		lua_pop(L, 1);

		lua_pushstring(L, "gzip");
		lua_gettable(L, 1);

		if (!!lua_toboolean(L, -1)) {
			gzip = TRUE;
		}

		lua_pop(L, 1);

		lua_pushstring(L, "no_ssl_verify");
		lua_gettable(L, 1);

		if (!!lua_toboolean(L, -1)) {
			flags |= RSPAMD_LUA_HTTP_FLAG_NOVERIFY;
		}

		lua_pop(L, 1);

		lua_pushstring(L, "forbid_local");
		lua_gettable(L, 1);

		if (!!lua_toboolean(L, -1)) {
			flags |= RSPAMD_LUA_HTTP_FLAG_NO_LOCAL;
		}

		lua_pop(L, 1);

		lua_pushstring(L, "keepalive");
		lua_gettable(L, 1);

		if (!!lua_toboolean(L, -1)) {
			flags |= RSPAMD_LUA_HTTP_FLAG_KEEP_ALIVE;
		}

		lua_pop(L, 1);

		lua_pushstring(L, "max_size");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TNUMBER) {
			lua_Integer i = lua_tointeger(L, -1);

			if (i >= 0) {
				max_size = i;
			}
			else {
				msg_err_task_check("ignore negative max_size %L in HTTP request to %s",
								   (int64_t) i, url);
			}
		}

		lua_pop(L, 1);

		lua_pushstring(L, "method");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TSTRING) {
			rspamd_http_message_set_method(msg, lua_tostring(L, -1));
		}

		lua_pop(L, 1);

		lua_pushstring(L, "upstream");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TUSERDATA) {
			struct rspamd_lua_upstream *lup = lua_check_upstream(L, -1);

			if (lup) {
				/* Preserve pointer in case if lup is destructed */
				up = lup->up;
			}
		}

		lua_pop(L, 1);

		lua_pushstring(L, "user");
		lua_gettable(L, 1);

		if (lua_type(L, -1) == LUA_TSTRING) {
			const char *user = lua_tostring(L, -1);

			lua_pushstring(L, "password");
			lua_gettable(L, 1);

			if (lua_type(L, -1) == LUA_TSTRING) {
				const char *password = lua_tostring(L, -1);
				char *tmpbuf;
				gsize tlen;

				tlen = strlen(user) + strlen(password) + 1;
				tmpbuf = g_malloc(tlen + 1);
				rspamd_snprintf(tmpbuf, tlen + 1, "%s:%s", user, password);
				tlen *= 2;
				tlen += sizeof("Basic ") - 1;
				auth = g_malloc(tlen + 1);
				rspamd_snprintf(auth, tlen + 1, "Basic %Bs", tmpbuf);
				g_free(tmpbuf);
			}
			else {
				msg_warn("HTTP user must have password, disabling auth");
			}

			lua_pop(L, 1); /* password */
		}

		lua_pop(L, 1); /* username */
	}
	else {
		msg_err("http request has bad params");
		lua_pushboolean(L, FALSE);

		return 1;
	}

	if (session && rspamd_session_blocked(session)) {
		g_free(auth);
		rspamd_http_message_unref(msg);
		if (body) {
			rspamd_fstring_free(body);
		}
		if (local_kp) {
			rspamd_keypair_unref(local_kp);
		}

		if (cbref == -1) {
			/* Coroutine callers expect (err, response), not a bare false */
			lua_pushstring(L, "session is terminating");
			lua_pushnil(L);

			return 2;
		}

		lua_pushboolean(L, FALSE);

		return 1;
	}
	if (task == NULL && cfg == NULL) {
		g_free(auth);
		rspamd_http_message_unref(msg);
		if (body) {
			rspamd_fstring_free(body);
		}
		if (local_kp) {
			rspamd_keypair_unref(local_kp);
		}

		return luaL_error(L,
						  "Bad params to rspamd_http:request(): either task or config should be set");
	}

	if (ev_base == NULL) {
		g_free(auth);
		rspamd_http_message_unref(msg);
		if (body) {
			rspamd_fstring_free(body);
		}
		if (local_kp) {
			rspamd_keypair_unref(local_kp);
		}

		return luaL_error(L,
						  "Bad params to rspamd_http:request(): ev_base isn't passed");
	}

	cbd = g_malloc0(sizeof(*cbd));
	cbd->cbref = cbref;
	cbd->msg = msg;
	cbd->event_loop = ev_base;
	cbd->mime_type = mime_type;
	cbd->timeout = timeout;
	cbd->fd = -1;
	cbd->cfg = cfg;
	cbd->peer_pk = peer_key;
	cbd->local_kp = local_kp;
	cbd->flags = flags;
	if (max_size >= 0) {
		cbd->max_size = max_size;
	}
	else {
		cbd->max_size = cfg ? cfg->max_lua_http_response : 0;
	}
	cbd->url = url;
	cbd->auth = auth;
	cbd->task = task;
	cbd->L = L;

	if (up) {
		cbd->up = rspamd_upstream_ref(up);
	}

	if (cbd->cbref == -1) {
		cbd->thread = lua_thread_pool_get_running_entry(cfg->lua_thread_pool);
		cbd->thread_generation = cbd->thread->generation;
		/* Held for the lifetime of cbd, released in lua_http_cbd_dtor() */
		lua_thread_pool_retain_entry(cbd->thread);
	}

	REF_INIT_RETAIN(cbd, lua_http_cbd_dtor);

	if (task) {
		cbd->item = rspamd_symcache_get_cur_item(task);
	}


	if (body) {
		if (gzip) {
			if (rspamd_fstring_gzip(&body)) {
				rspamd_http_message_add_header(msg, CONTENT_ENCODING_HEADER, "gzip");
			}
		}

		rspamd_http_message_set_body_from_fstring_steal(msg, body);
	}

	if (session) {
		cbd->session = session;
	}

	bool numeric_ip = false;

	/* Check if we can skip resolving */

	gsize hostlen = 0;
	const char *host = rspamd_http_message_get_http_host(msg, &hostlen);

	if (host) {
		cbd->host = g_malloc(hostlen + 1);
		rspamd_strlcpy(cbd->host, host, hostlen + 1);

		/* Keep-alive entry is available */
		if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_KEEP_ALIVE) {
			const rspamd_inet_addr_t *ka_addr = rspamd_http_context_has_keepalive(NULL,
																				  cbd->host,
																				  msg->port,
																				  msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL);

			if (ka_addr) {
				cbd->addr = rspamd_inet_address_copy(ka_addr, NULL);
				numeric_ip = true;
			}
		}

		/*
		 * No keep-alive stuff, check if we have upstream or if we can parse host as
		 * a numeric address
		 */
		if (!cbd->addr) {
			if (cbd->up) {
				numeric_ip = true;
				cbd->addr = rspamd_inet_address_copy(rspamd_upstream_addr_next(cbd->up), NULL);
			}
			else {
				/* We use msg->host here, not cbd->host ! */
				if (rspamd_parse_inet_address(&cbd->addr,
											  msg->host->str, msg->host->len,
											  RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
					numeric_ip = true;
				}
			}
		}
	}
	else {
		if (cbd->up) {
			numeric_ip = true;
			cbd->addr = rspamd_inet_address_copy(rspamd_upstream_addr_next(cbd->up), NULL);
		}
		cbd->host = NULL;
	}

	if (numeric_ip) {
		/* Host is numeric IP, no need to resolve */
		gboolean ret;

		REF_RETAIN(cbd);
		ret = lua_http_make_connection(cbd);

		if (!ret) {
			if (cbd->up) {
				rspamd_upstream_fail(cbd->up, true, "HTTP connection failed");
			}
			if (cbd->ref.refcount > 1) {
				/* Not released by make_connection */
				REF_RELEASE(cbd);
			}

			REF_RELEASE(cbd);

			if (cbref == -1) {
				/* Coroutine callers expect (err, response), not a bare false */
				lua_pushstring(L, "cannot make HTTP connection");
				lua_pushnil(L);

				return 2;
			}

			lua_pushboolean(L, FALSE);

			return 1;
		}

		REF_RELEASE(cbd);
	}
	else {
		if (!cbd->host) {
			REF_RELEASE(cbd);

			return luaL_error(L, "no host has been specified");
		}
		if (task == NULL) {

			REF_RETAIN(cbd);
			if (!rspamd_dns_resolver_request(resolver, session, NULL, lua_http_dns_handler, cbd,
											 RDNS_REQUEST_A,
											 cbd->host)) {
				if (cbd->ref.refcount > 1) {
					/* Not released by make_connection */
					REF_RELEASE(cbd);
				}

				REF_RELEASE(cbd);

				if (cbref == -1) {
					/* Coroutine callers expect (err, response), not a bare false */
					lua_pushstring(L, "cannot make DNS request");
					lua_pushnil(L);

					return 2;
				}

				lua_pushboolean(L, FALSE);

				return 1;
			}

			REF_RELEASE(cbd);
		}
		else {
			REF_RETAIN(cbd);

			if (!rspamd_dns_resolver_request_task_forced(task, lua_http_dns_handler, cbd,
														 RDNS_REQUEST_A, cbd->host)) {
				if (cbd->ref.refcount > 1) {
					/* Not released by make_connection */
					REF_RELEASE(cbd);
				}

				REF_RELEASE(cbd);

				if (cbref == -1) {
					/* Coroutine callers expect (err, response), not a bare false */
					lua_pushstring(L, "cannot make DNS request");
					lua_pushnil(L);

					return 2;
				}

				lua_pushboolean(L, FALSE);

				return 1;
			}
			else if (cbd->item) {
				rspamd_symcache_item_async_inc(cbd->task, cbd->item, M);
			}

			REF_RELEASE(cbd);
		}
	}

	if (cbd->cbref == -1) {
		cbd->thread = lua_thread_pool_get_running_entry(cfg->lua_thread_pool);
		cbd->thread_generation = cbd->thread->generation;
		cbd->flags |= RSPAMD_LUA_HTTP_FLAG_YIELDED;

		return lua_thread_yield(cbd->thread, 0);
	}
	else {
		lua_pushboolean(L, TRUE);
	}

	return 1;
}

static int
lua_load_http(lua_State *L)
{
	lua_newtable(L);
	luaL_register(L, NULL, httplib_m);

	return 1;
}

void luaopen_http(lua_State *L)
{
	rspamd_lua_add_preload(L, "rspamd_http", lua_load_http);
}
