1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-30 08:51:19 +01:00

Merge pull request #17888 from miri64/gcoap/enh/caching

gcoap: add nanocoap_cache support for clients
This commit is contained in:
Cenk Gündoğan 2022-05-13 15:39:10 +02:00 committed by GitHub
commit fb3f1a2138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 405 additions and 178 deletions

View File

@ -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
};
/**
@ -856,6 +865,12 @@ void gcoap_register_listener(gcoap_listener_t *listener);
* If @p code is COAP_CODE_EMPTY, prepares a complete "CoAP ping" 4 byte empty
* message request, ready to send.
*
* With module module [`nanocoap_cache`](@ref net_nanocoap_cache) an all-zero ETag option of
* length 8 which is updated with a value or removed in @ref gcoap_req_send() /
* @ref gcoap_req_send_tl() depending on existing cache entries for cache (re-)validation. If you do
* not use the given send functions or do not want cache entries to revalidated for any reason,
* remove that empty option using @ref coap_opt_remove().
*
* @param[out] pdu Request metadata
* @param[out] buf Buffer containing the PDU
* @param[in] len Length of the buffer
@ -880,6 +895,12 @@ int gcoap_req_init_path_buffer(coap_pkt_t *pdu, uint8_t *buf, size_t len,
* If @p code is COAP_CODE_EMPTY, prepares a complete "CoAP ping" 4 byte empty
* message request, ready to send.
*
* With module module [`nanocoap_cache`](@ref net_nanocoap_cache) an all-zero ETag option of
* length 8 which is updated with a value or removed in @ref gcoap_req_send() /
* @ref gcoap_req_send_tl() depending on existing cache entries for cache (re-)validation. If you do
* not use the given send functions or do not want cache entries to revalidated for any reason,
* remove that empty option using @ref coap_opt_remove().
*
* @param[out] pdu Request metadata
* @param[out] buf Buffer containing the PDU
* @param[in] len Length of the buffer
@ -914,9 +935,14 @@ static inline int gcoap_req_init(coap_pkt_t *pdu, uint8_t *buf, size_t len,
static inline ssize_t gcoap_request(coap_pkt_t *pdu, uint8_t *buf, size_t len,
unsigned code, char *path)
{
return (gcoap_req_init(pdu, buf, len, code, path) == 0)
? coap_opt_finish(pdu, COAP_OPT_FINISH_NONE)
: -1;
if (gcoap_req_init(pdu, buf, len, code, path) == 0) {
if (IS_USED(MODULE_NANOCOAP_CACHE)) {
/* remove ETag option slack added for cache validation */
coap_opt_remove(pdu, COAP_OPT_ETAG);
}
return coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
return -1;
}
/**

View File

@ -23,6 +23,7 @@
#define NET_NANOCOAP_CACHE_H
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "clist.h"
#include "net/nanocoap.h"
@ -80,13 +81,16 @@ typedef struct {
size_t response_len; /**< length of the message in @p response */
unsigned request_method; /**< the method of the initial request */
uint8_t request_method; /**< the method of the initial request */
#if IS_USED(MODULE_GCOAP) || defined(DOXYGEN)
bool truncated; /**< the cached response is truncated */
#endif /* IS_USED(MODULE_GCOAP) || defined(DOXYGEN) */
/**
* @brief absolute system time in seconds until which this cache entry
* is considered valid.
*/
ztimer_now_t max_age;
uint32_t max_age;
} nanocoap_cache_entry_t;
/**
@ -142,11 +146,11 @@ size_t nanocoap_cache_free_count(void);
* @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
* @return The cache entry on successfully handling the response
* @return NULL on error
*/
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 *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.
@ -227,6 +231,21 @@ void nanocoap_cache_key_generate(const coap_pkt_t *req, uint8_t *cache_key);
*/
ssize_t nanocoap_cache_key_compare(uint8_t *cache_key1, uint8_t *cache_key2);
/**
* @brief Check if the Max-Age of a cache entry has passed
*
* @param[in] ce A cache entry
* @param[in] now The current time
*
* @return true, if Max-Age of cache entry has passed.
* @return false, if Max-Age of cache entry has not yet passed.
*/
static inline bool nanocoap_cache_entry_is_stale(const nanocoap_cache_entry_t *ce, uint32_t now)
{
/* see https://en.wikipedia.org/w/index.php?title=Serial_number_arithmetic&oldid=1085516466#General_solution */
return ((int)(now - ce->max_age) > 0);
}
#ifdef __cplusplus
}
#endif

View File

@ -27,10 +27,10 @@
typedef struct {
bool in_use;
bool validating;
uint8_t req_etag_len;
sock_udp_ep_t ep;
#if IS_USED(MODULE_NANOCOAP_CACHE)
uint8_t cache_key[CONFIG_NANOCOAP_CACHE_KEY_LENGTH];
uint8_t req_etag[COAP_ETAG_LENGTH_MAX];
#endif
} client_ep_t;
@ -59,78 +59,6 @@ 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)
{
if ((pdu->hdr->code == COAP_METHOD_GET) || (pdu->hdr->code == COAP_METHOD_FETCH)) {
uint8_t *req_etag;
/* Searching for more ETags might become necessary in the future */
ssize_t req_etag_len = coap_opt_get_opaque(pdu, COAP_OPT_ETAG, &req_etag);
if (req_etag_len > 0) {
/* ETag found, validate from cache entry */
uint8_t *cache_etag;
ssize_t cache_etag_len = coap_opt_get_opaque(&ce->response_pkt, COAP_OPT_ETAG,
&cache_etag);
if ((cache_etag_len == req_etag_len) &&
(memcmp(req_etag, cache_etag, req_etag_len) == 0)) {
gcoap_resp_init(pdu, buf, len, COAP_CODE_VALID);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, req_etag, req_etag_len);
return coap_get_total_hdr_len(pdu);
}
}
}
/* 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,
nanocoap_cache_entry_t **ce)
{
(void) cep;
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);
/* 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)
@ -141,7 +69,7 @@ static client_ep_t *_allocate_client_ep(sock_udp_ep_t *ep)
cep++) {
if (!cep->in_use) {
cep->in_use = true;
cep->validating = false;
cep->req_etag_len = 0U;
memcpy(&cep->ep, ep, sizeof(*ep));
return cep;
}
@ -270,58 +198,41 @@ static void _forward_resp_handler(const gcoap_request_memo_t *memo,
{
(void) remote; /* this is the origin server */
client_ep_t *cep = (client_ep_t *)memo->context;
size_t buf_len = (pdu->payload - (uint8_t *)pdu->hdr) + pdu->payload_len;
if (memo->state == GCOAP_MEMO_RESP) {
if (!IS_USED(MODULE_NANOCOAP_CACHE) ||
/* only forward 2.03 Valid, if client endpoint sent an ETag to validate cached content */
(pdu->hdr->code != COAP_CODE_VALID) || cep->validating) {
/* forward the response packet as-is to the client */
gcoap_forward_proxy_dispatch((uint8_t *)pdu->hdr,
(pdu->payload -
(uint8_t *)pdu->hdr + pdu->payload_len),
&cep->ep);
}
#if IS_USED(MODULE_NANOCOAP_CACHE)
/* if response is a 2.03, but client endpoint did not send an ETag to validate content */
if ((pdu->hdr->code == COAP_CODE_VALID) && !cep->validating) {
nanocoap_cache_entry_t *ce = NULL;
/* req_tag in cep is pre-processor guarded so we need to as well */
if (cep->req_etag_len > 0) {
uint8_t *resp_etag;
if ((ce = nanocoap_cache_key_lookup(cep->cache_key))) {
/* 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 */
unsigned header_len_req = coap_get_total_hdr_len(pdu);
unsigned header_len_cached = coap_get_total_hdr_len(&ce->response_pkt);
uint8_t *buf = (uint8_t *)pdu->hdr;
size_t len = pdu->payload_len + header_len_req;
gcoap_resp_init(pdu, buf, len, ce->response_pkt.hdr->code);
unsigned opt_payload_len = ce->response_len - header_len_cached;
memcpy((buf + header_len_req),
(ce->response_buf + header_len_cached),
opt_payload_len);
gcoap_forward_proxy_dispatch(buf, header_len_req + opt_payload_len, &cep->ep);
}
else {
/* cache entry to be validated cached out while trying to validate */
/* TODO: re-request real response (without ETag) */
/* check if we can just send 2.03 Valid instead */
if ((cep->req_etag_len == coap_opt_get_opaque(pdu, COAP_OPT_ETAG, &resp_etag)) &&
(memcmp(cep->req_etag, resp_etag, cep->req_etag_len) == 0)) {
gcoap_resp_init(pdu, (uint8_t *)pdu->hdr, buf_len, COAP_CODE_VALID);
coap_opt_add_opaque(pdu, COAP_OPT_ETAG, cep->req_etag, cep->req_etag_len);
coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
}
else {
coap_pkt_t req;
req.hdr = gcoap_request_memo_get_hdr(memo);
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);
}
/* we do not need to check if valid came from upstream as this is already automatically
* converted by the client-side to the cached response */
#endif
/* else forward the response packet as-is to the client */
}
else if (memo->state == GCOAP_MEMO_RESP_TRUNC) {
/* the response was truncated, so there should be enough space
* to allocate an empty error message instead (with a potential Observe option) if not,
* _listen_buf is _way_ too short ;-) */
assert(buf_len >= (sizeof(*pdu->hdr) + 4U));
gcoap_resp_init(pdu, (uint8_t *)pdu->hdr, buf_len, COAP_CODE_INTERNAL_SERVER_ERROR);
coap_opt_finish(pdu, COAP_OPT_FINISH_NONE);
}
/* don't use buf_len here, in case the above `gcoap_resp_init`s changed `pdu` */
gcoap_forward_proxy_dispatch((uint8_t *)pdu->hdr,
(pdu->payload -
(uint8_t *)pdu->hdr + pdu->payload_len),
&cep->ep);
_free_client_ep(cep);
}
@ -348,38 +259,44 @@ static int _gcoap_forward_proxy_add_uri_path(coap_pkt_t *pkt,
static int _gcoap_forward_proxy_copy_options(coap_pkt_t *pkt,
coap_pkt_t *client_pkt,
client_ep_t *cep,
uri_parser_result_t *urip,
nanocoap_cache_entry_t *ce)
uri_parser_result_t *urip)
{
/* copy all options from client_pkt to pkt */
coap_optpos_t opt = {0, 0};
uint8_t *value;
bool uri_path_added = false, etag_added = false;
bool uri_path_added = false;
bool etag_added = false;
for (int i = 0; i < client_pkt->options_len; i++) {
ssize_t optlen = coap_opt_get_next(client_pkt, &opt, &value, !i);
/* wrt to ETag: we always have at least the Proxy-URI option in the client_pkt, so
* we should hit at least once (and its opt_num is also >= COAP_OPT_ETAG) */
/* wrt to ETag option slack: we always have at least the Proxy-URI option in the client_pkt,
* so we should hit at least once (and it's opt_num is also >= COAP_OPT_ETAG) */
if (optlen >= 0) {
/* Add ETag before any larger opt num, but skip old ETag if ETag in cache */
if (!etag_added && (opt.opt_num >= COAP_OPT_ETAG)) {
if (IS_USED(MODULE_NANOCOAP_CACHE) && ce) {
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);
if (etag_len > 0) {
coap_opt_add_opaque(pkt, COAP_OPT_ETAG, etag, etag_len);
}
if (IS_USED(MODULE_NANOCOAP_CACHE) && !etag_added && (opt.opt_num >= COAP_OPT_ETAG)) {
static const uint8_t tmp[COAP_ETAG_LENGTH_MAX] = { 0 };
/* add slack to maybe add an ETag on stale cache hit later, as is done in gcoap_req_send()
* (which we circumvented in _gcoap_forward_proxy_via_coap()) */
if (coap_opt_add_opaque(pkt, COAP_OPT_ETAG, tmp, sizeof(tmp))) {
etag_added = true;
}
}
/* skip original ETag of request, otherwise we might accidentally fill the cache
* with 2.03 Valid responses which would require additional handling */
#if IS_USED(MODULE_NANOCOAP_CACHE)
/* req_tag in cep is pre-processor guarded so we need to as well */
if (opt.opt_num == COAP_OPT_ETAG) {
cep->validating = true;
if (cep->req_etag_len == 0) {
/* TODO: what to do on multiple ETags? */
cep->req_etag_len = (uint8_t)optlen;
memcpy(cep->req_etag, value, optlen);
}
/* skip original ETag of request, otherwise we might accidentally fill the cache
* with 2.03 Valid responses which would require additional handling.
* For upstream validation, gcoap_req_send() will add an ETag, if the response
* was in cache */
continue;
}
#else
(void)cep;
#endif
/* add URI-PATH before any larger opt num */
if (!uri_path_added && (opt.opt_num > COAP_OPT_URI_PATH)) {
if (_gcoap_forward_proxy_add_uri_path(pkt, urip) == -EINVAL) {
@ -410,8 +327,7 @@ static int _gcoap_forward_proxy_copy_options(coap_pkt_t *pkt,
static int _gcoap_forward_proxy_via_coap(coap_pkt_t *client_pkt,
client_ep_t *client_ep,
uri_parser_result_t *urip,
nanocoap_cache_entry_t *ce)
uri_parser_result_t *urip)
{
coap_pkt_t pkt;
sock_udp_ep_t origin_server_ep;
@ -447,7 +363,7 @@ static int _gcoap_forward_proxy_via_coap(coap_pkt_t *client_pkt,
}
/* copy all options from client_pkt to pkt */
len = _gcoap_forward_proxy_copy_options(&pkt, client_pkt, client_ep, urip, ce);
len = _gcoap_forward_proxy_copy_options(&pkt, client_pkt, client_ep, urip);
if (len == -EINVAL) {
return -EINVAL;
@ -466,27 +382,11 @@ int gcoap_forward_proxy_request_process(coap_pkt_t *pkt,
ssize_t optlen = 0;
client_ep_t *cep = _allocate_client_ep(client);
nanocoap_cache_entry_t *ce = NULL;
if (!cep) {
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,
&ce);
/* 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) {
@ -505,7 +405,7 @@ int gcoap_forward_proxy_request_process(coap_pkt_t *pkt,
/* target is using CoAP */
if (!strncmp("coap", urip.scheme, urip.scheme_len)) {
int res = _gcoap_forward_proxy_via_coap(pkt, cep, &urip, ce);
int res = _gcoap_forward_proxy_via_coap(pkt, cep, &urip);
if (res < 0) {
_free_client_ep(cep);
return -EINVAL;

View File

@ -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"
@ -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 */
@ -435,6 +444,32 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_
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]);
if (_cache_build_response(ce, &pdu, _listen_buf,
sizeof(_listen_buf)) < 0) {
memo->state = GCOAP_MEMO_ERR;
}
if (ce->truncated) {
memo->state = GCOAP_MEMO_RESP_TRUNC;
}
}
/* TODO: resend request if VALID but no cache entry? */
else if ((pdu.hdr->code != COAP_CODE_VALID)) {
_cache_process(memo, &pdu);
}
}
if (memo->resp_handler) {
memo->resp_handler(memo, &pdu, remote);
}
@ -1084,6 +1119,207 @@ 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;
req.hdr = gcoap_request_memo_get_hdr(memo);
size_t pdu_len = pdu->payload_len +
(pdu->payload - (uint8_t *)pdu->hdr);
#if IS_USED(MODULE_NANOCOAP_CACHE)
nanocoap_cache_entry_t *ce;
/* cache_key in memo is pre-processor guarded so we need to as well */
if ((ce = nanocoap_cache_process(memo->cache_key, coap_get_code(&req), pdu, pdu_len))) {
ce->truncated = (memo->state == GCOAP_MEMO_RESP_TRUNC);
}
#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;
req_pdu.hdr = gcoap_request_memo_get_hdr(memo);
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) {
memo->state = (ce->truncated) ? GCOAP_MEMO_RESP_TRUNC : 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)
if (memo) {
/* 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(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)) &&
!nanocoap_cache_entry_is_stale(*ce, now)) {
return true;
}
}
return false;
}
static ssize_t _cache_check(const uint8_t *buf, size_t len,
gcoap_request_memo_t *memo,
bool *cache_hit)
{
if (!IS_USED(MODULE_NANOCOAP_CACHE)) {
return len;
}
coap_pkt_t req;
nanocoap_cache_entry_t *ce = NULL;
/* XXX cast to const might cause problems here :-/ */
ssize_t res = coap_parse(&req, (uint8_t *)buf, len);
if (res < 0) {
DEBUG("gcoap: parse failure for cache lookup: %d\n", (int)res);
return -EINVAL;
}
*cache_hit = _cache_lookup(memo, &req, &ce);
if (!(*cache_hit) && (ce != NULL)) {
/* Cache entry was found, but it is stale. Try to validate */
uint8_t *resp_etag;
/* Searching for more ETags might become necessary in the future */
ssize_t resp_etag_len = coap_opt_get_opaque(&ce->response_pkt, COAP_OPT_ETAG, &resp_etag);
/* ETag found, but don't act on illegal ETag size */
if ((resp_etag_len > 0) && ((size_t)resp_etag_len <= COAP_ETAG_LENGTH_MAX)) {
uint8_t *tmp_etag;
ssize_t tmp_etag_len = coap_opt_get_opaque(&req, COAP_OPT_ETAG, &tmp_etag);
if (tmp_etag_len >= resp_etag_len) {
memcpy(tmp_etag, resp_etag, resp_etag_len);
/* shorten ETag option if necessary */
if ((size_t)resp_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(&req, 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 resp_etag_len */
*start |= (uint8_t)resp_etag_len;
/* remove padding */
size_t rem_len = (len - (tmp_etag + COAP_ETAG_LENGTH_MAX - buf));
memmove(tmp_etag + resp_etag_len, tmp_etag + COAP_ETAG_LENGTH_MAX, rem_len);
len -= (COAP_ETAG_LENGTH_MAX - resp_etag_len);
}
}
}
else {
len = coap_opt_remove(&req, COAP_OPT_ETAG);
}
}
else {
len = coap_opt_remove(&req, COAP_OPT_ETAG);
}
return len;
}
/*
* gcoap interface functions
*/
@ -1105,6 +1341,9 @@ kernel_pid_t gcoap_init(void)
/* randomize initial value */
atomic_init(&_coap_state.next_message_id, (unsigned)random_uint32());
if (IS_USED(MODULE_NANOCOAP_CACHE)) {
nanocoap_cache_init();
}
/* initialize the forward proxy operation, if compiled */
if (IS_ACTIVE(MODULE_GCOAP_FORWARD_PROXY)) {
gcoap_forward_proxy_init();
@ -1163,7 +1402,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;
@ -1179,6 +1423,7 @@ ssize_t gcoap_req_send_tl(const uint8_t *buf, size_t len,
unsigned msg_type = (*buf & 0x30) >> 4;
uint32_t timeout = 0;
ssize_t res = 0;
bool cache_hit = false;
assert(remote != NULL);
@ -1209,6 +1454,15 @@ ssize_t gcoap_req_send_tl(const uint8_t *buf, size_t len,
memcpy(&memo->remote_ep, remote, sizeof(sock_udp_ep_t));
memo->socket = socket;
if (IS_USED(MODULE_NANOCOAP_CACHE)) {
ssize_t res = _cache_check(buf, len, memo, &cache_hit);
if (res < 0) {
return res;
}
len = res;
}
switch (msg_type) {
case COAP_TYPE_CON:
/* copy buf to resend_bufs record */
@ -1250,6 +1504,25 @@ ssize_t gcoap_req_send_tl(const uint8_t *buf, size_t len,
if (memo->state == GCOAP_MEMO_UNUSED) {
return 0;
}
if (cache_hit) {
/* post to receive cache entry */
event_callback_init(&_receive_from_cache,
_receive_from_cache_cb,
memo);
event_post(&_queue, &_receive_from_cache.super);
return len;
}
}
/* check cache without memo */
else if (IS_USED(MODULE_NANOCOAP_CACHE)) {
ssize_t res = _cache_check(buf, len, NULL, &cache_hit);
if (res < 0) {
return res;
}
if (cache_hit > 0) {
return res;
}
}
_tl_init_coap_socket(&socket, tl_type);

View File

@ -163,8 +163,8 @@ nanocoap_cache_entry_t *nanocoap_cache_request_lookup(const coap_pkt_t *req)
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 *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);
@ -218,14 +218,14 @@ int nanocoap_cache_process(const uint8_t *cache_key, unsigned request_method,
ETag Option for validation.
*/
else if (resp->hdr->code == COAP_CODE_CONTENT) {
if (NULL == nanocoap_cache_add_by_key(cache_key, request_method,
resp, resp_len)) {
if ((ce = nanocoap_cache_add_by_key(cache_key, request_method,
resp, resp_len)) == NULL) {
/* no space left in the cache? */
return -1;
return NULL;
}
}
return 0;
return ce;
}
static nanocoap_cache_entry_t *_nanocoap_cache_pop(void)
{

View File

@ -22,6 +22,12 @@
#include "unittests-constants.h"
#include "tests-gcoap.h"
#if IS_USED(MODULE_NANOCOAP_CACHE)
#define ETAG_SLACK 9 /* account for ETag slack implicitly added by gcoap_req_init() */
#else
#define ETAG_SLACK 0
#endif
/*
* A test set of dummy resources. The resource handlers are set to NULL.
*/
@ -161,7 +167,7 @@ static void test_gcoap__client_put_req(void)
static void test_gcoap__client_put_req_overfill(void)
{
/* header 4, token 2, path 11, format 1, marker 1 = 19 */
uint8_t buf[18];
uint8_t buf[18 + ETAG_SLACK];
coap_pkt_t pdu;
ssize_t len;
char path[] = "/riot/value";
@ -194,7 +200,7 @@ static void test_gcoap__client_get_path_defer(void)
len = coap_opt_finish(&pdu, COAP_OPT_FINISH_NONE);
TEST_ASSERT_EQUAL_INT(len,
sizeof(coap_hdr_t) + CONFIG_GCOAP_TOKENLEN +optlen);
sizeof(coap_hdr_t) + CONFIG_GCOAP_TOKENLEN + ETAG_SLACK + optlen);
coap_parse(&pdu, buf, len);
@ -222,7 +228,7 @@ static void test_gcoap__client_ping(void)
/* confirm length */
res = coap_opt_finish(&pdu, COAP_OPT_FINISH_NONE);
TEST_ASSERT_EQUAL_INT(4, res);
TEST_ASSERT_EQUAL_INT(4 + ETAG_SLACK, res);
}
/*

View File

@ -231,7 +231,7 @@ static void test_nanocoap_cache__max_age(void)
/* 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));
TEST_ASSERT(nanocoap_cache_entry_is_stale(c, now + 31));
/* delete previously added cache entry */
nanocoap_cache_del(c);
@ -251,7 +251,10 @@ static void test_nanocoap_cache__max_age(void)
/* 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_ASSERT(nanocoap_cache_entry_is_stale(c, now + 61));
/* check overflow cases */
c->max_age = UINT32_MAX - 40;
TEST_ASSERT(nanocoap_cache_entry_is_stale(c, 20));
}
Test *tests_nanocoap_cache_tests(void)