From 4864787a1675d1b96af1d72d994305425fce417e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Je=C5=BEek?= <lukas.jezek@nic.cz> Date: Mon, 16 Nov 2020 09:28:50 +0100 Subject: [PATCH] doh2: split POST and GET method processing --- daemon/http.c | 97 +++++++++++++++++++++++++++++++++++++++------------ daemon/http.h | 8 +++++ 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/daemon/http.c b/daemon/http.c index b8c150024..beccac202 100644 --- a/daemon/http.c +++ b/daemon/http.c @@ -166,7 +166,6 @@ static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stre } ctx->buf_pos += ret; - ctx->incomplete_stream = stream_id; queue_push(ctx->streams, stream_id); return 0; } @@ -177,14 +176,39 @@ static void refuse_stream(nghttp2_session *h2, int32_t stream_id) 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. - * - * Since we don't need any headers for POST request, avoid processing them entirely to - * avoid potential issues if dns variable would be present in path. */ static int header_callback(nghttp2_session *h2, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, @@ -193,22 +217,35 @@ static int header_callback(nghttp2_session *h2, const nghttp2_frame *frame, struct http_ctx *ctx = (struct http_ctx *)user_data; int32_t stream_id = frame->hd.stream_id; - /* If the HEADERS don't have END_STREAM set, there are some DATA frames, - * which implies POST method. Skip header processing for POST. */ - if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) + + 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 (ctx->incomplete_stream != -1) { - kr_log_verbose( - "[http] stream %d incomplete, refusing\n", ctx->incomplete_stream); - 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 (process_uri_path(ctx, (const char*)value, stream_id) < 0) - refuse_stream(h2, stream_id); + 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; } @@ -226,27 +263,30 @@ static int data_chunk_recv_callback(nghttp2_session *h2, uint8_t flags, int32_t 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 != -1 && ctx->incomplete_stream != stream_id) { + 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; - if (ctx->incomplete_stream == -1) + /* 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 (ctx->incomplete_stream == -1) { - ctx->incomplete_stream = stream_id; + if (is_first) { ctx->buf_pos = sizeof(uint16_t); /* Reserve 2B for dnsmsg len. */ queue_push(ctx->streams, stream_id); } @@ -272,7 +312,15 @@ static int on_frame_recv_callback(nghttp2_session *h2, const nghttp2_frame *fram 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); + } + free(ctx->uri_path); + ctx->uri_path = NULL; + } ctx->incomplete_stream = -1; + ctx->current_method = HTTP_METHOD_NONE; len = ctx->buf_pos - sizeof(uint16_t); if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) { @@ -336,6 +384,7 @@ struct http_ctx* http_new(struct session *session, http_send_callback send_cb) 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( @@ -352,6 +401,8 @@ struct http_ctx* http_new(struct session *session, http_send_callback send_cb) 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, @@ -437,13 +488,13 @@ static int http_send_response(nghttp2_session *h2, int32_t stream_id, int ret; const char *directive_max_age = "max-age="; char size[MAX_DECIMAL_LENGTH(data->len)] = { 0 }; - char max_age[MAX_DECIMAL_LENGTH(data->ttl)+sizeof(directive_max_age)] = { 0 }; + int max_age_len = MAX_DECIMAL_LENGTH(data->ttl) + strlen(directive_max_age); + char max_age[max_age_len]; int size_len; - int max_age_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_DECIMAL_LENGTH(data->ttl)+sizeof(directive_max_age), - "%s%u", directive_max_age, data->ttl); + max_age_len = snprintf(max_age, max_age_len, "%s%u", directive_max_age, data->ttl); nghttp2_nv hdrs[] = { MAKE_STATIC_NV(":status", "200"), diff --git a/daemon/http.h b/daemon/http.h index 367181efe..f0662779b 100644 --- a/daemon/http.h +++ b/daemon/http.h @@ -26,6 +26,12 @@ typedef ssize_t(*http_send_callback)(const uint8_t *buffer, typedef queue_t(int32_t) queue_int32_t; +typedef enum { + HTTP_METHOD_NONE = 0, + HTTP_METHOD_GET = 1, + HTTP_METHOD_POST = 2, +} http_method_t; + struct http_ctx { struct nghttp2_session *h2; http_send_callback send_cb; @@ -33,6 +39,8 @@ struct http_ctx { queue_int32_t streams; /* IDs of streams present in the buffer. */ int32_t incomplete_stream; ssize_t submitted; + http_method_t current_method; + char *uri_path; uint8_t *buf; /* Part of the wire_buf that belongs to current HTTP/2 stream. */ ssize_t buf_pos; ssize_t buf_size; -- GitLab