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:
commit
fb3f1a2138
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user