From 98f930c793c0eca9bfc9d3d166989b1f79c3bf53 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Fri, 28 Feb 2020 00:46:43 -0500 Subject: [PATCH] [coap] add support for RFC7641 (#4396) * [coap] Add option scanning functions In a few places in the OpenThread code-base, the following construct is seen: ``` otCoapOptionIterator iterator; otCoapOptionIteratorInit(&iterator, message); for (const otCoapOption *opt = otCoapOptionIteratorGetFirstOption(&iterator); opt != NULL; opt = otCoapOptionIteratorGetNextOption(&iterator)) { if (opt->mNumber == OT_COAP_OPTION_DESIREDOPTION) { doSomethingWith(opt); break; } } ``` or its equivalent. In cases where multiple options are sought, the above is still the better approach (using a `switch` statement), but otherwise this seems repetitive. `otCoapOptionIteratorGetFirstOptionMatching` basically implements the above loop, returning the first option that matches. This same code can also be used for testing for presence too by comparing the resultant pointer to the `NULL` singleton. For completeness in cases where a software developer may want to *resume* looking for further matching options, `otCoapOptionIteratorGetNextOptionMatching` is also provided. * [coap] Add function for decoding Uint options To properly implement features such as RFC7641 and RFC7959, one must be able to decode unsigned integer options. It seems silly to have an encoder but not a decoder, so here is the missing piece. Future considerations may be a `uint32_t` version since the majority of use cases do not call for a `uint64_t`; the type was chosen since it was the most general case. * [coap] Reference the RFC for the Observe option. For the sake of completeness, reference the relevant RFC in the documentation so that developers can read up more on how it should work. * [coap] Add mObserve flag to metadata. When we're implementing observations, a few exceptions to the usual CoAP conventions apply, notably: - when we receive the initial reply, the request is _not_ finished, we will receive subsequent requests until we tell the server to stop. - when we send a confirmable notification, the observing client will reply with an `ACK` with no further traffic: we need to differentiate this from an empty `ACK` to a request meaning we should expect a reply later. While we could just repeatedly scan for the `Observe` option on the request, that is wasteful in terms of processing time. This allows us to scan the request just once, and set a single-bit flag to save processing later. * [coap] Don't expect traffic after ACK to notification If the client sends an empty `ACK` in reply to a confirmable RFC7641 notification, we will _NOT_ see further replies with additional information. Consider the notification as acknowledged and pass the details back to the application. * [coap] Pass through notifications to handler Ensure that all notifications sent by the CoAP server are passed back to the application's CoAP request handler. When we receive the first one, the request continues -- it does not finish until the server drops the `Observe` flag or we tell the server to stop. * [coap] Disable expiry timer once subscribed If a request carries `Observe=0` and we receive the first reply, ignore future time-out events as the transaction will finish only when explicitly terminated. * [coap] Support eager subscription cancellation A CoAP client may signal its intention to unsubscribe from a resource by sending a `GET` request with a token matching that of the original request and setting the `Observe` option to the value 1. In the event the application does this, we should pick this up and cancel the pending transaction locally. The handler will be notified of this by a final call where `aError=OT_ERROR_NONE` and no message given. * [cli] Add RFC7641 support to CLI example As an aide to those developing code that uses RFC7641, implement an example client and server that supports this feature. --- examples/Makefile-simulation | 1 + examples/common-switches.mk | 5 + include/openthread/coap.h | 42 ++- src/cli/README_COAP.md | 39 +++ src/cli/cli_coap.cpp | 317 +++++++++++++++++- src/cli/cli_coap.hpp | 33 +- src/core/api/coap_api.cpp | 16 + src/core/coap/coap.cpp | 136 +++++++- src/core/coap/coap.hpp | 8 +- src/core/coap/coap_message.cpp | 54 +++ src/core/coap/coap_message.hpp | 41 +++ src/core/config/coap.h | 10 + src/posix/Makefile-posix | 1 + tests/scripts/thread-cert/Makefile.am | 3 + tests/scripts/thread-cert/config.py | 1 + tests/scripts/thread-cert/node.py | 177 ++++++++++ .../scripts/thread-cert/test_coap_observe.py | 159 +++++++++ 17 files changed, 1014 insertions(+), 29 deletions(-) create mode 100755 tests/scripts/thread-cert/test_coap_observe.py diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation index 2ee983eed..0a71ab0e5 100644 --- a/examples/Makefile-simulation +++ b/examples/Makefile-simulation @@ -38,6 +38,7 @@ DEBUG ?= 0 BORDER_AGENT ?= 1 BORDER_ROUTER ?= 1 COAP ?= 1 +COAP_OBSERVE ?= 1 COAPS ?= 1 COMMISSIONER ?= 1 CHANNEL_MANAGER ?= 1 diff --git a/examples/common-switches.mk b/examples/common-switches.mk index c5506469e..e8e7dd646 100644 --- a/examples/common-switches.mk +++ b/examples/common-switches.mk @@ -32,6 +32,7 @@ BIG_ENDIAN ?= 0 BORDER_AGENT ?= 0 BORDER_ROUTER ?= 0 COAP ?= 0 +COAP_OBSERVE ?= 0 COAPS ?= 0 COMMISSIONER ?= 0 COVERAGE ?= 0 @@ -90,6 +91,10 @@ ifeq ($(COAPS),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE=1 endif +ifeq ($(COAP_OBSERVE),1) +COMMONCFLAGS += -DOPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE=1 +endif + ifeq ($(COMMISSIONER),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_COMMISSIONER_ENABLE=1 endif diff --git a/include/openthread/coap.h b/include/openthread/coap.h index 22ba3d247..8bbea6291 100644 --- a/include/openthread/coap.h +++ b/include/openthread/coap.h @@ -127,7 +127,7 @@ typedef enum otCoapOptionType OT_COAP_OPTION_URI_HOST = 3, ///< Uri-Host OT_COAP_OPTION_E_TAG = 4, ///< ETag OT_COAP_OPTION_IF_NONE_MATCH = 5, ///< If-None-Match - OT_COAP_OPTION_OBSERVE = 6, ///< Observe + OT_COAP_OPTION_OBSERVE = 6, ///< Observe [RFC7641] OT_COAP_OPTION_URI_PORT = 7, ///< Uri-Port OT_COAP_OPTION_LOCATION_PATH = 8, ///< Location-Path OT_COAP_OPTION_URI_PATH = 11, ///< Uri-Path @@ -491,6 +491,7 @@ otError otCoapMessageAppendOption(otMessage *aMessage, uint16_t aNumber, uint16_ * @retval OT_ERROR_INVALID_ARGS The option type is not equal or greater than the last option type. * @retval OT_ERROR_NO_BUFS The option length exceeds the buffer size. * + * @see otCoapMessageGetOptionUintValue */ otError otCoapMessageAppendUintOption(otMessage *aMessage, uint16_t aNumber, uint32_t aValue); @@ -681,6 +682,18 @@ const uint8_t *otCoapMessageGetToken(const otMessage *aMessage); */ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessage *aMessage); +/** + * This function returns a pointer to the first option matching the specified option number. + * + * @param[in] aIterator A pointer to the CoAP message option iterator. + * @param[in] aOption The option number sought. + * + * @returns A pointer to the first matching option. If no matching option is present NULL pointer is returned. + * + */ +const otCoapOption *otCoapOptionIteratorGetFirstOptionMatching(otCoapOptionIterator *aIterator, + otCoapOptionType aOption); + /** * This function returns a pointer to the first option. * @@ -691,6 +704,18 @@ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessag */ const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIterator); +/** + * This function returns a pointer to the next option matching the specified option number. + * + * @param[in] aIterator A pointer to the CoAP message option iterator. + * @param[in] aOption The option number sought. + * + * @returns A pointer to the next matching option. If no further matching option is present NULL pointer is returned. + * + */ +const otCoapOption *otCoapOptionIteratorGetNextOptionMatching(otCoapOptionIterator *aIterator, + otCoapOptionType aOption); + /** * This function returns a pointer to the next option. * @@ -701,6 +726,21 @@ const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIt */ const otCoapOption *otCoapOptionIteratorGetNextOption(otCoapOptionIterator *aIterator); +/** + * This function fills current option value into @p aValue assuming the current value is an unsigned integer encoded + * according to https://tools.ietf.org/html/rfc7252#section-3.2 + * + * @param[inout] aIterator A pointer to the CoAP message option iterator. + * @param[out] aValue A pointer to an unsigned integer to receive the option value. + * + * @retval OT_ERROR_NONE Successfully filled value. + * @retval OT_ERROR_NOT_FOUND No current option. + * @retval OT_ERROR_NO_BUFS Value is too long to fit in a uint64_t. + * + * @see otCoapMessageAppendUintOption + */ +otError otCoapOptionIteratorGetOptionUintValue(otCoapOptionIterator *aIterator, uint64_t *const aValue); + /** * This function fills current option value into @p aValue. * diff --git a/src/cli/README_COAP.md b/src/cli/README_COAP.md index d335ce18a..209746b9c 100644 --- a/src/cli/README_COAP.md +++ b/src/cli/README_COAP.md @@ -55,12 +55,15 @@ coap response sent ## Command List * [help](#help) +* [cancel](#cancel) * [delete](#delete-address-uri-path-type-payload) * [get](#get-address-uri-path-type) +* [observe](#observe-address-uri-path-type) * [parameters](#parameters) * [post](#post-address-uri-path-type-payload) * [put](#put-address-uri-path-type-payload) * [resource](#resource-uri-path) +* [set](#set-new-content) * [start](#start) * [stop](#stop) @@ -71,12 +74,15 @@ coap response sent ```bash > coap help help +cancel delete get +observe parameters post put resource +set start stop Done @@ -84,6 +90,16 @@ Done List the CoAP CLI commands. +### cancel + +Request the cancellation of an existing observation subscription to a remote +resource. + +```bash +> coap cancel +Done +``` + ### delete \ \ \[type\] \[payload\] * address: IPv6 address of the CoAP server. @@ -107,6 +123,20 @@ Done Done ``` +### observe \ \ \[type\] + +This is the same a `get`, but the `Observe` parameter will be sent, set to 0 +triggering a subscription request. + +* address: IPv6 address of the CoAP server. +* uri-path: URI path of the resource. +* type: "con" for Confirmable or "non-con" for Non-confirmable (default). + +```bash +> coap observe fdde:ad00:beef:0:2780:9423:166c:1aac test-resource +Done +``` + ### parameters \ \["default"| \] Sets transmission parameters for the following interactions. @@ -180,6 +210,15 @@ test-resource Done ``` +### set \[new-content\] + +Sets the content sent by the test resource. If a CoAP client is observing the resource, a notification is sent to that client. + +```bash +> coap set Testing123 +Done +``` + ### start Starts the application coap service. diff --git a/src/cli/cli_coap.cpp b/src/cli/cli_coap.cpp index 2828a32b5..106d7a677 100644 --- a/src/cli/cli_coap.cpp +++ b/src/cli/cli_coap.cpp @@ -45,10 +45,21 @@ namespace ot { namespace Cli { const struct Coap::Command Coap::sCommands[] = { - {"help", &Coap::ProcessHelp}, {"delete", &Coap::ProcessRequest}, - {"get", &Coap::ProcessRequest}, {"parameters", &Coap::ProcessParameters}, - {"post", &Coap::ProcessRequest}, {"put", &Coap::ProcessRequest}, - {"resource", &Coap::ProcessResource}, {"start", &Coap::ProcessStart}, + {"help", &Coap::ProcessHelp}, +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + {"cancel", &Coap::ProcessCancel}, +#endif + {"delete", &Coap::ProcessRequest}, + {"get", &Coap::ProcessRequest}, +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + {"observe", &Coap::ProcessRequest}, +#endif + {"parameters", &Coap::ProcessParameters}, + {"post", &Coap::ProcessRequest}, + {"put", &Coap::ProcessRequest}, + {"resource", &Coap::ProcessResource}, + {"set", &Coap::ProcessSet}, + {"start", &Coap::ProcessStart}, {"stop", &Coap::ProcessStop}, }; @@ -56,10 +67,70 @@ Coap::Coap(Interpreter &aInterpreter) : mInterpreter(aInterpreter) , mUseDefaultRequestTxParameters(true) , mUseDefaultResponseTxParameters(true) +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + , mObserveSerial(0) + , mRequestTokenLength(0) + , mSubscriberTokenLength(0) + , mSubscriberConfirmableNotifications(false) +#endif { memset(&mResource, 0, sizeof(mResource)); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + memset(&mRequestAddr, 0, sizeof(mRequestAddr)); + memset(&mSubscriberSock, 0, sizeof(mSubscriberSock)); + memset(&mRequestToken, 0, sizeof(mRequestToken)); + memset(&mSubscriberToken, 0, sizeof(mSubscriberToken)); + memset(&mRequestUri, 0, sizeof(mRequestUri)); +#endif + memset(&mUriPath, 0, sizeof(mUriPath)); + strncpy(mResourceContent, "0", sizeof(mResourceContent) - 1); } +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE +otError Coap::CancelResourceSubscription(void) +{ + otError error = OT_ERROR_NONE; + otMessage * message = NULL; + otMessageInfo messageInfo; + + memset(&messageInfo, 0, sizeof(messageInfo)); + messageInfo.mPeerAddr = mRequestAddr; + messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT; + + VerifyOrExit(mRequestTokenLength != 0, error = OT_ERROR_INVALID_STATE); + + message = otCoapNewMessage(mInterpreter.mInstance, NULL); + VerifyOrExit(message != NULL, error = OT_ERROR_NO_BUFS); + + otCoapMessageInit(message, OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_GET); + + SuccessOrExit(error = otCoapMessageSetToken(message, mRequestToken, mRequestTokenLength)); + SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 1)); + SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, mRequestUri)); + SuccessOrExit(error = + otCoapSendRequest(mInterpreter.mInstance, message, &messageInfo, &Coap::HandleResponse, this)); + + memset(&mRequestAddr, 0, sizeof(mRequestAddr)); + memset(&mRequestUri, 0, sizeof(mRequestUri)); + mRequestTokenLength = 0; + +exit: + + if ((error != OT_ERROR_NONE) && (message != NULL)) + { + otMessageFree(message); + } + + return error; +} + +void Coap::CancelSubscriber(void) +{ + memset(&mSubscriberSock, 0, sizeof(mSubscriberSock)); + mSubscriberTokenLength = 0; +} +#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + void Coap::PrintPayload(otMessage *aMessage) const { uint8_t buf[kMaxBufferSize]; @@ -86,6 +157,16 @@ void Coap::PrintPayload(otMessage *aMessage) const mInterpreter.mServer->OutputFormat("\r\n"); } +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE +otError Coap::ProcessCancel(int argc, char *argv[]) +{ + OT_UNUSED_VARIABLE(argc); + OT_UNUSED_VARIABLE(argv); + + return CancelResourceSubscription(); +} +#endif + otError Coap::ProcessHelp(int argc, char *argv[]) { OT_UNUSED_VARIABLE(argc); @@ -123,6 +204,67 @@ exit: return OT_ERROR_NONE; } +otError Coap::ProcessSet(int argc, char *argv[]) +{ +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + otMessage * notificationMessage = NULL; + otMessageInfo messageInfo; +#endif + otError error = OT_ERROR_NONE; + + if (argc > 1) + { + VerifyOrExit(strlen(argv[1]) < (kMaxBufferSize - 1), error = OT_ERROR_INVALID_ARGS); + strncpy(mResourceContent, argv[1], sizeof(mResourceContent) - 1); + +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (mSubscriberTokenLength > 0) + { + // Notify the subscriber + memset(&messageInfo, 0, sizeof(messageInfo)); + messageInfo.mPeerAddr = mSubscriberSock.mAddress; + messageInfo.mPeerPort = mSubscriberSock.mPort; + + mInterpreter.mServer->OutputFormat("sending coap notification to "); + mInterpreter.OutputIp6Address(mSubscriberSock.mAddress); + mInterpreter.mServer->OutputFormat("\r\n"); + + notificationMessage = otCoapNewMessage(mInterpreter.mInstance, NULL); + VerifyOrExit(notificationMessage != NULL, error = OT_ERROR_NO_BUFS); + + otCoapMessageInit( + notificationMessage, + ((mSubscriberConfirmableNotifications) ? OT_COAP_TYPE_CONFIRMABLE : OT_COAP_TYPE_NON_CONFIRMABLE), + OT_COAP_CODE_CONTENT); + + SuccessOrExit(error = otCoapMessageSetToken(notificationMessage, mSubscriberToken, mSubscriberTokenLength)); + SuccessOrExit(error = otCoapMessageAppendObserveOption(notificationMessage, mObserveSerial++)); + SuccessOrExit(error = otCoapMessageSetPayloadMarker(notificationMessage)); + SuccessOrExit(error = otMessageAppend(notificationMessage, mResourceContent, + static_cast(strlen(mResourceContent)))); + + SuccessOrExit(error = otCoapSendRequest(mInterpreter.mInstance, notificationMessage, &messageInfo, + &Coap::HandleNotificationResponse, this)); + } +#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + } + else + { + mInterpreter.mServer->OutputFormat("%s\r\n", mResourceContent); + } + +exit: + +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if ((error != OT_ERROR_NONE) && (notificationMessage != NULL)) + { + otMessageFree(notificationMessage); + } +#endif + + return error; +} + otError Coap::ProcessStart(int argc, char *argv[]) { OT_UNUSED_VARIABLE(argc); @@ -226,6 +368,9 @@ otError Coap::ProcessRequest(int argc, char *argv[]) otCoapType coapType = OT_COAP_TYPE_NON_CONFIRMABLE; otCoapCode coapCode = OT_COAP_CODE_GET; otIp6Address coapDestinationIp; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + bool coapObserve = false; +#endif VerifyOrExit(argc > 0, error = OT_ERROR_INVALID_ARGS); @@ -234,6 +379,14 @@ otError Coap::ProcessRequest(int argc, char *argv[]) { coapCode = OT_COAP_CODE_GET; } +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + else if (strcmp(argv[0], "observe") == 0) + { + // Observe request. This is a GET with Observe=0 + coapCode = OT_COAP_CODE_GET; + coapObserve = true; + } +#endif else if (strcmp(argv[0], "post") == 0) { coapCode = OT_COAP_CODE_POST; @@ -281,11 +434,27 @@ otError Coap::ProcessRequest(int argc, char *argv[]) } } +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (coapObserve && mRequestTokenLength) + { + // New observe request, cancel any existing observation + SuccessOrExit(error = CancelResourceSubscription()); + } +#endif + message = otCoapNewMessage(mInterpreter.mInstance, NULL); VerifyOrExit(message != NULL, error = OT_ERROR_NO_BUFS); otCoapMessageInit(message, coapType, coapCode); otCoapMessageGenerateToken(message, ot::Coap::Message::kDefaultTokenLength); + +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (coapObserve) + { + SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 0)); + } +#endif + SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, coapUri)); if (argc > 4) @@ -308,6 +477,18 @@ otError Coap::ProcessRequest(int argc, char *argv[]) messageInfo.mPeerAddr = coapDestinationIp; messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (coapObserve) + { + // Make a note of the message details for later so we can cancel it later. + memcpy(&mRequestAddr, &coapDestinationIp, sizeof(mRequestAddr)); + mRequestTokenLength = otCoapMessageGetTokenLength(message); + memcpy(mRequestToken, otCoapMessageGetToken(message), mRequestTokenLength); + strncpy(mRequestUri, coapUri, sizeof(mRequestUri) - 1); + mRequestUri[sizeof(mRequestUri) - 1] = '\0'; // Fix gcc-9.2 warning + } +#endif + if ((coapType == OT_COAP_TYPE_CONFIRMABLE) || (coapCode == OT_COAP_CODE_GET)) { error = otCoapSendRequestWithParameters(mInterpreter.mInstance, message, &messageInfo, &Coap::HandleResponse, @@ -363,7 +544,11 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo) otError error = OT_ERROR_NONE; otMessage *responseMessage = NULL; otCoapCode responseCode = OT_COAP_CODE_EMPTY; - char responseContent = '0'; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + uint64_t observe = 0; + bool observePresent = false; + otCoapOptionIterator iterator; +#endif mInterpreter.mServer->OutputFormat("coap request from "); mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr); @@ -373,6 +558,16 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo) { case OT_COAP_CODE_GET: mInterpreter.mServer->OutputFormat("GET"); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + SuccessOrExit(error = otCoapOptionIteratorInit(&iterator, aMessage)); + if (otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE) != NULL) + { + SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observe)); + observePresent = true; + + mInterpreter.mServer->OutputFormat(" OBS=%lu", static_cast(observe)); + } +#endif break; case OT_COAP_CODE_DELETE: @@ -397,9 +592,51 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo) if (otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE || otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET) { - if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET) +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (observePresent && (mSubscriberTokenLength > 0) && (observe == 0)) + { + // There is already a subscriber + responseCode = OT_COAP_CODE_SERVICE_UNAVAILABLE; + } + else +#endif + if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET) { responseCode = OT_COAP_CODE_CONTENT; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (observePresent) + { + if (observe == 0) + { + // New subscriber + mInterpreter.mServer->OutputFormat("Subscribing client\r\n"); + mSubscriberSock.mAddress = aMessageInfo->mPeerAddr; + mSubscriberSock.mPort = aMessageInfo->mPeerPort; + mSubscriberTokenLength = otCoapMessageGetTokenLength(aMessage); + memcpy(mSubscriberToken, otCoapMessageGetToken(aMessage), mSubscriberTokenLength); + + /* + * Implementer note. + * + * Here, we try to match a confirmable GET request with confirmable + * notifications, however this is not a requirement of RFC7641: + * the server can send notifications of either type regardless of + * what the client used to subscribe initially. + */ + mSubscriberConfirmableNotifications = (otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE); + } + else if (observe == 1) + { + // See if it matches our subscriber token + if ((otCoapMessageGetTokenLength(aMessage) == mSubscriberTokenLength) && + (memcmp(otCoapMessageGetToken(aMessage), mSubscriberToken, mSubscriberTokenLength) == 0)) + { + // Unsubscribe request + CancelSubscriber(); + } + } + } +#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE } else { @@ -412,10 +649,17 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo) SuccessOrExit( error = otCoapMessageInitResponse(responseMessage, aMessage, OT_COAP_TYPE_ACKNOWLEDGMENT, responseCode)); - if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET) + if (responseCode == OT_COAP_CODE_CONTENT) { +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (observePresent && (observe == 0)) + { + SuccessOrExit(error = otCoapMessageAppendObserveOption(responseMessage, mObserveSerial++)); + } +#endif SuccessOrExit(error = otCoapMessageSetPayloadMarker(responseMessage)); - SuccessOrExit(error = otMessageAppend(responseMessage, &responseContent, sizeof(responseContent))); + SuccessOrExit(error = otMessageAppend(responseMessage, mResourceContent, + static_cast(strlen(mResourceContent)))); } SuccessOrExit(error = otCoapSendResponseWithParameters(mInterpreter.mInstance, responseMessage, aMessageInfo, @@ -439,6 +683,39 @@ exit: } } +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE +void Coap::HandleNotificationResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aError) +{ + static_cast(aContext)->HandleNotificationResponse(aMessage, aMessageInfo, aError); +} + +void Coap::HandleNotificationResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError) +{ + OT_UNUSED_VARIABLE(aMessage); + + switch (aError) + { + case OT_ERROR_NONE: + if (aMessageInfo != NULL) + { + mInterpreter.mServer->OutputFormat("Received ACK in reply to notification from "); + mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr); + mInterpreter.mServer->OutputFormat("\r\n"); + } + break; + + default: + mInterpreter.mServer->OutputFormat("coap receive notification response error %d: %s\r\n", aError, + otThreadErrorToString(aError)); + CancelSubscriber(); + break; + } +} +#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + void Coap::HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError) { static_cast(aContext)->HandleResponse(aMessage, aMessageInfo, aError); @@ -451,11 +728,33 @@ void Coap::HandleResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo mInterpreter.mServer->OutputFormat("coap receive response error %d: %s\r\n", aError, otThreadErrorToString(aError)); } - else + else if ((aMessageInfo != NULL) && (aMessage != NULL)) { +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + otCoapOptionIterator iterator; +#endif + mInterpreter.mServer->OutputFormat("coap response from "); mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (otCoapOptionIteratorInit(&iterator, aMessage) == OT_ERROR_NONE) + { + const otCoapOption *observeOpt = + otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE); + + if (observeOpt != NULL) + { + uint64_t observeVal = 0; + otError error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observeVal); + + if (error == OT_ERROR_NONE) + { + mInterpreter.mServer->OutputFormat(" OBS=%u", observeVal); + } + } + } +#endif PrintPayload(aMessage); } } diff --git a/src/cli/cli_coap.hpp b/src/cli/cli_coap.hpp index b3d84b046..2029f2829 100644 --- a/src/cli/cli_coap.hpp +++ b/src/cli/cli_coap.hpp @@ -82,18 +82,35 @@ private: otError (Coap::*mCommand)(int argc, char *argv[]); }; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + otError CancelResourceSubscription(void); + void CancelSubscriber(void); +#endif + void PrintPayload(otMessage *aMessage) const; otError ProcessHelp(int argc, char *argv[]); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + otError ProcessCancel(int argc, char *argv[]); +#endif otError ProcessParameters(int argc, char *argv[]); otError ProcessRequest(int argc, char *argv[]); otError ProcessResource(int argc, char *argv[]); + otError ProcessSet(int argc, char *argv[]); otError ProcessStart(int argc, char *argv[]); otError ProcessStop(int argc, char *argv[]); static void HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); void HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + static void HandleNotificationResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aError); + void HandleNotificationResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError); +#endif + static void HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError); void HandleResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError); @@ -117,7 +134,21 @@ private: otCoapTxParameters mResponseTxParameters; otCoapResource mResource; - char mUriPath[kMaxUriLength]; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + otIp6Address mRequestAddr; + otSockAddr mSubscriberSock; + char mRequestUri[kMaxUriLength]; + uint8_t mRequestToken[OT_COAP_MAX_TOKEN_LENGTH]; + uint8_t mSubscriberToken[OT_COAP_MAX_TOKEN_LENGTH]; +#endif + char mUriPath[kMaxUriLength]; + char mResourceContent[kMaxBufferSize]; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + uint32_t mObserveSerial; + uint8_t mRequestTokenLength; + uint8_t mSubscriberTokenLength; + bool mSubscriberConfirmableNotifications; +#endif }; } // namespace Cli diff --git a/src/core/api/coap_api.cpp b/src/core/api/coap_api.cpp index 9844ead9e..0c8dce530 100644 --- a/src/core/api/coap_api.cpp +++ b/src/core/api/coap_api.cpp @@ -180,16 +180,32 @@ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessag return static_cast(aIterator)->Init(static_cast(aMessage)); } +const otCoapOption *otCoapOptionIteratorGetFirstOptionMatching(otCoapOptionIterator *aIterator, + otCoapOptionType aOption) +{ + return static_cast(aIterator)->GetFirstOptionMatching(aOption); +} + const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIterator) { return static_cast(aIterator)->GetFirstOption(); } +const otCoapOption *otCoapOptionIteratorGetNextOptionMatching(otCoapOptionIterator *aIterator, otCoapOptionType aOption) +{ + return static_cast(aIterator)->GetNextOptionMatching(aOption); +} + const otCoapOption *otCoapOptionIteratorGetNextOption(otCoapOptionIterator *aIterator) { return static_cast(aIterator)->GetNextOption(); } +otError otCoapOptionIteratorGetOptionUintValue(otCoapOptionIterator *aIterator, uint64_t *const aValue) +{ + return static_cast(aIterator)->GetOptionValue(*aValue); +} + otError otCoapOptionIteratorGetOptionValue(otCoapOptionIterator *aIterator, void *aValue) { return static_cast(aIterator)->GetOptionValue(aValue); diff --git a/src/core/coap/coap.cpp b/src/core/coap/coap.cpp index f700df8cd..3ab3f1cc2 100644 --- a/src/core/coap/coap.cpp +++ b/src/core/coap/coap.cpp @@ -169,7 +169,45 @@ otError CoapBase::SendMessage(Message & aMessage, { Metadata metadata; - metadata.Init(aMessage.IsConfirmable(), aMessageInfo, aHandler, aContext, aTxParameters); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + // Whether or not to turn on special "Observe" handling. + OptionIterator iterator; + bool observe; + + SuccessOrExit(error = iterator.Init(&aMessage)); + observe = (iterator.GetFirstOptionMatching(OT_COAP_OPTION_OBSERVE) != NULL); + + // Special case, if we're sending a GET with Observe=1, that is a cancellation. + if (observe && (aMessage.GetCode() == OT_COAP_CODE_GET)) + { + uint64_t observeVal = 0; + + SuccessOrExit(error = iterator.GetOptionValue(observeVal)); + + if (observeVal == 1) + { + Metadata handlerMetadata; + + // We're cancelling our subscription, so disable special-case handling on this request. + observe = false; + + // If we can find the previous handler context, cancel that too. Peer address + // and tokens, etc should all match. + Message *origRequest = FindRelatedRequest(aMessage, aMessageInfo, handlerMetadata); + if (origRequest != NULL) + { + FinalizeCoapTransaction(*origRequest, handlerMetadata, NULL, NULL, OT_ERROR_NONE); + } + } + } +#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + + // Enqueue and send + metadata.Init(aMessage.IsConfirmable(), +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + observe, +#endif + aMessageInfo, aHandler, aContext, aTxParameters); storedCopy = CopyAndEnqueueMessage(aMessage, copyLength, metadata); VerifyOrExit(storedCopy != NULL, error = OT_ERROR_NO_BUFS); } @@ -300,6 +338,14 @@ void CoapBase::HandleRetransmissionTimer(void) if (now >= metadata.mNextTimerShot) { +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (message->IsRequest() && metadata.mObserve && metadata.mAcknowledged) + { + // This is a RFC7641 subscription. Do not time out. + continue; + } +#endif + if (!metadata.mConfirmable || (metadata.mRetransmissionsRemaining == 0)) { // No expected response or acknowledgment. @@ -501,10 +547,24 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo Metadata metadata; Message *request = NULL; otError error = OT_ERROR_NONE; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + bool responseObserve = false; +#endif request = FindRelatedRequest(aMessage, aMessageInfo, metadata); VerifyOrExit(request != NULL); +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (metadata.mObserve && request->IsRequest()) + { + // We sent Observe in our request, see if we received Observe in the response too. + OptionIterator iterator; + + SuccessOrExit(error = iterator.Init(&aMessage)); + responseObserve = (iterator.GetFirstOptionMatching(OT_COAP_OPTION_OBSERVE) != NULL); + } +#endif + switch (aMessage.GetType()) { case OT_COAP_TYPE_RESET: @@ -520,22 +580,54 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo if (aMessage.IsEmpty()) { // Empty acknowledgment. - if (metadata.mConfirmable) +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (metadata.mObserve && !request->IsRequest()) { - metadata.mAcknowledged = true; - metadata.UpdateIn(*request); + // This is the ACK to our RFC7641 notification. There will be no + // "separate" response so pass it back as if it were a piggy-backed + // response so we can stop re-sending and the application can move on. + FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE); } - - // Remove the message if response is not expected, otherwise await response. - if (metadata.mResponseHandler == NULL) + else +#endif { - DequeueMessage(*request); + // This is not related to RFC7641 or the outgoing "request" was not a + // notification. + if (metadata.mConfirmable) + { + metadata.mAcknowledged = true; + metadata.UpdateIn(*request); + } + + // Remove the message if response is not expected, otherwise await + // response. + if (metadata.mResponseHandler == NULL) + { + DequeueMessage(*request); + } } } else if (aMessage.IsResponse() && aMessage.IsTokenEqual(*request)) { - // Piggybacked response. - FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE); + // Piggybacked response. If there's an Observe option present in both + // request and response, and we have a response handler; then we're + // dealing with RFC7641 rules here. + // (If there is no response handler, then we're wasting our time!) +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + if (metadata.mObserve && responseObserve && (metadata.mResponseHandler != NULL)) + { + // This is a RFC7641 notification. The request is *not* done! + metadata.mResponseHandler(metadata.mResponseContext, &aMessage, &aMessageInfo, OT_ERROR_NONE); + + // Consider the message acknowledged at this point. + metadata.mAcknowledged = true; + metadata.UpdateIn(*request); + } + else +#endif + { + FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE); + } } // Silently ignore acknowledgments carrying requests (RFC 7252, p. 4.2) @@ -545,13 +637,17 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo case OT_COAP_TYPE_CONFIRMABLE: // Send empty ACK if it is a CON message. SendAck(aMessage, aMessageInfo); - FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE); - break; - + // Fall through + // Handling of RFC7641 and multicast is below. case OT_COAP_TYPE_NON_CONFIRMABLE: - // Separate response. - - if (metadata.mDestinationAddress.IsMulticast() && metadata.mResponseHandler != NULL) + // Separate response or observation notification. If the request was to a multicast + // address, OR both the request and response carry Observe options, then this is NOT + // the final message, we may see multiples. + if ((metadata.mResponseHandler != NULL) && (metadata.mDestinationAddress.IsMulticast() +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + || (metadata.mObserve && responseObserve) +#endif + )) { // If multicast non-confirmable request, allow multiple responses metadata.mResponseHandler(metadata.mResponseContext, &aMessage, &aMessageInfo, OT_ERROR_NONE); @@ -664,7 +760,10 @@ exit: } } -void CoapBase::Metadata::Init(bool aConfirmable, +void CoapBase::Metadata::Init(bool aConfirmable, +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + bool aObserve, +#endif const Ip6::MessageInfo &aMessageInfo, ResponseHandler aHandler, void * aContext, @@ -679,6 +778,9 @@ void CoapBase::Metadata::Init(bool aConfirmable, mRetransmissionTimeout = aTxParameters.CalculateInitialRetransmissionTimeout(); mAcknowledged = false; mConfirmable = aConfirmable; +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + mObserve = aObserve; +#endif mNextTimerShot = TimerMilli::GetNow() + (aConfirmable ? mRetransmissionTimeout : aTxParameters.CalculateMaxTransmitWait()); } diff --git a/src/core/coap/coap.hpp b/src/core/coap/coap.hpp index bde7aa62a..259815196 100644 --- a/src/core/coap/coap.hpp +++ b/src/core/coap/coap.hpp @@ -525,7 +525,10 @@ protected: private: struct Metadata { - void Init(bool aConfirmable, + void Init(bool aConfirmable, +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + bool aObserve, +#endif const Ip6::MessageInfo &aMessageInfo, ResponseHandler aHandler, void * aContext, @@ -545,6 +548,9 @@ private: uint8_t mRetransmissionsRemaining; // Number of retransmissions remaining. bool mAcknowledged : 1; // Information that request was acknowledged. bool mConfirmable : 1; // Information that message is confirmable. +#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + bool mObserve : 1; // Information that this request involves Observations. +#endif }; static void HandleRetransmissionTimer(Timer &aTimer); diff --git a/src/core/coap/coap_message.cpp b/src/core/coap/coap_message.cpp index a32722a5d..00b115512 100644 --- a/src/core/coap/coap_message.cpp +++ b/src/core/coap/coap_message.cpp @@ -432,6 +432,23 @@ exit: return err; } +const otCoapOption *OptionIterator::GetFirstOptionMatching(otCoapOptionType aOption) +{ + const otCoapOption *rval = NULL; + + for (const otCoapOption *option = GetFirstOption(); option != NULL; option = GetNextOption()) + { + if (option->mNumber == aOption) + { + // Found, stop searching + rval = option; + break; + } + } + + return rval; +} + const otCoapOption *OptionIterator::GetFirstOption(void) { const otCoapOption *option = NULL; @@ -449,6 +466,23 @@ const otCoapOption *OptionIterator::GetFirstOption(void) return option; } +const otCoapOption *OptionIterator::GetNextOptionMatching(otCoapOptionType aOption) +{ + const otCoapOption *rval = NULL; + + for (const otCoapOption *option = GetNextOption(); option != NULL; option = GetNextOption()) + { + if (option->mNumber == aOption) + { + // Found, stop searching + rval = option; + break; + } + } + + return rval; +} + const otCoapOption *OptionIterator::GetNextOption(void) { otError error = OT_ERROR_NONE; @@ -532,6 +566,26 @@ exit: return rval; } +otError OptionIterator::GetOptionValue(uint64_t &aValue) const +{ + otError error = OT_ERROR_NONE; + uint8_t value[sizeof(aValue)]; + uint8_t pos = 0; + + VerifyOrExit(mOption.mLength <= sizeof(aValue), error = OT_ERROR_NO_BUFS); + SuccessOrExit(error = GetOptionValue(value)); + + aValue = 0; + for (pos = 0; pos < mOption.mLength; pos++) + { + aValue <<= 8; + aValue |= value[pos]; + } + +exit: + return error; +} + otError OptionIterator::GetOptionValue(void *aValue) const { otError error = OT_ERROR_NONE; diff --git a/src/core/coap/coap_message.hpp b/src/core/coap/coap_message.hpp index 236ef8483..5bf5b1d12 100644 --- a/src/core/coap/coap_message.hpp +++ b/src/core/coap/coap_message.hpp @@ -639,6 +639,19 @@ public: */ otError Init(const Message *aMessage); + /** + * This method returns a pointer to the first option matching the given option number. + * + * The internal option pointer is advanced until matching option is seen, if no matching + * option is seen, the iterator will advance to the end of the options block. + * + * @param[in] aOption Option number to look for. + * + * @returns A pointer to the first matching option. If no option matching @p aOption is seen, NULL pointer is + * returned. + */ + const otCoapOption *GetFirstOptionMatching(otCoapOptionType aOption); + /** * This method returns a pointer to the first option. * @@ -646,6 +659,19 @@ public: */ const otCoapOption *GetFirstOption(void); + /** + * This method returns a pointer to the next option matching the given option number. + * + * The internal option pointer is advanced until matching option is seen, if no matching + * option is seen, the iterator will advance to the end of the options block. + * + * @param[in] aOption Option number to look for. + * + * @returns A pointer to the next matching option (relative to current iterator position). If no option matching @p + * aOption is seen, NULL pointer is returned. + */ + const otCoapOption *GetNextOptionMatching(otCoapOptionType aOption); + /** * This method returns a pointer to the next option. * @@ -653,9 +679,24 @@ public: */ const otCoapOption *GetNextOption(void); + /** + * This function fills current option value into @p aValue. The option is assumed to be an unsigned integer. + * + * @param[out] aValue Buffer to store the option value. + * + * @retval OT_ERROR_NONE Successfully filled value. + * @retval OT_ERROR_NOT_FOUND No more options, aIterator->mNextOptionOffset is set to offset of payload. + * @retval OT_ERROR_NO_BUFS Value is too long to fit in a uint64_t. + * + */ + otError GetOptionValue(uint64_t &aValue) const; + /** * This function fills current option value into @p aValue. * + * @param[out] aValue Buffer to store the option value. This buffer is assumed to be sufficiently large + * (see @ref otCoapOption::mLength). + * * @retval OT_ERROR_NONE Successfully filled value. * @retval OT_ERROR_NOT_FOUND No more options, mNextOptionOffset is set to offset of payload. * diff --git a/src/core/config/coap.h b/src/core/config/coap.h index 6daa7c7fe..5484d02d2 100644 --- a/src/core/config/coap.h +++ b/src/core/config/coap.h @@ -100,6 +100,16 @@ #define OPENTHREAD_CONFIG_COAP_API_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE + * + * Define to 1 to enable the CoAP Observe (RFC7641) API. + * + */ +#ifndef OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE +#define OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE 0 +#endif + /** * @def OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE * diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix index 2c9a4d7b0..fbfc9d9d8 100644 --- a/src/posix/Makefile-posix +++ b/src/posix/Makefile-posix @@ -38,6 +38,7 @@ DEBUG ?= 0 BORDER_AGENT ?= 1 BORDER_ROUTER ?= 1 COAP ?= 1 +COAP_OBSERVE ?= 1 COAPS ?= 1 COMMISSIONER ?= 1 CHANNEL_MANAGER ?= 1 diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am index 72db53ff3..908d2dad0 100644 --- a/tests/scripts/thread-cert/Makefile.am +++ b/tests/scripts/thread-cert/Makefile.am @@ -143,6 +143,7 @@ EXTRA_DIST = \ sniffer.py \ sniffer_transport.py \ test_coap.py \ + test_coap_observe.py \ test_coaps.py \ test_common.py \ test_crypto.py \ @@ -164,6 +165,7 @@ check_PROGRAMS = \ check_SCRIPTS = \ test_coap.py \ + test_coap_observe.py \ test_coaps.py \ test_common.py \ test_crypto.py \ @@ -284,6 +286,7 @@ TESTS = \ XFAIL_NCP_TESTS = \ test_coaps.py \ + test_coap_observe.py \ test_diag.py \ test_ipv6_fragmentation.py \ test_ipv6_source_selection.py \ diff --git a/tests/scripts/thread-cert/config.py b/tests/scripts/thread-cert/config.py index ad05ab444..fb973aaa0 100644 --- a/tests/scripts/thread-cert/config.py +++ b/tests/scripts/thread-cert/config.py @@ -460,6 +460,7 @@ def create_default_based_on_src_dst_ports_udp_payload_factory(master_key): 19788: mle_message_factory, 61631: coap_message_factory, 1000: dtls_message_factory, + 5683: coap_message_factory, 5684: dtls_message_factory, }) diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 0d2cce5d9..d04f8e5af 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -38,6 +38,7 @@ import simulator import socket import time import unittest +import binascii class Node: @@ -1015,6 +1016,182 @@ class Node: self.send_command(cmd) self._expect('Done') + def coap_cancel(self): + """ + Cancel a CoAP subscription. + """ + cmd = 'coap cancel' + self.send_command(cmd) + self._expect('Done') + + def coap_delete(self, ipaddr, uri, con=False, payload=None): + """ + Send a DELETE request via CoAP. + """ + return self._coap_rq('delete', ipaddr, uri, con, payload) + + def coap_get(self, ipaddr, uri, con=False, payload=None): + """ + Send a GET request via CoAP. + """ + return self._coap_rq('get', ipaddr, uri, con, payload) + + def coap_observe(self, ipaddr, uri, con=False, payload=None): + """ + Send a GET request via CoAP with Observe set. + """ + return self._coap_rq('observe', ipaddr, uri, con, payload) + + def coap_post(self, ipaddr, uri, con=False, payload=None): + """ + Send a POST request via CoAP. + """ + return self._coap_rq('post', ipaddr, uri, con, payload) + + def coap_put(self, ipaddr, uri, con=False, payload=None): + """ + Send a PUT request via CoAP. + """ + return self._coap_rq('put', ipaddr, uri, con, payload) + + def _coap_rq(self, method, ipaddr, uri, con=False, payload=None): + """ + Issue a GET/POST/PUT/DELETE/GET OBSERVE request. + """ + cmd = 'coap %s %s %s' % (method, ipaddr, uri) + if con: + cmd += ' con' + else: + cmd += ' non' + + if payload is not None: + cmd += ' %s' % payload + + self.send_command(cmd) + return self.coap_wait_response() + + def coap_wait_response(self): + """ + Wait for a CoAP response, and return it. + """ + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.go(5) + timeout = 1 + else: + timeout = 5 + + self._expect( + r'coap response from ([\da-f:]+)(?: OBS=(\d+))?' + r'(?: with payload: ([\da-f]+))?\b', + timeout=timeout) + (source, observe, payload) = self.pexpect.match.groups() + source = source.decode('UTF-8') + + if observe is not None: + observe = int(observe, base=10) + + if payload is not None: + payload = binascii.a2b_hex(payload).decode('UTF-8') + + # Return the values received + return dict(source=source, observe=observe, payload=payload) + + def coap_wait_request(self): + """ + Wait for a CoAP request to be made. + """ + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.go(5) + timeout = 1 + else: + timeout = 5 + + self._expect( + r'coap request from ([\da-f:]+)(?: OBS=(\d+))?' + r'(?: with payload: ([\da-f]+))?\b', + timeout=timeout) + (source, observe, payload) = self.pexpect.match.groups() + source = source.decode('UTF-8') + + if observe is not None: + observe = int(observe, base=10) + + if payload is not None: + payload = binascii.a2b_hex(payload).decode('UTF-8') + + # Return the values received + return dict(source=source, observe=observe, payload=payload) + + def coap_wait_subscribe(self): + """ + Wait for a CoAP client to be subscribed. + """ + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.go(5) + timeout = 1 + else: + timeout = 5 + + self._expect(r'Subscribing client\b', timeout=timeout) + + def coap_wait_ack(self): + """ + Wait for a CoAP notification ACK. + """ + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.go(5) + timeout = 1 + else: + timeout = 5 + + self._expect( + r'Received ACK in reply to notification ' + r'from ([\da-f:]+)\b', + timeout=timeout) + (source,) = self.pexpect.match.groups() + source = source.decode('UTF-8') + + return source + + def coap_set_resource_path(self, path): + """ + Set the path for the CoAP resource. + """ + cmd = 'coap resource %s' % path + self.send_command(cmd) + self._expect('Done') + + def coap_set_content(self, content): + """ + Set the content of the CoAP resource. + """ + cmd = 'coap set %s' % content + self.send_command(cmd) + self._expect('Done') + + def coap_start(self): + """ + Start the CoAP service. + """ + cmd = 'coap start' + self.send_command(cmd) + self._expect('Done') + + def coap_stop(self): + """ + Stop the CoAP service. + """ + cmd = 'coap stop' + self.send_command(cmd) + + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.go(5) + timeout = 1 + else: + timeout = 5 + + self._expect('Done', timeout=timeout) + def coaps_start_psk(self, psk, pskIdentity): cmd = 'coaps psk %s %s' % (psk, pskIdentity) self.send_command(cmd) diff --git a/tests/scripts/thread-cert/test_coap_observe.py b/tests/scripts/thread-cert/test_coap_observe.py new file mode 100755 index 000000000..bec9d57a1 --- /dev/null +++ b/tests/scripts/thread-cert/test_coap_observe.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import unittest + +import pexpect +import config +import node + +LEADER = 1 +ROUTER = 2 + + +class TestCoapObserve(unittest.TestCase): + """ + Test suite for CoAP Observations (RFC7641). + """ + + def setUp(self): + """ + Start up two nodes and get them on the virtual network. + """ + self.simulator = config.create_default_simulator() + + self.nodes = {} + for i in range(1, 3): + self.nodes[i] = node.Node(i, simulator=self.simulator) + + self.nodes[LEADER].set_panid(0xface) + self.nodes[LEADER].set_mode('rsdn') + self.nodes[LEADER].add_whitelist(self.nodes[ROUTER].get_addr64()) + self.nodes[LEADER].enable_whitelist() + + self.nodes[ROUTER].set_panid(0xface) + self.nodes[ROUTER].set_mode('rsdn') + self.nodes[ROUTER].add_whitelist(self.nodes[LEADER].get_addr64()) + self.nodes[ROUTER].enable_whitelist() + self.nodes[ROUTER].set_router_selection_jitter(1) + + def tearDown(self): + """ + Tear down the nodes created. + """ + for n in list(self.nodes.values()): + n.stop() + n.destroy() + self.simulator.stop() + + def _do_notification_test(self, con): + self.nodes[LEADER].start() + self.simulator.go(5) + self.assertEqual(self.nodes[LEADER].get_state(), 'leader') + + self.nodes[ROUTER].start() + self.simulator.go(5) + self.assertEqual(self.nodes[ROUTER].get_state(), 'router') + + mleid = self.nodes[LEADER].get_ip6_address(config.ADDRESS_TYPE.ML_EID) + + self.nodes[LEADER].coap_start() + self.nodes[LEADER].coap_set_resource_path('test') + self.nodes[LEADER].coap_set_content('Test123') + + self.nodes[ROUTER].coap_start() + response = self.nodes[ROUTER].coap_observe(mleid, 'test', con=con) + + first_observe = response['observe'] + self.assertIsNotNone(first_observe) + self.assertEqual(response['payload'], 'Test123') + self.assertEqual(response['source'], mleid) + + # This should have been emitted already, so should return immediately + self.nodes[LEADER].coap_wait_subscribe() + + # Now change the content on the leader and wait for it to show up + # on the router. We will do this a few times with a short delay. + for n in range(0, 5): + content = 'msg%d' % n + + self.nodes[LEADER].coap_set_content(content) + + response = self.nodes[ROUTER].coap_wait_response() + self.assertGreater(response['observe'], first_observe) + self.assertEqual(response['payload'], content) + self.assertEqual(response['source'], mleid) + + # Stop subscription + self.nodes[ROUTER].coap_cancel() + + # We should see the response, but with no Observe option + response = self.nodes[ROUTER].coap_wait_response() + self.assertIsNone(response['observe']) + # Content won't have changed. + self.assertEqual(response['payload'], content) + + # Make another change, no notification should be sent + self.nodes[LEADER].coap_set_content('LastNote') + + # This should time out! + try: + self.nodes[ROUTER].coap_wait_response() + self.fail('Should not have received notification') + except pexpect.exceptions.TIMEOUT: + pass + + self.nodes[ROUTER].coap_stop() + self.nodes[LEADER].coap_stop() + + def test_con(self): + """ + Test notification using CON messages. + """ + for trial in range(0, 3): + try: + self._do_notification_test(con=True) + break + except (AssertionError, pexpect.exceptions.TIMEOUT): + continue + + def test_non(self): + """ + Test notification using NON messages. + """ + for trial in range(0, 3): + try: + self._do_notification_test(con=False) + break + except (AssertionError, pexpect.exceptions.TIMEOUT): + continue + + +if __name__ == '__main__': + unittest.main()