/*
 * Copyright (c) 2013-2014 Tim Ruehsen
 * Copyright (c) 2015-2026 Free Software Foundation, Inc.
 *
 * This file is part of Wget
 *
 * Wget is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wget is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Wget  If not, see <https://www.gnu.org/licenses/>.
 *
 *
 * Test suite function library
 *
 * Changelog
 * 16.01.2013  Tim Ruehsen  created
 *
 * To create the X.509 stuff, I followed the instructions at
 *   gnutls.org/manual/html_node/gnutls_002dserv-Invocation.html
 *
 */

#include <config.h>

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <utime.h>
#include <dirent.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <wget.h>
#include "../src/wget_utils.h"
#include "libtest.h"

#include <microhttpd.h>
#ifndef HAVE_MHD_FREE
#  define MHD_free wget_free
#endif

#ifdef WITH_LIBNGHTTP2
#  include <nghttp2/nghttp2.h>
#  include <pthread.h>
#endif
#ifndef MHD_HTTP_RANGE_NOT_SATISFIABLE
#  define MHD_HTTP_RANGE_NOT_SATISFIABLE MHD_HTTP_REQUESTED_RANGE_NOT_SATISFIABLE
#endif
#ifndef MHD_USE_TLS
#  define MHD_USE_TLS MHD_USE_SSL
#endif

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

#ifdef WITH_GNUTLS_IN_TESTSUITE
#ifdef WITH_GNUTLS_OCSP
#  include <gnutls/ocsp.h>
#  include <gnutls/x509.h>
#  include <gnutls/abstract.h>
#endif

#  include <gnutls/gnutls.h>
#  define file_load_err(fname, msg) wget_error_printf_exit("Couldn't load '%s' : %s\n", fname, msg)
#endif

static int
	http_server_port,
	https_server_port,
	ocsp_server_port,
	h2_server_port,
	keep_tmpfiles,
	clean_directory,
	reject_http_connection,
	reject_https_connection,
	ocsp_response_pos,
	h2_min_concurrent_streams;
static wget_vector
	*request_urls,
	*ocsp_responses;
static wget_test_url_t
	*urls;
static size_t
	nurls;
static char
	tmpdir[128];
static char
	server_send_content_length = 1;
static const char
	*custom_server_key,
	*custom_server_cert;
static bool atexit_registered;

static enum CHECK_POST_HANDSHAKE_AUTH {
	CHECK_ENABLED,
	CHECK_PASSED,
	CHECK_FAILED
} *post_handshake_auth;

// MHD_Daemon instance
static struct MHD_Daemon
	*httpdaemon,
	*httpsdaemon,
	*ocspdaemon;

#ifdef WITH_GNUTLS_OCSP
static gnutls_pcert_st *pcrt;
static gnutls_privkey_t *privkey;

typedef struct {
	char
		*data;
	size_t
		size;
} ocsp_resp_t;
#endif

#ifdef WITH_GNUTLS_OCSP
static gnutls_ocsp_data_st *ocsp_stap_resp;
#endif

#ifdef WITH_LIBNGHTTP2
typedef struct {
	nghttp2_session *session;
	gnutls_session_t tls_session;
	int sockfd;
	char *method;
	char *path;
	const char *response_body;  // Points to test URL body
	size_t response_body_len;
	size_t response_body_offset;
	int32_t stream_id;
	char **header_names;   // Allocated header names to free later
	char **header_values;  // Allocated header values to free later
	int num_headers;       // Number of allocated headers
} h2_session_data;

typedef struct {
	int listen_fd;
	pthread_t thread;
	volatile int running;
	gnutls_certificate_credentials_t creds;
} h2_server_t;

static h2_server_t *nghttp2_server;
#endif

// for passing URL query string
struct query_string {
	wget_buffer
		*params;
	int
		it;
};

static char
	*key_pem,
	*cert_pem;

enum SERVER_MODE {
	HTTP_MODE,
	HTTPS_MODE,
	OCSP_MODE,
	OCSP_STAP_MODE,
	H2_MODE
};

static enum PASS {
	HTTP_1_1_PASS,
	H2_PASS,
	END_PASS
} proto_pass;

static const char *_parse_hostname(const char* data)
{
	if (data) {
		if (!wget_strncasecmp_ascii(data, "http://", 7)) {
			return strchr(data + 7, '/');
		}
		if (!wget_strncasecmp_ascii(data, "https://", 8)) {
			return strchr(data + 8, '/');
		}
	}

	return data;
}

static void _replace_space_with_plus(wget_buffer *buf, const char *data)
{
	for (; *data; data++)
		wget_buffer_memcat(buf, *data == ' ' ? "+" : data, 1);
}

static enum MHD_Result _print_query_string(
	void *cls,
	enum MHD_ValueKind kind WGET_GCC_UNUSED,
	const char *key,
	const char *value)
{
	struct query_string *query = cls;

	if (key && !query->it) {
		wget_buffer_strcpy(query->params, "?");
		_replace_space_with_plus(query->params, key);
		if (value) {
			wget_buffer_strcat(query->params, "=");
			_replace_space_with_plus(query->params, value);
		}
	}
	if (key && query->it) {
		wget_buffer_strcat(query->params, "&");
		_replace_space_with_plus(query->params, key);
		if (value) {
			wget_buffer_strcat(query->params, "=");
			_replace_space_with_plus(query->params, value);
		}
	}

	query->it++;
	return MHD_YES;
}

static enum MHD_Result _print_header_range(
	void *cls,
	enum MHD_ValueKind kind WGET_GCC_UNUSED,
	const char *key,
	const char *value)
{
	wget_buffer *header_range = cls;

	if (!strcasecmp(key, MHD_HTTP_HEADER_RANGE)) {
		wget_buffer_strcpy(header_range, key);
		if (value) {
			wget_buffer_strcat(header_range, value);
		}
	}

	return MHD_YES;
}

struct ResponseContentCallbackParam
{
	const char *response_data;
	size_t response_size;
	interrupt_response_mode_t interrupt_response_mode;
	size_t interrupt_response_after_nbytes;
};

static ssize_t _callback (void *cls, uint64_t pos, char *buf, size_t buf_size)
{
	size_t size_to_copy;
	struct ResponseContentCallbackParam *const param =
		(struct ResponseContentCallbackParam *)cls;

	if (pos >= param->response_size)
		return MHD_CONTENT_READER_END_OF_STREAM;

	// divide data into two chunks
	buf_size = (param->response_size / 2) + 1;
	if (buf_size < (param->response_size - pos))
		size_to_copy = buf_size;
	else
		size_to_copy = param->response_size - pos;

	memcpy(buf, param->response_data + pos, size_to_copy);

	return size_to_copy;
}

static ssize_t _callback_interruptable (void *cls, uint64_t pos, char *buf, size_t buf_size)
{
	size_t size_to_copy;
	struct ResponseContentCallbackParam *const param =
		(struct ResponseContentCallbackParam *)cls;

	if (pos >= param->response_size)
		return MHD_CONTENT_READER_END_OF_STREAM;

	if (buf_size <= (param->response_size - pos)) {
		size_to_copy = buf_size;
	} else {
		size_to_copy = param->response_size - pos;
	}

	if (param->interrupt_response_mode != INTERRUPT_RESPONSE_DISABLED) {
		if (pos >= param->interrupt_response_after_nbytes) {
			return MHD_CONTENT_READER_END_WITH_ERROR;
		}

		if (size_to_copy > (param->interrupt_response_after_nbytes - pos)) {
			size_to_copy = param->interrupt_response_after_nbytes - pos;
		}
	}

	memcpy(buf, param->response_data + pos, size_to_copy);
	return size_to_copy;
}

static void _free_callback_param(void *cls)
{
	wget_free(cls);
}

#ifdef WITH_GNUTLS_OCSP
static enum MHD_Result _ocsp_ahc(
	void *cls WGET_GCC_UNUSED,
	struct MHD_Connection *connection,
	const char *url WGET_GCC_UNUSED,
	const char *method WGET_GCC_UNUSED,
	const char *version WGET_GCC_UNUSED,
	const char *upload_data,
	size_t *upload_data_size,
	void **con_cls WGET_GCC_UNUSED)
{
	static bool first = true;

	if (first && upload_data == NULL) {
		first = false;

		return MHD_YES;
	} else if (!first && upload_data == NULL) {
		int ret = 0;

		ocsp_resp_t *ocsp_resp = wget_vector_get(ocsp_responses, ocsp_response_pos++);

		if (ocsp_resp) {
			struct MHD_Response *response = MHD_create_response_from_buffer (ocsp_resp->size, ocsp_resp->data, MHD_RESPMEM_MUST_COPY);

			ret = MHD_queue_response (connection, MHD_HTTP_OK, response);

			MHD_destroy_response (response);
		}

		return ret;
	}

	*upload_data_size = 0;

	return MHD_YES;
}

static int _ocsp_cert_callback(
	gnutls_session_t session WGET_GCC_UNUSED,
	const gnutls_datum_t* req_ca_dn WGET_GCC_UNUSED,
	int nreqs WGET_GCC_UNUSED,
	const gnutls_pk_algorithm_t* pk_algos WGET_GCC_UNUSED,
	int pk_algos_length WGET_GCC_UNUSED,
	gnutls_pcert_st** pcert,
	unsigned int *pcert_length,
	gnutls_privkey_t *pkey)
{
	*pcert = pcrt;
	*(pcert+1) = pcrt+1;
	*pkey = *privkey;
	*pcert_length = 2;

	return 0;
}

static int _ocsp_stap_cert_callback(
	gnutls_session_t session WGET_GCC_UNUSED,
	const struct gnutls_cert_retr_st *info WGET_GCC_UNUSED,
	gnutls_pcert_st **certs,
	unsigned int *pcert_length,
	gnutls_ocsp_data_st **ocsp,
	unsigned int *ocsp_length,
	gnutls_privkey_t *pkey,
	unsigned int *flags WGET_GCC_UNUSED)
{
	*certs = pcrt;
	*(certs+1) = pcrt+1;
	*pcert_length = 2;

	*pkey = *privkey;

	*ocsp = ocsp_stap_resp;
	*ocsp_length = 1;

	return 0;
}
#endif

static enum MHD_Result _answer_to_connection(
	void *cls WGET_GCC_UNUSED,
	struct MHD_Connection *connection,
	const char *url,
	const char *method,
	const char *version WGET_GCC_UNUSED,
	const char *upload_data WGET_GCC_UNUSED,
	size_t *upload_data_size WGET_GCC_UNUSED,
	void **con_cls WGET_GCC_UNUSED)
{
	if (post_handshake_auth) {
		gnutls_session_t tls_sess;
		const union MHD_ConnectionInfo *conn_info = MHD_get_connection_info (connection, MHD_CONNECTION_INFO_GNUTLS_SESSION);

		if (conn_info) {
			int check_auth;
			tls_sess = conn_info->tls_session;
			gnutls_certificate_server_set_request(tls_sess, GNUTLS_CERT_REQUEST);
			do
				check_auth = gnutls_reauth(tls_sess, 0);
			while (check_auth == GNUTLS_E_AGAIN);

			*post_handshake_auth = (check_auth == GNUTLS_E_SUCCESS) ? CHECK_PASSED : CHECK_FAILED;
		}
	}

	struct MHD_Response *response = NULL;
	struct query_string query;
	int ret = 0;
	int64_t modified;
	const char *modified_val, *to_bytes_string = "";
	ssize_t from_bytes, to_bytes;
	char content_len[100], content_range[100];

	// whether or not this connection is HTTPS
	bool https = !!MHD_get_connection_info(connection, MHD_CONNECTION_INFO_PROTOCOL);

	// get query string
	query.params = wget_buffer_alloc(1024);
	query.it = 0;
	MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, _print_query_string, &query);

	// get if-modified-since header
	modified_val = MHD_lookup_connection_value(connection, MHD_HEADER_KIND,
												MHD_HTTP_HEADER_IF_MODIFIED_SINCE);
	modified = 0;
	if (modified_val)
		modified = wget_http_parse_full_date(modified_val);

	// get header range
	wget_buffer *header_range = wget_buffer_alloc(1024);
	if (!strcmp(method, "GET"))
		MHD_get_connection_values(connection, MHD_HEADER_KIND, _print_header_range, header_range);

	from_bytes = to_bytes = 0;
	if (*header_range->data) {
		const char *from_bytes_string;
		const char *range_string = strchr(header_range->data, '=');

		to_bytes_string = strchr(range_string, '-');
		if (strcmp(to_bytes_string, "-"))
			to_bytes = (ssize_t) atoi(to_bytes_string + 1);
		from_bytes_string = wget_strmemdup(range_string, to_bytes_string - range_string);
		from_bytes = (ssize_t) atoi(from_bytes_string + 1);
		wget_xfree(from_bytes_string);
	}

	// append 'index.html' to directory and append query string
	const char *url_full, *p;
	if ((p = strrchr(url, '/')) && p[1] == 0) {
		url_full = wget_aprintf("%sindex.html%s", url, query.params->data ? query.params->data : "");
	} else {
		url_full = wget_aprintf("%s%s", url, query.params->data ? query.params->data : "");
	}
	wget_buffer_free(&query.params);

	// iterate over test urls array
	bool found = false, chunked = false;
	char *url_iri = NULL;

	for (wget_test_url_t *request_url = urls; request_url < urls + nurls; request_url++) {
		if (request_url->http_only && https)
			continue;
		if (request_url->https_only && !https)
			continue;

		// convert remote url into escaped char for iri encoding
		wget_xfree(url_iri);
		url_iri = wget_strdup(request_url->name);
		MHD_http_unescape(url_iri);

		if (!strcmp(_parse_hostname(url_full), _parse_hostname(url_iri))) {
			size_t body_length =
				request_url->body_len ? request_url->body_len
				: (request_url->body ? strlen(request_url->body) : 0);

			// check request headers
			bool bad_request = false;

			if (request_url->expected_method && strcmp(method, request_url->expected_method)) {
				wget_debug_printf("%s: Expected request method '%s', but got '%s'\n",
					__func__, request_url->expected_method, method);
				bad_request = true;
			}

			for (const char **header = request_url->expected_req_headers; *header; header++) {
				const char *header_value = strchr(*header, ':');
				const char *header_key = wget_strmemdup(*header, header_value - *header);
				const char *got_val = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, header_key);
				wget_xfree(header_key);

				// 400 Bad Request
				if (!got_val || strcmp(got_val, header_value + 2)) {
					wget_debug_printf("%s: Missing expected header '%s'\n", __func__, *header);
					bad_request = true;
					break;
				}
			}

			// check unexpected headers
			for (const char **header_key = request_url->unexpected_req_headers; *header_key; header_key++) {
				const char *got_val = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, *header_key);

				// 400 Bad Request
				if (got_val) {
					wget_debug_printf("%s: Got unexpected header '%s'\n", __func__, *header_key);
					bad_request = true;
					break;
				}
			}

			// return with "400 Bad Request"
			if (bad_request) {
				response = MHD_create_response_from_buffer(0, (void *) "", MHD_RESPMEM_PERSISTENT);
				ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);
				found = true;
				break;
			}

			// chunked encoding
			if (!wget_strcmp(request_url->name + 3, "bad.txt")) {
				response = MHD_create_response_from_buffer(body_length,
					(void *) request_url->body, MHD_RESPMEM_MUST_COPY);
				ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
				MHD_add_response_header(response, "Transfer-Encoding", "chunked");
				MHD_add_response_header(response, "Connection", "close");
				found = true;
				break;
			}
			for (const char **header = request_url->headers; *header; header++) {
				const char *header_value = strchr(*header, ':');
				const char *header_key = wget_strmemdup(*header, header_value - *header);
				if (!strcmp(header_key, "Transfer-Encoding") && !strcmp(header_value + 2, "chunked"))
					chunked = true;
				wget_xfree(header_key);
			}
			if (chunked) {
				struct ResponseContentCallbackParam *callback_param = wget_malloc(sizeof(struct ResponseContentCallbackParam));

				callback_param->response_data = request_url->body;
				callback_param->response_size = body_length;

				response = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,
					1024, _callback, callback_param, _free_callback_param);
				ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
				found = true;
				break;
			}

			// redirection
			if (atoi(request_url->code)/100 == 3) {
				response = MHD_create_response_from_buffer(0, (void *) "", MHD_RESPMEM_PERSISTENT);

				// add available headers
				for (const char **header = request_url->headers; *header; header++) {
					const char *header_value = strchr(*header, ':');
					const char *header_key = wget_strmemdup(*header, header_value - *header);
					MHD_add_response_header(response, header_key, header_value + 2);
					wget_xfree(header_key);
				}
				ret = MHD_queue_response(connection, atoi(request_url->code), response);
				found = true;
				break;
			}

			// 404 with non-empty "body"
			if (atoi(request_url->code) != 200) {
				response = MHD_create_response_from_buffer(body_length,
					(void *) request_url->body, MHD_RESPMEM_MUST_COPY);
				ret = MHD_queue_response(connection, atoi(request_url->code), response);
				found = true;
				break;
			}

			// basic authentication
			if (!wget_strcmp(request_url->auth_method, "Basic")) {
				char *pass = NULL;
				char *user = MHD_basic_auth_get_username_password(connection, &pass);
				if ((user == NULL && pass == NULL) ||
					wget_strcmp(user, request_url->auth_username) ||
					wget_strcmp(pass, request_url->auth_password))
				{
					response = MHD_create_response_from_buffer(strlen ("DENIED"),
						(void *) "DENIED", MHD_RESPMEM_PERSISTENT);
					ret = MHD_queue_basic_auth_fail_response(connection, "basic@example.com", response);
					MHD_free(user);
					MHD_free(pass);
					found = true;
					break;
				}
				MHD_free(user);
				MHD_free(pass);
			}

			// digest authentication
			if (!wget_strcmp(request_url->auth_method, "Digest")) {
				const char *realm = "digest@example.com";
				char *user = MHD_digest_auth_get_username(connection);
				if (wget_strcmp(user, request_url->auth_username)) {
					response = MHD_create_response_from_buffer(strlen ("DENIED"),
						(void *) "DENIED", MHD_RESPMEM_PERSISTENT);
					ret = MHD_queue_auth_fail_response(connection, realm, TEST_OPAQUE_STR, response, MHD_NO);
					MHD_free(user);
					found = true;
					break;
				}
				ret = MHD_digest_auth_check(connection, realm, user, request_url->auth_password, 300);
				MHD_free(user);
				if ((ret == MHD_INVALID_NONCE) || (ret == MHD_NO)) {
					response = MHD_create_response_from_buffer(strlen ("DENIED"),
						(void *) "DENIED", MHD_RESPMEM_PERSISTENT);

					if (response) {
						ret = MHD_queue_auth_fail_response(connection, realm, TEST_OPAQUE_STR, response,
							(ret == MHD_INVALID_NONCE) ? MHD_YES : MHD_NO);
						found = true;
					} else
						ret = MHD_NO;

					break;
				}
			}

			if (modified && request_url->modified <= modified) {
				response = MHD_create_response_from_buffer(0, (void *) "", MHD_RESPMEM_PERSISTENT);
				ret = MHD_queue_response(connection, MHD_HTTP_NOT_MODIFIED, response);
			}
			else if (*header_range->data) {
				if (!strcmp(to_bytes_string, "-"))
					to_bytes = body_length - 1;

				size_t body_len = to_bytes - from_bytes + 1;

				if (from_bytes > to_bytes || from_bytes >= (int) body_length) {
					response = MHD_create_response_from_buffer(0, (void *) "", MHD_RESPMEM_PERSISTENT);
					ret = MHD_queue_response(connection, MHD_HTTP_RANGE_NOT_SATISFIABLE, response);
				} else {
					if (request_url->interrupt_response_mode != INTERRUPT_RESPONSE_DISABLED) {
						struct ResponseContentCallbackParam *callback_param = wget_malloc(sizeof(struct ResponseContentCallbackParam));
						callback_param->response_data = (void *) (request_url->body + from_bytes);
						callback_param->response_size = body_len;
						callback_param->interrupt_response_mode = request_url->interrupt_response_mode;
						callback_param->interrupt_response_after_nbytes = request_url->interrupt_response_after_nbytes;

						response = MHD_create_response_from_callback(body_len,
								1024, _callback_interruptable, callback_param, _free_callback_param);
					} else {
						response = MHD_create_response_from_buffer(body_len,
							(void *) (request_url->body + from_bytes), MHD_RESPMEM_MUST_COPY);
					}
					MHD_add_response_header(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
					wget_snprintf(content_range, sizeof(content_range), "%zd-%zd/%zu", from_bytes, to_bytes, body_len);
					MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_RANGE, content_range);
					wget_snprintf(content_len, sizeof(content_len), "%zu", body_len);
					MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_LENGTH, content_len);
					ret = MHD_queue_response(connection, MHD_HTTP_PARTIAL_CONTENT, response);
				}
			} else {
				if (request_url->interrupt_response_mode != INTERRUPT_RESPONSE_DISABLED) {
					struct ResponseContentCallbackParam *callback_param = wget_malloc(sizeof(struct ResponseContentCallbackParam));
					callback_param->response_data = request_url->body;
					callback_param->response_size = body_length;
					callback_param->interrupt_response_mode = request_url->interrupt_response_mode;
					callback_param->interrupt_response_after_nbytes = request_url->interrupt_response_after_nbytes;

					response = MHD_create_response_from_callback(body_length,
							1024, _callback_interruptable, callback_param, _free_callback_param);
				} else {
					response = MHD_create_response_from_buffer(body_length, (void *) request_url->body, MHD_RESPMEM_MUST_COPY);
				}

				ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
			}

			// switch off Content-Length sanity checks
			MHD_set_response_options(response,
				MHD_RF_INSANITY_HEADER_CONTENT_LENGTH,
				MHD_RO_END);

			// add available headers
			for (const char **header = request_url->headers; *header; header++) {
				const char *header_value = strchr(*header, ':');
				const char *header_key = wget_strmemdup(*header, header_value - *header);
				MHD_add_response_header(response, header_key, header_value + 2);
				wget_xfree(header_key);
			}

			found = true;
		}
	}

	// 404 with empty "body"
	if (!found) {
		response = MHD_create_response_from_buffer(0, (void *) "", MHD_RESPMEM_PERSISTENT);
		ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);
	}

	wget_xfree(url_iri);
	wget_xfree(url_full);
	wget_buffer_free(&header_range);
	char server_version[50];
	wget_snprintf(server_version, sizeof(server_version), "Libmicrohttpd/%08x", (unsigned int) MHD_VERSION);
	MHD_add_response_header(response, "Server", server_version);
	MHD_destroy_response(response);
	return ret;
}

#ifdef WITH_LIBNGHTTP2

static ssize_t h2_send_callback(nghttp2_session *session WGET_GCC_UNUSED,
	const uint8_t *data, size_t length, int flags WGET_GCC_UNUSED, void *user_data)
{
	h2_session_data *session_data = (h2_session_data *) user_data;
	ssize_t nwrite = gnutls_record_send(session_data->tls_session, data, length);

	if (nwrite < 0) {
		if (nwrite == GNUTLS_E_AGAIN || nwrite == GNUTLS_E_INTERRUPTED)
			return NGHTTP2_ERR_WOULDBLOCK;
		return NGHTTP2_ERR_CALLBACK_FAILURE;
	}

	return nwrite;
}

static ssize_t h2_recv_callback(nghttp2_session *session WGET_GCC_UNUSED,
	uint8_t *buf, size_t length, int flags WGET_GCC_UNUSED, void *user_data)
{
	h2_session_data *session_data = (h2_session_data *) user_data;
	ssize_t nread = gnutls_record_recv(session_data->tls_session, buf, length);

	if (nread < 0) {
		if (nread == GNUTLS_E_AGAIN || nread == GNUTLS_E_INTERRUPTED)
			return NGHTTP2_ERR_WOULDBLOCK;
		return NGHTTP2_ERR_CALLBACK_FAILURE;
	}

	if (nread == 0)
		return NGHTTP2_ERR_EOF;

	return nread;
}

static ssize_t h2_data_read_callback(nghttp2_session *session,
	int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags,
	nghttp2_data_source *source WGET_GCC_UNUSED, void *user_data WGET_GCC_UNUSED)
{
	h2_session_data *stream_data = nghttp2_session_get_stream_user_data(session, stream_id);
	if (!stream_data || !stream_data->response_body)
		return 0;

	size_t remaining = stream_data->response_body_len - stream_data->response_body_offset;

	// Limit chunk size to 8KB to allow proper rate limiting on the client side
	// Without this limit, large responses are sent too quickly and bypass rate limiting
	// Smaller chunks allow better rate limiting granularity, especially with HTTP/2 multiplexing
	static const size_t max_chunk = 8 * 1024;
	if (length > max_chunk)
		length = max_chunk;

	size_t to_send = remaining < length ? remaining : length;

	if (to_send > 0) {
		memcpy(buf, stream_data->response_body + stream_data->response_body_offset, to_send);
		stream_data->response_body_offset += to_send;
	}

	if (stream_data->response_body_offset >= stream_data->response_body_len)
		*data_flags |= NGHTTP2_DATA_FLAG_EOF;

	return (ssize_t) to_send;
}

static int h2_on_header_callback(nghttp2_session *session,
	const nghttp2_frame *frame, const uint8_t *name, size_t namelen,
	const uint8_t *value, size_t valuelen, uint8_t flags WGET_GCC_UNUSED, void *user_data WGET_GCC_UNUSED)
{
	h2_session_data *session_data = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);

	if (!session_data || frame->hd.type != NGHTTP2_HEADERS)
		return 0;

	if (frame->headers.cat != NGHTTP2_HCAT_REQUEST)
		return 0;

	// Parse :method and :path pseudo-headers
	if (namelen == 7 && memcmp(name, ":method", 7) == 0) {
		session_data->method = wget_strmemdup((const char *) value, valuelen);
	} else if (namelen == 5 && memcmp(name, ":path", 5) == 0) {
		session_data->path = wget_strmemdup((const char *) value, valuelen);
	}

	return 0;
}

static int h2_on_begin_headers_callback(nghttp2_session *session,
	const nghttp2_frame *frame, void *user_data)
{
	if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST)
		return 0;

	h2_session_data *stream_data = wget_calloc(1, sizeof(h2_session_data));
	if (!stream_data)
		return NGHTTP2_ERR_CALLBACK_FAILURE;

	h2_session_data *session_data = (h2_session_data *)user_data;
	stream_data->session = session;
	stream_data->tls_session = session_data->tls_session;
	stream_data->sockfd = session_data->sockfd;
	stream_data->stream_id = frame->hd.stream_id;
	stream_data->response_body = NULL;
	stream_data->response_body_len = 0;
	stream_data->response_body_offset = 0;

	nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, stream_data);

	return 0;
}

static int h2_on_frame_recv_callback(nghttp2_session *session,
	const nghttp2_frame *frame, void *user_data WGET_GCC_UNUSED)
{
	if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST)
		return 0;

	h2_session_data *stream_data = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
	if (!stream_data || !stream_data->path)
		return 0;

	// Find matching URL in test definitions
	const char *path = stream_data->path;
	if (*path == '/')
		path++;

	wget_test_url_t *url = NULL;
	for (size_t i = 0; i < nurls; i++) {
		const char *url_name = urls[i].name;
		if (*url_name == '/')
			url_name++;

		if (strcmp(path, url_name) == 0) {
			url = &urls[i];
			break;
		}
	}

	// Prepare response
	const char *status = "404 Not Found";
	const char *body = "Not Found";
	size_t body_len = strlen(body);

	if (url) {
		status = url->code;
		body = url->body;
		body_len = url->body_len ? url->body_len : (body ? strlen(body) : 0);
	}

	// Store response body in stream context
	stream_data->response_body = body;
	stream_data->response_body_len = body_len;
	stream_data->response_body_offset = 0;

	// Parse status code
	int status_code = 404;
	if (status)
		status_code = atoi(status);

	// Build response headers
	char status_str[16];
	wget_snprintf(status_str, sizeof(status_str), "%d", status_code);

	#define MAX_HEADERS 20
	nghttp2_nv hdrs[MAX_HEADERS];
	int nhdrs = 0;

	hdrs[nhdrs].name = (uint8_t *) ":status";
	hdrs[nhdrs].namelen = strlen((char *) hdrs[nhdrs].name);
	hdrs[nhdrs].value = (uint8_t *) status_str;
	hdrs[nhdrs].valuelen = strlen(status_str);
	hdrs[nhdrs].flags = NGHTTP2_NV_FLAG_NONE;
	nhdrs++;

	hdrs[nhdrs].name = (uint8_t *) "server";
	hdrs[nhdrs].namelen = strlen((char *) hdrs[nhdrs].name);
	hdrs[nhdrs].value = (uint8_t *) "libwget-test-h2/1.0";
	hdrs[nhdrs].valuelen = strlen((char *) hdrs[nhdrs].value);
	hdrs[nhdrs].flags = NGHTTP2_NV_FLAG_NONE;
	nhdrs++;

	// Add custom headers from test definition
	if (url) {
		for (int i = 0; i < 10 && url->headers[i] && nhdrs < MAX_HEADERS - 1; i++) {
			const char *header = url->headers[i];
			const char *colon = strchr(header, ':');
			if (colon) {
				size_t name_len = colon - header;
				const char *value = colon + 1;
				while (*value == ' ') value++;

				// Convert header name to lowercase for HTTP/2
				char *name_lower = wget_strmemdup(header, name_len);
				for (size_t j = 0; j < name_len; j++)
					name_lower[j] = (char) tolower((unsigned char) name_lower[j]);

				hdrs[nhdrs].name = (uint8_t *) name_lower;
				hdrs[nhdrs].namelen = name_len;
				hdrs[nhdrs].value = (uint8_t *) wget_strdup(value);
				hdrs[nhdrs].valuelen = strlen(value);
				hdrs[nhdrs].flags = NGHTTP2_NV_FLAG_NONE;
				nhdrs++;
			}
		}
	}

	// Setup data provider for response body
	nghttp2_data_provider data_prov;
	data_prov.read_callback = h2_data_read_callback;

	// send 1xx response if requested
	if (url && url->code_1xx) {
		nghttp2_nv hdrs_1xx = {
			.name = (uint8_t *) ":status",
			.namelen = 7,
			.value = (uint8_t *) url->code_1xx,
			.valuelen = strlen(url->code_1xx),
			.flags = NGHTTP2_NV_FLAG_NONE
		};

		nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, stream_data->stream_id, NULL, &hdrs_1xx, 1, NULL);
	}

	// Store allocated headers in stream_data so we can free them later
	// (nghttp2 doesn't copy header data, so they must remain valid)
	if (nhdrs > 2) {
		stream_data->num_headers = nhdrs - 2;  // Exclude :status and server headers
		stream_data->header_names = wget_malloc(stream_data->num_headers * sizeof(char *));
		stream_data->header_values = wget_malloc(stream_data->num_headers * sizeof(char *));
		for (int i = 0; i < stream_data->num_headers; i++) {
			stream_data->header_names[i] = (char *) hdrs[i + 2].name;
			stream_data->header_values[i] = (char *) hdrs[i + 2].value;
		}
	} else {
		stream_data->header_names = NULL;
		stream_data->header_values = NULL;
		stream_data->num_headers = 0;
	}

	// Submit response with data provider
	int rv = nghttp2_submit_response(session, stream_data->stream_id, hdrs, nhdrs,
		body_len > 0 ? &data_prov : NULL);

	return (rv == 0) ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

static int h2_on_stream_close_callback(nghttp2_session *session,
	int32_t stream_id, uint32_t error_code WGET_GCC_UNUSED, void *user_data WGET_GCC_UNUSED)
{
	h2_session_data *stream_data = nghttp2_session_get_stream_user_data(session, stream_id);

	if (stream_data) {
		wget_free(stream_data->method);
		wget_free(stream_data->path);

		// Free allocated header strings
		if (stream_data->header_names) {
			for (int i = 0; i < stream_data->num_headers; i++) {
				wget_free(stream_data->header_names[i]);
				wget_free(stream_data->header_values[i]);
			}
			wget_free(stream_data->header_names);
			wget_free(stream_data->header_values);
		}

		wget_free(stream_data);
	}

	return 0;
}

static void *h2_server_thread(void *arg WGET_GCC_UNUSED)
{
	if (!nghttp2_server)
		return NULL;

	while (nghttp2_server->running) {
		struct sockaddr_storage client_addr;
		socklen_t client_len = sizeof(client_addr);

                if (wget_ready_2_read(nghttp2_server->listen_fd, 100) <= 0)
                    continue;

		int client_fd = accept(nghttp2_server->listen_fd, (struct sockaddr *) &client_addr, &client_len);
		if (client_fd < 0)
			continue;

		// Setup TLS (keep blocking for handshake)
		gnutls_session_t tls_session;
		gnutls_init(&tls_session, GNUTLS_SERVER);
		gnutls_priority_set_direct(tls_session, "NORMAL:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2", NULL);
		gnutls_credentials_set(tls_session, GNUTLS_CRD_CERTIFICATE, nghttp2_server->creds);

		// ALPN for h2
		gnutls_datum_t alpn_protos[1];
		alpn_protos[0].data = (unsigned char *)"h2";
		alpn_protos[0].size = 2;
		gnutls_alpn_set_protocols(tls_session, alpn_protos, 1, 0);

		gnutls_transport_set_int(tls_session, client_fd);
		gnutls_handshake_set_timeout(tls_session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);

		// TLS handshake (blocking)
		int ret = gnutls_handshake(tls_session);
		if (ret < 0) {
			gnutls_deinit(tls_session);
			close(client_fd);
			continue;
		}

		// Make socket non-blocking after handshake
		int flags = fcntl(client_fd, F_GETFL, 0);
		fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
		gnutls_transport_set_int(tls_session, client_fd);

		// Verify ALPN selected h2
		gnutls_datum_t alpn;
		ret = gnutls_alpn_get_selected_protocol(tls_session, &alpn);
		if (ret < 0 || alpn.size != 2 || memcmp(alpn.data, "h2", 2) != 0) {
			gnutls_bye(tls_session, GNUTLS_SHUT_RDWR);
			gnutls_deinit(tls_session);
			close(client_fd);
			continue;
		}

		// Create nghttp2 session
		h2_session_data session_data = {
			.session = NULL,
			.tls_session = tls_session,
			.sockfd = client_fd,
			.method = NULL,
			.path = NULL,
			.response_body = NULL,
			.response_body_len = 0,
			.response_body_offset = 0,
			.stream_id = 0
		};

		nghttp2_session_callbacks *callbacks;
		nghttp2_session_callbacks_new(&callbacks);
		nghttp2_session_callbacks_set_send_callback(callbacks, h2_send_callback);
		nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, h2_on_frame_recv_callback);
		nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, h2_on_stream_close_callback);
		nghttp2_session_callbacks_set_on_header_callback(callbacks, h2_on_header_callback);
		nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, h2_on_begin_headers_callback);
		nghttp2_session_callbacks_set_recv_callback(callbacks, h2_recv_callback);

		nghttp2_session_server_new(&session_data.session, callbacks, &session_data);
		nghttp2_session_callbacks_del(callbacks);

		// Send server connection preface (SETTINGS frame)
		nghttp2_settings_entry iv[2] = {
			{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100},
			{NGHTTP2_SETTINGS_ENABLE_PUSH, 0}
		};
		nghttp2_submit_settings(session_data.session, NGHTTP2_FLAG_NONE, iv, 2);

		// Event loop for this connection
		while (nghttp2_server->running) {
			// Check if we want to read or write
			int mode = nghttp2_session_want_read(session_data.session) != 0 ? WGET_IO_READABLE : 0;
			mode |= nghttp2_session_want_write(session_data.session) != 0 ? WGET_IO_WRITABLE : 0;

			if (mode == 0)
				break;

			int rc;
			if ((rc = wget_ready_2_transfer(client_fd, 1000, WGET_IO_READABLE | WGET_IO_WRITABLE)) <= 0)
				continue;

			if (rc & WGET_IO_READABLE) {
				int rv = nghttp2_session_recv(session_data.session);
				if (rv != 0 && rv != NGHTTP2_ERR_WOULDBLOCK)
					break;
			}

			if (rc & WGET_IO_WRITABLE) {
				int rv = nghttp2_session_send(session_data.session);
				if (rv != 0 && rv != NGHTTP2_ERR_WOULDBLOCK)
					break;
			}
		}

		// Cleanup
		nghttp2_session_del(session_data.session);
		gnutls_bye(tls_session, GNUTLS_SHUT_RDWR);
		gnutls_deinit(tls_session);
		close(client_fd);
	}

	return NULL;
}

static int h2_server_start(void)
{
	if (nghttp2_server)
		return 0;  // Already running

	nghttp2_server = wget_calloc(1, sizeof(h2_server_t));
	if (!nghttp2_server)
		return -1;

	// Create listen socket (IPv6 dual-stack)
	nghttp2_server->listen_fd = socket(AF_INET6, SOCK_STREAM, 0);
	if (nghttp2_server->listen_fd < 0) {
		wget_free(nghttp2_server);
		nghttp2_server = NULL;
		return -1;
	}

	int optval = 1;
	setsockopt(nghttp2_server->listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

	// Disable IPv6-only to allow IPv4 connections on the same socket
	int ipv6only = 0;
	setsockopt(nghttp2_server->listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, sizeof(ipv6only));

	struct sockaddr_in6 server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin6_family = AF_INET6;
	server_addr.sin6_addr = in6addr_loopback;
	server_addr.sin6_port = 0;  // Let OS assign port

	if (bind(nghttp2_server->listen_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) {
		close(nghttp2_server->listen_fd);
		wget_free(nghttp2_server);
		nghttp2_server = NULL;
		return -1;
	}

	// Get assigned port
	socklen_t addr_len = sizeof(server_addr);
	if (getsockname(nghttp2_server->listen_fd, (struct sockaddr *) &server_addr, &addr_len) < 0) {
		close(nghttp2_server->listen_fd);
		wget_free(nghttp2_server);
		nghttp2_server = NULL;
		return -1;
	}

	h2_server_port = ntohs(server_addr.sin6_port);

	if (listen(nghttp2_server->listen_fd, 5) < 0) {
		close(nghttp2_server->listen_fd);
		wget_free(nghttp2_server);
		nghttp2_server = NULL;
		return -1;
	}

	// Setup TLS credentials
	gnutls_certificate_allocate_credentials(&nghttp2_server->creds);
	gnutls_certificate_set_x509_key_file(nghttp2_server->creds,
		SRCDIR "/certs/x509-server-cert.pem",
		SRCDIR "/certs/x509-server-key.pem",
		GNUTLS_X509_FMT_PEM);

	// Start server thread
	nghttp2_server->running = 1;
	if (pthread_create(&nghttp2_server->thread, NULL, h2_server_thread, NULL) != 0) {
		gnutls_certificate_free_credentials(nghttp2_server->creds);
		close(nghttp2_server->listen_fd);
		wget_free(nghttp2_server);
		nghttp2_server = NULL;
		return -1;
	}

	wget_info_printf("HTTP/2 server started on port %d\n", h2_server_port);

	return 0;
}

static void h2_server_stop(void)
{
	if (!nghttp2_server)
		return;

	nghttp2_server->running = 0;

	// Shutdown the listen socket to unblock accept()
	shutdown(nghttp2_server->listen_fd, SHUT_RDWR);

	pthread_join(nghttp2_server->thread, NULL);

	gnutls_certificate_free_credentials(nghttp2_server->creds);
	close(nghttp2_server->listen_fd);

	wget_free(nghttp2_server);
	nghttp2_server = NULL;

	h2_server_port = 0;
}

#endif // WITH_LIBNGHTTP2

static void _http_server_stop(void)
{
	MHD_stop_daemon(httpdaemon);
	MHD_stop_daemon(httpsdaemon);
	MHD_stop_daemon(ocspdaemon);

#ifdef WITH_LIBNGHTTP2
	h2_server_stop();
#endif

	wget_xfree(key_pem);
	wget_xfree(cert_pem);

	// Don't call gnutls_global_deinit() here as it breaks subsequent tests
	// that need TLS. GnuTLS will clean up automatically at process exit.
}

static enum MHD_Result _check_to_accept(
	void *cls,
	WGET_GCC_UNUSED const struct sockaddr *addr,
	WGET_GCC_UNUSED socklen_t addrlen)
{
	int server_mode = (int) (ptrdiff_t) cls;

	if (server_mode == HTTP_MODE)
		return reject_http_connection ? MHD_NO : MHD_YES;

	return reject_https_connection ? MHD_NO : MHD_YES;
}

static int _http_server_start(int SERVER_MODE)
{
	uint16_t port_num = 0;

	if (SERVER_MODE == HTTP_MODE) {
		static char rnd[8] = "realrnd"; // fixed 'random' value

		httpdaemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY | MHD_USE_DUAL_STACK,
			port_num, _check_to_accept,
			(void *) (ptrdiff_t) SERVER_MODE, _answer_to_connection, NULL,
			MHD_OPTION_DIGEST_AUTH_RANDOM, sizeof(rnd), rnd,
			MHD_OPTION_NONCE_NC_SIZE, 300,
			MHD_OPTION_STRICT_FOR_CLIENT, 1,
			MHD_OPTION_SERVER_INSANITY, 1,
			MHD_OPTION_END);

		if (!httpdaemon)
			return 1;
	} else if (SERVER_MODE == HTTPS_MODE || SERVER_MODE == H2_MODE) {
		size_t size;

		if (!ocspdaemon) {
			key_pem = wget_read_file(custom_server_key, &size);
			cert_pem = wget_read_file(custom_server_cert, &size);

			if ((key_pem == NULL) || (cert_pem == NULL))
			{
				wget_error_printf("The key/certificate files could not be read.\n");
				return 1;
			}

			if (SERVER_MODE == HTTPS_MODE) {
				httpsdaemon = MHD_start_daemon(
					MHD_USE_SELECT_INTERNALLY | MHD_USE_TLS | MHD_USE_POST_HANDSHAKE_AUTH_SUPPORT | MHD_USE_DUAL_STACK,
					port_num, _check_to_accept,
					(void *) (ptrdiff_t) SERVER_MODE, _answer_to_connection, NULL,
					MHD_OPTION_HTTPS_MEM_KEY, key_pem,
					MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
					MHD_OPTION_STRICT_FOR_CLIENT, 1,
					MHD_OPTION_SERVER_INSANITY, 1,
					MHD_OPTION_CONNECTION_MEMORY_LIMIT, (size_t) 1*1024*1024,
					MHD_OPTION_END);

				if (!httpsdaemon) {
					wget_error_printf("Cannot start the HTTPS server.\n");
					return 1;
				}
			}
		}
#ifdef WITH_GNUTLS_OCSP
		else {
			httpsdaemon = MHD_start_daemon(
				MHD_USE_SELECT_INTERNALLY | MHD_USE_TLS | MHD_USE_POST_HANDSHAKE_AUTH_SUPPORT | MHD_USE_DUAL_STACK,
				port_num, _check_to_accept,
				(void *) (ptrdiff_t) SERVER_MODE, _answer_to_connection, NULL,
				MHD_OPTION_HTTPS_CERT_CALLBACK, _ocsp_cert_callback,
				MHD_OPTION_STRICT_FOR_CLIENT, 1,
				MHD_OPTION_SERVER_INSANITY, 1,
				MHD_OPTION_CONNECTION_MEMORY_LIMIT, (size_t) 1*1024*1024,
				MHD_OPTION_END);

			int rc;
			gnutls_datum_t data;

			privkey = wget_malloc(sizeof(gnutls_privkey_t));
			gnutls_privkey_init(privkey);

			if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-server-key.pem", &data)) < 0)
				file_load_err(SRCDIR "/certs/ocsp/x509-server-key.pem", gnutls_strerror(rc));

			gnutls_privkey_import_x509_raw(*privkey, &data, GNUTLS_X509_FMT_PEM, NULL, 0);
			wget_xfree(data.data);

			pcrt = wget_malloc(sizeof(gnutls_pcert_st)*2);

			if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-server-cert.pem", &data)) < 0)
				file_load_err(SRCDIR "/certs/ocsp/x509-server-cert.pem", gnutls_strerror(rc));

			gnutls_pcert_import_x509_raw(pcrt, &data, GNUTLS_X509_FMT_PEM, 0);
			wget_xfree(data.data);

			if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-interm-cert.pem", &data)) < 0)
				file_load_err(SRCDIR "/certs/ocsp/x509-interm-cert.pem", gnutls_strerror(rc));

			gnutls_pcert_import_x509_raw(pcrt+1, &data, GNUTLS_X509_FMT_PEM, 0);
			wget_xfree(data.data);

			if (!httpsdaemon) {
				wget_error_printf("Cannot start the HTTPS server.\n");
				return 1;
			}

		}
#endif
	} else if (SERVER_MODE == OCSP_MODE) {
#ifdef WITH_GNUTLS_OCSP
		static char rnd[8] = "realrnd"; // fixed 'random' value

		ocspdaemon = MHD_start_daemon(MHD_USE_SELECT_INTERNALLY | MHD_USE_DUAL_STACK,
			port_num, NULL, NULL, _ocsp_ahc, NULL,
			MHD_OPTION_DIGEST_AUTH_RANDOM, sizeof(rnd), rnd,
			MHD_OPTION_NONCE_NC_SIZE, 300,
			MHD_OPTION_STRICT_FOR_CLIENT, 1,
			MHD_OPTION_SERVER_INSANITY, 1,
			MHD_OPTION_CONNECTION_MEMORY_LIMIT, (size_t) 1*1024*1024,
			MHD_OPTION_END);
#endif

		if (!ocspdaemon)
			return 1;
	}
#ifdef WITH_GNUTLS_OCSP
	else if (SERVER_MODE == OCSP_STAP_MODE) {
		int rc;

		gnutls_datum_t data;

		/* Load private key */
		privkey = wget_malloc(sizeof(gnutls_privkey_t));

		gnutls_privkey_init(privkey);

		if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-server-key.pem", &data)) < 0)
			file_load_err(SRCDIR "/certs/ocsp/x509-server-key.pem", gnutls_strerror(rc));

		gnutls_privkey_import_x509_raw(*privkey, &data, GNUTLS_X509_FMT_PEM, NULL, 0);
		wget_xfree(data.data);

		/* Load certificate chain */
		pcrt = wget_malloc(sizeof(gnutls_pcert_st) * 2);

		if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-server-cert.pem", &data)) < 0)
			file_load_err(SRCDIR "/certs/ocsp/x509-server-cert.pem", gnutls_strerror(rc));

		gnutls_pcert_import_x509_raw(pcrt, &data, GNUTLS_X509_FMT_PEM, 0);
		wget_xfree(data.data);

		if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/x509-interm-cert.pem", &data)) < 0)
			file_load_err(SRCDIR "/certs/ocsp/x509-interm-cert.pem", gnutls_strerror(rc));

		gnutls_pcert_import_x509_raw(pcrt+1, &data, GNUTLS_X509_FMT_PEM, 0);
		wget_xfree(data.data);

		/* Load stapled OCSP response */
		ocsp_stap_resp = wget_malloc(sizeof(gnutls_ocsp_data_st));

		if ((rc = gnutls_load_file(SRCDIR "/certs/ocsp/ocsp_stapled_resp.der", &data)) < 0)
			file_load_err(SRCDIR "/certs/ocsp/ocsp_stapled_resp.der", gnutls_strerror(rc));

		ocsp_stap_resp->response.data = data.data;
		ocsp_stap_resp->response.size = data.size;
		ocsp_stap_resp->exptime = 0;

		/* Start HTTPS daemon with stapled OCSP responses */
		httpsdaemon = MHD_start_daemon(
			MHD_USE_SELECT_INTERNALLY | MHD_USE_TLS | MHD_USE_POST_HANDSHAKE_AUTH_SUPPORT | MHD_USE_DUAL_STACK,
			port_num, _check_to_accept,
			(void *) (ptrdiff_t) SERVER_MODE, _answer_to_connection, NULL,
			MHD_OPTION_HTTPS_CERT_CALLBACK2, _ocsp_stap_cert_callback,
			MHD_OPTION_STRICT_FOR_CLIENT, 1,
			MHD_OPTION_SERVER_INSANITY, 1,
			MHD_OPTION_CONNECTION_MEMORY_LIMIT, (size_t) 1*1024*1024,
			MHD_OPTION_END);
	}
#endif

	// get open random port number
	if (MHD_NO != MHD_is_feature_supported(MHD_FEATURE_AUTODETECT_BIND_PORT))
	{
		const union MHD_DaemonInfo *dinfo = NULL;
		if (SERVER_MODE == HTTP_MODE)
			dinfo = MHD_get_daemon_info(httpdaemon, MHD_DAEMON_INFO_BIND_PORT);
		else if (SERVER_MODE == HTTPS_MODE || SERVER_MODE == OCSP_STAP_MODE)
			dinfo = MHD_get_daemon_info(httpsdaemon, MHD_DAEMON_INFO_BIND_PORT);
#ifdef WITH_GNUTLS_OCSP
		else if (SERVER_MODE == OCSP_MODE)
			dinfo = MHD_get_daemon_info(ocspdaemon, MHD_DAEMON_INFO_BIND_PORT);
#endif

		if (!dinfo || dinfo->port == 0)
			return 1;

		port_num = dinfo->port;
		if (SERVER_MODE == HTTP_MODE)
			http_server_port = port_num;
		else if (SERVER_MODE == HTTPS_MODE || SERVER_MODE == OCSP_STAP_MODE)
			https_server_port = port_num;
#ifdef WITH_GNUTLS_OCSP
		else if (SERVER_MODE == OCSP_MODE)
			ocsp_server_port = port_num;
#endif
#ifdef WITH_LIBNGHTTP2
		else if (SERVER_MODE == H2_MODE) {
			h2_server_port = port_num;
		}
#endif
	}
	else
	{
		const union MHD_DaemonInfo *dinfo = NULL;
		int sock_fd;

		if (SERVER_MODE == HTTP_MODE)
			dinfo = MHD_get_daemon_info(httpdaemon, MHD_DAEMON_INFO_LISTEN_FD);
		else if (SERVER_MODE == HTTPS_MODE || SERVER_MODE == OCSP_STAP_MODE)
			dinfo = MHD_get_daemon_info(httpsdaemon, MHD_DAEMON_INFO_LISTEN_FD);
#ifdef WITH_GNUTLS_OCSP
		else if (SERVER_MODE == OCSP_MODE)
			dinfo = MHD_get_daemon_info(ocspdaemon, MHD_DAEMON_INFO_LISTEN_FD);
#endif

		if (!dinfo)
			return 1;
#ifdef _WIN32
		sock_fd = _open_osfhandle(dinfo->listen_fd, O_RDWR | O_BINARY);
#else
		sock_fd = dinfo->listen_fd;
#endif

		struct sockaddr_storage addr_store;
		struct sockaddr *addr = (struct sockaddr *)&addr_store;
		socklen_t addr_len = sizeof(addr_store);

		// get automatic retrieved port number
		if (getsockname(sock_fd, addr, &addr_len) == 0) {
			char s_port[NI_MAXSERV];

			if (getnameinfo(addr, addr_len, NULL, 0, s_port, sizeof(s_port), NI_NUMERICSERV) == 0) {
				port_num = (uint16_t)atoi(s_port);
				if (SERVER_MODE == HTTP_MODE)
					http_server_port = port_num;
				else if (SERVER_MODE == HTTPS_MODE || SERVER_MODE == OCSP_STAP_MODE)
					https_server_port = port_num;
#ifdef WITH_GNUTLS_OCSP
				else if (SERVER_MODE == OCSP_MODE)
					ocsp_server_port = port_num;
#endif

#ifdef WITH_LIBNGHTTP2
				else if (SERVER_MODE == H2_MODE)
					h2_server_port = port_num;
#endif
			}
		}
	}

	return 0;
}

#if defined __CYGWIN__
// Using opendir/readdir loop plus unlink() has a race condition
// with CygWin. Not sure if this also happens on other systems as well.
// Since we don't have valgrind, we can use system() without issues.
static void _remove_directory(const char *dirname)
{
	char cmd[strlen(dirname) + 16];

	wget_snprintf(cmd, sizeof(cmd), "rm -rf %s", dirname);
	system(cmd);
}
static void _empty_directory(const char *dirname)
{
	_remove_directory(dirname);

	if (mkdir(dirname, 0755) != 0)
		wget_error_printf_exit("Failed to re-create directory (%d)\n", errno);
}
#else
// To reduce the verbosity of 'valgrind --trace-children=yes' output,
//   we avoid system("rm -rf ...") calls.
static void _remove_directory(const char *dirname);
static void _empty_directory(const char *dirname)
{
	DIR *dir;

	if ((dir = opendir(dirname))) {
		struct dirent *dp;

		while ((dp = readdir(dir))) {
			if (*dp->d_name == '.' && (dp->d_name[1] == 0 || (dp->d_name[1] == '.' && dp->d_name[2] == 0)))
				continue;

			char *fname = wget_aprintf("%s/%s", dirname, dp->d_name);

			if (unlink(fname) == -1) {
				// in case fname is a directory glibc returns EISDIR but correct POSIX value would be EPERM.
				// MinGW + Wine returns EACCESS here.
				if (errno == EISDIR || errno == EPERM || errno == EACCES)
					_remove_directory(fname);
				else
					wget_error_printf("Failed to unlink %s (%d)\n", fname, errno);
			}

			wget_xfree(fname);
		}

		closedir(dir);

		wget_debug_printf("Removed test directory '%s'\n", dirname);
	} else if (errno != ENOENT)
		wget_error_printf("Failed to opendir %s (%d)\n", dirname, errno);
}

static void _remove_directory(const char *dirname)
{
	_empty_directory(dirname);
	if (rmdir(dirname) == -1 && errno != ENOENT)
		wget_error_printf("Failed to rmdir %s (%d)\n", dirname, errno);
}
#endif

void wget_test_stop_server(void)
{
//	wget_vector_free(&response_headers);
	wget_vector_free(&request_urls);
	wget_vector_free(&ocsp_responses);

	for (wget_test_url_t *url = urls; url < urls + nurls; url++) {
		if (url->body_original) {
			wget_xfree(url->body);
			url->body_original = NULL;
		}

		for (size_t it = 0; it < countof(url->headers); it++) {
			if (url->headers_original[it]) {
				wget_xfree(url->headers[it]);
				url->headers_original[it] = NULL;
			}
		}
	}

	if (chdir("..") != 0)
		wget_error_printf("Failed to chdir ..\n");

	if (!keep_tmpfiles)
		_remove_directory(tmpdir);

	wget_global_deinit();
	_http_server_stop();
}

static char *_insert_ports(const char *src)
{
	if (!src || (!strstr(src, "{{port}}") && !strstr(src, "{{sslport}}") && !strstr(src, "{{ocspport}}")))
		return NULL;

	size_t srclen = strlen(src) + 1;
	char *ret = wget_malloc(srclen);
	char *dst = ret;

	while (*src) {
		if (*src == '{') {
			if (!strncmp(src, "{{port}}", 8)) {
				if (proto_pass == HTTP_1_1_PASS) {
					dst += wget_snprintf(dst, srclen - (dst - ret), "%d", http_server_port);
				}
#ifdef WITH_LIBNGHTTP2
				else {
					dst += wget_snprintf(dst, srclen - (dst - ret), "%d", reject_https_connection ? http_server_port : h2_server_port);
				}
#endif
				src += 8;
				continue;
			}
			else if (!strncmp(src, "{{sslport}}", 11)) {
				if (proto_pass == HTTP_1_1_PASS) {
					dst += wget_snprintf(dst, srclen - (dst - ret), "%d", https_server_port);
				}
#ifdef WITH_LIBNGHTTP2
				else {
					dst += wget_snprintf(dst, srclen - (dst - ret), "%d", h2_server_port);
				}
#endif
				src += 11;
				continue;
			}
			else if (!strncmp(src, "{{ocspport}}", 12)) {
				dst += wget_snprintf(dst, srclen - (dst - ret), "%d", ocsp_server_port);
				src += 12;
				continue;
			}
		}

		*dst++ = *src++;
	}
	*dst = 0;

	return ret;
}

static void _write_msg(const char *msg, size_t len)
{
#ifdef _WIN32
	fwrite(msg, 1, len, stderr);
#else
	if (isatty(fileno(stderr))) {
		if (len && msg[len - 1] == '\n')
			len--;

		wget_fprintf(stderr, "\033[33m%.*s\033[m\n", (int) len, msg);
	} else
		fwrite(msg, 1, len, stderr);
#endif
}

void wget_test_start_server(int first_key, ...)
{
	int rc, key;
	va_list args;
	bool start_http = 1;

	custom_server_key = SRCDIR "/certs/x509-server-key.pem";
	custom_server_cert = SRCDIR "/certs/x509-server-cert.pem";

#ifdef WITH_TLS
	bool start_https = 1;
#ifdef WITH_GNUTLS_OCSP
	bool ocsp_stap = 0;
	bool start_ocsp = 0;
#endif
#ifdef WITH_LIBNGHTTP2
	bool start_h2 = 0;  // Changed to 0: HTTP/2 is now opt-in with WGET_TEST_H2_ONLY
#endif
#endif

	wget_global_init(
		WGET_DEBUG_FUNC, _write_msg,
		WGET_ERROR_FUNC, _write_msg,
		WGET_INFO_FUNC, _write_msg,
		0);

	wget_debug_printf("MHD compiled with 0x%08x, linked with %s\n", (unsigned) MHD_VERSION, MHD_get_version());
	wget_debug_printf("MHD_OPTION_STRICT_FOR_CLIENT: yes\n");
	wget_debug_printf("MHD_OPTION_SERVER_INSANITY: yes\n");
#ifdef WITH_LIBNGHTTP2
	wget_debug_printf("WITH_LIBNGHTTP2: yes\n");
#else
	wget_debug_printf("WITH_LIBNGHTTP2: no\n");
#endif
#ifdef HAVE_GNUTLS_OCSP_H
	wget_debug_printf("HAVE_GNUTLS_OCSP_H: yes\n");
#else
	wget_debug_printf("HAVE_GNUTLS_OCSP_H: no\n");
#endif
	wget_debug_printf("\n");

	va_start(args, first_key);
	for (key = first_key; key; key = va_arg(args, int)) {
		switch (key) {
/*		case WGET_TEST_RESPONSE_BODY:
			response_body = va_arg(args, const char *);
			break;
		case WGET_TEST_RESPONSE_HEADER:
			if (!response_headers)
				response_headers = wget_vector_create(4,4,NULL);
			wget_vector_add_str(response_headers, va_arg(args, const char *));
			break;
		case WGET_TEST_RESPONSE_CODE:
			response_code = va_arg(args, const char *);
			break;
*/		case WGET_TEST_RESPONSE_URLS:
			urls = va_arg(args, wget_test_url_t *);
			nurls = va_arg(args, size_t);
			break;
		case WGET_TEST_SERVER_SEND_CONTENT_LENGTH:
			server_send_content_length = !!va_arg(args, int);
			break;
		case WGET_TEST_HTTPS_ONLY:
			start_http = 0;
			break;
		case WGET_TEST_HTTP_ONLY:
#ifdef WITH_TLS
			start_https = 0;
#ifdef WITH_LIBNGHTTP2
			start_h2 = 0;
#endif
#endif
			break;
		case WGET_TEST_H2_ONLY:
			start_http = 0;
#ifdef WITH_TLS
			start_https = 0;
#endif
#ifdef WITH_LIBNGHTTP2
			start_h2 = 1;
#endif
			break;
		case WGET_TEST_HTTP_REJECT_CONNECTIONS:
			reject_http_connection = 1;
			break;
		case WGET_TEST_HTTPS_REJECT_CONNECTIONS:
			reject_https_connection = 1;
			break;
		case WGET_TEST_FEATURE_MHD:
			break;
		case WGET_TEST_FEATURE_TLS:
#if !defined WITH_TLS
			wget_error_printf("Test requires TLS. Skipping\n");
			exit(WGET_TEST_EXIT_SKIP);
#endif
			break;
		case WGET_TEST_FEATURE_IDN:
#if !defined WITH_LIBIDN && !defined WITH_LIBIDN2
			wget_error_printf("Support for LibIDN not found. Skipping\n");
			exit(WGET_TEST_EXIT_SKIP);
#endif
			break;
		case WGET_TEST_FEATURE_PLUGIN:
#ifndef PLUGIN_SUPPORT
			wget_error_printf("Plugin Support Disabled. Skipping\n");
			exit(WGET_TEST_EXIT_SKIP);
#endif
			break;
		case WGET_TEST_FEATURE_OCSP:
#if !defined WITH_GNUTLS_OCSP
			wget_error_printf("Test requires GnuTLS with OCSP support. Skipping\n");
			exit(WGET_TEST_EXIT_SKIP);
#else
			start_http = 0;
#ifdef WITH_LIBNGHTTP2
			start_h2 = 0;
#endif
#ifdef WITH_TLS
#ifdef WITH_GNUTLS_OCSP
			start_ocsp = 1;
#endif
#endif
			break;
#endif
		case WGET_TEST_FEATURE_OCSP_STAPLING:
#if !defined WITH_GNUTLS_OCSP
			wget_error_printf("MHD or GnuTLS version insufficient. Skipping\n");
			exit(WGET_TEST_EXIT_SKIP);
#else
			start_http = 0;
#ifdef WITH_TLS
			start_https = 0;
#endif
#ifdef WITH_LIBNGHTTP2
			start_h2 = 0;
#endif
#ifdef WITH_TLS
#ifdef WITH_GNUTLS_OCSP
			ocsp_stap = 1;
#endif
#endif
			break;
#endif
		case WGET_TEST_SKIP_H2:
#ifdef WITH_LIBNGHTTP2
			start_h2 = 0;
#endif
			break;
		case WGET_TEST_HTTPS_CERT_FILE:
			custom_server_cert = va_arg(args, const char *);
			break;
		case WGET_TEST_HTTPS_KEY_FILE:
			custom_server_key = va_arg(args, const char *);
			break;
		default:
			wget_error_printf("Unknown option %d\n", key);
		}
	}
	va_end(args);

	if (!atexit_registered) {
		atexit(wget_test_stop_server);
		atexit_registered = true;
	}

	wget_snprintf(tmpdir, sizeof(tmpdir), ".test_%d", (int) getpid());

	// remove tmpdir if exists from previous tests
	_remove_directory(tmpdir);

	if (mkdir(tmpdir, 0755) != 0)
		wget_error_printf_exit("Failed to create tmpdir (%d)\n", errno);

	if (chdir(tmpdir) != 0)
		wget_error_printf_exit("Failed to change to tmpdir (%d)\n", errno);

	// start HTTP server
	if (start_http) {
		if ((rc = _http_server_start(HTTP_MODE)) != 0)
			wget_error_printf_exit("Failed to start HTTP server, error %d\n", rc);
	}

#ifdef WITH_TLS
#ifdef WITH_GNUTLS_OCSP
	// start OCSP responder
	if (start_ocsp) {
		if ((rc = _http_server_start(OCSP_MODE)) != 0)
			wget_error_printf_exit("Failed to start OCSP server, error %d\n", rc);
	}

	// start OCSP server (stapling)
	if (ocsp_stap) {
		if ((rc = _http_server_start(OCSP_STAP_MODE)) != 0)
			wget_error_printf_exit("Failed to start OCSP Stapling server, error %d\n", rc);
	}
#endif

	// start HTTPS server
	if (start_https) {
		if ((rc = _http_server_start(HTTPS_MODE)) != 0)
			wget_error_printf_exit("Failed to start HTTPS server, error %d\n", rc);
	}

#ifdef WITH_LIBNGHTTP2
	// start libnghttp2 h2 server
	if (start_h2) {
		if ((rc = h2_server_start()) != 0)
			wget_error_printf_exit("Failed to start HTTP/2 server, error %d\n", rc);
	}
#endif
#endif
}

static void _scan_for_unexpected(const char *dirname, const wget_test_file_t *expected_files)
{
	DIR *dir;
	struct stat st;

	wget_info_printf("Entering %s\n", dirname);

	if ((dir = opendir(dirname))) {
		struct dirent *dp;

		while ((dp = readdir(dir))) {
			if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, ".."))
				continue;

			char *fname;
			if (*dirname == '.' && dirname[1] == 0)
				fname = wget_strdup(dp->d_name);
			else
				fname = wget_aprintf("%s/%s", dirname, dp->d_name);

			wget_info_printf(" - %s/%s\n", dirname, dp->d_name);
			if (stat(fname, &st) == 0 && S_ISDIR(st.st_mode)) {
				_scan_for_unexpected(fname, expected_files);
				wget_xfree(fname);
				continue;
			}

			if (expected_files) {
// Mac OS X converts to NFD, so we might find an unexpected file name, e.g. when using accents.
// Example: cedilla (%C3%A7) will be converted to c+composed_cedilla (%63%CC%A7)
// Since there are a few pitfalls with Apple's NFD, just skip the check here.
#if !(defined __APPLE__ && defined __MACH__)
				size_t it;

				wget_info_printf("search %s\n", fname);

				for (it = 0; expected_files[it].name; it++) {
#ifdef _WIN32
					char buf[strlen(expected_files[it].name) * 3 + 1];
					const char *restricted_fname = wget_restrict_file_name(expected_files[it].name, buf,
						expected_files[it].restricted_mode ? expected_files[it].restricted_mode : WGET_RESTRICT_NAMES_WINDOWS);
#else
					const char *restricted_fname = expected_files[it].name;
#endif
/*
					{
						char b[256];
						if (it==0) {
							wget_memtohex(fname, strlen(fname), b, sizeof(b));
							wget_debug_printf("f %s\n", b);
						}
						wget_memtohex(restricted_fname, strlen(restricted_fname), b, sizeof(b));
						wget_debug_printf("r %s\n", b);
					}
*/
					if (!strcmp(restricted_fname, fname))
						break;
				}

				if (!expected_files[it].name)
					wget_error_printf_exit("Unexpected file %s/%s found\n", tmpdir, fname);
#endif
			} else
				wget_error_printf_exit("Unexpected file %s/%s found\n", tmpdir, fname);

			wget_xfree(fname);
		}

		closedir(dir);
	} else
		wget_error_printf_exit("Failed to diropen %s\n", dirname);
}

static const char *global_executable;
void wget_test_set_executable(const char *program)
{
	global_executable = program;
}

void wget_test(int first_key, ...)
{
#ifndef WITH_LIBNGHTTP2
	if (!httpdaemon && !httpsdaemon)
		exit(WGET_TEST_EXIT_SKIP);
#endif

	for (proto_pass = 0; proto_pass < END_PASS; proto_pass++) {
		if (proto_pass == HTTP_1_1_PASS && !httpdaemon && !httpsdaemon)
			continue;

		if (proto_pass == H2_PASS) {
#ifndef WITH_LIBNGHTTP2
			continue;
#endif
#ifdef WITH_LIBNGHTTP2
			if (!nghttp2_server)
				continue;
#else
			continue;  // No HTTP/2 support without libnghttp2
#endif
		}

		// now replace {{port}} in the body by the actual server port
		for (wget_test_url_t *url = urls; url < urls + nurls; url++) {
			char *p = _insert_ports(url->body);

			if (p) {
				url->body_original = url->body;
				url->body = p;
			}

			for (unsigned it = 0; it < countof(url->headers) && url->headers[it]; it++) {
				p = _insert_ports(url->headers[it]);

				if (p) {
					url->headers_original[it] = url->headers[it];
					url->headers[it] = p;
				}
			}
		}

		const char
			*request_url,
			*options = "",
			*executable = global_executable;
		const wget_test_file_t
			*expected_files = NULL,
			*existing_files = NULL;
		wget_buffer
			*cmd = wget_buffer_alloc(1024);
		unsigned
			it;
		int
			key,
			fd,
			rc,
			expected_error_code2 = -1,
			expected_error_code = 0;
		va_list
			args;
		char
			server_send_content_length_old = server_send_content_length;
		bool
			options_alloc = 0;

		if (!executable) {
#if defined _WIN32 && !defined __MINGW32__
			if (proto_pass == H2_PASS)
				executable = BUILDDIR "\\..\\src\\wget2_noinstall" EXEEXT " -d --no-config --no-local-db --max-threads=1 --prefer-family=ipv4 --no-proxy --timeout 3 --tries=1 --https-enforce=hard --ca-certificate=" SRCDIR "/certs/x509-ca-cert.pem --no-ocsp";
			else
				executable = BUILDDIR "\\..\\src\\wget2_noinstall" EXEEXT " -d --no-config --no-local-db --max-threads=1 --prefer-family=ipv4 --no-proxy --timeout 3 --tries=1";
#else
			if (proto_pass == H2_PASS)
				executable = BUILDDIR "/../src/wget2_noinstall" EXEEXT " -d --no-config --no-local-db --max-threads=1 --prefer-family=ipv4 --no-proxy --timeout 3  --tries=1 --https-enforce=hard --ca-certificate=" SRCDIR "/certs/x509-ca-cert.pem --no-ocsp";
			else
				executable = BUILDDIR "/../src/wget2_noinstall" EXEEXT " -d --no-config --no-local-db --max-threads=1 --prefer-family=ipv4 --no-proxy --timeout 3 --tries=1";
#endif
		}

		keep_tmpfiles = 0;
		clean_directory = 1;
		h2_min_concurrent_streams = 0;

		if (!request_urls) {
			request_urls = wget_vector_create(8, NULL);
			wget_vector_set_destructor(request_urls, NULL);
		}

#ifdef WITH_GNUTLS_OCSP
		if (!ocsp_responses) {
			ocsp_responses = wget_vector_create(2, NULL);
		}
#endif

		va_start (args, first_key);
		for (key = first_key; key; key = va_arg(args, int)) {
			switch (key) {
			case WGET_TEST_REQUEST_URL:
				if ((request_url = va_arg(args, const char *)))
					wget_vector_add(request_urls, request_url);
				break;
			case WGET_TEST_REQUEST_URLS:
				while ((request_url = va_arg(args, const char *)))
					wget_vector_add(request_urls, request_url);
				break;
			case WGET_TEST_EXPECTED_ERROR_CODE:
				expected_error_code = va_arg(args, int);
				break;
			case WGET_TEST_EXPECTED_ERROR_CODE2:
				expected_error_code2 = va_arg(args, int);
				break;
			case WGET_TEST_EXPECTED_FILES:
				expected_files = va_arg(args, const wget_test_file_t *);
				break;
			case WGET_TEST_EXISTING_FILES:
				existing_files = va_arg(args, const wget_test_file_t *);
				break;
			case WGET_TEST_OPTIONS:
			{
				options = va_arg(args, const char *);
				const char *tmp = _insert_ports(options);
				if (tmp) {
					options = tmp;
					options_alloc = 1;
				}
				break;
			}
			case WGET_TEST_KEEP_TMPFILES:
				keep_tmpfiles = va_arg(args, int);
				break;
			case WGET_TEST_CLEAN_DIRECTORY:
				clean_directory = va_arg(args, int);
				break;
			case WGET_TEST_EXECUTABLE:
				executable = va_arg(args, const char *);
				break;
			case WGET_TEST_SERVER_SEND_CONTENT_LENGTH:
				server_send_content_length = !!va_arg(args, int);
				break;
			case WGET_TEST_H2_MIN_CONCURRENT_STREAMS:
				h2_min_concurrent_streams = va_arg(args, int);
				break;
			case WGET_TEST_POST_HANDSHAKE_AUTH:
				if (va_arg(args, int)) {
					post_handshake_auth = wget_malloc(sizeof(enum CHECK_POST_HANDSHAKE_AUTH));
				}
				break;
			case WGET_TEST_OCSP_RESP_FILES:
#ifdef WITH_GNUTLS_OCSP
			{
				const char *ocsp_resp_file = NULL;
				while ((ocsp_resp_file = va_arg(args, const char *))) {
					if (ocspdaemon) {
						ocsp_resp_t ocsp_resp = { .data = NULL, .size = 0 };
						if (*ocsp_resp_file) {
							ocsp_resp.data = wget_read_file(ocsp_resp_file, &ocsp_resp.size);
							if (ocsp_resp.data == NULL) {
								wget_error_printf_exit("Couldn't read the response from '%s'.\n", ocsp_resp_file);
							}
						}
						wget_vector_add_memdup(ocsp_responses, &ocsp_resp, sizeof(ocsp_resp));
					}
				}
				ocsp_response_pos = 0;
			}
#endif
				break;
			default:
				wget_error_printf_exit("Unknown option %d [%s]\n", key, options);
			}
		}
		va_end(args);

		if (clean_directory) {
			// clean directory
			wget_buffer_printf(cmd, "../%s", tmpdir);
			_empty_directory(cmd->data);
		}

		// create files
		if (existing_files) {
			for (it = 0; existing_files[it].name; it++) {
				mkdir_path(existing_files[it].name, 1);

				if (existing_files[it].hardlink) {
					if (link(existing_files[it].hardlink, existing_files[it].name) != 0) {
						wget_error_printf_exit("Failed to link %s/%s -> %s/%s [%s]\n",
							tmpdir, existing_files[it].hardlink,
							tmpdir, existing_files[it].name, options);
					}
				}
				else if ((fd = open(existing_files[it].name, O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, 0644)) != -1) {
					const char *existing_content = _insert_ports(existing_files[it].content);
					if (!existing_content)
						existing_content = existing_files[it].content;

					ssize_t nbytes = write(fd, existing_content, strlen(existing_content));
					close(fd);

					if (nbytes != (ssize_t)strlen(existing_content))
						wget_error_printf_exit("Failed to write %zu bytes to file %s/%s [%s]\n",
							strlen(existing_content), tmpdir, existing_files[it].name, options);

					if (existing_files[it].timestamp) {
						// take the old utime() instead of utimes()
						if (utime(existing_files[it].name, &(struct utimbuf){ 0, existing_files[it].timestamp }))
							wget_error_printf_exit("Failed to set mtime of %s/%s [%s]\n",
								tmpdir, existing_files[it].name, options);
					}

					if (existing_content != existing_files[it].content)
						wget_xfree(existing_content);

				} else {
					wget_error_printf_exit("Failed to write open file %s/%s [%s] (%d,%s)\n",
						tmpdir, *existing_files[it].name == '/' ? existing_files[it].name + 1 : existing_files[it].name , options,
						errno, strerror(errno));
				}
			}
		}

		const char *valgrind = getenv("VALGRIND_TESTS");
		if (!valgrind || !*valgrind || !strcmp(valgrind, "0")) {
			// On some system we get random IP order (v4, v6) for localhost, so we need --prefer-family for testing since
			// the test servers will listen only on the first IP and also prefers IPv4
			const char *emulator = getenv("EMULATOR");
			if (emulator && *emulator)
				wget_buffer_printf(cmd, "%s %s %s", emulator, executable, options);
			else
				wget_buffer_printf(cmd, "%s %s", executable, options);
		} else if (!strcmp(valgrind, "1")) {
			wget_buffer_printf(cmd, "valgrind --error-exitcode=301 --leak-check=yes --show-reachable=yes --track-origins=yes --child-silent-after-fork=yes --suppressions=" SRCDIR "/valgrind-suppressions --gen-suppressions=all %s %s", executable, options);
		} else
			wget_buffer_printf(cmd, "%s %s %s", valgrind, executable, options);

		for (it = 0; it < (size_t)wget_vector_size(request_urls); it++) {
			request_url = wget_vector_get(request_urls, it);

			if (!wget_strncasecmp_ascii(request_url, "http://", 7)
				|| !wget_strncasecmp_ascii(request_url, "https://", 8))
			{
				char *tmp = _insert_ports(request_url);
				wget_buffer_printf_append(cmd, " \"%s\"", tmp ? tmp : request_url);
				wget_xfree(tmp);
			} else {
				if (proto_pass == HTTP_1_1_PASS) {
					wget_buffer_printf_append(cmd, " \"http://localhost:%d/%s\"",
					http_server_port, request_url);
				}
#ifdef WITH_LIBNGHTTP2
				else {
					wget_buffer_printf_append(cmd, " \"https://localhost:%d/%s\"",
					h2_server_port, request_url);
				}
#endif
			}
		}

		wget_buffer_strcat(cmd, " 2>&1");

		wget_error_printf("\n##### Testing '%s'\n", cmd->data);

		// catch stdout and write to stderr so all output is in sync
		FILE *pp;
		if ((pp = popen(cmd->data, "r"))) {
			char buf[4096];

			while (fgets(buf, sizeof(buf), pp)) {
				fputs(buf, stderr);
				fflush(stderr);
			}

			rc = pclose(pp);
		} else
			wget_error_printf_exit("Failed to execute test (%d) [%s]\n", errno, options);
		/*
			rc = system(cmd->data);
		*/
		if (!WIFEXITED(rc)) {
			wget_error_printf_exit("Unexpected error code %d, expected %d [%s]\n", rc, expected_error_code, options);
		}
		else if (WEXITSTATUS(rc) != expected_error_code) {
			if (expected_error_code2 >= 0) {
				if (WEXITSTATUS(rc) != expected_error_code2)
					wget_error_printf_exit("Unexpected error code %d, expected %d or %d [%s]\n",
						WEXITSTATUS(rc), expected_error_code, expected_error_code2, options);
			}
			else
				wget_error_printf_exit("Unexpected error code %d, expected %d [%s]\n",
					WEXITSTATUS(rc), expected_error_code, options);
		}

		if (expected_files) {
			for (it = 0; expected_files[it].name; it++) {
				struct stat st;
#ifdef _WIN32
				char buf[strlen(expected_files[it].name) * 3 + 1];
				const char *fname = wget_restrict_file_name(expected_files[it].name, buf,
					expected_files[it].restricted_mode ? expected_files[it].restricted_mode : WGET_RESTRICT_NAMES_WINDOWS);
#else
				const char *fname = expected_files[it].name;
#endif

				if (stat(fname, &st) != 0)
					wget_error_printf_exit("Missing expected file '%s/%s' [%s]\n", tmpdir, fname, options);

				if (expected_files[it].content) {
					size_t nbytes;
					char *content = wget_read_file(fname, &nbytes);

					if (content) {
						const char *expected_content = _insert_ports(expected_files[it].content);
						bool expected_content_alloc = 0;

						if (!expected_content)
							expected_content = expected_files[it].content;
						else
							expected_content_alloc = 1;

						size_t content_length = expected_files[it].content_length ? expected_files[it].content_length : strlen(expected_content);

						if (content_length != nbytes || memcmp(expected_content, content, nbytes) != 0) {
							wget_error_printf("Unexpected content in %s [%s]\n", fname, options);
							wget_error_printf("  Expected %zu bytes:\n%s\n", content_length, expected_content);
							wget_error_printf("  Got %zu bytes:\n%s\n", nbytes, content);
							exit(EXIT_FAILURE);
						}

						if (expected_content_alloc)
							wget_xfree(expected_content);
					}

					wget_xfree(content);
				}

				if (expected_files[it].timestamp && st.st_mtime != expected_files[it].timestamp)
					wget_error_printf_exit("Unexpected timestamp '%s/%s' (%ld) [%s]\n", tmpdir, fname, st.st_mtime, options);
			}
		}

		// look if there are unexpected files in our working dir
		_scan_for_unexpected(".", expected_files);

		if (post_handshake_auth && *post_handshake_auth == CHECK_FAILED) {
			wget_free(post_handshake_auth);
			wget_error_printf_exit("Post-handshake authentication failed\n");
		} else if (post_handshake_auth) {
			wget_free(post_handshake_auth);
		}

#ifdef WITH_GNUTLS_OCSP
		for (int i = 0; i < wget_vector_size(ocsp_responses); i++) {
			ocsp_resp_t *r = wget_vector_get(ocsp_responses, i);
			wget_xfree(r->data);
		}
		wget_vector_clear(ocsp_responses);
#endif
		wget_vector_clear(request_urls);
		wget_buffer_free(&cmd);

		if (options_alloc)
			wget_xfree(options);

		server_send_content_length = server_send_content_length_old;

		// system("ls -la");

		// cleanup for next iteration
		for (wget_test_url_t *url = urls; url < urls + nurls; url++) {
			if (url->body_original) {
				wget_xfree(url->body);
				url->body = url->body_original;
				url->body_original = NULL;
			}

			for (it = 0; it < countof(url->headers) && url->headers[it]; it++) {
				if (url->headers_original[it]) {
					wget_xfree(url->headers[it]);
					url->headers[it] = url->headers_original[it];
					url->headers_original[it] = NULL;
				}
			}
		}
	}
}

int wget_test_get_http_server_port(void)
{
	return proto_pass == H2_PASS ? h2_server_port : http_server_port;
}

int wget_test_get_https_server_port(void)
{
	return proto_pass == H2_PASS ? h2_server_port : https_server_port;
}

int wget_test_get_h2_server_port(void)
{
#ifdef WITH_LIBNGHTTP2
	return h2_server_port;
#else
	return -1;
#endif
}

int wget_test_get_ocsp_server_port(void)
{
	return ocsp_server_port;
}

// assume that we are in 'tmpdir'
int wget_test_check_file_system(void)
{
	static char fname[3][3] = { "Ab", "ab", "AB" };
	char buf[sizeof(fname[0])];
	int flags = 0, fd;
	ssize_t rc;

	_empty_directory(tmpdir);

	// Create 3 files with differently cased names with different content.
	// On a case-mangling file system like HFS+ there will be just one file with the contents of the last write.
	for (unsigned it = 0; it < countof(fname); it++) {
		if ((fd = open(fname[it], O_WRONLY | O_TRUNC | O_CREAT | O_BINARY, 0644)) != -1) {
			rc = write(fd, fname[it], sizeof(fname[0]));
			close(fd);

			if (rc != sizeof(fname[0])) {
				wget_debug_printf("%s: Failed to write to '%s/%s' (%d) %zd %zu\n", __func__, tmpdir, fname[it], errno, rc, sizeof(fname[0]));
				goto out;
			}
		} else {
			wget_debug_printf("%s: Failed to write open '%s/%s'\n", __func__, tmpdir, fname[it]);
			goto out;
		}
	}

	// Check file content to see if FS is case-mangling
	for (unsigned it = 0; it < countof(fname); it++) {
		if ((fd = open(fname[it], O_RDONLY | O_BINARY, 0644)) != -1) {
			rc = read(fd, buf, sizeof(fname[0]));
			close(fd);

			if (rc != sizeof(fname[0])) {
				wget_debug_printf("%s: Failed to read from '%s/%s'\n", __func__, tmpdir, fname[it]);
				goto out;
			}

			if (memcmp(buf, fname[it], sizeof(fname[0]))) {
				wget_debug_printf("%s: Found case-mangling file system\n", __func__);
				flags = WGET_TEST_FS_CASEMATTERS;
				goto out; // we can stop here
			}
		} else {
			wget_debug_printf("%s: Failed to read open '%s/%s'\n", __func__, tmpdir, fname[it]);
			goto out;
		}
	}

	wget_debug_printf("%s: Found case-respecting file system\n", __func__);

out:
	_empty_directory(tmpdir);

	return flags;
}
