Ensure that HTTP resources can only be served to a client connected on the specific service(s) that the resource was registered against using the HTTP_RESOURCE_DEFINE macro. This allows different resources to be registered to different services, for example to make some resources only available via an HTTPS service and not via unencrypted HTTP. Signed-off-by: Matt Rodgers <mrodgers@witekio.com>
1776 lines
46 KiB
C
1776 lines
46 KiB
C
/*
|
|
* Copyright (c) 2023, Emna Rekik
|
|
* Copyright (c) 2024 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#include <zephyr/fs/fs.h>
|
|
#include <zephyr/fs/fs_interface.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/net/http/service.h>
|
|
|
|
LOG_MODULE_DECLARE(net_http_server, CONFIG_NET_HTTP_SERVER_LOG_LEVEL);
|
|
|
|
#include "headers/server_internal.h"
|
|
|
|
static const char content_404[] = {
|
|
#ifdef INCLUDE_HTML_CONTENT
|
|
#include "not_found_page.html.gz.inc"
|
|
#endif
|
|
};
|
|
|
|
static bool is_header_flag_set(uint8_t flags, uint8_t mask)
|
|
{
|
|
return (flags & mask) != 0;
|
|
}
|
|
|
|
static void clear_header_flag(uint8_t *flags, uint8_t mask)
|
|
{
|
|
*flags &= ~mask;
|
|
}
|
|
|
|
static void print_http_frames(struct http_client_ctx *client)
|
|
{
|
|
#if defined(PRINT_COLOR)
|
|
const char *bold = "\033[1m";
|
|
const char *reset = "\033[0m";
|
|
const char *green = "\033[32m";
|
|
const char *blue = "\033[34m";
|
|
#else
|
|
const char *bold = "";
|
|
const char *reset = "";
|
|
const char *green = "";
|
|
const char *blue = "";
|
|
#endif
|
|
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
LOG_DBG("%s=====================================%s", green, reset);
|
|
LOG_DBG("%sReceived %s Frame :%s", bold, get_frame_type_name(frame->type), reset);
|
|
LOG_DBG(" %sLength:%s %u", blue, reset, frame->length);
|
|
LOG_DBG(" %sType:%s %u (%s)", blue, reset, frame->type, get_frame_type_name(frame->type));
|
|
LOG_DBG(" %sFlags:%s %u", blue, reset, frame->flags);
|
|
LOG_DBG(" %sStream Identifier:%s %u", blue, reset, frame->stream_identifier);
|
|
LOG_DBG("%s=====================================%s", green, reset);
|
|
}
|
|
|
|
static struct http2_stream_ctx *find_http_stream_context(
|
|
struct http_client_ctx *client, uint32_t stream_id)
|
|
{
|
|
ARRAY_FOR_EACH(client->streams, i) {
|
|
if (client->streams[i].stream_id == stream_id) {
|
|
return &client->streams[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct http2_stream_ctx *allocate_http_stream_context(
|
|
struct http_client_ctx *client, uint32_t stream_id)
|
|
{
|
|
ARRAY_FOR_EACH(client->streams, i) {
|
|
if (client->streams[i].stream_state == HTTP2_STREAM_IDLE) {
|
|
client->streams[i].stream_id = stream_id;
|
|
client->streams[i].stream_state = HTTP2_STREAM_OPEN;
|
|
client->streams[i].window_size =
|
|
HTTP_SERVER_INITIAL_WINDOW_SIZE;
|
|
client->streams[i].headers_sent = false;
|
|
client->streams[i].end_stream_sent = false;
|
|
return &client->streams[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void release_http_stream_context(struct http_client_ctx *client,
|
|
uint32_t stream_id)
|
|
{
|
|
ARRAY_FOR_EACH(client->streams, i) {
|
|
if (client->streams[i].stream_id == stream_id) {
|
|
client->streams[i].stream_id = 0;
|
|
client->streams[i].stream_state = HTTP2_STREAM_IDLE;
|
|
client->streams[i].current_detail = NULL;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int add_header_field(struct http_client_ctx *client, uint8_t **buf,
|
|
size_t *buflen, const char *name, const char *value)
|
|
{
|
|
int ret;
|
|
|
|
client->header_field.name = name;
|
|
client->header_field.name_len = strlen(name);
|
|
client->header_field.value = value;
|
|
client->header_field.value_len = strlen(value);
|
|
|
|
ret = http_hpack_encode_header(*buf, *buflen, &client->header_field);
|
|
if (ret < 0) {
|
|
LOG_DBG("Failed to encode header, err %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
*buf += ret;
|
|
*buflen -= ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void encode_frame_header(uint8_t *buf, uint32_t payload_len,
|
|
enum http2_frame_type frame_type,
|
|
uint8_t flags, uint32_t stream_id)
|
|
{
|
|
sys_put_be24(payload_len, &buf[HTTP2_FRAME_LENGTH_OFFSET]);
|
|
buf[HTTP2_FRAME_TYPE_OFFSET] = frame_type;
|
|
buf[HTTP2_FRAME_FLAGS_OFFSET] = flags;
|
|
sys_put_be32(stream_id, &buf[HTTP2_FRAME_STREAM_ID_OFFSET]);
|
|
}
|
|
|
|
static int send_headers_frame(struct http_client_ctx *client, enum http_status status,
|
|
uint32_t stream_id, struct http_resource_detail *detail_common,
|
|
uint8_t flags, const struct http_header *extra_headers,
|
|
size_t extra_headers_count)
|
|
{
|
|
uint8_t headers_frame[CONFIG_HTTP_SERVER_HTTP2_MAX_HEADER_FRAME_LEN];
|
|
uint8_t status_str[4];
|
|
uint8_t *buf = headers_frame + HTTP2_FRAME_HEADER_SIZE;
|
|
size_t buflen = sizeof(headers_frame) - HTTP2_FRAME_HEADER_SIZE;
|
|
bool content_encoding_sent = false;
|
|
bool content_type_sent = false;
|
|
size_t payload_len;
|
|
int ret;
|
|
|
|
ret = snprintf(status_str, sizeof(status_str), "%d", status);
|
|
if (ret > sizeof(status_str) - 1) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = add_header_field(client, &buf, &buflen, ":status", status_str);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
for (size_t i = 0; i < extra_headers_count; i++) {
|
|
const struct http_header *hdr = &extra_headers[i];
|
|
|
|
if (strcasecmp(hdr->name, "content-encoding") == 0) {
|
|
content_encoding_sent = true;
|
|
}
|
|
|
|
if (strcasecmp(hdr->name, "content-type") == 0) {
|
|
content_type_sent = true;
|
|
}
|
|
|
|
ret = add_header_field(client, &buf, &buflen, hdr->name, hdr->value);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (!content_encoding_sent && detail_common && detail_common->content_encoding != NULL) {
|
|
ret = add_header_field(client, &buf, &buflen, "content-encoding",
|
|
detail_common->content_encoding);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (!content_type_sent && detail_common && detail_common->content_type != NULL) {
|
|
ret = add_header_field(client, &buf, &buflen, "content-type",
|
|
detail_common->content_type);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
payload_len = sizeof(headers_frame) - buflen - HTTP2_FRAME_HEADER_SIZE;
|
|
flags |= HTTP2_FLAG_END_HEADERS;
|
|
|
|
encode_frame_header(headers_frame, payload_len, HTTP2_HEADERS_FRAME,
|
|
flags, stream_id);
|
|
|
|
ret = http_server_sendall(client, headers_frame,
|
|
payload_len + HTTP2_FRAME_HEADER_SIZE);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int send_data_frame(struct http_client_ctx *client, const char *payload,
|
|
size_t length, uint32_t stream_id, uint8_t flags)
|
|
{
|
|
uint8_t frame_header[HTTP2_FRAME_HEADER_SIZE];
|
|
int ret;
|
|
|
|
encode_frame_header(frame_header, length, HTTP2_DATA_FRAME,
|
|
is_header_flag_set(flags, HTTP2_FLAG_END_STREAM) ?
|
|
HTTP2_FLAG_END_STREAM : 0,
|
|
stream_id);
|
|
|
|
ret = http_server_sendall(client, frame_header, sizeof(frame_header));
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
} else {
|
|
if (payload != NULL && length > 0) {
|
|
ret = http_server_sendall(client, payload, length);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
int send_settings_frame(struct http_client_ctx *client, bool ack)
|
|
{
|
|
uint8_t settings_frame[HTTP2_FRAME_HEADER_SIZE +
|
|
2 * sizeof(struct http2_settings_field)];
|
|
struct http2_settings_field *setting;
|
|
size_t len;
|
|
int ret;
|
|
|
|
if (ack) {
|
|
encode_frame_header(settings_frame, 0,
|
|
HTTP2_SETTINGS_FRAME,
|
|
HTTP2_FLAG_SETTINGS_ACK, 0);
|
|
len = HTTP2_FRAME_HEADER_SIZE;
|
|
} else {
|
|
encode_frame_header(settings_frame,
|
|
2 * sizeof(struct http2_settings_field),
|
|
HTTP2_SETTINGS_FRAME, 0, 0);
|
|
|
|
setting = (struct http2_settings_field *)
|
|
(settings_frame + HTTP2_FRAME_HEADER_SIZE);
|
|
UNALIGNED_PUT(htons(HTTP2_SETTINGS_HEADER_TABLE_SIZE),
|
|
&setting->id);
|
|
UNALIGNED_PUT(0, &setting->value);
|
|
|
|
setting++;
|
|
UNALIGNED_PUT(htons(HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS),
|
|
&setting->id);
|
|
UNALIGNED_PUT(htonl(CONFIG_HTTP_SERVER_MAX_STREAMS),
|
|
&setting->value);
|
|
|
|
len = HTTP2_FRAME_HEADER_SIZE +
|
|
2 * sizeof(struct http2_settings_field);
|
|
}
|
|
|
|
ret = http_server_sendall(client, settings_frame, len);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int send_window_update_frame(struct http_client_ctx *client,
|
|
struct http2_stream_ctx *stream)
|
|
{
|
|
uint8_t window_update_frame[HTTP2_FRAME_HEADER_SIZE +
|
|
sizeof(uint32_t)];
|
|
uint32_t window_update;
|
|
uint32_t stream_id;
|
|
int ret;
|
|
|
|
if (stream != NULL) {
|
|
window_update = HTTP_SERVER_INITIAL_WINDOW_SIZE - stream->window_size;
|
|
stream->window_size = HTTP_SERVER_INITIAL_WINDOW_SIZE;
|
|
stream_id = stream->stream_id;
|
|
} else {
|
|
window_update = HTTP_SERVER_INITIAL_WINDOW_SIZE - client->window_size;
|
|
client->window_size = HTTP_SERVER_INITIAL_WINDOW_SIZE;
|
|
stream_id = 0;
|
|
}
|
|
|
|
encode_frame_header(window_update_frame, sizeof(uint32_t),
|
|
HTTP2_WINDOW_UPDATE_FRAME,
|
|
0, stream_id);
|
|
sys_put_be32(window_update,
|
|
window_update_frame + HTTP2_FRAME_HEADER_SIZE);
|
|
|
|
ret = http_server_sendall(client, window_update_frame,
|
|
sizeof(window_update_frame));
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int send_http2_404(struct http_client_ctx *client,
|
|
struct http2_frame *frame)
|
|
{
|
|
int ret;
|
|
|
|
ret = send_headers_frame(client, HTTP_404_NOT_FOUND, frame->stream_identifier, NULL, 0,
|
|
NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = send_data_frame(client, content_404, sizeof(content_404),
|
|
frame->stream_identifier,
|
|
HTTP2_FLAG_END_STREAM);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int send_http2_409(struct http_client_ctx *client,
|
|
struct http2_frame *frame)
|
|
{
|
|
int ret;
|
|
|
|
ret = send_headers_frame(client, HTTP_409_CONFLICT, frame->stream_identifier, NULL,
|
|
HTTP2_FLAG_END_STREAM, NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int handle_http2_static_resource(
|
|
struct http_resource_detail_static *static_detail,
|
|
struct http2_frame *frame, struct http_client_ctx *client)
|
|
{
|
|
const char *content_200;
|
|
size_t content_len;
|
|
int ret;
|
|
|
|
if (!(static_detail->common.bitmask_of_supported_http_methods & BIT(HTTP_GET))) {
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if (client->current_stream == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
content_200 = static_detail->static_data;
|
|
content_len = static_detail->static_data_len;
|
|
|
|
ret = send_headers_frame(client, HTTP_200_OK, frame->stream_identifier,
|
|
&static_detail->common, 0, NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
goto out;
|
|
}
|
|
|
|
client->current_stream->headers_sent = true;
|
|
|
|
ret = send_data_frame(client, content_200, content_len,
|
|
frame->stream_identifier,
|
|
HTTP2_FLAG_END_STREAM);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
goto out;
|
|
}
|
|
|
|
client->current_stream->end_stream_sent = true;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static int handle_http2_static_fs_resource(struct http_resource_detail_static_fs *static_fs_detail,
|
|
struct http2_frame *frame,
|
|
struct http_client_ctx *client)
|
|
{
|
|
int ret;
|
|
struct fs_file_t file;
|
|
char fname[HTTP_SERVER_MAX_URL_LENGTH];
|
|
char content_type[HTTP_SERVER_MAX_CONTENT_TYPE_LEN] = "text/html";
|
|
struct http_resource_detail res_detail = {
|
|
.bitmask_of_supported_http_methods =
|
|
static_fs_detail->common.bitmask_of_supported_http_methods,
|
|
.content_type = content_type,
|
|
.path_len = static_fs_detail->common.path_len,
|
|
.type = static_fs_detail->common.type,
|
|
};
|
|
bool gzipped;
|
|
int len;
|
|
int remaining;
|
|
char tmp[64];
|
|
|
|
if (!(static_fs_detail->common.bitmask_of_supported_http_methods & BIT(HTTP_GET))) {
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if (client->current_stream == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* get filename and content-type from url */
|
|
len = strlen(client->url_buffer);
|
|
if (len == 1) {
|
|
/* url is just the leading slash, use index.html as filename */
|
|
snprintk(fname, sizeof(fname), "%s/index.html", static_fs_detail->fs_path);
|
|
} else {
|
|
http_server_get_content_type_from_extension(client->url_buffer, content_type,
|
|
sizeof(content_type));
|
|
snprintk(fname, sizeof(fname), "%s%s", static_fs_detail->fs_path,
|
|
client->url_buffer);
|
|
}
|
|
|
|
/* open file, if it exists */
|
|
ret = http_server_find_file(fname, sizeof(fname), &client->data_len, &gzipped);
|
|
if (ret < 0) {
|
|
LOG_ERR("fs_stat %s: %d", fname, ret);
|
|
|
|
ret = send_headers_frame(client, HTTP_404_NOT_FOUND, frame->stream_identifier, NULL,
|
|
0, NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
}
|
|
return ret;
|
|
}
|
|
fs_file_t_init(&file);
|
|
ret = fs_open(&file, fname, FS_O_READ);
|
|
if (ret < 0) {
|
|
LOG_ERR("fs_open %s: %d", fname, ret);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/* send headers */
|
|
if (gzipped) {
|
|
res_detail.content_encoding = "gzip";
|
|
}
|
|
ret = send_headers_frame(client, HTTP_200_OK, frame->stream_identifier, &res_detail, 0,
|
|
NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
goto out;
|
|
}
|
|
|
|
client->current_stream->headers_sent = true;
|
|
|
|
/* read and send file */
|
|
remaining = client->data_len;
|
|
while (remaining > 0) {
|
|
len = fs_read(&file, tmp, sizeof(tmp));
|
|
if (len < 0) {
|
|
LOG_ERR("Filesystem read error (%d)", len);
|
|
goto out;
|
|
}
|
|
|
|
remaining -= len;
|
|
ret = send_data_frame(client, tmp, len, frame->stream_identifier,
|
|
(remaining > 0) ? 0 : HTTP2_FLAG_END_STREAM);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
client->current_stream->end_stream_sent = true;
|
|
|
|
out:
|
|
/* close file */
|
|
fs_close(&file);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int http2_dynamic_response(struct http_client_ctx *client, struct http2_frame *frame,
|
|
struct http_response_ctx *rsp, enum http_data_status data_status,
|
|
struct http_resource_detail_dynamic *dynamic_detail)
|
|
{
|
|
int ret;
|
|
uint8_t flags = 0;
|
|
bool final_response = http_response_is_final(rsp, data_status);
|
|
|
|
if (client->current_stream->headers_sent && (rsp->header_count > 0 || rsp->status != 0)) {
|
|
LOG_WRN("Already sent headers, dropping new headers and/or response code");
|
|
}
|
|
|
|
/* Send headers and response code if not already sent */
|
|
if (!client->current_stream->headers_sent) {
|
|
/* Use '200 OK' status if not specified by application */
|
|
if (rsp->status == 0) {
|
|
rsp->status = 200;
|
|
}
|
|
|
|
if (rsp->status < HTTP_100_CONTINUE ||
|
|
rsp->status > HTTP_511_NETWORK_AUTHENTICATION_REQUIRED) {
|
|
LOG_DBG("Invalid HTTP status code: %d", rsp->status);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (rsp->headers == NULL && rsp->header_count > 0) {
|
|
LOG_DBG("NULL headers, but count is > 0");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (final_response && rsp->body_len == 0) {
|
|
flags |= HTTP2_FLAG_END_STREAM;
|
|
client->current_stream->end_stream_sent = true;
|
|
}
|
|
|
|
ret = send_headers_frame(client, rsp->status, frame->stream_identifier,
|
|
(struct http_resource_detail *)dynamic_detail, flags,
|
|
rsp->headers, rsp->header_count);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
client->current_stream->headers_sent = true;
|
|
}
|
|
|
|
/* Send body data if provided */
|
|
if (rsp->body != NULL && rsp->body_len > 0) {
|
|
if (final_response) {
|
|
flags |= HTTP2_FLAG_END_STREAM;
|
|
client->current_stream->end_stream_sent = true;
|
|
}
|
|
|
|
ret = send_data_frame(client, rsp->body, rsp->body_len, frame->stream_identifier,
|
|
flags);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dynamic_get_del_req_v2(struct http_resource_detail_dynamic *dynamic_detail,
|
|
struct http_client_ctx *client)
|
|
{
|
|
int ret, len;
|
|
char *ptr;
|
|
struct http2_frame *frame = &client->current_frame;
|
|
enum http_data_status status;
|
|
struct http_request_ctx request_ctx;
|
|
struct http_response_ctx response_ctx;
|
|
|
|
if (client->current_stream == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* Start of GET params */
|
|
ptr = &client->url_buffer[dynamic_detail->common.path_len];
|
|
len = strlen(ptr);
|
|
status = HTTP_SERVER_DATA_FINAL;
|
|
|
|
do {
|
|
memset(&response_ctx, 0, sizeof(response_ctx));
|
|
populate_request_ctx(&request_ctx, ptr, len, &client->header_capture_ctx);
|
|
|
|
ret = dynamic_detail->cb(client, status, &request_ctx, &response_ctx,
|
|
dynamic_detail->user_data);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
ret = http2_dynamic_response(client, frame, &response_ctx, status, dynamic_detail);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* URL params are passed in the first cb only */
|
|
len = 0;
|
|
} while (!http_response_is_final(&response_ctx, status));
|
|
|
|
if (!client->current_stream->end_stream_sent) {
|
|
client->current_stream->end_stream_sent = true;
|
|
ret = send_data_frame(client, NULL, 0, frame->stream_identifier,
|
|
HTTP2_FLAG_END_STREAM);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot send last frame (%d)", ret);
|
|
}
|
|
}
|
|
|
|
dynamic_detail->holder = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int dynamic_post_put_req_v2(struct http_resource_detail_dynamic *dynamic_detail,
|
|
struct http_client_ctx *client, bool headers_only)
|
|
{
|
|
int ret = 0;
|
|
char *ptr = client->cursor;
|
|
size_t data_len;
|
|
enum http_data_status status;
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http_request_ctx request_ctx;
|
|
struct http_response_ctx response_ctx;
|
|
struct http_header_capture_ctx *request_headers_ctx =
|
|
headers_only ? &client->header_capture_ctx : NULL;
|
|
|
|
if (dynamic_detail == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (client->current_stream == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (headers_only) {
|
|
data_len = 0;
|
|
} else {
|
|
data_len = MIN(frame->length, client->data_len);
|
|
frame->length -= data_len;
|
|
client->cursor += data_len;
|
|
client->data_len -= data_len;
|
|
}
|
|
|
|
if (frame->length == 0 && is_header_flag_set(frame->flags, HTTP2_FLAG_END_STREAM) &&
|
|
!headers_only) {
|
|
status = HTTP_SERVER_DATA_FINAL;
|
|
} else {
|
|
status = HTTP_SERVER_DATA_MORE;
|
|
}
|
|
|
|
memset(&response_ctx, 0, sizeof(response_ctx));
|
|
populate_request_ctx(&request_ctx, ptr, data_len, request_headers_ctx);
|
|
|
|
ret = dynamic_detail->cb(client, status, &request_ctx, &response_ctx,
|
|
dynamic_detail->user_data);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* For POST the application might not send a response until all data has been received.
|
|
* Don't send a default response until the application has had a chance to respond.
|
|
*/
|
|
if (http_response_is_provided(&response_ctx)) {
|
|
ret = http2_dynamic_response(client, frame, &response_ctx, status, dynamic_detail);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/* Once all data is transferred to application, repeat cb until response is complete */
|
|
while (!http_response_is_final(&response_ctx, status) && status == HTTP_SERVER_DATA_FINAL) {
|
|
memset(&response_ctx, 0, sizeof(response_ctx));
|
|
populate_request_ctx(&request_ctx, ptr, 0, request_headers_ctx);
|
|
|
|
ret = dynamic_detail->cb(client, status, &request_ctx, &response_ctx,
|
|
dynamic_detail->user_data);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
ret = http2_dynamic_response(client, frame, &response_ctx, status, dynamic_detail);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/* At end of stream, ensure response is sent and terminated */
|
|
if (frame->length == 0 && !client->current_stream->end_stream_sent &&
|
|
is_header_flag_set(frame->flags, HTTP2_FLAG_END_STREAM)) {
|
|
if (client->current_stream->headers_sent) {
|
|
ret = send_data_frame(client, NULL, 0, frame->stream_identifier,
|
|
HTTP2_FLAG_END_STREAM);
|
|
} else {
|
|
memset(&response_ctx, 0, sizeof(response_ctx));
|
|
response_ctx.final_chunk = true;
|
|
ret = http2_dynamic_response(client, frame, &response_ctx,
|
|
HTTP_SERVER_DATA_FINAL, dynamic_detail);
|
|
}
|
|
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot send last frame (%d)", ret);
|
|
}
|
|
|
|
client->current_stream->end_stream_sent = true;
|
|
dynamic_detail->holder = NULL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int handle_http2_dynamic_resource(
|
|
struct http_resource_detail_dynamic *dynamic_detail,
|
|
struct http2_frame *frame, struct http_client_ctx *client)
|
|
{
|
|
uint32_t user_method;
|
|
int ret;
|
|
|
|
if (dynamic_detail->cb == NULL) {
|
|
return -ESRCH;
|
|
}
|
|
|
|
user_method = dynamic_detail->common.bitmask_of_supported_http_methods;
|
|
|
|
if (!(BIT(client->method) & user_method)) {
|
|
return -ENOPROTOOPT;
|
|
}
|
|
|
|
if (dynamic_detail->holder != NULL && dynamic_detail->holder != client) {
|
|
ret = send_http2_409(client, frame);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
return enter_http_done_state(client);
|
|
}
|
|
|
|
dynamic_detail->holder = client;
|
|
|
|
switch (client->method) {
|
|
case HTTP_GET:
|
|
case HTTP_DELETE:
|
|
if (user_method & BIT(client->method)) {
|
|
return dynamic_get_del_req_v2(dynamic_detail, client);
|
|
}
|
|
|
|
goto not_supported;
|
|
|
|
case HTTP_POST:
|
|
case HTTP_PUT:
|
|
case HTTP_PATCH:
|
|
/* The data will come in DATA frames. Remember the detail ptr
|
|
* which needs to be known when passing data to application.
|
|
*/
|
|
if (user_method & BIT(client->method)) {
|
|
client->current_stream->current_detail =
|
|
(struct http_resource_detail *)dynamic_detail;
|
|
|
|
/* If there are any header fields to pass to the application, call the
|
|
* dynamic handler now with the header data so that this may be cleared to
|
|
* re-use for any other concurrent streams
|
|
*/
|
|
if (IS_ENABLED(CONFIG_HTTP_SERVER_CAPTURE_HEADERS)) {
|
|
ret = dynamic_post_put_req_v2(dynamic_detail, client, true);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
goto not_supported;
|
|
|
|
not_supported:
|
|
default:
|
|
LOG_DBG("HTTP method %s (%d) not supported.",
|
|
http_method_str(client->method),
|
|
client->method);
|
|
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int enter_http2_request(struct http_client_ctx *client)
|
|
{
|
|
int ret;
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
client->data_len -= sizeof(HTTP2_PREFACE) - 1;
|
|
client->cursor += sizeof(HTTP2_PREFACE) - 1;
|
|
|
|
/* HTTP/2 client preface received, send server preface
|
|
* (settings frame).
|
|
*/
|
|
if (!client->preface_sent) {
|
|
ret = send_settings_frame(client, false);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
client->preface_sent = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_settings_state(struct http_client_ctx *client)
|
|
{
|
|
client->server_state = HTTP_SERVER_FRAME_SETTINGS_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_data_state(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http2_stream_ctx *stream;
|
|
|
|
if (frame->stream_identifier == 0) {
|
|
LOG_DBG("Stream ID 0 is forbidden for data frames.");
|
|
return -EBADMSG;
|
|
}
|
|
|
|
stream = find_http_stream_context(client, frame->stream_identifier);
|
|
if (stream == NULL) {
|
|
LOG_DBG("No stream context found for ID %d",
|
|
frame->stream_identifier);
|
|
return -EBADMSG;
|
|
}
|
|
|
|
if (stream->stream_state != HTTP2_STREAM_OPEN &&
|
|
stream->stream_state != HTTP2_STREAM_HALF_CLOSED_REMOTE) {
|
|
LOG_DBG("Stream ID %d in a wrong state %d", stream->stream_id,
|
|
stream->stream_state);
|
|
return -EBADMSG;
|
|
}
|
|
|
|
stream->window_size -= frame->length;
|
|
client->window_size -= frame->length;
|
|
client->server_state = HTTP_SERVER_FRAME_DATA_STATE;
|
|
client->current_stream = stream;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_headers_state(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http2_stream_ctx *stream;
|
|
|
|
stream = find_http_stream_context(client, frame->stream_identifier);
|
|
if (!stream) {
|
|
LOG_DBG("|| stream ID || %d", frame->stream_identifier);
|
|
|
|
stream = allocate_http_stream_context(client, frame->stream_identifier);
|
|
if (!stream) {
|
|
LOG_DBG("No available stream slots. Connection closed.");
|
|
|
|
return -ENOMEM;
|
|
}
|
|
}
|
|
|
|
client->current_stream = stream;
|
|
|
|
if (!is_header_flag_set(frame->flags, HTTP2_FLAG_END_HEADERS)) {
|
|
client->expect_continuation = true;
|
|
} else {
|
|
client->expect_continuation = false;
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_HTTP_SERVER_CAPTURE_HEADERS)) {
|
|
/* Reset header capture state for new headers frame */
|
|
client->header_capture_ctx.count = 0;
|
|
client->header_capture_ctx.cursor = 0;
|
|
client->header_capture_ctx.status = HTTP_HEADER_STATUS_OK;
|
|
client->header_capture_ctx.current_stream = stream;
|
|
}
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADERS_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_continuation_state(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
if (!is_header_flag_set(frame->flags, HTTP2_FLAG_END_HEADERS)) {
|
|
client->expect_continuation = true;
|
|
} else {
|
|
client->expect_continuation = false;
|
|
}
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_CONTINUATION_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_window_update_state(struct http_client_ctx *client)
|
|
{
|
|
client->server_state = HTTP_SERVER_FRAME_WINDOW_UPDATE_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_priority_state(struct http_client_ctx *client)
|
|
{
|
|
client->server_state = HTTP_SERVER_FRAME_PRIORITY_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_rst_stream_state(struct http_client_ctx *client)
|
|
{
|
|
client->server_state = HTTP_SERVER_FRAME_RST_STREAM_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enter_http_frame_goaway_state(struct http_client_ctx *client)
|
|
{
|
|
client->server_state = HTTP_SERVER_FRAME_GOAWAY_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_header(struct http_client_ctx *client)
|
|
{
|
|
int ret;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_HEADER");
|
|
|
|
ret = parse_http_frame_header(client, client->cursor, client->data_len);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
client->cursor += HTTP2_FRAME_HEADER_SIZE;
|
|
client->data_len -= HTTP2_FRAME_HEADER_SIZE;
|
|
|
|
print_http_frames(client);
|
|
|
|
if (client->expect_continuation &&
|
|
client->current_frame.type != HTTP2_CONTINUATION_FRAME) {
|
|
LOG_ERR("Continuation frame expected");
|
|
return -EBADMSG;
|
|
}
|
|
|
|
client->current_stream = NULL;
|
|
|
|
switch (client->current_frame.type) {
|
|
case HTTP2_DATA_FRAME:
|
|
return enter_http_frame_data_state(client);
|
|
case HTTP2_HEADERS_FRAME:
|
|
return enter_http_frame_headers_state(client);
|
|
case HTTP2_CONTINUATION_FRAME:
|
|
return enter_http_frame_continuation_state(client);
|
|
case HTTP2_SETTINGS_FRAME:
|
|
return enter_http_frame_settings_state(client);
|
|
case HTTP2_WINDOW_UPDATE_FRAME:
|
|
return enter_http_frame_window_update_state(client);
|
|
case HTTP2_RST_STREAM_FRAME:
|
|
return enter_http_frame_rst_stream_state(client);
|
|
case HTTP2_GOAWAY_FRAME:
|
|
return enter_http_frame_goaway_state(client);
|
|
case HTTP2_PRIORITY_FRAME:
|
|
return enter_http_frame_priority_state(client);
|
|
default:
|
|
return enter_http_done_state(client);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* This feature is theoretically obsoleted in RFC9113, but curl for instance
|
|
* still uses it, so implement as described in RFC7540.
|
|
*/
|
|
int handle_http1_to_http2_upgrade(struct http_client_ctx *client)
|
|
{
|
|
static const char switching_protocols[] =
|
|
"HTTP/1.1 101 Switching Protocols\r\n"
|
|
"Connection: Upgrade\r\n"
|
|
"Upgrade: h2c\r\n"
|
|
"\r\n";
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http_resource_detail *detail;
|
|
struct http2_stream_ctx *stream;
|
|
int path_len;
|
|
int ret;
|
|
|
|
/* Create an artificial Data frame, so that we can proceed with HTTP2
|
|
* processing. The HTTP/1.1 request that is sent prior to upgrade is
|
|
* assigned a stream identifier of 1.
|
|
*/
|
|
frame->stream_identifier = 1;
|
|
frame->type = HTTP2_DATA_FRAME;
|
|
frame->length = client->http1_frag_data_len;
|
|
if (client->parser_state == HTTP1_MESSAGE_COMPLETE_STATE) {
|
|
frame->flags = HTTP2_FLAG_END_STREAM;
|
|
} else {
|
|
frame->flags = 0;
|
|
}
|
|
|
|
/* Allocate stream. */
|
|
stream = find_http_stream_context(client, frame->stream_identifier);
|
|
if (stream == NULL) {
|
|
stream = allocate_http_stream_context(client, frame->stream_identifier);
|
|
if (!stream) {
|
|
LOG_DBG("No available stream slots. Connection closed.");
|
|
return -ENOMEM;
|
|
}
|
|
}
|
|
|
|
client->current_stream = stream;
|
|
|
|
if (!client->preface_sent) {
|
|
ret = http_server_sendall(client, switching_protocols,
|
|
sizeof(switching_protocols) - 1);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
|
|
/* The first HTTP/2 frame sent by the server MUST be a server connection
|
|
* preface.
|
|
*/
|
|
ret = send_settings_frame(client, false);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
|
|
client->preface_sent = true;
|
|
}
|
|
|
|
detail = get_resource_detail(client->service, client->url_buffer, &path_len, false);
|
|
if (detail != NULL) {
|
|
detail->path_len = path_len;
|
|
|
|
if (detail->type == HTTP_RESOURCE_TYPE_STATIC) {
|
|
ret = handle_http2_static_resource(
|
|
(struct http_resource_detail_static *)detail,
|
|
frame, client);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
} else if (detail->type == HTTP_RESOURCE_TYPE_STATIC_FS) {
|
|
ret = handle_http2_static_fs_resource(
|
|
(struct http_resource_detail_static_fs *)detail, frame, client);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
} else if (detail->type == HTTP_RESOURCE_TYPE_DYNAMIC) {
|
|
ret = handle_http2_dynamic_resource(
|
|
(struct http_resource_detail_dynamic *)detail,
|
|
frame, client);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
|
|
if (client->method == HTTP_POST ||
|
|
client->method == HTTP_PUT ||
|
|
client->method == HTTP_PATCH) {
|
|
ret = dynamic_post_put_req_v2(
|
|
(struct http_resource_detail_dynamic *)detail, client,
|
|
false);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
}
|
|
} else {
|
|
ret = send_http2_404(client, frame);
|
|
if (ret < 0) {
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
/* Only after the complete HTTP1 payload has been processed, switch
|
|
* to HTTP2.
|
|
*/
|
|
if (client->parser_state == HTTP1_MESSAGE_COMPLETE_STATE) {
|
|
release_http_stream_context(client, frame->stream_identifier);
|
|
client->current_detail = NULL;
|
|
client->server_state = HTTP_SERVER_PREFACE_STATE;
|
|
client->cursor += client->data_len;
|
|
client->data_len = 0;
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
return ret;
|
|
}
|
|
|
|
static int parse_http_frame_padded_field(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
if (client->data_len == 0) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
frame->padding_len = *client->cursor;
|
|
client->cursor++;
|
|
client->data_len--;
|
|
frame->length--;
|
|
|
|
if (frame->length <= frame->padding_len) {
|
|
return -EBADMSG;
|
|
}
|
|
|
|
/* Subtract the padding length from frame length now to simplify
|
|
* payload processing. Padding will be handled based on
|
|
* frame->padding_len in a separate state.
|
|
*/
|
|
frame->length -= frame->padding_len;
|
|
|
|
/* Clear the padded flag, this indicates that padding field was
|
|
* already parsed.
|
|
*/
|
|
clear_header_flag(&frame->flags, HTTP2_FLAG_PADDED);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int parse_http_frame_priority_field(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
if (client->data_len < HTTP2_HEADERS_FRAME_PRIORITY_LEN) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
/* Priority signalling is deprecated by RFC 9113, however it still
|
|
* should be expected to receive, just drop the bytes.
|
|
*/
|
|
client->cursor += HTTP2_HEADERS_FRAME_PRIORITY_LEN;
|
|
client->data_len -= HTTP2_HEADERS_FRAME_PRIORITY_LEN;
|
|
frame->length -= HTTP2_HEADERS_FRAME_PRIORITY_LEN;
|
|
|
|
/* Clear the priority flag, this indicates that priority field was
|
|
* already parsed.
|
|
*/
|
|
clear_header_flag(&frame->flags, HTTP2_FLAG_PRIORITY);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_data(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
int ret;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_DATA_STATE");
|
|
|
|
if (client->current_stream->current_detail == NULL) {
|
|
/* There is no handler */
|
|
LOG_DBG("No dynamic handler found.");
|
|
(void)send_http2_404(client, frame);
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (is_header_flag_set(frame->flags, HTTP2_FLAG_PADDED)) {
|
|
ret = parse_http_frame_padded_field(client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
ret = dynamic_post_put_req_v2(
|
|
(struct http_resource_detail_dynamic *)client->current_stream->current_detail,
|
|
client, false);
|
|
if (ret < 0 && ret == -ENOENT) {
|
|
ret = send_http2_404(client, frame);
|
|
}
|
|
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
if (frame->length == 0) {
|
|
struct http2_stream_ctx *stream =
|
|
find_http_stream_context(client, frame->stream_identifier);
|
|
|
|
if (stream == NULL) {
|
|
LOG_DBG("No stream context found for ID %d",
|
|
frame->stream_identifier);
|
|
return -EBADMSG;
|
|
}
|
|
|
|
ret = send_window_update_frame(client, stream);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
ret = send_window_update_frame(client, NULL);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
if (is_header_flag_set(frame->flags, HTTP2_FLAG_END_STREAM)) {
|
|
client->current_stream->current_detail = NULL;
|
|
release_http_stream_context(client, frame->stream_identifier);
|
|
}
|
|
|
|
/* Whole frame consumed, expect next one. */
|
|
if (frame->padding_len > 0) {
|
|
client->server_state = HTTP_SERVER_FRAME_PADDING_STATE;
|
|
} else {
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void check_user_request_headers_http2(struct http_header_capture_ctx *ctx,
|
|
struct http_hpack_header_buf *hdr_buf)
|
|
{
|
|
size_t required_len;
|
|
char *dest = &ctx->buffer[ctx->cursor];
|
|
size_t remaining = sizeof(ctx->buffer) - ctx->cursor;
|
|
struct http_header *current_header = &ctx->headers[ctx->count];
|
|
|
|
STRUCT_SECTION_FOREACH(http_header_name, header) {
|
|
required_len = hdr_buf->name_len + hdr_buf->value_len + 2;
|
|
|
|
if (hdr_buf->name_len == strlen(header->name) &&
|
|
(strncasecmp(hdr_buf->name, header->name, hdr_buf->name_len) == 0)) {
|
|
if (ctx->count == ARRAY_SIZE(ctx->headers)) {
|
|
LOG_DBG("Header '%s' dropped: not enough slots", header->name);
|
|
ctx->status = HTTP_HEADER_STATUS_DROPPED;
|
|
break;
|
|
}
|
|
|
|
if (remaining < required_len) {
|
|
LOG_DBG("Header '%s' dropped: buffer too small", header->name);
|
|
ctx->status = HTTP_HEADER_STATUS_DROPPED;
|
|
break;
|
|
}
|
|
|
|
/* Copy header name from user-registered header to make HTTP1/HTTP2
|
|
* transparent to the user - they do not need a case-insensitive comparison
|
|
* to check which header was matched.
|
|
*/
|
|
memcpy(dest, header->name, hdr_buf->name_len);
|
|
dest[hdr_buf->name_len] = '\0';
|
|
current_header->name = dest;
|
|
ctx->cursor += (hdr_buf->name_len + 1);
|
|
dest += (hdr_buf->name_len + 1);
|
|
|
|
/* Copy header value */
|
|
memcpy(dest, hdr_buf->value, hdr_buf->value_len);
|
|
dest[hdr_buf->value_len] = '\0';
|
|
current_header->value = dest;
|
|
ctx->cursor += (hdr_buf->value_len + 1);
|
|
|
|
ctx->count++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int process_header(struct http_client_ctx *client,
|
|
struct http_hpack_header_buf *header)
|
|
{
|
|
if (IS_ENABLED(CONFIG_HTTP_SERVER_CAPTURE_HEADERS)) {
|
|
check_user_request_headers_http2(&client->header_capture_ctx, header);
|
|
}
|
|
|
|
if (header->name_len == (sizeof(":method") - 1) &&
|
|
memcmp(header->name, ":method", header->name_len) == 0) {
|
|
const enum http_method supported_methods[] = {
|
|
HTTP_GET, HTTP_DELETE, HTTP_POST, HTTP_PUT, HTTP_PATCH
|
|
};
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(supported_methods); i++) {
|
|
if ((header->value_len ==
|
|
strlen(http_method_str(supported_methods[i]))) &&
|
|
(memcmp(header->value,
|
|
http_method_str(supported_methods[i]),
|
|
header->value_len) == 0)) {
|
|
client->method = supported_methods[i];
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/* Unknown method */
|
|
return -EBADMSG;
|
|
} else if (header->name_len == (sizeof(":path") - 1) &&
|
|
memcmp(header->name, ":path", header->name_len) == 0) {
|
|
if (header->value_len > sizeof(client->url_buffer) - 1) {
|
|
/* URL too long to handle */
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
memcpy(client->url_buffer, header->value, header->value_len);
|
|
client->url_buffer[header->value_len] = '\0';
|
|
} else if (header->name_len == (sizeof("content-type") - 1) &&
|
|
memcmp(header->name, "content-type", header->name_len) == 0) {
|
|
if (header->value_len > sizeof(client->content_type) - 1) {
|
|
/* Content-type too long to handle */
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
memcpy(client->content_type, header->value, header->value_len);
|
|
client->content_type[header->value_len] = '\0';
|
|
} else if (header->name_len == (sizeof("content-length") - 1) &&
|
|
memcmp(header->name, "content-length", header->name_len) == 0) {
|
|
char len_str[16] = { 0 };
|
|
char *endptr;
|
|
unsigned long len;
|
|
|
|
memcpy(len_str, header->value, MIN(sizeof(len_str), header->value_len));
|
|
len_str[sizeof(len_str) - 1] = '\0';
|
|
|
|
len = strtoul(len_str, &endptr, 10);
|
|
if (*endptr != '\0') {
|
|
return -EINVAL;
|
|
}
|
|
|
|
client->content_len = (size_t)len;
|
|
} else {
|
|
/* Just ignore for now. */
|
|
LOG_DBG("Ignoring field %.*s", (int)header->name_len, header->name);
|
|
}
|
|
|
|
out:
|
|
return 0;
|
|
}
|
|
|
|
static int handle_incomplete_http_header(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
size_t extra_len, prev_frame_len;
|
|
int ret;
|
|
|
|
if (client->data_len < frame->length) {
|
|
/* Still did not receive entire frame content */
|
|
return -EAGAIN;
|
|
}
|
|
|
|
if (!client->expect_continuation) {
|
|
/* Failed to parse header field while the frame is complete
|
|
* and no continuation frame is expected - report protocol
|
|
* error.
|
|
*/
|
|
LOG_ERR("Incomplete header field");
|
|
return -EBADMSG;
|
|
}
|
|
|
|
/* A header field can be split between two frames, i. e. header and
|
|
* continuation or two continuation frames. Because of this, the
|
|
* continuation frame header can be present in the stream in between
|
|
* header field data, so in such case we need to check for header here
|
|
* and remove it from the stream to unblock further processing of the
|
|
* header field.
|
|
*/
|
|
prev_frame_len = frame->length;
|
|
extra_len = client->data_len - frame->length;
|
|
ret = parse_http_frame_header(client, client->cursor + prev_frame_len,
|
|
extra_len);
|
|
if (ret < 0) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
/* Continuation frame expected. */
|
|
if (frame->type != HTTP2_CONTINUATION_FRAME) {
|
|
LOG_ERR("Continuation frame expected");
|
|
return -EBADMSG;
|
|
}
|
|
|
|
print_http_frames(client);
|
|
/* Now remove continuation frame header from the stream, and proceed
|
|
* with processing.
|
|
*/
|
|
extra_len -= HTTP2_FRAME_HEADER_SIZE;
|
|
client->data_len -= HTTP2_FRAME_HEADER_SIZE;
|
|
frame->length += prev_frame_len;
|
|
memmove(client->cursor + prev_frame_len,
|
|
client->cursor + prev_frame_len + HTTP2_FRAME_HEADER_SIZE,
|
|
extra_len);
|
|
|
|
return enter_http_frame_continuation_state(client);
|
|
}
|
|
|
|
static int handle_http_frame_headers_end_stream(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http_request_ctx request_ctx;
|
|
struct http_response_ctx response_ctx;
|
|
int ret = 0;
|
|
|
|
if (client->current_stream == NULL) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (client->current_stream->current_detail == NULL) {
|
|
goto out;
|
|
}
|
|
|
|
if (client->current_stream->current_detail->type == HTTP_RESOURCE_TYPE_DYNAMIC) {
|
|
struct http_resource_detail_dynamic *dynamic_detail =
|
|
(struct http_resource_detail_dynamic *)
|
|
client->current_stream->current_detail;
|
|
|
|
memset(&response_ctx, 0, sizeof(response_ctx));
|
|
populate_request_ctx(&request_ctx, NULL, 0, NULL);
|
|
|
|
ret = dynamic_detail->cb(client, HTTP_SERVER_DATA_FINAL, &request_ctx,
|
|
&response_ctx, dynamic_detail->user_data);
|
|
if (ret < 0) {
|
|
dynamic_detail->holder = NULL;
|
|
goto out;
|
|
}
|
|
|
|
/* Force end stream */
|
|
response_ctx.final_chunk = true;
|
|
|
|
ret = http2_dynamic_response(client, frame, &response_ctx, HTTP_SERVER_DATA_FINAL,
|
|
dynamic_detail);
|
|
dynamic_detail->holder = NULL;
|
|
|
|
if (ret < 0) {
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (!client->current_stream->headers_sent) {
|
|
ret = send_headers_frame(client, HTTP_200_OK, frame->stream_identifier,
|
|
client->current_stream->current_detail,
|
|
HTTP2_FLAG_END_STREAM, NULL, 0);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
goto out;
|
|
}
|
|
} else if (!client->current_stream->end_stream_sent) {
|
|
ret = send_data_frame(client, NULL, 0, frame->stream_identifier,
|
|
HTTP2_FLAG_END_STREAM);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot send last frame (%d)", ret);
|
|
}
|
|
}
|
|
|
|
client->current_stream->current_detail = NULL;
|
|
|
|
out:
|
|
release_http_stream_context(client, frame->stream_identifier);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int handle_http_frame_headers(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http_resource_detail *detail;
|
|
int ret, path_len;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_HEADERS");
|
|
|
|
if (is_header_flag_set(frame->flags, HTTP2_FLAG_PADDED)) {
|
|
ret = parse_http_frame_padded_field(client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (is_header_flag_set(frame->flags, HTTP2_FLAG_PRIORITY)) {
|
|
ret = parse_http_frame_priority_field(client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
while (frame->length > 0) {
|
|
struct http_hpack_header_buf *header = &client->header_field;
|
|
size_t datalen = MIN(client->data_len, frame->length);
|
|
|
|
ret = http_hpack_decode_header(client->cursor, datalen, header);
|
|
if (ret <= 0) {
|
|
if (ret == -EAGAIN) {
|
|
ret = handle_incomplete_http_header(client);
|
|
} else if (ret == 0) {
|
|
ret = -EBADMSG;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
if (ret > frame->length) {
|
|
LOG_ERR("Protocol error, frame length exceeded");
|
|
return -EBADMSG;
|
|
}
|
|
|
|
frame->length -= ret;
|
|
client->cursor += ret;
|
|
client->data_len -= ret;
|
|
|
|
LOG_DBG("Parsed header: %.*s %.*s", (int)header->name_len,
|
|
header->name, (int)header->value_len, header->value);
|
|
|
|
ret = process_header(client, header);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (client->expect_continuation) {
|
|
/* More headers to come in the continuation frame. */
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
return 0;
|
|
}
|
|
|
|
detail = get_resource_detail(client->service, client->url_buffer, &path_len, false);
|
|
if (detail != NULL) {
|
|
detail->path_len = path_len;
|
|
|
|
if (detail->type == HTTP_RESOURCE_TYPE_STATIC) {
|
|
ret = handle_http2_static_resource(
|
|
(struct http_resource_detail_static *)detail,
|
|
frame, client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
} else if (detail->type == HTTP_RESOURCE_TYPE_STATIC_FS) {
|
|
ret = handle_http2_static_fs_resource(
|
|
(struct http_resource_detail_static_fs *)detail, frame, client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
} else if (detail->type == HTTP_RESOURCE_TYPE_DYNAMIC) {
|
|
ret = handle_http2_dynamic_resource(
|
|
(struct http_resource_detail_dynamic *)detail,
|
|
frame, client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
ret = send_http2_404(client, frame);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (is_header_flag_set(frame->flags, HTTP2_FLAG_END_STREAM)) {
|
|
ret = handle_http_frame_headers_end_stream(client);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (frame->padding_len > 0) {
|
|
client->server_state = HTTP_SERVER_FRAME_PADDING_STATE;
|
|
} else {
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_priority(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_PRIORITY_STATE");
|
|
|
|
if (frame->length != HTTP2_PRIORITY_FRAME_LEN) {
|
|
return -EBADMSG;
|
|
}
|
|
|
|
if (client->data_len < frame->length) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
/* Priority signalling is deprecated by RFC 9113, however it still
|
|
* should be expected to receive, just drop the bytes.
|
|
*/
|
|
client->data_len -= HTTP2_PRIORITY_FRAME_LEN;
|
|
client->cursor += HTTP2_PRIORITY_FRAME_LEN;
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_rst_stream(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
struct http2_stream_ctx *stream_ctx;
|
|
uint32_t error_code;
|
|
|
|
LOG_DBG("FRAME_RST_STREAM");
|
|
|
|
if (frame->length != HTTP2_RST_STREAM_FRAME_LEN) {
|
|
return -EBADMSG;
|
|
}
|
|
|
|
if (client->data_len < frame->length) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
if (frame->stream_identifier == 0) {
|
|
return -EBADMSG;
|
|
}
|
|
|
|
stream_ctx = find_http_stream_context(client, frame->stream_identifier);
|
|
if (stream_ctx == NULL) {
|
|
return -EBADMSG;
|
|
}
|
|
|
|
error_code = sys_get_be32(client->cursor);
|
|
|
|
LOG_DBG("Stream %u reset with error code %u", stream_ctx->stream_id,
|
|
error_code);
|
|
|
|
release_http_stream_context(client, stream_ctx->stream_id);
|
|
|
|
client->data_len -= HTTP2_RST_STREAM_FRAME_LEN;
|
|
client->cursor += HTTP2_RST_STREAM_FRAME_LEN;
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_settings(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
int bytes_consumed;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_SETTINGS");
|
|
|
|
if (client->data_len < frame->length) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
bytes_consumed = client->current_frame.length;
|
|
client->data_len -= bytes_consumed;
|
|
client->cursor += bytes_consumed;
|
|
|
|
if (!is_header_flag_set(frame->flags, HTTP2_FLAG_SETTINGS_ACK)) {
|
|
int ret;
|
|
|
|
ret = send_settings_frame(client, true);
|
|
if (ret < 0) {
|
|
LOG_DBG("Cannot write to socket (%d)", ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_goaway(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
int bytes_consumed;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_GOAWAY");
|
|
|
|
if (client->data_len < frame->length) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
bytes_consumed = client->current_frame.length;
|
|
client->data_len -= bytes_consumed;
|
|
client->cursor += bytes_consumed;
|
|
|
|
enter_http_done_state(client);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_window_update(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
int bytes_consumed;
|
|
|
|
LOG_DBG("HTTP_SERVER_FRAME_WINDOW_UPDATE");
|
|
|
|
/* TODO Implement flow control, for now just ignore. */
|
|
|
|
if (client->data_len < frame->length) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
bytes_consumed = client->current_frame.length;
|
|
client->data_len -= bytes_consumed;
|
|
client->cursor += bytes_consumed;
|
|
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_continuation(struct http_client_ctx *client)
|
|
{
|
|
LOG_DBG("HTTP_SERVER_FRAME_CONTINUATION_STATE");
|
|
client->server_state = HTTP_SERVER_FRAME_HEADERS_STATE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int handle_http_frame_padding(struct http_client_ctx *client)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
size_t bytes_consumed;
|
|
|
|
if (client->data_len == 0) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
bytes_consumed = MIN(client->data_len, frame->padding_len);
|
|
client->data_len -= bytes_consumed;
|
|
client->cursor += bytes_consumed;
|
|
frame->padding_len -= bytes_consumed;
|
|
|
|
if (frame->padding_len == 0) {
|
|
client->server_state = HTTP_SERVER_FRAME_HEADER_STATE;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
const char *get_frame_type_name(enum http2_frame_type type)
|
|
{
|
|
switch (type) {
|
|
case HTTP2_DATA_FRAME:
|
|
return "DATA";
|
|
case HTTP2_HEADERS_FRAME:
|
|
return "HEADERS";
|
|
case HTTP2_PRIORITY_FRAME:
|
|
return "PRIORITY";
|
|
case HTTP2_RST_STREAM_FRAME:
|
|
return "RST_STREAM";
|
|
case HTTP2_SETTINGS_FRAME:
|
|
return "SETTINGS";
|
|
case HTTP2_PUSH_PROMISE_FRAME:
|
|
return "PUSH_PROMISE";
|
|
case HTTP2_PING_FRAME:
|
|
return "PING";
|
|
case HTTP2_GOAWAY_FRAME:
|
|
return "GOAWAY";
|
|
case HTTP2_WINDOW_UPDATE_FRAME:
|
|
return "WINDOW_UPDATE";
|
|
case HTTP2_CONTINUATION_FRAME:
|
|
return "CONTINUATION";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
int parse_http_frame_header(struct http_client_ctx *client, const uint8_t *buffer,
|
|
size_t buflen)
|
|
{
|
|
struct http2_frame *frame = &client->current_frame;
|
|
|
|
if (buflen < HTTP2_FRAME_HEADER_SIZE) {
|
|
return -EAGAIN;
|
|
}
|
|
|
|
frame->length = sys_get_be24(&buffer[HTTP2_FRAME_LENGTH_OFFSET]);
|
|
frame->type = buffer[HTTP2_FRAME_TYPE_OFFSET];
|
|
frame->flags = buffer[HTTP2_FRAME_FLAGS_OFFSET];
|
|
frame->stream_identifier = sys_get_be32(
|
|
&buffer[HTTP2_FRAME_STREAM_ID_OFFSET]);
|
|
frame->stream_identifier &= HTTP2_FRAME_STREAM_ID_MASK;
|
|
frame->padding_len = 0;
|
|
|
|
LOG_DBG("Frame len %d type 0x%02x flags 0x%02x id %d",
|
|
frame->length, frame->type, frame->flags, frame->stream_identifier);
|
|
|
|
return 0;
|
|
}
|