net: lib: http_server: add static fs resource

adds filesystem as a resource for the http_server which serves static
(gzipped) files from a filesystem to the client.

Signed-off-by: Gerhard Jörges <joerges@metratec.com>
This commit is contained in:
Gerhard Jörges 2024-07-26 10:54:42 +02:00 committed by Carles Cufí
commit e55278a87b
9 changed files with 351 additions and 9 deletions

View file

@ -187,6 +187,38 @@ following code to the application's ``CMakeLists.txt`` file:
where ``src/index.html`` is the location of the webpage to be compressed.
Static filesystem resources
===========================
Static filesystem resource content is defined build-time and is immutable. The following
example shows how the path can be defined as a static resource in the application:
.. code-block:: c
struct http_resource_detail_static_fs static_fs_resource_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_STATIC_FS,
.bitmask_of_supported_http_methods = BIT(HTTP_GET),
},
.fs_path = "/lfs1/www",
};
HTTP_RESOURCE_DEFINE(static_fs_resource, my_service, "*", &static_fs_resource_detail);
All files located in /lfs1/www are made available to the client. If a file is
gzipped, .gz must be appended to the file name (e.g. index.html.gz), then the
server delivers index.html.gz when the client requests index.html and adds gzip
content-encoding to the HTTP header.
The content type is evaluated based on the file extension. The server supports
.html, .js, .css, .jpg, .png and .svg. More content types can be provided with the
:c:macro:`HTTP_SERVER_CONTENT_TYPE` macro. All other files are provided with the
content type text/html.
.. code-block:: c
HTTP_SERVER_CONTENT_TYPE(json, "application/json")
Dynamic resources
=================

View file

@ -61,6 +61,9 @@ enum http_resource_type {
/** Static resource, cannot be modified on runtime. */
HTTP_RESOURCE_TYPE_STATIC,
/** serves static gzipped files from a filesystem */
HTTP_RESOURCE_TYPE_STATIC_FS,
/** Dynamic resource, server interacts with the application via registered
* @ref http_resource_dynamic_cb_t.
*/
@ -117,6 +120,37 @@ struct http_resource_detail_static {
BUILD_ASSERT(offsetof(struct http_resource_detail_static, common) == 0);
/** @endcond */
/**
* @brief Representation of a static filesystem server resource.
*/
struct http_resource_detail_static_fs {
/** Common resource details. */
struct http_resource_detail common;
/** Path in the local filesystem */
const char *fs_path;
};
/** @cond INTERNAL_HIDDEN */
/* Make sure that the common is the first in the struct. */
BUILD_ASSERT(offsetof(struct http_resource_detail_static_fs, common) == 0);
/** @endcond */
struct http_content_type {
const char *extension;
size_t extension_len;
const char *content_type;
};
#define HTTP_SERVER_CONTENT_TYPE(_extension, _content_type) \
const STRUCT_SECTION_ITERABLE(http_content_type, _extension) = { \
.extension = STRINGIFY(_extension), \
.extension_len = sizeof(STRINGIFY(_extension)) - 1, \
.content_type = _content_type, \
};
#define HTTP_SERVER_CONTENT_TYPE_FOREACH(_it) STRUCT_SECTION_FOREACH(http_content_type, _it)
struct http_client_ctx;
/** Indicates the status of the currently processed piece of data. */

View file

@ -20,3 +20,7 @@ if(CONFIG_HTTP_SERVER AND CONFIG_WEBSOCKET)
zephyr_library_sources(http_server_ws.c)
zephyr_library_link_libraries_ifdef(CONFIG_MBEDTLS mbedTLS)
endif()
if(CONFIG_HTTP_SERVER AND CONFIG_FILE_SYSTEM)
zephyr_linker_sources(SECTIONS iterables.ld)
endif()

View file

@ -39,6 +39,9 @@ int enter_http_done_state(struct http_client_ctx *client);
/* Others */
struct http_resource_detail *get_resource_detail(const char *path, int *len, bool is_ws);
int http_server_sendall(struct http_client_ctx *client, const void *buf, size_t len);
void http_server_get_content_type_from_extension(char *url, char *content_type,
size_t content_type_size);
int http_server_find_file(char *fname, size_t fname_size, size_t *file_size, bool *gzipped);
void http_client_timer_restart(struct http_client_ctx *client);
/* TODO Could be static, but currently used in tests. */

View file

@ -58,6 +58,13 @@ static bool server_running;
static void close_client_connection(struct http_client_ctx *client);
HTTP_SERVER_CONTENT_TYPE(html, "text/html")
HTTP_SERVER_CONTENT_TYPE(css, "text/css")
HTTP_SERVER_CONTENT_TYPE(js, "text/javascript")
HTTP_SERVER_CONTENT_TYPE(jpg, "image/jpeg")
HTTP_SERVER_CONTENT_TYPE(png, "image/png")
HTTP_SERVER_CONTENT_TYPE(svg, "image/svg+xml")
int http_server_init(struct http_server_ctx *ctx)
{
int proto;
@ -683,7 +690,8 @@ struct http_resource_detail *get_resource_detail(const char *path,
if (IS_ENABLED(CONFIG_HTTP_SERVER_RESOURCE_WILDCARD)) {
int ret;
ret = fnmatch(resource->resource, path, FNM_PATHNAME);
ret = fnmatch(resource->resource, path,
(FNM_PATHNAME | FNM_LEADING_DIR));
if (ret == 0) {
*path_len = strlen(resource->resource);
return resource->detail;
@ -704,6 +712,43 @@ struct http_resource_detail *get_resource_detail(const char *path,
return NULL;
}
int http_server_find_file(char *fname, size_t fname_size, size_t *file_size, bool *gzipped)
{
struct fs_dirent dirent;
size_t len;
int ret;
ret = fs_stat(fname, &dirent);
if (ret < 0) {
len = strlen(fname);
snprintk(fname + len, fname_size - len, ".gz");
ret = fs_stat(fname, &dirent);
*gzipped = (ret == 0);
}
if (ret == 0) {
*file_size = dirent.size;
return ret;
}
return -ENOENT;
}
void http_server_get_content_type_from_extension(char *url, char *content_type,
size_t content_type_size)
{
size_t url_len = strlen(url);
HTTP_SERVER_CONTENT_TYPE_FOREACH(ct) {
char *ext = &url[url_len - ct->extension_len];
if (strncmp(ext, ct->extension, ct->extension_len) == 0) {
strncpy(content_type, ct->content_type, content_type_size);
return;
}
}
}
int http_server_sendall(struct http_client_ctx *client, const void *buf, size_t len)
{
while (len) {

View file

@ -15,6 +15,7 @@
#include <string.h>
#include <strings.h>
#include <zephyr/fs/fs.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/http/service.h>
@ -25,6 +26,14 @@ LOG_MODULE_DECLARE(net_http_server, CONFIG_NET_HTTP_SERVER_LOG_LEVEL);
#define TEMP_BUF_LEN 64
static const char not_found_response[] = "HTTP/1.1 404 Not Found\r\n"
"Content-Length: 9\r\n\r\n"
"Not Found";
static const char not_allowed_response[] = "HTTP/1.1 405 Method Not Allowed\r\n"
"Content-Length: 18\r\n\r\n"
"Method Not Allowed";
static const char conflict_response[] = "HTTP/1.1 409 Conflict\r\n\r\n";
static const char final_chunk[] = "0\r\n\r\n";
static const char *crlf = &final_chunk[3];
@ -256,6 +265,101 @@ static int dynamic_post_req(struct http_resource_detail_dynamic *dynamic_detail,
return 0;
}
#if defined(CONFIG_FILE_SYSTEM)
int handle_http1_static_fs_resource(struct http_resource_detail_static_fs *static_fs_detail,
struct http_client_ctx *client)
{
#define RESPONSE_TEMPLATE_STATIC_FS \
"HTTP/1.1 200 OK\r\n" \
"Content-Type: %s%s\r\n\r\n"
#define CONTENT_ENCODING_GZIP "\r\nContent-Encoding: gzip"
bool gzipped = false;
int len;
int remaining;
int ret;
size_t file_size;
struct fs_file_t file;
char fname[HTTP_SERVER_MAX_URL_LENGTH];
char content_type[HTTP_SERVER_MAX_CONTENT_TYPE_LEN] = "text/html";
/* Add couple of bytes to response template size to have space
* for the content type and encoding
*/
char http_response[sizeof(RESPONSE_TEMPLATE_STATIC_FS) + HTTP_SERVER_MAX_CONTENT_TYPE_LEN +
sizeof(CONTENT_ENCODING_GZIP)];
if (!(static_fs_detail->common.bitmask_of_supported_http_methods & BIT(HTTP_GET))) {
ret = http_server_sendall(client, not_allowed_response,
sizeof(not_allowed_response) - 1);
if (ret < 0) {
LOG_DBG("Cannot write to socket (%d)", ret);
}
return ret;
}
/* 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), &file_size, &gzipped);
if (ret < 0) {
LOG_ERR("fs_stat %s: %d", fname, ret);
ret = http_server_sendall(client, not_found_response,
sizeof(not_found_response) - 1);
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;
}
}
LOG_DBG("found %s, file size: %d", fname, file_size);
/* send HTTP header */
len = snprintk(http_response, sizeof(http_response), RESPONSE_TEMPLATE_STATIC_FS,
content_type, gzipped ? CONTENT_ENCODING_GZIP : "");
ret = http_server_sendall(client, http_response, len);
if (ret < 0) {
goto close;
}
/* read and send file */
remaining = file_size;
while (remaining > 0) {
len = fs_read(&file, http_response, sizeof(http_response));
ret = http_server_sendall(client, http_response, len);
if (ret < 0) {
goto close;
}
remaining -= len;
}
ret = http_server_sendall(client, "\r\n\r\n", 4);
close:
/* close file */
fs_close(&file);
return ret;
}
#endif
static int handle_http1_dynamic_resource(
struct http_resource_detail_dynamic *dynamic_detail,
struct http_client_ctx *client)
@ -274,9 +378,6 @@ static int handle_http1_dynamic_resource(
}
if (dynamic_detail->holder != NULL && dynamic_detail->holder != client) {
static const char conflict_response[] =
"HTTP/1.1 409 Conflict\r\n\r\n";
ret = http_server_sendall(client, conflict_response,
sizeof(conflict_response) - 1);
if (ret < 0) {
@ -606,6 +707,14 @@ upgrade_not_found:
if (ret < 0) {
return ret;
}
#if defined(CONFIG_FILE_SYSTEM)
} else if (detail->type == HTTP_RESOURCE_TYPE_STATIC_FS) {
ret = handle_http1_static_fs_resource(
(struct http_resource_detail_static_fs *)detail, client);
if (ret < 0) {
return ret;
}
#endif
} else if (detail->type == HTTP_RESOURCE_TYPE_DYNAMIC) {
ret = handle_http1_dynamic_resource(
(struct http_resource_detail_dynamic *)detail,
@ -616,11 +725,6 @@ upgrade_not_found:
}
} else {
not_found: ; /* Add extra semicolon to make clang to compile when using label */
static const char not_found_response[] =
"HTTP/1.1 404 Not Found\r\n"
"Content-Length: 9\r\n\r\n"
"Not Found";
ret = http_server_sendall(client, not_found_response,
sizeof(not_found_response) - 1);
if (ret < 0) {

View file

@ -371,6 +371,102 @@ 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);
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);
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));
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 dynamic_get_req_v2(struct http_resource_detail_dynamic *dynamic_detail,
struct http_client_ctx *client)
{
@ -869,6 +965,12 @@ int handle_http1_to_http2_upgrade(struct http_client_ctx *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,
@ -1290,6 +1392,12 @@ int handle_http_frame_headers(struct http_client_ctx *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,

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 metraTec GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/linker/iterable_sections.h>
ITERABLE_SECTION_ROM(http_content_type, 4)

View file

@ -41,6 +41,9 @@ static int cmd_net_http(const struct shell *sh, size_t argc, char *argv[])
case HTTP_RESOURCE_TYPE_STATIC:
detail_type = "static";
break;
case HTTP_RESOURCE_TYPE_STATIC_FS:
detail_type = "static_fs";
break;
case HTTP_RESOURCE_TYPE_DYNAMIC:
detail_type = "dynamic";
break;