diff options
Diffstat (limited to '')
-rw-r--r-- | daemon/http.c | 953 |
1 files changed, 953 insertions, 0 deletions
diff --git a/daemon/http.c b/daemon/http.c new file mode 100644 index 0000000..0c6f361 --- /dev/null +++ b/daemon/http.c @@ -0,0 +1,953 @@ +/* + * Copyright (C) CZ.NIC, z.s.p.o + * + * Initial Author: Jan Hák <jan.hak@nic.cz> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <errno.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "daemon/io.h" +#include "daemon/http.h" +#include "daemon/worker.h" +#include "daemon/session.h" +#include "lib/layer/iterate.h" /* kr_response_classify */ +#include "lib/cache/util.h" +#include "lib/generic/array.h" + +#include "contrib/cleanup.h" +#include "contrib/base64url.h" + +/** Makes a `nghttp2_nv`. `K` is the key, `KS` is the key length, + * `V` is the value, `VS` is the value length. */ +#define MAKE_NV(K, KS, V, VS) \ + (nghttp2_nv) { (uint8_t *)(K), (uint8_t *)(V), (KS), (VS), NGHTTP2_NV_FLAG_NONE } + +/** Makes a `nghttp2_nv` with static data. `K` is the key, + * `V` is the value. Both `K` and `V` MUST be string literals. */ +#define MAKE_STATIC_NV(K, V) \ + MAKE_NV((K), sizeof(K) - 1, (V), sizeof(V) - 1) + +/** Makes a `nghttp2_nv` with a static key. `K` is the key, + * `V` is the value, `VS` is the value length. `K` MUST be a string literal. */ +#define MAKE_STATIC_KEY_NV(K, V, VS) \ + MAKE_NV((K), sizeof(K) - 1, (V), (VS)) + +/* Use same maximum as for tcp_pipeline_max. */ +#define HTTP_MAX_CONCURRENT_STREAMS UINT16_MAX + +#define HTTP_MAX_HEADER_IN_SIZE 1024 + +#define HTTP_FRAME_HDLEN 9 +#define HTTP_FRAME_PADLEN 1 + +#define MAX_DECIMAL_LENGTH(VT) ((CHAR_BIT * sizeof(VT) / 3) + 3) + +struct http_data { + uint8_t *buf; + size_t len; + size_t pos; + uint32_t ttl; + uv_write_cb on_write; + uv_write_t *req; +}; + +typedef array_t(nghttp2_nv) nghttp2_array_t; + +static int http_send_response(struct http_ctx *ctx, int32_t stream_id, + nghttp2_data_provider *prov, enum http_status status); +static int http_send_response_rst_stream(struct http_ctx *ctx, int32_t stream_id, + nghttp2_data_provider *prov, enum http_status status); + +/** Checks if `status` has the correct `category`. + * E.g. status 200 has category 2, status 404 has category 4, 501 has category 5 etc. */ +static inline bool http_status_has_category(enum http_status status, int category) +{ + return status / 100 == category; +} + +/* + * Write HTTP/2 protocol data to underlying transport layer. + */ +static ssize_t send_callback(nghttp2_session *h2, const uint8_t *data, size_t length, + int flags, void *user_data) +{ + struct http_ctx *ctx = (struct http_ctx *)user_data; + return ctx->send_cb(data, length, ctx->session); +} + +/* + * Sets the HTTP status of the specified `context`, but only if its status has + * not already been changed to an unsuccessful one. + */ +static inline void set_status(struct http_ctx *ctx, enum http_status status) +{ + if (http_status_has_category(ctx->status, 2)) + ctx->status = status; +} + +/* + * Send padding length (if greater than zero). + */ +static int send_padlen(struct http_ctx *ctx, size_t padlen) +{ + int ret; + uint8_t buf; + + if (padlen == 0) + return 0; + + buf = (uint8_t)padlen; + ret = ctx->send_cb(&buf, HTTP_FRAME_PADLEN, ctx->session); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + return 0; +} + +/* + * Send HTTP/2 zero-byte padding. + * + * This sends only padlen-1 bytes of padding (if any), since padlen itself + * (already sent) is also considered padding. Refer to RFC7540, section 6.1 + */ +static int send_padding(struct http_ctx *ctx, uint8_t padlen) +{ + static const uint8_t buf[UINT8_MAX]; + int ret; + + if (padlen <= 1) + return 0; + + ret = ctx->send_cb(buf, padlen - 1, ctx->session); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + return 0; +} + +/* + * Write entire DATA frame to underlying transport layer. + * + * This function reads directly from data provider to avoid copying packet wire buffer. + */ +static int send_data_callback(nghttp2_session *h2, nghttp2_frame *frame, const uint8_t *framehd, + size_t length, nghttp2_data_source *source, void *user_data) +{ + struct http_data *data; + int ret; + struct http_ctx *ctx; + + ctx = (struct http_ctx *)user_data; + data = (struct http_data*)source->ptr; + + ret = ctx->send_cb(framehd, HTTP_FRAME_HDLEN, ctx->session); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + ret = send_padlen(ctx, frame->data.padlen); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + ret = ctx->send_cb(data->buf + data->pos, length, ctx->session); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + data->pos += length; + if (kr_fails_assert(data->pos <= data->len)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + ret = send_padding(ctx, (uint8_t)frame->data.padlen); + if (ret < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + return 0; +} + +/* + * Check endpoint and uri path + */ +static int check_uri(const char* path) +{ + static const char *endpoints[] = {"dns-query", "doh"}; + ssize_t endpoint_len; + ssize_t ret; + + if (!path) + return kr_error(EINVAL); + + char *query_mark = strstr(path, "?"); + + /* calculating of endpoint_len - for POST or GET method */ + endpoint_len = (query_mark) ? query_mark - path - 1 : strlen(path) - 1; + + /* check endpoint */ + ret = -1; + for(int i = 0; i < sizeof(endpoints)/sizeof(*endpoints); i++) + { + if (strlen(endpoints[i]) != endpoint_len) + continue; + ret = strncmp(path + 1, endpoints[i], strlen(endpoints[i])); + if (!ret) + break; + } + + return (ret) ? kr_error(ENOENT) : kr_ok(); +} + +static kr_http_header_array_t *headers_dup(kr_http_header_array_t *src) +{ + kr_http_header_array_t *dst = malloc(sizeof(kr_http_header_array_t)); + kr_require(dst); + array_init(*dst); + for (size_t i = 0; i < src->len; i++) { + struct kr_http_header_array_entry *src_entry = &src->at[i]; + struct kr_http_header_array_entry dst_entry = { + .name = strdup(src_entry->name), + .value = strdup(src_entry->value) + }; + array_push(*dst, dst_entry); + } + + return dst; +} + +/* + * Process a query from URI path if there's base64url encoded dns variable. + */ +static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stream_id) +{ + if (!ctx || !path) + return kr_error(EINVAL); + + static const char key[] = "dns="; + static const char *delim = "&"; + char *beg, *end; + uint8_t *dest; + uint32_t remaining; + + char *query_mark = strstr(path, "?"); + if (!query_mark || strlen(query_mark) == 0) /* no parameters in path */ + return kr_error(EINVAL); + + /* go over key:value pair */ + for (beg = strtok(query_mark + 1, delim); beg != NULL; beg = strtok(NULL, delim)) { + if (!strncmp(beg, key, 4)) /* dns variable in path found */ + break; + } + + if (!beg) /* no dns variable in path */ + return kr_error(EINVAL); + + beg += sizeof(key) - 1; + end = strchr(beg, '&'); + if (end == NULL) + end = beg + strlen(beg); + + ctx->buf_pos = sizeof(uint16_t); /* Reserve 2B for dnsmsg len. */ + remaining = ctx->buf_size - ctx->submitted - ctx->buf_pos; + dest = ctx->buf + ctx->buf_pos; + + /* Decode dns message from the parameter */ + int ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining); + if (ret < 0) { + ctx->buf_pos = 0; + kr_log_debug(DOH, "[%p] base64url decode failed %s\n", (void *)ctx->h2, kr_strerror(ret)); + return ret; + } + + ctx->buf_pos += ret; + + struct http_stream stream = { + .id = stream_id, + .headers = headers_dup(ctx->headers) + }; + queue_push(ctx->streams, stream); + + return kr_ok(); +} + +static void refuse_stream(nghttp2_session *h2, int32_t stream_id) +{ + nghttp2_submit_rst_stream( + h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_REFUSED_STREAM); +} + +void http_free_headers(kr_http_header_array_t *headers) +{ + if (headers == NULL) + return; + + for (int i = 0; i < headers->len; i++) { + free(headers->at[i].name); + free(headers->at[i].value); + } + array_clear(*headers); + free(headers); +} +/* Return the http ctx into a pristine state in which no stream is being processed. */ +static void http_cleanup_stream(struct http_ctx *ctx) +{ + ctx->incomplete_stream = -1; + ctx->current_method = HTTP_METHOD_NONE; + free(ctx->uri_path); + ctx->uri_path = NULL; + http_free_headers(ctx->headers); + ctx->headers = NULL; +} + +/* + * Save stream id from first header's frame. + * + * We don't support interweaving from different streams. To successfully parse + * multiple subsequent streams, each one must be fully received before processing + * a new stream. + */ +static int begin_headers_callback(nghttp2_session *h2, const nghttp2_frame *frame, + void *user_data) +{ + struct http_ctx *ctx = (struct http_ctx *)user_data; + int32_t stream_id = frame->hd.stream_id; + + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + if (ctx->incomplete_stream != -1) { + kr_log_debug(DOH, "[%p] stream %d incomplete, refusing (begin_headers_callback)\n", + (void *)h2, ctx->incomplete_stream); + refuse_stream(h2, stream_id); + } else { + http_cleanup_stream(ctx); // Free any leftover data and ensure pristine state + ctx->incomplete_stream = stream_id; + ctx->last_stream = stream_id; + ctx->headers = malloc(sizeof(kr_http_header_array_t)); + array_init(*ctx->headers); + } + return 0; +} + +/* + * Process a received header name-value pair. + * + * In DoH, GET requests contain the base64url-encoded query in dns variable present in path. + * This variable is parsed from :path pseudoheader. + */ +static int header_callback(nghttp2_session *h2, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags, void *user_data) +{ + struct http_ctx *ctx = (struct http_ctx *)user_data; + int32_t stream_id = frame->hd.stream_id; + + if (frame->hd.type != NGHTTP2_HEADERS) + return 0; + + if (ctx->incomplete_stream != stream_id) { + kr_log_debug(DOH, "[%p] stream %d incomplete, refusing (header_callback)\n", + (void *)h2, ctx->incomplete_stream); + refuse_stream(h2, stream_id); + return 0; + } + + /* Store chosen headers to pass them to kr_request. */ + for (int i = 0; i < the_worker->doh_qry_headers.len; i++) { + if (!strcasecmp(the_worker->doh_qry_headers.at[i], (const char *)name)) { + kr_http_header_array_entry_t header; + + /* Limit maximum value size to reduce attack surface. */ + if (valuelen > HTTP_MAX_HEADER_IN_SIZE) { + kr_log_debug(DOH, + "[%p] stream %d: header too large (%zu B), refused\n", + (void *)h2, stream_id, valuelen); + set_status(ctx, HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE); + return 0; + } + + /* Copy the user-provided header name to keep the original case. */ + header.name = malloc(sizeof(*header.name) * (namelen + 1)); + memcpy(header.name, the_worker->doh_qry_headers.at[i], namelen); + header.name[namelen] = '\0'; + + header.value = malloc(sizeof(*header.value) * (valuelen + 1)); + memcpy(header.value, value, valuelen); + header.value[valuelen] = '\0'; + + array_push(*ctx->headers, header); + break; + } + } + + if (!strcasecmp(":path", (const char *)name)) { + int uri_result = check_uri((const char *)value); + if (uri_result == kr_error(ENOENT)) { + set_status(ctx, HTTP_STATUS_NOT_FOUND); + return 0; + } else if (uri_result < 0) { + set_status(ctx, HTTP_STATUS_BAD_REQUEST); + return 0; + } + + kr_assert(ctx->uri_path == NULL); + ctx->uri_path = malloc(sizeof(*ctx->uri_path) * (valuelen + 1)); + if (!ctx->uri_path) + return kr_error(ENOMEM); + memcpy(ctx->uri_path, value, valuelen); + ctx->uri_path[valuelen] = '\0'; + } + + if (!strcasecmp(":method", (const char *)name)) { + if (!strcasecmp("get", (const char *)value)) { + ctx->current_method = HTTP_METHOD_GET; + } else if (!strcasecmp("post", (const char *)value)) { + ctx->current_method = HTTP_METHOD_POST; + } else if (!strcasecmp("head", (const char *)value)) { + ctx->current_method = HTTP_METHOD_HEAD; + } else { + ctx->current_method = HTTP_METHOD_NONE; + set_status(ctx, HTTP_STATUS_NOT_IMPLEMENTED); + return 0; + } + } + + if (!strcasecmp("content-type", (const char *)name)) { + if (strcasecmp("application/dns-message", (const char *)value)) { + set_status(ctx, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE); + return 0; + } + } + + return 0; +} + +/* + * Process DATA chunk sent by the client (by POST method). + * + * We use a single DNS message buffer for the entire connection. Therefore, we + * don't support interweaving DATA chunks from different streams. To successfully + * parse multiple subsequent streams, each one must be fully received before + * processing a new stream. See https://gitlab.nic.cz/knot/knot-resolver/-/issues/619 + */ +static int data_chunk_recv_callback(nghttp2_session *h2, uint8_t flags, int32_t stream_id, + const uint8_t *data, size_t len, void *user_data) +{ + struct http_ctx *ctx = (struct http_ctx *)user_data; + ssize_t remaining; + ssize_t required; + bool is_first = queue_len(ctx->streams) == 0 || queue_tail(ctx->streams).id != ctx->incomplete_stream; + + if (ctx->incomplete_stream != stream_id) { + kr_log_debug(DOH, "[%p] stream %d incomplete, refusing (data_chunk_recv_callback)\n", + (void *)h2, ctx->incomplete_stream); + refuse_stream(h2, stream_id); + ctx->incomplete_stream = -1; + return 0; + } + + remaining = ctx->buf_size - ctx->submitted - ctx->buf_pos; + required = len; + /* First data chunk of the new stream */ + if (is_first) + required += sizeof(uint16_t); + + if (required > remaining) { + kr_log_error(DOH, "[%p] insufficient space in buffer\n", (void *)h2); + ctx->incomplete_stream = -1; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + if (is_first) { + /* FIXME: reserving the 2B length should be done elsewhere, + * ideally for both POST and GET at the same time. The right + * place would probably be after receiving HEADERS frame in + * on_frame_recv() + * + * queue_push() should be moved: see FIXME in + * submit_to_wirebuffer() */ + ctx->buf_pos = sizeof(uint16_t); /* Reserve 2B for dnsmsg len. */ + struct http_stream stream = { + .id = stream_id, + .headers = headers_dup(ctx->headers) + }; + queue_push(ctx->streams, stream); + } + + memmove(ctx->buf + ctx->buf_pos, data, len); + ctx->buf_pos += len; + return 0; +} + +static int submit_to_wirebuffer(struct http_ctx *ctx) +{ + int ret = -1; + ssize_t len; + + /* Free http_ctx's headers - by now the stream has obtained its own + * copy of the headers which it can operate on. */ + /* FIXME: technically, transferring memory ownership should happen + * along with queue_push(ctx->streams) to avoid confusion of who owns + * what and when. Pushing to queue should be done AFTER we successfully + * finish this function. On error, we'd clean up and not push anything. + * However, queue's content is now also used to detect first DATA frame + * in stream, so it needs to be refactored first. + * + * For now, we assume memory is transferred even on error and the + * headers themselves get cleaned up during http_free() which is + * triggered after the error when session is closed. + * + * EDIT(2022-05-19): The original logic was causing occasional + * double-free conditions once status code support was extended. + * + * Currently, we are copying the headers from ctx instead of transferring + * ownership, which is still a dirty workaround and, ideally, the whole + * logic around header (de)allocation should be reworked to make + * the ownership situation clear. */ + http_free_headers(ctx->headers); + ctx->headers = NULL; + + len = ctx->buf_pos - sizeof(uint16_t); + if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) { + kr_log_debug(DOH, "[%p] invalid dnsmsg size: %zd B\n", (void *)ctx->h2, len); + set_status(ctx, (len <= 0) + ? HTTP_STATUS_BAD_REQUEST + : HTTP_STATUS_PAYLOAD_TOO_LARGE); + ret = 0; + goto cleanup; + } + + /* Submit data to wirebuffer. */ + knot_wire_write_u16(ctx->buf, len); + ctx->submitted += ctx->buf_pos; + ctx->buf += ctx->buf_pos; + ctx->buf_pos = 0; + ret = 0; +cleanup: + http_cleanup_stream(ctx); + return ret; +} + +/* + * Finalize existing buffer upon receiving the last frame in the stream. + * + * For GET, this would be HEADERS frame. + * For POST, it is a DATA frame. + * + * Unrelated frames (such as SETTINGS) are ignored (no data was buffered). + */ +static int on_frame_recv_callback(nghttp2_session *h2, const nghttp2_frame *frame, void *user_data) +{ + struct http_ctx *ctx = (struct http_ctx *)user_data; + int32_t stream_id = frame->hd.stream_id; + if(kr_fails_assert(stream_id != -1)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + + if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) && ctx->incomplete_stream == stream_id) { + ctx->streaming = false; + + if (ctx->current_method == HTTP_METHOD_GET || ctx->current_method == HTTP_METHOD_HEAD) { + if (process_uri_path(ctx, ctx->uri_path, stream_id) < 0) { + /* End processing - don't submit to wirebuffer. */ + set_status(ctx, HTTP_STATUS_BAD_REQUEST); + return 0; + } + } + + if (submit_to_wirebuffer(ctx) < 0) + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +/* + * Call on_write() callback for written (or failed) packet data. + */ +static void on_pkt_write(struct http_data *data, int status) +{ + if (!data || !data->req || !data->on_write) + return; + + data->on_write(data->req, status); + free(data); +} + +static int stream_write_data_free_err(trie_val_t *val, void *null) +{ + on_pkt_write(*val, kr_error(EIO)); + return 0; +} + +/* + * Cleanup for closed streams. + */ +static int on_stream_close_callback(nghttp2_session *h2, int32_t stream_id, + uint32_t error_code, void *user_data) +{ + struct http_data *data; + struct http_ctx *ctx = (struct http_ctx *)user_data; + int ret; + + /* Ensure connection state is cleaned up in case the stream gets + * unexpectedly closed, e.g. by PROTOCOL_ERROR issued from nghttp2. */ + if (ctx->incomplete_stream == stream_id) + http_cleanup_stream(ctx); + + ret = trie_del(ctx->stream_write_data, (char *)&stream_id, sizeof(stream_id), (trie_val_t*)&data); + if (ret == KNOT_EOK && data) + on_pkt_write(data, error_code == 0 ? 0 : kr_error(EIO)); + + return 0; +} + +/* + * Setup and initialize connection with new HTTP/2 context. + */ +struct http_ctx* http_new(struct session *session, http_send_callback send_cb) +{ + if (!session || !send_cb) + return NULL; + + nghttp2_session_callbacks *callbacks; + struct http_ctx *ctx = NULL; + static const nghttp2_settings_entry iv[] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, HTTP_MAX_CONCURRENT_STREAMS } + }; + + if (nghttp2_session_callbacks_new(&callbacks) < 0) + return ctx; + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + nghttp2_session_callbacks_set_send_data_callback(callbacks, send_data_callback); + nghttp2_session_callbacks_set_on_header_callback(callbacks, header_callback); + nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, begin_headers_callback); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, data_chunk_recv_callback); + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, on_frame_recv_callback); + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + ctx = calloc(1UL, sizeof(struct http_ctx)); + if (!ctx) + goto finish; + + ctx->send_cb = send_cb; + ctx->session = session; + queue_init(ctx->streams); + ctx->stream_write_data = trie_create(NULL); + ctx->incomplete_stream = -1; + ctx->last_stream = -1; + ctx->submitted = 0; + ctx->streaming = true; + ctx->current_method = HTTP_METHOD_NONE; + ctx->uri_path = NULL; + ctx->status = HTTP_STATUS_OK; + + nghttp2_session_server_new(&ctx->h2, callbacks, ctx); + nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, + iv, sizeof(iv)/sizeof(*iv)); + + struct sockaddr *peer = session_get_peer(session); + kr_log_debug(DOH, "[%p] h2 session created for %s\n", (void *)ctx->h2, kr_straddr(peer)); +finish: + nghttp2_session_callbacks_del(callbacks); + return ctx; +} + +/* + * Process inbound HTTP/2 data and return number of bytes read into session wire buffer. + * + * This function may trigger outgoing HTTP/2 data, such as stream resets, window updates etc. + * + * Returns 1 if stream has not ended yet, 0 if the stream has ended, or + * a negative value on error. + */ +int http_process_input_data(struct session *session, const uint8_t *buf, ssize_t nread, + ssize_t *out_submitted) +{ + struct http_ctx *ctx = session_http_get_server_ctx(session); + ssize_t ret = 0; + + if (!ctx->h2) + return kr_error(ENOSYS); + if (kr_fails_assert(ctx->session == session)) + return kr_error(EINVAL); + + /* FIXME It is possible for the TLS/HTTP processing to be cut off at + * any point, waiting for more data. If we're using POST which is split + * into multiple DATA frames and such a stream is in the middle of + * processing, resetting buf_pos will corrupt its contents (and the + * query will be ignored). This may also be problematic in other + * cases. */ + ctx->submitted = 0; + ctx->streaming = true; + ctx->buf = session_wirebuf_get_free_start(session); + ctx->buf_pos = 0; + ctx->buf_size = session_wirebuf_get_free_size(session); + + ret = nghttp2_session_mem_recv(ctx->h2, buf, nread); + if (ret < 0) { + kr_log_debug(DOH, "[%p] nghttp2_session_mem_recv failed: %s (%zd)\n", + (void *)ctx->h2, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + ret = nghttp2_session_send(ctx->h2); + if (ret < 0) { + kr_log_debug(DOH, "[%p] nghttp2_session_send failed: %s (%zd)\n", + (void *)ctx->h2, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + if (!http_status_has_category(ctx->status, 2)) { + *out_submitted = 0; + http_send_status(session, ctx->status); + http_cleanup_stream(ctx); + return 0; + } + + *out_submitted = ctx->submitted; + return ctx->streaming; +} + +int http_send_status(struct session *session, enum http_status status) +{ + struct http_ctx *ctx = session_http_get_server_ctx(session); + if (ctx->last_stream >= 0) + return http_send_response_rst_stream( + ctx, ctx->last_stream, NULL, status); + + return 0; +} + +/* + * Provide data from buffer to HTTP/2 library. + * + * To avoid copying the packet wire buffer, we use NGHTTP2_DATA_FLAG_NO_COPY + * and take care of sending entire DATA frames ourselves with nghttp2_send_data_callback. + * + * See https://www.nghttp2.org/documentation/types.html#c.nghttp2_data_source_read_callback + */ +static ssize_t read_callback(nghttp2_session *h2, int32_t stream_id, uint8_t *buf, + size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) +{ + struct http_data *data; + size_t avail; + size_t send; + + data = (struct http_data*)source->ptr; + avail = data->len - data->pos; + send = MIN(avail, length); + + if (avail == send) + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + return send; +} + +/** Convenience function for pushing `nghttp2_nv` made with MAKE_*_NV into + * arrays. */ +static inline void push_nv(nghttp2_array_t *arr, nghttp2_nv nv) +{ + array_push(*arr, nv); +} + +/* + * Send dns response provided by the HTTP/2 data provider. + * + * Data isn't guaranteed to be sent immediately due to underlying HTTP/2 flow control. + */ +static int http_send_response(struct http_ctx *ctx, int32_t stream_id, + nghttp2_data_provider *prov, enum http_status status) +{ + nghttp2_session *h2 = ctx->h2; + int ret; + + nghttp2_array_t hdrs; + array_init(hdrs); + array_reserve(hdrs, 5); + + auto_free char *status_str = NULL; + if (likely(status == HTTP_STATUS_OK)) { + push_nv(&hdrs, MAKE_STATIC_NV(":status", "200")); + } else { + int status_len = asprintf(&status_str, "%d", (int)status); + kr_require(status_len >= 0); + push_nv(&hdrs, MAKE_STATIC_KEY_NV(":status", status_str, status_len)); + } + push_nv(&hdrs, MAKE_STATIC_NV("access-control-allow-origin", "*")); + + struct http_data *data = NULL; + auto_free char *size = NULL; + auto_free char *max_age = NULL; + + if (ctx->current_method == HTTP_METHOD_HEAD && prov) { + /* HEAD method is the same as GET but only returns headers, + * so let's clean up the data here as we don't need it. */ + free(prov->source.ptr); + prov = NULL; + } + + if (prov) { + data = (struct http_data*)prov->source.ptr; + const char *directive_max_age = "max-age="; + int max_age_len; + int size_len; + + size_len = asprintf(&size, "%zu", data->len); + kr_require(size_len >= 0); + max_age_len = asprintf(&max_age, "%s%" PRIu32, directive_max_age, data->ttl); + kr_require(max_age_len >= 0); + + push_nv(&hdrs, MAKE_STATIC_NV("content-type", "application/dns-message")); + push_nv(&hdrs, MAKE_STATIC_KEY_NV("content-length", size, size_len)); + push_nv(&hdrs, MAKE_STATIC_KEY_NV("cache-control", max_age, max_age_len)); + } + + ret = nghttp2_submit_response(h2, stream_id, hdrs.at, hdrs.len, prov); + array_clear(hdrs); + if (ret != 0) { + kr_log_debug(DOH, "[%p] nghttp2_submit_response failed: %s\n", (void *)h2, nghttp2_strerror(ret)); + free(data); + return kr_error(EIO); + } + + /* Keep reference to data, since we need to free it later on. + * Due to HTTP/2 flow control, this stream data may be sent at a later point, or not at all. + */ + trie_val_t *stream_data_p = trie_get_ins(ctx->stream_write_data, (char *)&stream_id, sizeof(stream_id)); + if (kr_fails_assert(stream_data_p)) { + kr_log_debug(DOH, "[%p] failed to insert to stream_write_data\n", (void *)h2); + free(data); + return kr_error(EIO); + } + *stream_data_p = data; + ret = nghttp2_session_send(h2); + if(ret < 0) { + kr_log_debug(DOH, "[%p] nghttp2_session_send failed: %s\n", (void *)h2, nghttp2_strerror(ret)); + + /* At this point, there was an error in some nghttp2 callback. The on_pkt_write() + * callback which also calls free(data) may or may not have been called. Therefore, + * we must guarantee it will have been called by explicitly closing the stream. + * Afterwards, we have no option but to pretend this function was a success. If we + * returned an error, qr_task_send() logic would lead to a double-free because + * on_write() was already called. */ + nghttp2_submit_rst_stream(h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_INTERNAL_ERROR); + return 0; + } + + return 0; +} + +/* + * Same as `http_send_response`, but resets the HTTP stream afterwards. Used + * for sending negative status messages. + */ +static int http_send_response_rst_stream(struct http_ctx *ctx, int32_t stream_id, + nghttp2_data_provider *prov, enum http_status status) +{ + int ret = http_send_response(ctx, stream_id, prov, status); + if (ret) + return ret; + + ctx->last_stream = -1; + nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_NO_ERROR); + ret = nghttp2_session_send(ctx->h2); + return ret; +} + + +/* + * Send HTTP/2 stream data created from packet's wire buffer. + * + * If this function returns an error, the on_write() callback isn't (and + * mustn't be!) called, since such errors are handled in an upper layer - in + * qr_task_step() in daemon/worker. + */ +static int http_write_pkt(struct http_ctx *ctx, knot_pkt_t *pkt, int32_t stream_id, + uv_write_t *req, uv_write_cb on_write) +{ + struct http_data *data; + nghttp2_data_provider prov; + + data = malloc(sizeof(struct http_data)); + if (!data) + return kr_error(ENOMEM); + + data->buf = pkt->wire; + data->len = pkt->size; + data->pos = 0; + data->on_write = on_write; + data->req = req; + data->ttl = packet_ttl(pkt); + + prov.source.ptr = data; + prov.read_callback = read_callback; + + return http_send_response(ctx, stream_id, &prov, HTTP_STATUS_OK); +} + +/* + * Write request to HTTP/2 stream. + * + * Packet wire buffer must stay valid until the on_write callback. + */ +int http_write(uv_write_t *req, uv_handle_t *handle, knot_pkt_t *pkt, int32_t stream_id, + uv_write_cb on_write) +{ + struct session *session; + struct http_ctx *ctx; + int ret; + + if (!req || !pkt || !handle || !handle->data || stream_id < 0) + return kr_error(EINVAL); + req->handle = (uv_stream_t *)handle; + + session = handle->data; + if (session_flags(session)->outgoing) + return kr_error(ENOSYS); + + ctx = session_http_get_server_ctx(session); + if (!ctx || !ctx->h2) + return kr_error(EINVAL); + + ret = http_write_pkt(ctx, pkt, stream_id, req, on_write); + if (ret < 0) + return ret; + + return kr_ok(); +} + +/* + * Release HTTP/2 context. + */ +void http_free(struct http_ctx *ctx) +{ + if (!ctx) + return; + + kr_log_debug(DOH, "[%p] h2 session freed\n", (void *)ctx->h2); + + /* Clean up any headers whose ownership may not have been transferred. + * This may happen when connection is abruptly ended (e.g. due to errors while + * processing HTTP stream. */ + while (queue_len(ctx->streams) > 0) { + struct http_stream stream = queue_head(ctx->streams); + http_free_headers(stream.headers); + queue_pop(ctx->streams); + } + + trie_apply(ctx->stream_write_data, stream_write_data_free_err, NULL); + trie_free(ctx->stream_write_data); + + http_cleanup_stream(ctx); + queue_deinit(ctx->streams); + nghttp2_session_del(ctx->h2); + free(ctx); +} |