From 8750605d26562b7befc80e342b77e58807c553b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20G=C3=BCndo=C4=9Fan?= Date: Fri, 17 Apr 2020 13:43:52 +0200 Subject: [PATCH 1/3] nanocoap: add cache for response messages --- dist/tools/doccheck/exclude_patterns | 1 + sys/Makefile.dep | 5 + sys/include/net/coap.h | 1 + sys/include/net/nanocoap/cache.h | 234 +++++++++++++++ sys/net/application_layer/nanocoap/Kconfig | 20 ++ sys/net/application_layer/nanocoap/cache.c | 324 +++++++++++++++++++++ 6 files changed, 585 insertions(+) create mode 100644 sys/include/net/nanocoap/cache.h create mode 100644 sys/net/application_layer/nanocoap/cache.c diff --git a/dist/tools/doccheck/exclude_patterns b/dist/tools/doccheck/exclude_patterns index 194b1dd6cb..031396fef5 100644 --- a/dist/tools/doccheck/exclude_patterns +++ b/dist/tools/doccheck/exclude_patterns @@ -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\. diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 54f7187aa9..093606ba31 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -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 diff --git a/sys/include/net/coap.h b/sys/include/net/coap.h index 8f902c2a94..f75e9ba36c 100644 --- a/sys/include/net/coap.h +++ b/sys/include/net/coap.h @@ -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) diff --git a/sys/include/net/nanocoap/cache.h b/sys/include/net/nanocoap/cache.h new file mode 100644 index 0000000000..775e112db5 --- /dev/null +++ b/sys/include/net/nanocoap/cache.h @@ -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 + */ + +#ifndef NET_NANOCOAP_CACHE_H +#define NET_NANOCOAP_CACHE_H + +#include +#include +#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 */ +/** @} */ diff --git a/sys/net/application_layer/nanocoap/Kconfig b/sys/net/application_layer/nanocoap/Kconfig index 33fde07fa4..e405deea70 100644 --- a/sys/net/application_layer/nanocoap/Kconfig +++ b/sys/net/application_layer/nanocoap/Kconfig @@ -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 diff --git a/sys/net/application_layer/nanocoap/cache.c b/sys/net/application_layer/nanocoap/cache.c new file mode 100644 index 0000000000..8849a618f6 --- /dev/null +++ b/sys/net/application_layer/nanocoap/cache.c @@ -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 + * + * @} + */ + +#include + +#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; +} From c5eb53095d36e933ec931b4f27a7fbe315eb1db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20G=C3=BCndo=C4=9Fan?= Date: Wed, 13 May 2020 18:17:20 +0200 Subject: [PATCH 2/3] gcoap: integrate nanocoap cache --- .../application_layer/gcoap/forward_proxy.c | 95 +++++++++++++++++-- sys/net/application_layer/nanocoap/cache.c | 85 ++++++++--------- 2 files changed, 130 insertions(+), 50 deletions(-) diff --git a/sys/net/application_layer/gcoap/forward_proxy.c b/sys/net/application_layer/gcoap/forward_proxy.c index a45cb217df..88ec3ba531 100644 --- a/sys/net/application_layer/gcoap/forward_proxy.c +++ b/sys/net/application_layer/gcoap/forward_proxy.c @@ -16,6 +16,7 @@ #include "net/gcoap.h" #include "net/gcoap/forward_proxy.h" #include "uri_parser.h" +#include "net/nanocoap/cache.h" #define ENABLE_DEBUG 0 #include "debug.h" @@ -23,6 +24,9 @@ typedef struct { int in_use; sock_udp_ep_t ep; +#if IS_USED(MODULE_NANOCOAP_CACHE) + uint8_t cache_key[CONFIG_NANOCOAP_CACHE_KEY_LENGTH]; +#endif } client_ep_t; static uint8_t proxy_req_buf[CONFIG_GCOAP_PDU_BUF_SIZE]; @@ -50,6 +54,58 @@ gcoap_listener_t forward_proxy_listener = { void gcoap_forward_proxy_init(void) { gcoap_register_listener(&forward_proxy_listener); + + /* initialize the nanocoap cache operation, if compiled */ + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + nanocoap_cache_init(); + } +} + +static int _cache_build_response(nanocoap_cache_entry_t *ce, + coap_pkt_t *pdu, + uint8_t *buf, + size_t len) +{ + /* Use the same code from the cached content. Use other header + * fields from the incoming request */ + gcoap_resp_init(pdu, buf, len, ce->response_pkt.hdr->code); + /* copy all options and possible payload from the cached response + * to the new response */ + unsigned header_len_req = coap_get_total_hdr_len(pdu); + unsigned header_len_cached = coap_get_total_hdr_len(&ce->response_pkt); + unsigned opt_payload_len = ce->response_len - header_len_cached; + + memcpy((buf + header_len_req), + (ce->response_buf + header_len_cached), + opt_payload_len); + return header_len_req + opt_payload_len; +} + +static int _cache_lookup_and_process(coap_pkt_t *pdu, + uint8_t *buf, + size_t len, + client_ep_t *cep) +{ + (void) cep; + + uint8_t cache_key[SHA256_DIGEST_LENGTH]; + ztimer_now_t now = ztimer_now(ZTIMER_SEC); + nanocoap_cache_key_generate(pdu, cache_key); + nanocoap_cache_entry_t *ce = nanocoap_cache_key_lookup(cache_key); + + /* cache hit, methods are equal, and cache entry is not stale */ + if (ce && + (ce->request_method == coap_get_code(pdu)) && + (ce->max_age > now)) { + /* use response from cache */ + return _cache_build_response(ce, pdu, buf, len); + } + +#if IS_USED(MODULE_NANOCOAP_CACHE) + memcpy(cep->cache_key, cache_key, CONFIG_NANOCOAP_CACHE_KEY_LENGTH); +#endif + + return 0; } static client_ep_t *_allocate_client_ep(sock_udp_ep_t *ep) @@ -91,24 +147,25 @@ static int _request_matcher_forward_proxy(gcoap_listener_t *listener, static ssize_t _forward_proxy_handler(coap_pkt_t *pdu, uint8_t *buf, size_t len, void *ctx) { + int pdu_len = 0; sock_udp_ep_t *remote = (sock_udp_ep_t *)ctx; - int proxy_res = gcoap_forward_proxy_request_process(pdu, remote); + pdu_len = gcoap_forward_proxy_request_process(pdu, remote); /* Out of memory, reply with 5.00 */ - if (proxy_res == -ENOMEM) { + if (pdu_len == -ENOMEM) { return gcoap_response(pdu, buf, len, COAP_CODE_INTERNAL_SERVER_ERROR); } /* Proxy-Uri malformed, reply with 4.02 */ - else if (proxy_res == -EINVAL) { + else if (pdu_len == -EINVAL) { return gcoap_response(pdu, buf, len, COAP_CODE_BAD_OPTION); } /* scheme not supported */ - else if (proxy_res == -EPERM) { + else if (pdu_len == -EPERM) { return gcoap_response(pdu, buf, len, COAP_CODE_PROXYING_NOT_SUPPORTED); } - return 0; + return pdu_len; } static bool _parse_endpoint(sock_udp_ep_t *remote, @@ -194,6 +251,19 @@ static void _forward_resp_handler(const gcoap_request_memo_t *memo, (pdu->payload - (uint8_t *)pdu->hdr + pdu->payload_len), &cep->ep); +#if IS_USED(MODULE_NANOCOAP_CACHE) + coap_pkt_t req; + if (memo->send_limit == GCOAP_SEND_LIMIT_NON) { + req.hdr = (coap_hdr_t *) &memo->msg.hdr_buf[0]; + } + else { + req.hdr = (coap_hdr_t *) memo->msg.data.pdu_buf; + } + + size_t pdu_len = pdu->payload_len + + (pdu->payload - (uint8_t *)pdu->hdr); + nanocoap_cache_process(cep->cache_key, coap_get_code(&req), pdu, pdu_len); +#endif } _free_client_ep(cep); } @@ -312,7 +382,6 @@ int gcoap_forward_proxy_request_process(coap_pkt_t *pkt, sock_udp_ep_t *client) { char *uri; uri_parser_result_t urip; - ssize_t optlen = 0; client_ep_t *cep = _allocate_client_ep(client); @@ -321,6 +390,20 @@ int gcoap_forward_proxy_request_process(coap_pkt_t *pkt, return -ENOMEM; } + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + int pdu_len = _cache_lookup_and_process(pkt, + (uint8_t *)pkt->hdr, + CONFIG_GCOAP_PDU_BUF_SIZE, + cep); + /* if a valid cache entry was found, then pdu_len contains the + * length of that response message */ + if (pdu_len > 0) { + _free_client_ep(cep); + return pdu_len; + } + /* if there was no cache hit, then we continue forwarding */ + } + optlen = coap_get_proxy_uri(pkt, &uri); if (optlen < 0) { diff --git a/sys/net/application_layer/nanocoap/cache.c b/sys/net/application_layer/nanocoap/cache.c index 8849a618f6..1a035ba17c 100644 --- a/sys/net/application_layer/nanocoap/cache.c +++ b/sys/net/application_layer/nanocoap/cache.c @@ -27,34 +27,39 @@ #define ENABLE_DEBUG 0 #include "debug.h" +static int _cache_replacement_lru(void); +static int _cache_update_lru(clist_node_t *node); + 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 const nanocoap_cache_replacement_strategy_t _replacement_strategy = _cache_replacement_lru; +static const nanocoap_cache_update_strategy_t _update_strategy = _cache_update_lru; static int _cache_replacement_lru(void) { - nanocoap_cache_entry_t *least = NULL; - clist_node_t *it = _cache_list_head.next; + clist_node_t *lru_node = clist_lpeek(&_cache_list_head); /* no element in the list */ - if (!it) { + if (!lru_node) { return -1; } - least = container_of(it, nanocoap_cache_entry_t, node); + nanocoap_cache_entry_t *lru_ce = container_of(lru_node, nanocoap_cache_entry_t, node); + return nanocoap_cache_del(lru_ce); +} - 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); +static int _cache_update_lru(clist_node_t *node) +{ + if (clist_remove(&_cache_list_head, node)) { + /* Move an accessed node to the end of the list. Least + * recently used nodes are at the beginning of this list */ + clist_rpush(&_cache_list_head, node); + return 0; + } + return -1; } void nanocoap_cache_init(void) @@ -66,9 +71,6 @@ void nanocoap_cache_init(void) 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) @@ -125,27 +127,21 @@ static int _compare_cache_keys(clist_node_t *ce, void *arg) } 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; - } +static clist_node_t *_nanocoap_cache_foreach(const uint8_t *key) +{ + return clist_foreach(&_cache_list_head, _compare_cache_keys, (uint8_t *)key); } nanocoap_cache_entry_t *nanocoap_cache_key_lookup(const uint8_t *key) { - nanocoap_cache_entry_t *ce = _nanocoap_cache_foreach(key); + clist_node_t *node = _nanocoap_cache_foreach(key); - if (ce) { - ce->access_time = ztimer_now(ZTIMER_SEC); + if (node) { + _update_strategy(node); } - return ce; + return container_of(node, nanocoap_cache_entry_t, node); } nanocoap_cache_entry_t *nanocoap_cache_request_lookup(const coap_pkt_t *req) @@ -217,11 +213,6 @@ int nanocoap_cache_process(const uint8_t *cache_key, unsigned request_method, 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? */ @@ -250,16 +241,20 @@ nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key, const coap_pkt_t *resp, size_t resp_len) { - nanocoap_cache_entry_t *ce; - ce = nanocoap_cache_key_lookup(cache_key); + nanocoap_cache_entry_t *ce = nanocoap_cache_key_lookup(cache_key); + bool add_to_cache = false; - /* found an already existing cache entry */ - if (ce) { - return ce; + if (resp_len > CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE) { + DEBUG("nanocoap_cache: response too large to cache (%lu > %d)\n", + (long unsigned)resp_len, CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE); + return NULL; } - /* did not find .. get an empty cache container */ - ce = _nanocoap_cache_pop(); + if (!ce) { + /* did not find .. get an empty cache container */ + ce = _nanocoap_cache_pop(); + add_to_cache = true; + } /* no space left */ if (!ce) { @@ -269,6 +264,7 @@ nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key, } /* could remove an entry */ ce = _nanocoap_cache_pop(); + add_to_cache = true; if (!ce) { /* still no free space ? stop trying now */ return NULL; @@ -281,7 +277,6 @@ nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key, 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 */ @@ -289,7 +284,9 @@ nanocoap_cache_entry_t *nanocoap_cache_add_by_key(const uint8_t *cache_key, 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); + if (add_to_cache) { + clist_rpush(&_cache_list_head, &ce->node); + } return ce; } From b4cae9d09de2381905e8bec8af647658a6f71ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20G=C3=BCndo=C4=9Fan?= Date: Fri, 17 Apr 2020 13:50:06 +0200 Subject: [PATCH 3/3] nanocoap: add unittests for nanocoap-cache --- tests/unittests/tests-nanocoap_cache/Makefile | 1 + .../tests-nanocoap_cache/Makefile.include | 1 + .../tests-nanocoap_cache.c | 275 ++++++++++++++++++ .../tests-nanocoap_cache.h | 37 +++ 4 files changed, 314 insertions(+) create mode 100644 tests/unittests/tests-nanocoap_cache/Makefile create mode 100644 tests/unittests/tests-nanocoap_cache/Makefile.include create mode 100644 tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.c create mode 100644 tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.h diff --git a/tests/unittests/tests-nanocoap_cache/Makefile b/tests/unittests/tests-nanocoap_cache/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/tests/unittests/tests-nanocoap_cache/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/tests/unittests/tests-nanocoap_cache/Makefile.include b/tests/unittests/tests-nanocoap_cache/Makefile.include new file mode 100644 index 0000000000..99ae98aff2 --- /dev/null +++ b/tests/unittests/tests-nanocoap_cache/Makefile.include @@ -0,0 +1 @@ +USEMODULE += nanocoap_cache diff --git a/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.c b/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.c new file mode 100644 index 0000000000..f00eb80e1a --- /dev/null +++ b/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.c @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2020 Cenk Gündoğan + * + * 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 + */ +#include +#include +#include +#include + +#include "embUnit.h" + +#include "net/nanocoap/cache.h" +#include "ztimer.h" +#include "hashes/sha256.h" + +#include "unittests-constants.h" +#include "tests-nanocoap_cache.h" +#define _BUF_SIZE (128U) + +static void test_nanocoap_cache__cachekey(void) +{ + uint8_t digest1[SHA256_DIGEST_LENGTH]; + uint8_t digest2[SHA256_DIGEST_LENGTH]; + uint8_t buf1[_BUF_SIZE]; + uint8_t buf2[_BUF_SIZE]; + coap_pkt_t pkt1; + coap_pkt_t pkt2; + uint16_t msgid = 0xABCD; + uint8_t token[2] = {0xDA, 0xEC}; + char path[] = "/time"; + char path2[] = "/time2"; + size_t len; + + /* 1. packet */ + len = coap_build_hdr((coap_hdr_t *)&buf1[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&pkt1, &buf1[0], sizeof(buf1), len); + coap_opt_add_string(&pkt1, COAP_OPT_URI_PATH, &path[0], '/'); + coap_opt_finish(&pkt1, COAP_OPT_FINISH_NONE); + + /* 2. packet */ + len = coap_build_hdr((coap_hdr_t *)&buf2[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&pkt2, &buf2[0], sizeof(buf2), len); + coap_opt_add_string(&pkt2, COAP_OPT_URI_PATH, &path[0], '/'); + coap_opt_finish(&pkt2, COAP_OPT_FINISH_NONE); + + nanocoap_cache_key_generate((const coap_pkt_t *) &pkt1, digest1); + nanocoap_cache_key_generate((const coap_pkt_t *) &pkt2, digest2); + + /* compare 1. and 2. packet */ + TEST_ASSERT_EQUAL_INT(0, nanocoap_cache_key_compare(digest1, digest2)); + + /* 3. packet */ + len = coap_build_hdr((coap_hdr_t *)&buf2[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&pkt2, &buf2[0], sizeof(buf2), len); + coap_opt_add_string(&pkt2, COAP_OPT_URI_PATH, &path2[0], '/'); + coap_opt_finish(&pkt2, COAP_OPT_FINISH_NONE); + + nanocoap_cache_key_generate((const coap_pkt_t *) &pkt2, digest2); + + /* compare 1. and 3. packet */ + TEST_ASSERT_EQUAL_INT(-1, nanocoap_cache_key_compare(digest1, digest2)); + /* compare 3. and 1. packet */ + TEST_ASSERT_EQUAL_INT(1, nanocoap_cache_key_compare(digest2, digest1)); +} +static void test_nanocoap_cache__add(void) +{ + uint8_t buf[_BUF_SIZE]; + uint8_t rbuf[_BUF_SIZE]; + coap_pkt_t req, resp; + + uint16_t msgid = 0xABCD; + uint8_t token[2] = {0xDA, 0xEC}; + char path[16]; + nanocoap_cache_entry_t *c = NULL; + size_t len; + uint8_t temp_cache_key[CONFIG_NANOCOAP_CACHE_KEY_LENGTH]; + + /* initialize the nanocoap cache */ + nanocoap_cache_init(); + + /* add more entries to test LRU replacement */ + for (unsigned i = 0; i < CONFIG_NANOCOAP_CACHE_ENTRIES + 4; i++) { + if (i < CONFIG_NANOCOAP_CACHE_ENTRIES) { + TEST_ASSERT_EQUAL_INT(CONFIG_NANOCOAP_CACHE_ENTRIES - i, + nanocoap_cache_free_count()); + TEST_ASSERT_EQUAL_INT(i, nanocoap_cache_used_count()); + } + else { + TEST_ASSERT_EQUAL_INT(0, nanocoap_cache_free_count()); + TEST_ASSERT_EQUAL_INT(CONFIG_NANOCOAP_CACHE_ENTRIES, + nanocoap_cache_used_count()); + } + + snprintf(path, sizeof(path), "/path_%u", i); + + /* request */ + len = coap_build_hdr((coap_hdr_t *)&buf[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&req, &buf[0], sizeof(buf), len); + coap_opt_add_string(&req, COAP_OPT_URI_PATH, &path[0], '/'); + coap_opt_finish(&req, COAP_OPT_FINISH_NONE); + + /* response */ + len = coap_build_hdr((coap_hdr_t *)&rbuf[0], COAP_TYPE_NON, + &token[0], 2, COAP_CODE_205, msgid); + coap_pkt_init(&resp, &rbuf[0], sizeof(rbuf), len); + coap_opt_finish(&resp, COAP_OPT_FINISH_NONE); + + /* add a fake response buffer with fake response length */ + c = nanocoap_cache_add_by_req((const coap_pkt_t *)&req, + (const coap_pkt_t *)&req, + CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE); + + TEST_ASSERT_NOT_NULL(c); + + /* keep a pointer to the first cache entry for testing the LRU + replacement */ + if (i == 0) { + memcpy(temp_cache_key, c->cache_key, + CONFIG_NANOCOAP_CACHE_KEY_LENGTH); + } + + if (i == CONFIG_NANOCOAP_CACHE_ENTRIES-1) { + /* lookup the first entry to update its list position */ + nanocoap_cache_key_lookup(temp_cache_key); + } + } + + /* the first entry should NOT be cached out, because we updated + * its list position */ + c = nanocoap_cache_key_lookup(temp_cache_key); + TEST_ASSERT_NOT_NULL(c); +} + +static void test_nanocoap_cache__del(void) +{ + uint8_t buf[_BUF_SIZE]; + uint8_t rbuf[_BUF_SIZE]; + coap_pkt_t req, resp; + + uint16_t msgid = 0xABCD; + uint8_t token[2] = {0xDA, 0xEC}; + char *path = "/path"; + nanocoap_cache_entry_t *c = NULL; + size_t len; + int res; + + /* initialize the nanocoap cache */ + nanocoap_cache_init(); + + TEST_ASSERT_EQUAL_INT(CONFIG_NANOCOAP_CACHE_ENTRIES, + nanocoap_cache_free_count()); + TEST_ASSERT_EQUAL_INT(0, nanocoap_cache_used_count()); + + /* request */ + len = coap_build_hdr((coap_hdr_t *)&buf[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&req, &buf[0], sizeof(buf), len); + coap_opt_add_string(&req, COAP_OPT_URI_PATH, &path[0], '/'); + coap_opt_finish(&req, COAP_OPT_FINISH_NONE); + + /* response */ + len = coap_build_hdr((coap_hdr_t *)&rbuf[0], COAP_TYPE_NON, + &token[0], 2, COAP_CODE_205, msgid); + coap_pkt_init(&resp, &rbuf[0], sizeof(rbuf), len); + coap_opt_finish(&resp, COAP_OPT_FINISH_NONE); + + c = nanocoap_cache_add_by_req((const coap_pkt_t *)&req, + (const coap_pkt_t *)&resp, + CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE); + TEST_ASSERT_NOT_NULL(c); + + TEST_ASSERT_EQUAL_INT(CONFIG_NANOCOAP_CACHE_ENTRIES - 1, + nanocoap_cache_free_count()); + TEST_ASSERT_EQUAL_INT(1, nanocoap_cache_used_count()); + + /* delete previously added cache entry */ + res = nanocoap_cache_del(c); + TEST_ASSERT_EQUAL_INT(0, res); + TEST_ASSERT_EQUAL_INT(CONFIG_NANOCOAP_CACHE_ENTRIES, + nanocoap_cache_free_count()); + TEST_ASSERT_EQUAL_INT(0, nanocoap_cache_used_count()); +} + +static void test_nanocoap_cache__max_age(void) +{ + uint8_t buf[_BUF_SIZE]; + uint8_t rbuf[_BUF_SIZE]; + coap_pkt_t req, resp; + + uint16_t msgid = 0xABCD; + uint8_t token[2] = {0xDA, 0xEC}; + char *path = "/path"; + nanocoap_cache_entry_t *c = NULL; + size_t len; + uint32_t now; + + /* initialize the nanocoap cache */ + nanocoap_cache_init(); + + /* request */ + len = coap_build_hdr((coap_hdr_t *)&buf[0], COAP_TYPE_NON, + &token[0], 2, COAP_METHOD_GET, msgid); + coap_pkt_init(&req, &buf[0], sizeof(buf), len); + coap_opt_add_string(&req, COAP_OPT_URI_PATH, &path[0], '/'); + coap_opt_finish(&req, COAP_OPT_FINISH_NONE); + + /* response with max-age 30 sec */ + len = coap_build_hdr((coap_hdr_t *)&rbuf[0], COAP_TYPE_NON, + &token[0], 2, COAP_CODE_205, msgid); + coap_pkt_init(&resp, &rbuf[0], sizeof(rbuf), len); + coap_opt_add_uint(&resp, COAP_OPT_MAX_AGE, 30); + coap_opt_finish(&resp, COAP_OPT_FINISH_NONE); + + c = nanocoap_cache_add_by_req((const coap_pkt_t *)&req, + (const coap_pkt_t *)&resp, + CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE); + + /* the absolute time of max-age should be at approx. now + 30 sec + (1 sec buffer) */ + now = ztimer_now(ZTIMER_SEC); + TEST_ASSERT(c->max_age < (now + 31)); + + /* delete previously added cache entry */ + nanocoap_cache_del(c); + + /* response with max-age 60 sec (default, if option is missing) */ + len = coap_build_hdr((coap_hdr_t *)&rbuf[0], COAP_TYPE_NON, + &token[0], 2, COAP_CODE_205, msgid); + coap_pkt_init(&resp, &rbuf[0], sizeof(rbuf), len); + coap_opt_finish(&resp, COAP_OPT_FINISH_NONE); + + now = ztimer_now(ZTIMER_SEC); + + c = nanocoap_cache_add_by_req((const coap_pkt_t *)&req, + (const coap_pkt_t *)&resp, + CONFIG_NANOCOAP_CACHE_RESPONSE_SIZE); + + /* the absolute time of max-age should be at approx. now + 60 sec + (1 sec buffer) */ + now = ztimer_now(ZTIMER_SEC); + TEST_ASSERT(c->max_age < (now + 61)); +} + +Test *tests_nanocoap_cache_tests(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_nanocoap_cache__add), + new_TestFixture(test_nanocoap_cache__del), + new_TestFixture(test_nanocoap_cache__cachekey), + new_TestFixture(test_nanocoap_cache__max_age), + }; + + EMB_UNIT_TESTCALLER(nanocoap_cache_entry_tests, NULL, NULL, fixtures); + + return (Test *)&nanocoap_cache_entry_tests; +} + +void tests_nanocoap_cache(void) +{ + TESTS_RUN(tests_nanocoap_cache_tests()); +} +/** @} */ diff --git a/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.h b/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.h new file mode 100644 index 0000000000..5ca8d58c90 --- /dev/null +++ b/tests/unittests/tests-nanocoap_cache/tests-nanocoap_cache.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * @addtogroup unittests + * @{ + * + * @file + * @brief Unit tests for the nanocoap-cache module + * + * @author Cenk Gündoğan + */ +#ifndef TESTS_NANOCOAP_CACHE_H +#define TESTS_NANOCOAP_CACHE_H + +#include "embUnit.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The entry point of this test suite. + */ +void tests_nanocoap_cache(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TESTS_NANOCOAP_CACHE_H */ +/** @} */