From ffee1fa1f2e29f0ec1a2eaeebc964d58c086b206 Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 15:19:48 +0100 Subject: [PATCH 1/9] examples/gcoap: add option to register observe as a client --- examples/gcoap/client.c | 126 ++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 49 deletions(-) diff --git a/examples/gcoap/client.c b/examples/gcoap/client.c index 491a905ef8..c8ea3d6140 100644 --- a/examples/gcoap/client.c +++ b/examples/gcoap/client.c @@ -52,6 +52,15 @@ static char proxy_uri[64]; #define _LAST_REQ_PATH_MAX (64) static char _last_req_path[_LAST_REQ_PATH_MAX]; +/* whether this node is currently observing a resource as a client */ +static bool observing = false; + +/* the token used for observing a remote resource */ +static uint8_t obs_req_token[GCOAP_TOKENLEN_MAX]; + +/* actual length of above token */ +static size_t obs_req_tkl = 0; + uint16_t req_count = 0; /* @@ -145,31 +154,9 @@ static void _resp_handler(const gcoap_request_memo_t *memo, coap_pkt_t* pdu, } } -static size_t _send(uint8_t *buf, size_t len, char *addr_str) +static size_t _send(uint8_t *buf, size_t len, sock_udp_ep_t *remote) { size_t bytes_sent; - sock_udp_ep_t *remote; - sock_udp_ep_t new_remote; - - if (_proxied) { - remote = &_proxy_remote; - } - else { - if (sock_udp_name2ep(&new_remote, addr_str) != 0) { - return 0; - } - - if (new_remote.port == 0) { - if (IS_USED(MODULE_GCOAP_DTLS)) { - new_remote.port = CONFIG_GCOAPS_PORT; - } - else { - new_remote.port = CONFIG_GCOAP_PORT; - } - } - - remote = &new_remote; - } bytes_sent = gcoap_req_send(buf, len, remote, _resp_handler, NULL); if (bytes_sent > 0) { @@ -180,10 +167,27 @@ static size_t _send(uint8_t *buf, size_t len, char *addr_str) static int _print_usage(char **argv) { - printf("usage: %s \n", argv[0]); + printf("usage: %s \n", argv[0]); return 1; } +static int _addrstr2remote(const char *addr_str, sock_udp_ep_t *remote) +{ + if (sock_udp_name2ep(remote, addr_str) != 0) { + return -1; + } + + if (remote->port == 0) { + if (IS_USED(MODULE_GCOAP_DTLS)) { + remote->port = CONFIG_GCOAPS_PORT; + } + else { + remote->port = CONFIG_GCOAP_PORT; + } + } + return 0; +} + int gcoap_cli_cmd(int argc, char **argv) { /* Ordered like the RFC method code numbers, but off by 1. GET is code 0. */ @@ -191,6 +195,9 @@ int gcoap_cli_cmd(int argc, char **argv) uint8_t buf[CONFIG_GCOAP_PDU_BUF_SIZE]; coap_pkt_t pdu; size_t len; + unsigned observe = false; + uint32_t obs_value = COAP_OBS_REGISTER; + sock_udp_ep_t remote; if (argc == 1) { /* show help for main commands */ @@ -275,6 +282,20 @@ int gcoap_cli_cmd(int argc, char **argv) /* parse options */ int apos = 2; /* position of address argument */ + + if (code_pos == COAP_METHOD_GET) { + if (argc > apos) { + if (strcmp(argv[apos], "-o") == 0) { + if (observing) { + puts("Only one observe supported"); + return 1; + } + observe = true; + apos++; + } + } + } + /* ping must be confirmable */ unsigned msg_type = (!code_pos ? COAP_TYPE_CON : COAP_TYPE_NON); if (argc > apos && strcmp(argv[apos], "-c") == 0) { @@ -287,6 +308,12 @@ int gcoap_cli_cmd(int argc, char **argv) ((argc == apos + 2 || argc == apos + 3) && (code_pos > 1))) { /* post or put */ + /* get unproxied endpoint from address string */ + if (_addrstr2remote(argv[apos], &remote)) { + printf("'%s' is not a valid address\n", argv[apos]); + return _print_usage(argv); + } + char *uri = NULL; int uri_len = 0; if (code_pos) { @@ -300,43 +327,44 @@ int gcoap_cli_cmd(int argc, char **argv) } if (_proxied) { - sock_udp_ep_t tmp_remote; - if (sock_udp_name2ep(&tmp_remote, argv[apos]) != 0) { - return _print_usage(argv); - } - - if (tmp_remote.port == 0) { - if (IS_USED(MODULE_GCOAP_DTLS)) { - tmp_remote.port = CONFIG_GCOAPS_PORT; - } - else { - tmp_remote.port = CONFIG_GCOAP_PORT; - } - } - #ifdef SOCK_HAS_IPV6 char addrstr[IPV6_ADDR_MAX_STR_LEN]; #else char addrstr[IPV4_ADDR_MAX_STR_LEN]; #endif - inet_ntop(tmp_remote.family, &tmp_remote.addr, addrstr, sizeof(addrstr)); + inet_ntop(remote.family, &remote.addr, addrstr, sizeof(addrstr)); - if (tmp_remote.family == AF_INET6) { + if (remote.family == AF_INET6) { uri_len = snprintf(proxy_uri, sizeof(proxy_uri), "coap://[%s]:%d%s", - addrstr, tmp_remote.port, uri); + addrstr, remote.port, uri); } else { uri_len = snprintf(proxy_uri, sizeof(proxy_uri), "coap://%s:%d%s", - addrstr, tmp_remote.port, uri); + addrstr, remote.port, uri); } uri = proxy_uri; + } - gcoap_req_init(&pdu, &buf[0], CONFIG_GCOAP_PDU_BUF_SIZE, code_pos, NULL); - } - else { - gcoap_req_init(&pdu, &buf[0], CONFIG_GCOAP_PDU_BUF_SIZE, code_pos, uri); + gcoap_req_init(&pdu, buf, CONFIG_GCOAP_PDU_BUF_SIZE, code_pos, + (_proxied || observe) ? NULL : uri); + + if (observe) { + uint8_t *token = coap_get_token(&pdu); + if (obs_value == COAP_OBS_REGISTER) { + obs_req_tkl = coap_get_token_len(&pdu); + /* backup the token of the initial observe registration */ + memcpy(obs_req_token, token, obs_req_tkl); + observing = true; + } + coap_opt_add_uint(&pdu, COAP_OPT_OBSERVE, obs_value); + if (!_proxied) { + /* add uri path option separately + * (options must be added in order) */ + coap_opt_add_string(&pdu, COAP_OPT_URI_PATH, uri, '/'); + } } + coap_hdr_set_type(pdu.hdr, msg_type); memset(_last_req_path, 0, _LAST_REQ_PATH_MAX); @@ -369,8 +397,8 @@ int gcoap_cli_cmd(int argc, char **argv) } printf("gcoap_cli: sending msg ID %u, %" PRIuSIZE " bytes\n", - coap_get_id(&pdu), len); - if (!_send(&buf[0], len, argv[apos])) { + coap_get_id(&pdu), len); + if (!_send(&buf[0], len, _proxied ? &_proxy_remote : &remote)) { puts("gcoap_cli: msg send failed"); } else { @@ -380,7 +408,7 @@ int gcoap_cli_cmd(int argc, char **argv) return 0; } else { - printf("usage: %s [-c] [:port] [data]\n", + printf("usage: %s [-c] [:port] [data]\n", argv[0]); printf(" %s ping [:port]\n", argv[0]); printf("Options\n"); From c003e5ed9c884a0907c10b134bab2e963d00c024 Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 17:28:35 +0100 Subject: [PATCH 2/9] gcoap: keep request on observe notification --- sys/net/application_layer/gcoap/gcoap.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 1aa0431032..69e53d522d 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -409,7 +409,8 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ } /* validate class and type for incoming */ - switch (coap_get_code_class(&pdu)) { + unsigned code_class = coap_get_code_class(&pdu); + switch (code_class) { /* incoming request or empty */ case COAP_CLASS_REQ: if (coap_get_code_raw(&pdu) == COAP_CODE_EMPTY) { @@ -498,6 +499,9 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ _cache_process(memo, &pdu); } } + + bool observe_notification = coap_has_observe(&pdu); + if (memo->resp_handler) { memo->resp_handler(memo, &pdu, remote); } @@ -505,7 +509,15 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ if (memo->send_limit >= 0) { /* if confirmable */ *memo->msg.data.pdu_buf = 0; /* clear resend PDU buffer */ } - memo->state = GCOAP_MEMO_UNUSED; + + /* The memo must be kept if the response is an observe notification. + * Non-2.xx notifications indicate that the associated observe entry + * was removed on the server side. Then also free the memo here. */ + if (!observe_notification || (code_class != COAP_CLASS_SUCCESS)) { + /* setting the state to unused frees (drops) the memo entry */ + memo->state = GCOAP_MEMO_UNUSED; + } + break; default: DEBUG("gcoap: illegal response type: %u\n", coap_get_type(&pdu)); From 5956e93d5893c64cd418122673d9e925e71f1693 Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 18:26:39 +0100 Subject: [PATCH 3/9] gcoap: split find-memo utility functions This splits the _find_req_memo util function into multiple variants that match on different things. This is done in preparation of a feature that has to find a request based on a token value, without creating an artificial pdu for that. A nice side effect is that it also makes the calls to the find functions a bit more readable by not relying on an anonymous bool input. --- sys/net/application_layer/gcoap/gcoap.c | 93 ++++++++++++++++++------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 69e53d522d..6046091ed3 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -68,8 +68,12 @@ static void _cease_retransmission(gcoap_request_memo_t *memo); static size_t _handle_req(gcoap_socket_t *sock, coap_pkt_t *pdu, uint8_t *buf, size_t len, sock_udp_ep_t *remote); static void _expire_request(gcoap_request_memo_t *memo); -static void _find_req_memo(gcoap_request_memo_t **memo_ptr, coap_pkt_t *pdu, - const sock_udp_ep_t *remote, bool by_mid); +static gcoap_request_memo_t* _find_req_memo_by_mid(const sock_udp_ep_t *remote, + uint16_t mid); +static gcoap_request_memo_t* _find_req_memo_by_token(const sock_udp_ep_t *remote, + const uint8_t *token, size_t tkl); +static gcoap_request_memo_t* _find_req_memo_by_pdu_token(const coap_pkt_t *src_pdu, + const sock_udp_ep_t *remote); static int _find_resource(gcoap_socket_type_t tl_type, coap_pkt_t *pdu, const coap_resource_t **resource_ptr, @@ -401,7 +405,7 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ if (coap_get_type(&pdu) == COAP_TYPE_RST) { DEBUG("gcoap: received RST, expiring potentially existing memo\n"); - _find_req_memo(&memo, &pdu, remote, true); + memo = _find_req_memo_by_mid(remote, pdu.hdr->id); if (memo) { event_timeout_clear(&memo->resp_evt_tmout); _expire_request(memo); @@ -419,7 +423,7 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ messagelayer_emptyresponse_type = COAP_TYPE_RST; DEBUG("gcoap: Answering empty CON request with RST\n"); } else if (coap_get_type(&pdu) == COAP_TYPE_ACK) { - _find_req_memo(&memo, &pdu, remote, true); + memo = _find_req_memo_by_mid(remote, pdu.hdr->id); if ((memo != NULL) && (memo->send_limit != GCOAP_SEND_LIMIT_NON)) { DEBUG("gcoap: empty ACK processed, stopping retransmissions\n"); _cease_retransmission(memo); @@ -460,7 +464,7 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ case COAP_CLASS_SUCCESS: case COAP_CLASS_CLIENT_FAILURE: case COAP_CLASS_SERVER_FAILURE: - _find_req_memo(&memo, &pdu, remote, false); + memo = _find_req_memo_by_pdu_token(&pdu, remote); if (memo) { switch (coap_get_type(&pdu)) { case COAP_TYPE_CON: @@ -860,20 +864,18 @@ static int _find_resource(gcoap_socket_type_t tl_type, * Finds the memo for an outstanding request within the _coap_state.open_reqs * array. Matches on remote endpoint and token. * - * memo_ptr[out] -- Registered request memo, or NULL if not found - * src_pdu[in] -- PDU for token to match - * remote[in] -- Remote endpoint to match - * by_mid[in] -- true if matches are to be done based on Message ID, otherwise they are done by - * token + * remote[in] Remote endpoint to match + * token[in] Token to match + * tkl[in] Length of the token in bytes + * + * return Registered request memo, or NULL if not found */ -static void _find_req_memo(gcoap_request_memo_t **memo_ptr, coap_pkt_t *src_pdu, - const sock_udp_ep_t *remote, bool by_mid) +static gcoap_request_memo_t* _find_req_memo_by_token(const sock_udp_ep_t *remote, + const uint8_t *token, size_t tkl) { - *memo_ptr = NULL; /* no need to initialize struct; we only care about buffer contents below */ coap_pkt_t memo_pdu_data; coap_pkt_t *memo_pdu = &memo_pdu_data; - unsigned cmplen = coap_get_token_len(src_pdu); for (int i = 0; i < CONFIG_GCOAP_REQ_WAITING_MAX; i++) { if (_coap_state.open_reqs[i].state == GCOAP_MEMO_UNUSED) { @@ -881,25 +883,64 @@ static void _find_req_memo(gcoap_request_memo_t **memo_ptr, coap_pkt_t *src_pdu, } gcoap_request_memo_t *memo = &_coap_state.open_reqs[i]; - memo_pdu->hdr = gcoap_request_memo_get_hdr(memo); - if (by_mid) { - if ((src_pdu->hdr->id == memo_pdu->hdr->id) - && sock_udp_ep_equal(&memo->remote_ep, remote)) { - *memo_ptr = memo; - break; - } - } else if (coap_get_token_len(memo_pdu) == cmplen) { - if ((memcmp(coap_get_token(src_pdu), coap_get_token(memo_pdu), cmplen) == 0) + + if (coap_get_token_len(memo_pdu) == tkl) { + if ((memcmp(token, coap_get_token(memo_pdu), tkl) == 0) && (sock_udp_ep_equal(&memo->remote_ep, remote) /* Multicast addresses are not considered in matching responses */ || sock_udp_ep_is_multicast(&memo->remote_ep) )) { - *memo_ptr = memo; - break; + return memo; } } } + return NULL; +} + +/* + * Utility wrapper for _find_req_memo_by_token(), using the pdu token. + * Finds the memo for an outstanding request within the _coap_state.open_reqs + * array. Matches on remote endpoint and token of the pdu. + * + * src_pdu[in] PDU which holds the token for matching + * remote[in] Remote endpoint to match + * + * return Registered request memo, or NULL if not found + */ +static gcoap_request_memo_t* _find_req_memo_by_pdu_token( + const coap_pkt_t *src_pdu, + const sock_udp_ep_t *remote) +{ + unsigned tkl = coap_get_token_len(src_pdu); + uint8_t *token = coap_get_token(src_pdu); + return _find_req_memo_by_token(remote, token, tkl); +} + +/* + * Finds the memo for an outstanding request within the _coap_state.open_reqs + * array. Matches on remote endpoint and message ID. + * + * remote[in] Remote endpoint to match + * mid[in] Message ID to match + * + * return Registered request memo, or NULL if not found + */ +static gcoap_request_memo_t* _find_req_memo_by_mid(const sock_udp_ep_t *remote, uint16_t mid) +{ + for (int i = 0; i < CONFIG_GCOAP_REQ_WAITING_MAX; i++) { + if (_coap_state.open_reqs[i].state == GCOAP_MEMO_UNUSED) { + continue; + } + + gcoap_request_memo_t *memo = &_coap_state.open_reqs[i]; + + if ((mid == gcoap_request_memo_get_hdr(memo)->id) && + sock_udp_ep_equal(&memo->remote_ep, remote)) { + return memo; + } + } + return NULL; } /* Calls handler callback on receipt of a timeout message. */ @@ -1818,7 +1859,7 @@ void gcoap_forward_proxy_find_req_memo(gcoap_request_memo_t **memo_ptr, coap_pkt_t *src_pdu, const sock_udp_ep_t *remote) { - _find_req_memo(memo_ptr, src_pdu, remote, false); + *memo_ptr = _find_req_memo_by_pdu_token(src_pdu, remote); } void gcoap_forward_proxy_post_event(void *arg) From ab821a1ddad25edaf67562a2de1f830ed7929bdf Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 18:33:55 +0100 Subject: [PATCH 4/9] gcoap: add api to forget a client-side observe request --- sys/include/net/gcoap.h | 31 +++++++++++++++++++++++++ sys/net/application_layer/gcoap/gcoap.c | 17 ++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/sys/include/net/gcoap.h b/sys/include/net/gcoap.h index 96f7229312..9618d12bb4 100644 --- a/sys/include/net/gcoap.h +++ b/sys/include/net/gcoap.h @@ -1074,6 +1074,37 @@ int gcoap_obs_init(coap_pkt_t *pdu, uint8_t *buf, size_t len, size_t gcoap_obs_send(const uint8_t *buf, size_t len, const coap_resource_t *resource); +/** + * @brief Forgets (invalidates) an existing observe request. + * + * This invalidates the internal (local) observe request state without actually + * sending a deregistration request to the server. Ths mechanism may be referred + * to as passive deregistration, as it does not send a deregistration request. + * This is implemented according to the description in RFC 7641, + * Section 3.6 (Cancellation): 'A client that is no longer interested in + * receiving notifications for a resource can simply "forget" the observation.' + * Successfully invalidating the request by calling this function guarantees + * that the corresponding observe response handler will not be called anymore. + * + * NOTE: There are cases were active deregistration is preferred instead. + * A server may continue sending notifications if it chooses to ignore the RST + * which is meant to indicate the client did not recognize the notification. + * For such server implementations this function must be called *before* + * sending an explicit deregister request (i.e., a GET request with the token + * of the registration and the observe option set to COAP_OBS_DEREGISTER). + * This will instruct the server to stop sending further notifications. + * + * @param[in] remote remote endpoint that hosts the observed resource + * @param[in] token token of the original GET request used for registering + * an observe + * @param[in] tokenlen the length of the token in bytes + * + * @return 0 on success + * @return < 0 on error + */ +int gcoap_obs_req_forget(const sock_udp_ep_t *remote, const uint8_t *token, + size_t tokenlen); + /** * @brief Provides important operational statistics * diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 6046091ed3..2d782685ea 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -1535,6 +1535,23 @@ int gcoap_req_init_path_buffer(coap_pkt_t *pdu, uint8_t *buf, size_t len, return (res > 0) ? 0 : res; } +int gcoap_obs_req_forget(const sock_udp_ep_t *remote, const uint8_t *token, + size_t tokenlen) { + int res = -ENOENT; + gcoap_request_memo_t *obs_req_memo; + mutex_lock(&_coap_state.lock); + /* Find existing request memo of the observe */ + obs_req_memo = _find_req_memo_by_token(remote, token, tokenlen); + if (obs_req_memo) { + /* forget the existing observe memo. */ + obs_req_memo->state = GCOAP_MEMO_UNUSED; + res = 0; + } + + mutex_unlock(&_coap_state.lock); + return res; +} + ssize_t gcoap_req_send_tl(const uint8_t *buf, size_t len, const sock_udp_ep_t *remote, gcoap_resp_handler_t resp_handler, void *context, From 021986c08717a1f6bde1d63ed8ebe922715365ee Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 18:36:21 +0100 Subject: [PATCH 5/9] examples/gcoap: allow deregistering observe request as a client --- examples/gcoap/client.c | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/examples/gcoap/client.c b/examples/gcoap/client.c index c8ea3d6140..3135899a25 100644 --- a/examples/gcoap/client.c +++ b/examples/gcoap/client.c @@ -167,7 +167,7 @@ static size_t _send(uint8_t *buf, size_t len, sock_udp_ep_t *remote) static int _print_usage(char **argv) { - printf("usage: %s \n", argv[0]); + printf("usage: %s \n", argv[0]); return 1; } @@ -283,6 +283,8 @@ int gcoap_cli_cmd(int argc, char **argv) /* parse options */ int apos = 2; /* position of address argument */ + /* For GET requests additional switches allow for registering and + * deregistering an observe. This example only supports one observe. */ if (code_pos == COAP_METHOD_GET) { if (argc > apos) { if (strcmp(argv[apos], "-o") == 0) { @@ -292,6 +294,14 @@ int gcoap_cli_cmd(int argc, char **argv) } observe = true; apos++; + } else if (strcmp(argv[apos], "-d") == 0) { + if (!observing) { + puts("Not observing"); + return 1; + } + observe = true; + apos++; + obs_value = COAP_OBS_DEREGISTER; } } } @@ -356,12 +366,22 @@ int gcoap_cli_cmd(int argc, char **argv) /* backup the token of the initial observe registration */ memcpy(obs_req_token, token, obs_req_tkl); observing = true; - } - coap_opt_add_uint(&pdu, COAP_OPT_OBSERVE, obs_value); - if (!_proxied) { - /* add uri path option separately - * (options must be added in order) */ - coap_opt_add_string(&pdu, COAP_OPT_URI_PATH, uri, '/'); + coap_opt_add_uint(&pdu, COAP_OPT_OBSERVE, obs_value); + if (!_proxied) { + /* add uri path option separately + * (options must be added in order) */ + coap_opt_add_uri_path(&pdu, uri); + } + } else { + /* use the token of the registration for deregistration */ + memcpy(token, obs_req_token, obs_req_tkl); + if (gcoap_obs_req_forget(&remote, obs_req_token, obs_req_tkl)) { + printf("could not remove observe request\n"); + return 1; + } + observing = false; + /* deregistration does not require further actions */ + return 0; } } @@ -408,7 +428,7 @@ int gcoap_cli_cmd(int argc, char **argv) return 0; } else { - printf("usage: %s [-c] [:port] [data]\n", + printf("usage: %s [-c] [:port] [data]\n", argv[0]); printf(" %s ping [:port]\n", argv[0]); printf("Options\n"); From 94e8786715233f7b4a83321b2902747dab790d9f Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 18:56:53 +0100 Subject: [PATCH 6/9] gcoap: issue RST on unknown observe notifications --- sys/net/application_layer/gcoap/gcoap.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 2d782685ea..98814ef114 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -536,6 +536,13 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ messagelayer_emptyresponse_type = COAP_TYPE_RST; DEBUG("gcoap: Answering unknown CON response with RST to " "shut up sender\n"); + } else { + /* if the response was a (NON) observe notification and there is no + * matching request, the server must be informed that this node is + * no longer interested in this notification. */ + if (coap_has_observe(&pdu)) { + messagelayer_emptyresponse_type = COAP_TYPE_RST; + } } } break; From 45a84af3469be0bbabf2d5dfd8049cb938a5d221 Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 9 Nov 2023 19:14:38 +0100 Subject: [PATCH 7/9] gcoap: clear observe state on RST response to notification In order to properly handle an observe cancellation of a client, the server has to keep track of the notification MIDs (to be able to match an RST to a notification), see [RFC7641, 3.6 Cancellation](https://www.rfc-editor.org/rfc/rfc7641.html#section-3.6) for mor details. An alternative to this would be to make either the client send an explicit observe deregister request, or make the server send the next notification via CON (which hten allows matching of the RST due to the CON state). --- sys/include/net/gcoap.h | 1 + sys/net/application_layer/gcoap/gcoap.c | 57 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/sys/include/net/gcoap.h b/sys/include/net/gcoap.h index 9618d12bb4..3c8384aff8 100644 --- a/sys/include/net/gcoap.h +++ b/sys/include/net/gcoap.h @@ -837,6 +837,7 @@ typedef struct { sock_udp_ep_t *observer; /**< Client endpoint; unused if null */ const coap_resource_t *resource; /**< Entity being observed */ uint8_t token[GCOAP_TOKENLEN_MAX]; /**< Client token for notifications */ + uint16_t last_msgid; /**< Message ID of last notification */ unsigned token_len; /**< Actual length of token attribute */ gcoap_socket_t socket; /**< Transport type to observer */ } gcoap_observe_memo_t; diff --git a/sys/net/application_layer/gcoap/gcoap.c b/sys/net/application_layer/gcoap/gcoap.c index 98814ef114..dae1a3e7fd 100644 --- a/sys/net/application_layer/gcoap/gcoap.c +++ b/sys/net/application_layer/gcoap/gcoap.c @@ -83,6 +83,10 @@ 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 void _check_and_expire_obs_memo_last_mid(sock_udp_ep_t *remote, + uint16_t last_notify_mid); + 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); @@ -410,6 +414,10 @@ static void _process_coap_pdu(gcoap_socket_t *sock, sock_udp_ep_t *remote, sock_ event_timeout_clear(&memo->resp_evt_tmout); _expire_request(memo); } + + /* check if this RST is due to the client not being interested + * in receiving observe notifications anymore. */ + _check_and_expire_obs_memo_last_mid(remote, coap_get_id(&pdu)); } /* validate class and type for incoming */ @@ -1063,6 +1071,51 @@ static int _find_obs_memo(gcoap_observe_memo_t **memo, sock_udp_ep_t *remote, return empty_slot; } +/* + * Checks if an observe memo exists for which a notification with the given + * msg ID was sent out. If so, it expires the memo and frees up the + * observer entry if needed. + * + * remote[in] The remote to check for a stale observe memo. + * last_notify_mid[in] The message ID of the last notification send to the + * given remote. + */ +static void _check_and_expire_obs_memo_last_mid(sock_udp_ep_t *remote, + uint16_t last_notify_mid) +{ + /* find observer entry from remote */ + sock_udp_ep_t *observer; + _find_observer(&observer, remote); + + if (observer) { + gcoap_observe_memo_t *stale_obs_memo = NULL; + /* get the observe memo corresponding to the notification with the + * given msg ID. */ + for (unsigned i = 0; i < CONFIG_GCOAP_OBS_REGISTRATIONS_MAX; i++) { + if (_coap_state.observe_memos[i].observer == NULL) { + continue; + } + if ((_coap_state.observe_memos[i].observer == observer) && + (last_notify_mid == _coap_state.observe_memos[i].last_msgid)) { + stale_obs_memo = &_coap_state.observe_memos[i]; + break; + } + } + + if (stale_obs_memo) { + stale_obs_memo->observer = NULL; /* clear memo */ + + /* check if the observer has more observe memos registered... */ + stale_obs_memo = NULL; + _find_obs_memo(&stale_obs_memo, observer, NULL); + if (stale_obs_memo == NULL) { + /* ... if not -> also free the observer entry */ + observer->family = AF_UNSPEC; + } + } + } +} + /* * Find registered observe memo for a resource. * @@ -1755,6 +1808,10 @@ int gcoap_obs_init(coap_pkt_t *pdu, uint8_t *buf, size_t len, coap_pkt_init(pdu, buf, len, hdrlen); _add_generated_observe_option(pdu); + /* Store message ID of the last notification sent. This is needed + * to match a potential RST returned by a client in order to signal + * it does not recognize this notification. */ + memo->last_msgid = msgid; return GCOAP_OBS_INIT_OK; } From 645ee83727251b41d9cdaf672b9c084dd7c7f9c4 Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 16 Nov 2023 14:10:41 +0100 Subject: [PATCH 8/9] examples/gcoap: send deregistration request with -d --- examples/gcoap/client.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/gcoap/client.c b/examples/gcoap/client.c index 3135899a25..8f4039baef 100644 --- a/examples/gcoap/client.c +++ b/examples/gcoap/client.c @@ -356,8 +356,7 @@ int gcoap_cli_cmd(int argc, char **argv) uri = proxy_uri; } - gcoap_req_init(&pdu, buf, CONFIG_GCOAP_PDU_BUF_SIZE, code_pos, - (_proxied || observe) ? NULL : uri); + gcoap_req_init(&pdu, buf, CONFIG_GCOAP_PDU_BUF_SIZE, code_pos, NULL); if (observe) { uint8_t *token = coap_get_token(&pdu); @@ -365,24 +364,23 @@ int gcoap_cli_cmd(int argc, char **argv) obs_req_tkl = coap_get_token_len(&pdu); /* backup the token of the initial observe registration */ memcpy(obs_req_token, token, obs_req_tkl); - observing = true; - coap_opt_add_uint(&pdu, COAP_OPT_OBSERVE, obs_value); - if (!_proxied) { - /* add uri path option separately - * (options must be added in order) */ - coap_opt_add_uri_path(&pdu, uri); - } } else { - /* use the token of the registration for deregistration */ + /* use the token of the registration for deregistration + * (manually replace the token set by gcoap_req_init) */ memcpy(token, obs_req_token, obs_req_tkl); if (gcoap_obs_req_forget(&remote, obs_req_token, obs_req_tkl)) { printf("could not remove observe request\n"); return 1; } - observing = false; - /* deregistration does not require further actions */ - return 0; } + + coap_opt_add_uint(&pdu, COAP_OPT_OBSERVE, obs_value); + } + + if (!_proxied) { + /* add uri path option separately + * (options must be added in order) */ + coap_opt_add_uri_path(&pdu, uri); } coap_hdr_set_type(pdu.hdr, msg_type); @@ -422,6 +420,11 @@ int gcoap_cli_cmd(int argc, char **argv) puts("gcoap_cli: msg send failed"); } else { + if (observe) { + /* on successful observe request, store that this node is + * observing / not observing anymore */ + observing = obs_value == COAP_OBS_REGISTER; + } /* send Observe notification for /cli/stats */ notify_observers(); } From 12982a0f14abe4ed93654008afa2913dd341125d Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Thu, 1 Feb 2024 15:23:55 +0100 Subject: [PATCH 9/9] gcoap: update documentation on supported features --- sys/include/net/gcoap.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sys/include/net/gcoap.h b/sys/include/net/gcoap.h index 3c8384aff8..8c63709491 100644 --- a/sys/include/net/gcoap.h +++ b/sys/include/net/gcoap.h @@ -371,7 +371,8 @@ * - Message Type: Supports non-confirmable (NON) messaging. Additionally * provides a callback on timeout. Provides piggybacked ACK response to a * confirmable (CON) request. - * - Observe extension: Provides server-side registration and notifications. + * - Observe extension: Provides server-side registration and notifications + * and client-side observe. * - Server and Client provide helper functions for writing the * response/request. See the CoAP topic in the source documentation for * details. See the gcoap example for sample implementations.