/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "str.h"
#include "llist.h"
#include "path-util.h"
#include "hostpid.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "ostream-final-trickle.h"
#include "istream-crlf.h"
#include "iostream-temp.h"
#include "iostream-ssl.h"
#include "iostream-ssl-test.h"
#include "iostream-openssl.h"
#include "connection.h"
#include "test-common.h"
#include "test-dir.h"
#include "test-subprocess.h"
#include "http-url.h"
#include "http-request.h"
#include "http-server-private.h"
#include "http-client-private.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#define CLIENT_PROGRESS_TIMEOUT     30
#define SERVER_KILL_TIMEOUT_SECS    20

enum payload_handling {
	PAYLOAD_HANDLING_LOW_LEVEL,
	PAYLOAD_HANDLING_FORWARD,
	PAYLOAD_HANDLING_HANDLER,
};

static bool debug = FALSE;
static struct event *common_event, *client_event, *server_event;
static bool small_socket_buffers = FALSE;
static const char *failure = NULL;
static struct timeout *to_continue = NULL;
static bool files_finished = FALSE;
static bool running_continue = FALSE;
static char *tmp_dir = NULL;

static struct test_settings {
	/* client */
	bool client_blocking;
	unsigned int max_pending;
	unsigned int client_ioloop_nesting;
	bool request_100_continue;
	unsigned int parallel_clients;
	bool parallel_clients_global;
	size_t read_client_partial;
	bool client_trickle_final_byte;
	bool unknown_size;

	/* server */
	bool server_blocking;
	bool server_ostream;
	enum payload_handling server_payload_handling;
	size_t read_server_partial;
	bool server_cork;
	bool server_trickle_final_byte;
	bool preset_header;

	bool ssl;
} tset;

static struct ip_addr bind_ip;
static in_port_t bind_port = 0;
static int fd_listen = -1;
static struct ioloop *ioloop_nested = NULL;
static unsigned int ioloop_nested_first = 0;
static unsigned int ioloop_nested_last = 0;
static unsigned int ioloop_nested_depth = 0;

static void main_deinit(void);

/*
 * Test settings
 */

static void test_init_defaults(void)
{
	i_zero(&tset);
	tset.max_pending = 200;
	tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
	tset.parallel_clients = 1;
}

/*
 * Test files
 */
static const char unsafe_characters[] = "\"<>#%{}|\\^~[]` ;/?:@=&";

static ARRAY_TYPE(const_string) files;
static pool_t files_pool;

static void test_files_read_dir(const char *path)
{
	DIR *dirp;

	/* open the directory */
	if ((dirp = opendir(path)) == NULL) {
		if (errno == ENOENT || errno == EACCES)
			return;
		i_fatal("test files: "
			"failed to open directory %s: %m", path);
	}

	/* read entries */
	for (;;) {
		const char *file;
		struct dirent *dp;
		struct stat st;

		errno = 0;
		if ((dp = readdir(dirp)) == NULL)
			break;
		if (*dp->d_name == '.' ||
		    dp->d_name[strcspn(dp->d_name, unsafe_characters)] != '\0')
			continue;

		if (str_ends_with(dp->d_name, ".tmp") ||
		    str_ends_with(dp->d_name, ".log") ||
		    str_ends_with(dp->d_name, ".trs"))
			continue;

		file = t_abspath_to(dp->d_name, path);
		if (stat(file, &st) == 0) {
			if (S_ISREG(st.st_mode)) {
				file += 2; /* skip "./" */
				file = p_strdup(files_pool, file);
				array_push_back(&files, &file);
			} else if (S_ISDIR(st.st_mode)) {
				test_files_read_dir(file);
			}
		}
	}

	if (errno != 0)
		i_fatal("test files: "
			"failed to read directory %s: %m", path);

	/* Close the directory */
	if (closedir(dirp) < 0)
		e_error(common_event, "test files: "
			"failed to close directory %s: %m", path);
}

static void test_files_init(void)
{
	/* initialize file array */
	files_pool = pool_alloconly_create(
		MEMPOOL_GROWING"http_server_request", 4096);
	p_array_init(&files, files_pool, 512);

	/* obtain all filenames */
	test_files_read_dir(".");
}

static void test_files_deinit(void)
{
	pool_unref(&files_pool);
}

static struct istream *
test_file_open(const char *path, unsigned int *status_r, const char **reason_r)
	       ATTR_NULL(2, 3)
{
	int fd;

	if (status_r != NULL)
		*status_r = 200;
	if (reason_r != NULL)
		*reason_r = "OK";

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		e_debug(common_event, "test files: open(%s) failed: %m", path);

		switch (errno) {
		case EFAULT:
		case ENOENT:
			if (status_r != NULL)
				*status_r = 404;
			if (reason_r != NULL)
				*reason_r = "Not Found";
			break;
		case EISDIR:
		case EACCES:
			if (status_r != NULL)
				*status_r = 403;
			if (reason_r != NULL)
				*reason_r = "Forbidden";
			break;
		default:
			if (status_r != NULL)
				*status_r = 500;
			if (reason_r != NULL)
				*reason_r = "Internal Server Error";
		}
		return NULL;
	}

	struct istream *input = i_stream_create_fd_autoclose(&fd, 40960);
	i_stream_set_name(input, path);
	return input;
}

/*
 * Test server
 */

struct client {
	pool_t pool;
	struct client *prev, *next;

	struct http_server_connection *http_conn;
};

struct client_request {
	struct client *client;
	struct http_server_request *server_req;

	const char *path;

	struct istream *data;
	struct istream *payload_input;
	struct ostream *payload_output;
	struct io *io;

	bool all_sent:1;
};

static const struct http_server_callbacks http_callbacks;
static struct http_server *http_server;

static struct io *io_listen;
static struct client *clients;

/* location: /success */

static void client_handle_success_request(struct client_request *creq)
{
	struct http_server_request *req = creq->server_req;
	const struct http_request *hreq = http_server_request_get(req);
	struct http_server_response *resp;

	if (strcmp(hreq->method, "GET") != 0) {
		http_server_request_fail(req,
			405, "Method Not Allowed");
		return;
	}

	resp = http_server_response_create(req, 200, "OK");
	http_server_response_submit(resp);
}

/* location: /download/... */

static void
client_handle_download_request(struct client_request *creq,
			       const char *path)
{
	struct http_server_request *req = creq->server_req;
	const struct http_request *hreq = http_server_request_get(req);
	struct http_server_response *resp;
	const char *fpath, *reason;
	struct istream *fstream;
	struct ostream *output;
	unsigned int status;
	int ret;

	if (strcmp(hreq->method, "GET") != 0) {
		http_server_request_fail(req,
			405, "Method Not Allowed");
		return;
	}

	fpath = t_strconcat(".", path, NULL);

	e_debug(server_event, "download: sending payload for %s", fpath);

	fstream = test_file_open(fpath, &status, &reason);
	if (fstream == NULL) {
		http_server_request_fail(req, status, reason);
		return;
	}

	resp = http_server_response_create(req, 200, "OK");
	http_server_response_add_header(resp, "Content-Type", "text/plain");

	if (tset.server_blocking) {
		output = http_server_response_get_payload_output(
			resp, IO_BLOCK_SIZE, TRUE);

		switch (o_stream_send_istream(output, fstream)) {
		case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
		case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
			i_unreached();
		case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
			/* finish it */
			ret = o_stream_finish(output);
			i_assert(ret != 0);
			if (ret > 0)
				break;
			/* fall through */
		case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
			i_assert(output->stream_errno != 0);
			i_fatal("test server: download: "
				"write(%s) failed: %s",
				o_stream_get_name(output),
				o_stream_get_error(output));
		case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
			i_assert(fstream->stream_errno != 0);
			i_fatal("test server: download: "
				"read(%s) failed: %s",
				i_stream_get_name(fstream),
				i_stream_get_error(fstream));
		}

		e_debug(server_event, "download: "
			"finished sending blocking payload for %s"
			"(%"PRIuUOFF_T":%"PRIuUOFF_T")",
			fpath, fstream->v_offset, output->offset);

		o_stream_destroy(&output);
	} else {
		http_server_response_set_payload(resp, fstream);
		if (!tset.server_trickle_final_byte)
			http_server_response_submit(resp);
		else {
			/* close connection immediately, so ostream-delay can
			   catch bugs with too early disconnects. */
			http_server_response_submit_close(resp);
		}
		/* seeking the payload stream shouldn't affect lib-http */
		i_stream_seek(fstream, 1);
	}
	i_stream_unref(&fstream);
}

/* location: /echo */

static int client_request_echo_send_more(struct client_request *creq)
{
	struct ostream *output = creq->payload_output;
	enum ostream_send_istream_result res;
	uoff_t offset;
	int ret;

	if ((ret = o_stream_flush(output)) <= 0) {
		if (ret < 0) {
			i_fatal("test server: echo: "
				"write(%s) failed for %s (flush): %s",
				o_stream_get_name(output), creq->path,
				o_stream_get_error(output));
		}
		return ret;
	}

	if (creq->all_sent) {
		e_debug(server_event, "echo: flushed all payload for %s",
			creq->path);
		i_stream_unref(&creq->data);
		o_stream_destroy(&creq->payload_output);
		return 1;
	}

	i_assert(output != NULL);
	i_assert(creq->data != NULL);

	offset = creq->data->v_offset;
	o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
	res = o_stream_send_istream(output, creq->data);
	o_stream_set_max_buffer_size(output, SIZE_MAX);

	i_assert(creq->data->v_offset >= offset);
	e_debug(server_event, "echo: sent data for %s "
		"(sent %"PRIuUOFF_T", buffered %zu)",
		creq->path, (uoff_t)(creq->data->v_offset - offset),
		o_stream_get_buffer_used_size(output));

	switch (res) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		/* finish it */
		creq->all_sent = TRUE;
		if ((ret = o_stream_finish(output)) < 0) {
			i_fatal("test server: echo: "
				"write(%s) failed for %s (finish): %s",
				o_stream_get_name(output), creq->path,
				o_stream_get_error(output));
		}
		e_debug(server_event, "echo: finished sending payload for %s",
			creq->path);
		if (ret == 0)
			return 0;
		e_debug(server_event, "echo: flushed all payload for %s",
			creq->path);
		i_stream_unref(&creq->data);
		o_stream_destroy(&creq->payload_output);
		return 1;
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		e_debug(server_event, "echo: "
			"partially sent payload for %s", creq->path);
		return 1;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		i_fatal("test server: echo: "
			"read(%s) failed for %s: %s",
			i_stream_get_name(creq->data), creq->path,
			i_stream_get_error(creq->data));
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		i_fatal("test server: echo: "
			"write(%s) failed for %s: %s",
			o_stream_get_name(output), creq->path,
			o_stream_get_error(output));
	}
	i_unreached();
}

static void
client_request_echo_ostream_nonblocking(struct client_request *creq,
					struct http_server_response *resp,
					struct istream *data)
{
	creq->data = data;
	i_stream_ref(data);

	creq->payload_output = http_server_response_get_payload_output(
		resp, IO_BLOCK_SIZE, FALSE);
	if (tset.server_cork)
		o_stream_cork(creq->payload_output);
	o_stream_set_flush_callback(creq->payload_output,
				    client_request_echo_send_more, creq);
	o_stream_set_flush_pending(creq->payload_output, TRUE);
}

static void
client_request_echo_blocking(struct client_request *creq,
			     struct http_server_response *resp,
			     struct istream *input)
{
	const unsigned char *data;
	size_t size;
	int ret;

	while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
		ret = http_server_response_send_payload(&resp, data, size);
		i_assert(ret <= 0);
		if (ret < 0)
			break;
		i_stream_skip(input, size);
	}
	i_assert(ret < 0);
	if (input->stream_errno != 0) {
		i_fatal("test server: echo: "
			"read(%s) failed for %s: %s",
			i_stream_get_name(input), creq->path,
			i_stream_get_error(input));
	} else if (i_stream_have_bytes_left(input)) {
		i_fatal("test server: echo: "
			"failed to send all blocking payload for %s",
			creq->path);
	}

	/* finish it */
	if (http_server_response_finish_payload(&resp) < 0) {
		i_fatal("test server: echo: "
			"failed to finish blocking payload for %s", creq->path);
	}

	e_debug(server_event, "echo: sent all payload for %s", creq->path);
}

static void
client_request_echo_ostream_blocking(struct client_request *creq,
				     struct http_server_response *resp,
				     struct istream *input)
{
	struct ostream *payload_output;
	int ret;

	payload_output = http_server_response_get_payload_output(
		resp, IO_BLOCK_SIZE, TRUE);

	if (tset.server_cork)
		o_stream_cork(payload_output);

	switch (o_stream_send_istream(payload_output, input)) {
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		/* finish it */
		ret = o_stream_finish(payload_output);
		i_assert(ret != 0);
		if (ret > 0)
			break;
		/* fall through */
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		i_assert(payload_output->stream_errno != 0);
		i_fatal("test server: echo: "
			"write(%s) failed for %s: %s",
			o_stream_get_name(payload_output), creq->path,
			o_stream_get_error(payload_output));
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		i_assert(input->stream_errno != 0);
		i_fatal("test server: echo: "
			"read(%s) failed for %s: %s",
			i_stream_get_name(input), creq->path,
			i_stream_get_error(input));
	}

	e_debug(server_event, "echo: sent all payload for %s", creq->path);

	o_stream_destroy(&payload_output);
}

static void client_request_finish_payload_in(struct client_request *creq)
{
	struct http_server_response *resp;
	struct istream *payload_input;

	payload_input =
		iostream_temp_finish(&creq->payload_output, 4096);

	e_debug(server_event, "echo: finished receiving payload for %s",
		creq->path);

	resp = http_server_response_create(creq->server_req, 200, "OK");
	http_server_response_add_header(resp, "Content-Type", "text/plain");

	if (tset.server_ostream) {
		client_request_echo_ostream_nonblocking(creq, resp,
							payload_input);
	} else {
		http_server_response_set_payload(resp, payload_input);
		http_server_response_submit(resp);
		/* seeking the payload stream shouldn't affect lib-http */
		i_stream_seek(payload_input, 1);
	}

	i_stream_unref(&payload_input);
}

static void client_request_read_echo(struct client_request *creq)
{
	enum ostream_send_istream_result res;

	o_stream_set_max_buffer_size(creq->payload_output, IO_BLOCK_SIZE);
	res = o_stream_send_istream(creq->payload_output, creq->payload_input);
	o_stream_set_max_buffer_size(creq->payload_output, SIZE_MAX);

	switch (res) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		break;
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		return;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		i_fatal("test server: echo: "
			"Failed to read all echo payload [%s]",
			creq->path);
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		i_fatal("test server: echo: "
			"Failed to write all echo payload [%s]",
			creq->path);
	}

	client_request_finish_payload_in(creq);
	i_stream_unref(&creq->payload_input);
}

static void client_request_read_echo_more(struct client_request *creq)
{
	client_request_read_echo(creq);

	if (creq->payload_input != NULL)
		return;

	io_remove(&creq->io);

	e_debug(server_event, "echo: finished receiving payload for %s",
		creq->path);
}

static void
client_handle_echo_request(struct client_request *creq,
			   const char *path)
{
	struct http_server_request *req = creq->server_req;
	const struct http_request *hreq = http_server_request_get(req);
	struct http_server_response *resp;
	struct ostream *payload_output;
	uoff_t size;

	creq->path = p_strdup(http_server_request_get_pool(req), path);

	if (strcmp(hreq->method, "PUT") != 0) {
		http_server_request_fail(req,
			405, "Method Not Allowed");
		return;
	}

	size = 0;
	if (http_request_get_payload_size(hreq, &size) > 0 && size == 0) {
		e_debug(server_event, "echo: empty payload for %s", creq->path);

		resp = http_server_response_create(creq->server_req, 200, "OK");
		http_server_response_add_header(
			resp, "Content-Type", "text/plain");
		http_server_response_submit(resp);
		return;
	}

	payload_output = iostream_temp_create(
		t_strconcat(tmp_dir, "/payload.tmp.", NULL), 0);

	if (tset.server_blocking) {
		struct istream *payload_input;

		payload_input =
			http_server_request_get_payload_input(req, TRUE);

		if (tset.read_server_partial > 0) {
			struct istream *partial =
				i_stream_create_limit(payload_input,
						      tset.read_server_partial);
			i_stream_unref(&payload_input);
			payload_input = partial;
		}

		if (o_stream_send_istream(payload_output, payload_input) !=
			OSTREAM_SEND_ISTREAM_RESULT_FINISHED) {
			i_fatal("test server: echo: "
				"failed to receive blocking echo payload");
		}
		i_stream_unref(&payload_input);

		payload_input = iostream_temp_finish(&payload_output, 4096);

		e_debug(server_event, "echo: "
			"finished receiving blocking payload for %s",
			path);

		resp = http_server_response_create(req, 200, "OK");
		http_server_response_add_header(resp,
			"Content-Type", "text/plain");

		if (tset.server_ostream) {
			client_request_echo_ostream_blocking(creq, resp,
							     payload_input);
		} else {
			client_request_echo_blocking(creq, resp, payload_input);
		}
		i_stream_unref(&payload_input);
	} else {
		creq->payload_output = payload_output;

		switch (tset.server_payload_handling) {
		case PAYLOAD_HANDLING_LOW_LEVEL:
			creq->payload_input =
				http_server_request_get_payload_input(req, FALSE);

			if (tset.read_server_partial > 0) {
				struct istream *partial =
					i_stream_create_limit(creq->payload_input,
							      tset.read_server_partial);
				i_stream_unref(&creq->payload_input);
				creq->payload_input = partial;
			}

			creq->io = io_add_istream(creq->payload_input,
					 client_request_read_echo_more, creq);
			client_request_read_echo_more(creq);
			break;
		case PAYLOAD_HANDLING_FORWARD:
			http_server_request_forward_payload(req,
				payload_output, SIZE_MAX,
				client_request_finish_payload_in, creq);
			break;
		case PAYLOAD_HANDLING_HANDLER:
			creq->payload_input =
				http_server_request_get_payload_input(req, FALSE);
			http_server_request_handle_payload(req,
				client_request_read_echo, creq);
			break;
		}
	}
}

/* request */

static void http_server_request_destroyed(struct client_request *creq);

static struct client_request *
client_request_init(struct client *client,
		    struct http_server_request *req)
{
	struct client_request *creq;
	pool_t pool = http_server_request_get_pool(req);

	http_server_request_ref(req);

	creq = p_new(pool, struct client_request, 1);
	creq->client = client;
	creq->server_req = req;

	http_server_request_set_destroy_callback(req,
		http_server_request_destroyed, creq);

	return creq;
}

static void client_request_deinit(struct client_request **_creq)
{
	struct client_request *creq = *_creq;
	struct http_server_request *req = creq->server_req;

	*_creq = NULL;

	i_stream_unref(&creq->data);
	i_stream_unref(&creq->payload_input);
	io_remove(&creq->io);

	http_server_request_unref(&req);
}

static void http_server_request_destroyed(struct client_request *creq)
{
	client_request_deinit(&creq);
}

static void
client_handle_request(void *context,
		      struct http_server_request *req)
{
	const struct http_request *hreq = http_server_request_get(req);
	const char *path = hreq->target.url->path, *p;
	struct client *client = (struct client *)context;
	struct client_request *creq;

	i_assert(hreq->target.url->have_ssl == tset.ssl);

	e_debug(server_event, "request method=`%s' path=`%s'",
		hreq->method, path);

	creq = client_request_init(client, req);

	if (tset.preset_header) {
		http_server_request_add_response_header(req,
			"X-Dovecot-Test", "Payload");
	}

	if (strcmp(path, "/success") == 0) {
		client_handle_success_request(creq);
		return;
	}

	if ((p = strchr(path+1, '/')) == NULL) {
		http_server_request_fail(req, 404, "Not found");
		return;
	}
	if (strncmp(path, "/download", p-path) == 0) {
		client_handle_download_request(creq, p);
		return;
	}
	if (strncmp(path, "/echo", p-path) == 0) {
		client_handle_echo_request(creq, p);
		return;
	}

	http_server_request_fail(req, 404, "Not found");
	return;
}

/* client connection */

static void client_connection_destroy(void *context, const char *reason);

static const struct http_server_callbacks http_callbacks = {
	.connection_destroy = client_connection_destroy,
	.handle_request = client_handle_request,
};

static void client_init(int fd)
{
	struct client *client;
	pool_t pool;

	net_set_nonblock(fd, TRUE);

	pool = pool_alloconly_create("client", 512);
	client = p_new(pool, struct client, 1);
	client->pool = pool;

	client->http_conn = http_server_connection_create(
		http_server, fd, fd, tset.ssl, &http_callbacks, client);
	DLLIST_PREPEND(&clients, client);
}

static void client_deinit(struct client **_client)
{
	struct client *client = *_client;

	*_client = NULL;

	DLLIST_REMOVE(&clients, client);

	if (client->http_conn != NULL) {
		http_server_connection_close(&client->http_conn,
					     "deinit");
	}
	pool_unref(&client->pool);
}

static void
client_connection_destroy(void *context, const char *reason ATTR_UNUSED)
{
	struct client *client = context;

	client->http_conn = NULL;
	client_deinit(&client);
}

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

	for (;;) {
		/* accept new client */
		if ((fd = net_accept(fd_listen, NULL, NULL)) < 0) {
			if (errno == EAGAIN)
				break;
			if (errno == ECONNABORTED)
				continue;
			i_fatal("test server: accept() failed: %m");
		}

		client_init(fd);
	}
}

static void test_http_server_connection_init(struct connection *conn)
{
	if (!tset.server_trickle_final_byte)
		return;
	struct ostream *output = o_stream_create_final_trickle(conn->output);
	o_stream_unref(&conn->output);
	conn->output = output;
}

/* */

static void test_server_init(const struct http_server_settings *server_set,
			     const struct ssl_iostream_settings *ssl)
{
	/* open server socket */
	io_listen = io_add(fd_listen, IO_READ, client_accept, NULL);

	http_server = http_server_init(server_set, server_event);
	http_server_set_ssl_settings(http_server, ssl);
	http_server->conn_list->v.init = test_http_server_connection_init;
}

static void test_server_deinit(void)
{
	/* close server socket */
	io_remove(&io_listen);

	/* deinitialize */
	http_server_deinit(&http_server);
}

/*
 * Test client
 */

struct test_client_request {
	int refcount;

	struct test_client_request *prev, *next;
	struct http_client *client;
	struct http_client_request *hreq;

	struct io *io;
	struct istream *payload;
	struct istream *file_in, *file_out;
	unsigned int files_idx;
};

static struct http_client **http_clients;
static struct test_client_request *client_requests;
static unsigned int client_files_first, client_files_last;
struct timeout *to_client_progress = NULL;

static struct test_client_request *
test_client_request_new(struct http_client *client)
{
	struct test_client_request *tcreq;

	tcreq = i_new(struct test_client_request, 1);
	tcreq->refcount = 1;
	tcreq->client = client;
	DLLIST_PREPEND(&client_requests, tcreq);

	return tcreq;
}

static void test_client_request_ref(struct test_client_request *tcreq)
{
	tcreq->refcount++;
}

static void test_client_request_unref(struct test_client_request **_tcreq)
{
	struct test_client_request *tcreq = *_tcreq;

	*_tcreq = NULL;

	i_assert(tcreq->refcount > 0);
	if (--tcreq->refcount > 0)
		return;

	io_remove(&tcreq->io);
	i_stream_unref(&tcreq->payload);
	i_stream_unref(&tcreq->file_in);
	i_stream_unref(&tcreq->file_out);

	DLLIST_REMOVE(&client_requests, tcreq);
	i_free(tcreq);
}

static void test_client_request_destroy(struct test_client_request *tcreq)
{
	test_client_request_unref(&tcreq);
}

static void test_client_switch_ioloop(struct ioloop *prev_ioloop ATTR_UNUSED)
{
	struct test_client_request *tcreq;

	e_debug(client_event, "switch ioloop");

	if (to_continue != NULL)
		to_continue = io_loop_move_timeout(&to_continue);
	if (to_client_progress != NULL)
		to_client_progress = io_loop_move_timeout(&to_client_progress);

	for (tcreq = client_requests; tcreq != NULL;
		tcreq = tcreq->next) {
		if (tcreq->io != NULL)
			tcreq->io = io_loop_move_io(&tcreq->io);
		if (tcreq->payload != NULL)
			i_stream_switch_ioloop(tcreq->payload);
	}
}

static void test_client_progress_timeout(void *context ATTR_UNUSED)
{
	/* Terminate test due to lack of progress */
	failure = "Test is hanging";
	timeout_remove(&to_client_progress);
	io_loop_stop(current_ioloop);
}

static void test_client_init(void)
{
	i_assert(to_client_progress == NULL);
	if (!small_socket_buffers) {
		to_client_progress = timeout_add(
			CLIENT_PROGRESS_TIMEOUT*1000,
			test_client_progress_timeout, NULL);
	}

	io_loop_add_switch_callback(test_client_switch_ioloop);
}

static void test_client_deinit(void)
{
	unsigned int i;

	for (i = 0; i < tset.parallel_clients; i++)
		http_client_deinit(&http_clients[i]);
	i_free(http_clients);

	tset.parallel_clients = 1;

	io_loop_remove_switch_callback(test_client_switch_ioloop);
	timeout_remove(&to_continue);
	timeout_remove(&to_client_progress);

	http_client_global_context_free();
}

static void
(*old_test_http_client_connection_connected)(struct connection *, bool) = NULL;

static void
test_http_client_connection_connected(struct connection *conn, bool success)
{
	old_test_http_client_connection_connected(conn, success);

	if (!tset.client_trickle_final_byte)
		return;

	struct ostream *output = o_stream_create_final_trickle(conn->output);
	o_stream_unref(&conn->output);
	conn->output = output;
}

static void
test_client_create_clients(const struct http_client_settings *client_set,
			   const struct ssl_iostream_settings *ssl_client_set)
{
	struct http_client_context *http_context = NULL;
	unsigned int i;

	if (!tset.parallel_clients_global)
		http_context = http_client_context_create();

	if (tset.parallel_clients < 1)
		tset.parallel_clients = 1;
	http_clients = i_new(struct http_client *, tset.parallel_clients);
	for (i = 0; i < tset.parallel_clients; i++) {
		http_clients[i] = (tset.parallel_clients_global ?
				   http_client_init(client_set, client_event) :
				   http_client_init_shared(http_context, client_set, client_event));
		http_client_set_ssl_settings(http_clients[i], ssl_client_set);
		if (old_test_http_client_connection_connected == NULL) {
			old_test_http_client_connection_connected =
				http_clients[i]->cctx->conn_list->v.client_connected;
		}
		http_clients[i]->cctx->conn_list->v.client_connected =
			test_http_client_connection_connected;
	}

	if (http_context != NULL)
		http_client_context_unref(&http_context);
}

/* download */

static void test_client_download_continue(void);

static void test_client_download_finished(struct test_client_request *tcreq)
{
	const char **paths;
	unsigned int files_idx = tcreq->files_idx;
	unsigned int count;

	paths = array_get_modifiable(&files, &count);
	i_assert(files_idx < count);
	i_assert(client_files_first < count);
	i_assert(paths[files_idx] != NULL);

	paths[files_idx] = NULL;
	test_client_download_continue();
}

static void
test_client_download_payload_input(struct test_client_request *tcreq)
{
	struct istream *payload = tcreq->payload;
	const unsigned char *pdata, *fdata;
	size_t psize, fsize, pleft;
	off_t ret;

	if (to_client_progress != NULL)
		timeout_reset(to_client_progress);

	/* read payload */
	while ((ret = i_stream_read_more(payload, &pdata, &psize)) > 0) {
		e_debug(client_event, "download: got data for [%u] (size=%d)",
			tcreq->files_idx, (int)psize);
		/* compare with file on disk */
		pleft = psize;
		while ((ret = i_stream_read_more(tcreq->file_in,
						 &fdata, &fsize)) > 0 &&
		       pleft > 0) {
			fsize = (fsize > pleft ? pleft : fsize);
			if (memcmp(pdata, fdata, fsize) != 0) {
				i_fatal("test client: download: "
					"received data does not match file "
					"(%s, %"PRIuUOFF_T":%"PRIuUOFF_T")",
					i_stream_get_name(tcreq->file_in),
					payload->v_offset,
					tcreq->file_in->v_offset);
			}
			i_stream_skip(tcreq->file_in, fsize);
			pleft -= fsize;
			pdata += fsize;
		}
		if (ret < 0 && tcreq->file_in->stream_errno != 0) {
			i_fatal("test client: download: "
				"failed to read file: %s",
				i_stream_get_error(tcreq->file_in));
		}
		i_stream_skip(payload, psize);
	}

	if (ret == 0) {
		e_debug(client_event, "download: need more data for [%u]",
			tcreq->files_idx);
		/* we will be called again for this request */
	} else {
		(void)i_stream_read(tcreq->file_in);
		if (payload->stream_errno != 0) {
			i_fatal("test client: download: "
				"failed to read request payload: %s",
				i_stream_get_error(payload));
		} if (i_stream_have_bytes_left(tcreq->file_in)) {
			if (i_stream_read_more(tcreq->file_in,
					       &fdata, &fsize) <= 0)
				fsize = 0;
			i_fatal("test client: download: "
				"payload ended prematurely "
				"(at least %zu bytes left)", fsize);
		} else {
			e_debug(client_event, "download: "
				"finished request for [%u]",
				tcreq->files_idx);
		}

		/* finished */
		tcreq->payload = NULL;
		test_client_download_finished(tcreq);

		/* dereference payload stream; finishes the request */
		i_stream_unref(&tcreq->file_in);
		io_remove(&tcreq->io); /* holds a reference too */
		i_stream_unref(&payload);
	}
}

static void
test_client_download_response(const struct http_response *resp,
			      struct test_client_request *tcreq)
{
	const char **paths;
	const char *path;
	unsigned int count, status;
	struct istream *fstream;
	const char *reason;

	e_debug(client_event, "download: got response for [%u]",
		tcreq->files_idx);

	if (to_client_progress != NULL)
		timeout_reset(to_client_progress);

	paths = array_get_modifiable(&files, &count);
	i_assert(tcreq->files_idx < count);
	i_assert(client_files_first < count);
	path = paths[tcreq->files_idx];
	i_assert(path != NULL);

	e_debug(client_event, "download: path for [%u]: %s",
		tcreq->files_idx, path);

	fstream = test_file_open(path, &status, &reason);
	i_assert(fstream != NULL);

	if (status != resp->status) {
		i_fatal("test client: download: "
			"got wrong response for %s: %u %s "
			"(expected: %u %s)", path,
			resp->status, resp->reason, status, reason);
	}

	if (resp->status / 100 != 2) {
		e_debug(client_event, "download: "
			"HTTP request for %s failed: %u %s",
			path, resp->status, resp->reason);
		i_stream_unref(&fstream);
		test_client_download_finished(tcreq);
		return;
	}

	if (resp->payload == NULL) {
		e_debug(client_event, "download: no payload for %s [%u]",
			path, tcreq->files_idx);
		i_stream_unref(&fstream);
		test_client_download_finished(tcreq);
		return;
	}

	i_assert(fstream != NULL);
	if (tset.read_client_partial == 0) {
		i_stream_ref(resp->payload);
		tcreq->payload = resp->payload;
		tcreq->file_in = fstream;
	} else {
		struct istream *payload = resp->payload;
		tcreq->payload = i_stream_create_limit(
			payload, tset.read_client_partial);
		tcreq->file_in = i_stream_create_limit(
			fstream, tset.read_client_partial);
		i_stream_unref(&fstream);
	}

	tcreq->io = io_add_istream(tcreq->payload,
		test_client_download_payload_input, tcreq);
	test_client_download_payload_input(tcreq);
}

static void test_client_download_continue(void)
{
	struct test_client_request *tcreq;
	struct http_client_request *hreq;
	const char *const *paths;
	unsigned int count;

	paths = array_get(&files, &count);
	i_assert(client_files_first <= count);
	i_assert(client_files_last <= count);

	i_assert(client_files_first <= client_files_last);
	for (; client_files_first < client_files_last &&
		paths[client_files_first] == NULL; client_files_first++)

	e_debug(client_event, "download: received until [%u]",
		client_files_first-1);

	if (client_files_first >= count) {
		io_loop_stop(current_ioloop);
		return;
	}

	for (; (client_files_last < count &&
	        (client_files_last - client_files_first) < tset.max_pending);
	     client_files_last++) {
		struct http_client *http_client =
			http_clients[client_files_last % tset.parallel_clients];
		const char *path = paths[client_files_last];

		tcreq = test_client_request_new(http_client);
		tcreq->files_idx = client_files_last;

		e_debug(client_event, "download: retrieving %s [%u]",
			path, tcreq->files_idx);
		hreq = tcreq->hreq = http_client_request(
			http_client, "GET", net_ip2addr(&bind_ip),
			t_strconcat("/download/", path, NULL),
			test_client_download_response, tcreq);
		http_client_request_set_port(hreq, bind_port);
		http_client_request_set_ssl(hreq, tset.ssl);
		http_client_request_set_destroy_callback(
			hreq, test_client_request_destroy, tcreq);
		http_client_request_submit(hreq);
	}
}

static void test_client_download(const struct http_client_settings *client_set,
				 const struct ssl_iostream_settings *ssl_client_set)
{
	/* create client(s) */
	test_client_create_clients(client_set, ssl_client_set);

	/* start querying server */
	client_files_first = client_files_last = 0;
	test_client_download_continue();
}

/* echo */

static void test_client_echo_continue(void *context);

static void test_client_echo_finished(struct test_client_request *tcreq)
{
	unsigned int files_idx = tcreq->files_idx;
	const char **paths;
	unsigned int count;

	paths = array_get_modifiable(&files, &count);
	i_assert(files_idx < count);
	i_assert(client_files_first < count);
	i_assert(paths[files_idx] != NULL);

	if (tcreq->file_out != NULL)
		return;
	if (tcreq->file_in != NULL)
		return;

	e_debug(client_event, "echo: finished [%u]: %s",
		files_idx, paths[files_idx]);

	paths[files_idx] = NULL;
	files_finished = TRUE;
	if (!running_continue && to_continue == NULL) {
		to_continue = timeout_add_short(0,
			test_client_echo_continue, NULL);
	}
}

static void test_client_echo_payload_input(struct test_client_request *tcreq)
{
	struct istream *payload = tcreq->payload;
	const unsigned char *pdata, *fdata;
	size_t psize, fsize, pleft;
	off_t ret;

	if (to_client_progress != NULL)
		timeout_reset(to_client_progress);

	/* read payload */
	while ((ret = i_stream_read_more(payload, &pdata, &psize)) > 0) {
		e_debug(client_event, "echo: got data for [%u] (size=%d)",
			tcreq->files_idx, (int)psize);
		/* compare with file on disk */
		pleft = psize;
		while ((ret = i_stream_read_more(tcreq->file_in,
						 &fdata, &fsize)) > 0 &&
		       pleft > 0) {
			fsize = (fsize > pleft ? pleft : fsize);
			if (memcmp(pdata, fdata, fsize) != 0) {
				i_fatal("test client: echo: "
					"received data does not match file "
					"(%s, %"PRIuUOFF_T":%"PRIuUOFF_T")",
					i_stream_get_name(tcreq->file_in),
					payload->v_offset,
					tcreq->file_in->v_offset);
			}
			i_stream_skip(tcreq->file_in, fsize);
			pleft -= fsize;
			pdata += fsize;
		}
		if (ret < 0 && tcreq->file_in->stream_errno != 0) {
			i_fatal("test client: echo: "
				"failed to read file: %s",
				i_stream_get_error(tcreq->file_in));
		}
		i_stream_skip(payload, psize);
	}

	if (ret == 0) {
		e_debug(client_event, "echo: need more data for [%u]",
			tcreq->files_idx);
		/* we will be called again for this request */
	} else {
		(void)i_stream_read(tcreq->file_in);
		if (payload->stream_errno != 0) {
			i_fatal("test client: echo: "
				"failed to read request payload: %s",
				i_stream_get_error(payload));
		} if (i_stream_have_bytes_left(tcreq->file_in)) {
			if (i_stream_read_more(tcreq->file_in,
					       &fdata, &fsize) <= 0)
				fsize = 0;
			i_fatal("test client: echo: "
				"payload ended prematurely "
				"(at least %zu bytes left)", fsize);
		} else {
			e_debug(client_event, "echo: finished request for [%u]",
				tcreq->files_idx);
		}

		/* finished */
		tcreq->payload = NULL;
		i_stream_unref(&tcreq->file_in);
		test_client_echo_finished(tcreq);

		/* dereference payload stream; finishes the request */
		io_remove(&tcreq->io); /* holds a reference too */
		i_stream_unref(&payload);
	}
}

static void
test_client_echo_response(const struct http_response *resp,
			  struct test_client_request *tcreq)
{
	const char **paths;
	const char *path;
	unsigned int count, status;
	struct istream *fstream;

	e_debug(client_event, "echo: got response for [%u]",
		tcreq->files_idx);

	if (to_client_progress != NULL)
		timeout_reset(to_client_progress);

	paths = array_get_modifiable(&files, &count);
	i_assert(tcreq->files_idx < count);
	i_assert(client_files_first < count);
	path = paths[tcreq->files_idx];
	i_assert(path != NULL);

	e_debug(client_event, "echo: path for [%u]: %s",
		tcreq->files_idx, path);

	if (resp->status / 100 != 2) {
		i_fatal("test client: echo: "
			"HTTP request for %s failed: %u %s",
			path, resp->status, resp->reason);
	}

	fstream = test_file_open(path, &status, NULL);
	if (fstream == NULL) {
		i_fatal("test client: echo: failed to open %s", path);
	}

	if (tset.unknown_size) {
		struct istream *ustream;

		ustream = i_stream_create_crlf(fstream);
		i_stream_unref(&fstream);
		fstream = ustream;
	}

	if (tset.read_server_partial > 0) {
		struct istream *partial =
			i_stream_create_limit(fstream, tset.read_server_partial);
		i_stream_unref(&fstream);
		fstream = partial;
	}

	if (resp->payload == NULL) {
		// FIXME: check file is empty
		e_debug(client_event, "echo: no payload for %s [%u]",
			path, tcreq->files_idx);
		i_stream_unref(&fstream);
		test_client_echo_finished(tcreq);
		return;
	}

	i_assert(fstream != NULL);
	tcreq->file_in = fstream;

	i_stream_ref(resp->payload);
	tcreq->payload = resp->payload;
	tcreq->io = io_add_istream(resp->payload,
		test_client_echo_payload_input, tcreq);
	test_client_echo_payload_input(tcreq);
}

static void
test_client_echo_nonblocking(struct test_client_request *tcreq ATTR_UNUSED,
			     struct http_client_request *hreq,
			     struct istream *fstream)
{
	http_client_request_set_payload(hreq, fstream,
					tset.request_100_continue);
	http_client_request_submit(hreq);
	if (fstream->seekable) {
		/* seeking the payload stream shouldn't affect lib-http */
		i_stream_seek(fstream, 1);
	}
}

static void
test_client_echo_blocking(struct test_client_request *tcreq,
			  struct http_client_request *hreq,
			  struct istream *fstream)
{
	const unsigned char *data;
	size_t size;
	int ret;

	test_client_request_ref(tcreq);
	tcreq->file_out = fstream;

	while ((ret = i_stream_read_more(fstream, &data, &size)) > 0) {
		ret = http_client_request_send_payload(&hreq, data, size);
		i_assert(ret <= 0);
		if (ret < 0)
			break;
		i_stream_skip(fstream, size);
	}
	i_assert(ret < 0);
	if (fstream->stream_errno != 0) {
		i_fatal("test client: echo: "
			"read(%s) failed: %s [%u]",
			i_stream_get_name(fstream),
			i_stream_get_error(fstream),
			tcreq->files_idx);
	} else if (i_stream_have_bytes_left(fstream)) {
		i_fatal("test client: echo: "
			"failed to send all blocking payload [%u]",
			tcreq->files_idx);
	}

	/* finish it */
	if (http_client_request_finish_payload(&hreq) < 0) {
		i_fatal("test client: echo: "
			"failed to finish blocking payload [%u]",
			tcreq->files_idx);
	}
	http_client_wait(tcreq->client);

	e_debug(client_event, "echo: sent all payload [%u]", tcreq->files_idx);

	tcreq->file_out = NULL;
	test_client_echo_finished(tcreq);
	test_client_request_unref(&tcreq);
}

static void test_client_echo_continue(void *context ATTR_UNUSED)
{
	struct test_client_request *tcreq;
	struct http_client_request *hreq;
	const char **paths;
	unsigned int count, first_submitted;
	bool prev_files_finished = files_finished;

	running_continue = TRUE;
	files_finished = FALSE;
	timeout_remove(&to_continue);

	paths = array_get_modifiable(&files, &count);

	i_assert(client_files_first <= count);
	i_assert(client_files_last <= count);

	i_assert(client_files_first <= client_files_last);
	for (; client_files_first < client_files_last &&
		paths[client_files_first] == NULL; client_files_first++);

	e_debug(client_event, "echo: received until [%u/%u]",
		client_files_first-1, count);

	if (client_files_first < count) {
		const char *path = paths[client_files_first];
		e_debug(client_event, "echo: next blocking: %s [%d]",
			(path == NULL ? "none" : path), client_files_first);
	}

	if (client_files_first >= count || failure != NULL) {
		running_continue = FALSE;
		files_finished = prev_files_finished;
		io_loop_stop(current_ioloop);
		return;
	}

	first_submitted = client_files_last;
	for (; (client_files_last < count &&
	        (client_files_last - client_files_first) < tset.max_pending);
	     client_files_last++) {
		struct http_client *http_client =
			http_clients[client_files_last % tset.parallel_clients];
		struct istream *fstream;
		const char *path = paths[client_files_last];

		if (path == NULL)
			continue;

		fstream = test_file_open(path, NULL, NULL);
		if (fstream == NULL) {
			paths[client_files_last] = NULL;
			e_debug(client_event, "echo: skipping %s [%u]",
				path, client_files_last);
			continue;
		}

		e_debug(client_event, "echo: retrieving %s [%u]",
			path, client_files_last);

		if (tset.unknown_size) {
			struct istream *ustream;

			ustream = i_stream_create_crlf(fstream);
			i_stream_unref(&fstream);
			fstream = ustream;
		}

		tcreq = test_client_request_new(http_client);
		tcreq->files_idx = client_files_last;

		hreq = tcreq->hreq = http_client_request(http_client,
			"PUT", net_ip2addr(&bind_ip),
			t_strconcat("/echo/", path, NULL),
			test_client_echo_response, tcreq);
		http_client_request_set_port(hreq, bind_port);
		http_client_request_set_ssl(hreq, tset.ssl);
		http_client_request_set_destroy_callback(hreq,
			test_client_request_destroy, tcreq);

		if (!tset.client_blocking)
			test_client_echo_nonblocking(tcreq, hreq, fstream);
		else
			test_client_echo_blocking(tcreq, hreq, fstream);
		i_stream_unref(&fstream);

		if (tset.client_blocking && paths[client_files_last] != NULL) {
			running_continue = FALSE;
			files_finished = prev_files_finished;
			return;
		}
	}

	if (files_finished && to_continue == NULL) {
		to_continue = timeout_add_short(
			0, test_client_echo_continue, NULL);
	}
	running_continue = FALSE;
	files_finished = prev_files_finished;

	/* run nested ioloop (if requested) if new requests cross a nesting
	   boundary */
	if (ioloop_nested != NULL) {
		unsigned int i;

		i_assert(ioloop_nested_first <= count);
		i_assert(ioloop_nested_last <= count);
		for (i = ioloop_nested_first; i < ioloop_nested_last; i++) {
			if (paths[i] != NULL) {
				e_debug(client_event, "not leaving ioloop [%u]", i);
				break;
			}
		}

		if (i == ioloop_nested_last)
			io_loop_stop(ioloop_nested);
	} else if (tset.client_ioloop_nesting > 0 &&
		   ((client_files_last / tset.client_ioloop_nesting) !=
			(first_submitted / tset.client_ioloop_nesting))) {
		struct ioloop *prev_ioloop = current_ioloop;
		unsigned int i;

		ioloop_nested_first = first_submitted;
		ioloop_nested_last =
			first_submitted + tset.client_ioloop_nesting;
		if (ioloop_nested_last > client_files_last)
			ioloop_nested_last = client_files_last;

		e_debug(client_event,
			"echo: entering ioloop for %u...%u (depth=%u)",
			ioloop_nested_first, ioloop_nested_last,
			ioloop_nested_depth);

		ioloop_nested_depth++;

		ioloop_nested = io_loop_create();
		for (i = 0; i < tset.parallel_clients; i++)
			http_client_switch_ioloop(http_clients[i]);

		io_loop_run(ioloop_nested);

		io_loop_set_current(prev_ioloop);
		for (i = 0; i < tset.parallel_clients; i++)
			http_client_switch_ioloop(http_clients[i]);
		io_loop_set_current(ioloop_nested);
		io_loop_destroy(&ioloop_nested);
		ioloop_nested = NULL;

		ioloop_nested_depth--;

		e_debug(client_event, "echo: leaving ioloop for %u...%u "
			"(depth=%u)", ioloop_nested_first,
			ioloop_nested_last, ioloop_nested_depth);
		ioloop_nested_first = ioloop_nested_last = 0;

		if (client_files_first >= count || failure != NULL) {
			io_loop_stop(current_ioloop);
			return;
		}
	}
}

static void test_client_echo(const struct http_client_settings *client_set,
			     const struct ssl_iostream_settings *ssl_client_set)
{
	/* create client */
	test_client_create_clients(client_set, ssl_client_set);

	/* start querying server */
	client_files_first = client_files_last = 0;

	i_assert(to_continue == NULL);
	to_continue = timeout_add_short(0, test_client_echo_continue, NULL);
}

/*
 * Tests
 */

struct test_server_data {
	const struct http_server_settings *set;
	const struct ssl_iostream_settings *ssl_set;
};

static void test_open_server_fd(void)
{
	i_close_fd(&fd_listen);
	fd_listen = net_listen(&bind_ip, &bind_port, 128);
	if (fd_listen == -1) {
		i_fatal("listen(%s:%u) failed: %m",
			net_ip2addr(&bind_ip), bind_port);
	}
	net_set_nonblock(fd_listen, TRUE);
}

static int test_run_server(struct test_server_data *data)
{
	const struct http_server_settings *server_set = data->set;
	const struct ssl_iostream_settings *ssl_set = data->ssl_set;
	struct ioloop *ioloop;

	i_set_failure_prefix("SERVER: ");

	e_debug(common_event, "PID=%s", my_pid);

	ioloop_nested = NULL;
	ioloop_nested_depth = 0;
	ioloop = io_loop_create();
	test_server_init(server_set, ssl_set);
	io_loop_run(ioloop);
	test_server_deinit();
	io_loop_destroy(&ioloop);

	e_debug(common_event, "Terminated");

	i_close_fd(&fd_listen);
	test_files_deinit();
	main_deinit();
	return 0;
}

static void
test_run_client(
	const struct http_client_settings *client_set,
	const struct ssl_iostream_settings *ssl_client_set,
	void (*client_init)(const struct http_client_settings *client_set,
			    const struct ssl_iostream_settings *ssl_client_set))
{
	struct ioloop *ioloop;

	i_set_failure_prefix("CLIENT: ");

	e_debug(common_event, "PID=%s", my_pid);

	ioloop_nested = NULL;
	ioloop_nested_depth = 0;
	ioloop = io_loop_create();
	test_client_init();
	client_init(client_set, ssl_client_set);
	io_loop_run(ioloop);
	test_client_deinit();
	io_loop_destroy(&ioloop);

	e_debug(common_event, "Terminated");
}

static void
test_run_client_server(
	const struct http_client_settings *client_set,
	const struct ssl_iostream_settings *ssl_client_set,
	const struct http_server_settings *server_set,
	const struct ssl_iostream_settings *ssl_server_set,
	void (*client_init)(const struct http_client_settings *client_set,
			    const struct ssl_iostream_settings *ssl_client_set))
{
	struct test_server_data data;

	failure = NULL;

	test_files_init();

	i_zero(&data);
	data.set = server_set;
	data.ssl_set = ssl_server_set;

	/* Fork server */
	test_open_server_fd();
	test_subprocess_fork(test_run_server, &data, FALSE);
	i_close_fd(&fd_listen);

	/* Run client */
	test_run_client(client_set, ssl_client_set, client_init);

	i_unset_failure_prefix();
	test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
	test_files_deinit();
}

static void
test_init_server_settings(struct http_server_settings *server_set_r)
{
	http_server_settings_init(null_pool, server_set_r);
	server_set_r->request_max_payload_size = UOFF_T_MAX;

	if (small_socket_buffers) {
		server_set_r->socket_send_buffer_size = 40960;
		server_set_r->socket_recv_buffer_size = 40960;
	}
}

static void
test_init_client_settings(struct http_client_settings *client_set_r)
{
	http_client_settings_init(null_pool, client_set_r);
	client_set_r->request_max_redirects = 0;
	client_set_r->request_max_attempts = 1;
	client_set_r->max_idle_time_msecs =  5* 1000;

	if (small_socket_buffers) {
		client_set_r->socket_send_buffer_size = 40960;
		client_set_r->socket_recv_buffer_size = 40960;
		client_set_r->request_timeout_msecs = 20 * 60 * 1000;
		client_set_r->connect_timeout_msecs = 20 * 60 * 1000;
	}
}

static void
test_run_sequential(
	void (*client_init)(const struct http_client_settings *client_set,
			    const struct ssl_iostream_settings *ssl_client_set))
{
	struct http_server_settings http_server_set;
	struct http_client_settings http_client_set;
	struct ssl_iostream_settings ssl_server_set, ssl_client_set;

	/* download files from blocking server */

	/* ssl settings */
	ssl_iostream_test_settings_server(&ssl_server_set);
	ssl_iostream_test_settings_client(&ssl_client_set);

	/* server settings */
	test_init_server_settings(&http_server_set);

	/* client settings */
	test_init_client_settings(&http_client_set);
	http_client_set.max_parallel_connections = 1;
	http_client_set.max_pipelined_requests = 1;

	test_run_client_server(&http_client_set, &ssl_client_set,
			       &http_server_set, &ssl_server_set,
			       client_init);
	ssl_iostream_context_cache_free();

	test_out_reason("sequential", (failure == NULL), failure);
}

static void
test_run_pipeline(
	void (*client_init)(const struct http_client_settings *client_set,
			    const struct ssl_iostream_settings *ssl_client_set))
{
	struct http_server_settings http_server_set;
	struct http_client_settings http_client_set;
	struct ssl_iostream_settings ssl_server_set, ssl_client_set;

	/* download files from blocking server */

	/* ssl settings */
	ssl_iostream_test_settings_server(&ssl_server_set);
	ssl_iostream_test_settings_client(&ssl_client_set);

	/* server settings */
	test_init_server_settings(&http_server_set);
	http_server_set.max_pipelined_requests = 4;

	/* client settings */
	test_init_client_settings(&http_client_set);
	http_client_set.max_parallel_connections = 1;
	http_client_set.max_pipelined_requests = 8;

	test_run_client_server(&http_client_set, &ssl_client_set,
			       &http_server_set, &ssl_server_set,
			       client_init);
	ssl_iostream_context_cache_free();

	test_out_reason("pipeline", (failure == NULL), failure);
}

static void
test_run_parallel(
	void (*client_init)(const struct http_client_settings *client_set,
			    const struct ssl_iostream_settings *ssl_client_set))
{
	struct http_server_settings http_server_set;
	struct http_client_settings http_client_set;
	struct ssl_iostream_settings ssl_server_set, ssl_client_set;

	/* download files from blocking server */

	/* ssl settings */
	ssl_iostream_test_settings_server(&ssl_server_set);
	ssl_iostream_test_settings_client(&ssl_client_set);

	/* server settings */
	test_init_server_settings(&http_server_set);
	http_server_set.max_pipelined_requests = 4;

	/* client settings */
	test_init_client_settings(&http_client_set);
	http_client_set.max_parallel_connections = 40;
	http_client_set.max_pipelined_requests = 8;

	test_run_client_server(&http_client_set, &ssl_client_set,
			       &http_server_set, &ssl_server_set,
			       client_init);
	ssl_iostream_context_cache_free();

	test_out_reason("parallel", (failure == NULL), failure);
}

static void test_download_server_nonblocking(void)
{
	test_begin("http payload download (server non-blocking)");
	test_init_defaults();
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();

	test_begin("http payload download (server non-blocking; client trickle)");
	test_init_defaults();
	tset.client_trickle_final_byte = TRUE;
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();

	test_begin("http payload download (server non-blocking; server trickle)");
	test_init_defaults();
	tset.server_trickle_final_byte = TRUE;
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();
}

static void test_download_server_blocking(void)
{
	test_begin("http payload download (server blocking)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();
}

static void test_echo_server_nonblocking(void)
{
	test_begin("http payload echo "
		   "(server non-blocking)");
	test_init_defaults();
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; low-level)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; handler)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; size unknown)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; size unknown; client trickle)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
	tset.client_trickle_final_byte = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; size unknown; server trickle)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
	tset.server_trickle_final_byte = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; ostream)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; ostream; cork)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_server_blocking(void)
{
	test_begin("http payload echo (server blocking)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (server blocking; ostream)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.server_ostream = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (server blocking; ostream; cork)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.server_blocking = TRUE;
	tset.server_cork = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_server_nonblocking_sync(void)
{
	test_begin("http payload echo "
		   "(server non-blocking; 100-continue)");
	test_init_defaults();
	tset.request_100_continue = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; 100-continue; low-level)");
	test_init_defaults();
	tset.request_100_continue = TRUE;
	tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; 100-continue; handler)");
	test_init_defaults();
	tset.request_100_continue = TRUE;
	tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; 100-continue; preset header)");
	test_init_defaults();
	tset.request_100_continue = TRUE;
	tset.preset_header = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_server_blocking_sync(void)
{
	test_begin("http payload echo (server blocking; 100-continue)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.request_100_continue = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server blocking; ostream; 100-continue)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.server_ostream = TRUE;
	tset.request_100_continue = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_server_nonblocking_partial(void)
{
	test_begin("http payload echo "
		   "(server non-blocking; partial short)");
	test_init_defaults();
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server non-blocking; partial long)");
	test_init_defaults();
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (server non-blocking; "
		   "partial short; low-level)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server non-blocking; partial long; low-level)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; partial short; handler)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server non-blocking; partial long; handler)");
	test_init_defaults();
	tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; partial short; ostream)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server non-blocking; partial long; ostream)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; partial short; ostream; corked)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server non-blocking; partial long; ostream; corked)");
	test_init_defaults();
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_server_blocking_partial(void)
{
	test_begin("http payload echo (server blocking; partial short)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo (server blocking; partial long)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server blocking; partial short; ostream; cork)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	tset.read_server_partial = 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
	test_begin("http payload echo "
		   "(server blocking; partial long; ostream; cork)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	tset.read_server_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_download_client_partial(void)
{
	test_begin("http payload download (client partial)");
	test_init_defaults();
	tset.read_client_partial = 1024;
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();
	test_begin("http payload download (client partial long)");
	test_init_defaults();
	tset.read_client_partial = IO_BLOCK_SIZE + 1024;
	test_run_sequential(test_client_download);
	test_run_pipeline(test_client_download);
	test_run_parallel(test_client_download);
	test_end();
}

static void test_download_client_nested_ioloop(void)
{
	test_begin("http payload echo (client nested ioloop)");
	test_init_defaults();
	tset.client_ioloop_nesting = 10;
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_client_shared(void)
{
	test_begin("http payload download "
		   "(server non-blocking; client shared)");
	test_init_defaults();
	tset.parallel_clients = 4;
	test_run_sequential(test_client_download);
	tset.parallel_clients = 4;
	test_run_pipeline(test_client_download);
	tset.parallel_clients = 4;
	test_run_parallel(test_client_download);
	test_end();

	test_begin("http payload download "
		   "(server blocking; client shared)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.parallel_clients = 4;
	test_run_sequential(test_client_download);
	tset.parallel_clients = 4;
	test_run_pipeline(test_client_download);
	tset.parallel_clients = 4;
	test_run_parallel(test_client_download);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; client shared)");
	test_init_defaults();
	tset.parallel_clients = 4;
	test_run_sequential(test_client_echo);
	tset.parallel_clients = 4;
	test_run_pipeline(test_client_echo);
	tset.parallel_clients = 4;
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server blocking; client shared)");
	test_init_defaults();
	tset.server_blocking = TRUE;
	tset.server_ostream = TRUE;
	tset.parallel_clients = 4;
	test_run_sequential(test_client_echo);
	tset.parallel_clients = 4;
	test_run_pipeline(test_client_echo);
	tset.parallel_clients = 4;
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo "
		   "(server non-blocking; client global)");
	test_init_defaults();
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_sequential(test_client_echo);
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_pipeline(test_client_echo);
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_ssl(void)
{
	test_begin("http payload echo (ssl)");
	test_init_defaults();
	tset.ssl = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (ssl; unknown size)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.ssl = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (ssl; unknown size; client trickle)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.ssl = TRUE;
	tset.client_trickle_final_byte = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (ssl; unknown size; server trickle)");
	test_init_defaults();
	tset.unknown_size = TRUE;
	tset.ssl = TRUE;
	tset.server_trickle_final_byte = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (ssl; server ostream, cork)");
	test_init_defaults();
	tset.ssl = TRUE;
	tset.server_ostream = TRUE;
	tset.server_cork = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();
}

static void test_echo_client_blocking(void)
{
	test_begin("http payload echo (client blocking)");
	test_init_defaults();
	tset.client_blocking = TRUE;
	test_run_sequential(test_client_echo);
	test_run_pipeline(test_client_echo);
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (client blocking; client shared)");
	test_init_defaults();
	tset.client_blocking = TRUE;
	tset.parallel_clients = 4;
	test_run_sequential(test_client_echo);
	tset.parallel_clients = 4;
	test_run_pipeline(test_client_echo);
	tset.parallel_clients = 4;
	test_run_parallel(test_client_echo);
	test_end();

	test_begin("http payload echo (client blocking; client global)");
	test_init_defaults();
	tset.client_blocking = TRUE;
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_sequential(test_client_echo);
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_pipeline(test_client_echo);
	tset.parallel_clients = 4;
	tset.parallel_clients_global = TRUE;
	test_run_parallel(test_client_echo);
	test_end();
}

static void (*const test_functions[])(void) = {
	test_download_server_nonblocking,
	test_download_server_blocking,
	test_echo_server_nonblocking,
	test_echo_server_blocking,
	test_echo_server_nonblocking_sync,
	test_echo_server_blocking_sync,
	test_echo_server_nonblocking_partial,
	test_echo_server_blocking_partial,
	test_download_client_partial,
	test_download_client_nested_ioloop,
	test_echo_client_shared,
	test_echo_ssl,
	test_echo_client_blocking,
	NULL
};

/*
 * Main
 */

static void main_init(void)
{
	ssl_iostream_openssl_init();
	common_event = test_event;
	client_event = event_create(common_event);
	event_set_append_log_prefix(client_event, "test client: ");
	server_event = event_create(common_event);
	event_set_append_log_prefix(server_event, "test server: ");

	tmp_dir = i_strdup(test_dir_get());
}

static void main_deinit(void)
{
	ssl_iostream_context_cache_free();
	ssl_iostream_openssl_deinit();
	event_unref(&client_event);
	event_unref(&server_event);
	common_event = NULL;

	i_free(tmp_dir);
}

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

	lib_init();

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

	test_init();
	event_set_forced_debug(test_event, debug);
	test_dir_init("http-payload");
	test_subprocesses_init();
	main_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;
}
