/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <ctype.h>
#include <inttypes.h>
#include <limits.h>
#include <nghttp2/nghttp2.h>
#include <signal.h>
#include <string.h>

#include <isc/base64.h>
#include <isc/netmgr.h>
#include <isc/print.h>
#include <isc/tls.h>
#include <isc/url.h>

#include "netmgr-int.h"

#define AUTHEXTRA 7

#define MAX_DNS_MESSAGE_SIZE (UINT16_MAX)

#define DNS_MEDIA_TYPE "application/dns-message"

#define DEFAULT_CACHE_CONTROL "no-cache, no-store"

/*
 * If server during request processing surpasses any of the limits
 * below, it will just reset the stream without returning any error
 * codes in a response.  Ideally, these parameters should be
 * configurable both globally and per every HTTP endpoint description
 * in the configuration file, but for now it should be enough.
 */

/*
 * 128K should be enough to encode 64K of data into base64url inside GET
 * request and have extra space for other headers
 */
#define MAX_ALLOWED_DATA_IN_HEADERS (MAX_DNS_MESSAGE_SIZE * 2)

#define MAX_ALLOWED_DATA_IN_POST \
	(MAX_DNS_MESSAGE_SIZE + MAX_DNS_MESSAGE_SIZE / 2)

#define MAX_STREAMS_PER_SESSION (NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS)

#define HEADER_MATCH(header, name, namelen)   \
	(((namelen) == sizeof(header) - 1) && \
	 (strncasecmp((header), (const char *)(name), (namelen)) == 0))

#define MIN_SUCCESSFUL_HTTP_STATUS (200)
#define MAX_SUCCESSFUL_HTTP_STATUS (299)

#define SUCCESSFUL_HTTP_STATUS(code)             \
	((code) >= MIN_SUCCESSFUL_HTTP_STATUS && \
	 (code) <= MAX_SUCCESSFUL_HTTP_STATUS)

typedef struct isc_nm_http_response_status {
	size_t code;
	size_t content_length;
	bool content_type_valid;
} isc_nm_http_response_status_t;

typedef struct http_cstream {
	isc_nm_recv_cb_t read_cb;
	void *read_cbarg;
	isc_nm_cb_t connect_cb;
	void *connect_cbarg;

	bool sending;
	bool reading;

	char *uri;
	isc_url_parser_t up;

	char *authority;
	size_t authoritylen;
	char *path;

	uint8_t rbuf[MAX_DNS_MESSAGE_SIZE];
	size_t rbufsize;

	size_t pathlen;
	int32_t stream_id;

	bool post; /* POST or GET */
	isc_region_t postdata;
	size_t postdata_pos;
	char *GET_path;
	size_t GET_path_len;

	isc_nm_http_response_status_t response_status;

	LINK(struct http_cstream) link;
} http_cstream_t;

#define HTTP2_SESSION_MAGIC    ISC_MAGIC('H', '2', 'S', 'S')
#define VALID_HTTP2_SESSION(t) ISC_MAGIC_VALID(t, HTTP2_SESSION_MAGIC)

struct isc_nm_http_session {
	unsigned int magic;
	isc_refcount_t references;
	isc_mem_t *mctx;

	size_t sending;
	bool reading;
	bool closed;
	bool closing;

	nghttp2_session *ngsession;
	bool client;

	ISC_LIST(http_cstream_t) cstreams;
	ISC_LIST(isc_nmsocket_h2_t) sstreams;
	size_t nsstreams;

	isc_nmhandle_t *handle;
	isc_nmhandle_t *client_httphandle;
	isc_nmsocket_t *serversocket;
	isc_nmiface_t server_iface;

	uint8_t buf[MAX_DNS_MESSAGE_SIZE];
	size_t bufsize;

	isc_tlsctx_t *tlsctx;
};

typedef enum isc_http_error_responses {
	ISC_HTTP_ERROR_SUCCESS,		       /* 200 */
	ISC_HTTP_ERROR_NOT_FOUND,	       /* 404 */
	ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE,      /* 413 */
	ISC_HTTP_ERROR_URI_TOO_LONG,	       /* 414 */
	ISC_HTTP_ERROR_UNSUPPORTED_MEDIA_TYPE, /* 415 */
	ISC_HTTP_ERROR_BAD_REQUEST,	       /* 400 */
	ISC_HTTP_ERROR_NOT_IMPLEMENTED,	       /* 501 */
	ISC_HTTP_ERROR_GENERIC,		       /* 500 Internal Server Error */
	ISC_HTTP_ERROR_MAX
} isc_http_error_responses_t;

typedef struct isc_http_send_req {
	isc_nm_http_session_t *session;
	isc_nmhandle_t *transphandle;
	isc_nmhandle_t *httphandle;
	isc_region_t data;
	isc_nm_cb_t cb;
	void *cbarg;
} isc_http_send_req_t;

static bool
http_send_outgoing(isc_nm_http_session_t *session, isc_nmhandle_t *httphandle,
		   isc_nm_cb_t cb, void *cbarg);

static void
http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle,
	    isc_nm_cb_t send_cb, void *send_cbarg);

static void
failed_httpstream_read_cb(isc_nmsocket_t *sock, isc_result_t result,
			  isc_nm_http_session_t *session);

static void
failed_read_cb(isc_result_t result, isc_nm_http_session_t *session);

static isc_result_t
server_send_error_response(const isc_http_error_responses_t error,
			   nghttp2_session *ngsession, isc_nmsocket_t *socket);

static isc_result_t
client_send(isc_nmhandle_t *handle, const isc_region_t *region);

static void
finish_http_session(isc_nm_http_session_t *session);

static void
http_transpost_tcp_nodelay(isc_nmhandle_t *transphandle);

static bool
http_session_active(isc_nm_http_session_t *session) {
	REQUIRE(VALID_HTTP2_SESSION(session));
	return (!session->closed && !session->closing);
}

static bool
inactive(isc_nmsocket_t *sock) {
	return (!isc__nmsocket_active(sock) || atomic_load(&sock->closing) ||
		atomic_load(&sock->mgr->closing) ||
		(sock->server != NULL && !isc__nmsocket_active(sock->server)));
}

static void *
http_malloc(size_t sz, isc_mem_t *mctx) {
	return (isc_mem_allocate(mctx, sz));
}

static void *
http_calloc(size_t n, size_t sz, isc_mem_t *mctx) {
	const size_t msize = n * sz;
	void *data = isc_mem_allocate(mctx, msize);

	memset(data, 0, msize);
	return (data);
}

static void *
http_realloc(void *p, size_t newsz, isc_mem_t *mctx) {
	return (isc_mem_reallocate(mctx, p, newsz));
}

static void
http_free(void *p, isc_mem_t *mctx) {
	if (p == NULL) { /* as standard free() behaves */
		return;
	}
	isc_mem_free(mctx, p);
}

static void
init_nghttp2_mem(isc_mem_t *mctx, nghttp2_mem *mem) {
	*mem = (nghttp2_mem){ .malloc = (nghttp2_malloc)http_malloc,
			      .calloc = (nghttp2_calloc)http_calloc,
			      .realloc = (nghttp2_realloc)http_realloc,
			      .free = (nghttp2_free)http_free,
			      .mem_user_data = mctx };
}

static void
new_session(isc_mem_t *mctx, isc_tlsctx_t *tctx,
	    isc_nm_http_session_t **sessionp) {
	isc_nm_http_session_t *session = NULL;

	REQUIRE(sessionp != NULL && *sessionp == NULL);
	REQUIRE(mctx != NULL);

	session = isc_mem_get(mctx, sizeof(isc_nm_http_session_t));
	*session = (isc_nm_http_session_t){ .magic = HTTP2_SESSION_MAGIC,
					    .tlsctx = tctx };
	isc_refcount_init(&session->references, 1);
	isc_mem_attach(mctx, &session->mctx);
	ISC_LIST_INIT(session->cstreams);
	ISC_LIST_INIT(session->sstreams);

	*sessionp = session;
}

void
isc__nm_httpsession_attach(isc_nm_http_session_t *source,
			   isc_nm_http_session_t **targetp) {
	REQUIRE(VALID_HTTP2_SESSION(source));
	REQUIRE(targetp != NULL && *targetp == NULL);

	isc_refcount_increment(&source->references);

	*targetp = source;
}

void
isc__nm_httpsession_detach(isc_nm_http_session_t **sessionp) {
	isc_nm_http_session_t *session = NULL;

	REQUIRE(sessionp != NULL);

	session = *sessionp;
	*sessionp = NULL;

	REQUIRE(VALID_HTTP2_SESSION(session));

	if (isc_refcount_decrement(&session->references) > 1) {
		return;
	}

	finish_http_session(session);

	INSIST(ISC_LIST_EMPTY(session->sstreams));
	INSIST(ISC_LIST_EMPTY(session->cstreams));

	if (session->ngsession != NULL) {
		nghttp2_session_del(session->ngsession);
		session->ngsession = NULL;
	}

	/* We need an acquire memory barrier here */
	(void)isc_refcount_current(&session->references);

	session->magic = 0;
	isc_mem_putanddetach(&session->mctx, session,
			     sizeof(isc_nm_http_session_t));
}

static http_cstream_t *
find_http_cstream(int32_t stream_id, isc_nm_http_session_t *session) {
	http_cstream_t *cstream = NULL;
	REQUIRE(VALID_HTTP2_SESSION(session));

	if (ISC_LIST_EMPTY(session->cstreams)) {
		return (NULL);
	}

	for (cstream = ISC_LIST_HEAD(session->cstreams); cstream != NULL;
	     cstream = ISC_LIST_NEXT(cstream, link))
	{
		if (cstream->stream_id == stream_id) {
			break;
		}
	}

	/* LRU-like behaviour */
	if (cstream && ISC_LIST_HEAD(session->cstreams) != cstream) {
		ISC_LIST_UNLINK(session->cstreams, cstream, link);
		ISC_LIST_PREPEND(session->cstreams, cstream, link);
	}

	return (cstream);
}

static isc_result_t
new_http_cstream(isc_nmsocket_t *sock, http_cstream_t **streamp) {
	isc_mem_t *mctx = sock->mgr->mctx;
	const char *uri = NULL;
	bool post;
	http_cstream_t *stream = NULL;
	isc_result_t result;

	uri = sock->h2.session->handle->sock->h2.connect.uri;
	post = sock->h2.session->handle->sock->h2.connect.post;

	stream = isc_mem_get(mctx, sizeof(http_cstream_t));
	*stream = (http_cstream_t){ .stream_id = -1,
				    .post = post,
				    .uri = isc_mem_strdup(mctx, uri) };
	ISC_LINK_INIT(stream, link);

	result = isc_url_parse(stream->uri, strlen(stream->uri), 0,
			       &stream->up);
	if (result != ISC_R_SUCCESS) {
		isc_mem_free(mctx, stream->uri);
		isc_mem_put(mctx, stream, sizeof(http_cstream_t));
		return (result);
	}

	stream->authoritylen = stream->up.field_data[ISC_UF_HOST].len;
	stream->authority = isc_mem_get(mctx, stream->authoritylen + AUTHEXTRA);
	memmove(stream->authority, &uri[stream->up.field_data[ISC_UF_HOST].off],
		stream->up.field_data[ISC_UF_HOST].len);

	if (stream->up.field_set & (1 << ISC_UF_PORT)) {
		stream->authoritylen += (size_t)snprintf(
			stream->authority +
				stream->up.field_data[ISC_UF_HOST].len,
			AUTHEXTRA, ":%u", stream->up.port);
	}

	/* If we don't have path in URI, we use "/" as path. */
	stream->pathlen = 1;
	if (stream->up.field_set & (1 << ISC_UF_PATH)) {
		stream->pathlen = stream->up.field_data[ISC_UF_PATH].len;
	}
	if (stream->up.field_set & (1 << ISC_UF_QUERY)) {
		/* +1 for '?' character */
		stream->pathlen +=
			(size_t)(stream->up.field_data[ISC_UF_QUERY].len + 1);
	}

	stream->path = isc_mem_get(mctx, stream->pathlen);
	if (stream->up.field_set & (1 << ISC_UF_PATH)) {
		memmove(stream->path,
			&uri[stream->up.field_data[ISC_UF_PATH].off],
			stream->up.field_data[ISC_UF_PATH].len);
	} else {
		stream->path[0] = '/';
	}

	if (stream->up.field_set & (1 << ISC_UF_QUERY)) {
		stream->path[stream->pathlen -
			     stream->up.field_data[ISC_UF_QUERY].len - 1] = '?';
		memmove(stream->path + stream->pathlen -
				stream->up.field_data[ISC_UF_QUERY].len,
			&uri[stream->up.field_data[ISC_UF_QUERY].off],
			stream->up.field_data[ISC_UF_QUERY].len);
	}

	*streamp = stream;

	return (ISC_R_SUCCESS);
}

static void
put_http_cstream(isc_mem_t *mctx, http_cstream_t *stream) {
	isc_mem_put(mctx, stream->path, stream->pathlen);
	isc_mem_put(mctx, stream->authority,
		    stream->up.field_data[ISC_UF_HOST].len + AUTHEXTRA);
	isc_mem_free(mctx, stream->uri);
	if (stream->GET_path != NULL) {
		isc_mem_free(mctx, stream->GET_path);
		stream->GET_path = NULL;
		stream->GET_path_len = 0;
	}
	if (stream->postdata.base != NULL) {
		isc_mem_put(mctx, stream->postdata.base,
			    stream->postdata.length);
	}
	isc_mem_put(mctx, stream, sizeof(http_cstream_t));
}

static void
finish_http_session(isc_nm_http_session_t *session) {
	if (session->closed) {
		return;
	}

	if (session->handle != NULL) {
		if (!session->closed) {
			session->closed = true;
			isc_nm_cancelread(session->handle);
		}

		if (!ISC_LIST_EMPTY(session->cstreams)) {
			http_cstream_t *cstream =
				ISC_LIST_HEAD(session->cstreams);
			while (cstream != NULL) {
				http_cstream_t *next = ISC_LIST_NEXT(cstream,
								     link);
				ISC_LIST_DEQUEUE(session->cstreams, cstream,
						 link);
				cstream->read_cb(
					session->client_httphandle,
					ISC_R_UNEXPECTED,
					&(isc_region_t){ cstream->rbuf,
							 cstream->rbufsize },
					cstream->read_cbarg);
				put_http_cstream(session->mctx, cstream);
				cstream = next;
			}
		}
		isc_nmhandle_detach(&session->handle);
	}

	if (session->client_httphandle != NULL) {
		isc_nmhandle_detach(&session->client_httphandle);
	}

	INSIST(ISC_LIST_EMPTY(session->cstreams));

	/* detach from server socket */
	if (session->serversocket != NULL) {
		isc__nmsocket_detach(&session->serversocket);
	}
	session->closed = true;
}

static int
on_client_data_chunk_recv_callback(int32_t stream_id, const uint8_t *data,
				   size_t len, isc_nm_http_session_t *session) {
	http_cstream_t *cstream = find_http_cstream(stream_id, session);

	if (cstream != NULL) {
		size_t new_rbufsize = cstream->rbufsize + len;
		if (new_rbufsize <= MAX_DNS_MESSAGE_SIZE &&
		    new_rbufsize <= cstream->response_status.content_length)
		{
			memmove(cstream->rbuf + cstream->rbufsize, data, len);
			cstream->rbufsize = new_rbufsize;
		} else {
			return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
		}
	} else {
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}

	return (0);
}

static int
on_server_data_chunk_recv_callback(int32_t stream_id, const uint8_t *data,
				   size_t len, isc_nm_http_session_t *session) {
	isc_nmsocket_h2_t *h2 = ISC_LIST_HEAD(session->sstreams);
	while (h2 != NULL) {
		if (stream_id == h2->stream_id) {
			size_t new_bufsize = h2->bufsize + len;
			if (new_bufsize <= MAX_DNS_MESSAGE_SIZE &&
			    new_bufsize <= h2->content_length) {
				memmove(h2->buf + h2->bufsize, data, len);
				h2->bufsize = new_bufsize;
				break;
			}

			return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
		}
		h2 = ISC_LIST_NEXT(h2, link);
	}
	if (h2 == NULL) {
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}

	return (0);
}

static int
on_data_chunk_recv_callback(nghttp2_session *ngsession, uint8_t flags,
			    int32_t stream_id, const uint8_t *data, size_t len,
			    void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	int rv;

	UNUSED(ngsession);
	UNUSED(flags);

	if (session->client) {
		rv = on_client_data_chunk_recv_callback(stream_id, data, len,
							session);
	} else {
		rv = on_server_data_chunk_recv_callback(stream_id, data, len,
							session);
	}

	return (rv);
}

static void
call_unlink_cstream_readcb(http_cstream_t *cstream,
			   isc_nm_http_session_t *session,
			   isc_result_t result) {
	REQUIRE(VALID_HTTP2_SESSION(session));
	REQUIRE(cstream != NULL);
	cstream->read_cb(session->handle, result,
			 &(isc_region_t){ cstream->rbuf, cstream->rbufsize },
			 cstream->read_cbarg);
	ISC_LIST_UNLINK(session->cstreams, cstream, link);
	put_http_cstream(session->mctx, cstream);
}

static int
on_client_stream_close_callback(int32_t stream_id,
				isc_nm_http_session_t *session) {
	http_cstream_t *cstream = find_http_cstream(stream_id, session);

	if (cstream != NULL) {
		isc_result_t result =
			SUCCESSFUL_HTTP_STATUS(cstream->response_status.code)
				? ISC_R_SUCCESS
				: ISC_R_FAILURE;
		call_unlink_cstream_readcb(cstream, session, result);
		if (ISC_LIST_EMPTY(session->cstreams)) {
			int rv = 0;
			rv = nghttp2_session_terminate_session(
				session->ngsession, NGHTTP2_NO_ERROR);
			if (rv != 0) {
				return (rv);
			}
		}
	} else {
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}

	return (0);
}

static int
on_server_stream_close_callback(int32_t stream_id,
				isc_nm_http_session_t *session) {
	isc_nmsocket_t *sock = nghttp2_session_get_stream_user_data(
		session->ngsession, stream_id);
	int rv = 0;

	ISC_LIST_UNLINK(session->sstreams, &sock->h2, link);
	if (ISC_LIST_EMPTY(session->sstreams)) {
		rv = nghttp2_session_terminate_session(session->ngsession,
						       NGHTTP2_NO_ERROR);
	}
	session->nsstreams--;
	isc__nmsocket_detach(&sock);
	return (rv);
}

static int
on_stream_close_callback(nghttp2_session *ngsession, int32_t stream_id,
			 uint32_t error_code, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	int rv = 0;

	REQUIRE(VALID_HTTP2_SESSION(session));
	REQUIRE(session->ngsession == ngsession);

	UNUSED(error_code);

	if (session->client) {
		rv = on_client_stream_close_callback(stream_id, session);
	} else {
		rv = on_server_stream_close_callback(stream_id, session);
	}

	return (rv);
}

static bool
client_handle_status_header(http_cstream_t *cstream, const uint8_t *value,
			    const size_t valuelen) {
	char tmp[32] = { 0 };
	const size_t tmplen = sizeof(tmp) - 1;

	strncpy(tmp, (const char *)value, ISC_MIN(tmplen, valuelen));
	cstream->response_status.code = strtoul(tmp, NULL, 10);

	if (SUCCESSFUL_HTTP_STATUS(cstream->response_status.code)) {
		return (true);
	}

	return (false);
}

static bool
client_handle_content_length_header(http_cstream_t *cstream,
				    const uint8_t *value,
				    const size_t valuelen) {
	char tmp[32] = { 0 };
	const size_t tmplen = sizeof(tmp) - 1;

	strncpy(tmp, (const char *)value, ISC_MIN(tmplen, valuelen));
	cstream->response_status.content_length = strtoul(tmp, NULL, 10);

	if (cstream->response_status.content_length == 0 ||
	    cstream->response_status.content_length > MAX_DNS_MESSAGE_SIZE)
	{
		return (false);
	}

	return (true);
}

static bool
client_handle_content_type_header(http_cstream_t *cstream, const uint8_t *value,
				  const size_t valuelen) {
	const char type_dns_message[] = DNS_MEDIA_TYPE;
	const size_t len = sizeof(type_dns_message) - 1;

	UNUSED(valuelen);

	if (strncasecmp((const char *)value, type_dns_message, len) == 0) {
		cstream->response_status.content_type_valid = true;
		return (true);
	}

	return (false);
}

static int
client_on_header_callback(nghttp2_session *ngsession,
			  const nghttp2_frame *frame, const uint8_t *name,
			  size_t namelen, const uint8_t *value, size_t valuelen,
			  uint8_t flags, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	http_cstream_t *cstream = NULL;
	const char status[] = ":status";
	const char content_length[] = "Content-Length";
	const char content_type[] = "Content-Type";
	bool header_ok = true;

	REQUIRE(VALID_HTTP2_SESSION(session));
	REQUIRE(session->client);
	REQUIRE(!ISC_LIST_EMPTY(session->cstreams));

	UNUSED(flags);
	UNUSED(ngsession);

	cstream = find_http_cstream(frame->hd.stream_id, session);

	switch (frame->hd.type) {
	case NGHTTP2_HEADERS:
		if (frame->headers.cat != NGHTTP2_HCAT_RESPONSE) {
			break;
		}

		if (HEADER_MATCH(status, name, namelen)) {
			header_ok = client_handle_status_header(cstream, value,
								valuelen);
		} else if (HEADER_MATCH(content_length, name, namelen)) {
			header_ok = client_handle_content_length_header(
				cstream, value, valuelen);
		} else if (HEADER_MATCH(content_type, name, namelen)) {
			header_ok = client_handle_content_type_header(
				cstream, value, valuelen);
		}
		break;
	}

	if (!header_ok) {
		return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
	}

	return (0);
}

static void
initialize_nghttp2_client_session(isc_nm_http_session_t *session) {
	nghttp2_session_callbacks *callbacks = NULL;
	nghttp2_option *option = NULL;
	nghttp2_mem mem;

	init_nghttp2_mem(session->mctx, &mem);
	RUNTIME_CHECK(nghttp2_session_callbacks_new(&callbacks) == 0);
	RUNTIME_CHECK(nghttp2_option_new(&option) == 0);

#if NGHTTP2_VERSION_NUM >= (0x010c00)
	nghttp2_option_set_max_send_header_block_length(
		option, MAX_ALLOWED_DATA_IN_HEADERS);
#endif

	nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
		callbacks, on_data_chunk_recv_callback);

	nghttp2_session_callbacks_set_on_stream_close_callback(
		callbacks, on_stream_close_callback);

	nghttp2_session_callbacks_set_on_header_callback(
		callbacks, client_on_header_callback);

	RUNTIME_CHECK(nghttp2_session_client_new3(&session->ngsession,
						  callbacks, session, option,
						  &mem) == 0);

	nghttp2_option_del(option);
	nghttp2_session_callbacks_del(callbacks);
}

static bool
send_client_connection_header(isc_nm_http_session_t *session) {
	nghttp2_settings_entry iv[] = { { NGHTTP2_SETTINGS_ENABLE_PUSH, 0 } };
	int rv;

	rv = nghttp2_submit_settings(session->ngsession, NGHTTP2_FLAG_NONE, iv,
				     sizeof(iv) / sizeof(iv[0]));
	if (rv != 0) {
		return (false);
	}

	return (true);
}

#define MAKE_NV(NAME, VALUE, VALUELEN)                                       \
	{                                                                    \
		(uint8_t *)(uintptr_t)(NAME), (uint8_t *)(uintptr_t)(VALUE), \
			sizeof(NAME) - 1, VALUELEN, NGHTTP2_NV_FLAG_NONE     \
	}

#define MAKE_NV2(NAME, VALUE)                                                \
	{                                                                    \
		(uint8_t *)(uintptr_t)(NAME), (uint8_t *)(uintptr_t)(VALUE), \
			sizeof(NAME) - 1, sizeof(VALUE) - 1,                 \
			NGHTTP2_NV_FLAG_NONE                                 \
	}

static ssize_t
client_read_callback(nghttp2_session *ngsession, int32_t stream_id,
		     uint8_t *buf, size_t length, uint32_t *data_flags,
		     nghttp2_data_source *source, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	http_cstream_t *cstream = NULL;

	REQUIRE(session->client);
	REQUIRE(!ISC_LIST_EMPTY(session->cstreams));

	UNUSED(ngsession);
	UNUSED(source);

	cstream = find_http_cstream(stream_id, session);
	if (!cstream || cstream->stream_id != stream_id) {
		/* We haven't found the stream, so we are not reading */
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}

	if (cstream->post) {
		size_t len = cstream->postdata.length - cstream->postdata_pos;

		if (len > length) {
			len = length;
		}

		memmove(buf, cstream->postdata.base + cstream->postdata_pos,
			len);
		cstream->postdata_pos += len;

		if (cstream->postdata_pos == cstream->postdata.length) {
			*data_flags |= NGHTTP2_DATA_FLAG_EOF;
		}

		return (len);
	} else {
		*data_flags |= NGHTTP2_DATA_FLAG_EOF;
		return (0);
	}

	return (0);
}

/*
 * Send HTTP request to the remote peer.
 */
static isc_result_t
client_submit_request(isc_nm_http_session_t *session, http_cstream_t *stream) {
	int32_t stream_id;
	char *uri = stream->uri;
	isc_url_parser_t *up = &stream->up;
	nghttp2_data_provider dp;

	if (stream->post) {
		char p[64];
		snprintf(p, sizeof(p), "%u", stream->postdata.length);
		nghttp2_nv hdrs[] = {
			MAKE_NV2(":method", "POST"),
			MAKE_NV(":scheme",
				&uri[up->field_data[ISC_UF_SCHEMA].off],
				up->field_data[ISC_UF_SCHEMA].len),
			MAKE_NV(":authority", stream->authority,
				stream->authoritylen),
			MAKE_NV(":path", stream->path, stream->pathlen),
			MAKE_NV2("content-type", DNS_MEDIA_TYPE),
			MAKE_NV2("accept", DNS_MEDIA_TYPE),
			MAKE_NV("content-length", p, strlen(p)),
			MAKE_NV2("cache-control", DEFAULT_CACHE_CONTROL)
		};

		dp = (nghttp2_data_provider){ .read_callback =
						      client_read_callback };
		stream_id = nghttp2_submit_request(
			session->ngsession, NULL, hdrs,
			sizeof(hdrs) / sizeof(hdrs[0]), &dp, stream);
	} else {
		INSIST(stream->GET_path != NULL);
		INSIST(stream->GET_path_len != 0);
		nghttp2_nv hdrs[] = {
			MAKE_NV2(":method", "GET"),
			MAKE_NV(":scheme",
				&uri[up->field_data[ISC_UF_SCHEMA].off],
				up->field_data[ISC_UF_SCHEMA].len),
			MAKE_NV(":authority", stream->authority,
				stream->authoritylen),
			MAKE_NV(":path", stream->GET_path,
				stream->GET_path_len),
			MAKE_NV2("accept", DNS_MEDIA_TYPE),
			MAKE_NV2("cache-control", DEFAULT_CACHE_CONTROL)
		};

		dp = (nghttp2_data_provider){ .read_callback =
						      client_read_callback };
		stream_id = nghttp2_submit_request(
			session->ngsession, NULL, hdrs,
			sizeof(hdrs) / sizeof(hdrs[0]), &dp, stream);
	}
	if (stream_id < 0) {
		return (ISC_R_FAILURE);
	}

	stream->stream_id = stream_id;

	return (ISC_R_SUCCESS);
}

/*
 * Read callback from TLS socket.
 */
static void
http_readcb(isc_nmhandle_t *handle, isc_result_t result, isc_region_t *region,
	    void *data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)data;
	ssize_t readlen;

	REQUIRE(VALID_HTTP2_SESSION(session));

	UNUSED(handle);

	if (result != ISC_R_SUCCESS) {
		if (result != ISC_R_TIMEDOUT) {
			session->reading = false;
		}
		failed_read_cb(result, session);
		return;
	}

	readlen = nghttp2_session_mem_recv(session->ngsession, region->base,
					   region->length);
	if (readlen < 0) {
		failed_read_cb(ISC_R_UNEXPECTED, session);
		return;
	}

	if ((size_t)readlen < region->length) {
		INSIST(session->bufsize == 0);
		INSIST(region->length - readlen < MAX_DNS_MESSAGE_SIZE);
		memmove(session->buf, region->base, region->length - readlen);
		session->bufsize = region->length - readlen;
		isc_nm_pauseread(session->handle);
	}

	/* We might have something to receive or send, do IO */
	http_do_bio(session, NULL, NULL, NULL);
}

static void
http_writecb(isc_nmhandle_t *handle, isc_result_t result, void *arg) {
	isc_http_send_req_t *req = (isc_http_send_req_t *)arg;
	isc_nm_http_session_t *session = req->session;
	isc_nmhandle_t *transphandle = req->transphandle;

	REQUIRE(VALID_HTTP2_SESSION(session));
	REQUIRE(VALID_NMHANDLE(handle));

	if (http_session_active(session)) {
		INSIST(session->handle == handle);
	}

	if (req->cb != NULL) {
		req->cb(req->httphandle, result, req->cbarg);
		isc_nmhandle_detach(&req->httphandle);
	}

	isc_mem_put(session->mctx, req->data.base, req->data.length);
	isc_mem_put(session->mctx, req, sizeof(*req));

	http_do_bio(session, NULL, NULL, NULL);
	session->sending--;
	isc_nmhandle_detach(&transphandle);
	isc__nm_httpsession_detach(&session);
}

static bool
http_send_outgoing(isc_nm_http_session_t *session, isc_nmhandle_t *httphandle,
		   isc_nm_cb_t cb, void *cbarg) {
	isc_http_send_req_t *send = NULL;
	const uint8_t *data = NULL;
	size_t pending;

	if (!http_session_active(session) ||
	    !nghttp2_session_want_write(session->ngsession))
	{
		return (false);
	}

	pending = nghttp2_session_mem_send(session->ngsession, &data);
	if (pending == 0) {
		/* No data returned */
		return (false);
	}

	send = isc_mem_get(session->mctx, sizeof(*send));
	*send = (isc_http_send_req_t){
		.data.base = isc_mem_get(session->mctx, pending),
		.data.length = pending,
	};
	memmove(send->data.base, data, pending);
	isc_nmhandle_attach(session->handle, &send->transphandle);
	isc__nm_httpsession_attach(session, &send->session);

	if (cb != NULL) {
		INSIST(VALID_NMHANDLE(httphandle));
		send->cb = cb;
		send->cbarg = cbarg;
		isc_nmhandle_attach(httphandle, &send->httphandle);
	}

	session->sending++;
	isc_nm_send(session->handle, &send->data, http_writecb, send);
	return (true);
}

static void
http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle,
	    isc_nm_cb_t send_cb, void *send_cbarg) {
	REQUIRE(VALID_HTTP2_SESSION(session));

	if (session->closed) {
		return;
	} else if (session->closing) {
		/*
		 * There might be leftover callbacks waiting to be received
		 */
		if (session->sending == 0) {
			finish_http_session(session);
		}
		return;
	} else if ((nghttp2_session_want_read(session->ngsession) == 0 &&
		    nghttp2_session_want_write(session->ngsession) == 0))
	{
		session->closing = true;
		return;
	}

	if (nghttp2_session_want_read(session->ngsession) != 0) {
		if (!session->reading) {
			/* We have not yet started reading from this handle */
			isc_nm_read(session->handle, http_readcb, session);
			session->reading = true;
		} else if (session->bufsize > 0) {
			/* Leftover data in the buffer, use it */
			size_t readlen = nghttp2_session_mem_recv(
				session->ngsession, session->buf,
				session->bufsize);

			if (readlen == session->bufsize) {
				session->bufsize = 0;
			} else {
				memmove(session->buf, session->buf + readlen,
					session->bufsize - readlen);
				session->bufsize -= readlen;
			}

			http_do_bio(session, send_httphandle, send_cb,
				    send_cbarg);
			return;
		} else {
			/* Resume reading, it's idempotent, wait for more */
			isc_nm_resumeread(session->handle);
		}
	} else {
		/* We don't want more data, stop reading for now */
		isc_nm_pauseread(session->handle);
	}

	if (send_cb != NULL) {
		INSIST(VALID_NMHANDLE(send_httphandle));
		(void)http_send_outgoing(session, send_httphandle, send_cb,
					 send_cbarg);
	} else {
		INSIST(send_httphandle == NULL);
		INSIST(send_cb == NULL);
		INSIST(send_cbarg == NULL);
		(void)http_send_outgoing(session, NULL, NULL, NULL);
	}

	return;
}

static isc_result_t
get_http_cstream(isc_nmsocket_t *sock, http_cstream_t **streamp) {
	http_cstream_t *cstream = sock->h2.connect.cstream;
	isc_result_t result;

	REQUIRE(streamp != NULL && *streamp == NULL);

	sock->h2.connect.cstream = NULL;

	if (cstream == NULL) {
		result = new_http_cstream(sock, &cstream);
		if (result != ISC_R_SUCCESS) {
			INSIST(cstream == NULL);
			return (result);
		}
	}

	*streamp = cstream;
	return (ISC_R_SUCCESS);
}

static void
http_call_connect_cb(isc_nmsocket_t *sock, isc_nm_http_session_t *session,
		     isc_result_t result) {
	isc__nm_uvreq_t *req = NULL;
	isc_nmhandle_t *httphandle = isc__nmhandle_get(sock, &sock->peer,
						       &sock->iface->addr);

	REQUIRE(sock->connect_cb != NULL);

	req = isc__nm_uvreq_get(sock->mgr, sock);
	req->cb.connect = sock->connect_cb;
	req->cbarg = sock->connect_cbarg;
	if (session != NULL) {
		session->client_httphandle = httphandle;
		req->handle = NULL;
		isc_nmhandle_attach(httphandle, &req->handle);
	} else {
		req->handle = httphandle;
	}

	isc__nmsocket_clearcb(sock);
	isc__nm_connectcb(sock, req, result, true);
}

static void
transport_connect_cb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) {
	isc_nmsocket_t *http_sock = (isc_nmsocket_t *)cbarg;
	isc_nmsocket_t *transp_sock = NULL;
	isc_nm_http_session_t *session = NULL;
	http_cstream_t *cstream = NULL;
	isc_mem_t *mctx = NULL;

	REQUIRE(VALID_NMSOCK(http_sock));
	REQUIRE(VALID_NMHANDLE(handle));

	transp_sock = handle->sock;

	REQUIRE(VALID_NMSOCK(transp_sock));

	mctx = transp_sock->mgr->mctx;

	INSIST(http_sock->h2.connect.uri != NULL);

	http_sock->tid = transp_sock->tid;
	if (result != ISC_R_SUCCESS) {
		goto error;
	}

	new_session(mctx, http_sock->h2.connect.tlsctx, &session);
	session->client = true;
	transp_sock->h2.session = session;
	http_sock->h2.connect.tlsctx = NULL;

	transp_sock->h2.connect.post = http_sock->h2.connect.post;
	transp_sock->h2.connect.uri = http_sock->h2.connect.uri;
	http_sock->h2.connect.uri = NULL;
	isc__nm_httpsession_attach(session, &http_sock->h2.session);

	if (session->tlsctx != NULL) {
		const unsigned char *alpn = NULL;
		unsigned int alpnlen = 0;

		INSIST(transp_sock->type == isc_nm_tlssocket);

		isc_tls_get_http2_alpn(transp_sock->tlsstream.tls, &alpn,
				       &alpnlen);
		if (alpn == NULL || alpnlen != NGHTTP2_PROTO_VERSION_ID_LEN ||
		    memcmp(NGHTTP2_PROTO_VERSION_ID, alpn,
			   NGHTTP2_PROTO_VERSION_ID_LEN) != 0)
		{
			/*
			 * HTTP/2 negotiation error. Any sensible DoH
			 * client will fail if HTTP/2 cannot be
			 * negotiated via ALPN.
			 */
			isc__nmsocket_prep_destroy(transp_sock);
			result = ISC_R_HTTP2ALPNERROR;
			goto error;
		}
	}

	isc_nmhandle_attach(handle, &session->handle);

	initialize_nghttp2_client_session(session);
	if (!send_client_connection_header(session)) {
		goto error;
	}

	result = get_http_cstream(http_sock, &cstream);
	http_sock->h2.connect.cstream = cstream;
	if (result != ISC_R_SUCCESS) {
		goto error;
	}

	http_transpost_tcp_nodelay(handle);

	http_call_connect_cb(http_sock, session, result);

	http_do_bio(session, NULL, NULL, NULL);
	isc__nmsocket_detach(&http_sock);
	return;

error:
	http_call_connect_cb(http_sock, session, result);

	if (http_sock->h2.connect.uri != NULL) {
		isc_mem_free(mctx, http_sock->h2.connect.uri);
	}

	isc__nmsocket_detach(&http_sock);
}

void
isc_nm_httpconnect(isc_nm_t *mgr, isc_nmiface_t *local, isc_nmiface_t *peer,
		   const char *uri, bool post, isc_nm_cb_t cb, void *cbarg,
		   isc_tlsctx_t *tlsctx, unsigned int timeout,
		   size_t extrahandlesize) {
	isc_nmiface_t local_interface;
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NM(mgr));
	REQUIRE(cb != NULL);
	REQUIRE(peer != NULL);
	REQUIRE(uri != NULL);
	REQUIRE(*uri != '\0');

	if (local == NULL) {
		isc_sockaddr_anyofpf(&local_interface.addr,
				     (peer->addr).type.sa.sa_family);
		local = &local_interface;
	}

	sock = isc_mem_get(mgr->mctx, sizeof(*sock));
	isc__nmsocket_init(sock, mgr, isc_nm_httpsocket, local);

	sock->extrahandlesize = extrahandlesize;
	sock->connect_timeout = timeout;
	sock->result = ISC_R_DEFAULT;
	sock->connect_cb = cb;
	sock->connect_cbarg = cbarg;
	atomic_init(&sock->client, true);

	if (isc__nm_closing(sock)) {
		isc__nm_uvreq_t *req = isc__nm_uvreq_get(mgr, sock);

		req->cb.connect = cb;
		req->cbarg = cbarg;
		req->peer = peer->addr;
		req->local = local->addr;
		req->handle = isc__nmhandle_get(sock, &req->peer,
						&sock->iface->addr);

		if (isc__nm_in_netthread()) {
			sock->tid = isc_nm_tid();
		}

		isc__nmsocket_clearcb(sock);
		isc__nm_connectcb(sock, req, ISC_R_CANCELED, true);
		isc__nmsocket_prep_destroy(sock);
		isc__nmsocket_detach(&sock);
		return;
	}

	sock->h2 = (isc_nmsocket_h2_t){ .connect.uri = isc_mem_strdup(mgr->mctx,
								      uri),
					.connect.post = post,
					.connect.tlsctx = tlsctx };
	ISC_LINK_INIT(&sock->h2, link);

	/*
	 * We need to prevent the interface object data from going out of
	 * scope too early.
	 */
	if (local == &local_interface) {
		sock->h2.connect.local_interface = local_interface;
		sock->iface = &sock->h2.connect.local_interface;
	}

	if (tlsctx != NULL) {
		isc_nm_tlsconnect(mgr, local, peer, transport_connect_cb, sock,
				  tlsctx, timeout, 0);
	} else {
		isc_nm_tcpconnect(mgr, local, peer, transport_connect_cb, sock,
				  timeout, 0);
	}
}

static isc_result_t
client_send(isc_nmhandle_t *handle, const isc_region_t *region) {
	isc_result_t result = ISC_R_SUCCESS;
	isc_nmsocket_t *sock = handle->sock;
	isc_mem_t *mctx = sock->mgr->mctx;
	isc_nm_http_session_t *session = sock->h2.session;
	http_cstream_t *cstream = sock->h2.connect.cstream;

	REQUIRE(VALID_HTTP2_SESSION(handle->sock->h2.session));
	REQUIRE(session->client);
	REQUIRE(region != NULL);
	REQUIRE(region->base != NULL);
	REQUIRE(region->length <= MAX_DNS_MESSAGE_SIZE);
	REQUIRE(cstream != NULL);

	if (cstream->post) {
		/* POST */
		cstream->postdata = (isc_region_t){
			.base = isc_mem_get(mctx, region->length),
			.length = region->length
		};
		memmove(cstream->postdata.base, region->base, region->length);
		cstream->postdata_pos = 0;
	} else {
		/* GET */
		size_t path_size = 0;
		char *base64url_data = NULL;
		size_t base64url_data_len = 0;
		isc_buffer_t *buf = NULL;
		isc_region_t data = *region;
		isc_region_t base64_region;
		size_t base64_len = ((4 * data.length / 3) + 3) & ~3;

		isc_buffer_allocate(mctx, &buf, base64_len);

		result = isc_base64_totext(&data, -1, "", buf);
		if (result != ISC_R_SUCCESS) {
			isc_buffer_free(&buf);
			goto error;
		}

		isc__buffer_usedregion(buf, &base64_region);
		INSIST(base64_region.length == base64_len);

		base64url_data = isc__nm_base64_to_base64url(
			mctx, (const char *)base64_region.base,
			base64_region.length, &base64url_data_len);
		isc_buffer_free(&buf);
		if (base64url_data == NULL) {
			goto error;
		}

		/* len("?dns=") + len(path) + len(base64url) + len("\0") */
		path_size = cstream->pathlen + base64url_data_len + 5 + 1;
		cstream->GET_path = isc_mem_allocate(mctx, path_size);
		cstream->GET_path_len = (size_t)snprintf(
			cstream->GET_path, path_size, "%.*s?dns=%s",
			(int)cstream->pathlen, cstream->path, base64url_data);

		INSIST(cstream->GET_path_len == (path_size - 1));
		isc_mem_free(mctx, base64url_data);
	}

	cstream->sending = true;
	if (!ISC_LINK_LINKED(cstream, link)) {
		ISC_LIST_PREPEND(session->cstreams, cstream, link);
	}

	sock->h2.connect.cstream = NULL;
	result = client_submit_request(session, cstream);
	if (result != ISC_R_SUCCESS) {
		ISC_LIST_UNLINK(session->cstreams, cstream, link);
		goto error;
	}

error:
	return (result);
}

isc_result_t
isc__nm_http_request(isc_nmhandle_t *handle, isc_region_t *region,
		     isc_nm_recv_cb_t cb, void *cbarg) {
	isc_result_t result = ISC_R_SUCCESS;
	isc_nmsocket_t *sock = NULL;
	http_cstream_t *cstream = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->tid == isc_nm_tid());

	REQUIRE(cb != NULL);

	sock = handle->sock;

	isc__nm_http_read(handle, cb, cbarg);
	if (!http_session_active(handle->sock->h2.session)) {
		/* the callback was called by isc__nm_http_read() */
		return (ISC_R_CANCELED);
	}
	result = client_send(handle, region);
	if (result != ISC_R_SUCCESS) {
		goto error;
	}

	http_do_bio(sock->h2.session, NULL, NULL, NULL);
	return (ISC_R_SUCCESS);

error:
	cstream = sock->h2.connect.cstream;
	if (cstream->read_cb != NULL) {
		cstream->read_cb(handle, result, NULL, cstream->read_cbarg);
	}
	return (result);
}

static int
server_on_begin_headers_callback(nghttp2_session *ngsession,
				 const nghttp2_frame *frame, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	isc_nmsocket_t *socket = NULL;

	if (frame->hd.type != NGHTTP2_HEADERS ||
	    frame->headers.cat != NGHTTP2_HCAT_REQUEST)
	{
		return (0);
	} else if (frame->hd.length > MAX_ALLOWED_DATA_IN_HEADERS) {
		return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
	}

	if (session->nsstreams >= MAX_STREAMS_PER_SESSION) {
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}

	socket = isc_mem_get(session->mctx, sizeof(isc_nmsocket_t));
	isc__nmsocket_init(socket, session->serversocket->mgr,
			   isc_nm_httpsocket,
			   (isc_nmiface_t *)&session->server_iface);
	socket->h2 = (isc_nmsocket_h2_t){
		.buf = isc_mem_allocate(session->mctx, MAX_DNS_MESSAGE_SIZE),
		.psock = socket,
		.stream_id = frame->hd.stream_id,
		.headers_error_code = ISC_HTTP_ERROR_SUCCESS
	};
	session->nsstreams++;
	isc__nm_httpsession_attach(session, &socket->h2.session);
	socket->tid = session->handle->sock->tid;
	ISC_LINK_INIT(&socket->h2, link);
	ISC_LIST_APPEND(session->sstreams, &socket->h2, link);

	nghttp2_session_set_stream_user_data(ngsession, frame->hd.stream_id,
					     socket);
	return (0);
}

static isc_nm_httphandler_t *
find_server_request_handler(const char *request_path,
			    isc_nmsocket_t *serversocket) {
	isc_nm_httphandler_t *handler = NULL;

	REQUIRE(VALID_NMSOCK(serversocket));

	if (request_path == NULL || *request_path == '\0') {
		return (NULL);
	}

	RWLOCK(&serversocket->h2.lock, isc_rwlocktype_read);
	if (atomic_load(&serversocket->listening)) {
		for (handler = ISC_LIST_HEAD(serversocket->h2.handlers);
		     handler != NULL; handler = ISC_LIST_NEXT(handler, link))
		{
			if (!strcmp(request_path, handler->path)) {
				break;
			}
		}
	}
	RWUNLOCK(&serversocket->h2.lock, isc_rwlocktype_read);

	return (handler);
}

static isc_http_error_responses_t
server_handle_path_header(isc_nmsocket_t *socket, const uint8_t *value,
			  const size_t valuelen) {
	isc_nm_httphandler_t *handler = NULL;
	const uint8_t *qstr = NULL;
	size_t vlen = valuelen;

	qstr = memchr(value, '?', valuelen);
	if (qstr != NULL) {
		vlen = qstr - value;
	}

	if (socket->h2.request_path != NULL) {
		isc_mem_free(socket->mgr->mctx, socket->h2.request_path);
	}
	socket->h2.request_path = isc_mem_strndup(
		socket->mgr->mctx, (const char *)value, vlen + 1);
	handler = find_server_request_handler(socket->h2.request_path,
					      socket->h2.session->serversocket);
	if (handler != NULL) {
		socket->h2.cb = handler->cb;
		socket->h2.cbarg = handler->cbarg;
		socket->extrahandlesize = handler->extrahandlesize;
	} else {
		isc_mem_free(socket->mgr->mctx, socket->h2.request_path);
		socket->h2.request_path = NULL;
		return (ISC_HTTP_ERROR_NOT_FOUND);
	}
	if (qstr != NULL) {
		const char *dns_value = NULL;
		size_t dns_value_len = 0;

		if (socket->h2.request_type != ISC_HTTP_REQ_GET) {
			return (ISC_HTTP_ERROR_BAD_REQUEST);
		}

		if (isc__nm_parse_httpquery((const char *)qstr, &dns_value,
					    &dns_value_len)) {
			const size_t decoded_size = dns_value_len / 4 * 3;
			if (decoded_size <= MAX_DNS_MESSAGE_SIZE) {
				if (socket->h2.query_data != NULL) {
					isc_mem_free(socket->mgr->mctx,
						     socket->h2.query_data);
				}
				socket->h2.query_data =
					isc__nm_base64url_to_base64(
						socket->mgr->mctx, dns_value,
						dns_value_len,
						&socket->h2.query_data_len);
			} else {
				socket->h2.query_too_large = true;
				return (ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE);
			}
		} else {
			return (ISC_HTTP_ERROR_BAD_REQUEST);
		}
	}
	return (ISC_HTTP_ERROR_SUCCESS);
}

static isc_http_error_responses_t
server_handle_method_header(isc_nmsocket_t *socket, const uint8_t *value,
			    const size_t valuelen) {
	const char get[] = "GET";
	const char post[] = "POST";

	if (HEADER_MATCH(get, value, valuelen)) {
		socket->h2.request_type = ISC_HTTP_REQ_GET;
	} else if (HEADER_MATCH(post, value, valuelen)) {
		socket->h2.request_type = ISC_HTTP_REQ_POST;
	} else {
		return (ISC_HTTP_ERROR_NOT_IMPLEMENTED);
	}
	return (ISC_HTTP_ERROR_SUCCESS);
}

static isc_http_error_responses_t
server_handle_scheme_header(isc_nmsocket_t *socket, const uint8_t *value,
			    const size_t valuelen) {
	const char http[] = "http";
	const char http_secure[] = "https";

	if (HEADER_MATCH(http_secure, value, valuelen)) {
		socket->h2.request_scheme = ISC_HTTP_SCHEME_HTTP_SECURE;
	} else if (HEADER_MATCH(http, value, valuelen)) {
		socket->h2.request_scheme = ISC_HTTP_SCHEME_HTTP;
	} else {
		return (ISC_HTTP_ERROR_BAD_REQUEST);
	}
	return (ISC_HTTP_ERROR_SUCCESS);
}

static isc_http_error_responses_t
server_handle_content_length_header(isc_nmsocket_t *socket,
				    const uint8_t *value,
				    const size_t valuelen) {
	char tmp[32] = { 0 };
	const size_t tmplen = sizeof(tmp) - 1;

	if (socket->h2.request_type != ISC_HTTP_REQ_POST) {
		return (ISC_HTTP_ERROR_BAD_REQUEST);
	}
	strncpy(tmp, (const char *)value,
		valuelen > tmplen ? tmplen : valuelen);
	socket->h2.content_length = strtoul(tmp, NULL, 10);
	if (socket->h2.content_length > MAX_DNS_MESSAGE_SIZE) {
		return (ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE);
	} else if (socket->h2.content_length == 0) {
		return (ISC_HTTP_ERROR_BAD_REQUEST);
	}
	return (ISC_HTTP_ERROR_SUCCESS);
}

static isc_http_error_responses_t
server_handle_content_type_header(isc_nmsocket_t *socket, const uint8_t *value,
				  const size_t valuelen) {
	const char type_dns_message[] = DNS_MEDIA_TYPE;
	isc_http_error_responses_t resp = ISC_HTTP_ERROR_SUCCESS;

	UNUSED(socket);

	if (!HEADER_MATCH(type_dns_message, value, valuelen)) {
		resp = ISC_HTTP_ERROR_UNSUPPORTED_MEDIA_TYPE;
	}
	return (resp);
}

static isc_http_error_responses_t
server_handle_accept_header(isc_nmsocket_t *socket, const uint8_t *value,
			    const size_t valuelen) {
	const char type_accept_all[] = "*/*";
	const char type_dns_message[] = DNS_MEDIA_TYPE;
	isc_http_error_responses_t resp = ISC_HTTP_ERROR_SUCCESS;

	UNUSED(socket);

	if (!(HEADER_MATCH(type_dns_message, value, valuelen) ||
	      HEADER_MATCH(type_accept_all, value, valuelen)))
	{
		resp = ISC_HTTP_ERROR_UNSUPPORTED_MEDIA_TYPE;
	}
	return (resp);
}

static isc_http_error_responses_t
server_handle_header(isc_nmsocket_t *socket, const uint8_t *name,
		     size_t namelen, const uint8_t *value,
		     const size_t valuelen) {
	isc_http_error_responses_t code = ISC_HTTP_ERROR_SUCCESS;
	bool was_error;
	const char path[] = ":path";
	const char method[] = ":method";
	const char scheme[] = ":scheme";
	const char accept[] = "accept";
	const char content_length[] = "Content-Length";
	const char content_type[] = "Content-Type";

	was_error = socket->h2.headers_error_code != ISC_HTTP_ERROR_SUCCESS;
	/*
	 * process Content-Length even when there was an error,
	 * to drop the connection earlier if required.
	 */
	if (HEADER_MATCH(content_length, name, namelen)) {
		code = server_handle_content_length_header(socket, value,
							   valuelen);
	} else if (!was_error && HEADER_MATCH(path, name, namelen)) {
		code = server_handle_path_header(socket, value, valuelen);
	} else if (!was_error && HEADER_MATCH(method, name, namelen)) {
		code = server_handle_method_header(socket, value, valuelen);
	} else if (!was_error && HEADER_MATCH(scheme, name, namelen)) {
		code = server_handle_scheme_header(socket, value, valuelen);
	} else if (!was_error && HEADER_MATCH(content_type, name, namelen)) {
		code = server_handle_content_type_header(socket, value,
							 valuelen);
	} else if (!was_error &&
		   HEADER_MATCH(accept, (const char *)name, namelen)) {
		code = server_handle_accept_header(socket, value, valuelen);
	}

	return (code);
}

static int
server_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,
			  void *user_data) {
	isc_nmsocket_t *socket = NULL;
	isc_http_error_responses_t code = ISC_HTTP_ERROR_SUCCESS;

	UNUSED(flags);
	UNUSED(user_data);

	socket = nghttp2_session_get_stream_user_data(session,
						      frame->hd.stream_id);
	if (socket == NULL) {
		return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
	}

	socket->h2.headers_data_processed += (namelen + valuelen);

	switch (frame->hd.type) {
	case NGHTTP2_HEADERS:
		if (frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
			break;
		}
		code = server_handle_header(socket, name, namelen, value,
					    valuelen);
		break;
	}

	INSIST(socket != NULL);

	if (socket->h2.headers_data_processed > MAX_ALLOWED_DATA_IN_HEADERS) {
		return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
	} else if (socket->h2.content_length > MAX_ALLOWED_DATA_IN_POST) {
		return (NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE);
	}

	if (code == ISC_HTTP_ERROR_SUCCESS) {
		return (0);
	} else {
		socket->h2.headers_error_code = code;
	}

	return (0);
}

static ssize_t
server_read_callback(nghttp2_session *ngsession, int32_t stream_id,
		     uint8_t *buf, size_t length, uint32_t *data_flags,
		     nghttp2_data_source *source, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	isc_nmsocket_t *socket = (isc_nmsocket_t *)source->ptr;
	size_t buflen;

	REQUIRE(socket->h2.stream_id == stream_id);

	UNUSED(ngsession);
	UNUSED(session);

	buflen = socket->h2.bufsize - socket->h2.bufpos;
	if (buflen > length) {
		buflen = length;
	}

	memmove(buf, socket->h2.buf + socket->h2.bufpos, buflen);
	socket->h2.bufpos += buflen;
	if (socket->h2.bufpos == socket->h2.bufsize) {
		*data_flags |= NGHTTP2_DATA_FLAG_EOF;
	}

	return (buflen);
}

static isc_result_t
server_send_response(nghttp2_session *ngsession, int32_t stream_id,
		     const nghttp2_nv *nva, size_t nvlen,
		     isc_nmsocket_t *socket) {
	nghttp2_data_provider data_prd;
	int rv;

	data_prd.source.ptr = socket;
	data_prd.read_callback = server_read_callback;

	rv = nghttp2_submit_response(ngsession, stream_id, nva, nvlen,
				     &data_prd);
	if (rv != 0) {
		return (ISC_R_FAILURE);
	}
	return (ISC_R_SUCCESS);
}

#define MAKE_ERROR_REPLY(tag, code)             \
	{                                       \
		tag, MAKE_NV2(":status", #code) \
	}

/*
 * Here we use roughly the same error codes that Unbound uses.
 * (https://blog.nlnetlabs.nl/dns-over-https-in-unbound/)
 */

static struct http_error_responses {
	const isc_http_error_responses_t type;
	const nghttp2_nv header;
} error_responses[] = {
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_SUCCESS, 200),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_NOT_FOUND, 404),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE, 413),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_URI_TOO_LONG, 414),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_UNSUPPORTED_MEDIA_TYPE, 415),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_BAD_REQUEST, 400),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_NOT_IMPLEMENTED, 501),
	MAKE_ERROR_REPLY(ISC_HTTP_ERROR_GENERIC, 500),
};

static isc_result_t
server_send_error_response(const isc_http_error_responses_t error,
			   nghttp2_session *ngsession, isc_nmsocket_t *socket) {
	socket->h2.bufsize = 0;
	socket->h2.bufpos = 0;

	for (size_t i = 0;
	     i < sizeof(error_responses) / sizeof(error_responses[0]); i++)
	{
		if (error_responses[i].type == error) {
			return (server_send_response(
				ngsession, socket->h2.stream_id,
				&error_responses[i].header, 1, socket));
		}
	}

	return (server_send_error_response(ISC_HTTP_ERROR_GENERIC, ngsession,
					   socket));
}

static int
server_on_request_recv(nghttp2_session *ngsession,
		       isc_nm_http_session_t *session, isc_nmsocket_t *socket) {
	isc_result_t result;
	isc_nmhandle_t *handle = NULL;
	isc_sockaddr_t addr;
	isc_http_error_responses_t code = ISC_HTTP_ERROR_SUCCESS;
	isc_region_t data;

	code = socket->h2.headers_error_code;
	if (code != ISC_HTTP_ERROR_SUCCESS) {
		goto error;
	}

	if (socket->h2.request_path == NULL || socket->h2.cb == NULL) {
		code = ISC_HTTP_ERROR_NOT_FOUND;
	} else if (socket->h2.request_type == ISC_HTTP_REQ_POST &&
		   socket->h2.bufsize > socket->h2.content_length)
	{
		code = ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE;
	} else if (socket->h2.request_type == ISC_HTTP_REQ_POST &&
		   socket->h2.bufsize != socket->h2.content_length)
	{
		code = ISC_HTTP_ERROR_BAD_REQUEST;
	}

	if (code != ISC_HTTP_ERROR_SUCCESS) {
		goto error;
	}

	if (socket->h2.request_type == ISC_HTTP_REQ_GET) {
		isc_buffer_t decoded_buf;
		isc__buffer_init(&decoded_buf, socket->h2.buf,
				 MAX_DNS_MESSAGE_SIZE);
		if (isc_base64_decodestring(socket->h2.query_data,
					    &decoded_buf) != ISC_R_SUCCESS)
		{
			code = ISC_HTTP_ERROR_GENERIC;
			goto error;
		}
		isc__buffer_usedregion(&decoded_buf, &data);
	} else if (socket->h2.request_type == ISC_HTTP_REQ_POST) {
		INSIST(socket->h2.content_length > 0);
		data = (isc_region_t){ socket->h2.buf, socket->h2.bufsize };
	} else {
		INSIST(0);
		ISC_UNREACHABLE();
	}

	addr = isc_nmhandle_peeraddr(session->handle);
	handle = isc__nmhandle_get(socket, &addr, NULL);
	socket->h2.cb(handle, ISC_R_SUCCESS, &data, socket->h2.cbarg);
	isc_nmhandle_detach(&handle);
	return (0);

error:
	result = server_send_error_response(code, ngsession, socket);
	if (result != ISC_R_SUCCESS) {
		return (NGHTTP2_ERR_CALLBACK_FAILURE);
	}
	return (0);
}

void
isc__nm_http_send(isc_nmhandle_t *handle, const isc_region_t *region,
		  isc_nm_cb_t cb, void *cbarg) {
	isc_nmsocket_t *sock = NULL;
	isc__netievent_httpsend_t *ievent = NULL;
	isc__nm_uvreq_t *uvreq = NULL;

	REQUIRE(VALID_NMHANDLE(handle));

	sock = handle->sock;

	REQUIRE(VALID_NMSOCK(sock));

	uvreq = isc__nm_uvreq_get(sock->mgr, sock);
	isc_nmhandle_attach(handle, &uvreq->handle);
	uvreq->cb.send = cb;
	uvreq->cbarg = cbarg;

	uvreq->uvbuf.base = (char *)region->base;
	uvreq->uvbuf.len = region->length;

	ievent = isc__nm_get_netievent_httpsend(sock->mgr, sock, uvreq);
	isc__nm_enqueue_ievent(&sock->mgr->workers[sock->tid],
			       (isc__netievent_t *)ievent);
}

static void
failed_send_cb(isc_nmsocket_t *sock, isc__nm_uvreq_t *req,
	       isc_result_t eresult) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(VALID_UVREQ(req));

	if (req->cb.send != NULL) {
		isc__nm_sendcb(sock, req, eresult, true);
	} else {
		isc__nm_uvreq_put(&req, sock);
	}
}

static void
client_httpsend(isc_nmhandle_t *handle, isc_nmsocket_t *sock,
		isc__nm_uvreq_t *req) {
	isc_result_t result = ISC_R_SUCCESS;
	isc_nm_cb_t cb = req->cb.send;
	void *cbarg = req->cbarg;

	result = client_send(
		handle,
		&(isc_region_t){ (uint8_t *)req->uvbuf.base, req->uvbuf.len });
	if (result != ISC_R_SUCCESS) {
		failed_send_cb(sock, req, result);
		return;
	}

	http_do_bio(sock->h2.session, handle, cb, cbarg);
	isc__nm_uvreq_put(&req, sock);
}

static void
server_httpsend(isc_nmhandle_t *handle, isc_nmsocket_t *sock,
		isc__nm_uvreq_t *req) {
	size_t len;
	isc_result_t result = ISC_R_SUCCESS;
	isc_nm_cb_t cb = req->cb.send;
	void *cbarg = req->cbarg;
	if (inactive(sock) || !http_session_active(handle->httpsession)) {
		failed_send_cb(sock, req, ISC_R_CANCELED);
		return;
	}

	INSIST(handle->httpsession->handle->sock->tid == isc_nm_tid());
	INSIST(VALID_NMHANDLE(handle->httpsession->handle));
	INSIST(VALID_NMSOCK(handle->httpsession->handle->sock));

	memmove(sock->h2.buf, req->uvbuf.base, req->uvbuf.len);
	sock->h2.bufsize = req->uvbuf.len;

	len = snprintf(sock->h2.clenbuf, sizeof(sock->h2.clenbuf), "%lu",
		       (unsigned long)req->uvbuf.len);
	const nghttp2_nv hdrs[] = {
		MAKE_NV2(":status", "200"),
		MAKE_NV2("Content-Type", DNS_MEDIA_TYPE),
		MAKE_NV("Content-Length", sock->h2.clenbuf, len),
		/*
		 * TODO: implement Cache-Control: max-age=<seconds>
		 * (https://tools.ietf.org/html/rfc8484#section-5.1)
		 */
		MAKE_NV2("cache-control", DEFAULT_CACHE_CONTROL)
	};

	result = server_send_response(handle->httpsession->ngsession,
				      sock->h2.stream_id, hdrs,
				      sizeof(hdrs) / sizeof(nghttp2_nv), sock);

	if (result == ISC_R_SUCCESS) {
		http_do_bio(handle->httpsession, handle, cb, cbarg);
	} else {
		cb(handle, result, cbarg);
	}
	isc__nm_uvreq_put(&req, sock);
}

void
isc__nm_async_httpsend(isc__networker_t *worker, isc__netievent_t *ev0) {
	isc__netievent_httpsend_t *ievent = (isc__netievent_httpsend_t *)ev0;
	isc_nmsocket_t *sock = ievent->sock;
	isc__nm_uvreq_t *req = ievent->req;
	isc_nmhandle_t *handle = NULL;
	isc_nm_http_session_t *session = NULL;

	UNUSED(worker);

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(VALID_UVREQ(req));
	REQUIRE(VALID_HTTP2_SESSION(sock->h2.session));

	ievent->req = NULL;
	handle = req->handle;

	REQUIRE(VALID_NMHANDLE(handle));

	session = sock->h2.session;
	if (session != NULL && session->client) {
		client_httpsend(handle, sock, req);
	} else {
		server_httpsend(handle, sock, req);
	}
}

void
isc__nm_http_read(isc_nmhandle_t *handle, isc_nm_recv_cb_t cb, void *cbarg) {
	isc_result_t result;
	http_cstream_t *cstream = NULL;
	isc_nm_http_session_t *session = NULL;

	REQUIRE(VALID_NMHANDLE(handle));

	session = handle->sock->h2.session;
	if (!http_session_active(session)) {
		cb(handle, ISC_R_CANCELED, NULL, cbarg);
		return;
	}

	result = get_http_cstream(handle->sock, &cstream);
	if (result != ISC_R_SUCCESS) {
		return;
	}

	handle->sock->h2.connect.cstream = cstream;
	cstream->read_cb = cb;
	cstream->read_cbarg = cbarg;
	cstream->reading = true;

	if (!ISC_LINK_LINKED(cstream, link)) {
		ISC_LIST_PREPEND(session->cstreams, cstream, link);
	}

	if (cstream->sending) {
		result = client_submit_request(session, cstream);
		if (result != ISC_R_SUCCESS) {
			ISC_LIST_UNLINK(session->cstreams, cstream, link);
			return;
		}

		http_do_bio(session, NULL, NULL, NULL);
	}
}

static int
server_on_frame_recv_callback(nghttp2_session *ngsession,
			      const nghttp2_frame *frame, void *user_data) {
	isc_nm_http_session_t *session = (isc_nm_http_session_t *)user_data;
	isc_nmsocket_t *socket = NULL;

	switch (frame->hd.type) {
	case NGHTTP2_DATA:
	case NGHTTP2_HEADERS:
		/* Check that the client request has finished */
		if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
			socket = nghttp2_session_get_stream_user_data(
				ngsession, frame->hd.stream_id);

			/*
			 * For DATA and HEADERS frame, this callback may be
			 * called after on_stream_close_callback. Check that
			 * the stream is still alive.
			 */
			if (socket == NULL) {
				return (0);
			}

			return (server_on_request_recv(ngsession, session,
						       socket));
		}
		break;
	default:
		break;
	}
	return (0);
}

static void
initialize_nghttp2_server_session(isc_nm_http_session_t *session) {
	nghttp2_session_callbacks *callbacks = NULL;
	nghttp2_mem mem;

	init_nghttp2_mem(session->mctx, &mem);

	RUNTIME_CHECK(nghttp2_session_callbacks_new(&callbacks) == 0);

	nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
		callbacks, on_data_chunk_recv_callback);

	nghttp2_session_callbacks_set_on_stream_close_callback(
		callbacks, on_stream_close_callback);

	nghttp2_session_callbacks_set_on_header_callback(
		callbacks, server_on_header_callback);

	nghttp2_session_callbacks_set_on_begin_headers_callback(
		callbacks, server_on_begin_headers_callback);

	nghttp2_session_callbacks_set_on_frame_recv_callback(
		callbacks, server_on_frame_recv_callback);

	RUNTIME_CHECK(nghttp2_session_server_new3(&session->ngsession,
						  callbacks, session, NULL,
						  &mem) == 0);

	nghttp2_session_callbacks_del(callbacks);
}

static int
server_send_connection_header(isc_nm_http_session_t *session) {
	nghttp2_settings_entry iv[1] = {
		{ NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
		  MAX_STREAMS_PER_SESSION }
	};
	int rv;

	rv = nghttp2_submit_settings(session->ngsession, NGHTTP2_FLAG_NONE, iv,
				     1);
	if (rv != 0) {
		return (-1);
	}
	return (0);
}

/*
 * It is advisable to disable Nagle's algorithm for HTTP/2
 * connections because multiple HTTP/2 streams could be multiplexed
 * over one transport connection. Thus, delays when delivering small
 * packets could bring down performance for the whole session.
 * HTTP/2 is meant to be used this way.
 */
static void
http_transpost_tcp_nodelay(isc_nmhandle_t *transphandle) {
#ifndef _WIN32
	isc_nmsocket_t *tcpsock = NULL;
	uv_os_fd_t tcp_fd = (uv_os_fd_t)-1;

	if (transphandle->sock->type == isc_nm_tlssocket) {
		tcpsock = transphandle->sock->outerhandle->sock;
	} else {
		tcpsock = transphandle->sock;
	}

	(void)uv_fileno((uv_handle_t *)&tcpsock->uv_handle.tcp, &tcp_fd);
	RUNTIME_CHECK(tcp_fd != (uv_os_fd_t)-1);
	(void)isc__nm_socket_tcp_nodelay((uv_os_sock_t)tcp_fd);
#endif
}

static isc_result_t
httplisten_acceptcb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) {
	isc_nmsocket_t *httplistensock = (isc_nmsocket_t *)cbarg;
	isc_nm_http_session_t *session = NULL;
	isc_nmsocket_t *listener = NULL, *httpserver = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));

	if (handle->sock->type == isc_nm_tlssocket) {
		REQUIRE(VALID_NMSOCK(handle->sock->listener));
		listener = handle->sock->listener;
		httpserver = listener->h2.httpserver;
	} else {
		REQUIRE(VALID_NMSOCK(handle->sock->server));
		listener = handle->sock->server;
		REQUIRE(VALID_NMSOCK(listener->parent));
		httpserver = listener->parent->h2.httpserver;
	}

	/*
	 * NOTE: HTTP listener socket might be destroyed by the time this
	 * function gets invoked, so we need to do extra sanity checks to
	 * detect this case.
	 */
	if (inactive(handle->sock) || httpserver == NULL) {
		return (ISC_R_CANCELED);
	}

	if (result != ISC_R_SUCCESS) {
		/* XXXWPK do nothing? */
		return (result);
	}

	REQUIRE(VALID_NMSOCK(httplistensock));
	INSIST(httplistensock == httpserver);

	if (inactive(httplistensock) ||
	    !atomic_load(&httplistensock->listening)) {
		return (ISC_R_CANCELED);
	}

	http_transpost_tcp_nodelay(handle);

	new_session(httplistensock->mgr->mctx, NULL, &session);
	initialize_nghttp2_server_session(session);
	handle->sock->h2.session = session;

	isc_nmhandle_attach(handle, &session->handle);
	isc__nmsocket_attach(httplistensock, &session->serversocket);
	session->server_iface.addr = isc_nmhandle_localaddr(session->handle);
	server_send_connection_header(session);

	/* TODO H2 */
	http_do_bio(session, NULL, NULL, NULL);
	return (ISC_R_SUCCESS);
}

isc_result_t
isc_nm_listenhttp(isc_nm_t *mgr, isc_nmiface_t *iface, int backlog,
		  isc_quota_t *quota, isc_tlsctx_t *ctx,
		  isc_nmsocket_t **sockp) {
	isc_nmsocket_t *sock = NULL;
	isc_result_t result;

	sock = isc_mem_get(mgr->mctx, sizeof(*sock));
	isc__nmsocket_init(sock, mgr, isc_nm_httplistener, iface);

	if (ctx != NULL) {
		isc_tlsctx_enable_http2server_alpn(ctx);
		result = isc_nm_listentls(mgr, iface, httplisten_acceptcb, sock,
					  sizeof(isc_nm_http_session_t),
					  backlog, quota, ctx, &sock->outer);
	} else {
		result = isc_nm_listentcp(mgr, iface, httplisten_acceptcb, sock,
					  sizeof(isc_nm_http_session_t),
					  backlog, quota, &sock->outer);
	}

	if (result != ISC_R_SUCCESS) {
		atomic_store(&sock->closed, true);
		isc__nmsocket_detach(&sock);
		return (result);
	}

	isc__nmsocket_attach(sock, &sock->outer->h2.httpserver);

	sock->nchildren = sock->outer->nchildren;
	sock->result = ISC_R_DEFAULT;
	sock->tid = isc_random_uniform(sock->nchildren);
	sock->fd = (uv_os_sock_t)-1;

	atomic_store(&sock->listening, true);
	*sockp = sock;
	return (ISC_R_SUCCESS);
}

/*
 * In DoH we just need to intercept the request - the response can be sent
 * to the client code via the nmhandle directly as it's always just the
 * http content.
 */
static void
http_callback(isc_nmhandle_t *handle, isc_result_t result, isc_region_t *data,
	      void *arg) {
	isc_nm_httpcbarg_t *httpcbarg = arg;

	REQUIRE(VALID_NMHANDLE(handle));

	if (result != ISC_R_SUCCESS) {
		/* Shut down the client, then ourselves */
		httpcbarg->cb(handle, result, NULL, httpcbarg->cbarg);
		/* XXXWPK FREE */
		return;
	}
	httpcbarg->cb(handle, result, data, httpcbarg->cbarg);
}

isc_result_t
isc_nm_http_endpoint(isc_nmsocket_t *sock, const char *uri, isc_nm_recv_cb_t cb,
		     void *cbarg, size_t extrahandlesize) {
	isc_nm_httphandler_t *handler = NULL;
	isc_nm_httpcbarg_t *httpcbarg = NULL;
	bool newhandler = false;

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_httplistener);

	httpcbarg = isc_mem_get(sock->mgr->mctx, sizeof(isc_nm_httpcbarg_t));
	*httpcbarg = (isc_nm_httpcbarg_t){ .cb = cb, .cbarg = cbarg };
	ISC_LINK_INIT(httpcbarg, link);

	if (find_server_request_handler(uri, sock) == NULL) {
		handler = isc_mem_get(sock->mgr->mctx, sizeof(*handler));
		*handler = (isc_nm_httphandler_t){
			.cb = http_callback,
			.cbarg = httpcbarg,
			.extrahandlesize = extrahandlesize,
			.path = isc_mem_strdup(sock->mgr->mctx, uri)
		};
		ISC_LINK_INIT(handler, link);

		newhandler = true;
	}

	RWLOCK(&sock->h2.lock, isc_rwlocktype_write);
	if (newhandler) {
		ISC_LIST_APPEND(sock->h2.handlers, handler, link);
	}
	ISC_LIST_APPEND(sock->h2.handler_cbargs, httpcbarg, link);
	RWUNLOCK(&sock->h2.lock, isc_rwlocktype_write);

	return (ISC_R_SUCCESS);
}

void
isc__nm_http_stoplistening(isc_nmsocket_t *sock) {
	isc__netievent_httpstop_t *ievent = NULL;

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_httplistener);

	if (!atomic_compare_exchange_strong(&sock->closing, &(bool){ false },
					    true)) {
		INSIST(0);
		ISC_UNREACHABLE();
	}

	ievent = isc__nm_get_netievent_httpstop(sock->mgr, sock);
	isc__nm_enqueue_ievent(&sock->mgr->workers[sock->tid],
			       (isc__netievent_t *)ievent);
}

static void
clear_handlers(isc_nmsocket_t *sock) {
	isc_nm_httphandler_t *handler = NULL;
	isc_nm_httpcbarg_t *httpcbarg = NULL;

	/* Delete all handlers */
	RWLOCK(&sock->h2.lock, isc_rwlocktype_write);
	handler = ISC_LIST_HEAD(sock->h2.handlers);
	while (handler != NULL) {
		isc_nm_httphandler_t *next = NULL;

		next = ISC_LIST_NEXT(handler, link);
		ISC_LIST_DEQUEUE(sock->h2.handlers, handler, link);
		isc_mem_free(sock->mgr->mctx, handler->path);
		isc_mem_put(sock->mgr->mctx, handler, sizeof(*handler));
		handler = next;
	}

	httpcbarg = ISC_LIST_HEAD(sock->h2.handler_cbargs);
	while (httpcbarg != NULL) {
		isc_nm_httpcbarg_t *next = NULL;

		next = ISC_LIST_NEXT(httpcbarg, link);
		ISC_LIST_DEQUEUE(sock->h2.handler_cbargs, httpcbarg, link);
		isc_mem_put(sock->mgr->mctx, httpcbarg,
			    sizeof(isc_nm_httpcbarg_t));
		httpcbarg = next;
	}
	RWUNLOCK(&sock->h2.lock, isc_rwlocktype_write);
}

void
isc__nm_async_httpstop(isc__networker_t *worker, isc__netievent_t *ev0) {
	isc__netievent_httpstop_t *ievent = (isc__netievent_httpstop_t *)ev0;
	isc_nmsocket_t *sock = ievent->sock;

	UNUSED(worker);

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->tid == isc_nm_tid());

	atomic_store(&sock->listening, false);
	atomic_store(&sock->closing, false);
	atomic_store(&sock->closed, true);
	if (sock->outer != NULL) {
		isc_nm_stoplistening(sock->outer);
		isc_nmsocket_close(&sock->outer);
	}
}

static void
http_close_direct(isc_nmsocket_t *sock) {
	isc_nm_http_session_t *session = NULL;

	REQUIRE(VALID_NMSOCK(sock));

	atomic_store(&sock->closed, true);
	session = sock->h2.session;

	if (session != NULL && session->handle) {
		http_do_bio(session, NULL, NULL, NULL);
	}
}

void
isc__nm_http_close(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_httpsocket);
	REQUIRE(!isc__nmsocket_active(sock));

	if (!atomic_compare_exchange_strong(&sock->closing, &(bool){ false },
					    true)) {
		return;
	}

	isc__netievent_httpclose_t *ievent =
		isc__nm_get_netievent_httpclose(sock->mgr, sock);

	isc__nm_enqueue_ievent(&sock->mgr->workers[sock->tid],
			       (isc__netievent_t *)ievent);
}

void
isc__nm_async_httpclose(isc__networker_t *worker, isc__netievent_t *ev0) {
	isc__netievent_httpclose_t *ievent = (isc__netievent_httpclose_t *)ev0;
	isc_nmsocket_t *sock = ievent->sock;

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->tid == isc_nm_tid());

	UNUSED(worker);

	http_close_direct(sock);
}

static void
failed_httpstream_read_cb(isc_nmsocket_t *sock, isc_result_t result,
			  isc_nm_http_session_t *session) {
	isc_nmhandle_t *handle = NULL;
	isc_sockaddr_t addr;

	REQUIRE(VALID_NMSOCK(sock));
	INSIST(sock->type == isc_nm_httpsocket);

	if (sock->h2.request_path == NULL) {
		return;
	}

	INSIST(sock->h2.cbarg != NULL);

	(void)nghttp2_submit_rst_stream(
		session->ngsession, NGHTTP2_FLAG_END_STREAM, sock->h2.stream_id,
		NGHTTP2_REFUSED_STREAM);
	addr = isc_nmhandle_peeraddr(session->handle);
	handle = isc__nmhandle_get(sock, &addr, NULL);
	sock->h2.cb(handle, result,
		    &(isc_region_t){ sock->h2.buf, sock->h2.bufsize },
		    sock->h2.cbarg);
	isc_nmhandle_detach(&handle);
}

static void
failed_read_cb(isc_result_t result, isc_nm_http_session_t *session) {
	REQUIRE(VALID_HTTP2_SESSION(session));
	REQUIRE(result != ISC_R_SUCCESS);

	if (session->client) {
		http_cstream_t *cstream = NULL;
		cstream = ISC_LIST_HEAD(session->cstreams);
		while (cstream != NULL) {
			http_cstream_t *next = ISC_LIST_NEXT(cstream, link);
			cstream->read_cb(session->client_httphandle, result,
					 &(isc_region_t){ cstream->rbuf,
							  cstream->rbufsize },
					 cstream->read_cbarg);
			if (result != ISC_R_TIMEDOUT ||
			    !isc__nmsocket_timer_running(session->handle->sock))
			{
				ISC_LIST_DEQUEUE(session->cstreams, cstream,
						 link);
				put_http_cstream(session->mctx, cstream);
			}
			cstream = next;
		}

		/*
		 * If result was ISC_R_TIMEDOUT and the timer was reset,
		 * then we still have active streams and should not close
		 * the session.
		 */
		if (ISC_LIST_EMPTY(session->cstreams)) {
			finish_http_session(session);
		}
	} else {
		isc_nmsocket_h2_t *h2data = NULL; /* stream socket */
		for (h2data = ISC_LIST_HEAD(session->sstreams); h2data != NULL;
		     h2data = ISC_LIST_NEXT(h2data, link))
		{
			failed_httpstream_read_cb(h2data->psock, result,
						  session);
		}

		h2data = ISC_LIST_HEAD(session->sstreams);
		while (h2data != NULL) {
			isc_nmsocket_h2_t *next = ISC_LIST_NEXT(h2data, link);
			ISC_LIST_DEQUEUE(session->sstreams, h2data, link);
			/* Cleanup socket in place */
			atomic_store(&h2data->psock->active, false);
			atomic_store(&h2data->psock->closed, true);
			isc__nmsocket_detach(&h2data->psock);

			h2data = next;
		}

		/*
		 * All streams are now destroyed; close the session.
		 */
		finish_http_session(session);
	}
}

static const bool base64url_validation_table[256] = {
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, true,  false, false, true,  true,
	true,  true,  true,  true,  true,  true,  true,	 true,	false, false,
	false, false, false, false, false, true,  true,	 true,	true,  true,
	true,  true,  true,  true,  true,  true,  true,	 true,	true,  true,
	true,  true,  true,  true,  true,  true,  true,	 true,	true,  true,
	true,  false, false, false, false, true,  false, true,	true,  true,
	true,  true,  true,  true,  true,  true,  true,	 true,	true,  true,
	true,  true,  true,  true,  true,  true,  true,	 true,	true,  true,
	true,  true,  true,  false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false
};

char *
isc__nm_base64url_to_base64(isc_mem_t *mem, const char *base64url,
			    const size_t base64url_len, size_t *res_len) {
	char *res = NULL;
	size_t i, k, len;

	if (mem == NULL || base64url == NULL || base64url_len == 0) {
		return (NULL);
	}

	len = base64url_len % 4 ? base64url_len + (4 - base64url_len % 4)
				: base64url_len;
	res = isc_mem_allocate(mem, len + 1); /* '\0' */

	for (i = 0; i < base64url_len; i++) {
		switch (base64url[i]) {
		case '-':
			res[i] = '+';
			break;
		case '_':
			res[i] = '/';
			break;
		default:
			if (base64url_validation_table[(size_t)base64url[i]]) {
				res[i] = base64url[i];
			} else {
				isc_mem_free(mem, res);
				return (NULL);
			}
			break;
		}
	}

	if (base64url_len % 4 != 0) {
		for (k = 0; k < (4 - base64url_len % 4); k++, i++) {
			res[i] = '=';
		}
	}

	INSIST(i == len);

	if (res_len != NULL) {
		*res_len = len;
	}

	res[len] = '\0';

	return (res);
}

char *
isc__nm_base64_to_base64url(isc_mem_t *mem, const char *base64,
			    const size_t base64_len, size_t *res_len) {
	char *res = NULL;
	size_t i;

	if (mem == NULL || base64 == NULL || base64_len == 0) {
		return (NULL);
	}

	res = isc_mem_allocate(mem, base64_len + 1); /* '\0' */

	for (i = 0; i < base64_len; i++) {
		switch (base64[i]) {
		case '+':
			res[i] = '-';
			break;
		case '/':
			res[i] = '_';
			break;
		case '=':
			goto end;
			break;
		default:
			/*
			 * All other characters from the alphabet are the same
			 * for both base64 and base64url, so we can reuse the
			 * validation table for the rest of the characters.
			 */
			if (base64[i] != '-' && base64[i] != '_' &&
			    base64url_validation_table[(size_t)base64[i]])
			{
				res[i] = base64[i];
			} else {
				isc_mem_free(mem, res);
				return (NULL);
			}
			break;
		}
	}
end:
	if (res_len) {
		*res_len = i;
	}

	res[i] = '\0';

	return (res);
}

void
isc__nm_http_initsocket(isc_nmsocket_t *sock) {
	REQUIRE(sock != NULL);

	sock->h2 = (isc_nmsocket_h2_t){
		.request_type = ISC_HTTP_REQ_UNSUPPORTED,
		.request_scheme = ISC_HTTP_SCHEME_UNSUPPORTED,
	};

	if (sock->type == isc_nm_httplistener) {
		ISC_LIST_INIT(sock->h2.handlers);
		ISC_LIST_INIT(sock->h2.handler_cbargs);
		isc_rwlock_init(&sock->h2.lock, 0, 1);
	}
}

void
isc__nm_http_cleanup_data(isc_nmsocket_t *sock) {
	if ((sock->type == isc_nm_tcplistener ||
	     sock->type == isc_nm_tlslistener) &&
	    sock->h2.httpserver != NULL)
	{
		isc__nmsocket_detach(&sock->h2.httpserver);
	}

	if (sock->type == isc_nm_httplistener ||
	    sock->type == isc_nm_httpsocket) {
		if (sock->type == isc_nm_httplistener) {
			clear_handlers(sock);
			isc_rwlock_destroy(&sock->h2.lock);
		}

		if (sock->h2.request_path != NULL) {
			isc_mem_free(sock->mgr->mctx, sock->h2.request_path);
			sock->h2.request_path = NULL;
		}

		if (sock->h2.query_data != NULL) {
			isc_mem_free(sock->mgr->mctx, sock->h2.query_data);
			sock->h2.query_data = NULL;
		}

		if (sock->h2.connect.cstream != NULL) {
			put_http_cstream(sock->mgr->mctx,
					 sock->h2.connect.cstream);
			sock->h2.connect.cstream = NULL;
		}

		if (sock->h2.buf != NULL) {
			isc_mem_free(sock->mgr->mctx, sock->h2.buf);
			sock->h2.buf = NULL;
		}
	}

	if ((sock->type == isc_nm_httplistener ||
	     sock->type == isc_nm_httpsocket ||
	     sock->type == isc_nm_tcpsocket ||
	     sock->type == isc_nm_tlssocket) &&
	    sock->h2.session != NULL)
	{
		if (sock->h2.connect.uri != NULL) {
			isc_mem_free(sock->mgr->mctx, sock->h2.connect.uri);
			sock->h2.connect.uri = NULL;
		}
		isc__nm_httpsession_detach(&sock->h2.session);
	}
}

void
isc__nm_http_cleartimeout(isc_nmhandle_t *handle) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_httpsocket);

	sock = handle->sock;
	if (sock->h2.session != NULL && sock->h2.session->handle) {
		INSIST(VALID_HTTP2_SESSION(sock->h2.session));
		INSIST(VALID_NMHANDLE(sock->h2.session->handle));
		isc_nmhandle_cleartimeout(sock->h2.session->handle);
	}
}

void
isc__nm_http_settimeout(isc_nmhandle_t *handle, uint32_t timeout) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_httpsocket);

	sock = handle->sock;
	if (sock->h2.session != NULL && sock->h2.session->handle) {
		INSIST(VALID_HTTP2_SESSION(sock->h2.session));
		INSIST(VALID_NMHANDLE(sock->h2.session->handle));
		isc_nmhandle_settimeout(sock->h2.session->handle, timeout);
	}
}

/*
 * DoH GET Query String Scanner-less Recursive Descent Parser/Verifier
 *
 * It is based on the following grammar (using WSN/EBNF):
 *
 * S                = query-string.
 * query-string     = ['?'] { key-value-pair } EOF.
 * key-value-pair   = key '=' value [ '&' ].
 * key              = ('_' | alpha) { '_' | alnum}.
 * value            = value-char {value-char}.
 * value-char       = unreserved-char | percent-charcode.
 * unreserved-char  = alnum |'_' | '.' | '-' | '~'. (* RFC3986, Section 2.3 *)
 * percent-charcode = '%' hexdigit hexdigit.
 * ...
 *
 * Should be good enough.
 */
typedef struct isc_httpparser_state {
	const char *str;

	const char *last_key;
	size_t last_key_len;

	const char *last_value;
	size_t last_value_len;

	bool query_found;
	const char *query;
	size_t query_len;
} isc_httpparser_state_t;

#define MATCH(ch)      (st->str[0] == (ch))
#define MATCH_ALPHA()  isalpha((unsigned char)(st->str[0]))
#define MATCH_ALNUM()  isalnum((unsigned char)(st->str[0]))
#define MATCH_XDIGIT() isxdigit((unsigned char)(st->str[0]))
#define ADVANCE()      st->str++
#define GETP()	       (st->str)

static bool
rule_query_string(isc_httpparser_state_t *st);

bool
isc__nm_parse_httpquery(const char *query_string, const char **start,
			size_t *len) {
	isc_httpparser_state_t state;

	REQUIRE(start != NULL);
	REQUIRE(len != NULL);

	if (query_string == NULL || query_string[0] == '\0') {
		return (false);
	}

	state = (isc_httpparser_state_t){ .str = query_string };
	if (!rule_query_string(&state)) {
		return (false);
	}

	if (!state.query_found) {
		return (false);
	}

	*start = state.query;
	*len = state.query_len;

	return (true);
}

static bool
rule_key_value_pair(isc_httpparser_state_t *st);

static bool
rule_key(isc_httpparser_state_t *st);

static bool
rule_value(isc_httpparser_state_t *st);

static bool
rule_value_char(isc_httpparser_state_t *st);

static bool
rule_percent_charcode(isc_httpparser_state_t *st);

static bool
rule_unreserved_char(isc_httpparser_state_t *st);

static bool
rule_query_string(isc_httpparser_state_t *st) {
	if (MATCH('?')) {
		ADVANCE();
	}

	while (rule_key_value_pair(st)) {
		/* skip */;
	}

	if (!MATCH('\0')) {
		return (false);
	}

	ADVANCE();
	return (true);
}

static bool
rule_key_value_pair(isc_httpparser_state_t *st) {
	if (!rule_key(st)) {
		return (false);
	}

	if (MATCH('=')) {
		ADVANCE();
	} else {
		return (false);
	}

	if (rule_value(st)) {
		const char dns[] = "dns";
		if (st->last_key_len == sizeof(dns) - 1 &&
		    memcmp(st->last_key, dns, sizeof(dns) - 1) == 0)
		{
			st->query_found = true;
			st->query = st->last_value;
			st->query_len = st->last_value_len;
		}
	} else {
		return (false);
	}

	if (MATCH('&')) {
		ADVANCE();
	}

	return (true);
}

static bool
rule_key(isc_httpparser_state_t *st) {
	if (MATCH('_') || MATCH_ALPHA()) {
		st->last_key = GETP();
		ADVANCE();
	} else {
		return (false);
	}

	while (MATCH('_') || MATCH_ALNUM()) {
		ADVANCE();
	}

	st->last_key_len = GETP() - st->last_key;
	return (true);
}

static bool
rule_value(isc_httpparser_state_t *st) {
	const char *s = GETP();
	if (!rule_value_char(st)) {
		return (false);
	}

	st->last_value = s;
	while (rule_value_char(st)) {
		/* skip */;
	}
	st->last_value_len = GETP() - st->last_value;
	return (true);
}

static bool
rule_value_char(isc_httpparser_state_t *st) {
	if (rule_unreserved_char(st)) {
		return (true);
	}

	return (rule_percent_charcode(st));
}

static bool
rule_unreserved_char(isc_httpparser_state_t *st) {
	if (MATCH_ALNUM() || MATCH('_') || MATCH('.') || MATCH('-') ||
	    MATCH('~')) {
		ADVANCE();
		return (true);
	}
	return (false);
}

static bool
rule_percent_charcode(isc_httpparser_state_t *st) {
	if (MATCH('%')) {
		ADVANCE();
	} else {
		return (false);
	}

	if (!MATCH_XDIGIT()) {
		return (false);
	}
	ADVANCE();

	if (!MATCH_XDIGIT()) {
		return (false);
	}
	ADVANCE();

	return (true);
}
