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

nanocoap: add cache for response messages

This commit is contained in:
Cenk Gündoğan 2020-04-17 13:43:52 +02:00
parent 59ec6f1ccd
commit 8750605d26
No known key found for this signature in database
GPG Key ID: A3DBC2F744D484D2
6 changed files with 585 additions and 0 deletions

View File

@ -14273,6 +14273,7 @@ sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_ACCEPT \(macro definiti
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_BLOCK1 \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_BLOCK2 \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_CONTENT_FORMAT \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_MAX_AGE \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_PATH \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_LOCATION_QUERY \(macro definition\) of group net_coap is not documented\.
sys/include/net/coap\.h:[0-9]+: warning: Member COAP_OPT_OBSERVE \(macro definition\) of group net_coap is not documented\.

View File

@ -662,6 +662,11 @@ ifneq (,$(filter nanocoap_sock,$(USEMODULE)))
USEMODULE += ztimer_msec
endif
ifneq (,$(filter nanocoap_cache,$(USEMODULE)))
USEMODULE += ztimer_sec
USEMODULE += hashes
endif
ifneq (,$(filter nanocoap_%,$(USEMODULE)))
USEMODULE += nanocoap
endif

View File

@ -40,6 +40,7 @@ extern "C" {
#define COAP_OPT_LOCATION_PATH (8)
#define COAP_OPT_URI_PATH (11)
#define COAP_OPT_CONTENT_FORMAT (12)
#define COAP_OPT_MAX_AGE (14)
#define COAP_OPT_URI_QUERY (15)
#define COAP_OPT_ACCEPT (17)
#define COAP_OPT_LOCATION_QUERY (20)

View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2020 HAW Hamburg
*
* 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_nanocoap_cache Nanocoap-Cache implementation
* @ingroup net_nanocoap
* @brief A cache implementation for nanocoap response messages
*
* @{
*
* @file
* @brief nanocoap-cache API
*
* @author Cenk Gündoğan <cenk.guendogan@haw-hamburg.de>
*/
#ifndef NET_NANOCOAP_CACHE_H
#define NET_NANOCOAP_CACHE_H
#include <assert.h>
#include <stdint.h>
#include "clist.h"
#include "net/nanocoap.h"
#include "hashes/sha256.h"
#include "ztimer.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief The number of maximum cache entries.
*/
#ifndef CONFIG_NANOCOAP_CACHE_ENTRIES
#define CONFIG_NANOCOAP_CACHE_ENTRIES (8)
#endif
/**
* @brief The length of the cache key in bytes.
*/
#ifndef CONFIG_NANOCOAP_CACHE_KEY_LENGTH
#define CONFIG_NANOCOAP_CACHE_KEY_LENGTH (8)
#endif
/**
* @brief Size of the buffer to store responses in the cache.
*/
#ifndef CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE
#define CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE (128)
#endif
/**
* @brief Cache container that holds a @p coap_pkt_t struct.
*/
typedef struct {
/**
* @brief needed for clist_t, must be the first struct member!
*/
clist_node_t node;
/**
* @brief the calculated cache key, see nanocoap_cache_key_generate().
*/
uint8_t cache_key[CONFIG_NANOCOAP_CACHE_KEY_LENGTH];
/**
* @brief packet representation of the response
*/
coap_pkt_t response_pkt;
/**
* @brief buffer to hold the response message.
*/
uint8_t response_buf[CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE];
size_t response_len; /**< length of the message in @p response */
unsigned request_method; /**< the method of the initial request */
/**
* @brief absolute system time in seconds until which this cache entry
* is considered valid.
*/
ztimer_now_t max_age;
} nanocoap_cache_entry_t;
/**
* @brief Typedef for the cache replacement strategy on full cache list.
*
* @return 0 on successfully replacing a cache element
* @return -1 on error
*/
typedef int (*nanocoap_cache_replacement_strategy_t)(void);
/**
* @brief Typedef for the cache update strategy on element access.
*
* @param[in] node The accessed node.
*
* @return 0 on successfully updating the element
* @return -1 on error
*/
typedef int (*nanocoap_cache_update_strategy_t)(clist_node_t *node);
/**
* @brief Initializes the internal state of the nanocoap cache.
*/
#if IS_USED(MODULE_NANOCOAP_CACHE)
void nanocoap_cache_init(void);
#else
static inline void nanocoap_cache_init(void)
{
return;
}
#endif
/**
* @brief Returns the number of cached entries.
*
* @return Number of cached entries
*/
size_t nanocoap_cache_used_count(void);
/**
* @brief Returns the number of unused cache entries.
*
* @return Number of unused cache entries
*/
size_t nanocoap_cache_free_count(void);
/**
* @brief Determines if a response is cacheable and modifies the cache
* as reflected in RFC7252, Section 5.9.
* @param[in] cache_key The cache key of the request
* @param[in] request_method The method of the initial request
* @param[in] resp The response to operate on
* @param[in] resp_len The actual length of the response in @p resp
*
* @return 0 on successfully handling the response
* @return -1 on error
*/
int nanocoap_cache_process(const uint8_t *cache_key, unsigned request_method,
const coap_pkt_t *resp, size_t resp_len);
/**
* @brief Creates a new or gets an existing cache entry using the
* request packet.
*
* @param[in] req The request to calculate the cache-key
* @param[in] resp The response to add to the cache
* @param[in] resp_len The actual length of the response message in @p resp
*
* @return The previously existing or newly added cache entry on success
* @return NULL, if there is no space left
*/
nanocoap_cache_entry_t *nanocoap_cache_add_by_req(const coap_pkt_t *req,
const coap_pkt_t *resp,
size_t resp_len);
/**
* @brief Creates a new or gets an existing cache entry using the cache key.
*
* @param[in] cache_key The cache key of the request
* @param[in] request_method The method of the initial request
* @param[in] resp The response to add to the cache
* @param[in] resp_len The actual length of the response in @p resp
*
* @return The previously existing or newly added cache entry on success
* @return NULL, if there is no space left
*/
nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key,
unsigned request_method,
const coap_pkt_t *resp,
size_t resp_len);
/**
* @brief Performs a cache lookup based on the @p req.
*
* @param[in] req The request to calculate the cache-key
*
* @return An existing cache entry on cache hit
* @return NULL on cache miss
*/
nanocoap_cache_entry_t *nanocoap_cache_request_lookup(const coap_pkt_t *req);
/**
* @brief Performs a cache lookup based on the cache key of a request.
*
* @param[in] cache_key The cache key of a request
*
* @return An existing cache entry on cache hit
* @return NULL on cache miss
*/
nanocoap_cache_entry_t *nanocoap_cache_key_lookup(const uint8_t *cache_key);
/**
* @brief Deletes the provided cache entry @p ce.
*
* @param[in] ce The cache entry to delete
*
* @return 0 on success
* @return -1 if entry is not available in the cache
*/
int nanocoap_cache_del(const nanocoap_cache_entry_t *ce);
/**
* @brief Generates a cache key based on the request @p req.
*
* @param[in] req The request to generate the cache key from
* @param[out] cache_key The generated cache key
*/
void nanocoap_cache_key_generate(const coap_pkt_t *req, uint8_t *cache_key);
/**
* @brief Compares two cache keys.
*
* @param[in] cache_key1 The first cache key in the comparison
* @param[in] cache_key2 The second cache key in the comparison
*
* @return 0 if cache keys are equal
* @return <0 or 0> (see memcmp()) for unequal cache keys
*/
ssize_t nanocoap_cache_key_compare(uint8_t *cache_key1, uint8_t *cache_key2);
#ifdef __cplusplus
}
#endif
#endif /* NET_NANOCOAP_CACHE_H */
/** @} */

View File

@ -35,4 +35,24 @@ config NANOCOAP_QS_MAX
int "Maximum length of a query string written to a message"
default 64
menuconfig KCONFIG_USEMODULE_NANOCOAP_CACHE
bool "Configure Nanocoap Cache module"
depends on USEMODULE_NANOCOAP_CACHE
if KCONFIG_USEMODULE_NANOCOAP_CACHE
config NANOCOAP_CACHE_ENTRIES
int "Number of maximum cache entries"
default 8
config NANOCOAP_CACHE_KEY_LENGTH
int "The length of the cache key in bytes"
default 8
config NANOCOAP_CACHE_RESPONSE_SIZE
int "Size of the buffer to store responses in the cache"
default 128
endif # KCONFIG_USEMODULE_NANOCOAP_CACHE
endif # KCONFIG_USEMODULE_NANOCOAP

View File

@ -0,0 +1,324 @@
/*
* Copyright (C) 2020 HAW Hamburg
*
* 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.
*/
/**
* @ingroup net_nanocoap_cache
* @{
*
* @file
* @brief Implementation of common functions for the nanocoap-cache
*
* @author Cenk Gündoğan <cenk.guendogan@haw-hamburg.de>
*
* @}
*/
#include <string.h>
#include "kernel_defines.h"
#include "net/nanocoap/cache.h"
#include "hashes/sha256.h"
#define ENABLE_DEBUG 0
#include "debug.h"
static clist_node_t _cache_list_head = { NULL };
static clist_node_t _empty_list_head = { NULL };
static nanocoap_cache_entry_t _cache_entries[CONFIG_NANOCOAP_CACHE_ENTRIES];
static nanocoap_cache_replacement_strategy_t _replacement_strategy = NULL;
static int _cache_replacement_lru(void)
{
nanocoap_cache_entry_t *least = NULL;
clist_node_t *it = _cache_list_head.next;
/* no element in the list */
if (!it) {
return -1;
}
least = container_of(it, nanocoap_cache_entry_t, node);
do {
it = it->next;
nanocoap_cache_entry_t *nit = container_of(it, nanocoap_cache_entry_t, node);
if (least->access_time > nit->access_time) {
least = nit;
}
} while (it != _cache_list_head.next);
return nanocoap_cache_del(least);
}
void nanocoap_cache_init(void)
{
_cache_list_head.next = NULL;
_empty_list_head.next = NULL;
memset(_cache_entries, 0, sizeof(_cache_entries));
/* construct list of empty entries */
for (unsigned i = 0; i < CONFIG_NANOCOAP_CACHE_ENTRIES; i++) {
clist_rpush(&_empty_list_head, &_cache_entries[i].node);
}
/* hardcode cache replacement strategy for now */
_replacement_strategy = _cache_replacement_lru;
}
size_t nanocoap_cache_used_count(void)
{
return clist_count(&_cache_list_head);
}
size_t nanocoap_cache_free_count(void)
{
return clist_count(&_empty_list_head);
}
void nanocoap_cache_key_generate(const coap_pkt_t *req, uint8_t *cache_key)
{
sha256_context_t ctx;
sha256_init(&ctx);
coap_optpos_t opt = {0, 0};
uint8_t *value;
for (unsigned i = 0; i < req->options_len; i++) {
ssize_t optlen = coap_opt_get_next(req, &opt, &value, !i);
if (optlen >= 0) {
/* skip NoCacheKey,
see https://tools.ietf.org/html/rfc7252#section-5.4.6 */
if ((opt.opt_num & 0x1E) == 0x1C) {
continue;
}
sha256_update(&ctx, value, optlen);
}
}
switch (req->hdr->code) {
case COAP_METHOD_FETCH:
sha256_update(&ctx, req->payload, req->payload_len);
break;
default:
break;
}
sha256_final(&ctx, cache_key);
}
ssize_t nanocoap_cache_key_compare(uint8_t *cache_key1, uint8_t *cache_key2)
{
return memcmp(cache_key1, cache_key2, CONFIG_NANOCOAP_CACHE_KEY_LENGTH);
}
static int _compare_cache_keys(clist_node_t *ce, void *arg)
{
nanocoap_cache_entry_t *iterator = container_of(ce, nanocoap_cache_entry_t, node);
uint8_t *cache_key = (uint8_t *) arg;
if (!memcmp(iterator->cache_key, cache_key, CONFIG_NANOCOAP_CACHE_KEY_LENGTH)) {
return 1;
}
return 0;
}
static nanocoap_cache_entry_t *_nanocoap_cache_foreach(const uint8_t *key)
{
clist_node_t *node = clist_foreach(&_cache_list_head, _compare_cache_keys, (uint8_t *)key);
if (node != NULL) {
return container_of(node, nanocoap_cache_entry_t, node);
}
else {
return NULL;
}
}
nanocoap_cache_entry_t *nanocoap_cache_key_lookup(const uint8_t *key)
{
nanocoap_cache_entry_t *ce = _nanocoap_cache_foreach(key);
if (ce) {
ce->access_time = ztimer_now(ZTIMER_SEC);
}
return ce;
}
nanocoap_cache_entry_t *nanocoap_cache_request_lookup(const coap_pkt_t *req)
{
uint8_t cache_key[SHA256_DIGEST_LENGTH];
nanocoap_cache_entry_t *ce;
/* generate cache key */
nanocoap_cache_key_generate(req, cache_key);
/* check if cache key already exists */
ce = nanocoap_cache_key_lookup(cache_key);
return ce;
}
int nanocoap_cache_process(const uint8_t *cache_key, unsigned request_method,
const coap_pkt_t *resp, size_t resp_len)
{
nanocoap_cache_entry_t *ce;
ce = nanocoap_cache_key_lookup(cache_key);
/* This response is not cacheable. */
if (resp->hdr->code == COAP_CODE_CREATED) {
/* NO OP */
}
/* This response is not cacheable. However, a cache MUST mark any
stored response for the deleted resource as not fresh.
*/
else if (resp->hdr->code == COAP_CODE_DELETED) {
if (ce) {
/* set max_age to now(), so that the cache is considered
* stale immdiately */
ce->max_age = ztimer_now(ZTIMER_SEC);
}
}
/* When a cache that recognizes and processes the ETag response
option receives a 2.03 (Valid) response, it MUST update the
stored response with the value of the Max-Age Option included
in the response (explicitly, or implicitly as a default value;
see also Section 5.6.2). For each type of Safe-to-Forward
option present in the response, the (possibly empty) set of
options of this type that are present in the stored response
MUST be replaced with the set of options of this type in the
response received. (Unsafe options may trigger similar
option-specific processing as defined by the option.)
*/
else if (resp->hdr->code == COAP_CODE_VALID) {
if (ce) {
/* refresh max_age() */
uint32_t max_age = 60;
coap_opt_get_uint((coap_pkt_t *)resp, COAP_OPT_MAX_AGE, &max_age);
ce->max_age = ztimer_now(ZTIMER_SEC) + max_age;
}
/* TODO: handle the copying of the new options (if changed) */
}
/* This response is not cacheable. However, a cache MUST mark any
stored response for the changed resource as not fresh.
*/
else if (resp->hdr->code == COAP_CODE_CHANGED) {
if (ce) {
/* set max_age to now(), so that the cache is considered
* stale immdiately */
ce->max_age = ztimer_now(ZTIMER_SEC);
}
}
/* This response is cacheable: Caches can use the Max-Age Option
to determine freshness and (if present) the
ETag Option for validation.
*/
else if (resp->hdr->code == COAP_CODE_CONTENT) {
uint32_t now = ztimer_now(ZTIMER_SEC);
/* cache entry is stale */
if (ce && (ce->max_age < now)) {
nanocoap_cache_del(ce);
}
if (NULL == nanocoap_cache_add_by_key(cache_key, request_method,
resp, resp_len)) {
/* no space left in the cache? */
return -1;
}
/* TODO: ETAG handling */
}
return 0;
}
static nanocoap_cache_entry_t *_nanocoap_cache_pop(void)
{
clist_node_t *node;
node = clist_lpop(&_empty_list_head);
if (node != NULL) {
return container_of(node, nanocoap_cache_entry_t, node);
}
else {
return NULL;
}
}
nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key,
unsigned request_method,
const coap_pkt_t *resp,
size_t resp_len)
{
nanocoap_cache_entry_t *ce;
ce = nanocoap_cache_key_lookup(cache_key);
/* found an already existing cache entry */
if (ce) {
return ce;
}
/* did not find .. get an empty cache container */
ce = _nanocoap_cache_pop();
/* no space left */
if (!ce) {
/* could not remove any entry */
if (_replacement_strategy()) {
return NULL;
}
/* could remove an entry */
ce = _nanocoap_cache_pop();
if (!ce) {
/* still no free space ? stop trying now */
return NULL;
}
}
memcpy(ce->cache_key, cache_key, CONFIG_NANOCOAP_CACHE_KEY_LENGTH);
memcpy(&ce->response_pkt, resp, sizeof(coap_pkt_t));
memcpy(&ce->response_buf, resp->hdr, resp_len);
ce->response_pkt.hdr = (coap_hdr_t *) ce->response_buf;
ce->response_pkt.payload = ce->response_buf + (resp->payload - ((uint8_t *)resp->hdr));
ce->response_len = resp_len;
ce->access_time = ztimer_now(ZTIMER_SEC);
ce->request_method = request_method;
/* default value is 60 seconds, if MAX_AGE not present */
uint32_t max_age = 60;
coap_opt_get_uint((coap_pkt_t *)resp, COAP_OPT_MAX_AGE, &max_age);
ce->max_age = ztimer_now(ZTIMER_SEC) + max_age;
clist_rpush(&_cache_list_head, &ce->node);
return ce;
}
nanocoap_cache_entry_t *nanocoap_cache_add_by_req(const coap_pkt_t *req,
const coap_pkt_t *resp,
size_t resp_len)
{
uint8_t cache_key[SHA256_DIGEST_LENGTH];
/* generate cache key */
nanocoap_cache_key_generate(req, cache_key);
return nanocoap_cache_add_by_key(cache_key,
coap_get_code((coap_pkt_t *)req),
resp,
resp_len);
}
int nanocoap_cache_del(const nanocoap_cache_entry_t *ce)
{
clist_node_t *entry = clist_find(&_cache_list_head, &ce->node);
if (entry) {
clist_remove(&_cache_list_head, entry);
memset(entry, 0, sizeof(nanocoap_cache_entry_t));
clist_rpush(&_empty_list_head, entry);
return 0;
}
return -1;
}