/* * Copyright (C) 2020 CZ.NIC, z.s.p.o * * Initial Author: Jan Hák <jan.hak@nic.cz> * * SPDX-License-Identifier: GPL-3.0-or-later */ #include <assert.h> #include <errno.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 "contrib/cleanup.h" #include "contrib/base64url.h" #define MAKE_NV(K, KS, V, VS) \ { (uint8_t *)(K), (uint8_t *)(V), (KS), (VS), NGHTTP2_NV_FLAG_NONE } #define MAKE_STATIC_NV(K, V) \ MAKE_NV((K), sizeof(K) - 1, (V), sizeof(V) - 1) /* Use same maximum as for tcp_pipeline_max. */ #define HTTP_MAX_CONCURRENT_STREAMS UINT16_MAX #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; }; /* * 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); } /* * 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; assert(data->pos <= data->len); 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* uri_path) { static const char key[] = "dns="; static const char *delim = "&"; static const char *endpoins[] = {"dns-query", "doh"}; char *beg; char *end_prev; ssize_t endpoint_len; ssize_t ret; if (!uri_path) return kr_error(EINVAL); auto_free char *path = malloc(sizeof(*path) * (strlen(uri_path) + 1)); if (!path) return kr_error(ENOMEM); memcpy(path, uri_path, strlen(uri_path)); path[strlen(uri_path)] = '\0'; 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(endpoins)/sizeof(*endpoins); i++) { if (strlen(endpoins[i]) != endpoint_len) continue; ret = strncmp(path + 1, endpoins[i], strlen(endpoins[i])); if (!ret) break; } if (ret) /* no endpoint found */ return -1; if (endpoint_len == strlen(path) - 1) /* done for POST method */ return 0; /* go over key:value pair */ beg = strtok(query_mark + 1, delim); if (beg) { while (beg != NULL) { if (!strncmp(beg, key, 4)) { /* dns variable in path found */ break; } end_prev = beg + strlen(beg); beg = strtok(NULL, delim); if (beg-1 != end_prev) { /* detect && */ return -1; } } if (!beg) { /* no dns variable in path */ return -1; } } return 0; } /* * 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="; char *beg = strstr(path, key); char *end; size_t remaining; ssize_t ret; uint8_t *dest; if (!beg) /* No dns variable in path. */ return 0; 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; ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining); if (ret < 0) { ctx->buf_pos = 0; kr_log_verbose("[http] base64url decode failed %s\n", strerror(ret)); return ret; } ctx->buf_pos += ret; queue_push(ctx->streams, stream_id); return 0; } static void refuse_stream(nghttp2_session *h2, int32_t stream_id) { nghttp2_submit_rst_stream( h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_REFUSED_STREAM); } /* * 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_verbose( "[http] stream %d incomplete, refusing\n", ctx->incomplete_stream); refuse_stream(h2, stream_id); } else { ctx->incomplete_stream = stream_id; } 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_verbose( "[http] stream %d incomplete, refusing\n", ctx->incomplete_stream); refuse_stream(h2, stream_id); return 0; } if (!strcasecmp(":path", (const char *)name)) { if (check_uri((const char *)value) < 0) { refuse_stream(h2, stream_id); return 0; } 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 { ctx->current_method = HTTP_METHOD_NONE; } } 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) != ctx->incomplete_stream; if (ctx->incomplete_stream != stream_id) { kr_log_verbose( "[http] stream %d incomplete, refusing\n", 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("[http] insufficient space in buffer\n"); ctx->incomplete_stream = -1; return NGHTTP2_ERR_CALLBACK_FAILURE; } if (is_first) { ctx->buf_pos = sizeof(uint16_t); /* Reserve 2B for dnsmsg len. */ queue_push(ctx->streams, stream_id); } memmove(ctx->buf + ctx->buf_pos, data, len); ctx->buf_pos += len; return 0; } /* * 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; ssize_t len; int32_t stream_id = frame->hd.stream_id; assert(stream_id != -1); if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) && ctx->incomplete_stream == stream_id) { if (ctx->current_method == HTTP_METHOD_GET) { if (process_uri_path(ctx, ctx->uri_path, stream_id) < 0) { refuse_stream(h2, stream_id); } } ctx->incomplete_stream = -1; ctx->current_method = HTTP_METHOD_NONE; free(ctx->uri_path); ctx->uri_path = NULL; len = ctx->buf_pos - sizeof(uint16_t); if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) { kr_log_verbose("[http] invalid dnsmsg size: %zd B\n", len); return NGHTTP2_ERR_CALLBACK_FAILURE; } knot_wire_write_u16(ctx->buf, len); ctx->submitted += ctx->buf_pos; ctx->buf += ctx->buf_pos; ctx->buf_pos = 0; } 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); } /* * Cleanup for closed steams. * * If any stream_user_data was set, call the on_write callback to allow * freeing of the underlying data structure. */ static int on_stream_close_callback(nghttp2_session *h2, int32_t stream_id, uint32_t error_code, void *user_data) { struct http_data *data; data = nghttp2_session_get_stream_user_data(h2, stream_id); if (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 } }; nghttp2_session_callbacks_new(&callbacks); 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->incomplete_stream = -1; ctx->submitted = 0; ctx->current_method = HTTP_METHOD_NONE; ctx->uri_path = NULL; nghttp2_session_server_new(&ctx->h2, callbacks, ctx); nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, iv, sizeof(iv)/sizeof(*iv)); 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. */ ssize_t http_process_input_data(struct session *session, const uint8_t *buf, ssize_t nread) { struct http_ctx *ctx = session_http_get_server_ctx(session); ssize_t ret = 0; if (!ctx->h2) return kr_error(ENOSYS); assert(ctx->session == session); ctx->submitted = 0; 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_verbose("[http] nghttp2_session_mem_recv failed: %s (%zd)\n", nghttp2_strerror(ret), ret); return kr_error(EIO); } ret = nghttp2_session_send(ctx->h2); if (ret < 0) { kr_log_verbose("[http] nghttp2_session_send failed: %s (%zd)\n", nghttp2_strerror(ret), ret); return kr_error(EIO); } return ctx->submitted; } /* * 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; } /* * Send dns response provided by the HTTTP/2 data provider. * * Data isn't guaranteed to be sent immediately due to underlying HTTP/2 flow control. */ static int http_send_response(nghttp2_session *h2, int32_t stream_id, nghttp2_data_provider *prov) { struct http_data *data = (struct http_data*)prov->source.ptr; int ret; const char *directive_max_age = "max-age="; char size[MAX_DECIMAL_LENGTH(data->len)] = { 0 }; int max_age_len = MAX_DECIMAL_LENGTH(data->ttl) + strlen(directive_max_age); char max_age[max_age_len]; int size_len; memset(max_age, 0, max_age_len * sizeof(*max_age)); size_len = snprintf(size, MAX_DECIMAL_LENGTH(data->len), "%zu", data->len); max_age_len = snprintf(max_age, max_age_len, "%s%u", directive_max_age, data->ttl); nghttp2_nv hdrs[] = { MAKE_STATIC_NV(":status", "200"), MAKE_STATIC_NV("content-type", "application/dns-message"), MAKE_NV("content-length", 14, size, size_len), MAKE_NV("cache-control", 13, max_age, max_age_len), }; ret = nghttp2_submit_response(h2, stream_id, hdrs, sizeof(hdrs)/sizeof(*hdrs), prov); if (ret != 0) { kr_log_verbose("[http] nghttp2_submit_response failed: %s\n", nghttp2_strerror(ret)); return kr_error(EIO); } ret = nghttp2_session_set_stream_user_data(h2, stream_id, (void*)data); if (ret != 0) { kr_log_verbose("[http] failed to set stream user data: %s\n", nghttp2_strerror(ret)); return kr_error(EIO); } ret = nghttp2_session_send(h2); if(ret < 0) { kr_log_verbose("[http] nghttp2_session_send failed: %s\n", nghttp2_strerror(ret)); return kr_error(EIO); } return 0; } /* * Send HTTP/2 stream data created from packet's wire buffer. */ static int http_write_pkt(nghttp2_session *h2, 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; const bool is_negative = kr_response_classify(pkt) & (PKT_NODATA|PKT_NXDOMAIN); 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, is_negative); prov.source.ptr = data; prov.read_callback = read_callback; return http_send_response(h2, stream_id, &prov); } /* * 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->h2, 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; queue_deinit(ctx->streams); nghttp2_session_del(ctx->h2); free(ctx); }