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()