1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

gcoap_fileserver: New module to serve VFS via CoAP

This commit is contained in:
chrysn 2020-10-24 00:43:24 +02:00 committed by Benjamin Valentin
parent dc7bc9f854
commit 185dfea07f
5 changed files with 514 additions and 1 deletions

View File

@ -65,6 +65,7 @@ PSEUDOMODULES += evtimer_on_ztimer
PSEUDOMODULES += fatfs_vfs_format
PSEUDOMODULES += fmt_%
PSEUDOMODULES += gcoap_forward_proxy
PSEUDOMODULES += gcoap_fileserver
PSEUDOMODULES += gcoap_dtls
PSEUDOMODULES += fido2_tests
PSEUDOMODULES += gnrc_dhcpv6_%

View File

@ -623,6 +623,12 @@ ifneq (,$(filter l2filter_%,$(USEMODULE)))
USEMODULE += l2filter
endif
ifneq (,$(filter gcoap_fileserver,$(USEMODULE)))
USEMODULE += gcoap
USEMODULE += checksum
USEMODULE += vfs
endif
ifneq (,$(filter gcoap_forward_proxy,$(USEMODULE)))
USEMODULE += gcoap
USEMODULE += uri_parser

View File

@ -627,8 +627,19 @@ extern "C" {
#define GCOAP_DTLS_EXTRA_STACKSIZE (0)
#endif
/**
* @brief Extra stack for VFS operations
*/
#if IS_USED(MODULE_GCOAP_FILESERVER)
#include "vfs.h"
#define GCOAP_VFS_EXTRA_STACKSIZE (VFS_DIR_BUFFER_SIZE + VFS_FILE_BUFFER_SIZE)
#else
#define GCOAP_VFS_EXTRA_STACKSIZE (0)
#endif
#define GCOAP_STACK_SIZE (THREAD_STACKSIZE_DEFAULT + DEBUG_EXTRA_STACKSIZE \
+ sizeof(coap_pkt_t) + GCOAP_DTLS_EXTRA_STACKSIZE)
+ sizeof(coap_pkt_t) + GCOAP_DTLS_EXTRA_STACKSIZE \
+ GCOAP_VFS_EXTRA_STACKSIZE)
#endif
/** @} */

View File

@ -0,0 +1,122 @@
/*
* Copyright (C) 2020 chrysn
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup net_gcoap_fileserver GCoAP file server
* @ingroup net_gcoap
* @brief Library for serving files from the VFS to CoAP clients
*
* # About
*
* This maps files in the local file system onto a resources in CoAP. In that,
* it is what is called a static web server in the unconstrained web.
*
* As usual, GET operations are used to read files<!-- WRITESUPPORT, and PUT writes to files.
* In the current implementation, PUTs are expressed as random-access, meaning
* that files are not updated atomically -->.
*
* Directories are expressed to URIs with trailing slashes<!-- WRITESUPPORT, and are always
* created implicitly when files are PUT under them; they can be DELETEd when
* empty -->.
*
* @note The file server uses ETag for cache validation. The ETags are built
* from the file system stat values. As clients rely on the ETag to differ when
* the file changes, it is important that file modification times are set. The
* precise time values do not matter, but if a file is changed in place and
* neither its length nor its modification time is varied, then clients will
* not become aware of the change or may even mix up the versions half way
* through if they have a part of the old version cached.
*
* # Usage
*
* * ``USEMODULE += gcoap_fileserver``
*
* * Have a @ref gcoap_fileserver_entry_t populated with the path you want to serve,
* and the number of path components to strip from incoming requests:
*
* ```
* static const gcoap_fileserver_entry_t files_sd = {
* .root = "/sd0",
* .resource = "/files/sd"
* };
* ```
*
* * Enter a @ref gcoap_fileserver_handler handler into your CoAP server's
* resource list like this:
*
* ```
* static const coap_resource_t _resources[] = {
* ...
* { "/files/sd", COAP_GET | COAP_MATCH_SUBTREE, gcoap_fileserver_handler, (void*)&files_sd },
* ...
* }
* ```
*
* The allowed methods dictate whether it's read-only (``COAP_GET``) or (in the
* future<!-- WRITESUPPORT -->) read-write (``COAP_GET | COAP_PUT | COAP_DELETE``).
*
* @{
*
* @file
* @brief Resource handler for the CoAP file system server
*
* @author chrysn <chrysn@fsfe.org>
*/
#ifndef NET_GCOAP_FILESERVER_H
#define NET_GCOAP_FILESERVER_H
#ifdef __cplusplus
extern "C" {
#endif
#include "net/nanocoap.h"
/**
* @brief File server starting point
*
* This struct needs to be present at the ctx of a gcoap_fileserver_handler entry
* in a resource list.
*
*/
typedef struct {
/**
* @brief Path in the VFS that should be served.
*
* This does not need a trailing slash.
*/
const char *root;
/**
* @brief The associated CoAP resource path
*/
const char *resource;
} gcoap_fileserver_entry_t;
/**
* @brief File server handler
*
* Serve a directory from the VFS as a CoAP resource tree.
* @see net_gcoap_fileserver
*
* @param[in] pdu CoAP request package
* @param[out] buf Buffer for the response
* @param[in] len Response buffer length
* @param[in] ctx pointer to a @ref gcoap_fileserver_entry_t
*
* @return size of the response on success
* negative error
*/
ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, void *ctx);
#ifdef __cplusplus
}
#endif
#endif /* NET_GCOAP_FILESERVER_H */
/** @} */

View File

@ -0,0 +1,373 @@
/*
* Copyright (C) 2020 chrysn
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @{
*
* @file
* @brief CoAP file server implementation
*
* @author Christian Amsüss <chrysn@fsfe.org>
* @author Benjamin Valentin <benjamin.valentin@ml-pa.com>
*/
#include <assert.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include "checksum/fletcher32.h"
#include "net/gcoap/fileserver.h"
#include "net/gcoap.h"
#include "vfs.h"
#define ENABLE_DEBUG 0
#include "debug.h"
/** Maximum length of an expressible path, including the trailing 0 character. */
#define COAPFILESERVER_PATH_MAX (64)
/** Data extracted from a request on a file */
struct requestdata {
/** 0-terminated expanded file name in the VFS */
char namebuf[COAPFILESERVER_PATH_MAX];
uint32_t blocknum2;
unsigned int szx2; /* would prefer uint8_t but that's what coap_get_blockopt gives */
uint32_t etag;
bool etag_sent;
};
/**
* @brief Return true if path/name is a directory.
*/
static bool entry_is_dir(char *path, const char *name)
{
struct stat stat;
size_t path_len = strlen(path);
size_t name_len = strlen(name);
if (path_len + name_len + 1 >= COAPFILESERVER_PATH_MAX) {
return false;
}
/* re-use path buffer, it is already COAPFILESERVER_PATH_MAX bytes long */
path[path_len] = '/';
path[path_len + 1] = 0;
strncat(path, name, COAPFILESERVER_PATH_MAX - 1);
if (vfs_stat(path, &stat)) {
DEBUG("vfs_stat(%s) failed\n", path);
}
path[path_len] = 0;
return (stat.st_mode & S_IFMT) == S_IFDIR;
}
static unsigned _count_char(const char *s, char c)
{
unsigned count = 0;
while (*s) {
if (*s == c) {
++count;
}
++s;
}
return count;
}
/** Build an ETag based on the given file's VFS stat. If the stat fails,
* returns the error and leaves etag in any state; otherwise there's an etag
* in the stattag's field */
static int stat_etag(const char *filename, uint32_t *etag)
{
struct stat stat;
int err = vfs_stat(filename, &stat);
if (err < 0) {
return err;
}
/* Normalizing fields whose value can change without affecting the ETag */
stat.st_nlink = 0;
#if defined(CPU_ESP32) || defined(CPU_ESP8266) || defined(CPU_MIPS_PIC32MX) || defined(CPU_MIPS_PIC32MZ)
memset(&stat.st_atime, 0, sizeof(stat.st_atime));
#else
memset(&stat.st_atim, 0, sizeof(stat.st_atim));
#endif
*etag = fletcher32((void *)&stat, sizeof(stat) / 2);
return 0;
}
/** Create a CoAP response for a given errno (eg. EACCESS -> 4.03 Forbidden
* etc., defaulting to 5.03 Internal Server Error) */
static size_t gcoap_fileserver_errno_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, int err)
{
uint8_t code;
switch (err) {
case -EACCES:
code = COAP_CODE_FORBIDDEN;
break;
case -ENOENT:
code = COAP_CODE_PATH_NOT_FOUND;
break;
default:
code = COAP_CODE_INTERNAL_SERVER_ERROR;
};
DEBUG("gcoap_fileserver: Rejecting error %d (%s) as %d.%02d\n", err, strerror(err),
code >> 5, code & 0x1f);
return gcoap_response(pdu, buf, len, code);
}
static void _calc_szx2(coap_pkt_t *pdu, size_t reserve, struct requestdata *request)
{
assert(pdu->payload_len > reserve);
size_t remaining_length = pdu->payload_len - reserve;
/* > 0: To not wrap around; if that still won't fit that's later caught in
* an assertion */
while ((coap_szx2size(request->szx2) > remaining_length) && (request->szx2 > 0)) {
request->szx2--;
request->blocknum2 <<= 1;
}
}
static ssize_t gcoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request)
{
uint32_t etag;
int err = stat_etag(request->namebuf, &etag);
if (err < 0) {
return gcoap_fileserver_errno_handler(pdu, buf, len, err);
}
if (request->etag_sent && etag == request->etag) {
gcoap_resp_init(pdu, buf, len, COAP_CODE_VALID);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag));
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
int fd = vfs_open(request->namebuf, O_RDONLY, 0);
if (fd < 0) {
return gcoap_fileserver_errno_handler(pdu, buf, len, fd);
}
gcoap_resp_init(pdu, buf, len, COAP_CODE_CONTENT);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag));
coap_block_slicer_t slicer;
_calc_szx2(pdu,
5 + 1 + 1 /* reserve BLOCK2 size + payload marker + more */,
request);
coap_block_slicer_init(&slicer, request->blocknum2, coap_szx2size(request->szx2));
coap_opt_add_block2(pdu, &slicer, true);
size_t resp_len = coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD);
err = vfs_lseek(fd, slicer.start, SEEK_SET);
if (err < 0) {
goto late_err;
}
/* That'd only happen if the buffer is too small for even a 16-byte block,
* or if the above calculations were wrong.
*
* Not using payload_len here as that's needlessly underestimating the
* space by CONFIG_GCOAP_RESP_OPTIONS_BUF
* */
assert(pdu->payload + slicer.end - slicer.start <= buf + len);
bool more = 1;
int read = vfs_read(fd, pdu->payload, slicer.end - slicer.start + more);
if (read < 0) {
goto late_err;
}
more = (unsigned)read > slicer.end - slicer.start;
read -= more;
vfs_close(fd);
slicer.cur = slicer.end + more;
coap_block2_finish(&slicer);
if (read == 0) {
/* Rewind to clear payload marker */
read -= 1;
}
return resp_len + read;
late_err:
vfs_close(fd);
coap_hdr_set_code(pdu->hdr, COAP_CODE_INTERNAL_SERVER_ERROR);
return coap_get_total_hdr_len(pdu);
}
static ssize_t gcoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
struct requestdata *request,
gcoap_fileserver_entry_t *resource)
{
vfs_DIR dir;
coap_block_slicer_t slicer;
int err = vfs_opendir(&dir, request->namebuf);
if (err != 0) {
return gcoap_fileserver_errno_handler(pdu, buf, len, err);
}
DEBUG("gcoap_fileserver: Serving directory listing\n");
gcoap_resp_init(pdu, buf, len, COAP_CODE_CONTENT);
coap_opt_add_format(pdu, COAP_FORMAT_LINK);
_calc_szx2(pdu,
5 + 1 /* reserve BLOCK2 size + payload marker */,
request);
coap_block_slicer_init(&slicer, request->blocknum2, coap_szx2size(request->szx2));
coap_opt_add_block2(pdu, &slicer, true);
buf += coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD);
size_t root_len = resource->root ? strlen(resource->root) : 0;
const char *root_dir = &request->namebuf[root_len];
const char *resource_dir = resource->resource;
size_t root_dir_len = strlen(root_dir);
size_t resource_dir_len = strlen(resource_dir);
vfs_dirent_t entry;
while (vfs_readdir(&dir, &entry) > 0) {
const char *entry_name = entry.d_name;
size_t entry_len = strlen(entry_name);
if (entry_len <= 2 && memcmp(entry_name, "..", entry_len) == 0) {
/* Up pointers don't work the same way in URI semantics */
continue;
}
bool is_dir = entry_is_dir(request->namebuf, entry_name);
if (slicer.cur) {
buf += coap_blockwise_put_char(&slicer, buf, ',');
}
buf += coap_blockwise_put_char(&slicer, buf, '<');
buf += coap_blockwise_put_bytes(&slicer, buf, resource_dir, resource_dir_len);
buf += coap_blockwise_put_bytes(&slicer, buf, root_dir, root_dir_len);
buf += coap_blockwise_put_char(&slicer, buf, '/');
buf += coap_blockwise_put_bytes(&slicer, buf, entry_name, entry_len);
if (is_dir) {
buf += coap_blockwise_put_char(&slicer, buf, '/');
}
buf += coap_blockwise_put_char(&slicer, buf, '>');
}
vfs_closedir(&dir);
coap_block2_finish(&slicer);
return (uintptr_t)buf - (uintptr_t)pdu->hdr;
}
ssize_t gcoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, void *ctx) {
gcoap_fileserver_entry_t *entry = ctx;
struct requestdata request = {
.etag_sent = false,
.blocknum2 = 0,
.szx2 = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX,
};
/** Index in request.namebuf. Must not point at the last entry as that will be
* zeroed to get a 0-terminated string. */
size_t namelength = 0;
uint8_t errorcode = COAP_CODE_INTERNAL_SERVER_ERROR;
uint8_t strip_remaining = _count_char(entry->resource, '/');
/* If a root directory for the server was specified, use that */
if (entry->root && strlen(entry->root) > 1) {
strncpy(request.namebuf, entry->root, sizeof(request.namebuf));
namelength = strlen(entry->root);
}
bool is_directory = true; /* either no path component at all or trailing '/' */
coap_optpos_t opt = {
.offset = coap_get_total_hdr_len(pdu),
};
uint8_t *value;
ssize_t optlen;
while ((optlen = coap_opt_get_next(pdu, &opt, &value, 0)) != -ENOENT) {
if (optlen < 0) {
errorcode = COAP_CODE_BAD_REQUEST;
goto error;
}
switch (opt.opt_num) {
case COAP_OPT_URI_PATH:
if (strip_remaining != 0) {
strip_remaining -= 1;
continue;
}
if ((is_directory = (optlen == 0))) { /* '/' */
continue;
}
if (memchr(value, '\0', optlen) != NULL ||
memchr(value, '/', optlen) != NULL) {
/* Path can not be expressed in the file system */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
size_t newlength = namelength + 1 + optlen;
if (newlength > sizeof(request.namebuf) - 1) {
/* Path too long, therefore can't exist in this mapping */
errorcode = COAP_CODE_PATH_NOT_FOUND;
goto error;
}
request.namebuf[namelength] = '/';
memcpy(&request.namebuf[namelength] + 1, value, optlen);
namelength = newlength;
break;
case COAP_OPT_ETAG:
if (optlen != sizeof(request.etag)) {
/* Can't be a matching tag, no use in carrying that */
continue;
}
if (request.etag_sent) {
/* We can reasonably only check for a limited sized set,
* and it size is 1 here (sending multiple ETags is
* possible but rare) */
continue;
}
request.etag_sent = true;
memcpy(&request.etag, value, sizeof(request.etag));
break;
case COAP_OPT_BLOCK2:
/* Could be more efficient now that we already know where it
* is, but meh */
coap_get_blockopt(pdu, COAP_OPT_BLOCK2, &request.blocknum2, &request.szx2);
break;
default:
if (opt.opt_num & 1) {
errorcode = COAP_CODE_BAD_REQUEST;
goto error;
}
else {
/* Ignoring elective option */
}
}
}
request.namebuf[namelength] = '\0';
DEBUG("request: '%s'\n", request.namebuf);
/* Note to self: As we parse more options than just Uri-Path, we'll likely
* pass a struct pointer later. So far, those could even be hooked into the
* resource list, but that'll go away once we parse more options */
if (is_directory) {
return gcoap_fileserver_directory_handler(pdu, buf, len, &request, entry);
}
else {
return gcoap_fileserver_file_handler(pdu, buf, len, &request);
}
error:
return gcoap_response(pdu, buf, len, errorcode);
}
/** @} */