From 9926c8cda92941dcef03caeedee85dd286738889 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Wed, 30 Mar 2022 09:32:21 +0200 Subject: [PATCH] gcoap: add nanocoap_cache support for clients --- sys/include/net/gcoap.h | 9 + sys/net/application_layer/gcoap/gcoap.c | 255 +++++++++++++++++++++++- 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/sys/include/net/gcoap.h b/sys/include/net/gcoap.h index 331e9c01dd78f..1f9ea0952c86e 100644 --- a/sys/include/net/gcoap.h +++ b/sys/include/net/gcoap.h @@ -405,6 +405,7 @@ #include "net/sock/dtls.h" #endif #include "net/nanocoap.h" +#include "net/nanocoap/cache.h" #include "timex.h" #ifdef __cplusplus @@ -809,6 +810,14 @@ struct gcoap_request_memo { event_timeout_t resp_evt_tmout; /**< Limits wait for response */ event_callback_t resp_tmout_cb; /**< Callback for response timeout */ gcoap_socket_t socket; /**< Transport type to remote endpoint */ +#if IS_USED(MODULE_NANOCOAP_CACHE) || DOXYGEN + /** + * @brief Cache key for the request + * + * @note Only available with module ['nanocoap_cache'](@ref net_nanocoap_cache) + */ + uint8_t cache_key[CONFIG_NANOCOAP_CACHE_KEY_LENGTH]; +#endif }; /** diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 23dbcba156503..0e1f06da2dc19 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -26,7 +26,9 @@ #include #include "assert.h" +#include "net/coap.h" #include "net/gcoap.h" +#include "net/nanocoap/cache.h" #include "net/sock/async/event.h" #include "net/sock/util.h" #include "mutex.h" @@ -77,6 +79,12 @@ static int _find_obs_memo(gcoap_observe_memo_t **memo, sock_udp_ep_t *remote, coap_pkt_t *pdu); static void _find_obs_memo_resource(gcoap_observe_memo_t **memo, const coap_resource_t *resource); +static nanocoap_cache_entry_t *_cache_lookup_memo(gcoap_request_memo_t *cache_key); +static void _cache_process(gcoap_request_memo_t *memo, + coap_pkt_t *pdu); +static ssize_t _cache_build_response(nanocoap_cache_entry_t *ce, coap_pkt_t *pdu, + uint8_t *buf, size_t len); +static void _receive_from_cache_cb(void *arg); static int _request_matcher_default(gcoap_listener_t *listener, const coap_resource_t **resource, @@ -130,6 +138,7 @@ static char _msg_stack[GCOAP_STACK_SIZE]; static event_queue_t _queue; static uint8_t _listen_buf[CONFIG_GCOAP_PDU_BUF_SIZE]; static sock_udp_t _sock_udp; +static event_callback_t _receive_from_cache; #if IS_USED(MODULE_GCOAP_DTLS) /* DTLS variables and definitions */ @@ -422,6 +431,37 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, event_timeout_clear(&memo->resp_evt_tmout); } memo->state = truncated ? GCOAP_MEMO_RESP_TRUNC : GCOAP_MEMO_RESP; + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + nanocoap_cache_entry_t *ce = NULL; + + if ((pdu.hdr->code == COAP_CODE_VALID) && + (ce = _cache_lookup_memo(memo))) { + /* update max_age from response and send cached response */ + uint32_t max_age = 60; + + coap_opt_get_uint(&pdu, COAP_OPT_MAX_AGE, &max_age); + ce->max_age = ztimer_now(ZTIMER_SEC) + max_age; + /* copy all options and possible payload from the cached response + * to the new response */ + assert((uint8_t *)pdu.hdr == &_listen_buf[0]); + /* redirect hdr pointer, to have the response correctly from + * request header */ + if (memo->send_limit == GCOAP_SEND_LIMIT_NON) { + pdu.hdr = (coap_hdr_t *)(&memo->msg.hdr_buf[0]); + } + else { + pdu.hdr = (coap_hdr_t *)memo->msg.data.pdu_buf; + } + if (_cache_build_response(ce, &pdu, _listen_buf, + sizeof(_listen_buf)) < 0) { + pdu.hdr = (coap_hdr_t *)_listen_buf; + memo->state = GCOAP_MEMO_ERR; + } + } + else if ((pdu.hdr->code != COAP_CODE_VALID)) { + _cache_process(memo, &pdu); + } + } if (memo->resp_handler) { memo->resp_handler(memo, &pdu, remote); } @@ -1081,6 +1121,156 @@ static ssize_t _tl_authenticate(gcoap_socket_t *sock, const sock_udp_ep_t *remot #endif } +static nanocoap_cache_entry_t *_cache_lookup_memo(gcoap_request_memo_t *memo) +{ +#if IS_USED(MODULE_NANOCOAP_CACHE) + /* cache_key in memo is pre-processor guarded so we need to as well */ + return nanocoap_cache_key_lookup(memo->cache_key); +#else + (void)memo; + return NULL; +#endif +} + +static void _cache_process(gcoap_request_memo_t *memo, + coap_pkt_t *pdu) +{ + if (!IS_USED(MODULE_NANOCOAP_CACHE)) { + return; + } + 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); +#if IS_USED(MODULE_NANOCOAP_CACHE) + /* cache_key in memo is pre-processor guarded so we need to as well */ + nanocoap_cache_process(memo->cache_key, coap_get_code(&req), pdu, pdu_len); +#else + (void)req; + (void)pdu_len; +#endif +} + +static ssize_t _cache_build_response(nanocoap_cache_entry_t *ce, coap_pkt_t *pdu, + uint8_t *buf, size_t len) +{ + if (!IS_USED(MODULE_NANOCOAP_CACHE)) { + return -ENOTSUP; + } + if (len < ce->response_len) { + return -ENOBUFS; + } + /* 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; + + /* copy all options and possible payload from the cached response + * to the new response */ + memcpy((buf + header_len_req), + (ce->response_buf + header_len_cached), + opt_payload_len); + /* parse into pdu including all options and payload pointers etc */ + coap_parse(pdu, buf, header_len_req + opt_payload_len); + return ce->response_len; +} + +static void _copy_hdr_from_req_memo(coap_pkt_t *pdu, gcoap_request_memo_t *memo) +{ + coap_pkt_t req_pdu; + + if (memo->send_limit == GCOAP_SEND_LIMIT_NON) { + req_pdu.hdr = (coap_hdr_t *)(&memo->msg.hdr_buf[0]); + } + else { + req_pdu.hdr = (coap_hdr_t *)memo->msg.data.pdu_buf; + } + memcpy(pdu->hdr, req_pdu.hdr, coap_get_total_hdr_len(&req_pdu)); +} + +static void _receive_from_cache_cb(void *ctx) +{ + if (!IS_USED(MODULE_NANOCOAP_CACHE)) { + return; + } + + gcoap_request_memo_t *memo = ctx; + nanocoap_cache_entry_t *ce = NULL; + + if ((ce = _cache_lookup_memo(memo))) { + if (memo->resp_handler) { + /* copy header from request so gcoap_resp_init in _cache_build_response works correctly + */ + coap_pkt_t pdu = { .hdr = (coap_hdr_t *)_listen_buf }; + _copy_hdr_from_req_memo(&pdu, memo); + if (_cache_build_response(ce, &pdu, _listen_buf, sizeof(_listen_buf)) >= 0) { + /* TODO somehow find out if cached response was truncated? */ + memo->state = GCOAP_MEMO_RESP; + memo->resp_handler(memo, &pdu, &memo->remote_ep); + if (memo->send_limit >= 0) { /* if confirmable */ + *memo->msg.data.pdu_buf = 0; /* clear resend PDU buffer */ + } + memo->state = GCOAP_MEMO_UNUSED; + } + } + } + else { + /* oops we somehow lost the cache entry */ + DEBUG("gcoap: cache entry was lost\n"); + if (memo->resp_handler) { + memo->state = GCOAP_MEMO_ERR; + memo->resp_handler(memo, NULL, &memo->remote_ep); + } + } +} + +static void _update_memo_cache_key(gcoap_request_memo_t *memo, uint8_t *cache_key) +{ +#if IS_USED(MODULE_NANOCOAP_CACHE) + /* memo->cache_key is guarded by MODULE_NANOCOAP_CACHE, so preprocessor magic is needed */ + memcpy(memo->cache_key, cache_key, CONFIG_NANOCOAP_CACHE_KEY_LENGTH); +#else + (void)memo; + (void)cache_key; +#endif +} + +static bool _cache_lookup_and_post(gcoap_request_memo_t *memo, + coap_pkt_t *pdu, + nanocoap_cache_entry_t **ce) +{ + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + uint8_t cache_key[SHA256_DIGEST_LENGTH]; + ztimer_now_t now = ztimer_now(ZTIMER_SEC); + + nanocoap_cache_key_generate(pdu, cache_key); + *ce = nanocoap_cache_key_lookup(cache_key); + + _update_memo_cache_key(memo, 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 */ + event_callback_init(&_receive_from_cache, _receive_from_cache_cb, memo); + event_post(&_queue, &_receive_from_cache.super); + return true; + } + } + + return false; +} + /* * gcoap interface functions */ @@ -1106,6 +1296,10 @@ kernel_pid_t gcoap_init(void) if (IS_ACTIVE(MODULE_GCOAP_FORWARD_PROXY)) { gcoap_forward_proxy_init(); } + /* gcoap_forward_proxy_init() also initializes nanocoap_cache_init() */ + else if (IS_USED(MODULE_NANOCOAP_CACHE)) { + nanocoap_cache_init(); + } return _pid; } @@ -1160,7 +1354,12 @@ int gcoap_req_init_path_buffer(coap_pkt_t *pdu, uint8_t *buf, size_t len, } coap_pkt_init(pdu, buf, len, res); - if ((path != NULL) && (path_len > 0)) { + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + static const uint8_t tmp[COAP_ETAG_LENGTH_MAX] = { 0 }; + /* add slack to maybe add an ETag on stale cache hit later */ + res = coap_opt_add_opaque(pdu, COAP_OPT_ETAG, tmp, sizeof(tmp)); + } + if ((res > 0) && (path != NULL) && (path_len > 0)) { res = coap_opt_add_uri_path_buffer(pdu, path, path_len); } return (res > 0) ? 0 : res; @@ -1249,6 +1448,60 @@ ssize_t gcoap_req_send_tl(const uint8_t *buf, size_t len, } } + if (IS_USED(MODULE_NANOCOAP_CACHE)) { + coap_pkt_t pdu; + nanocoap_cache_entry_t *ce = NULL; + bool cache_hit; + /* XXX cast to const might cause problems here :-/ */ + ssize_t res = coap_parse(&pdu, (uint8_t *)buf, len); + + if (res < 0) { + DEBUG("gcoap: parse failure for cache lookup: %d\n", (int)res); + return -EINVAL; + } + + /* This cast might be dangerous! */ + cache_hit = _cache_lookup_and_post(memo, &pdu, &ce); + + if (cache_hit) { + /* Valid cache entry found and response event was issued. Don't send request. */ + return len; + } + else if (ce != NULL) { + /* Cache entry was found, but it is stale. Try to validate */ + uint8_t *etag; + /* Searching for more ETags might become necessary in the future */ + ssize_t etag_len = coap_opt_get_opaque(&ce->response_pkt, COAP_OPT_ETAG, &etag); + + /* ETag found, but don't act on illegal ETag size */ + if ((etag_len > 0) && (etag_len <= COAP_ETAG_LENGTH_MAX)) { + uint8_t *ptr; + + coap_opt_get_opaque(&pdu, COAP_OPT_ETAG, &ptr); + memcpy(ptr, etag, etag_len); + if (etag_len < COAP_ETAG_LENGTH_MAX) { + /* now we need the start of the option (not its value) so dig once more */ + uint8_t *start = coap_find_option(&pdu, COAP_OPT_ETAG); + /* option length must always be <= COAP_ETAG_LENGTH_MAX = 8 < 12, so the length + * is encoded in the first byte, see also RFC 7252, section 3.1 */ + *start &= 0x0f; + /* first if around here should make sure we are <= 8 < 0xf, so we don't need to + * bitmask etag_len */ + *start |= (uint8_t)etag_len; + /* remove padding */ + size_t rem_len = (len - (ptr + COAP_ETAG_LENGTH_MAX - buf)); + memmove(ptr + etag_len, ptr + COAP_ETAG_LENGTH_MAX, rem_len); + len -= (COAP_ETAG_LENGTH_MAX - etag_len); + } + } + else { + len = coap_opt_remove(&pdu, COAP_OPT_ETAG); + } + } + else { + len = coap_opt_remove(&pdu, COAP_OPT_ETAG); + } + } _tl_init_coap_socket(&socket, tl_type); if (IS_USED(MODULE_GCOAP_DTLS) && socket.type == GCOAP_SOCKET_TYPE_DTLS) { res = _tl_authenticate(&socket, remote, CONFIG_GCOAP_DTLS_HANDSHAKE_TIMEOUT_MSEC);