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:
parent
055800ba76
commit
e55278a87b
9 changed files with 351 additions and 9 deletions
|
@ -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
|
||||
=================
|
||||
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
9
subsys/net/lib/http/iterables.ld
Normal file
9
subsys/net/lib/http/iterables.ld
Normal 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)
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue