/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "str.h"
#include "hostpid.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "time-util.h"
#include "sleep.h"
#include "connection.h"
#include "test-common.h"
#include "test-subprocess.h"
#include "http-url.h"
#include "http-request.h"
#include "http-response-parser.h"
#include "http-server.h"

#include <unistd.h>

#define SERVER_MAX_TIMEOUT_MSECS 10*1000
#define CLIENT_KILL_TIMEOUT_SECS 20

static void main_deinit(void);

/*
 * Types
 */

struct client_connection {
	struct connection conn;

	pool_t pool;
	struct http_response_parser *resp_parser;
};

typedef void
(*test_server_init_t)(const struct http_server_settings *server_set);
typedef void (*test_client_init_t)(unsigned int index);

/*
 * State
 */

/* common */
static struct ip_addr bind_ip;
static in_port_t bind_port = 0;
static struct ioloop *ioloop;
static bool debug = FALSE;

/* server */
static struct http_server *http_server = NULL;
static struct io *io_listen;
static int fd_listen = -1;
static void (*test_server_request)(struct http_server_request *req);
static void (*test_server_request_finished)(struct http_server_request *req);

/* client */
static struct connection_list *client_conn_list;
static unsigned int client_index;
static void (*test_client_connected)(struct client_connection *conn);
static void (*test_client_input)(struct client_connection *conn);

/*
 * Forward declarations
 */

/* server */
static void test_server_defaults(struct http_server_settings *http_set);
static void test_server_run(const struct http_server_settings *http_set);

/* client */
static void client_connection_deinit(struct client_connection **_conn);
static void test_client_run(unsigned int index);

/* test*/
static void
test_run_client_server(const struct http_server_settings *server_set,
		       test_server_init_t server_test,
		       test_client_init_t client_test,
		       unsigned int client_tests_count) ATTR_NULL(3);

/*
 * Slow request
 */

/* client */

static void
test_slow_request_input(struct client_connection *conn ATTR_UNUSED)
{
	/* do nothing */
}

static void test_slow_request_connected(struct client_connection *conn)
{
	o_stream_nsend_str(conn->conn.output,
			   "GET / HTTP/1.1\r\n"
			   "Host: example.com\r\n"
			   "\r\n");
}

static void test_client_slow_request(unsigned int index)
{
	test_client_input = test_slow_request_input;
	test_client_connected = test_slow_request_connected;
	test_client_run(index);
}

/* server */

struct _slow_request {
	struct http_server_request *req;
	struct timeout *to_delay;
	bool serviced:1;
};

static void test_server_slow_request_destroyed(struct _slow_request *ctx)
{
	test_assert(ctx->serviced);
	timeout_remove(&ctx->to_delay);
	i_free(ctx);
	io_loop_stop(ioloop);
}

static void test_server_slow_request_delayed(struct _slow_request *ctx)
{
	struct http_server_response *resp;
	struct http_server_request *req = ctx->req;

	resp = http_server_response_create(req, 200, "OK");
	http_server_response_submit(resp);
	ctx->serviced = TRUE;

	http_server_request_unref(&req);
}

static void test_server_slow_request_request(struct http_server_request *req)
{
	const struct http_request *hreq = http_server_request_get(req);
	struct _slow_request *ctx;

	if (debug) {
		i_debug("REQUEST: %s %s HTTP/%u.%u",
			hreq->method, hreq->target_raw,
			hreq->version_major, hreq->version_minor);
	}

	ctx = i_new(struct _slow_request, 1);
	ctx->req = req;

	http_server_request_set_destroy_callback(
		req, test_server_slow_request_destroyed, ctx);

	http_server_request_ref(req);
	ctx->to_delay =
		timeout_add(4000, test_server_slow_request_delayed, ctx);
}

static void
test_server_slow_request(const struct http_server_settings *server_set)
{
	test_server_request = test_server_slow_request_request;
	test_server_run(server_set);
}

/* test */

static void test_slow_request(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.max_client_idle_time_msecs = 1000;

	test_begin("slow request");
	test_run_client_server(&http_server_set, test_server_slow_request,
			       test_client_slow_request, 1);
	test_end();
}

/*
 * Hanging request payload
 */

/* client */

static void
test_hanging_request_payload_connected(struct client_connection *conn)
{
	o_stream_nsend_str(conn->conn.output,
			   "GET / HTTP/1.1\r\n"
			   "Host: example.com\r\n"
			   "Content-Length: 1000\r\n"
			   "\r\n"
			   "To be continued... or not");
}

static void test_client_hanging_request_payload(unsigned int index)
{
	test_client_connected = test_hanging_request_payload_connected;
	test_client_run(index);
}

/* server */

struct _hanging_request_payload {
	struct http_server_request *req;
	struct istream *payload_input;
	struct io *io;
	bool serviced:1;
};

static void
test_server_hanging_request_payload_destroyed(
	struct _hanging_request_payload *ctx)
{
	test_assert(!ctx->serviced);
	io_remove(&ctx->io);
	i_free(ctx);
	io_loop_stop(ioloop);
}

static void
test_server_hanging_request_payload_input(struct _hanging_request_payload *ctx)
{
	struct http_server_response *resp;
	struct http_server_request *req = ctx->req;
	const unsigned char *data;
	size_t size;
	int ret;

	if (debug)
		i_debug("test server: got more payload");

	while ((ret = i_stream_read_data(ctx->payload_input,
					 &data, &size, 0)) > 0)
		i_stream_skip(ctx->payload_input, size);

	if (ret == 0)
		return;
	if (ctx->payload_input->stream_errno != 0) {
		if (debug) {
			i_debug("test server: failed to read payload: %s",
				i_stream_get_error(ctx->payload_input));
		}
		i_stream_unref(&ctx->payload_input);
		io_remove(&ctx->io);
		http_server_request_fail_close(req, 400, "Bad request");
		http_server_request_unref(&req);
		return;
	}

	i_assert(ctx->payload_input->eof);

	resp = http_server_response_create(req, 200, "OK");
	http_server_response_submit(resp);
	ctx->serviced = TRUE;

	i_stream_unref(&ctx->payload_input);
	http_server_request_unref(&req);
}

static void
test_server_hanging_request_payload_request(struct http_server_request *req)
{
	const struct http_request *hreq = http_server_request_get(req);
	struct _hanging_request_payload *ctx;

	if (debug) {
		i_debug("REQUEST: %s %s HTTP/%u.%u",
			hreq->method, hreq->target_raw,
			hreq->version_major, hreq->version_minor);
	}

	ctx = i_new(struct _hanging_request_payload, 1);
	ctx->req = req;

	http_server_request_set_destroy_callback(
		req, test_server_hanging_request_payload_destroyed, ctx);

	ctx->payload_input = http_server_request_get_payload_input(req, FALSE);

	http_server_request_ref(req);
	ctx->io = io_add_istream(ctx->payload_input,
				 test_server_hanging_request_payload_input,
				 ctx);
	test_server_hanging_request_payload_input(ctx);
}

static void
test_server_hanging_request_payload(
	const struct http_server_settings *server_set)
{
	test_server_request = test_server_hanging_request_payload_request;
	test_server_run(server_set);
}

/* test */

static void test_hanging_request_payload(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.max_client_idle_time_msecs = 1000;

	test_begin("hanging request payload");
	test_run_client_server(&http_server_set,
			       test_server_hanging_request_payload,
			       test_client_hanging_request_payload, 1);
	test_end();
}

/*
 * Hanging response payload
 */

/* client */

static void
test_hanging_response_payload_connected(struct client_connection *conn)
{
	o_stream_nsend_str(conn->conn.output,
			   "GET / HTTP/1.1\r\n"
			   "Host: example.com\r\n"
			   "Content-Length: 18\r\n"
			   "\r\n"
			   "Complete payload\r\n");
}

static void test_client_hanging_response_payload(unsigned int index)
{
	test_client_connected = test_hanging_response_payload_connected;
	test_client_run(index);
}

/* server */

struct _hanging_response_payload {
	struct http_server_request *req;
	struct istream *payload_input;
	struct io *io;
	bool serviced:1;
};

static void
test_server_hanging_response_payload_destroyed(
	struct _hanging_response_payload *ctx)
{
	test_assert(!ctx->serviced);
	io_remove(&ctx->io);
	i_free(ctx);
	io_loop_stop(ioloop);
}

static void
test_server_hanging_response_payload_request(struct http_server_request *req)
{
	const struct http_request *hreq =
		http_server_request_get(req);
	struct http_server_response *resp;
	struct _hanging_response_payload *ctx;
	string_t *payload;
	unsigned int i;

	if (debug) {
		i_debug("REQUEST: %s %s HTTP/%u.%u",
			hreq->method, hreq->target_raw,
			hreq->version_major, hreq->version_minor);
	}

	ctx = i_new(struct _hanging_response_payload, 1);
	ctx->req = req;

	http_server_request_set_destroy_callback(
		req, test_server_hanging_response_payload_destroyed, ctx);

	resp = http_server_response_create(req, 200, "OK");
	T_BEGIN {
		payload = t_str_new(204800);
		for (i = 0; i < 3200; i++) {
			str_append(payload,
				   "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
				   "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
		}

		http_server_response_set_payload_data(resp, str_data(payload),
						      str_len(payload));
	} T_END;
	http_server_response_submit(resp);
}

static void
test_server_hanging_response_payload(
	const struct http_server_settings *server_set)
{
	test_server_request = test_server_hanging_response_payload_request;
	test_server_run(server_set);
}

/* test */

static void test_hanging_response_payload(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.socket_send_buffer_size = 4096;
	http_server_set.max_client_idle_time_msecs = 1000;

	test_begin("hanging response payload");
	test_run_client_server(&http_server_set,
			       test_server_hanging_response_payload,
			       test_client_hanging_response_payload, 1);
	test_end();
}

/*
 * Excessive payload length
 */

/* client */

static void
test_excessive_payload_length_connected1(struct client_connection *conn)
{
	o_stream_nsend_str(
		conn->conn.output,
		"GET / HTTP/1.1\r\n"
		"Host: example.com\r\n"
		"Content-Length: 150\r\n"
		"\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n");
}

static void test_client_excessive_payload_length1(unsigned int index)
{
	test_client_connected = test_excessive_payload_length_connected1;
	test_client_run(index);
}

static void
test_excessive_payload_length_connected2(struct client_connection *conn)
{
	o_stream_nsend_str(
		conn->conn.output,
		"GET / HTTP/1.1\r\n"
		"Host: example.com\r\n"
		"Transfer-Encoding: chunked\r\n"
		"\r\n"
		"32\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
		"\r\n"
		"32\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
		"\r\n"
		"32\r\n"
		"Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
		"\r\n"
		"0\r\n"
		"\r\n");
}

static void test_client_excessive_payload_length2(unsigned int index)
{
	test_client_connected = test_excessive_payload_length_connected2;
	test_client_run(index);
}

/* server */

struct _excessive_payload_length {
	struct http_server_request *req;
	buffer_t *buffer;
	bool serviced:1;
};

static void
test_server_excessive_payload_length_destroyed(
	struct _excessive_payload_length *ctx)
{
	struct http_server_response *resp;
	const char *reason;
	int status;

	resp = http_server_request_get_response(ctx->req);
	test_assert(resp != NULL);
	if (resp != NULL) {
		http_server_response_get_status(resp, &status, &reason);
		test_assert(status == 413);
	}

	test_assert(!ctx->serviced);
	buffer_free(&ctx->buffer);
	i_free(ctx);
	io_loop_stop(ioloop);
}

static void
test_server_excessive_payload_length_finished(
	struct _excessive_payload_length *ctx)
{
	struct http_server_response *resp;

	resp = http_server_response_create(ctx->req, 200, "OK");
	http_server_response_submit(resp);
	ctx->serviced = TRUE;
}

static void
test_server_excessive_payload_length_request(struct http_server_request *req)
{
	const struct http_request *hreq = http_server_request_get(req);
	struct _excessive_payload_length *ctx;

	if (debug) {
		i_debug("REQUEST: %s %s HTTP/%u.%u",
			hreq->method, hreq->target_raw,
			hreq->version_major, hreq->version_minor);
	}

	ctx = i_new(struct _excessive_payload_length, 1);
	ctx->req = req;
	ctx->buffer = buffer_create_dynamic(default_pool, 128);

	http_server_request_set_destroy_callback(
		req, test_server_excessive_payload_length_destroyed, ctx);
	http_server_request_buffer_payload(
		req, ctx->buffer, 128,
		test_server_excessive_payload_length_finished, ctx);
}

static void
test_server_excessive_payload_length(
	const struct http_server_settings *server_set)
{
	test_server_request = test_server_excessive_payload_length_request;
	test_server_run(server_set);
}

/* test */

static void test_excessive_payload_length(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.max_client_idle_time_msecs = 1000;

	test_begin("excessive payload length (length)");
	test_run_client_server(&http_server_set,
			       test_server_excessive_payload_length,
			       test_client_excessive_payload_length1, 1);
	test_end();

	test_begin("excessive payload length (chunked)");
	test_run_client_server(&http_server_set,
			       test_server_excessive_payload_length,
			       test_client_excessive_payload_length2, 1);
	test_end();
}

/*
 * Response ostream disconnect
 */

/* client */

static void
test_response_ostream_disconnect_connected(struct client_connection *conn)
{
	o_stream_nsend_str(conn->conn.output,
			   "GET / HTTP/1.1\r\n"
			   "Host: example.com\r\n"
			   "Content-Length: 18\r\n"
			   "\r\n"
			   "Complete payload\r\n");
	i_sleep_intr_msecs(10);
	client_connection_deinit(&conn);
	io_loop_stop(ioloop);
}

static void test_client_response_ostream_disconnect(unsigned int index)
{
	test_client_connected = test_response_ostream_disconnect_connected;
	test_client_run(index);
}

/* server */

struct _response_ostream_disconnect {
	struct http_server_request *req;
	struct istream *payload_input;
	struct ostream *payload_output;
	struct io *io;
	bool finished:1;
	bool seen_stream_error:1;
};

static void
test_server_response_ostream_disconnect_destroyed(
	struct _response_ostream_disconnect *ctx)
{
	test_assert(ctx->seen_stream_error);
	io_remove(&ctx->io);
	i_stream_unref(&ctx->payload_input);
	i_free(ctx);
	io_loop_stop(ioloop);
}

static int
test_server_response_ostream_disconnect_output(
	struct _response_ostream_disconnect *ctx)
{
	struct ostream *output = ctx->payload_output;
	enum ostream_send_istream_result res;
	int ret;

	if (ctx->finished) {
		ret = o_stream_finish(output);
		if (ret == 0)
			return ret;
		if (ret < 0) {
			if (debug) {
				i_debug("OUTPUT ERROR: %s",
					o_stream_get_error(output));
			}
			test_assert(output->stream_errno == ECONNRESET ||
				    output->stream_errno == EPIPE);

			ctx->seen_stream_error = TRUE;
			o_stream_destroy(&ctx->payload_output);
			return -1;
		}
		return 1;
	}

	o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
	res = o_stream_send_istream(output, ctx->payload_input);
	o_stream_set_max_buffer_size(output, SIZE_MAX);

	switch (res) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		ctx->finished = TRUE;
		return test_server_response_ostream_disconnect_output(ctx);
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		if (debug)
			i_debug("WAIT OUTPUT");
		return 1;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		if (debug) {
			i_debug("OUTPUT ERROR: %s",
				o_stream_get_error(output));
		}
		test_assert(output->stream_errno == ECONNRESET ||
			    output->stream_errno == EPIPE);

		ctx->seen_stream_error = TRUE;
		o_stream_destroy(&ctx->payload_output);
		return -1;
	}
	i_unreached();
}

static void
test_server_response_ostream_disconnect_request(struct http_server_request *req)
{
	const struct http_request *hreq = http_server_request_get(req);
	struct http_server_response *resp;
	struct _response_ostream_disconnect *ctx;
	string_t *data;
	unsigned int i;

	if (debug) {
		i_debug("REQUEST: %s %s HTTP/%u.%u",
			hreq->method, hreq->target_raw,
			hreq->version_major, hreq->version_minor);
	}

	ctx = i_new(struct _response_ostream_disconnect, 1);
	ctx->req = req;

	data = str_new(default_pool, 2048000);
	for (i = 0; i < 32000; i++) {
		str_append(data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
				 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
	}
	ctx->payload_input = i_stream_create_copy_from_data(
		str_data(data), str_len(data));
	str_free(&data);

	resp = http_server_response_create(req, 200, "OK");
	ctx->payload_output = http_server_response_get_payload_output(
		resp, IO_BLOCK_SIZE, FALSE);

	o_stream_add_destroy_callback(
		ctx->payload_output,
		test_server_response_ostream_disconnect_destroyed, ctx);

	o_stream_set_flush_callback(
		ctx->payload_output,
		test_server_response_ostream_disconnect_output, ctx);
	o_stream_set_flush_pending(ctx->payload_output, TRUE);
}

static void
test_server_response_ostream_disconnect(
	const struct http_server_settings *server_set)
{
	test_server_request = test_server_response_ostream_disconnect_request;
	test_server_run(server_set);
}

/* test */

static void test_response_ostream_disconnect(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.socket_send_buffer_size = 4096;
	http_server_set.max_client_idle_time_msecs = 10000;

	test_begin("response ostream disconnect");
	test_run_client_server(&http_server_set,
			       test_server_response_ostream_disconnect,
			       test_client_response_ostream_disconnect, 1);
	test_end();
}

/*
 * Bad payload1
 */

/* client */

static void test_bad_payload_connected1(struct client_connection *conn)
{
	o_stream_nsend_str(
		conn->conn.output,
		"GET / HTTP/1.1\r\n"
		"Host: example.com\r\n"
		"Transfer-Encoding: chunked\r\n"
		"Content-Length: 13\r\n"
		"\r\n"
		"Bad payload\r\n");

	conn->resp_parser = http_response_parser_init(conn->conn.input, NULL, 0);
}

static void test_bad_payload_input1(struct client_connection *conn)
{
	struct http_response response;
	const char *error;
	int ret;

	ret = http_response_parse_next(conn->resp_parser,
				       HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED,
				       &response, &error);
	if (ret == 0)
		return;
	if (ret < 0)
		i_panic("Failed to parse server response: %s", error);

	test_assert(response.status == 400);
	io_loop_stop(current_ioloop);
}

static void test_client_bad_payload1(unsigned int index)
{
	test_client_connected = test_bad_payload_connected1;
	test_client_input = test_bad_payload_input1;
	test_client_run(index);
}

/* server */

static void
test_server_bad_payload_request(struct http_server_request *req ATTR_UNUSED)
{
	test_assert(FALSE);
}

static void
test_server_bad_payload_request_finished(struct http_server_request *req)
{
	struct http_server_response *resp;
	int status;
	const char *reason;

	resp = http_server_request_get_response(req);
	test_assert(resp != NULL);
	if (resp != NULL) {
		http_server_response_get_status(resp, &status, &reason);
		test_assert(status == 400);
	}

	io_loop_stop(ioloop);
}

static void
test_server_bad_payload(const struct http_server_settings *server_set)
{
	test_server_request = test_server_bad_payload_request;
	test_server_request_finished = test_server_bad_payload_request_finished;
	test_server_run(server_set);
}

/* test */

static void test_bad_payload(void)
{
	struct http_server_settings http_server_set;

	test_server_defaults(&http_server_set);
	http_server_set.max_client_idle_time_msecs = 1000;

	test_begin("bad payload");
	test_run_client_server(&http_server_set,
			       test_server_bad_payload,
			       test_client_bad_payload1, 1);
	test_end();
}

/*
 * All tests
 */

static void (*const test_functions[])(void) = {
	test_slow_request,
	test_hanging_request_payload,
	test_hanging_response_payload,
	test_excessive_payload_length,
	test_response_ostream_disconnect,
	test_bad_payload,
	NULL
};

/*
 * Test client
 */

/* client connection */

static void client_connection_input(struct connection *_conn)
{
	struct client_connection *conn = (struct client_connection *)_conn;

	if (test_client_input != NULL)
		test_client_input(conn);
}

static void client_connection_connected(struct connection *_conn, bool success)
{
	struct client_connection *conn = (struct client_connection *)_conn;

	if (success && test_client_connected != NULL)
		test_client_connected(conn);
}

static void client_connection_init(const struct ip_addr *ip, in_port_t port)
{
	struct client_connection *conn;
	pool_t pool;

	pool = pool_alloconly_create("client connection", 1024);
	conn = p_new(pool, struct client_connection, 1);
	conn->pool = pool;

	connection_init_client_ip(client_conn_list, &conn->conn, NULL,
				  ip, port);
	(void)connection_client_connect(&conn->conn);
}

static void client_connection_deinit(struct client_connection **_conn)
{
	struct client_connection *conn = *_conn;

	*_conn = NULL;

	if (conn->resp_parser != NULL)
		http_response_parser_deinit(&conn->resp_parser);
	connection_deinit(&conn->conn);
	pool_unref(&conn->pool);
}

static void client_connection_destroy(struct connection *_conn)
{
	struct client_connection *conn = (struct client_connection *)_conn;

	client_connection_deinit(&conn);
}

/* */

static struct connection_settings client_connection_set = {
	.input_max_size = SIZE_MAX,
	.output_max_size = SIZE_MAX,
	.client = TRUE
};

static const struct connection_vfuncs client_connection_vfuncs = {
	.destroy = client_connection_destroy,
	.client_connected = client_connection_connected,
	.input = client_connection_input
};

static void test_client_run(unsigned int index)
{
	client_index = index;

	if (debug)
		i_debug("client connecting to %u", bind_port);

	client_conn_list = connection_list_init(&client_connection_set,
						&client_connection_vfuncs);

	client_connection_init(&bind_ip, bind_port);

	io_loop_run(ioloop);

	/* close server socket */
	io_remove(&io_listen);

	connection_list_deinit(&client_conn_list);
}

/*
 * Test server
 */

static void test_server_defaults(struct http_server_settings *http_set)
{
	/* server settings */
	http_server_settings_init(null_pool, http_set);
	http_set->max_client_idle_time_msecs = 5*1000;
}

/* client connection */

static void
server_handle_request(void *context ATTR_UNUSED,
		      struct http_server_request *req)
{
	i_assert(test_server_request != NULL);
	test_server_request(req);
}

static void
server_request_finished(void *context ATTR_UNUSED,
			struct http_server_request *req)
{
	if (test_server_request_finished != NULL)
		test_server_request_finished(req);
}

struct http_server_callbacks http_server_callbacks = {
	.handle_request = server_handle_request,
	.request_finished = server_request_finished,
};

static void server_connection_accept(void *context ATTR_UNUSED)
{
	int fd;

	/* accept new client */
	fd = net_accept(fd_listen, NULL, NULL);
	if (fd == -1) {
		if (!NET_ACCEPT_ENOCONN(errno))
			i_fatal("test server: accept() failed: %m");
		return;
	}

	(void)http_server_connection_create(http_server, fd, fd, FALSE,
					    &http_server_callbacks, NULL);
}

/* */

static void test_server_timeout(void *context ATTR_UNUSED)
{
	i_fatal("Server timed out");
}

static void test_server_run(const struct http_server_settings *http_set)
{
	struct timeout *to;
	struct event *event = event_create(NULL);
	event_set_forced_debug(event, debug);

	to = timeout_add(SERVER_MAX_TIMEOUT_MSECS, test_server_timeout, NULL);

	/* open server socket */
	io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);

	http_server = http_server_init((struct http_server_settings*) http_set, event);

	io_loop_run(ioloop);

	/* close server socket */
	io_remove(&io_listen);
	timeout_remove(&to);

	http_server_deinit(&http_server);
	event_unref(&event);
}

/*
 * Tests
 */

struct test_client_data {
	unsigned int index;
	test_client_init_t client_test;
};

static int test_open_server_fd(void)
{
	int fd = net_listen(&bind_ip, &bind_port, 128);
	if (debug)
		i_debug("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_client(struct test_client_data *data)
{
	i_close_fd(&fd_listen);

	i_set_failure_prefix("CLIENT[%u]: ", data->index + 1);

	if (debug)
		i_debug("PID=%s", my_pid);

	/* Wait a little for server setup */
	i_sleep_msecs(100);

	ioloop = io_loop_create();
	data->client_test(data->index);
	io_loop_destroy(&ioloop);

	if (debug)
		i_debug("Terminated");

	main_deinit();
	return 0;
}

static void
test_run_server(const struct http_server_settings *server_set,
		test_server_init_t server_test)
{
	i_set_failure_prefix("SERVER: ");

	if (debug)
		i_debug("PID=%s", my_pid);

	ioloop = io_loop_create();
	server_test(server_set);
	io_loop_destroy(&ioloop);

	if (debug)
		i_debug("Terminated");
}

static void
test_run_client_server(const struct http_server_settings *server_set,
		       test_server_init_t server_test,
		       test_client_init_t client_test,
		       unsigned int client_tests_count)
{
	unsigned int i;

	test_client_input = NULL;
	test_client_connected = NULL;
	test_server_request = NULL;
	test_server_request_finished = NULL;

	fd_listen = test_open_server_fd();

	if (client_tests_count > 0) {
		for (i = 0; i < client_tests_count; i++) {
			struct test_client_data data;

			i_zero(&data);
			data.index = i;
			data.client_test = client_test;

			/* Fork client */
			test_subprocess_fork(test_run_client, &data, FALSE);
		}
	}

	/* Run server */
	test_run_server(server_set, server_test);

	i_unset_failure_prefix();
	i_close_fd(&fd_listen);
	test_subprocess_kill_all(CLIENT_KILL_TIMEOUT_SECS);
}

/*
 * Main
 */

static void main_init(void)
{
	/* nothing yet */
}

static void main_deinit(void)
{
	/* nothing yet; also called from sub-processes */
}

int main(int argc, char *argv[])
{
	int c;
	int ret;

	lib_init();
	main_init();

	while ((c = getopt(argc, argv, "D")) > 0) {
		switch (c) {
		case 'D':
			debug = TRUE;
			break;
		default:
			i_fatal("Usage: %s [-D]", argv[0]);
		}
	}

	test_init();
	event_set_forced_debug(test_event, debug);
	test_subprocesses_init();

	/* listen on localhost */
	i_zero(&bind_ip);
	bind_ip.family = AF_INET;
	bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);

	ret = test_run(test_functions);

	main_deinit();
	lib_deinit();

	return ret;
}
