summaryrefslogtreecommitdiffstats
path: root/daemon/http.c
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/http.c')
-rw-r--r--daemon/http.c669
1 files changed, 669 insertions, 0 deletions
diff --git a/daemon/http.c b/daemon/http.c
new file mode 100644
index 0000000..f72465f
--- /dev/null
+++ b/daemon/http.c
@@ -0,0 +1,669 @@
+/*
+ * 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);
+}