diff options
Diffstat (limited to 'lib/isc/netmgr/http.c')
-rw-r--r-- | lib/isc/netmgr/http.c | 3755 |
1 files changed, 3755 insertions, 0 deletions
diff --git a/lib/isc/netmgr/http.c b/lib/isc/netmgr/http.c new file mode 100644 index 0000000..f2d3e2d --- /dev/null +++ b/lib/isc/netmgr/http.c @@ -0,0 +1,3755 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * 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/log.h> +#include <isc/netmgr.h> +#include <isc/print.h> +#include <isc/sockaddr.h> +#include <isc/tls.h> +#include <isc/url.h> +#include <isc/util.h> + +#include "netmgr-int.h" + +#define AUTHEXTRA 7 + +#define MAX_DNS_MESSAGE_SIZE (UINT16_MAX) + +#define DNS_MEDIA_TYPE "application/dns-message" + +/* + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + * for additional details. Basically it means "avoid caching by any + * means." + */ +#define DEFAULT_CACHE_CONTROL "no-cache, no-store, must-revalidate" + +/* + * 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 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) + +/* This definition sets the upper limit of pending write buffer to an + * adequate enough value. That is done mostly to fight a limitation + * for a max TLS record size in flamethrower (2K). In a perfect world + * this constant should not be required, if we ever move closer to + * that state, the constant, and corresponding code, should be + * removed. For now the limit seems adequate enough to fight + * "tinygrams" problem. */ +#define FLUSH_HTTP_WRITE_BUFFER_AFTER (1536) + +/* This switch is here mostly to test the code interoperability with + * buggy implementations */ +#define ENABLE_HTTP_WRITE_BUFFERING 1 + +#define SUCCESSFUL_HTTP_STATUS(code) \ + ((code) >= MIN_SUCCESSFUL_HTTP_STATUS && \ + (code) <= MAX_SUCCESSFUL_HTTP_STATUS) + +#define INITIAL_DNS_MESSAGE_BUFFER_SIZE (512) + +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; + + isc_buffer_t *rbuf; + + size_t pathlen; + int32_t stream_id; + + bool post; /* POST or GET */ + isc_buffer_t *postdata; + char *GET_path; + size_t GET_path_len; + + isc_nm_http_response_status_t response_status; + isc_nmsocket_t *httpsock; + 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) + +typedef ISC_LIST(isc__nm_uvreq_t) isc__nm_http_pending_callbacks_t; + +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_buffer_t *buf; + + isc_tlsctx_t *tlsctx; + uint32_t max_concurrent_streams; + + isc__nm_http_pending_callbacks_t pending_write_callbacks; + isc_buffer_t *pending_write_data; +}; + +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_nm_cb_t cb; + void *cbarg; + isc_buffer_t *pending_write_data; + isc__nm_http_pending_callbacks_t pending_write_callbacks; +} isc_http_send_req_t; + +#define HTTP_ENDPOINTS_MAGIC ISC_MAGIC('H', 'T', 'E', 'P') +#define VALID_HTTP_ENDPOINTS(t) ISC_MAGIC_VALID(t, HTTP_ENDPOINTS_MAGIC) + +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 +client_call_failed_read_cb(isc_result_t result, isc_nm_http_session_t *session); + +static void +server_call_failed_read_cb(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 void +call_pending_callbacks(isc__nm_http_pending_callbacks_t pending_callbacks, + isc_result_t result); + +static void +server_call_cb(isc_nmsocket_t *socket, isc_nm_http_session_t *session, + const isc_result_t result, isc_region_t *data); + +static isc_nm_httphandler_t * +http_endpoints_find(const char *request_path, + const isc_nm_http_endpoints_t *restrict eps); + +static void +http_init_listener_endpoints(isc_nmsocket_t *listener, + isc_nm_http_endpoints_t *epset); + +static void +http_cleanup_listener_endpoints(isc_nmsocket_t *listener); + +static isc_nm_http_endpoints_t * +http_get_listener_endpoints(isc_nmsocket_t *listener, const int tid); + +static bool +http_session_active(isc_nm_http_session_t *session) { + REQUIRE(VALID_HTTP2_SESSION(session)); + return (!session->closed && !session->closing); +} + +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); + ISC_LIST_INIT(session->pending_write_callbacks); + + *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; + } + + if (session->buf != NULL) { + isc_buffer_free(&session->buf); + } + + /* 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); + } + + isc__nmsocket_attach(sock, &stream->httpsock); + 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); + } + + isc_buffer_allocate(mctx, &stream->rbuf, + INITIAL_DNS_MESSAGE_BUFFER_SIZE); + isc_buffer_setautorealloc(stream->rbuf, true); + + ISC_LIST_PREPEND(sock->h2.session->cstreams, stream, link); + *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 != NULL) { + INSIST(stream->post); + isc_buffer_free(&stream->postdata); + } + + if (stream == stream->httpsock->h2.connect.cstream) { + stream->httpsock->h2.connect.cstream = NULL; + } + if (ISC_LINK_LINKED(stream, link)) { + ISC_LIST_UNLINK(stream->httpsock->h2.session->cstreams, stream, + link); + } + isc__nmsocket_detach(&stream->httpsock); + + isc_buffer_free(&stream->rbuf); + 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 (session->client) { + client_call_failed_read_cb(ISC_R_UNEXPECTED, session); + } else { + server_call_failed_read_cb(ISC_R_UNEXPECTED, session); + } + + call_pending_callbacks(session->pending_write_callbacks, + ISC_R_UNEXPECTED); + ISC_LIST_INIT(session->pending_write_callbacks); + + if (session->pending_write_data != NULL) { + isc_buffer_free(&session->pending_write_data); + } + + 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 = len; + INSIST(cstream->rbuf != NULL); + new_rbufsize += isc_buffer_usedlength(cstream->rbuf); + if (new_rbufsize <= MAX_DNS_MESSAGE_SIZE && + new_rbufsize <= cstream->response_status.content_length) + { + isc_buffer_putmem(cstream->rbuf, data, len); + } 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) { + if (isc_buffer_base(&h2->rbuf) == NULL) { + isc_buffer_init( + &h2->rbuf, + isc_mem_allocate(session->mctx, + h2->content_length), + MAX_DNS_MESSAGE_SIZE); + } + size_t new_bufsize = isc_buffer_usedlength(&h2->rbuf) + + len; + if (new_bufsize <= MAX_DNS_MESSAGE_SIZE && + new_bufsize <= h2->content_length) + { + isc_buffer_putmem(&h2->rbuf, data, len); + 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) { + isc_region_t read_data; + REQUIRE(VALID_HTTP2_SESSION(session)); + REQUIRE(cstream != NULL); + ISC_LIST_UNLINK(session->cstreams, cstream, link); + INSIST(VALID_NMHANDLE(session->client_httphandle)); + isc_buffer_usedregion(cstream->rbuf, &read_data); + cstream->read_cb(session->client_httphandle, result, &read_data, + cstream->read_cbarg); + 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); + } + /* Mark the session as closing one to finish it on a + * subsequent call to http_do_bio() */ + session->closing = true; + } + } 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); + session->nsstreams--; + + /* + * By making a call to isc__nmsocket_prep_destroy(), we ensure that + * the socket gets marked as inactive, allowing the HTTP/2 data + * associated with it to be properly disposed of eventually. + * + * An HTTP/2 stream socket will normally be marked as inactive in + * the normal course of operation. However, when browsers terminate + * HTTP/2 streams prematurely (e.g. by sending RST_STREAM), + * corresponding sockets can remain marked as active, retaining + * references to the HTTP/2 data (most notably the session objects), + * preventing them from being correctly freed and leading to BIND + * hanging on shutdown. Calling isc__nmsocket_prep_destroy() + * ensures that this will not happen. + */ + isc__nmsocket_prep_destroy(sock); + 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); + + UNUSED(flags); + UNUSED(ngsession); + + cstream = find_http_cstream(frame->hd.stream_id, session); + if (cstream == NULL) { + /* + * This could happen in two cases: + * - the server sent us bad data, or + * - we closed the session prematurely before receiving all + * responses (i.e., because of a belated or partial response). + */ + return (NGHTTP2_ERR_CALLBACK_FAILURE); + } + + INSIST(!ISC_LIST_EMPTY(session->cstreams)); + + 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 = isc_buffer_remaininglength(cstream->postdata); + + if (len > length) { + len = length; + } + + if (len > 0) { + memmove(buf, isc_buffer_current(cstream->postdata), + len); + isc_buffer_forward(cstream->postdata, len); + } + + if (isc_buffer_remaininglength(cstream->postdata) == 0) { + *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", + isc_buffer_usedlength(stream->postdata)); + 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) { + size_t unread_size = region->length - readlen; + if (session->buf == NULL) { + isc_buffer_allocate(session->mctx, &session->buf, + unread_size); + isc_buffer_setautorealloc(session->buf, true); + } + isc_buffer_putmem(session->buf, region->base + readlen, + unread_size); + isc_nm_pauseread(session->handle); + } + + /* We might have something to receive or send, do IO */ + http_do_bio(session, NULL, NULL, NULL); +} + +static void +call_pending_callbacks(isc__nm_http_pending_callbacks_t pending_callbacks, + isc_result_t result) { + isc__nm_uvreq_t *cbreq = ISC_LIST_HEAD(pending_callbacks); + while (cbreq != NULL) { + isc__nm_uvreq_t *next = ISC_LIST_NEXT(cbreq, link); + ISC_LIST_UNLINK(pending_callbacks, cbreq, link); + isc__nm_sendcb(cbreq->handle->sock, cbreq, result, false); + cbreq = next; + } +} + +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); + } + + call_pending_callbacks(req->pending_write_callbacks, result); + + if (req->cb != NULL) { + req->cb(req->httphandle, result, req->cbarg); + isc_nmhandle_detach(&req->httphandle); + } + + isc_buffer_free(&req->pending_write_data); + isc_mem_put(session->mctx, req, sizeof(*req)); + + session->sending--; + http_do_bio(session, NULL, NULL, NULL); + isc_nmhandle_detach(&transphandle); + if (result != ISC_R_SUCCESS && session->sending == 0) { + finish_http_session(session); + } + isc__nm_httpsession_detach(&session); +} + +static void +move_pending_send_callbacks(isc_nm_http_session_t *session, + isc_http_send_req_t *send) { + STATIC_ASSERT( + sizeof(session->pending_write_callbacks) == + sizeof(send->pending_write_callbacks), + "size of pending writes requests callbacks lists differs"); + memmove(&send->pending_write_callbacks, + &session->pending_write_callbacks, + sizeof(session->pending_write_callbacks)); + ISC_LIST_INIT(session->pending_write_callbacks); +} + +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; + size_t total = 0; + isc_region_t send_data = { 0 }; + isc_nmhandle_t *transphandle = NULL; +#ifdef ENABLE_HTTP_WRITE_BUFFERING + size_t max_total_write_size = 0; +#endif /* ENABLE_HTTP_WRITE_BUFFERING */ + + if (!http_session_active(session) || + (!nghttp2_session_want_write(session->ngsession) && + session->pending_write_data == NULL)) + { + return (false); + } + + /* We need to attach to the session->handle earlier because as an + * indirect result of the nghttp2_session_mem_send() the session + * might get closed and the handle detached. However, there is + * still some outgoing data to handle and we need to call it + * anyway if only to get the write callback passed here to get + * called properly. */ + isc_nmhandle_attach(session->handle, &transphandle); + + while (nghttp2_session_want_write(session->ngsession)) { + const uint8_t *data = NULL; + const size_t pending = + nghttp2_session_mem_send(session->ngsession, &data); + const size_t new_total = total + pending; + + /* Sometimes nghttp2_session_mem_send() does not return any + * data to send even though nghttp2_session_want_write() + * returns success. */ + if (pending == 0 || data == NULL) { + break; + } + + /* reallocate buffer if required */ + if (session->pending_write_data == NULL) { + isc_buffer_allocate(session->mctx, + &session->pending_write_data, + INITIAL_DNS_MESSAGE_BUFFER_SIZE); + isc_buffer_setautorealloc(session->pending_write_data, + true); + } + isc_buffer_putmem(session->pending_write_data, data, pending); + total = new_total; + } + +#ifdef ENABLE_HTTP_WRITE_BUFFERING + if (session->pending_write_data != NULL) { + max_total_write_size = + isc_buffer_usedlength(session->pending_write_data); + } + + /* Here we are trying to flush the pending writes buffer earlier + * to avoid hitting unnecessary limitations on a TLS record size + * within some tools (e.g. flamethrower). */ + if (max_total_write_size >= FLUSH_HTTP_WRITE_BUFFER_AFTER) { + /* Case 1: We have equal or more than + * FLUSH_HTTP_WRITE_BUFFER_AFTER bytes to send. Let's flush it. + */ + total = max_total_write_size; + } else if (session->sending > 0 && total > 0) { + /* Case 2: There is one or more write requests in flight and + * we have some new data form nghttp2 to send. Let's put the + * write callback (if any) into the pending write callbacks + * list. Then let's return from the function: as soon as the + * "in-flight" write callback get's called or we have reached + * FLUSH_HTTP_WRITE_BUFFER_AFTER bytes in the write buffer, we + * will flush the buffer. */ + if (cb != NULL) { + isc__nm_uvreq_t *newcb = isc__nm_uvreq_get( + httphandle->sock->mgr, httphandle->sock); + + INSIST(VALID_NMHANDLE(httphandle)); + newcb->cb.send = cb; + newcb->cbarg = cbarg; + isc_nmhandle_attach(httphandle, &newcb->handle); + ISC_LIST_APPEND(session->pending_write_callbacks, newcb, + link); + } + goto nothing_to_send; + } else if (session->sending == 0 && total == 0 && + session->pending_write_data != NULL) + { + /* Case 3: There is no write in flight and we haven't got + * anything new from nghttp2, but there is some data pending + * in the write buffer. Let's flush the buffer. */ + isc_region_t region = { 0 }; + total = isc_buffer_usedlength(session->pending_write_data); + INSIST(total > 0); + isc_buffer_usedregion(session->pending_write_data, ®ion); + INSIST(total == region.length); + } else { + /* The other cases are, uninteresting, fall-through ones. */ + /* In the following cases (4-6) we will just bail out. */ + /* Case 4: There is nothing new to send, nor anything in the + * write buffer. */ + /* Case 5: There is nothing new to send and there is write + * request(s) in flight. */ + /* Case 6: There is nothing new to send nor there are any + * write requests in flight. */ + + /* Case 7: There is some new data to send and there are no any + * write requests in flight: Let's send the data.*/ + INSIST((total == 0 && session->pending_write_data == NULL) || + (total == 0 && session->sending > 0) || + (total == 0 && session->sending == 0) || + (total > 0 && session->sending == 0)); + } +#else + INSIST(ISC_LIST_EMPTY(session->pending_write_callbacks)); +#endif /* ENABLE_HTTP_WRITE_BUFFERING */ + + if (total == 0) { + /* No data returned */ + goto nothing_to_send; + } + + /* If we have reached the point it means that we need to send some + * data and flush the outgoing buffer. The code below does that. */ + send = isc_mem_get(session->mctx, sizeof(*send)); + + *send = (isc_http_send_req_t){ .pending_write_data = + session->pending_write_data, + .cb = cb, + .cbarg = cbarg }; + session->pending_write_data = NULL; + move_pending_send_callbacks(session, send); + + send->transphandle = transphandle; + isc__nm_httpsession_attach(session, &send->session); + + if (cb != NULL) { + INSIST(VALID_NMHANDLE(httphandle)); + isc_nmhandle_attach(httphandle, &send->httphandle); + } + + session->sending++; + isc_buffer_usedregion(send->pending_write_data, &send_data); + isc_nm_send(transphandle, &send_data, http_writecb, send); + return (true); +nothing_to_send: + isc_nmhandle_detach(&transphandle); + return (false); +} + +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->pending_write_data == NULL) + { + 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->buf != NULL) { + size_t remaining = + isc_buffer_remaininglength(session->buf); + /* Leftover data in the buffer, use it */ + size_t readlen = nghttp2_session_mem_recv( + session->ngsession, + isc_buffer_current(session->buf), remaining); + + if (readlen == remaining) { + isc_buffer_free(&session->buf); + } else { + isc_buffer_forward(session->buf, 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); + + REQUIRE(sock->connect_cb != NULL); + + if (result == ISC_R_SUCCESS) { + 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); + } else { + void *cbarg = sock->connect_cbarg; + isc_nm_cb_t connect_cb = sock->connect_cb; + + isc__nmsocket_clearcb(sock); + connect_cb(httphandle, result, cbarg); + isc_nmhandle_detach(&httphandle); + } +} + +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; + http_sock->h2.connect.tls_peer_verify_string = + isc_nm_verify_tls_peer_result_string(handle); + 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; + /* otherwise we will get some garbage output in DIG */ + http_sock->iface = handle->sock->iface; + http_sock->peer = handle->sock->peer; + + 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_selected_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. + */ + 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_prep_destroy(http_sock); + isc__nmsocket_detach(&http_sock); +} + +void +isc_nm_httpconnect(isc_nm_t *mgr, isc_sockaddr_t *local, isc_sockaddr_t *peer, + const char *uri, bool post, isc_nm_cb_t cb, void *cbarg, + isc_tlsctx_t *tlsctx, + isc_tlsctx_client_session_cache_t *client_sess_cache, + unsigned int timeout, size_t extrahandlesize) { + isc_sockaddr_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, peer->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_UNSET; + 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; + req->local = *local; + req->handle = isc__nmhandle_get(sock, &req->peer, &sock->iface); + + if (isc__nm_in_netthread()) { + sock->tid = isc_nm_tid(); + } + + isc__nmsocket_clearcb(sock); + isc__nm_connectcb(sock, req, ISC_R_SHUTTINGDOWN, 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, client_sess_cache, 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); + + if (session->closed) { + return (ISC_R_CANCELED); + } + + INSIST(cstream != NULL); + + if (cstream->post) { + /* POST */ + isc_buffer_allocate(mctx, &cstream->postdata, region->length); + isc_buffer_putmem(cstream->postdata, region->base, + region->length); + } 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; + + sock->h2.connect.cstream = NULL; + result = client_submit_request(session, cstream); + if (result != ISC_R_SUCCESS) { + put_http_cstream(session->mctx, cstream); + 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(atomic_load(&handle->sock->client)); + + 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; + } + + 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 >= session->max_concurrent_streams) { + 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_sockaddr_t *)&session->handle->sock->iface); + socket->peer = session->handle->sock->peer; + socket->h2 = (isc_nmsocket_h2_t){ + .psock = socket, + .stream_id = frame->hd.stream_id, + .headers_error_code = ISC_HTTP_ERROR_SUCCESS, + .request_type = ISC_HTTP_REQ_UNSUPPORTED, + .request_scheme = ISC_HTTP_SCHEME_UNSUPPORTED + }; + isc_buffer_initnull(&socket->h2.rbuf); + isc_buffer_initnull(&socket->h2.wbuf); + 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, const int tid) { + isc_nm_httphandler_t *handler = NULL; + + REQUIRE(VALID_NMSOCK(serversocket)); + + if (atomic_load(&serversocket->listening)) { + handler = http_endpoints_find( + request_path, + http_get_listener_endpoints(serversocket, tid)); + } + 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); + + if (!isc_nm_http_path_isvalid(socket->h2.request_path)) { + isc_mem_free(socket->mgr->mctx, socket->h2.request_path); + socket->h2.request_path = NULL; + return (ISC_HTTP_ERROR_BAD_REQUEST); + } + + handler = find_server_request_handler(socket->h2.request_path, + socket->h2.session->serversocket, + socket->tid); + 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 (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; + + 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_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 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); + } + + 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 = isc_buffer_remaininglength(&socket->h2.wbuf); + if (buflen > length) { + buflen = length; + } + + if (buflen > 0) { + (void)memmove(buf, isc_buffer_current(&socket->h2.wbuf), + buflen); + isc_buffer_forward(&socket->h2.wbuf, buflen); + } + + if (isc_buffer_remaininglength(&socket->h2.wbuf) == 0) { + *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; + + if (socket->h2.response_submitted) { + /* NGHTTP2 will gladly accept new response (write request) + * from us even though we cannot send more than one over the + * same HTTP/2 stream. Thus, we need to handle this case + * manually. We will return failure code so that it will be + * passed to the write callback. */ + return (ISC_R_FAILURE); + } + + 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); + } + + socket->h2.response_submitted = true; + return (ISC_R_SUCCESS); +} + +#define MAKE_ERROR_REPLY(tag, code, desc) \ + { \ + tag, MAKE_NV2(":status", #code), desc \ + } + +/* + * 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; + const char *desc; +} error_responses[] = { + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_BAD_REQUEST, 400, "Bad Request"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_NOT_FOUND, 404, "Not Found"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE, 413, + "Payload Too Large"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_URI_TOO_LONG, 414, "URI Too Long"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_UNSUPPORTED_MEDIA_TYPE, 415, + "Unsupported Media Type"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_GENERIC, 500, "Internal Server Error"), + MAKE_ERROR_REPLY(ISC_HTTP_ERROR_NOT_IMPLEMENTED, 501, "Not Implemented") +}; + +static void +log_server_error_response(const isc_nmsocket_t *socket, + const struct http_error_responses *response) { + const int log_level = ISC_LOG_DEBUG(1); + isc_sockaddr_t client_addr; + isc_sockaddr_t local_addr; + char client_sabuf[ISC_SOCKADDR_FORMATSIZE]; + char local_sabuf[ISC_SOCKADDR_FORMATSIZE]; + + if (!isc_log_wouldlog(isc_lctx, log_level)) { + return; + } + + client_addr = isc_nmhandle_peeraddr(socket->h2.session->handle); + local_addr = isc_nmhandle_localaddr(socket->h2.session->handle); + isc_sockaddr_format(&client_addr, client_sabuf, sizeof(client_sabuf)); + isc_sockaddr_format(&local_addr, local_sabuf, sizeof(local_sabuf)); + isc_log_write(isc_lctx, ISC_LOGCATEGORY_GENERAL, ISC_LOGMODULE_NETMGR, + log_level, "HTTP/2 request from %s (on %s) failed: %s %s", + client_sabuf, local_sabuf, response->header.value, + response->desc); +} + +static isc_result_t +server_send_error_response(const isc_http_error_responses_t error, + nghttp2_session *ngsession, isc_nmsocket_t *socket) { + void *base; + + REQUIRE(error != ISC_HTTP_ERROR_SUCCESS); + + base = isc_buffer_base(&socket->h2.rbuf); + if (base != NULL) { + isc_mem_free(socket->h2.session->mctx, base); + isc_buffer_initnull(&socket->h2.rbuf); + } + + /* We do not want the error response to be cached anywhere. */ + socket->h2.min_ttl = 0; + + for (size_t i = 0; + i < sizeof(error_responses) / sizeof(error_responses[0]); i++) + { + if (error_responses[i].type == error) { + log_server_error_response(socket, &error_responses[i]); + 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 void +server_call_cb(isc_nmsocket_t *socket, isc_nm_http_session_t *session, + const isc_result_t result, isc_region_t *data) { + isc_sockaddr_t addr; + isc_nmhandle_t *handle = NULL; + + REQUIRE(VALID_NMSOCK(socket)); + REQUIRE(VALID_HTTP2_SESSION(session)); + REQUIRE(socket->h2.cb != NULL); + + addr = isc_nmhandle_peeraddr(session->handle); + handle = isc__nmhandle_get(socket, &addr, NULL); + socket->h2.cb(handle, result, data, socket->h2.cbarg); + isc_nmhandle_detach(&handle); +} + +void +isc__nm_http_bad_request(isc_nmhandle_t *handle) { + isc_nmsocket_t *sock = NULL; + + REQUIRE(VALID_NMHANDLE(handle)); + REQUIRE(VALID_NMSOCK(handle->sock)); + sock = handle->sock; + REQUIRE(sock->type == isc_nm_httpsocket); + REQUIRE(!atomic_load(&sock->client)); + REQUIRE(VALID_HTTP2_SESSION(sock->h2.session)); + + (void)server_send_error_response(ISC_HTTP_ERROR_BAD_REQUEST, + sock->h2.session->ngsession, sock); +} + +static int +server_on_request_recv(nghttp2_session *ngsession, + isc_nm_http_session_t *session, isc_nmsocket_t *socket) { + isc_result_t result; + isc_http_error_responses_t code = ISC_HTTP_ERROR_SUCCESS; + isc_region_t data; + uint8_t tmp_buf[MAX_DNS_MESSAGE_SIZE]; + + 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.content_length == 0) + { + code = ISC_HTTP_ERROR_BAD_REQUEST; + } else if (socket->h2.request_type == ISC_HTTP_REQ_POST && + isc_buffer_usedlength(&socket->h2.rbuf) > + socket->h2.content_length) + { + code = ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE; + } else if (socket->h2.request_type == ISC_HTTP_REQ_POST && + isc_buffer_usedlength(&socket->h2.rbuf) != + socket->h2.content_length) + { + code = ISC_HTTP_ERROR_BAD_REQUEST; + } else if (socket->h2.request_type == ISC_HTTP_REQ_POST && + socket->h2.query_data != NULL) + { + /* The spec does not mention which value the query string for + * POST should have. For GET we use its value to decode a DNS + * message from it, for POST the message is transferred in the + * body of the request. Taking it into account, it is much safer + * to treat POST + * requests with query strings as malformed ones. */ + code = ISC_HTTP_ERROR_BAD_REQUEST; + } else if (socket->h2.request_type == ISC_HTTP_REQ_GET && + socket->h2.content_length > 0) + { + code = ISC_HTTP_ERROR_BAD_REQUEST; + } else if (socket->h2.request_type == ISC_HTTP_REQ_GET && + socket->h2.query_data == NULL) + { + /* A GET request without any query data - there is nothing to + * decode. */ + INSIST(socket->h2.query_data_len == 0); + 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, tmp_buf, sizeof(tmp_buf)); + if (isc_base64_decodestring(socket->h2.query_data, + &decoded_buf) != ISC_R_SUCCESS) + { + code = ISC_HTTP_ERROR_BAD_REQUEST; + goto error; + } + isc_buffer_usedregion(&decoded_buf, &data); + } else if (socket->h2.request_type == ISC_HTTP_REQ_POST) { + INSIST(socket->h2.content_length > 0); + isc_buffer_usedregion(&socket->h2.rbuf, &data); + } else { + UNREACHABLE(); + } + + server_call_cb(socket, session, ISC_R_SUCCESS, &data); + + return (0); + +error: + result = server_send_error_response(code, ngsession, socket); + if (result != ISC_R_SUCCESS) { + return (NGHTTP2_ERR_TEMPORAL_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 content_len_buf_len, cache_control_buf_len; + isc_result_t result = ISC_R_SUCCESS; + isc_nm_cb_t cb = req->cb.send; + void *cbarg = req->cbarg; + if (isc__nmsocket_closing(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)); + + isc_buffer_init(&sock->h2.wbuf, req->uvbuf.base, req->uvbuf.len); + isc_buffer_add(&sock->h2.wbuf, req->uvbuf.len); + + content_len_buf_len = snprintf(sock->h2.clenbuf, + sizeof(sock->h2.clenbuf), "%lu", + (unsigned long)req->uvbuf.len); + if (sock->h2.min_ttl == 0) { + cache_control_buf_len = + snprintf(sock->h2.cache_control_buf, + sizeof(sock->h2.cache_control_buf), "%s", + DEFAULT_CACHE_CONTROL); + } else { + cache_control_buf_len = + snprintf(sock->h2.cache_control_buf, + sizeof(sock->h2.cache_control_buf), + "max-age=%" PRIu32, sock->h2.min_ttl); + } + const nghttp2_nv hdrs[] = { MAKE_NV2(":status", "200"), + MAKE_NV2("Content-Type", DNS_MEDIA_TYPE), + MAKE_NV("Content-Length", sock->h2.clenbuf, + content_len_buf_len), + MAKE_NV("Cache-Control", + sock->h2.cache_control_buf, + cache_control_buf_len) }; + + 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 (cstream->sending) { + result = client_submit_request(session, cstream); + if (result != ISC_R_SUCCESS) { + put_http_cstream(session->mctx, cstream); + 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, + session->max_concurrent_streams } + }; + 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) { + 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); +} + +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 (isc__nmsocket_closing(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 (isc__nmsocket_closing(httplistensock) || + !atomic_load(&httplistensock->listening)) + { + return (ISC_R_CANCELED); + } + + http_transpost_tcp_nodelay(handle); + + new_session(httplistensock->mgr->mctx, NULL, &session); + session->max_concurrent_streams = + atomic_load(&httplistensock->h2.max_concurrent_streams); + initialize_nghttp2_server_session(session); + handle->sock->h2.session = session; + + isc_nmhandle_attach(handle, &session->handle); + isc__nmsocket_attach(httplistensock, &session->serversocket); + 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_sockaddr_t *iface, int backlog, + isc_quota_t *quota, isc_tlsctx_t *ctx, + isc_nm_http_endpoints_t *eps, uint32_t max_concurrent_streams, + isc_nmsocket_t **sockp) { + isc_nmsocket_t *sock = NULL; + isc_result_t result; + + REQUIRE(!ISC_LIST_EMPTY(eps->handlers)); + REQUIRE(!ISC_LIST_EMPTY(eps->handler_cbargs)); + REQUIRE(atomic_load(&eps->in_use) == false); + + sock = isc_mem_get(mgr->mctx, sizeof(*sock)); + isc__nmsocket_init(sock, mgr, isc_nm_httplistener, iface); + atomic_init(&sock->h2.max_concurrent_streams, + NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS); + + isc_nmsocket_set_max_streams(sock, max_concurrent_streams); + + atomic_store(&eps->in_use, true); + http_init_listener_endpoints(sock, eps); + + if (ctx != NULL) { + 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_UNSET; + sock->tid = 0; + sock->fd = (uv_os_sock_t)-1; + + isc__nmsocket_barrier_init(sock); + atomic_init(&sock->rchildren, sock->nchildren); + + atomic_store(&sock->listening, true); + *sockp = sock; + return (ISC_R_SUCCESS); +} + +isc_nm_http_endpoints_t * +isc_nm_http_endpoints_new(isc_mem_t *mctx) { + isc_nm_http_endpoints_t *restrict eps; + REQUIRE(mctx != NULL); + + eps = isc_mem_get(mctx, sizeof(*eps)); + *eps = (isc_nm_http_endpoints_t){ .mctx = NULL }; + + isc_mem_attach(mctx, &eps->mctx); + ISC_LIST_INIT(eps->handler_cbargs); + ISC_LIST_INIT(eps->handlers); + isc_refcount_init(&eps->references, 1); + atomic_init(&eps->in_use, false); + eps->magic = HTTP_ENDPOINTS_MAGIC; + + return eps; +} + +void +isc_nm_http_endpoints_detach(isc_nm_http_endpoints_t **restrict epsp) { + isc_nm_http_endpoints_t *restrict eps; + isc_mem_t *mctx; + isc_nm_httphandler_t *handler = NULL; + isc_nm_httpcbarg_t *httpcbarg = NULL; + + REQUIRE(epsp != NULL); + eps = *epsp; + REQUIRE(VALID_HTTP_ENDPOINTS(eps)); + + if (isc_refcount_decrement(&eps->references) > 1) { + *epsp = NULL; + return; + } + + mctx = eps->mctx; + + /* Delete all handlers */ + handler = ISC_LIST_HEAD(eps->handlers); + while (handler != NULL) { + isc_nm_httphandler_t *next = NULL; + + next = ISC_LIST_NEXT(handler, link); + ISC_LIST_DEQUEUE(eps->handlers, handler, link); + isc_mem_free(mctx, handler->path); + isc_mem_put(mctx, handler, sizeof(*handler)); + handler = next; + } + + httpcbarg = ISC_LIST_HEAD(eps->handler_cbargs); + while (httpcbarg != NULL) { + isc_nm_httpcbarg_t *next = NULL; + + next = ISC_LIST_NEXT(httpcbarg, link); + ISC_LIST_DEQUEUE(eps->handler_cbargs, httpcbarg, link); + isc_mem_put(mctx, httpcbarg, sizeof(isc_nm_httpcbarg_t)); + httpcbarg = next; + } + + eps->magic = 0; + + isc_mem_putanddetach(&mctx, eps, sizeof(*eps)); + *epsp = NULL; +} + +void +isc_nm_http_endpoints_attach(isc_nm_http_endpoints_t *source, + isc_nm_http_endpoints_t **targetp) { + REQUIRE(VALID_HTTP_ENDPOINTS(source)); + REQUIRE(targetp != NULL && *targetp == NULL); + + isc_refcount_increment(&source->references); + + *targetp = source; +} + +static isc_nm_httphandler_t * +http_endpoints_find(const char *request_path, + const isc_nm_http_endpoints_t *restrict eps) { + isc_nm_httphandler_t *handler = NULL; + + REQUIRE(VALID_HTTP_ENDPOINTS(eps)); + + if (request_path == NULL || *request_path == '\0') { + return (NULL); + } + + for (handler = ISC_LIST_HEAD(eps->handlers); handler != NULL; + handler = ISC_LIST_NEXT(handler, link)) + { + if (!strcmp(request_path, handler->path)) { + break; + } + } + + return (handler); +} + +/* + * 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_endpoints_add(isc_nm_http_endpoints_t *restrict eps, + const char *uri, const isc_nm_recv_cb_t cb, + void *cbarg, const size_t extrahandlesize) { + isc_mem_t *mctx; + isc_nm_httphandler_t *restrict handler = NULL; + isc_nm_httpcbarg_t *restrict httpcbarg = NULL; + bool newhandler = false; + + REQUIRE(VALID_HTTP_ENDPOINTS(eps)); + REQUIRE(isc_nm_http_path_isvalid(uri)); + REQUIRE(atomic_load(&eps->in_use) == false); + + mctx = eps->mctx; + + httpcbarg = isc_mem_get(mctx, sizeof(isc_nm_httpcbarg_t)); + *httpcbarg = (isc_nm_httpcbarg_t){ .cb = cb, .cbarg = cbarg }; + ISC_LINK_INIT(httpcbarg, link); + + if (http_endpoints_find(uri, eps) == NULL) { + handler = isc_mem_get(mctx, sizeof(*handler)); + *handler = (isc_nm_httphandler_t){ + .cb = http_callback, + .cbarg = httpcbarg, + .extrahandlesize = extrahandlesize, + .path = isc_mem_strdup(mctx, uri) + }; + ISC_LINK_INIT(handler, link); + + newhandler = true; + } + + if (newhandler) { + ISC_LIST_APPEND(eps->handlers, handler, link); + } + ISC_LIST_APPEND(eps->handler_cbargs, httpcbarg, link); + return (ISC_R_SUCCESS); +} + +void +isc__nm_http_stoplistening(isc_nmsocket_t *sock) { + REQUIRE(VALID_NMSOCK(sock)); + REQUIRE(sock->type == isc_nm_httplistener); + + isc__nmsocket_stop(sock); +} + +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); + atomic_store(&sock->active, false); + session = sock->h2.session; + + if (session != NULL && session->sending == 0 && !session->reading) { + /* + * The socket is going to be closed too early without been + * used even once (might happen in a case of low level + * error). + */ + finish_http_session(session); + } else if (session != NULL && session->handle) { + http_do_bio(session, NULL, NULL, NULL); + } +} + +void +isc__nm_http_close(isc_nmsocket_t *sock) { + bool destroy = false; + 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; + } + + if (sock->h2.session != NULL && sock->h2.session->closed && + sock->tid == isc_nm_tid()) + { + isc__nm_httpsession_detach(&sock->h2.session); + destroy = true; + } else if (sock->h2.session == NULL && sock->tid == isc_nm_tid()) { + destroy = true; + } + + if (destroy) { + http_close_direct(sock); + isc__nmsocket_prep_destroy(sock); + 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_region_t data; + 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); + isc_buffer_usedregion(&sock->h2.rbuf, &data); + server_call_cb(sock, session, result, &data); +} + +static void +client_call_failed_read_cb(isc_result_t result, + isc_nm_http_session_t *session) { + http_cstream_t *cstream = NULL; + + REQUIRE(VALID_HTTP2_SESSION(session)); + REQUIRE(result != ISC_R_SUCCESS); + + cstream = ISC_LIST_HEAD(session->cstreams); + while (cstream != NULL) { + http_cstream_t *next = ISC_LIST_NEXT(cstream, link); + + /* + * read_cb could be NULL if cstream was allocated and added + * to the tracking list, but was not properly initialized due + * to a low-level error. It is safe to get rid of the object + * in such a case. + */ + if (cstream->read_cb != NULL) { + isc_region_t read_data; + isc_buffer_usedregion(cstream->rbuf, &read_data); + cstream->read_cb(session->client_httphandle, result, + &read_data, cstream->read_cbarg); + } + + if (result != ISC_R_TIMEDOUT || cstream->read_cb == NULL || + !isc__nmsocket_timer_running(session->handle->sock)) + { + ISC_LIST_DEQUEUE(session->cstreams, cstream, link); + put_http_cstream(session->mctx, cstream); + } + + cstream = next; + } +} + +static void +server_call_failed_read_cb(isc_result_t result, + isc_nm_http_session_t *session) { + isc_nmsocket_h2_t *h2data = NULL; /* stream socket */ + + REQUIRE(VALID_HTTP2_SESSION(session)); + REQUIRE(result != ISC_R_SUCCESS); + + 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; + } +} + +static void +failed_read_cb(isc_result_t result, isc_nm_http_session_t *session) { + if (session->client) { + client_call_failed_read_cb(result, session); + /* + * 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 { + server_call_failed_read_cb(result, session); + /* + * All streams are now destroyed; close the session. + */ + finish_http_session(session); + } +} + +void +isc__nm_http_set_maxage(isc_nmhandle_t *handle, const uint32_t ttl) { + isc_nm_http_session_t *session; + isc_nmsocket_t *sock; + + REQUIRE(VALID_NMHANDLE(handle)); + REQUIRE(VALID_NMSOCK(handle->sock)); + + sock = handle->sock; + session = sock->h2.session; + + INSIST(VALID_HTTP2_SESSION(session)); + INSIST(!session->client); + + sock->h2.min_ttl = ttl; +} + +bool +isc__nm_http_has_encryption(const isc_nmhandle_t *handle) { + isc_nm_http_session_t *session; + isc_nmsocket_t *sock; + + REQUIRE(VALID_NMHANDLE(handle)); + REQUIRE(VALID_NMSOCK(handle->sock)); + + sock = handle->sock; + session = sock->h2.session; + + INSIST(VALID_HTTP2_SESSION(session)); + + return (isc_nm_socket_type(session->handle) == isc_nm_tlssocket); +} + +const char * +isc__nm_http_verify_tls_peer_result_string(const isc_nmhandle_t *handle) { + isc_nmsocket_t *sock = NULL; + isc_nm_http_session_t *session; + + REQUIRE(VALID_NMHANDLE(handle)); + REQUIRE(VALID_NMSOCK(handle->sock)); + REQUIRE(handle->sock->type == isc_nm_httpsocket); + + sock = handle->sock; + session = sock->h2.session; + + /* + * In the case of a low-level error the session->handle is not + * attached nor session object is created. + */ + if (session == NULL && sock->h2.connect.tls_peer_verify_string != NULL) + { + return (sock->h2.connect.tls_peer_verify_string); + } + + if (session == NULL) { + return (NULL); + } + + INSIST(VALID_HTTP2_SESSION(session)); + + return (isc_nm_verify_tls_peer_result_string(session->handle)); +} + +void +isc__nm_http_set_tlsctx(isc_nmsocket_t *listener, isc_tlsctx_t *tlsctx) { + REQUIRE(VALID_NMSOCK(listener)); + REQUIRE(listener->type == isc_nm_httplistener); + + isc_nmsocket_set_tlsctx(listener->outer, tlsctx); +} + +void +isc__nm_http_set_max_streams(isc_nmsocket_t *listener, + const uint32_t max_concurrent_streams) { + uint32_t max_streams = NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS; + + REQUIRE(VALID_NMSOCK(listener)); + REQUIRE(listener->type == isc_nm_httplistener); + + if (max_concurrent_streams > 0 && + max_concurrent_streams < NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS) + { + max_streams = max_concurrent_streams; + } + + atomic_store(&listener->h2.max_concurrent_streams, max_streams); +} + +void +isc_nm_http_set_endpoints(isc_nmsocket_t *listener, + isc_nm_http_endpoints_t *eps) { + size_t nworkers; + + REQUIRE(VALID_NMSOCK(listener)); + REQUIRE(listener->type == isc_nm_httplistener); + REQUIRE(VALID_HTTP_ENDPOINTS(eps)); + + atomic_store(&eps->in_use, true); + + nworkers = (size_t)listener->mgr->nworkers; + for (size_t i = 0; i < nworkers; i++) { + isc__netievent__http_eps_t *ievent = + isc__nm_get_netievent_httpendpoints(listener->mgr, + listener, eps); + isc__nm_enqueue_ievent(&listener->mgr->workers[i], + (isc__netievent_t *)ievent); + } +} + +void +isc__nm_async_httpendpoints(isc__networker_t *worker, isc__netievent_t *ev0) { + isc__netievent__http_eps_t *ievent = (isc__netievent__http_eps_t *)ev0; + const int tid = isc_nm_tid(); + isc_nmsocket_t *listener = ievent->sock; + isc_nm_http_endpoints_t *eps = ievent->endpoints; + UNUSED(worker); + + isc_nm_http_endpoints_detach(&listener->h2.listener_endpoints[tid]); + isc_nm_http_endpoints_attach(eps, + &listener->h2.listener_endpoints[tid]); +} + +static void +http_init_listener_endpoints(isc_nmsocket_t *listener, + isc_nm_http_endpoints_t *epset) { + size_t nworkers; + + REQUIRE(VALID_NMSOCK(listener)); + REQUIRE(VALID_NM(listener->mgr)); + REQUIRE(VALID_HTTP_ENDPOINTS(epset)); + + nworkers = (size_t)listener->mgr->nworkers; + INSIST(nworkers > 0); + + listener->h2.listener_endpoints = + isc_mem_get(listener->mgr->mctx, + sizeof(isc_nm_http_endpoints_t *) * nworkers); + listener->h2.n_listener_endpoints = nworkers; + for (size_t i = 0; i < nworkers; i++) { + listener->h2.listener_endpoints[i] = NULL; + isc_nm_http_endpoints_attach( + epset, &listener->h2.listener_endpoints[i]); + } +} + +static void +http_cleanup_listener_endpoints(isc_nmsocket_t *listener) { + REQUIRE(VALID_NM(listener->mgr)); + + if (listener->h2.listener_endpoints == NULL) { + return; + } + + for (size_t i = 0; i < listener->h2.n_listener_endpoints; i++) { + isc_nm_http_endpoints_detach( + &listener->h2.listener_endpoints[i]); + } + isc_mem_put(listener->mgr->mctx, listener->h2.listener_endpoints, + sizeof(isc_nm_http_endpoints_t *) * + listener->h2.n_listener_endpoints); + listener->h2.n_listener_endpoints = 0; +} + +static isc_nm_http_endpoints_t * +http_get_listener_endpoints(isc_nmsocket_t *listener, const int tid) { + isc_nm_http_endpoints_t *eps; + REQUIRE(VALID_NMSOCK(listener)); + REQUIRE(tid >= 0); + REQUIRE((size_t)tid < listener->h2.n_listener_endpoints); + + eps = listener->h2.listener_endpoints[tid]; + INSIST(eps != NULL); + return (eps); +} + +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, + }; +} + +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 && + sock->h2.listener_endpoints != NULL) + { + /* Delete all handlers */ + http_cleanup_listener_endpoints(sock); + } + + 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; + } + + INSIST(sock->h2.connect.cstream == NULL); + + if (isc_buffer_base(&sock->h2.rbuf) != NULL) { + void *base = isc_buffer_base(&sock->h2.rbuf); + isc_mem_free(sock->mgr->mctx, base); + isc_buffer_initnull(&sock->h2.rbuf); + } + } + + 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 != NULL) { + 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 != NULL) { + INSIST(VALID_HTTP2_SESSION(sock->h2.session)); + INSIST(VALID_NMHANDLE(sock->h2.session->handle)); + isc_nmhandle_settimeout(sock->h2.session->handle, timeout); + } +} + +void +isc__nmhandle_http_keepalive(isc_nmhandle_t *handle, bool value) { + 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_keepalive(sock->h2.session->handle, value); + } +} + +void +isc_nm_http_makeuri(const bool https, const isc_sockaddr_t *sa, + const char *hostname, const uint16_t http_port, + const char *abs_path, char *outbuf, + const size_t outbuf_len) { + char saddr[INET6_ADDRSTRLEN] = { 0 }; + int family; + bool ipv6_addr = false; + struct sockaddr_in6 sa6; + uint16_t host_port = http_port; + const char *host = NULL; + + REQUIRE(outbuf != NULL); + REQUIRE(outbuf_len != 0); + REQUIRE(isc_nm_http_path_isvalid(abs_path)); + + /* If hostname is specified, use that. */ + if (hostname != NULL && hostname[0] != '\0') { + /* + * The host name could be an IPv6 address. If so, + * wrap it between [ and ]. + */ + if (inet_pton(AF_INET6, hostname, &sa6) == 1 && + hostname[0] != '[') + { + ipv6_addr = true; + } + host = hostname; + } else { + /* + * A hostname was not specified; build one from + * the given IP address. + */ + INSIST(sa != NULL); + family = ((const struct sockaddr *)&sa->type.sa)->sa_family; + host_port = ntohs(family == AF_INET ? sa->type.sin.sin_port + : sa->type.sin6.sin6_port); + ipv6_addr = family == AF_INET6; + (void)inet_ntop( + family, + family == AF_INET + ? (const struct sockaddr *)&sa->type.sin.sin_addr + : (const struct sockaddr *)&sa->type.sin6 + .sin6_addr, + saddr, sizeof(saddr)); + host = saddr; + } + + /* + * If the port number was not specified, the default + * depends on whether we're using encryption or not. + */ + if (host_port == 0) { + host_port = https ? 443 : 80; + } + + (void)snprintf(outbuf, outbuf_len, "%s://%s%s%s:%u%s", + https ? "https" : "http", ipv6_addr ? "[" : "", host, + ipv6_addr ? "]" : "", host_port, abs_path); +} + +/* + * 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_DIGIT() isdigit((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); +} + +/* + * DoH URL Location Verifier. Based on the following grammar (EBNF/WSN + * notation): + * + * S = path_absolute. + * path_absolute = '/' [ segments ] '\0'. + * segments = segment_nz { slash_segment }. + * slash_segment = '/' segment. + * segment = { pchar }. + * segment_nz = pchar { pchar }. + * pchar = unreserved | pct_encoded | sub_delims | ':' | '@'. + * unreserved = ALPHA | DIGIT | '-' | '.' | '_' | '~'. + * pct_encoded = '%' XDIGIT XDIGIT. + * sub_delims = '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | + * ',' | ';' | '='. + * + * The grammar is extracted from RFC 3986. It is slightly modified to + * aid in parser creation, but the end result is the same + * (path_absolute is defined slightly differently - split into + * multiple productions). + * + * https://datatracker.ietf.org/doc/html/rfc3986#appendix-A + */ + +typedef struct isc_http_location_parser_state { + const char *str; +} isc_http_location_parser_state_t; + +static bool +rule_loc_path_absolute(isc_http_location_parser_state_t *); + +static bool +rule_loc_segments(isc_http_location_parser_state_t *); + +static bool +rule_loc_slash_segment(isc_http_location_parser_state_t *); + +static bool +rule_loc_segment(isc_http_location_parser_state_t *); + +static bool +rule_loc_segment_nz(isc_http_location_parser_state_t *); + +static bool +rule_loc_pchar(isc_http_location_parser_state_t *); + +static bool +rule_loc_unreserved(isc_http_location_parser_state_t *); + +static bool +rule_loc_pct_encoded(isc_http_location_parser_state_t *); + +static bool +rule_loc_sub_delims(isc_http_location_parser_state_t *); + +static bool +rule_loc_path_absolute(isc_http_location_parser_state_t *st) { + if (MATCH('/')) { + ADVANCE(); + } else { + return (false); + } + + (void)rule_loc_segments(st); + + if (MATCH('\0')) { + ADVANCE(); + } else { + return (false); + } + + return (true); +} + +static bool +rule_loc_segments(isc_http_location_parser_state_t *st) { + if (!rule_loc_segment_nz(st)) { + return (false); + } + + while (rule_loc_slash_segment(st)) { + /* zero or more */; + } + + return (true); +} + +static bool +rule_loc_slash_segment(isc_http_location_parser_state_t *st) { + if (MATCH('/')) { + ADVANCE(); + } else { + return (false); + } + + return (rule_loc_segment(st)); +} + +static bool +rule_loc_segment(isc_http_location_parser_state_t *st) { + while (rule_loc_pchar(st)) { + /* zero or more */; + } + + return (true); +} + +static bool +rule_loc_segment_nz(isc_http_location_parser_state_t *st) { + if (!rule_loc_pchar(st)) { + return (false); + } + + while (rule_loc_pchar(st)) { + /* zero or more */; + } + + return (true); +} + +static bool +rule_loc_pchar(isc_http_location_parser_state_t *st) { + if (rule_loc_unreserved(st)) { + return (true); + } else if (rule_loc_pct_encoded(st)) { + return (true); + } else if (rule_loc_sub_delims(st)) { + return (true); + } else if (MATCH(':') || MATCH('@')) { + ADVANCE(); + return (true); + } + + return (false); +} + +static bool +rule_loc_unreserved(isc_http_location_parser_state_t *st) { + if (MATCH_ALPHA() | MATCH_DIGIT() | MATCH('-') | MATCH('.') | + MATCH('_') | MATCH('~')) + { + ADVANCE(); + return (true); + } + return (false); +} + +static bool +rule_loc_pct_encoded(isc_http_location_parser_state_t *st) { + if (!MATCH('%')) { + return (false); + } + ADVANCE(); + + if (!MATCH_XDIGIT()) { + return (false); + } + ADVANCE(); + + if (!MATCH_XDIGIT()) { + return (false); + } + ADVANCE(); + + return (true); +} + +static bool +rule_loc_sub_delims(isc_http_location_parser_state_t *st) { + if (MATCH('!') | MATCH('$') | MATCH('&') | MATCH('\'') | MATCH('(') | + MATCH(')') | MATCH('*') | MATCH('+') | MATCH(',') | MATCH(';') | + MATCH('=')) + { + ADVANCE(); + return (true); + } + + return (false); +} + +bool +isc_nm_http_path_isvalid(const char *path) { + isc_http_location_parser_state_t state = { 0 }; + + REQUIRE(path != NULL); + + state.str = path; + + return (rule_loc_path_absolute(&state)); +} |