Skip to content

Commit

Permalink
gcoap: add nanocoap_cache support for clients
Browse files Browse the repository at this point in the history
  • Loading branch information
miri64 committed Apr 1, 2022
1 parent 3a32386 commit 9926c8c
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 1 deletion.
9 changes: 9 additions & 0 deletions sys/include/net/gcoap.h
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@
#include "net/sock/dtls.h"
#endif
#include "net/nanocoap.h"
#include "net/nanocoap/cache.h"
#include "timex.h"

#ifdef __cplusplus
Expand Down Expand Up @@ -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
};

/**
Expand Down
255 changes: 254 additions & 1 deletion sys/net/application_layer/gcoap/gcoap.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
#include <string.h>

#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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
*/
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 9926c8c

Please sign in to comment.