mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2025-12-16 01:53:51 +01:00
This changes the API of nanocoap with the goal to reduce the expose of
UDP specifics in the API. The plan is to eventually support transports
such as CoAP over TCP and CoAP over WebSocket directly in nanocoap
while sharing most of the code, as e.g. the CoAP Option processing
remains identical. Specifically, the plan is to unlock a transport with
modules and introduce overhead for dispatching to specific transport
only when multiple transports are actually in use.
Support for OSCORE directly in nanocoap is probably not sensible, as
the serialization is very much unlike the other transports. A unified
CoAP API for multiple transports including OSCORE is probably best
implemented on top. But when limited to the boring set of CoAP
transports, we probably can support them well with nanocoap with less
overhead.
Breaking API Changes:
=====================
- `coap_parse()` now returns `ssize_t` instead of `int`
- This function is not really user facing, so the impact should
be limited
- This is useful for stream transports where the buffer may
contain data of more than one packet. The return value contains
the number of bytes actually consumed, which will match the
buffer size for non-stream transports.
API Changes:
============
- `coap_pkt_t` now contains a `uint8_t *buf` pointer instead of a
`coap_hdr_t *hdr` pointer to the beginning of the buffer
- This will also work when the buffer is used by non-UDP
transports
- A deprecated `coap_udp_hdr_t *hdr` has been crammed into
an unnamed `union` with `uint8_t *buf`. For architectures
where pointers have the same memory layout regardless of type
(e.g. all of the supported ones), this will make `hdr` an
alias for `buf`.
- The alias will only be provided if no transport besides UDP is
used in nanocoap. So existing apps will continue to work, new
apps that want to support other transports need to move to
adapt.
- `coap_hdr_t` has been renamed to `coap_udp_hdr_t`
- A deprecated alias was created for deprecation
- `coap_hdr*()` functions have been deprecated
- Equivalent `coap_pkt*()` functions have been created that work
on `coap_pkt_t *` instead of `coap_hdr_t *`
- If non-UDP transports are used, the deprecated `coap_hdr*()`
will probably not be exposed to avoid footguns.
- `coap_build_hdr()` has been renamed to `coap_build_udp_hdr()` and
that works on an `uint8_t *` buffer with a given length, rather than
on a `coap_hdr_t *` with a *figers crossed* length
- a deprecated `coap_build_hdr()` function was added that calls
to `coap_build_udp_hdr()` and has the same signature, so that
users have time to update
753 lines
26 KiB
C
753 lines
26 KiB
C
/*
|
|
* 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/nanocoap/fileserver.h"
|
|
#include "vfs.h"
|
|
|
|
#if MODULE_VFS_UTIL
|
|
# include "vfs_util.h"
|
|
#endif
|
|
|
|
#define ENABLE_DEBUG 0
|
|
#include "debug.h"
|
|
|
|
/** Maximum length of an expressible path, including the trailing 0 character. */
|
|
#define COAPFILESERVER_PATH_MAX (64)
|
|
|
|
/**
|
|
* @brief fileserver event callback, only used with `nanocoap_fileserver_callback`
|
|
*/
|
|
static nanocoap_fileserver_event_handler_t _event_cb;
|
|
|
|
/**
|
|
* @brief fileserver event callback context, only used with `nanocoap_fileserver_callback`
|
|
*/
|
|
static void *_event_ctx;
|
|
|
|
/**
|
|
* @brief fileserver event mutex, protects event cb and event ctx from concurrent access
|
|
*/
|
|
static mutex_t _event_mtx;
|
|
|
|
/**
|
|
* @brief Structure holding information about present options in a request
|
|
*/
|
|
struct requestoptions {
|
|
uint32_t etag; /**< Etag value in an Etag option */
|
|
uint32_t if_match; /**< Etag value in an If-Match option */
|
|
char if_match_len; /**< Length of the Etag in an If-Match option */
|
|
struct {
|
|
bool if_match :1; /**< Request carries an If-Match option */
|
|
bool if_none_match :1; /**< Request carries an If-None-Match option */
|
|
bool etag :1; /**< Request carries an Etag option */
|
|
bool block2 :1; /**< Request carries a Block2 option */
|
|
bool block1 :1; /**< Request carries a Block1 option */
|
|
bool size2 :1; /**< Request carries a Size2 option */
|
|
} exists; /**< Structure holding flags of present request options */
|
|
};
|
|
|
|
/** Data extracted from a request on a file */
|
|
struct requestdata {
|
|
/** 0-terminated expanded file name in the VFS */
|
|
char namebuf[COAPFILESERVER_PATH_MAX];
|
|
struct requestoptions options;
|
|
};
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/* reuse 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;
|
|
}
|
|
|
|
static int _resp_init(coap_pkt_t *pdu, uint8_t *buf, size_t len, unsigned code)
|
|
{
|
|
int header_len = coap_build_reply(pdu, code, buf, len, 0);
|
|
|
|
/* request contained no-response option or not enough space for response */
|
|
if (header_len <= 0) {
|
|
return -1;
|
|
}
|
|
|
|
pdu->options_len = 0;
|
|
pdu->payload = buf + header_len;
|
|
pdu->payload_len = len - header_len;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** 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 void stat_etag(struct stat *stat, uint32_t *etag)
|
|
{
|
|
/* Normalizing fields whose value can change without affecting the ETag */
|
|
stat->st_nlink = 0;
|
|
#if defined(CPU_ESP32) || defined(CPU_ESP8266)
|
|
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);
|
|
}
|
|
|
|
/** Create a CoAP response for a given errno (eg. EACCESS -> 4.03 Forbidden
|
|
* etc., defaulting to 5.03 Internal Server Error), or interpret a positive
|
|
* value for err as a CoAP response code */
|
|
static size_t _error_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, int err)
|
|
{
|
|
uint8_t code = err;
|
|
if (err < 0) {
|
|
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("nanocoap_fileserver: Rejecting error %d (%s) as %d.%02d\n", err, strerror(err),
|
|
code >> 5, code & 0x1f);
|
|
}
|
|
|
|
if (_resp_init(pdu, buf, len, code)) {
|
|
return -1;
|
|
}
|
|
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
}
|
|
|
|
static void _calc_szx2(coap_pkt_t *pdu, size_t reserve, coap_block1_t *block2)
|
|
{
|
|
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(block2->szx) > remaining_length) && (block2->szx > 0)) {
|
|
block2->szx--;
|
|
block2->blknum <<= 1;
|
|
}
|
|
}
|
|
|
|
static inline void _event_file(nanocoap_fileserver_event_t event, struct requestdata *request)
|
|
{
|
|
if (!IS_USED(MODULE_NANOCOAP_FILESERVER_CALLBACK)) {
|
|
return;
|
|
}
|
|
|
|
mutex_lock(&_event_mtx);
|
|
nanocoap_fileserver_event_ctx_t ctx = {
|
|
.path = request->namebuf,
|
|
.user_ctx = _event_ctx,
|
|
};
|
|
|
|
nanocoap_fileserver_event_handler_t cb = _event_cb;
|
|
mutex_unlock(&_event_mtx);
|
|
|
|
if (cb) {
|
|
cb(event, &ctx);
|
|
}
|
|
}
|
|
|
|
static ssize_t _get_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
int err;
|
|
uint32_t etag, size_total;
|
|
|
|
coap_block1_t block2 = { .szx = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX };
|
|
{
|
|
struct stat stat;
|
|
if ((err = vfs_stat(request->namebuf, &stat)) < 0) {
|
|
return _error_handler(pdu, buf, len, err);
|
|
}
|
|
size_total = stat.st_size;
|
|
stat_etag(&stat, &etag);
|
|
}
|
|
if (request->options.exists.block2 && !coap_get_block2(pdu, &block2)) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_BAD_OPTION);
|
|
}
|
|
if (request->options.exists.if_match &&
|
|
memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
if (request->options.exists.etag &&
|
|
!memcmp(&etag, &request->options.etag, sizeof(etag))) {
|
|
_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 _error_handler(pdu, buf, len, fd);
|
|
}
|
|
|
|
_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 */,
|
|
&block2);
|
|
coap_block_slicer_init(&slicer, block2.blknum, coap_szx2size(block2.szx));
|
|
coap_opt_add_block2(pdu, &slicer, true);
|
|
|
|
if (request->options.exists.block2) {
|
|
coap_opt_add_uint(pdu, COAP_OPT_SIZE2, size_total);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (block2.blknum == 0) {
|
|
_event_file(NANOCOAP_FILESERVER_GET_FILE_START, request);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
if (!more) {
|
|
_event_file(NANOCOAP_FILESERVER_GET_FILE_END, request);
|
|
}
|
|
|
|
return resp_len + read;
|
|
|
|
late_err:
|
|
vfs_close(fd);
|
|
coap_pkt_set_code(pdu, COAP_CODE_INTERNAL_SERVER_ERROR);
|
|
return coap_get_total_hdr_len(pdu);
|
|
}
|
|
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_PUT)
|
|
static ssize_t _put_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
int ret, fd;
|
|
uint32_t etag;
|
|
struct stat stat;
|
|
coap_block1_t block1 = {0};
|
|
bool create = (vfs_stat(request->namebuf, &stat) == -ENOENT);
|
|
if (create) {
|
|
/* While a file 'f' is initially being created,
|
|
we save the partial content in '.f' and rename it afterwards */
|
|
if (!(ret = strlen(request->namebuf)) || (unsigned)ret >= sizeof(request->namebuf) - 1) {
|
|
/* need one more char '.' */
|
|
return _error_handler(pdu, buf, len, -ENOBUFS);
|
|
}
|
|
char *file = strrchr(request->namebuf, '/');
|
|
memmove(file + 2, file + 1, strlen(file + 1));
|
|
*(file + 1) = '.';
|
|
}
|
|
if (request->options.exists.block1 && !coap_get_block1(pdu, &block1)) {
|
|
ret = COAP_CODE_BAD_OPTION;
|
|
goto unlink_on_error;
|
|
}
|
|
if (block1.blknum == 0) {
|
|
/* "If-None-Match only works correctly on Block1 requests with (NUM=0)
|
|
and MUST NOT be used on Block1 requests with NUM != 0." */
|
|
if (request->options.exists.if_none_match && !create) {
|
|
ret = COAP_CODE_PRECONDITION_FAILED;
|
|
goto unlink_on_error;
|
|
}
|
|
|
|
_event_file(NANOCOAP_FILESERVER_PUT_FILE_START, request);
|
|
}
|
|
if (request->options.exists.if_match) {
|
|
stat_etag(&stat, &etag); /* Etag before write */
|
|
if (create || memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
|
|
ret = COAP_CODE_PRECONDITION_FAILED;
|
|
goto unlink_on_error;
|
|
}
|
|
}
|
|
ret = O_WRONLY;
|
|
ret |= (create ? O_CREAT | O_APPEND : 0);
|
|
if ((fd = vfs_open(request->namebuf, ret, 0666)) < 0) {
|
|
ret = fd;
|
|
goto unlink_on_error;
|
|
}
|
|
if (create) {
|
|
if ((ret = vfs_lseek(fd, 0, SEEK_END)) < 0) {
|
|
goto close_on_error;
|
|
}
|
|
if (block1.offset < (unsigned)ret) {
|
|
/* ignore duplicate packet */
|
|
create = false; /* don't delete file */
|
|
ret = COAP_CODE_CONTINUE;
|
|
goto close_on_error;
|
|
}
|
|
if (block1.offset > (unsigned)ret) {
|
|
/* expect block to be in the correct order during initial creation */
|
|
ret = COAP_CODE_REQUEST_ENTITY_INCOMPLETE;
|
|
goto close_on_error;
|
|
}
|
|
}
|
|
else {
|
|
if (block1.offset > (unsigned)stat.st_size) {
|
|
/* after initial file creation, random access is fine,
|
|
as long as it does not result in holes in the file */
|
|
ret = COAP_CODE_REQUEST_ENTITY_INCOMPLETE;
|
|
goto close_on_error;
|
|
}
|
|
if ((ret = vfs_lseek(fd, block1.offset, SEEK_SET)) < 0) {
|
|
goto close_on_error;
|
|
}
|
|
}
|
|
if ((ret = vfs_write(fd, pdu->payload, pdu->payload_len)) < 0 ||
|
|
(unsigned)ret != pdu->payload_len) {
|
|
goto close_on_error;
|
|
}
|
|
vfs_close(fd);
|
|
if (!block1.more) {
|
|
if ((ret = vfs_stat(request->namebuf, &stat)) < 0) {
|
|
goto unlink_on_error;
|
|
}
|
|
if (create) {
|
|
char path[COAPFILESERVER_PATH_MAX];
|
|
strcpy(path, request->namebuf);
|
|
char *file = strrchr(path, '/');
|
|
memmove(file + 1, file + 2, strlen(file + 2) + 1);
|
|
if ((ret = vfs_rename(request->namebuf, path)) < 0) {
|
|
goto unlink_on_error;
|
|
}
|
|
}
|
|
|
|
_event_file(NANOCOAP_FILESERVER_PUT_FILE_END, request);
|
|
|
|
stat_etag(&stat, &etag); /* Etag after write */
|
|
_resp_init(pdu, buf, len, create ? COAP_CODE_CREATED : COAP_CODE_CHANGED);
|
|
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, &etag, sizeof(etag));
|
|
}
|
|
else {
|
|
_resp_init(pdu, buf, len, COAP_CODE_CONTINUE);
|
|
block1.more = true; /* resource is created atomically */
|
|
coap_opt_add_block1_control(pdu, &block1);
|
|
}
|
|
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
|
|
close_on_error:
|
|
vfs_close(fd);
|
|
unlink_on_error:
|
|
if (create) {
|
|
vfs_unlink(request->namebuf);
|
|
}
|
|
return _error_handler(pdu, buf, len, ret);
|
|
}
|
|
#endif
|
|
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_DELETE)
|
|
static ssize_t _delete_file(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
int ret;
|
|
uint32_t etag;
|
|
struct stat stat;
|
|
if ((ret = vfs_stat(request->namebuf, &stat)) < 0) {
|
|
return _error_handler(pdu, buf, len, ret);
|
|
}
|
|
if (request->options.exists.if_match && request->options.if_match_len) {
|
|
stat_etag(&stat, &etag);
|
|
if (memcmp(&etag, &request->options.if_match, request->options.if_match_len)) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
}
|
|
|
|
_event_file(NANOCOAP_FILESERVER_DELETE_FILE, request);
|
|
|
|
if ((ret = vfs_unlink(request->namebuf)) < 0) {
|
|
return _error_handler(pdu, buf, len, ret);
|
|
}
|
|
_resp_init(pdu, buf, len, COAP_CODE_DELETED);
|
|
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
}
|
|
#endif
|
|
|
|
static ssize_t nanocoap_fileserver_file_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
switch (coap_get_method(pdu)) {
|
|
case COAP_METHOD_GET:
|
|
return _get_file(pdu, buf, len, request);
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_PUT)
|
|
case COAP_METHOD_PUT:
|
|
return _put_file(pdu, buf, len, request);
|
|
#endif
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_DELETE)
|
|
case COAP_METHOD_DELETE:
|
|
return _delete_file(pdu, buf, len, request);
|
|
#endif
|
|
default:
|
|
return _error_handler(pdu, buf, len, COAP_CODE_METHOD_NOT_ALLOWED);
|
|
}
|
|
}
|
|
|
|
static ssize_t _get_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request,
|
|
const char *root, const char* resource_dir)
|
|
{
|
|
int err;
|
|
vfs_DIR dir;
|
|
coap_block_slicer_t slicer;
|
|
coap_block1_t block2 = { .szx = CONFIG_NANOCOAP_BLOCK_SIZE_EXP_MAX };
|
|
if (request->options.exists.block2 && !coap_get_block2(pdu, &block2)) {
|
|
return _error_handler(pdu, buf, len, COAP_OPT_FINISH_NONE);
|
|
}
|
|
if ((err = vfs_opendir(&dir, request->namebuf)) < 0) {
|
|
return _error_handler(pdu, buf, len, err);
|
|
}
|
|
if (request->options.exists.if_match && request->options.if_match_len) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
DEBUG("nanocoap_fileserver: Serving directory listing\n");
|
|
|
|
_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 */,
|
|
&block2);
|
|
coap_block_slicer_init(&slicer, block2.blknum, coap_szx2size(block2.szx));
|
|
coap_opt_add_block2(pdu, &slicer, true);
|
|
buf += coap_opt_finish(pdu, COAP_OPT_FINISH_PAYLOAD);
|
|
|
|
size_t root_len = root ? strlen(root) : 0;
|
|
const char *root_dir = &request->namebuf[root_len];
|
|
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_name == '.') {
|
|
/* Exclude everything that starts with '.' */
|
|
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->buf;
|
|
}
|
|
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_PUT)
|
|
static ssize_t _put_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
vfs_DIR dir;
|
|
if (vfs_opendir(&dir, request->namebuf) == 0) {
|
|
vfs_closedir(&dir);
|
|
if (request->options.exists.if_match && request->options.if_match_len) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
_resp_init(pdu, buf, len, COAP_CODE_CHANGED);
|
|
}
|
|
else {
|
|
if (request->options.exists.if_match) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
int err;
|
|
if ((err = vfs_mkdir(request->namebuf, 0777)) < 0) {
|
|
return _error_handler(pdu, buf, len, err);
|
|
}
|
|
_resp_init(pdu, buf, len, COAP_CODE_CREATED);
|
|
}
|
|
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
}
|
|
#endif
|
|
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_DELETE)
|
|
static ssize_t _delete_directory(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request)
|
|
{
|
|
int err;
|
|
if (request->options.exists.if_match && request->options.if_match_len) {
|
|
if (request->options.if_match != byteorder_htonl(COAPFILESERVER_DIR_DELETE_ETAG).u32) {
|
|
return _error_handler(pdu, buf, len, COAP_CODE_PRECONDITION_FAILED);
|
|
}
|
|
if ((err = vfs_rmdir(request->namebuf)) < 0) {
|
|
return _error_handler(pdu, buf, len, err);
|
|
}
|
|
}
|
|
else if (IS_USED(MODULE_VFS_UTIL)) {
|
|
if ((err = vfs_unlink_recursive(request->namebuf,
|
|
request->namebuf,
|
|
sizeof(request->namebuf))) < 0) {
|
|
return _error_handler(pdu, buf, len, err);
|
|
}
|
|
}
|
|
_resp_init(pdu, buf, len, COAP_CODE_DELETED);
|
|
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
}
|
|
#endif
|
|
|
|
static ssize_t nanocoap_fileserver_directory_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
struct requestdata *request,
|
|
const char *root, const char* resource_dir)
|
|
{
|
|
switch (coap_get_method(pdu)) {
|
|
case COAP_METHOD_GET:
|
|
return _get_directory(pdu, buf, len, request, root, resource_dir);
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_PUT)
|
|
case COAP_METHOD_PUT:
|
|
return _put_directory(pdu, buf, len, request);
|
|
#endif
|
|
#if IS_USED(MODULE_NANOCOAP_FILESERVER_DELETE)
|
|
case COAP_METHOD_DELETE:
|
|
return _delete_directory(pdu, buf, len, request);
|
|
#endif
|
|
default:
|
|
return _error_handler(pdu, buf, len, COAP_CODE_METHOD_NOT_ALLOWED);
|
|
}
|
|
}
|
|
|
|
ssize_t nanocoap_fileserver_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len,
|
|
coap_request_ctx_t *ctx) {
|
|
const char *root = coap_request_ctx_get_context(ctx);
|
|
const char *resource = coap_request_ctx_get_path(ctx);
|
|
struct requestdata request = {0};
|
|
|
|
/** 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(resource, '/');
|
|
|
|
/* If a root directory for the server was specified, use that */
|
|
if (root && strlen(root) > 1) {
|
|
strncpy(request.namebuf, root, sizeof(request.namebuf));
|
|
namelength = strlen(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_IF_MATCH:
|
|
/* support only one if-match option, although it is repeatable */
|
|
if (request.options.exists.if_match) {
|
|
errorcode = COAP_CODE_NOT_IMPLEMENTED;
|
|
goto error;
|
|
}
|
|
if (optlen > 8) {
|
|
errorcode = COAP_CODE_BAD_OPTION;
|
|
goto error;
|
|
}
|
|
if (optlen && optlen != sizeof(request.options.if_match)) {
|
|
errorcode = COAP_CODE_PRECONDITION_FAILED;
|
|
goto error;
|
|
}
|
|
memcpy(&request.options.if_match, value,
|
|
request.options.if_match_len = optlen);
|
|
request.options.exists.if_match = true;
|
|
break;
|
|
case COAP_OPT_IF_NONE_MATCH:
|
|
if (optlen || request.options.exists.if_none_match) {
|
|
errorcode = COAP_CODE_BAD_OPTION;
|
|
goto error;
|
|
}
|
|
request.options.exists.if_none_match = true;
|
|
break;
|
|
case COAP_OPT_URI_HOST:
|
|
/* ignore host name option */
|
|
continue;
|
|
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.options.etag)) {
|
|
/* Can't be a matching tag, no use in carrying that */
|
|
continue;
|
|
}
|
|
if (request.options.exists.etag) {
|
|
/* 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.options.exists.etag = true;
|
|
memcpy(&request.options.etag, value, sizeof(request.options.etag));
|
|
break;
|
|
case COAP_OPT_BLOCK2:
|
|
if (optlen > 4 || request.options.exists.block2) {
|
|
errorcode = COAP_CODE_BAD_OPTION;
|
|
goto error;
|
|
}
|
|
request.options.exists.block2 = true;
|
|
break;
|
|
case COAP_OPT_BLOCK1:
|
|
if (optlen > 4 || request.options.exists.block1) {
|
|
errorcode = COAP_CODE_BAD_OPTION;
|
|
goto error;
|
|
}
|
|
request.options.exists.block1 = true;
|
|
break;
|
|
case COAP_OPT_SIZE2:
|
|
request.options.exists.size2 = true;
|
|
break;
|
|
default:
|
|
if (opt.opt_num & 1) {
|
|
errorcode = COAP_CODE_BAD_OPTION;
|
|
goto error;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (request.options.exists.if_match &&
|
|
request.options.exists.if_none_match) {
|
|
errorcode = COAP_CODE_PRECONDITION_FAILED;
|
|
goto error;
|
|
}
|
|
}
|
|
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 */
|
|
return is_directory
|
|
? nanocoap_fileserver_directory_handler(pdu, buf, len, &request, root, resource)
|
|
: nanocoap_fileserver_file_handler(pdu, buf, len, &request);
|
|
error:
|
|
if (_resp_init(pdu, buf, len, errorcode)) {
|
|
return -1;
|
|
}
|
|
coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
|
|
return 0;
|
|
}
|
|
|
|
#ifdef MODULE_NANOCOAP_FILESERVER_CALLBACK
|
|
void nanocoap_fileserver_set_event_cb(nanocoap_fileserver_event_handler_t cb, void *ctx)
|
|
{
|
|
mutex_lock(&_event_mtx);
|
|
|
|
_event_cb = cb;
|
|
_event_ctx = ctx;
|
|
|
|
mutex_unlock(&_event_mtx);
|
|
}
|
|
#endif
|
|
|
|
/** @} */
|