From 594a97541db206a2a4fe48bc177463604d32cbe7 Mon Sep 17 00:00:00 2001 From: Martin Zimmermann <30142883+martinzi@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:04:19 +0200 Subject: [PATCH 1/2] [rest] add joiner to commissioner joiner table, get status of joiner by indirect 'addThreadDeviceTask' processing This commit implements - POST `addThreadDeviceTask` on api/actions/ - GET api/actions/ - DELETE api/actions The commit also provides integration tests, see tests/restjsonapi. Please follow the following steps to install/build OTBR. 1. Checkout this PR 2. Build and Install OTBR as usual, e.g. on a Raspberry Pi 3. Restart the OTBR. `sudo systemctl restart otbr-agent` 4. To monitor the log [Errors|Warnings|Info] please open a different terminal instance and use following command: ``` tail -f /var/log/syslog | grep otbr ``` 5. Send POST request using BRUNO or CURL, e.g. to join a new device into your network. ``` curl -X POST -H 'Content-Type: application/vnd.api+json' http://localhost:8081/api/actions -d '{"data": [{"type": "addThreadDeviceTask", "attributes": {"eui": "6234567890AACDEA", "pskd": "J01NME", "timeout": 3600}}]}' | jq ``` should return ``` { "data": [ { "id": "2d5a8844-b1bc-4f02-93f0-d87b8c3b4e92", "type": "addThreadDeviceTask", "attributes": { "eui": "6234567890AACDEB", "pskd": "J01NME", "timeout": 3600, "status": "pending" }, } ] } ``` 6. You may check the status and get the full collection of actions. ``` curl -X GET -H 'Accept: application/vnd.api+json' http://localhost:8081/api/actions | jq ``` should return ``` { "data": [ { "id": "2d5a8844-b1bc-4f02-93f0-d87b8c3b4e92", "type": "addThreadDeviceTask", "attributes": { "eui": "6234567890AACDEB", "pskd": "J01NME", "timeout": 3600, "status": "pending" } } ], "meta": { "collection": { "offset": 0, "limit": 100, "total": 1 } } } ``` 7. View the entry added to the commissioner's table `sudo ot-ctl commissioner joiner table` and expect ``` | ID | PSKd | Expiration | +-----------------------+----------------------------------+------------+ | 6234567890aacdea | J01NME | 3459027 | Done ``` 8. Start your joiner and after a few seconds repeat above steps 6. and 7. 9. For running the included test script install Bruno-Cli and run the bash script on your border router ``` cd tests/restjsonapi source ./install_bruno_cli ./test-restjsonapi-server ``` --- src/rest/CMakeLists.txt | 6 + src/rest/extensions/CMakeLists.txt | 48 ++ .../extensions/commissioner_allow_list.cpp | 604 ++++++++++++++++++ .../extensions/commissioner_allow_list.hpp | 273 ++++++++ src/rest/extensions/linked_list.hpp | 584 +++++++++++++++++ src/rest/extensions/rest_server_common.cpp | 168 +++++ src/rest/extensions/rest_server_common.hpp | 97 +++ .../rest_task_add_thread_device.cpp | 279 ++++++++ .../rest_task_add_thread_device.hpp | 61 ++ src/rest/extensions/rest_task_handler.cpp | 163 +++++ src/rest/extensions/rest_task_handler.hpp | 162 +++++ src/rest/extensions/rest_task_queue.cpp | 538 ++++++++++++++++ src/rest/extensions/rest_task_queue.hpp | 206 ++++++ src/rest/extensions/timestamp.cpp | 83 +++ src/rest/extensions/timestamp.hpp | 36 ++ src/rest/extensions/uuid.cpp | 124 ++++ src/rest/extensions/uuid.hpp | 115 ++++ src/rest/parser.cpp | 134 ++-- src/rest/parser.hpp | 18 + src/rest/request.cpp | 44 +- src/rest/request.hpp | 45 +- src/rest/resource.cpp | 356 ++++++++++- src/rest/resource.hpp | 107 +++- src/rest/response.cpp | 7 +- src/rest/response.hpp | 8 + src/rest/rest_web_server.cpp | 18 + src/rest/rest_web_server.hpp | 9 + src/rest/types.hpp | 22 +- .../actions/Delete_Actions_Collection.bru | 19 + .../actions/Get_ActionItem_by_ItemId_404.bru | 24 + .../actions/Get_Actions_Collection.bru | 47 ++ tests/restjsonapi/actions/Options.bru | 15 + .../actions/Post_Add_ThreadDevice.bru | 39 ++ tests/restjsonapi/bruno.json | 9 + tests/restjsonapi/environments/localhost.bru | 6 + tests/restjsonapi/install_bruno_cli | 43 ++ tests/restjsonapi/test-restjsonapi-server | 50 ++ third_party/openthread/CMakeLists.txt | 1 + 38 files changed, 4437 insertions(+), 131 deletions(-) create mode 100644 src/rest/extensions/CMakeLists.txt create mode 100644 src/rest/extensions/commissioner_allow_list.cpp create mode 100644 src/rest/extensions/commissioner_allow_list.hpp create mode 100644 src/rest/extensions/linked_list.hpp create mode 100644 src/rest/extensions/rest_server_common.cpp create mode 100644 src/rest/extensions/rest_server_common.hpp create mode 100644 src/rest/extensions/rest_task_add_thread_device.cpp create mode 100644 src/rest/extensions/rest_task_add_thread_device.hpp create mode 100644 src/rest/extensions/rest_task_handler.cpp create mode 100644 src/rest/extensions/rest_task_handler.hpp create mode 100644 src/rest/extensions/rest_task_queue.cpp create mode 100644 src/rest/extensions/rest_task_queue.hpp create mode 100644 src/rest/extensions/timestamp.cpp create mode 100644 src/rest/extensions/timestamp.hpp create mode 100644 src/rest/extensions/uuid.cpp create mode 100644 src/rest/extensions/uuid.hpp create mode 100644 tests/restjsonapi/actions/Delete_Actions_Collection.bru create mode 100644 tests/restjsonapi/actions/Get_ActionItem_by_ItemId_404.bru create mode 100644 tests/restjsonapi/actions/Get_Actions_Collection.bru create mode 100644 tests/restjsonapi/actions/Options.bru create mode 100644 tests/restjsonapi/actions/Post_Add_ThreadDevice.bru create mode 100644 tests/restjsonapi/bruno.json create mode 100644 tests/restjsonapi/environments/localhost.bru create mode 100755 tests/restjsonapi/install_bruno_cli create mode 100755 tests/restjsonapi/test-restjsonapi-server diff --git a/src/rest/CMakeLists.txt b/src/rest/CMakeLists.txt index 63715c409e5..c8c412de009 100644 --- a/src/rest/CMakeLists.txt +++ b/src/rest/CMakeLists.txt @@ -26,6 +26,11 @@ # POSSIBILITY OF SUCH DAMAGE. # +# TODO: Make this KConfig, maybe something like this +# if(OTBR_REST_API_EXTENSIONS) +add_subdirectory(extensions) +# endif() + add_library(otbr-rest rest_web_server.cpp connection.cpp @@ -45,4 +50,5 @@ target_link_libraries(otbr-rest otbr-utils openthread-ftd openthread-posix + otbr-rest-extension ) diff --git a/src/rest/extensions/CMakeLists.txt b/src/rest/extensions/CMakeLists.txt new file mode 100644 index 00000000000..999e138b127 --- /dev/null +++ b/src/rest/extensions/CMakeLists.txt @@ -0,0 +1,48 @@ +# +# Copyright (c) 2024, 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. +# + +add_library(otbr-rest-extension + timestamp.cpp + uuid.cpp + rest_task_add_thread_device.cpp + rest_task_handler.cpp + rest_task_queue.cpp + rest_server_common.cpp + commissioner_allow_list.cpp +) + +target_link_libraries(otbr-rest-extension + PUBLIC + http_parser + PRIVATE + cjson + otbr-config + otbr-utils + openthread-ftd + openthread-posix +) diff --git a/src/rest/extensions/commissioner_allow_list.cpp b/src/rest/extensions/commissioner_allow_list.cpp new file mode 100644 index 00000000000..00c69979ff4 --- /dev/null +++ b/src/rest/extensions/commissioner_allow_list.cpp @@ -0,0 +1,604 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements functionality related to commisioner APIs. + * + */ +#include "commissioner_allow_list.hpp" +#include "rest/extensions/rest_task_add_thread_device.hpp" +#include "utils/hex.hpp" +#include "utils/thread_helper.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include "assert.h" +#include "cJSON.h" +#include +#include +#include +#include +#include +#include +#include +#include "openthread/instance.h" + +#define ALLOW_LIST_NAME "allowlist" +#define ALLOW_LIST_MOUNT "/" ALLOW_LIST_NAME +#define ALLOW_LIST_BASE_DIR ALLOW_LIST_MOUNT "/" + +#define COMMISSIONER_START_WAIT_TIME_MS 100 +#define COMMISSIONER_START_MAX_ATTEMPTS 5 + +static allow_list::LinkedList AllowListEntryList; +static void consoleEntryPrint(AllowListEntry *aEntry); + +bool otExtAddressMatch(const otExtAddress *aAddress1, const otExtAddress *aAddress2) +{ + for (int i = 0; i < OT_EXT_ADDRESS_SIZE; i++) + { + if (aAddress1->m8[i] != aAddress2->m8[i]) + { + return false; + } + } + return true; +} + +bool eui64IsNull(const otExtAddress aEui64) +{ + for (int i = 0; i < OT_EXT_ADDRESS_SIZE; i++) + { + if (aEui64.m8[i] != 0) + { + return false; + } + } + return true; +} + +AllowListEntry *entryEui64Find(const otExtAddress *aEui64) +{ + AllowListEntry *entry = nullptr; + + if (nullptr == aEui64) + { + return entry; + } + entry = AllowListEntryList.GetHead(); + while (entry) + { + if (otExtAddressMatch(&entry->meui64, aEui64)) + { + break; + } + entry = entry->GetNext(); + } + return entry; +} + +otError allowListCommissionerJoinerAdd(otExtAddress aEui64, + uint32_t aTimeout, + char *aPskd, + otInstance *aInstance, + uuid_t uuid) +{ + otError error; + AllowListEntry *entry = nullptr; + const otExtAddress *addrPtr = &aEui64; + + if (eui64IsNull(aEui64)) + { +#ifdef OPENTHREAD_COMMISSIONER_ALLOW_ANY_JOINER + return OT_ERROR_INVALID_ARGS; +#else + addrPtr = nullptr; +#endif + } + + allowListAddDevice(aEui64, aTimeout, aPskd, uuid); + entry = entryEui64Find(addrPtr); + + error = otCommissionerAddJoiner(aInstance, addrPtr, aPskd, aTimeout); + + if (OT_ERROR_NONE == error && nullptr != entry) + { + entry->update_state(AllowListEntry::kAllowListEntryPendingJoiner); + } + + if (OT_ERROR_NONE != error) + { + otbrLogWarning("otCommissionerAddJoiner error=%d %s", error, otThreadErrorToString(error)); + } + return error; +} + +otError allowListEntryErase(otExtAddress aEui64) +{ + otError error = OT_ERROR_FAILED; + AllowListEntry *entry = nullptr; + + const otExtAddress *addrPtr = &aEui64; + + entry = AllowListEntryList.GetHead(); + while (entry) + { + if (otExtAddressMatch(&entry->meui64, addrPtr)) + { + error = AllowListEntryList.Remove(*entry); + break; + } + entry = entry->GetNext(); + } + return error; +} + +otError allowListCommissionerJoinerRemove(otExtAddress aEui64, otInstance *aInstance) +{ + otError error = OT_ERROR_FAILED; + otCommissionerState state = OT_COMMISSIONER_STATE_DISABLED; + + const otExtAddress *addrPtr = &aEui64; + + if (eui64IsNull(aEui64)) + { + addrPtr = nullptr; + } + + state = otCommissionerGetState(aInstance); + if (OT_COMMISSIONER_STATE_DISABLED == state) + { + return OT_ERROR_NONE; + } + + error = otCommissionerRemoveJoiner(aInstance, addrPtr); + + if (OT_ERROR_NONE != error) + { + otLogWarnPlat("otCommissionerRemoveJoiner error=%d %s", error, otThreadErrorToString(error)); + } + return error; +} + +AllowListEntry *parse_buf_as_json(char *aBuf) +{ + // Need all vars to be declared here to use goto for graceful exit + cJSON *allow_entry_json = nullptr; + cJSON *attributesJSON = nullptr; + // cJSON *hasActivationKeyJSON = nullptr; + AllowListEntry *pEntry = nullptr; + otExtAddress eui64; + uint32_t timeout = 0; + AllowListEntry::AllowListEntryState state = AllowListEntry::kAllowListEntryNew; + UUID uuid_obj; + uuid_t uuid; + char *uuid_str = nullptr; + char *eui64_str = nullptr; + char *pskdValue = nullptr; + size_t pskdValueLen = 0; + char *pskd = nullptr; + + allow_entry_json = cJSON_Parse(aBuf); + if (nullptr == allow_entry_json) + { + otbrLogErr("%s: Err cJSON_Parse", __func__); + goto exit; + } + + attributesJSON = cJSON_GetObjectItemCaseSensitive(allow_entry_json, JSON_ATTRIBUTES); + if (nullptr == attributesJSON) + { + otbrLogErr("%s: Err cJSON Get %s", __func__, JSON_ATTRIBUTES); + goto exit; + } + eui64_str = cJSON_GetObjectItem(attributesJSON, JSON_EUI)->valuestring; + if (nullptr == eui64_str) + { + otbrLogErr("%s: Err cJSON Get eui64", __func__); + goto exit; + } + + otbr::Utils::Hex2Bytes(eui64_str, eui64.m8, sizeof(eui64)); + + uuid_str = cJSON_GetObjectItem(allow_entry_json, JSON_UUID)->valuestring; + if (nullptr == uuid_str) + { + otbrLogErr("%s: Err cJSON Get uuid", __func__); + goto exit; + } + uuid_obj.parse(std::string(uuid_str)); + uuid_obj.getUuid(uuid); + // uuid_parse(uuid_str, &uuid); + + pskdValue = cJSON_GetObjectItem(attributesJSON, JSON_PSKD)->valuestring; + if (nullptr == pskdValue || strlen(pskdValue) > OT_JOINER_MAX_PSKD_LENGTH) + { + otbrLogErr("%s: Err cJSON Get pskd", __func__); + goto exit; + } + + pskdValueLen = strlen(pskdValue) + 1; // account for NULL + pskd = (char *)malloc(pskdValueLen); + if (nullptr == pskd) + { + otbrLogErr("%s: Err no mem for pskd, need %d bytes", __func__, pskdValueLen); + goto exit; + } + memset(pskd, 0, pskdValueLen); + memcpy(pskd, pskdValue, strlen(pskdValue)); + + timeout = cJSON_GetObjectItem(allow_entry_json, JSON_TIMEOUT)->valueint; + state = (AllowListEntry::AllowListEntryState)cJSON_GetObjectItem(allow_entry_json, JSON_ALLOW_STATE)->valueint; + pEntry = new AllowListEntry(eui64, uuid, timeout, state, pskd); + +exit: + if (nullptr != allow_entry_json) + { + cJSON_Delete(allow_entry_json); + } + if (nullptr == pEntry) + { + otbrLogErr("%s: Err creating a new AllowListEntry", __func__); + if (nullptr != pskd) + { + free(pskd); + } + } + return pEntry; +} + +void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, uuid_t aUuid) +{ + assert(nullptr != aPskd); + + AllowListEntry *pEntry = entryEui64Find(&aEui64); + + int pskd_len = strlen(aPskd); + char *pskd_new = (char *)malloc(pskd_len + 1); + assert(nullptr != pskd_new); + memset(pskd_new, 0, pskd_len + 1); + memcpy(pskd_new, aPskd, pskd_len); + + if (nullptr != pEntry) + { + pEntry->mPSKd = pskd_new; + pEntry->mTimeout = aTimeout; + pEntry->muuid = aUuid; + } + else + { + // TODO if (aUuid == NULL) + //{ + // // this may not be needed + // uuid_generate_random(&aUuid); + // } + pEntry = new AllowListEntry(aEui64, aUuid, aTimeout, pskd_new); + if (nullptr == pEntry) + { + // otbrLogErr("%s: Err creating a new AllowListEntry", __func__); + free(pskd_new); + return; + } + AllowListEntryList.Add(*pEntry); + } + + consoleEntryPrint(pEntry); +} + +/** + * @brief Given an entry, prints its content to the console + * + * @param aEntry the entry to print + */ +static void consoleEntryPrint(AllowListEntry *aEntry) +{ + assert(nullptr != aEntry); + // char uuidStr[UUID_STR_LEN] = {0}; + + UUID uuid_obj = UUID(); + uuid_obj.setUuid(aEntry->muuid); + + // uuid_unparse(aEntry->muuid, uuidStr); + otbrLogInfo( + "Entry uuid: %s\n\tEUI64: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\n\tJoined: %s\n\tState: %d\n\tTimeout: %d", + uuid_obj.toString().c_str(), aEntry->meui64.m8[0], aEntry->meui64.m8[1], aEntry->meui64.m8[2], + aEntry->meui64.m8[3], aEntry->meui64.m8[4], aEntry->meui64.m8[5], aEntry->meui64.m8[6], aEntry->meui64.m8[7], + aEntry->is_joined() ? "TRUE" : "FALSE", aEntry->mstate, aEntry->mTimeout); +} + +void allow_list_print_all_entries_to_console(void) +{ + AllowListEntry *entry = AllowListEntryList.GetHead(); + while (entry) + { + consoleEntryPrint(entry); + entry = entry->GetNext(); + } +} + +int allowListJsonifyAll(cJSON *input_object) +{ + assert(nullptr != input_object); + + if (AllowListEntryList.IsEmpty()) + { + return 0; + } + + // Add each entry into an array called allow_list + cJSON *json_array = cJSON_CreateArray(); + if (nullptr == json_array) + { + otbrLogErr("%s: Err: cJSON_CreateArray", __func__); + return 0; + } + + AllowListEntry *entry = nullptr; + // cJSON* entryJson = nullptr; + int entry_count = 0; + for (entry = AllowListEntryList.GetHead(); nullptr != entry; entry = entry->GetNext()) + { + // Typically allow list entry uses type as JSON_ALLOW_LIST_TYPE. + // This call is one of the exception to reuse the Allow_list_entry_as_CJSON method + // entryJson = entry->Allow_list_entry_as_CJSON(DEVICE_TYPE); + // cJSON_AddItemToArray(json_array, entryJson); + // entry_count++; + } + + if (entry_count > 0) + { + cJSON_AddItemToObject(input_object, "allow_list", json_array); + } + else + { + otbrLogErr("%s: Err: cJSON Array is empty", __func__); + // Something is wrong, delete our array to not leak memory + cJSON_Delete(json_array); + return 0; + } + + // @note: Caller responsible to clean json_array, usually via cJSON_Delete(input_object); + return entry_count; +} + +void allowListEraseAll(void) +{ + if (!AllowListEntryList.IsEmpty()) + { + AllowListEntry *entry = nullptr; + // Free all malloc-ed pskd to not leak memory + for (entry = AllowListEntryList.GetHead(); nullptr != entry; entry = entry->GetNext()) + { + if (nullptr != entry->mPSKd) + { + free(entry->mPSKd); + } + } + AllowListEntryList.Clear(); // Mark Linked List as empty + } +} + +void HandleStateChanged(otCommissionerState aState, void *aContext) +{ + OT_UNUSED_VARIABLE(aContext); + otbrLogWarning("%s:%d - %s - commissioner state: %d", __FILE__, __LINE__, __func__, aState); + + switch (aState) + { + case OT_COMMISSIONER_STATE_ACTIVE: + rest_task_queue_handle(); + break; + case OT_COMMISSIONER_STATE_DISABLED: + break; + case OT_COMMISSIONER_STATE_PETITION: + break; + default: + break; + } +} + +uint8_t allowListGetPendingJoinersCount(void) +{ + uint8_t pendingJoinersCount = 0; + + AllowListEntry *entry = AllowListEntryList.GetHead(); + + while (entry) + { + if ((AllowListEntry::kAllowListEntryJoined != entry->mstate) || + (AllowListEntry::kAllowListEntryJoinFailed != entry->mstate)) + { + pendingJoinersCount++; + } + entry = entry->GetNext(); + } + + return pendingJoinersCount; +} + +void HandleJoinerEvent(otCommissionerJoinerEvent aEvent, + const otJoinerInfo *aJoinerInfo, + const otExtAddress *aJoinerId, + void *aContext) +{ + (void)aEvent; + (void)aContext; + OT_UNUSED_VARIABLE(aJoinerId); + AllowListEntry *entry = nullptr; + uint8_t pendingDevicesCount = 0; + + // @note: Thread may call this for joiners that we are not supposed to join + // do not assume `entry` is not null in the rest of the code. + entry = entryEui64Find(&aJoinerInfo->mSharedId.mEui64); + if (nullptr == entry && eui64IsNull(aJoinerInfo->mSharedId.mEui64)) + { + otbrLogWarning("Unauthorized device %s join attempt", aJoinerInfo->mSharedId.mEui64); + return; + } + + switch (aEvent) + { + case OT_COMMISSIONER_JOINER_START: + otbrLogWarning("Start Joiner"); + if (nullptr != entry) + { + entry->update_state(AllowListEntry::kAllowListEntryJoinAttempted); + consoleEntryPrint(entry); + } + break; + case OT_COMMISSIONER_JOINER_CONNECTED: + otbrLogWarning("Connect Joiner"); + break; + case OT_COMMISSIONER_JOINER_FINALIZE: + otbrLogWarning("Finalize Joiner"); + if (nullptr != entry) + { + entry->update_state(AllowListEntry::kAllowListEntryJoined); + consoleEntryPrint(entry); + } + break; + case OT_COMMISSIONER_JOINER_END: + otbrLogWarning("End Joiner"); + break; + case OT_COMMISSIONER_JOINER_REMOVED: + otbrLogWarning("Removed Joiner"); + + if (nullptr != entry) + { + // If this get called on a one of our joiners that has never attempted yet, then we mark this as expired + if (AllowListEntry::kAllowListEntryPendingJoiner == entry->mstate) + { + entry->update_state(AllowListEntry::kAllowListEntryExpired); + } + // If this get called on a one of our joiners that is not joined yet, then we need to mark this as failed + else if (AllowListEntry::kAllowListEntryJoined != entry->mstate) + { + entry->update_state(AllowListEntry::kAllowListEntryJoinFailed); + } + } + + // Scan allow list see if there are still pending joiners to process + pendingDevicesCount = allowListGetPendingJoinersCount(); + + // If all entries have been attempted and nothing is pending, stop the commissioner + if (0 == pendingDevicesCount) + { + allowListCommissionerStopPost(); + } + else + { + // Tracer print + otbrLogWarning("%u Pending Joiners", pendingDevicesCount); + } + + break; + } +} + +otError allowListCommissionerStart(otInstance *aInstance) +{ + otError error = OT_ERROR_FAILED; + + error = otCommissionerStart(aInstance, &HandleStateChanged, &HandleJoinerEvent, NULL); + + return error; +} + +otError allowListCommissionerStopPost(void) +{ + return OT_ERROR_NONE; +} + +cJSON *AllowListEntry::Allow_list_entry_as_CJSON(const char *entryType) +{ + assert(nullptr != entryType); + + cJSON *entry_json_obj = nullptr; + // cJSON *hasActivationKey = nullptr; + cJSON *attributes_json_obj = nullptr; + char eui64_str[17] = {0}; + // char uuid_str[UUID_STR_LEN] = {0}; + + UUID uuid_obj = UUID(); + + // hasActivationKey = cJSON_CreateObject(); + + memset(eui64_str, 0, sizeof(eui64_str)); + sprintf(eui64_str, "%02x%02x%02x%02x%02x%02x%02x%02x", meui64.m8[0], meui64.m8[1], meui64.m8[2], meui64.m8[3], + meui64.m8[4], meui64.m8[5], meui64.m8[6], meui64.m8[7]); + + // memset(uuid_str, 0, sizeof(uuid_str)); + // uuid_unparse(muuid, uuid_str); + uuid_obj.setUuid(muuid); + + attributes_json_obj = cJSON_CreateObject(); + // cJSON_AddItemToObject(attributes_json_obj, "hasActivationKey", hasActivationKey); + cJSON_AddItemToObject(attributes_json_obj, "eui", cJSON_CreateString(eui64_str)); + cJSON_AddItemToObject(attributes_json_obj, "pskd", cJSON_CreateString(mPSKd)); + + entry_json_obj = cJSON_CreateObject(); + + cJSON_AddItemToObject(entry_json_obj, JSON_UUID, cJSON_CreateString(uuid_obj.toString().c_str())); + cJSON_AddItemToObject(entry_json_obj, JSON_TYPE, cJSON_CreateString(entryType)); + cJSON_AddItemToObject(entry_json_obj, JSON_ATTRIBUTES, attributes_json_obj); + cJSON_AddNumberToObject(entry_json_obj, JSON_TIMEOUT, mTimeout); + cJSON_AddNumberToObject(entry_json_obj, JSON_ALLOW_STATE, mstate); + + return entry_json_obj; +} + +/** + * @brief I (kludegy) map joiner status to otError code + * + * @param eui64 the Joiner eui64 to check + * @return otError OT_ERROR_NONE == eui64 joiner joined + * OT_ERROR_FAILED == eui64 joiner failed + * OT_ERROR_PENDING == eui64 joiner still being processed + */ +otError allowListEntryJoinStatusGet(const otExtAddress *eui64) +{ + AllowListEntry *entry = entryEui64Find(eui64); + + if ((NULL == entry) || (entry->is_failed())) + { + return OT_ERROR_FAILED; + } + + if (true == entry->is_joined()) + { + return OT_ERROR_NONE; + } + + return OT_ERROR_PENDING; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/rest/extensions/commissioner_allow_list.hpp b/src/rest/extensions/commissioner_allow_list.hpp new file mode 100644 index 00000000000..b588b74dc46 --- /dev/null +++ b/src/rest/extensions/commissioner_allow_list.hpp @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements a class allow list entry and declares APIs + * to handle commissioner APIs. + * + */ +#ifndef EXTENSIONS_COMMISSIONER_ALLOW_LIST_HPP_ +#define EXTENSIONS_COMMISSIONER_ALLOW_LIST_HPP_ + +#include "linked_list.hpp" +#include "rest/extensions/rest_server_common.hpp" +#include "rest/extensions/rest_task_queue.hpp" +#include "rest/extensions/uuid.hpp" + +struct cJSON; + +#ifdef __cplusplus +extern "C" { +#endif + +#include "openthread/commissioner.h" + +#define JSON_TYPE "type" +#define JSON_ATTRIBUTES "attributes" +#define JSON_EUI "eui" +#define JSON_PSKD "pskd" +#define JSON_TIMEOUT "timeout" +#define JSON_UUID "uuid" +#define JSON_ALLOW_STATE "state" + +#define JSON_ALLOW_LIST_TYPE "addThreadDeviceTask" + +const std::string kAllowList_status_str[] = { + "new", + "undiscovered", // not attempted + "completed", // success + "attempted", // failed but waiting for retries + "failed", // failed after max attempts or timeout + "stopped", // timeout without any attempt +}; + +/** + * This class implements an allow list entry. It maintains the device's ID, + * eui64, join timeout, and Joiner PSKd + * + */ +class AllowListEntry : public allow_list::LinkedListEntry +{ + friend class allow_list::LinkedList; + ; + +public: + enum AllowListEntryState : uint8_t + { + kAllowListEntryNew, + kAllowListEntryPendingJoiner, // not attempted + kAllowListEntryJoined, // success + kAllowListEntryJoinAttempted, // failed but waiting for retries + kAllowListEntryJoinFailed, // failed and expired + kAllowListEntryExpired, // expired without any attempt + kAllowListStates + }; + + /** + * This constructor creates an AllowListEntry. + */ + AllowListEntry(otExtAddress aEui64, uuid_t uuid, uint32_t aTimeout, char *aPskd) + { + meui64 = aEui64; + muuid = uuid; + mTimeout = aTimeout; + mPSKd = aPskd; + mstate = kAllowListEntryNew; + mNext = nullptr; + } + + AllowListEntry(otExtAddress aEui64, uuid_t uuid, uint32_t aTimeout, AllowListEntryState state, char *aPskd) + { + meui64 = aEui64; + muuid = uuid; + mTimeout = aTimeout; + mPSKd = aPskd; + mstate = state; + mNext = nullptr; + } + + /** + * Update Entry State + */ + void update_state(AllowListEntryState new_state) { mstate = new_state; } + + std::string getStateStr(void) { return kAllowList_status_str[mstate]; } + + /** + * + * @return + */ + bool is_joined(void) const { return (kAllowListEntryJoined == mstate); } + bool is_failed(void) const { return ((kAllowListEntryJoinFailed == mstate) || (kAllowListEntryExpired == mstate)); } + + /** + * @brief JSON-ify Allow List Entry using the specified entryType + * + * @param entryType The string to use for "type" attribute + * (e.g. "addThreadDeviceTask" for actions or "device" for reportng) + * @return cJSON* The created JSON object + */ + cJSON *Allow_list_entry_as_CJSON(const char *entryType); + + // Members + otExtAddress meui64; + uuid_t muuid; + uint32_t mTimeout; + char *mPSKd; + AllowListEntryState mstate; + AllowListEntry *mNext; +}; + +/** + * @brief Find an allow List entry via the device's eui64 address + * + * @param[in] aEui64 eui64 address of the device to find + * + * @return pointer to the desired entry + * NULL if the entry could be found + */ +AllowListEntry *entryEui64Find(const otExtAddress *aEui64); + +/** + * @brief Check if the provided address is not null + * + * @param[in] aEui64 The EUI64 address to check. + * + * @return true if address is NULL, false otherwise + */ +bool eui64IsNull(const otExtAddress aEui64); + +/** + * @brief Add a device to the allow list and the On-Mesh commissioner + * + * @param[in] aEui64 eui64 Address of the devive to add + * @param[in] aTimeout timeout to use when adding a commissioner joiner for the device, after which a Joiner is + * automatically removed, in seconds. + * @param[in] aPskd Pskd to use when joining the device + * @param[in] aInstance Openthread instance + * + * @note If provided with a NULL eui64, this function will return immediately, unless + * OPENTHREAD_COMMISSIONER_ALLOW_ANY_JOINER is defined. + * + * @return OT_ERROR_NONE Successfully added the Joiner. + * OT_ERROR_NO_BUFS No buffers available to add the Joiner. + * OT_ERROR_INVALID_ARGS @p aPskd is invalid or @p aEui64 is NULL and OPENTHREAD_COMMISSIONER_ALLOW_ANY_JOINER + * is not set. OT_ERROR_INVALID_STATE On-Mesh commissioner is not active. + * + */ +otError allowListCommissionerJoinerAdd(otExtAddress aEui64, + uint32_t aTimeout, + char *aPskd, + otInstance *aInstance, + uuid_t uuid); + +/** + * @brief Remove a single entry from On-Mesh commissioner joiner table + * + * @param[in] aEui64 The EUI64 address of the device to be removed from the commissioner joiner table. + * @param[in] aInstance Openthread instance + * + * @retval OT_ERROR_NONE Successfully removed the Joiner. + * OT_ERROR_NOT_FOUND Joiner specified by @p aEui64 was not found. + * OT_ERROR_INVALID_STATE On-Mesh commissioner is not active. + */ +otError allowListCommissionerJoinerRemove(otExtAddress aEui64, otInstance *aInstance); + +/** + * @brief Remove a single entry from the allow list + * + * @param[in] aEui64 The EUI64 address of the device to be removed from the allow list. + * + * @return OT_ERROR_NONE The entry was successfully removed from the list. + * OT_ERROR_NOT_FOUND Could not find the entry in the list. + */ +otError allowListEntryErase(otExtAddress aEui64); + +/** + * @brief Add a new device (entry) to the allow List internal linked list + * + * @param[in] aEui64 eui64 Address of the devive to add + * @param[in] aTimeout timeout to use when adding a commissioner joiner for the device, after which a Joiner is + * automatically removed, in seconds. + * @param[in] aPskd Pskd to use when joinig the device + */ +void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, uuid_t uuid); + +/** + * @brief Print Allow List Entries available in memory to console + */ +void allow_list_print_all_entries_to_console(void); + +/** + * @brief Create unwrapped JSON response for all allow list entry in RAM + * + * @param input_object The input object to place the response into + * @return int Zero when allow list is empty (input_object unmodified) + * N>0 when N-entres are added + * <0 negative error code otherwise + * + */ +int allowListJsonifyAll(cJSON *input_object); + +/** + * @brief Erase ALL allowlist entries + * + */ +void allowListEraseAll(void); + +/** + * @brief Start the On-Mesh commissioner functionality + * @param[in] aInstance Openthread instance + * + * @return OT_ERROR_NONE Successfully started the commissioner service. + * OT_ERROR_ALREADY Commissioner is already started. + * OT_ERROR_INVALID_STATE Device is not currently attached to a network. + */ +otError allowListCommissionerStart(otInstance *aInstance); + +/** + * @brief This function posts to the OpenThread queue a task to stop the commissioner + * + * @return OT_ERROR_NONE + */ +otError allowListCommissionerStopPost(void); + +/** + * @brief This function returns the number of pending joiners in the allow list + * + * @return The number of pending joiners in the allow list. + */ +uint8_t allowListGetPendingJoinersCount(void); + +otError allowListEntryJoinStatusGet(const otExtAddress *eui64); + +#ifdef __cplusplus +} +#endif + +#endif /* EXTENSIONS_COMMISSIONER_ALLOW_LIST_HPP_ */ diff --git a/src/rest/extensions/linked_list.hpp b/src/rest/extensions/linked_list.hpp new file mode 100644 index 00000000000..7019bd1fc4b --- /dev/null +++ b/src/rest/extensions/linked_list.hpp @@ -0,0 +1,584 @@ +/* + * Copyright (c) 2019, 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. + */ + +/** + * @file + * This file includes definitions for a generic single linked list. + */ + +#ifndef LINKED_LIST_HPP_ +#define LINKED_LIST_HPP_ + +#include +#include + +namespace allow_list { + +/** + * @addtogroup core-linked-list + * + * @brief + * This module includes definitions for OpenThread Singly Linked List. + * + * @{ + * + */ + +/** + * This template class represents a linked list entry. + * + * This class provides methods to `GetNext()` and `SetNext()` in the linked list entry. + * + * Users of this class should follow CRTP-style inheritance, i.e., the `Type` class itself should publicly inherit + * from `LinkedListEntry`. + * + * The template type `Type` should contain a `mNext` member variable. The `mNext` should be of a type that can be + * down-casted to `Type` itself. + * + */ +template class LinkedListEntry +{ +public: + /** + * This method gets the next entry in the linked list. + * + * @returns A pointer to the next entry in the linked list or nullptr if at the end of the list. + * + */ + const Type *GetNext(void) const { return static_cast(static_cast(this)->mNext); } + + /** + * This method gets the next entry in the linked list. + * + * @returns A pointer to the next entry in the linked list or nullptr if at the end of the list. + * + */ + Type *GetNext(void) { return static_cast(static_cast(this)->mNext); } + + /** + * This method sets the next pointer on the entry. + * + * @param[in] aNext A pointer to the next entry. + * + */ + void SetNext(Type *aNext) { static_cast(this)->mNext = aNext; } +}; + +/** + * This template class represents a singly linked list. + * + * The template type `Type` should provide `GetNext()` and `SetNext()` methods (which can be realized by `Type` + * inheriting from `LinkedListEntry` class). + * + */ +template class LinkedList +{ +public: + /** + * This constructor initializes the linked list. + * + */ + LinkedList(void) + : mHead(nullptr) + { + } + + /** + * This method returns the entry at the head of the linked list + * + * @returns Pointer to the entry at the head of the linked list, or nullptr if the list is empty. + * + */ + Type *GetHead(void) { return mHead; } + + /** + * This method returns the entry at the head of the linked list. + * + * @returns Pointer to the entry at the head of the linked list, or nullptr if the list is empty. + * + */ + const Type *GetHead(void) const { return mHead; } + + /** + * This method sets the head of the linked list to a given entry. + * + * @param[in] aHead A pointer to an entry to set as the head of the linked list. + * + */ + void SetHead(Type *aHead) { mHead = aHead; } + + /** + * This method clears the linked list. + * + */ + void Clear(void) { mHead = nullptr; } + + /** + * This method indicates whether the linked list is empty or not. + * + * @retval TRUE If the linked list is empty. + * @retval FALSE If the linked list is not empty. + * + */ + bool IsEmpty(void) const { return (mHead == nullptr); } + + /** + * This method pushes an entry at the head of the linked list. + * + * @param[in] aEntry A reference to an entry to push at the head of linked list. + * + */ + void Push(Type &aEntry) + { + aEntry.SetNext(mHead); + mHead = &aEntry; + } + + /** + * This method pushes an entry after a given previous existing entry in the linked list. + * + * @param[in] aEntry A reference to an entry to push into the list. + * @param[in] aPrevEntry A reference to a previous entry (new entry @p aEntry will be pushed after this). + * + */ + void PushAfter(Type &aEntry, Type &aPrevEntry) + { + aEntry.SetNext(aPrevEntry.GetNext()); + aPrevEntry.SetNext(&aEntry); + } + + /** + * This method pops an entry from head of the linked list. + * + * @note This method does not change the popped entry itself, i.e., the popped entry next pointer stays as before. + * + * @returns The entry that was popped if the list is not empty, or nullptr if the list is empty. + * + */ + Type *Pop(void) + { + Type *entry = mHead; + + if (mHead != nullptr) + { + mHead = mHead->GetNext(); + } + + return entry; + } + + /** + * This method pops an entry after a given previous entry. + * + * @note This method does not change the popped entry itself, i.e., the popped entry next pointer stays as before. + * + * @param[in] aPrevEntry A pointer to a previous entry. If it is not nullptr the entry after this will be popped, + * otherwise (if it is nullptr) the entry at the head of the list is popped. + * + * @returns Pointer to the entry that was popped, or nullptr if there is no entry to pop. + * + */ + Type *PopAfter(Type *aPrevEntry) + { + Type *entry; + + if (aPrevEntry == nullptr) + { + entry = Pop(); + } + else + { + entry = aPrevEntry->GetNext(); + + if (entry != nullptr) + { + aPrevEntry->SetNext(entry->GetNext()); + } + } + + return entry; + } + + /** + * This method indicates whether the linked list contains a given entry. + * + * @param[in] aEntry A reference to an entry. + * + * @retval TRUE The linked list contains @p aEntry. + * @retval FALSE The linked list does not contain @p aEntry. + * + */ + bool Contains(const Type &aEntry) const + { + const Type *prev; + + return Find(aEntry, prev) == OT_ERROR_NONE; + } + + /** + * This template method indicates whether the linked list contains an entry matching a given entry indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aIndicator An entry indicator to match against entries in the list. + * + * @retval TRUE The linked list contains an entry matching @p aIndicator. + * @retval FALSE The linked list contains no entry matching @p aIndicator. + * + */ + template bool ContainsMatching(const Indicator &aIndicator) const + { + return FindMatching(aIndicator) != nullptr; + } + + /** + * This method adds an entry (at the head of the linked list) if it is not already in the list. + * + * @param[in] aEntry A reference to an entry to add. + * + * @retval OT_ERROR_NONE The entry was successfully added at the head of the list. + * @retval OT_ERROR_ALREADY The entry is already in the list. + * + */ + otError Add(Type &aEntry) + { + otError error = OT_ERROR_NONE; + + if (Contains(aEntry)) + { + error = OT_ERROR_ALREADY; + } + else + { + Push(aEntry); + } + + return error; + } + + /** + * This method removes an entry from the linked list. + * + * @note This method does not change the removed entry @p aEntry itself (it is `const`), i.e., the entry next + * pointer of @p aEntry stays as before. + * + * @param[in] aEntry A reference to an entry to remove. + * + * @retval OT_ERROR_NONE The entry was successfully removed from the list. + * @retval OT_ERROR_NOT_FOUND Could not find the entry in the list. + * + */ + otError Remove(const Type &aEntry) + { + Type *prev; + otError error = Find(aEntry, prev); + + if (error == OT_ERROR_NONE) + { + PopAfter(prev); + } + + return error; + } + + /** + * This template method removes an entry matching a given entry indicator from the linked list. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @note This method does not change the removed entry itself (which is returned in case of success), i.e., the + * entry next pointer stays as before. + * + * + * @param[in] aIndicator An entry indicator to match against entries in the list. + * + * @returns A pointer to the removed matching entry if one could be found, or nullptr if no matching entry is found. + * + */ + template Type *RemoveMatching(const Indicator &aIndicator) + { + Type *prev; + Type *entry = FindMatching(aIndicator, prev); + + if (entry != nullptr) + { + PopAfter(prev); + } + + return entry; + } + + /** + * This method searches within the linked list to find an entry and if found returns a pointer to previous entry. + * + * @param[in] aEntry A reference to an entry to find. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when @p aEntry is found in the list). + * @p aPrevEntry is set to nullptr if @p aEntry is the head of the list. Otherwise it is + * updated to point to the previous entry before @p aEntry in the list. + * + * @retval OT_ERROR_NONE The entry was found in the list and @p aPrevEntry was updated successfully. + * @retval OT_ERROR_NOT_FOUND The entry was not found in the list. + * + */ + otError Find(const Type &aEntry, const Type *&aPrevEntry) const + { + otError error = OT_ERROR_NOT_FOUND; + + aPrevEntry = nullptr; + + for (const Type *entry = mHead; entry != nullptr; aPrevEntry = entry, entry = entry->GetNext()) + { + if (entry == &aEntry) + { + error = OT_ERROR_NONE; + break; + } + } + + return error; + } + + /** + * This method searches within the linked list to find an entry and if found returns a pointer to previous entry. + * + * @param[in] aEntry A reference to an entry to find. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when @p aEntry is found in the list). + * @p aPrevEntry is set to nullptr if @p aEntry is the head of the list. Otherwise it is + * updated to point to the previous entry before @p aEntry in the list. + * + * @retval OT_ERROR_NONE The entry was found in the list and @p aPrevEntry was updated successfully. + * @retval OT_ERROR_NOT_FOUND The entry was not found in the list. + * + */ + otError Find(const Type &aEntry, Type *&aPrevEntry) + { + return const_cast(this)->Find(aEntry, const_cast(aPrevEntry)); + } + + /** + * This template method searches within a given range of the linked list to find an entry matching a given + * indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aBegin A pointer to the begin of the range. + * @param[in] aEnd A pointer to the end of the range, or nullptr to search all entries after @p aBegin. + * @param[in] aIndicator An indicator to match with entries in the list. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when a match is found in the list). + * @p aPrevEntry is set to nullptr if the matching entry is the head of the list. Otherwise + * it is updated to point to the previous entry before the matching entry in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template + const Type *FindMatching(const Type *aBegin, + const Type *aEnd, + const Indicator &aIndicator, + const Type *&aPrevEntry) const + { + const Type *entry; + + aPrevEntry = nullptr; + + for (entry = aBegin; entry != aEnd; aPrevEntry = entry, entry = entry->GetNext()) + { + if (entry->Matches(aIndicator)) + { + break; + } + } + + return entry; + } + + /** + * This template method searches within a given range of the linked list to find an entry matching a given + * indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aBegin A pointer to the begin of the range. + * @param[in] aEnd A pointer to the end of the range, or nullptr to search all entries after @p aBegin. + * @param[in] aIndicator An indicator to match with entries in the list. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when a match is found in the list). + * @p aPrevEntry is set to nullptr if the matching entry is the head of the list. Otherwise + * it is updated to point to the previous entry before the matching entry in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template + Type *FindMatching(const Type *aBegin, const Type *aEnd, const Indicator &aIndicator, Type *&aPrevEntry) + { + return const_cast(FindMatching(aBegin, aEnd, aIndicator, const_cast(aPrevEntry))); + } + + /** + * This template method searches within the linked list to find an entry matching a given indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aIndicator An indicator to match with entries in the list. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when a match is found in the list). + * @p aPrevEntry is set to nullptr if the matching entry is the head of the list. Otherwise + * it is updated to point to the previous entry before the matching entry in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template const Type *FindMatching(const Indicator &aIndicator, const Type *&aPrevEntry) const + { + return FindMatching(mHead, nullptr, aIndicator, aPrevEntry); + } + + /** + * This template method searches within the linked list to find an entry matching a given indicator, and if found + * returns a pointer to its previous entry in the list. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aIndicator An indicator to match with entries in the list. + * @param[out] aPrevEntry A pointer to output the previous entry on success (when a match is found in the list). + * @p aPrevEntry is set to nullptr if the matching entry is the head of the list. Otherwise + * it is updated to point to the previous entry before the matching entry in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template Type *FindMatching(const Indicator &aIndicator, Type *&aPrevEntry) + { + return const_cast( + const_cast(this)->FindMatching(aIndicator, const_cast(aPrevEntry))); + } + + /** + * This template method searches within the linked list to find an entry matching a given indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aIndicator An indicator to match with entries in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template const Type *FindMatching(const Indicator &aIndicator) const + { + const Type *prev; + + return FindMatching(aIndicator, prev); + } + + /** + * This template method searches within the linked list to find an entry matching a given indicator. + * + * The template type `Indicator` specifies the type of @p aIndicator object which is used to match against entries + * in the list. To check that an entry matches the given indicator, the `Matches()` method is invoked on each + * `Type` entry in the list. The `Matches()` method should be provided by `Type` class accordingly: + * + * bool Type::Matches(const Indicator &aIndicator) const + * + * @param[in] aIndicator An indicator to match with entries in the list. + * + * @returns A pointer to the matching entry if one is found, or nullptr if no matching entry was found. + * + */ + template Type *FindMatching(const Indicator &aIndicator) + { + return const_cast(const_cast(this)->FindMatching(aIndicator)); + } + + /** + * This method returns the tail of the linked list (i.e., the last entry in the list). + * + * @returns A pointer to the tail entry in the linked list or nullptr if the list is empty. + * + */ + const Type *GetTail(void) const + { + const Type *tail = mHead; + + if (tail != nullptr) + { + while (tail->GetNext() != nullptr) + { + tail = tail->GetNext(); + } + } + + return tail; + } + + /** + * This method returns the tail of the linked list (i.e., the last entry in the list). + * + * @returns A pointer to the tail entry in the linked list or nullptr if the list is empty. + * + */ + Type *GetTail(void) { return const_cast(const_cast(this)->GetTail()); } + +private: + Type *mHead; +}; + +/** + * @} + * + */ + +} // namespace allow_list + +#endif // LINKED_LIST_HPP_ diff --git a/src/rest/extensions/rest_server_common.cpp b/src/rest/extensions/rest_server_common.cpp new file mode 100644 index 00000000000..bf5baf9de07 --- /dev/null +++ b/src/rest/extensions/rest_server_common.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements APIs used for conversions of data from one form to another + * + */ +#include "rest_server_common.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// Function to combine Mesh Local Prefix and IID to form an IPv6 address +void combineMeshLocalPrefixAndIID(const otMeshLocalPrefix *meshLocalPrefix, + const otIp6InterfaceIdentifier *iid, + otIp6Address *ip6Address) +{ + // Copy the Mesh Local Prefix to the first 8 bytes of the IPv6 address + for (size_t i = 0; i < 8; ++i) + { + ip6Address->mFields.m8[i] = meshLocalPrefix->m8[i]; + } + + // Copy the IID to the last 8 bytes of the IPv6 address + for (size_t i = 0; i < 8; ++i) + { + ip6Address->mFields.m8[i + 8] = iid->mFields.m8[i]; + } +} + +// count number of 1s in bitmask +int my_count_ones(uint32_t bitmask) +{ + int count = 0; + while (bitmask) + { + count += bitmask & 1; // Increment count if the least significant bit is 1 + bitmask >>= 1; // Shift bitmask to the right by 1 + } + return count; +} + +static int hex_char_to_int(char c) +{ + if (('A' <= c) && (c <= 'F')) + { + return (uint8_t)c - (uint8_t)'A' + 10; + } + if (('a' <= c) && (c <= 'f')) + { + return (uint8_t)c - (uint8_t)'a' + 10; + } + if (('0' <= c) && (c <= '9')) + { + return (uint8_t)c - (uint8_t)'0'; + } + return -1; +} + +uint8_t joiner_verify_pskd(char *pskd) +{ + int len = strlen(pskd); + if (OT_PSKD_LENGTH_MIN > len) + { + otLogWarnPlat("PSKd %s has incorrect length %d", pskd, len); + return OT_JOINFAILED_LENGTH; + } + if (OT_PSKD_LENGTH_MAX < len) + { + otLogWarnPlat("PSKd %s has incorrect length %d", pskd, len); + return OT_JOINFAILED_LENGTH; + } + for (int i = 0; i < len; i++) + { + if (!isalnum(pskd[i])) + { + otLogWarnPlat("PSKd %s has incorrect format and is not alphanumeric", pskd); + return OT_JOINFAILED_PSKD_FORMAT; + } + if (islower(pskd[i])) + { + otLogWarnPlat("PSKd %s has incorrect format and is not all uppercase", pskd); + return OT_JOINFAILED_PSKD_FORMAT; + } + if ('I' == pskd[i] || 'O' == pskd[i] || 'Q' == pskd[i] || 'Z' == pskd[i]) + { + otLogWarnPlat("PSKd %s has incorrect format and contains illegal character %c", pskd, pskd[i]); + return OT_JOINFAILED_PSKD_FORMAT; + } + } + return WPANSTATUS_OK; +} + +otError str_to_m8(uint8_t *m8, const char *str, uint8_t size) +{ + if (size * 2 > strlen(str)) + { + return OT_ERROR_FAILED; + } + + for (int i = 0; i < size; i++) + { + int hex_int_1 = hex_char_to_int(str[i * 2]); + int hex_int_2 = hex_char_to_int(str[i * 2 + 1]); + if (-1 == hex_int_1 || -1 == hex_int_2) + { + return OT_ERROR_FAILED; + } + m8[i] = (uint8_t)(hex_int_1 * 16 + hex_int_2); + } + + return OT_ERROR_NONE; +} + +bool is_hex_string(char *str) +{ + int offset = 0; + if ('x' == str[1]) + { + if ('0' != str[0]) + { + return false; + } + offset = 2; + } + for (size_t i = offset; i < strlen(str); i++) + { + if (!isxdigit(str[i])) + { + return false; + } + } + return true; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/rest/extensions/rest_server_common.hpp b/src/rest/extensions/rest_server_common.hpp new file mode 100644 index 00000000000..be2ec1911dd --- /dev/null +++ b/src/rest/extensions/rest_server_common.hpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements APIs used for conversions of data from one form to another. + * + */ +#ifndef REST_SERVER_COMMON_HPP_ +#define REST_SERVER_COMMON_HPP_ + +#include "utils/thread_helper.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +typedef enum +{ + LOCK_TYPE_BLOCKING, + LOCK_TYPE_NONBLOCKING, + LOCK_TYPE_TIMED +} LockType; + +#define WPANSTATUS_OK 0 +#define OT_NETWORKKEY_LENGTH 32 +#define OT_PSKD_LENGTH_MIN 6 +#define OT_PSKD_LENGTH_MAX 32 +#define OT_JOINFAILED_LENGTH 16 +#define OT_JOINFAILED_PSKD_FORMAT 17 + +// Function to combine Mesh Local Prefix and IID to form an IPv6 address +void combineMeshLocalPrefixAndIID(const otMeshLocalPrefix *meshLocalPrefix, + const otIp6InterfaceIdentifier *iid, + otIp6Address *ip6Address); + +// count number of 1s in bitmask +int my_count_ones(uint32_t bitmask); + +uint8_t joiner_verify_pskd(char *pskd); + +/** + * @brief str_to_m8, is designed to convert a string of hexadecimal characters + * into an array of bytes (uint8_t). It performs this conversion by processing + * each pair of hexadecimal characters in the input string, converting them + * into their corresponding byte value, and storing the result in the provided array. + * @param uint8_t *m8: A pointer to the array where the converted bytes will be stored. + * @param const char *str: A pointer to the input string containing hexadecimal characters. + * @param uint8_t size: The number of bytes that the m8 array can hold, which dictates how many characters from str + * should be processed. + * @return The function returns an otError code, indicating the success or failure of the conversion process. + */ +otError str_to_m8(uint8_t *m8, const char *str, uint8_t size); + +/** + * @brief [DEPRECATED] please use isValidPerRegex() for new work. + * This function checks if the input string is hex or not + * @param str Hex string to be checked + * @return true if the string is HEX + * @return false if the string is not HEX + */ +bool is_hex_string(char *str); + +#ifdef __cplusplus +} // end of extern "C" +#endif + +#endif diff --git a/src/rest/extensions/rest_task_add_thread_device.cpp b/src/rest/extensions/rest_task_add_thread_device.cpp new file mode 100644 index 00000000000..f60ede8ccc0 --- /dev/null +++ b/src/rest/extensions/rest_task_add_thread_device.cpp @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements the APIs that validate, process, evaluate, jsonify and clean the task. + * + */ +#include "rest_task_add_thread_device.hpp" +#include "rest/extensions/commissioner_allow_list.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +// Accommodating naming convention without refactoring whole codebase +static const char *const ATTRIBUTE_PSKD = "pskd"; + +const char *taskNameAddThreadDevice = "addThreadDeviceTask"; + +static otInstance *mInstance; + +otError addJoiner(task_node_t *task_node, otInstance *aInstance); +uint32_t getJoinerExpirationTime(otExtAddress *aEui); + +cJSON *jsonify_add_thread_device_task(task_node_t *task_node) +{ + otExtAddress eui64 = {0}; + + cJSON *task_json = task_node_to_json(task_node); + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task_json, "attributes"); + // cJSON_DeleteItemFromObject(attributes, ATTRIBUTE_PSKD); + cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); + + if ((task_node->status > ACTIONS_TASK_STATUS_PENDING) && (task_node->status != ACTIONS_TASK_STATUS_UNIMPLEMENTED)) + { + // find allowListEntry and get more detailed status + str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + + if (entryEui64Find(&eui64) != nullptr) + { + cJSON_ReplaceItemInObjectCaseSensitive(attributes, "status", + cJSON_CreateString(entryEui64Find(&eui64)->getStateStr().c_str())); + cJSON_ReplaceItemInObjectCaseSensitive(attributes, "timeout", + cJSON_CreateNumber(getJoinerExpirationTime(&eui64))); + } + else + { + otbrLogWarning("%s:%d - %s - eui not in allowlist: %s", __FILE__, __LINE__, __func__, + cJSON_Print(attributes)); + } + } + return task_json; +} + +uint8_t validate_add_thread_device_task(cJSON *attributes) +{ + otError error = OT_ERROR_NONE; + otExtAddress eui64 = {0}; + + cJSON *timeout = cJSON_GetObjectItemCaseSensitive(attributes, "timeout"); + cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); + cJSON *pskd = cJSON_GetObjectItemCaseSensitive(attributes, ATTRIBUTE_PSKD); + + VerifyOrExit((NULL != timeout && cJSON_IsNumber(timeout)), error = OT_ERROR_FAILED); + + VerifyOrExit( + (NULL != eui && cJSON_IsString(eui) && 16 == strlen(eui->valuestring) && is_hex_string(eui->valuestring)), + error = OT_ERROR_FAILED); + // check eui is convertable + SuccessOrExit(error = str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); + + VerifyOrExit((NULL != pskd && cJSON_IsString(pskd) && (WPANSTATUS_OK == joiner_verify_pskd(pskd->valuestring))), + error = OT_ERROR_FAILED); + +exit: + if (error != OT_ERROR_NONE) + { + // otLogWarnPlat("%s:%d %s %s missing or bad value in field\n%s", __FILE__, __LINE__, taskNameAddThreadDevice, + // __func__, cJSON_Print(attributes)); + otbrLogWarning("%s:%d - %s - missing or bad value in a field: %s", __FILE__, __LINE__, __func__, + cJSON_Print(attributes)); + return ACTIONS_TASK_INVALID; + } + return ACTIONS_TASK_VALID; +} + +otError addJoiner(task_node_t *task_node, otInstance *aInstance) +{ + otError error = OT_ERROR_NONE; + otExtAddress eui64 = {0}; + task_node_t *old_task; + + cJSON *task = task_node->task; + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task, "attributes"); + cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); + cJSON *pskd = cJSON_GetObjectItemCaseSensitive(attributes, ATTRIBUTE_PSKD); + cJSON *timeout = cJSON_GetObjectItemCaseSensitive(attributes, "timeout"); + + str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + + if ((entryEui64Find(&eui64) != NULL) && + (entryEui64Find(&eui64)->mstate < AllowListEntry::kAllowListEntryJoinFailed)) + { + // cancel active task for same joiner, by convention has same uuid as allowListEntry + old_task = task_node_find_by_id((entryEui64Find(&eui64)->muuid)); + task_update_status(old_task, ACTIONS_TASK_STATUS_STOPPED); + } + SuccessOrExit(error = allowListCommissionerJoinerAdd(eui64, (uint32_t)timeout->valueint, pskd->valuestring, + aInstance, task_node->id)); + +exit: + if (error != OT_ERROR_NONE) + { + otbrLogWarning("%s:%d - %s - error: %s", __FILE__, __LINE__, __func__, otThreadErrorToString(error)); + } + return error; +} + +uint32_t getJoinerExpirationTime(otExtAddress *aEui) +{ + uint16_t iter = 0; + otJoinerInfo joinerInfo; + uint32_t retval = 0; + + while (otCommissionerGetNextJoinerInfo(mInstance, &iter, &joinerInfo) == OT_ERROR_NONE) + { + if ((joinerInfo.mType == OT_JOINER_INFO_TYPE_EUI64) && + (std::memcmp(aEui, &joinerInfo.mSharedId.mEui64, OT_EXT_ADDRESS_SIZE) == 0)) + { + retval = joinerInfo.mExpirationTime / 1000; + break; + } + } + return retval; +} + +rest_actions_task_result_t process_add_thread_device_task(task_node_t *task_node, + otInstance *aInstance, + task_doneCallback aCallback) +{ + otError error = OT_ERROR_NONE; + otCommissionerState commissionerState = OT_COMMISSIONER_STATE_DISABLED; + rest_actions_task_result_t ret = ACTIONS_RESULT_SUCCESS; + + OT_UNUSED_VARIABLE(aCallback); + + // Arg check before doing works + VerifyOrExit((NULL != task_node && NULL != task_node->task), error = OT_ERROR_INVALID_ARGS); + + mInstance = aInstance; + commissionerState = otCommissionerGetState(aInstance); + + // If the commissioner is already ACTIVE, we can add ot-joiners right away + if (OT_COMMISSIONER_STATE_ACTIVE == commissionerState) + { + SuccessOrExit(error = addJoiner(task_node, aInstance)); + } + else + { + // ot-commissioner is not ACTIVE yet, so we need to + // wait for the ot-commissioner to become ACTIVE + // and be called again from installed state_change callback + SuccessOrExit(error = allowListCommissionerStart(aInstance)); + ret = ACTIONS_RESULT_RETRY; + } + +exit: + if (error != OT_ERROR_NONE) + { + if (error == OT_ERROR_FAILED) + { + // otLogCritPlat("Cannot create restTaskJoinerAddConditionalTask"); + otbrLogCrit("%s:%d - %s - error %d - Cannot add Joiner.", __FILE__, __LINE__, __func__, error); + ret = ACTIONS_RESULT_FAILURE; + } + else if (error == OT_ERROR_INVALID_STATE || error == OT_ERROR_ALREADY) + { + // otLogWarnPlat("Failed to start the commissioner, error %d", error); + otbrLogWarning("%s:%d - %s - error %d - Failed to start the commissioner.", __FILE__, __LINE__, __func__, + error); + ret = ACTIONS_RESULT_RETRY; + } + else + { + otbrLogWarning("%s: error %d", __func__, error); + ret = ACTIONS_RESULT_FAILURE; + } + } + return ret; +} + +rest_actions_task_result_t evaluate_add_thread_device_task(task_node_t *task_node) +{ + otError error = OT_ERROR_NONE; + otExtAddress eui64 = {0}; + const otExtAddress *addrPtr; + + cJSON *task = task_node->task; + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task, "attributes"); + cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); + + str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + + addrPtr = &eui64; + SuccessOrExit(error = allowListEntryJoinStatusGet(addrPtr)); + +exit: + if (OT_ERROR_FAILED == error) + { + return ACTIONS_RESULT_FAILURE; + } + if (OT_ERROR_NONE == error) + { + return ACTIONS_RESULT_SUCCESS; // caller will mark it as complete in our task_node. + } + + // Don't need to check for OT_ERROR_PENDING as the task is currently pending anyway + return ACTIONS_RESULT_PENDING; +} + +rest_actions_task_result_t clean_add_thread_device_task(task_node_t *task_node, otInstance *aInstance) +{ + cJSON *task = task_node->task; + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task, "attributes"); + cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); + + otError error = OT_ERROR_NONE; + + otExtAddress eui64 = {0}; + str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + + SuccessOrExit(error = allowListCommissionerJoinerRemove(eui64, aInstance)); + SuccessOrExit(error = allowListEntryErase(eui64)); + +exit: + if (OT_ERROR_NONE == error) + { + return ACTIONS_RESULT_SUCCESS; + } + + otbrLogWarning("%s: error %d", __func__, error); + return ACTIONS_RESULT_FAILURE; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/rest/extensions/rest_task_add_thread_device.hpp b/src/rest/extensions/rest_task_add_thread_device.hpp new file mode 100644 index 00000000000..187fafdeeb5 --- /dev/null +++ b/src/rest/extensions/rest_task_add_thread_device.hpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements the APIs that validate, process, evaluate, jsonify and clean the task. + * + */ +#ifndef REST_TASK_ADD_THREAD_DEVICE_HPP_ +#define REST_TASK_ADD_THREAD_DEVICE_HPP_ + +#include "rest_server_common.hpp" +#include "rest_task_handler.hpp" +#include "rest_task_queue.hpp" + +struct cJSON; + +#ifdef __cplusplus +extern "C" { +#endif +#include +#include + +extern const char *taskNameAddThreadDevice; +cJSON *jsonify_add_thread_device_task(task_node_t *task_node); +uint8_t validate_add_thread_device_task(cJSON *task); +rest_actions_task_result_t process_add_thread_device_task(task_node_t *task_node, + otInstance *aInstance, + task_doneCallback aCallback); +rest_actions_task_result_t evaluate_add_thread_device_task(task_node_t *task_node); +rest_actions_task_result_t clean_add_thread_device_task(task_node_t *task_node, otInstance *aInstance); + +#ifdef __cplusplus +} // end of extern "C" +#endif + +#endif // REST_TASK_ADD_THREAD_DEVICE_HPP_ diff --git a/src/rest/extensions/rest_task_handler.cpp b/src/rest/extensions/rest_task_handler.cpp new file mode 100644 index 00000000000..8e1f6ec909f --- /dev/null +++ b/src/rest/extensions/rest_task_handler.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements additional functionailty like `task_node` creation, + * update task status and conversion of `task_node` to `JSON` format. + * + */ +#include "rest_task_handler.hpp" +#include "rest_task_queue.hpp" +#include "uuid.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +static CJSON_PUBLIC(cJSON_bool) + cJSON_AddOrReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + if (cJSON_HasObjectItem(object, string)) + { + return cJSON_ReplaceItemInObjectCaseSensitive(object, string, newitem); + } + else + { + return cJSON_AddItemToObject(object, string, newitem); + } +} + +task_node_t *task_node_new(cJSON *task) +{ + task_node_t *task_node = (task_node_t *)calloc(1, sizeof(task_node_t)); // free in rest_task_queue_handle on delete + assert(NULL != task_node); + + UUID uuid = UUID(); + + // Duplicate the client data associated with this task + task_node->task = cJSON_Duplicate(task, cJSON_True); + + // Initialize the data for this new task to known defaults + // + task_node->prev = NULL; // Task queue management will update this + task_node->next = NULL; // Task queue management will update this + task_node->deleteTask = false; // New tasks are not marked for deletion + + // Populate UUID + uuid.generateRandom(); + uuid.getUuid(task_node->id); + snprintf(task_node->id_str, sizeof(task_node->id_str), uuid.toString().c_str()); + otbrLogWarning("creating new task with id %s", task_node->id_str); + cJSON_AddStringToObject(task_node->task, "id", task_node->id_str); + + // Populated task type by name matching + cJSON *task_type = cJSON_GetObjectItemCaseSensitive(task_node->task, "type"); + task_type_id_from_name(task_type->valuestring, &task_node->type); + + // Populate task creation time + int timestamp = (int)time(NULL); + task_node->created = timestamp; + + // Setup task timeout if provided + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task_node->task, "attributes"); + cJSON *timeout = cJSON_GetObjectItemCaseSensitive(attributes, "timeout"); + + if (cJSON_IsNumber(timeout)) + { + // Set Up Timeout + task_node->timeout = timestamp + (int)(timeout->valueint); + } + else + { + task_node->timeout = ACTIONS_TASK_NO_TIMEOUT; + } + + // Setup task status to pending (both the enum and the string version) + task_update_status(task_node, ACTIONS_TASK_STATUS_PENDING); + if (NULL != attributes) + { + (void)cJSON_AddItemToObject(attributes, "status", + cJSON_CreateString(rest_actions_task_status_s[ACTIONS_TASK_STATUS_PENDING])); + } + // Return the prepared task node + return task_node; +} + +void task_update_status(task_node_t *aTaskNode, rest_actions_task_status_t status) +{ + assert(NULL != aTaskNode); + + aTaskNode->status = status; +} + +bool can_remove_task(task_node_t *aTaskNode) +{ + assert(NULL != aTaskNode); + + return (ACTIONS_TASK_STATUS_COMPLETED == aTaskNode->status || ACTIONS_TASK_STATUS_STOPPED == aTaskNode->status || + ACTIONS_TASK_STATUS_FAILED == aTaskNode->status); +} + +cJSON *task_node_to_json(task_node_t *task_node) +{ + if (NULL == task_node) + { + return NULL; + } + cJSON *task_json = cJSON_Duplicate(task_node->task, cJSON_True); + cJSON *task_attrs = cJSON_GetObjectItemCaseSensitive(task_json, "attributes"); + cJSON_AddOrReplaceItemInObjectCaseSensitive(task_attrs, "status", + cJSON_CreateString(rest_actions_task_status_s[task_node->status])); + // add relationship + // relationships:{ + // result:{ + // data: {type: diagnostics, id: diagnosticsId} + // } + // } + if ((task_node->status == ACTIONS_TASK_STATUS_COMPLETED) && (strlen(task_node->relationship.mType) > 0)) + { + cJSON *relation = cJSON_CreateObject(); + cJSON *result = cJSON_CreateObject(); + cJSON *result_data = cJSON_CreateObject(); + + cJSON_AddStringToObject(result_data, "type", task_node->relationship.mType); + cJSON_AddStringToObject(result_data, "id", task_node->relationship.mId); + cJSON_AddItemToObject(result, "data", result_data); + cJSON_AddItemToObject(relation, "result", result); + cJSON_AddItemToObject(task_json, "relationships", relation); + } + return task_json; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/rest/extensions/rest_task_handler.hpp b/src/rest/extensions/rest_task_handler.hpp new file mode 100644 index 00000000000..2bf41e5b767 --- /dev/null +++ b/src/rest/extensions/rest_task_handler.hpp @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements additional functionailty like `task_node` creation, + * update task status and conversion of `task_node` to `JSON` format. + * + */ +#ifndef REST_TASK_HANDLER_HPP_ +#define REST_TASK_HANDLER_HPP_ + +#include "uuid.hpp" + +struct cJSON; + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @brief Other api/action types can be added with the following enum. + * + */ +typedef enum +{ + // DISCOVER_THREAD_NETWORKS_TASK = 0, + // FORM_THREAD_NETWORK_TASK, + ADD_THREAD_DEVICE_TASK = 0, + // GET_THREAD_NETWORK_DIAGNOSTIC_TASK, + // RESET_THREAD_NETWORK_DIAGNOSTIC_COUNTERS_TASK, + // GET_THREAD_ENERGY_SCAN_TASK, + // IDENTIFY_BORDER_ROUTER_TASK, + // THREAD_BORDER_ROUTER_JOIN_TASK, + // GENERATE_LDEVID_TASK, + ACTIONS_TASKS_SIZE, +} rest_actions_task_t; + +static const char *const gRestActionsTaskNames[] = {"addThreadDeviceTask"}; + +typedef enum +{ + ACTIONS_TASK_STATUS_PENDING = 0, + ACTIONS_TASK_STATUS_ACTIVE, + ACTIONS_TASK_STATUS_COMPLETED, + ACTIONS_TASK_STATUS_STOPPED, + ACTIONS_TASK_STATUS_FAILED, + ACTIONS_TASK_STATUS_UNIMPLEMENTED, +} rest_actions_task_status_t; + +static const char *const rest_actions_task_status_s[] = { + "pending", "active", "completed", "stopped", "failed", "unimplemented", +}; + +#define ACTIONS_TASK_VALID 1 << 0 +#define ACTIONS_TASK_INVALID 1 << 1 +#define ACTIONS_TASK_NOT_IMPLEMENTED 1 << 2 + +typedef enum +{ + ACTIONS_RESULT_SUCCESS, + ACTIONS_RESULT_PENDING, + ACTIONS_RESULT_RETRY, + ACTIONS_RESULT_FAILURE, + ACTIONS_RESULT_STOPPED, + ACTIONS_RESULT_NO_CHANGE_REQUIRED, +} rest_actions_task_result_t; + +// keep a reference to the result of an action +#define MAX_TYPELENGTH 20 +typedef struct relationship +{ + char mType[MAX_TYPELENGTH]; + char mId[UUID_STR_LEN]; +} relationship_t; + +typedef struct task_node_s +{ + cJSON *task; + uuid_t id; + char id_str[UUID_STR_LEN]; + rest_actions_task_t type; + rest_actions_task_status_t status; + int created; + int timeout; + int last_evaluated; + struct task_node_s *prev; + struct task_node_s *next; + bool deleteTask; + relationship_t relationship; +} task_node_t; + +#define ACTIONS_TASK_NO_TIMEOUT -1 + +/** + * @brief Allocate and duplicate a new JSON task to be pushed into the REST action queue. + * The JSON task should be validated and no error checking is performed in this function. + * + * @param task Pointer to cJSON task to be queued + * @return The task_node_t pointer to the newly duplicated and allocated for the given + * JSON task. The pointer is ready to be assigned into a task_node_t queue as needed. + */ +task_node_t *task_node_new(cJSON *task); + +/** + * @brief This function updates the state to one of the value from + * rest_actions_task_status_t + * + * @param task_node pointer of a task to be updated with new status + * @param status Intended status value from rest_actions_task_status_t + */ +void task_update_status(task_node_t *task_node, rest_actions_task_status_t status); + +/** + * @brief This function converts the data from task node into JSON format. + * + * @param task_node A pointer of task node that we want to jsonify. + * @return cJSON* Returns the task node that is convered into JSON format. + */ +cJSON *task_node_to_json(task_node_t *task_node); + +/** + * @brief Checks if a task is completed, failed or stopped. + * + * @param aTaskNode A pointer of task that is being checked for stop, complete or fail condition. + * @return true If one of the condition gets satisfied. + * @return false None of the condition is satisfied. + */ +bool can_remove_task(task_node_t *aTaskNode); + +#ifdef __cplusplus +} // end of extern "C" +#endif + +#endif diff --git a/src/rest/extensions/rest_task_queue.cpp b/src/rest/extensions/rest_task_queue.cpp new file mode 100644 index 00000000000..d1f0019f04a --- /dev/null +++ b/src/rest/extensions/rest_task_queue.cpp @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements APIs related to thread creation and task handling. + * + */ +#include "rest_task_queue.hpp" +#include "rest_task_add_thread_device.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#define EVALUATE_INTERVAL 10 + +task_node_t *task_queue = NULL; +uint8_t task_queue_len = 0; +otInstance *mInstance; + +typedef struct +{ + rest_actions_task_t type_id; + const char **type_name; + task_jsonifier jsonify; + task_validator validate; + task_processor process; + task_evaluator evaluate; + task_cleaner clean; +} task_handlers_t; + +static task_handlers_t *task_handler_by_task_type_id(rest_actions_task_t type_id); + +/** + * This list contains the handlers for each type of task, it must define the + * tasks in the same order as the defined id from `rest_actions_task_t`. It must + * also define all of the tasks (though ACTIONS_TASKS_SIZE must not have an + * associated task as it's a counter). + * + * If these contraints are not met, it will assert during startup. + */ +static task_handlers_t handlers[] = { + + { + .type_id = ADD_THREAD_DEVICE_TASK, + .type_name = &taskNameAddThreadDevice, + .jsonify = jsonify_add_thread_device_task, + .validate = validate_add_thread_device_task, + .process = process_add_thread_device_task, + .evaluate = evaluate_add_thread_device_task, + .clean = clean_add_thread_device_task, + }, +}; + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + +/** + * @brief Finds a task_handlers_t struct for a specific type id if it exists. + * + * @return a task_handlers_t pointer for the specified type id, or NULL if one + * could not be found. + */ +static task_handlers_t *task_handler_by_task_type_id(rest_actions_task_t type_id) +{ + if (type_id < ACTIONS_TASKS_SIZE) + { + return &handlers[type_id]; + } + return NULL; +} + +cJSON *task_to_json(task_node_t *aTaskNode) +{ + if (NULL == aTaskNode || NULL == aTaskNode->task) + { + return NULL; + } + task_handlers_t *handlers = task_handler_by_task_type_id(aTaskNode->type); + if (NULL == handlers || NULL == handlers->jsonify) + { + return NULL; + } + return handlers->jsonify(aTaskNode); +} + +task_node_t *task_node_find_by_id(uuid_t uuid) +{ + task_node_t *head = task_queue; + while (NULL != head) + { + if (uuid_equals(uuid, head->id)) + { + return head; + } + head = head->next; + } + return NULL; +} + +uint8_t can_remove_task_max() +{ + uint8_t can_remove = 0; + task_node_t *head = task_queue; + while (NULL != head) + { + if (can_remove_task(head)) + { + can_remove++; + } + head = head->next; + } + return can_remove; +} + +static bool remove_oldest_non_running_task() +{ + int timestamp = (int)time(NULL); + struct timespec ts; + ts.tv_sec = 0; // Seconds + ts.tv_nsec = 10000000L; // Nanoseconds (10 millisecond) + task_node_t *head; + head = task_queue; + task_node_t *task_node_delete = NULL; + + while (NULL != head) + { + // Find the oldest task by finding the smallest timestamp + if (timestamp > head->created && can_remove_task(head)) + { + timestamp = head->created; + task_node_delete = head; + } + head = head->next; + } + + if (NULL != task_node_delete) + { + // we don't call task_update_status as the task should delete shortly + // after this + task_node_delete->status = ACTIONS_TASK_STATUS_STOPPED; + task_node_delete->deleteTask = true; + nanosleep(&ts, NULL); // 10 millisecond delay + return true; + } + + return false; +} + +void remove_all_task() +{ + task_node_t *head; + head = task_queue; + + while (NULL != head) + { + head->deleteTask = true; + head = head->next; + } +} + +uint8_t validate_task(cJSON *task) +{ + if (NULL == task) + { + return ACTIONS_TASK_INVALID; + } + otbrLogWarning("Validating task: %s", cJSON_PrintUnformatted(task)); + + cJSON *task_type = cJSON_GetObjectItemCaseSensitive(task, "type"); + if (NULL == task_type || !cJSON_IsString(task_type)) + { + otbrLogWarning("%s:%d task missing type field", __FILE__, __LINE__); + return ACTIONS_TASK_INVALID; + } + cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task, "attributes"); + if (NULL == attributes || !cJSON_IsObject(attributes)) + { + otbrLogWarning("%s:%d task missing attributes field", __FILE__, __LINE__); + return ACTIONS_TASK_INVALID; + } + + rest_actions_task_t task_type_id = ACTIONS_TASKS_SIZE; + if (task_type_id_from_name(task_type->valuestring, &task_type_id)) + { + if (task_type_id >= ACTIONS_TASKS_SIZE || NULL == handlers[task_type_id].validate) + { + otbrLogWarning("Could not find a validate handler for %d", task_type_id); + return ACTIONS_TASK_INVALID; + } + // validate task specific attributes + return handlers[task_type_id].validate(attributes); + } + + return ACTIONS_TASK_INVALID; +} + +bool queue_task(cJSON *task, uuid_t *task_id) +{ + otbrLogWarning("Queueing task: %s", cJSON_PrintUnformatted(task)); + if (TASK_QUEUE_MAX <= task_queue_len) + { + if (!remove_oldest_non_running_task()) + { + // Note: This case should not be possible as we already check to see if we exceed queue max before getting + // to this queue fcn + // otLogWarnPlat( + // "Maximum number of tasks hit, and no completed task available for removal, not queueing task"); + otbrLogWarning("%s:%d - %s - %s", __FILE__, __LINE__, __func__, + "Maximum number of tasks hit, not queueing task."); + return false; + } + } + // Generate the task object, and copy the ID to the output + task_node_t *task_node = task_node_new(task); + memcpy(task_id, &(task_node->id), sizeof(uuid_t)); + + if (NULL == task_queue) + { + task_queue = task_node; + task_queue_len = 1; + } + else + { + task_node_t *head = task_queue; + while (NULL != head->next) + { + head = head->next; + } + head->next = task_node; + task_node->prev = head; + task_queue_len++; + } + return true; +} + +void process_task(task_node_t *task_node, otInstance *aInstance, task_doneCallback aDoneCallback) +{ + task_handlers_t *handlers; + rest_actions_task_result_t processed; + + VerifyOrExit(NULL != task_node); + VerifyOrExit(ACTIONS_TASK_STATUS_PENDING == task_node->status); + + handlers = task_handler_by_task_type_id(task_node->type); + + VerifyOrExit((NULL != handlers) && (NULL != handlers->process)); + + processed = handlers->process(task_node, aInstance, aDoneCallback); + + switch (processed) + { + case ACTIONS_RESULT_FAILURE: + task_update_status(task_node, ACTIONS_TASK_STATUS_FAILED); + break; + case ACTIONS_RESULT_RETRY: + // fall through + case ACTIONS_RESULT_NO_CHANGE_REQUIRED: + break; + case ACTIONS_RESULT_PENDING: + // fall through + case ACTIONS_RESULT_SUCCESS: + task_update_status(task_node, ACTIONS_TASK_STATUS_ACTIVE); + break; + case ACTIONS_RESULT_STOPPED: + task_update_status(task_node, ACTIONS_TASK_STATUS_STOPPED); + break; + } + +exit: + // otLogWarnPlat("nullptr error"); + return; +} + +void evaluate_task(task_node_t *task_node) +{ + task_handlers_t *handlers; + rest_actions_task_result_t result; + + VerifyOrExit(NULL != task_node); + + VerifyOrExit(ACTIONS_TASK_STATUS_ACTIVE == task_node->status); + + handlers = task_handler_by_task_type_id(task_node->type); + VerifyOrExit((NULL != handlers) && (NULL != handlers->process)); + + result = handlers->evaluate(task_node); + + switch (result) + { + case ACTIONS_RESULT_FAILURE: + task_update_status(task_node, ACTIONS_TASK_STATUS_FAILED); + break; + case ACTIONS_RESULT_SUCCESS: + task_update_status(task_node, ACTIONS_TASK_STATUS_COMPLETED); + break; + case ACTIONS_RESULT_STOPPED: + task_update_status(task_node, ACTIONS_TASK_STATUS_STOPPED); + break; + default: + // do nothing, wait for next evaluation + break; + } + + task_node->last_evaluated = (int)time(NULL); + +exit: + return; +} + +cJSON *jsonCreateTaskMetaCollection(uint32_t aOffset, uint32_t aLimit, uint32_t aTotal) +{ + cJSON *meta = cJSON_CreateObject(); + cJSON *meta_collection = cJSON_CreateObject(); + // Abort if we are unable to create the necessary JSON objects + if (NULL == meta || NULL == meta_collection) + { + return NULL; + } + + (void)cJSON_AddNumberToObject(meta_collection, "offset", aOffset); + if (aLimit > 0) + { + (void)cJSON_AddNumberToObject(meta_collection, "limit", aLimit); + } + (void)cJSON_AddNumberToObject(meta_collection, "total", aTotal); + // add the count of unfinished actions + cJSON_AddItemToObject(meta_collection, "pending", cJSON_CreateNumber(task_queue_len - can_remove_task_max())); + (void)cJSON_AddItemToObject(meta, "collection", meta_collection); + return meta; +} + +/** + * @brief The main function that iterates through the task_queue and process each task + * High level processing steps: + * + * 1. Delete any tasks that are marked for deletion + * 2. Process any PENDING or ACTIVE tasks + * 3.1 If task is timed out, the task is marked STOPPED (and deleted) + * 3.2 If task is PENDING, call its process() function to make it ACTIVE + * 3.3 If task is ACTIVE, call its evaluate() function to see if it is PENDING|SUCCESS|FAILED + * + */ +void rest_task_queue_handle(void) +{ + task_node_t *head = task_queue; + /* + struct timespec ts; + ts.tv_sec = 0; // Seconds + ts.tv_nsec = 1000000L; // Nanoseconds (1 millisecond) + */ + + while (1) + { + if (NULL == head) + { + // Hit end of queue + break; + } + + // Is this task marked for deletion? + if (head->deleteTask) + { + task_handlers_t *handlers = task_handler_by_task_type_id(head->type); + if (NULL == handlers || NULL == handlers->clean) + { + otbrLogWarning("Could not find a clean handler for %d, assuming no clean needed", head->type); + } + else + { + // calls the clean function defined in handlers[] + handlers->clean(head, mInstance); + } + + if (ACTIONS_TASK_STATUS_STOPPED != head->status) + { + // we don't call task_update_status as we're going to be + // deleting this a few lines below. + head->status = ACTIONS_TASK_STATUS_STOPPED; + } + + task_node_t *next = head->next; + if (NULL == head->prev) + { + // If prev is empty, then we are the start of the list + task_queue = next; + if (NULL != next) + { + next->prev = NULL; + } + } + else + { + head->prev->next = next; + if (NULL != next) + { + next->prev = head->prev; + } + } + { + // Delete the cJSON task as well as the task_node + otbrLogInfo("Deleting task id %s", head->id_str); + cJSON_Delete(head->task); + head->task = NULL; + free(head); + if (task_queue_len > 0) + { + task_queue_len--; + } + } + + head = next; + continue; + } + + // Is this task PENDING or ACTIVE? + if (ACTIONS_TASK_STATUS_PENDING == head->status || ACTIONS_TASK_STATUS_ACTIVE == head->status) + { + // Check if task has timed out if so we need to clean it and mark it as stopped + // We do not delete the task because the GET handler want to keep tabs on what is happening to the tasks. + int current_time = (int)time(NULL); + if (head->timeout >= 0 && head->timeout < current_time) + { + /* Mark tasks that have timed-out without failing/being completed as "Stopped" and stop evaluating */ + otbrLogWarning("%s:%d - %s - task timed out %s.", __FILE__, __LINE__, __func__, + cJSON_PrintUnformatted(head->task)); + task_handlers_t *handlers = task_handler_by_task_type_id(head->type); + if (NULL == handlers || NULL == handlers->clean) + { + otbrLogWarning("Could not find a clean handler for %d, assuming no clean needed", head->type); + } + else + { + handlers->clean(head, mInstance); + } + + task_update_status(head, ACTIONS_TASK_STATUS_STOPPED); + } + // If task has not timed out, carry on with its processing + else + { + // If ACTIONS_TASK_STATUS_PENDING, run its process() function to see if we can make it active + if (ACTIONS_TASK_STATUS_PENDING == head->status) + { + process_task(head, mInstance, rest_task_queue_handle); + } + // Else If ACTIONS_TASK_STATUS_ACTIVE, run its evaluate, to see if it is completed/failed + else if (ACTIONS_TASK_STATUS_ACTIVE == head->status) + { + evaluate_task(head); + } + } + } + // Get ready to process the next task in the queue + head = head->next; + } + // otbrLogWarning("EXITING rest_task_queue_task"); + // pthread_exit(NULL); + + // return NULL; +} + +void rest_task_queue_task_init(otInstance *aInstance) +{ + mInstance = aInstance; + + // As noted above, the handler list needs to have an entry for each + // task type defined in `rest_actions_task_t + assert(ARRAY_SIZE(handlers) > 0); + assert(ARRAY_SIZE(handlers) == ACTIONS_TASKS_SIZE); + + // To optimize during runtime, we want to ensure that the list is ordered + // and contains each of the entries. This allows us to just index in via + // the task type id rather than having to iterate. + // + // This check iterates over the list an ensures that each entry has a + // type_id which is exactly 1 greater than the previous entry. + rest_actions_task_t previous_id = handlers[0].type_id; + for (size_t idx = 1; idx < ARRAY_SIZE(handlers); idx++) + { + assert(previous_id + 1 == handlers[idx].type_id); + previous_id = handlers[idx].type_id; + } +} + +bool task_type_id_from_name(const char *task_name, rest_actions_task_t *type_id) +{ + if (NULL == task_name || NULL == type_id) + { + return false; + } + + for (size_t idx = 0; idx < ACTIONS_TASKS_SIZE; idx++) + { + size_t name_length = strlen(*handlers[idx].type_name); + if (0 == strncmp(task_name, *handlers[idx].type_name, name_length)) + { + *type_id = handlers[idx].type_id; + return true; + } + } + return false; +} + +#ifdef __cplusplus +} +#endif diff --git a/src/rest/extensions/rest_task_queue.hpp b/src/rest/extensions/rest_task_queue.hpp new file mode 100644 index 00000000000..adeb3fed8a7 --- /dev/null +++ b/src/rest/extensions/rest_task_queue.hpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2024, 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. + */ + +/** + * @brief Implements APIs related to thread creation and task handling. + * + */ +#ifndef REST_TASK_QUEUE_HPP_ +#define REST_TASK_QUEUE_HPP_ + +#include "rest_server_common.hpp" +#include "rest_task_handler.hpp" +#include "utils/thread_helper.hpp" + +struct cJSON; + +#ifdef __cplusplus +extern "C" { +#endif + +#define TASK_QUEUE_MAX 100 + +typedef void (*task_doneCallback)(void); + +/** + * @brief Specifies the function signature that the task jsonifier function + * must adhere to. + * + * A task jsonifier is responsible for taking a `task_node_t` pointer + * and creating a JSON representation which can be returned to a + * client. + * + * @param task_node the node which is to be jsonified + * @return a cJSON pointer representing the task node. + */ +typedef cJSON *(*task_jsonifier)(task_node_t *task_node); + +/** + * @brief Specifies the function signature that the task validator function + * must adhere to. + * + * A task validator is responsible for taking the input cJSON from a user and + * ensuring all the various fields and structures meet the requirements set out + * in the API schema. + * + * See the `validate_task` function below for more info. + * + * @param task the JSON structure to return + * @return the value must be one of ACTIONS_TASK_VALID ACTIONS_TASK_INVALID, or + * ACTIONS_TASK_NOT_IMPLEMENTED. + */ +typedef uint8_t (*task_validator)(cJSON *task); + +/** + * @brief Specifies the function signature that the task processor function + * must adhere to. + * + * A task processor is responsible for starting the execution of a task. Once + * the execution has started, the evaluation function is called regularly for + * updates. + * + * @param task the task which is to be executed. + * @param aInstance openthread instance + * @param aDoneCallback is called when process has completed. + * @return the status of the task, which should ACTIONS_RESULT_SUCCESS, + * ACTIONS_RESULT_FAILURE, ACTIONS_RESULT_RETRY, or + * ACTIONS_RESULT_PENDING. + */ +typedef rest_actions_task_result_t (*task_processor)(task_node_t *task_node, + otInstance *aInstance, + task_doneCallback aDoneCallback); + +/** + * @brief Specifies the function signature that the task processor function + * must adhere to. + * + * A task evaluator is responsible for continued execution and processing of + * a task. This is responsible for monitoring the execution of a task and + * reporting when the execution has finished (either successfully or in + * failure). + * + * @param task the task which is being evaluated. + * @return the status of the task, which should be ACTIONS_RESULT_SUCCESS, + * ACTIONS_RESULT_FAILURE, ACTIONS_RESULT_PENDING, or + * ACTIONS_RESULT_NO_CHANGE_REQUIRED. + */ +typedef rest_actions_task_result_t (*task_evaluator)(task_node_t *task_node); + +/** + * @brief Specifies the function signature that the task processor function + * must adhere to. + * + * A task cleaner is responsible for releasing any resources that the task + * is holding so that it can be removed from the queue. + * + * @param task the task which is being cleaned + * @param aInstance openthread instance + * @return the status of the cleaning operation, which should be + * ACTIONS_RESULT_SUCCESS or ACTIONS_RESULT_FAILURE. + */ +typedef rest_actions_task_result_t (*task_cleaner)(task_node_t *task_node, otInstance *aInstance); + +/** + * @brief Validate the REST POST Action Task with the given JSON array + * + * @param task Pointer to cJSON task to be validated + * @return ACTIONS_TASK_VALID if the task is valid, + * ACTIONS_TASK_INVALID if the task is invalid, + * ACTIONS_TASK_NOT_IMPLEMENTED if the task has not been implemented + */ +uint8_t validate_task(cJSON *task); + +/** + * @brief Generates the new task object (task_node) of type 'task_node_t'. + * Initializes task_queue with the newly created task object which will + * be proccessed on different thread. + * + * @param task A pointer to JSON array item. + * @param uuid_t *task_id A reference to get the task_id + * @return true Task queued + * @return false Not able to queue task + */ +bool queue_task(cJSON *task, uuid_t *task_id); +cJSON *task_to_json(task_node_t *task_node); +task_node_t *task_node_find_by_id(uuid_t uuid); + +/** + * @brief When called, I generate a CJSON object for the task metadata + * as specified in the openapi.yaml + * + * sample output: + * + * meta: + * collection: + * offset: 0 // based on the args passed to this function + * limit: 4 // based on the args passed to this function + * total: 4 // determined by the total number of tasks in the queue + * + * @param aOffset the value to use for meta.collection.offset + * @param aLimit the value to use for meta.collection.limit + * @param aTotal the value to use for meta.collection.total + * + * @return cJSON* a populated meta.collection json object, NULL on error + */ +cJSON *jsonCreateTaskMetaCollection(uint32_t aOffset, uint32_t aLimit, uint32_t aTotal); + +/** + * @brief Number of tasks that have finished processing. + * + * @return uint8_t Count of inactive tasks, that are 'completed', 'stopped' or 'failed'. + */ +uint8_t can_remove_task_max(); + +void remove_all_task(); + +void rest_task_queue_task_init(otInstance *aInstance); + +/** + * @brief Iterates through list of tasks for processing and evaluation + * + */ +void rest_task_queue_handle(void); + +void evaluate_task(task_node_t *task_node); +void process_task(task_node_t *task_node, otInstance *aInstance, task_doneCallback aDoneCallback); + +/** + * @brief Looks up the type id for a given task name and updates the `type_id` + * argument if found. + * + * @param task_name the task name to look up the id for + * @param type_id [out] a pointer to place the result into + * @return true if found, false otherwise. + */ +bool task_type_id_from_name(const char *task_name, rest_actions_task_t *type_id); + +#ifdef __cplusplus +} // end of extern "C" +#endif + +#endif diff --git a/src/rest/extensions/timestamp.cpp b/src/rest/extensions/timestamp.cpp new file mode 100644 index 00000000000..3fe9ad38efa --- /dev/null +++ b/src/rest/extensions/timestamp.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024, 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. + */ + +#include "timestamp.hpp" + +#include +#include +//#include +//#include +#include + +using namespace std; +using namespace std::chrono; + +std::string now_rfc3339() +{ + return toRfc3339(system_clock::now()); +} + +/* +// create UTC timestamp +std::string toRfc3339Utc(system_clock::time_point timepoint) +{ + const auto now_ms = time_point_cast(timepoint); + const auto now_s = time_point_cast(now_ms); + const auto millis = now_ms - now_s; + const auto c_now = system_clock::to_time_t(now_s); + + std::stringstream ss; + ss << put_time(gmtime(&c_now), "%FT%T") + << '.' << setfill('0') << setw(3) << millis.count() << 'Z'; + return ss.str(); +} +*/ + +std::string toRfc3339(system_clock::time_point timepoint) +{ + char updated_str[26] = {'\0'}; + std::time_t time_since_epoch; + std::time_t utc_offset; + char offset_str[6] = {'\0'}; + char sign[2] = {'\0'}; + + time_since_epoch = std::chrono::system_clock::to_time_t(timepoint); + utc_offset = + std::difftime(std::mktime(std::localtime(&time_since_epoch)), std::mktime(std::gmtime(&time_since_epoch))); + + std::strftime(offset_str, sizeof(offset_str), "%H:%M", std::localtime(&utc_offset)); + strftime(updated_str, sizeof(updated_str), "%Y-%m-%dT%H:%M:%S", std::localtime(&time_since_epoch)); + + sign[0] = utc_offset < 0 ? '-' : '+'; + sign[1] = '\0'; + + strcat(updated_str, sign); + strcat(updated_str, offset_str); + + return std::string(updated_str); +} diff --git a/src/rest/extensions/timestamp.hpp b/src/rest/extensions/timestamp.hpp new file mode 100644 index 00000000000..6d93b8069f9 --- /dev/null +++ b/src/rest/extensions/timestamp.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, 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. + */ + +#include +#include + +std::string now_rfc3339(); + +// std::string toRfc3339Utc(std::chrono::system_clock::time_point timepoint); + +std::string toRfc3339(std::chrono::system_clock::time_point timepoint); diff --git a/src/rest/extensions/uuid.cpp b/src/rest/extensions/uuid.cpp new file mode 100644 index 00000000000..a78868a5e47 --- /dev/null +++ b/src/rest/extensions/uuid.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024, 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. + */ + +#include "uuid.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +UUID::UUID() +{ + std::memset(&uuid, 0, sizeof(uuid)); +} + +void UUID::generateRandom() +{ + std::random_device rd; // Seed for random number engine + std::mt19937 gen(rd()); // Mersenne Twister RNG + std::uniform_int_distribution<> dist(0, 255); // Byte range: 0 to 255 + + for (size_t i = 0; i < UUID_LEN; ++i) + { + uuid.buf[i] = static_cast(dist(gen)); + } + + // Mark off appropriate bits as per RFC4122 sction 4.4 + uuid.clock_seq_hi_and_reserved = (uuid.clock_seq_hi_and_reserved & 0x3F) | 0x80; + uuid.time_hi_and_version = (uuid.time_hi_and_version & 0x0FFF) | 0x4000; +} + +std::string UUID::toString() const +{ + char out[UUID_STR_LEN]; + std::snprintf(out, UUID_STR_LEN, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid.time_low, uuid.time_mid, + uuid.time_hi_and_version, uuid.clock_seq_hi_and_reserved, uuid.clock_seq_low, uuid.node[0], + uuid.node[1], uuid.node[2], uuid.node[3], uuid.node[4], uuid.node[5]); + return std::string(out); +} + +void UUID::getUuid(uuid_t &aId) const +{ + std::memcpy(aId.buf, uuid.buf, sizeof(aId.buf)); +} + +void UUID::setUuid(uuid_t &aId) +{ + std::memcpy(aId.buf, uuid.buf, sizeof(uuid.buf)); +} + +bool UUID::parse(const std::string &str) +{ + if (str.length() != UUID_STR_LEN - 1) + { + return false; + } + + int temp[11] = {0}; + int r = std::sscanf(str.c_str(), "%8x-%4x-%4x-%2x%2x-%2x%2x%2x%2x%2x%2x", &temp[0], &temp[1], &temp[2], &temp[3], + &temp[4], &temp[5], &temp[6], &temp[7], &temp[8], &temp[9], &temp[10]); + + if (r != 11) + { + return false; + } + + uuid.time_low = temp[0]; + uuid.time_mid = temp[1]; + uuid.time_hi_and_version = temp[2]; + uuid.clock_seq_hi_and_reserved = temp[3]; + uuid.clock_seq_low = temp[4]; + for (int i = 0; i < 6; i++) + { + uuid.node[i] = temp[5 + i]; + } + return true; +} + +bool UUID::equals(const UUID &other) const +{ + return uuid.time_low == other.uuid.time_low && uuid.time_mid == other.uuid.time_mid && + uuid.time_hi_and_version == other.uuid.time_hi_and_version && + uuid.clock_seq_hi_and_reserved == other.uuid.clock_seq_hi_and_reserved && + uuid.clock_seq_low == other.uuid.clock_seq_low && + std::memcmp(uuid.node, other.uuid.node, sizeof(uuid.node)) == 0; +} + +int uuid_equals(uuid_t uuid1, uuid_t uuid2) +{ + return uuid1.time_low == uuid2.time_low && uuid1.time_mid == uuid2.time_mid && + uuid1.time_hi_and_version == uuid2.time_hi_and_version && + uuid1.clock_seq_hi_and_reserved == uuid2.clock_seq_hi_and_reserved && + uuid1.clock_seq_low == uuid2.clock_seq_low && uuid1.node[0] == uuid2.node[0] && + uuid1.node[1] == uuid2.node[1] && uuid1.node[2] == uuid2.node[2] && uuid1.node[3] == uuid2.node[3] && + uuid1.node[4] == uuid2.node[4] && uuid1.node[5] == uuid2.node[5]; +} diff --git a/src/rest/extensions/uuid.hpp b/src/rest/extensions/uuid.hpp new file mode 100644 index 00000000000..dc377ed7f52 --- /dev/null +++ b/src/rest/extensions/uuid.hpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024, 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. + */ + +#ifndef UUID_HPP +#define UUID_HPP + +#include +#include +#include + +const int UUID_LEN = 16; +const int UUID_STR_LEN = 37; + +typedef union uuid_t +{ + struct + { + uint32_t time_low; + uint16_t time_mid; + uint16_t time_hi_and_version; + uint8_t clock_seq_hi_and_reserved; + uint8_t clock_seq_low; + uint8_t node[6]; + }; + uint8_t buf[UUID_LEN]; + +} uuid_t; + +class UUID +{ + // uint8_t buf[UUID_LEN]; + +public: + UUID(); + void generateRandom(); + std::string toString() const; + bool parse(const std::string &str); + bool equals(const UUID &other) const; + + // retrieve the internal uuid data for backwards compatibility + void getUuid(uuid_t &aId) const; + + // set the internal uuid data for backwards compatibility + void setUuid(uuid_t &aId); + + // copy constructor + UUID(const UUID &other) { std::memcpy(&uuid, &other.uuid, sizeof(UUIDData)); } + + bool operator<(const UUID &other) const { return std::memcmp(uuid.buf, other.uuid.buf, UUID_LEN) < 0; } + + bool operator==(const UUID &other) const { return equals(other); } + + // assigment operator + UUID &operator=(const UUID &other) + { + if (this != &other) + { + std::memcpy(&uuid, &other.uuid, sizeof(UUIDData)); + } + return *this; + } + +private: + union UUIDData + { + struct + { + uint32_t time_low; + uint16_t time_mid; + uint16_t time_hi_and_version; + uint8_t clock_seq_hi_and_reserved; + uint8_t clock_seq_low; + uint8_t node[6]; + }; + uint8_t buf[UUID_LEN]; + } uuid; +}; + +/** + * @fn uuid_equals + * + * @brief Check if the two provided UUIDs are equal. + * + * @param uuid1 + * @param uuid2 + * @return + */ +int uuid_equals(uuid_t uuid1, uuid_t uuid2); + +#endif // UUID_HPP diff --git a/src/rest/parser.cpp b/src/rest/parser.cpp index 9401e4d0d5b..05a2db8e30d 100644 --- a/src/rest/parser.cpp +++ b/src/rest/parser.cpp @@ -27,6 +27,7 @@ */ #include "rest/parser.hpp" +#include "utils/string_utils.hpp" #include #include @@ -34,105 +35,148 @@ namespace otbr { namespace rest { -static int OnUrl(http_parser *parser, const char *at, size_t len) +Parser::Parser(Request *aRequest) +{ + mState.mRequest = aRequest; + + mParser.data = &mState; +} + +void Parser::Init(void) +{ + mSettings.on_message_begin = Parser::OnMessageBegin; + mSettings.on_url = Parser::OnUrl; + mSettings.on_status = Parser::OnHandlerData; + mSettings.on_header_field = Parser::OnHeaderField; + mSettings.on_header_value = Parser::OnHeaderData; + mSettings.on_body = Parser::OnBody; + mSettings.on_headers_complete = Parser::OnHeaderComplete; + mSettings.on_message_complete = Parser::OnMessageComplete; + http_parser_init(&mParser, HTTP_REQUEST); +} + +void Parser::Process(const char *aBuf, size_t aLength) { - Request *request = reinterpret_cast(parser->data); + http_parser_execute(&mParser, &mSettings, aBuf, aLength); +} + +int Parser::OnUrl(http_parser *parser, const char *at, size_t len) +{ + State *state = reinterpret_cast(parser->data); if (len > 0) { - request->SetUrl(at, len); + state->mUrl.append(at, len); } return 0; } -static int OnBody(http_parser *parser, const char *at, size_t len) +int Parser::OnBody(http_parser *parser, const char *at, size_t len) { - Request *request = reinterpret_cast(parser->data); + State *state = reinterpret_cast(parser->data); if (len > 0) { - request->SetBody(at, len); + state->mRequest->SetBody(at, len); } return 0; } -static int OnMessageComplete(http_parser *parser) +int Parser::OnMessageComplete(http_parser *parser) { - Request *request = reinterpret_cast(parser->data); + State *state = reinterpret_cast(parser->data); + + http_parser_url urlParser; + http_parser_url_init(&urlParser); + http_parser_parse_url(state->mUrl.c_str(), state->mUrl.length(), 1, &urlParser); + + if (urlParser.field_set & (1 << UF_PATH)) + { + std::string path = state->mUrl.substr(urlParser.field_data[UF_PATH].off, urlParser.field_data[UF_PATH].len); + state->mRequest->SetUrlPath(path); + } - request->SetReadComplete(); + if (urlParser.field_set & (1 << UF_QUERY)) + { + uint16_t offset = urlParser.field_data[UF_QUERY].off; + uint16_t end = offset + urlParser.field_data[UF_QUERY].len; + + while (offset < end) + { + std::string::size_type next = state->mUrl.find('&', offset); + if (next == std::string::npos) + { + next = end; + } + + std::string::size_type split = state->mUrl.find('=', offset); + if (split == std::string::npos) + { + std::string query = state->mUrl.substr(offset, next - offset); + state->mRequest->AddQueryField(query, ""); + } + else if (static_cast(split) < next) + { + std::string query = state->mUrl.substr(offset, split - offset); + std::string value = state->mUrl.substr(split + 1, next - split - 1); + + state->mRequest->AddQueryField(query, value); + } + + offset = static_cast(next + 1); + } + } + + state->mRequest->SetReadComplete(); return 0; } -static int OnMessageBegin(http_parser *parser) +int Parser::OnMessageBegin(http_parser *parser) { - Request *request = reinterpret_cast(parser->data); - request->ResetReadComplete(); + State *state = reinterpret_cast(parser->data); + state->mRequest->ResetReadComplete(); return 0; } -static int OnHeaderComplete(http_parser *parser) +int Parser::OnHeaderComplete(http_parser *parser) { - Request *request = reinterpret_cast(parser->data); - request->SetMethod(parser->method); + State *state = reinterpret_cast(parser->data); + state->mRequest->SetMethod(parser->method); return 0; } -static int OnHandlerData(http_parser *, const char *, size_t) +int Parser::OnHandlerData(http_parser *, const char *, size_t) { return 0; } -static int OnHeaderField(http_parser *parser, const char *at, size_t len) +int Parser::OnHeaderField(http_parser *parser, const char *at, size_t len) { - Request *request = reinterpret_cast(parser->data); + State *state = reinterpret_cast(parser->data); if (len > 0) { - request->SetNextHeaderField(at, len); + state->mNextHeaderField = StringUtils::ToLowercase(std::string(at, len)); } return 0; } -static int OnHeaderData(http_parser *parser, const char *at, size_t len) +int Parser::OnHeaderData(http_parser *parser, const char *at, size_t len) { - Request *request = reinterpret_cast(parser->data); + State *state = reinterpret_cast(parser->data); if (len > 0) { - request->SetHeaderValue(at, len); + state->mRequest->AddHeaderField(state->mNextHeaderField, std::string(at, len)); } return 0; } -Parser::Parser(Request *aRequest) -{ - mParser.data = aRequest; -} - -void Parser::Init(void) -{ - mSettings.on_message_begin = OnMessageBegin; - mSettings.on_url = OnUrl; - mSettings.on_status = OnHandlerData; - mSettings.on_header_field = OnHeaderField; - mSettings.on_header_value = OnHeaderData; - mSettings.on_body = OnBody; - mSettings.on_headers_complete = OnHeaderComplete; - mSettings.on_message_complete = OnMessageComplete; - http_parser_init(&mParser, HTTP_REQUEST); -} - -void Parser::Process(const char *aBuf, size_t aLength) -{ - http_parser_execute(&mParser, &mSettings, aBuf, aLength); -} - } // namespace rest } // namespace otbr diff --git a/src/rest/parser.hpp b/src/rest/parser.hpp index f54378787e3..44090581bfc 100644 --- a/src/rest/parser.hpp +++ b/src/rest/parser.hpp @@ -76,8 +76,26 @@ class Parser void Process(const char *aBuf, size_t aLength); private: + class State + { + public: + Request *mRequest; + std::string mUrl; + std::string mNextHeaderField; + }; + + static int OnUrl(http_parser *parser, const char *at, size_t len); + static int OnBody(http_parser *parser, const char *at, size_t len); + static int OnMessageComplete(http_parser *parser); + static int OnMessageBegin(http_parser *parser); + static int OnHeaderComplete(http_parser *parser); + static int OnHandlerData(http_parser *, const char *, size_t); + static int OnHeaderField(http_parser *parser, const char *at, size_t len); + static int OnHeaderData(http_parser *parser, const char *at, size_t len); + http_parser mParser; http_parser_settings mSettings; + State mState; }; } // namespace rest diff --git a/src/rest/request.cpp b/src/rest/request.cpp index 50a84bed76c..ea093af1dea 100644 --- a/src/rest/request.cpp +++ b/src/rest/request.cpp @@ -37,9 +37,9 @@ Request::Request(void) { } -void Request::SetUrl(const char *aString, size_t aLength) +void Request::SetUrlPath(std::string aPath) { - mUrl += std::string(aString, aLength); + mUrlPath = aPath; } void Request::SetBody(const char *aString, size_t aLength) @@ -57,14 +57,14 @@ void Request::SetMethod(int32_t aMethod) mMethod = aMethod; } -void Request::SetNextHeaderField(const char *aString, size_t aLength) +void Request::AddHeaderField(std::string aField, std::string aValue) { - mNextHeaderField = StringUtils::ToLowercase(std::string(aString, aLength)); + mHeaders[aField] = aValue; } -void Request::SetHeaderValue(const char *aString, size_t aLength) +void Request::AddQueryField(std::string aField, std::string aValue) { - mHeaders[mNextHeaderField] = std::string(aString, aLength); + mQueryParameters[aField] = aValue; } HttpMethod Request::GetMethod() const @@ -77,32 +77,30 @@ std::string Request::GetBody() const return mBody; } -std::string Request::GetUrl(void) const +std::string Request::GetUrlPath(void) const { - std::string url = mUrl; + return mUrlPath; +} - size_t urlEnd = url.find("?"); +std::string Request::GetHeaderValue(const std::string aHeaderField) const +{ + auto it = mHeaders.find(StringUtils::ToLowercase(aHeaderField)); - if (urlEnd != std::string::npos) - { - url = url.substr(0, urlEnd); - } - while (!url.empty() && url[url.size() - 1] == '/') - { - url.pop_back(); - } + return (it == mHeaders.end()) ? "" : it->second; +} - VerifyOrExit(url.size() > 0, url = "/"); +std::string Request::GetQueryParameter(const std::string aQueryName) const +{ + auto it = mQueryParameters.find(aQueryName); -exit: - return url; + return (it == mQueryParameters.end()) ? "" : it->second; } -std::string Request::GetHeaderValue(const std::string aHeaderField) const +bool Request::HasQuery(const std::string aQueryName) const { - auto it = mHeaders.find(StringUtils::ToLowercase(aHeaderField)); + auto it = mQueryParameters.find(aQueryName); - return (it == mHeaders.end()) ? "" : it->second; + return (it == mQueryParameters.end()) ? false : true; } void Request::SetReadComplete(void) diff --git a/src/rest/request.hpp b/src/rest/request.hpp index ccae84f5ab2..b3e59e6a160 100644 --- a/src/rest/request.hpp +++ b/src/rest/request.hpp @@ -57,12 +57,12 @@ class Request Request(void); /** - * This method sets the Url field of a request. + * This method sets the Url Path field of a request. + * + * @param[in] aPath The url path * - * @param[in] aString A pointer points to url string. - * @param[in] aLength Length of the url string */ - void SetUrl(const char *aString, size_t aLength); + void SetUrlPath(std::string aPath); /** * This method sets the body field of a request. @@ -89,18 +89,19 @@ class Request /** * This method sets the next header field of a request. * - * @param[in] aString A pointer points to body string. - * @param[in] aLength Length of the body string + * @param[in] aField The field name. + * @param[in] aValue The value of the field. + * */ - void SetNextHeaderField(const char *aString, size_t aLength); + void AddHeaderField(std::string aField, std::string aValue); /** - * This method sets the header value of the previously set header of a request. + * This method adds a query field to the request. * - * @param[in] aString A pointer points to body string. - * @param[in] aLength Length of the body string + * @param[in] aField The field name. + * @param[in] aValue The value of the field. */ - void SetHeaderValue(const char *aString, size_t aLength); + void AddQueryField(std::string aField, std::string aValue); /** * This method labels the request as complete which means it no longer need to be parsed one more time . @@ -131,7 +132,7 @@ class Request * * @returns A string contains the url of this request. */ - std::string GetUrl(void) const; + std::string GetUrlPath(void) const; /** * This method returns the specified header field for this request. @@ -141,6 +142,22 @@ class Request */ std::string GetHeaderValue(const std::string aHeaderField) const; + /** + * This method returns a boolean describing the presence of the specified query name in this request. + * + * @param aQueryName A query name. + * @return True if the query name is found or False if the query name could not be found. + */ + bool HasQuery(const std::string aQueryName) const; + + /** + * This method returns the specified query parameter for this request. + * + * @param aQueryName A query name. + * @return A string containing the value of the query or an empty string if the query could not be found. + */ + std::string GetQueryParameter(const std::string aQueryName) const; + /** * This method indicates whether this request is parsed completely. */ @@ -149,10 +166,10 @@ class Request private: int32_t mMethod; size_t mContentLength; - std::string mUrl; + std::string mUrlPath; std::string mBody; - std::string mNextHeaderField; std::map mHeaders; + std::map mQueryParameters; bool mComplete; }; diff --git a/src/rest/resource.cpp b/src/rest/resource.cpp index ce154c2e5b3..7b89a10eba7 100644 --- a/src/rest/resource.cpp +++ b/src/rest/resource.cpp @@ -26,9 +26,25 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#include "openthread/mesh_diag.h" +#include "openthread/platform/toolchain.h" +#include "rest/json.hpp" +#include "rest/types.hpp" + +#ifndef OTBR_LOG_TAG #define OTBR_LOG_TAG "REST" +#endif +#include "common/logging.hpp" +#include "common/task_runner.hpp" #include "rest/resource.hpp" +#include "utils/string_utils.hpp" + +extern "C" { +#include +extern task_node_t *task_queue; +extern uint8_t task_queue_len; +} #define OT_PSKC_MAX_LENGTH 16 #define OT_EXTENDED_PANID_LENGTH 8 @@ -46,10 +62,10 @@ #define OT_REST_RESOURCE_PATH_NODE_EXTPANID "/node/ext-panid" #define OT_REST_RESOURCE_PATH_NODE_DATASET_ACTIVE "/node/dataset/active" #define OT_REST_RESOURCE_PATH_NODE_DATASET_PENDING "/node/dataset/pending" -#define OT_REST_RESOURCE_PATH_NETWORK "/networks" -#define OT_REST_RESOURCE_PATH_NETWORK_CURRENT "/networks/current" -#define OT_REST_RESOURCE_PATH_NETWORK_CURRENT_COMMISSION "/networks/commission" -#define OT_REST_RESOURCE_PATH_NETWORK_CURRENT_PREFIX "/networks/current/prefix" + +// API endpoint path definition +#define OT_REST_RESOURCE_PATH_API "/api" +#define OT_REST_RESOURCE_PATH_API_ACTIONS OT_REST_RESOURCE_PATH_API "/actions" #define OT_REST_HTTP_STATUS_200 "200 OK" #define OT_REST_HTTP_STATUS_201 "201 Created" @@ -59,10 +75,13 @@ #define OT_REST_HTTP_STATUS_405 "405 Method Not Allowed" #define OT_REST_HTTP_STATUS_408 "408 Request Timeout" #define OT_REST_HTTP_STATUS_409 "409 Conflict" +#define OT_REST_HTTP_STATUS_415 "415 Unsupported Media Type" #define OT_REST_HTTP_STATUS_500 "500 Internal Server Error" +#define OT_REST_HTTP_STATUS_503 "503 Service Unavailable" using std::chrono::duration_cast; using std::chrono::microseconds; +using std::chrono::milliseconds; using std::chrono::steady_clock; using std::placeholders::_1; @@ -113,14 +132,26 @@ static std::string GetHttpStatus(HttpStatusCode aErrorCode) case HttpStatusCode::kStatusConflict: httpStatus = OT_REST_HTTP_STATUS_409; break; + case HttpStatusCode::kStatusUnsupportedMediaType: + httpStatus = OT_REST_HTTP_STATUS_415; + break; case HttpStatusCode::kStatusInternalServerError: httpStatus = OT_REST_HTTP_STATUS_500; break; + case HttpStatusCode::kStatusServiceUnavailable: + httpStatus = OT_REST_HTTP_STATUS_503; + break; } return httpStatus; } +// extract ItemId from request url +std::string getItemIdFromUrl(const Request &aRequest, std::string aCollectionName); + +/** + * Initialize the Resource class with a pointer to the ControllerOpenThread instance. + */ Resource::Resource(RcpHost *aHost) : mInstance(nullptr) , mHost(aHost) @@ -140,6 +171,9 @@ Resource::Resource(RcpHost *aHost) mResourceMap.emplace(OT_REST_RESOURCE_PATH_NODE_DATASET_ACTIVE, &Resource::DatasetActive); mResourceMap.emplace(OT_REST_RESOURCE_PATH_NODE_DATASET_PENDING, &Resource::DatasetPending); + // API Resource Handler + mResourceMap.emplace(OT_REST_RESOURCE_PATH_API_ACTIONS, &Resource::ApiActionHandler); + // Resource callback handler mResourceCallbackMap.emplace(OT_REST_RESOURCE_PATH_DIAGNOSTICS, &Resource::HandleDiagnosticCallback); } @@ -147,12 +181,37 @@ Resource::Resource(RcpHost *aHost) void Resource::Init(void) { mInstance = mHost->GetThreadHelper()->GetInstance(); + // prepare to handle commissioner state changes + mHost->AddThreadStateChangedCallback((Ncp::RcpHost::ThreadStateChangedCallback)HandleThreadStateChanges); + // start task runner for api/actions + ApiActionRepeatedTaskRunner(2000); } -void Resource::Handle(Request &aRequest, Response &aResponse) const +std::string Resource::redirectToCollection(Request &aRequest) { - std::string url = aRequest.GetUrl(); - auto it = mResourceMap.find(url); + size_t endpointSize; + uint8_t apiPathLength = strlen("/api/"); + + std::string url = aRequest.GetUrlPath(); + + VerifyOrExit(url.compare(0, apiPathLength, std::string("/api/")) == 0); + // check url matches structure /api/{collection}/{itemId} + endpointSize = url.find('/', apiPathLength); + // redirect to /api/{collection} + if (endpointSize != std::string::npos) + { + url = url.substr(0, endpointSize); + } + +exit: + return url; +} + +void Resource::Handle(Request &aRequest, Response &aResponse) +{ + std::string url = redirectToCollection(aRequest); + + auto it = mResourceMap.find(url); if (it != mResourceMap.end()) { @@ -167,8 +226,9 @@ void Resource::Handle(Request &aRequest, Response &aResponse) const void Resource::HandleCallback(Request &aRequest, Response &aResponse) { - std::string url = aRequest.GetUrl(); - auto it = mResourceCallbackMap.find(url); + std::string url = redirectToCollection(aRequest); + + auto it = mResourceCallbackMap.find(url); if (it != mResourceCallbackMap.end()) { @@ -177,6 +237,13 @@ void Resource::HandleCallback(Request &aRequest, Response &aResponse) } } +// calls rest_task_queue_handler after delay_ms +void Resource::ApiActionRepeatedTaskRunner(uint16_t delay_ms) +{ + // TODO: post repeatedly + mHost->PostTimerTask(milliseconds(delay_ms), rest_task_queue_handle); +} + void Resource::HandleDiagnosticCallback(const Request &aRequest, Response &aResponse) { OT_UNUSED_VARIABLE(aRequest); @@ -282,7 +349,7 @@ void Resource::DeleteNodeInfo(Response &aResponse) const } } -void Resource::NodeInfo(const Request &aRequest, Response &aResponse) const +void Resource::NodeInfo(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -324,7 +391,7 @@ void Resource::GetDataBaId(Response &aResponse) const } } -void Resource::BaId(const Request &aRequest, Response &aResponse) const +void Resource::BaId(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -349,7 +416,7 @@ void Resource::GetDataExtendedAddr(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::ExtendedAddr(const Request &aRequest, Response &aResponse) const +void Resource::ExtendedAddr(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -419,7 +486,7 @@ void Resource::SetDataState(const Request &aRequest, Response &aResponse) const } } -void Resource::State(const Request &aRequest, Response &aResponse) const +void Resource::State(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -455,7 +522,7 @@ void Resource::GetDataNetworkName(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::NetworkName(const Request &aRequest, Response &aResponse) const +void Resource::NetworkName(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -494,7 +561,7 @@ void Resource::GetDataLeaderData(Response &aResponse) const } } -void Resource::LeaderData(const Request &aRequest, Response &aResponse) const +void Resource::LeaderData(const Request &aRequest, Response &aResponse) { std::string errorCode; if (aRequest.GetMethod() == HttpMethod::kGet) @@ -532,7 +599,7 @@ void Resource::GetDataNumOfRoute(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::NumOfRoute(const Request &aRequest, Response &aResponse) const +void Resource::NumOfRoute(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -559,7 +626,7 @@ void Resource::GetDataRloc16(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::Rloc16(const Request &aRequest, Response &aResponse) const +void Resource::Rloc16(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -584,7 +651,7 @@ void Resource::GetDataExtendedPanId(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::ExtendedPanId(const Request &aRequest, Response &aResponse) const +void Resource::ExtendedPanId(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -611,7 +678,7 @@ void Resource::GetDataRloc(Response &aResponse) const aResponse.SetResponsCode(errorCode); } -void Resource::Rloc(const Request &aRequest, Response &aResponse) const +void Resource::Rloc(const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -767,7 +834,7 @@ void Resource::SetDataset(DatasetType aDatasetType, const Request &aRequest, Res } } -void Resource::Dataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) const +void Resource::Dataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) { std::string errorCode; @@ -790,12 +857,12 @@ void Resource::Dataset(DatasetType aDatasetType, const Request &aRequest, Respon } } -void Resource::DatasetActive(const Request &aRequest, Response &aResponse) const +void Resource::DatasetActive(const Request &aRequest, Response &aResponse) { Dataset(DatasetType::kActive, aRequest, aResponse); } -void Resource::DatasetPending(const Request &aRequest, Response &aResponse) const +void Resource::DatasetPending(const Request &aRequest, Response &aResponse) { Dataset(DatasetType::kPending, aRequest, aResponse); } @@ -828,7 +895,7 @@ void Resource::UpdateDiag(std::string aKey, std::vector &aDiag mDiagSet[aKey] = value; } -void Resource::Diagnostic(const Request &aRequest, Response &aResponse) const +void Resource::Diagnostic(const Request &aRequest, Response &aResponse) { otbrError error = OTBR_ERROR_NONE; OT_UNUSED_VARIABLE(aRequest); @@ -898,5 +965,248 @@ void Resource::DiagnosticResponseHandler(otError aError, const otMessage *aMessa } } +void Resource::ApiActionHandler(const Request &aRequest, Response &aResponse) +{ + std::string errorCode; + std::string methods = "OPTIONS, GET, POST, DELETE"; + + switch (aRequest.GetMethod()) + { + case HttpMethod::kPost: + ApiActionPostHandler(aRequest, aResponse); + break; + case HttpMethod::kGet: + ApiActionGetHandler(aRequest, aResponse); + break; + case HttpMethod::kDelete: + ApiActionDeleteHandler(aRequest, aResponse); + break; + case HttpMethod::kOptions: + errorCode = GetHttpStatus(HttpStatusCode::kStatusOk); + aResponse.SetAllowMethods(methods); + aResponse.SetResponsCode(errorCode); + aResponse.SetComplete(); + break; + default: + aResponse.SetAllowMethods(methods); + ErrorHandler(aResponse, HttpStatusCode::kStatusMethodNotAllowed); + break; + } +} + +void Resource::HandleThreadStateChanges(otChangedFlags aFlags) +{ + if (aFlags & OT_CHANGED_COMMISSIONER_STATE) + { + otbrLogDebug("%s:%d - %s - commissioner state change.", __FILE__, __LINE__, __func__); + rest_task_queue_handle(); + } +} + +void Resource::ApiActionPostHandler(const Request &aRequest, Response &aResponse) +{ + std::string responseMessage; + std::string errorCode; + HttpStatusCode statusCode = HttpStatusCode::kStatusOk; + cJSON *root; + cJSON *dataArray; + cJSON *resp_data; + cJSON *datum; + uuid_t task_id; + task_node_t *task_node; + cJSON *resp; + const char *resp_str; + + VerifyOrExit((aRequest.GetHeaderValue(OT_REST_CONTENT_TYPE_HEADER).compare(OT_REST_CONTENT_TYPE_JSONAPI) == 0), + statusCode = HttpStatusCode::kStatusUnsupportedMediaType); + + root = cJSON_Parse(aRequest.GetBody().c_str()); + VerifyOrExit(root != NULL, statusCode = HttpStatusCode::kStatusBadRequest); + + // perform general validation before we attempt to + // perform any task specific validation + dataArray = cJSON_GetObjectItemCaseSensitive(root, "data"); + VerifyOrExit((dataArray != NULL) && cJSON_IsArray(dataArray), statusCode = HttpStatusCode::kStatusConflict); + + // validate the form and arguments of all tasks + // before we attempt to perform processing on any of the tasks. + for (int idx = 0; idx < cJSON_GetArraySize(dataArray); idx++) + { + // Require all items in the list to be valid Task items with all required attributes; + // otherwise rejects whole list and returns 409 Conflict. + // Unimplemented tasks counted as failed / invalid tasks + VerifyOrExit(ACTIONS_TASK_VALID == validate_task(cJSON_GetArrayItem(dataArray, idx)), + statusCode = HttpStatusCode::kStatusConflict); + } + + // Check queueing all tasks does not exceed the max number of tasks we can have queued + VerifyOrExit((TASK_QUEUE_MAX - task_queue_len + can_remove_task_max()) > cJSON_GetArraySize(dataArray), + statusCode = HttpStatusCode::kStatusConflict); + + // Queue the tasks and prepare response data + resp_data = cJSON_CreateArray(); + for (int i = 0; i < cJSON_GetArraySize(dataArray); i++) + { + datum = cJSON_GetArrayItem(dataArray, i); + if (queue_task(datum, &task_id)) + { + task_node = task_node_find_by_id(task_id); + cJSON_AddItemToArray(resp_data, task_to_json(task_node)); + // attempt first process of task + rest_task_queue_handle(); + // and another attempt after 2s + ApiActionRepeatedTaskRunner(2000); + } + } + + // prepare reponse object + resp = cJSON_CreateObject(); + cJSON_AddItemToObject(resp, "data", resp_data); + cJSON_AddItemToObject(resp, "meta", jsonCreateTaskMetaCollection(0, TASK_QUEUE_MAX, cJSON_GetArraySize(resp_data))); + + resp_str = cJSON_PrintUnformatted(resp); + otbrLogDebug("%s:%d - %s - Sending (%d):\n%s", __FILE__, __LINE__, __func__, strlen(resp_str), resp_str); + + responseMessage = resp_str; + aResponse.SetBody(responseMessage); + aResponse.SetContentType(OT_REST_CONTENT_TYPE_JSONAPI); + errorCode = GetHttpStatus(HttpStatusCode::kStatusOk); + aResponse.SetResponsCode(errorCode); + aResponse.SetComplete(); + + free((void *)resp_str); + cJSON_Delete(resp); + + // Clear the 'root' JSON object and release its memory (this should also delete 'data') + cJSON_Delete(root); + root = NULL; + +exit: + if (statusCode != HttpStatusCode::kStatusOk) + { + if (root != NULL) + { + cJSON_Delete(root); + } + otbrLogWarning("Error (%d)", statusCode); + ErrorHandler(aResponse, statusCode); + } +} + +void Resource::ApiActionGetHandler(const Request &aRequest, Response &aResponse) +{ + std::string resp_body; + std::string errorCode; + HttpStatusCode statusCode = HttpStatusCode::kStatusOk; + task_node_t *task_node; + cJSON *resp; + cJSON *resp_data; + + std::string itemId = getItemIdFromUrl(aRequest, "actions"); + + VerifyOrExit(aRequest.GetHeaderValue(OT_REST_ACCEPT_HEADER).compare(OT_REST_CONTENT_TYPE_JSONAPI) == 0, + statusCode = HttpStatusCode::kStatusUnsupportedMediaType); + + // update the task status in the queue + rest_task_queue_handle(); + // and another attempt after 2s + ApiActionRepeatedTaskRunner(2000); + + if (aRequest.GetHeaderValue(OT_REST_ACCEPT_HEADER).compare(OT_REST_CONTENT_TYPE_JSONAPI) == 0) + { + aResponse.SetContentType(OT_REST_CONTENT_TYPE_JSONAPI); + + if (!itemId.empty()) + { + // return the item + task_node = task_queue; + while (std::string(task_node->id_str) != itemId) + { + VerifyOrExit(task_node->next != nullptr, statusCode = HttpStatusCode::kStatusResourceNotFound); + + task_node = task_node->next; + } + evaluate_task(task_node); + + resp = cJSON_CreateObject(); + cJSON_AddItemToObject(resp, "data", task_to_json(task_node)); + resp_body = std::string(cJSON_PrintUnformatted(resp)); + } + else + { + // return all items + task_node = task_queue; + resp = cJSON_CreateObject(); + + resp_data = cJSON_CreateArray(); + while (task_node != NULL) + { + evaluate_task(task_node); + cJSON_AddItemToObject(resp_data, "data", task_to_json(task_node)); + task_node = task_node->next; + } + + cJSON_AddItemToObject(resp, "data", resp_data); + cJSON_AddItemToObject(resp, "meta", + jsonCreateTaskMetaCollection(0, TASK_QUEUE_MAX, cJSON_GetArraySize(resp_data))); + + resp_body = std::string(cJSON_PrintUnformatted(resp)); + } + } + + aResponse.SetBody(resp_body); + cJSON_Delete(resp); + + errorCode = GetHttpStatus(statusCode); + aResponse.SetResponsCode(errorCode); + aResponse.SetComplete(); + +exit: + if (statusCode != HttpStatusCode::kStatusOk) + { + ErrorHandler(aResponse, statusCode); + } +} + +void Resource::ApiActionDeleteHandler(const Request &aRequest, Response &aResponse) +{ + std::string errorCode; + + OTBR_UNUSED_VARIABLE(aRequest); + + remove_all_task(); + rest_task_queue_handle(); + + errorCode = GetHttpStatus(HttpStatusCode::kStatusNoContent); + aResponse.SetResponsCode(errorCode); + aResponse.SetComplete(); +} + +std::string getItemIdFromUrl(const Request &aRequest, std::string aCollectionName) +{ + std::string itemId = ""; + std::string url = aRequest.GetUrlPath(); + uint8_t basePathLength = + strlen(OT_REST_RESOURCE_PATH_API) + aCollectionName.length() + 2; // +2 for '/' before and after aCollectionName + size_t idSize = url.find('/', basePathLength); + + VerifyOrExit(url.size() >= basePathLength); + + if (idSize != std::string::npos) + { + idSize = idSize - basePathLength; + } + + itemId = url.substr(basePathLength, idSize); + + if (!itemId.empty()) + { + otbrLogWarning("%s:%d get ItemId %s/%s", __FILE__, __LINE__, aCollectionName.c_str(), itemId.c_str()); + } + +exit: + return itemId; +} + } // namespace rest } // namespace otbr diff --git a/src/rest/resource.hpp b/src/rest/resource.hpp index 7982843b3a6..45f49f28a91 100644 --- a/src/rest/resource.hpp +++ b/src/rest/resource.hpp @@ -34,6 +34,10 @@ #ifndef OTBR_REST_RESOURCE_HPP_ #define OTBR_REST_RESOURCE_HPP_ +/* +Include necessary headers for OpenThread functions, REST utilities, and JSON processing. +*/ + #include "openthread-br/config.h" #include @@ -45,6 +49,9 @@ #include "ncp/rcp_host.hpp" #include "openthread/dataset.h" #include "openthread/dataset_ftd.h" +#include "rest/extensions/rest_task_add_thread_device.hpp" +#include "rest/extensions/rest_task_handler.hpp" +#include "rest/extensions/rest_task_queue.hpp" #include "rest/json.hpp" #include "rest/request.hpp" #include "rest/response.hpp" @@ -81,7 +88,7 @@ class Resource * @param[in] aRequest A request instance referred by the Resource handler. * @param[in,out] aResponse A response instance will be set by the Resource handler. */ - void Handle(Request &aRequest, Response &aResponse) const; + void Handle(Request &aRequest, Response &aResponse); /** * This method distributes a callback handler for each connection needs a callback. @@ -110,22 +117,22 @@ class Resource kPending, ///< Pending Dataset }; - typedef void (Resource::*ResourceHandler)(const Request &aRequest, Response &aResponse) const; + typedef void (Resource::*ResourceHandler)(const Request &aRequest, Response &aResponse); typedef void (Resource::*ResourceCallbackHandler)(const Request &aRequest, Response &aResponse); - void NodeInfo(const Request &aRequest, Response &aResponse) const; - void BaId(const Request &aRequest, Response &aResponse) const; - void ExtendedAddr(const Request &aRequest, Response &aResponse) const; - void State(const Request &aRequest, Response &aResponse) const; - void NetworkName(const Request &aRequest, Response &aResponse) const; - void LeaderData(const Request &aRequest, Response &aResponse) const; - void NumOfRoute(const Request &aRequest, Response &aResponse) const; - void Rloc16(const Request &aRequest, Response &aResponse) const; - void ExtendedPanId(const Request &aRequest, Response &aResponse) const; - void Rloc(const Request &aRequest, Response &aResponse) const; - void Dataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) const; - void DatasetActive(const Request &aRequest, Response &aResponse) const; - void DatasetPending(const Request &aRequest, Response &aResponse) const; - void Diagnostic(const Request &aRequest, Response &aResponse) const; + void NodeInfo(const Request &aRequest, Response &aResponse); + void BaId(const Request &aRequest, Response &aResponse); + void ExtendedAddr(const Request &aRequest, Response &aResponse); + void State(const Request &aRequest, Response &aResponse); + void NetworkName(const Request &aRequest, Response &aResponse); + void LeaderData(const Request &aRequest, Response &aResponse); + void NumOfRoute(const Request &aRequest, Response &aResponse); + void Rloc16(const Request &aRequest, Response &aResponse); + void ExtendedPanId(const Request &aRequest, Response &aResponse); + void Rloc(const Request &aRequest, Response &aResponse); + void Dataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse); + void DatasetActive(const Request &aRequest, Response &aResponse); + void DatasetPending(const Request &aRequest, Response &aResponse); + void Diagnostic(const Request &aRequest, Response &aResponse); void HandleDiagnosticCallback(const Request &aRequest, Response &aResponse); void GetNodeInfo(Response &aResponse) const; @@ -152,6 +159,66 @@ class Resource void *aContext); void DiagnosticResponseHandler(otError aError, const otMessage *aMessage, const otMessageInfo *aMessageInfo); + /** + * Redirects requests from '/api/{collection}/{collectionItem}' to the corresponding collection. + * + * @param[in] aRequest A request instance to be checked and redirected. + * @returns A string containing the redirected url. + */ + std::string redirectToCollection(Request &aRequest); + + /** + * Handles all requests received on 'api/action'. [POST|GET|DELETE] + * + * Routes POST, GET, and DELETE requests to their respective handlers. + * + * @param[in] aRequest A request instance. + * @param[out] aResponse A response instance that will be populated based on the request. + */ + void ApiActionHandler(const Request &aRequest, Response &aResponse); + + /** + * Repeatedly iterates through the list of tasks. + * + * Calls `rest_task_queue_handle` after a specified delay, continuing the iteration. + * TODO: post repeatedly + * + * @param[in] delay_ms The delay in milliseconds between each iteration. + */ + void ApiActionRepeatedTaskRunner(uint16_t delay_ms); + + /** + * Handles the POST request received on 'api/actions'. + * + * This method parses the received JSON data, validates it, and updates the task status + * for further processing. The request will be processed and evaluated by the + * "rest_task_queue_task" thread function. + * + * @param[in] aRequest A request instance containing the POST data. + * @param[out] aResponse A response instance that will be set by an appropriate rest task action. + */ + void ApiActionPostHandler(const Request &aRequest, Response &aResponse); + + /** + * Handles the GET request received on 'api/actions'. + * + * This method retrieves the collection of actions. + * + * @param[in] aRequest A request instance. + * @param[out] aResponse A response instance that will be populated with the collection of actions. + */ + void ApiActionGetHandler(const Request &aRequest, Response &aResponse); + + /** + * Handles the DELETE request received on 'api/actions'. + * + * This method clears all items in the collection of actions. + * + * @param[in] aRequest A request instance. + * @param[out] aResponse A response instance populated with the outcome of the request. + */ + void ApiActionDeleteHandler(const Request &aRequest, Response &aResponse); + otInstance *mInstance; RcpHost *mHost; @@ -159,6 +226,14 @@ class Resource std::unordered_map mResourceCallbackMap; std::unordered_map mDiagSet; + + /** + * @brief A state change handler. Handle list of actions waiting for commissioner + * + * @param aFlags + * @return * void + */ + static void HandleThreadStateChanges(otChangedFlags aFlags); }; } // namespace rest diff --git a/src/rest/response.cpp b/src/rest/response.cpp index 3460b90e1f8..325dd9c6ad5 100644 --- a/src/rest/response.cpp +++ b/src/rest/response.cpp @@ -34,7 +34,7 @@ #define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS \ "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, " \ "Access-Control-Request-Headers" -#define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_METHOD "DELETE, GET, OPTIONS, PUT" +#define OT_REST_RESPONSE_ACCESS_CONTROL_ALLOW_METHOD "DELETE, GET, OPTIONS, PUT, POST" #define OT_REST_RESPONSE_CONNECTION "close" namespace otbr { @@ -80,6 +80,11 @@ void Response::SetResponsCode(std::string &aCode) mCode = aCode; } +void Response::SetAllowMethods(const std::string &aMethods) +{ + mHeaders[OT_REST_ALLOW_HEADER] = aMethods; +} + void Response::SetContentType(const std::string &aContentType) { mHeaders[OT_REST_CONTENT_TYPE_HEADER] = aContentType; diff --git a/src/rest/response.hpp b/src/rest/response.hpp index 1f544b65301..f63ab054565 100644 --- a/src/rest/response.hpp +++ b/src/rest/response.hpp @@ -83,6 +83,14 @@ class Response */ void SetResponsCode(std::string &aCode); + /** + * This method sets the supported methods in "Allow" header field. + * + * @param[in] aCode A string representing supported methods such as 'GET, POST, OPTIONS'. + * + */ + void SetAllowMethods(const std::string &aMethods); + /** * This method sets the content type. * diff --git a/src/rest/rest_web_server.cpp b/src/rest/rest_web_server.cpp index 4e17acf6794..92aad2d6581 100644 --- a/src/rest/rest_web_server.cpp +++ b/src/rest/rest_web_server.cpp @@ -49,6 +49,7 @@ static const uint32_t kMaxServeNum = 500; RestWebServer::RestWebServer(RcpHost &aHost, const std::string &aRestListenAddress, int aRestListenPort) : mResource(Resource(&aHost)) + , mHost(aHost) , mListenFd(-1) { mAddress.sin6_family = AF_INET6; @@ -74,6 +75,23 @@ RestWebServer::~RestWebServer(void) void RestWebServer::Init(void) { mResource.Init(); + + // Initialize the openthread instance + auto threadHelper = mHost.GetThreadHelper(); + mInstance = threadHelper->GetInstance(); + if (mInstance != NULL) + { + // Initialize the mutex and creates a thread to process the 'api/actions' + // Pass the openthread instance that can be used to call openthread apis. + + // removed 'task' (pthread) in the sense of a task for multithreading and do not create such + // parallel task objects that consequently require complicated lock and semaphore mechanisms. + // TODO: stick with term 'action' for an activity that requires indirect response. + + // Initialize the instance pointer + rest_task_queue_task_init(mInstance); + } + InitializeListenFd(); } diff --git a/src/rest/rest_web_server.hpp b/src/rest/rest_web_server.hpp index 08074c54108..f467e51f331 100644 --- a/src/rest/rest_web_server.hpp +++ b/src/rest/rest_web_server.hpp @@ -42,6 +42,9 @@ #include "common/mainloop.hpp" #include "rest/connection.hpp" +#include "rest/extensions/rest_task_add_thread_device.hpp" +#include "rest/extensions/rest_task_queue.hpp" +#include "rest/resource.hpp" using otbr::Ncp::RcpHost; using std::chrono::steady_clock; @@ -85,6 +88,12 @@ class RestWebServer : public MainloopProcessor // Resource handler Resource mResource; + + // OpenThread Controller reference + RcpHost &mHost; + + // Openthread Instance pointer + otInstance *mInstance; // Struct for server configuration sockaddr_in6 mAddress; // File descriptor for listening diff --git a/src/rest/types.hpp b/src/rest/types.hpp index aaa11d1b93a..fb74dca86ce 100644 --- a/src/rest/types.hpp +++ b/src/rest/types.hpp @@ -45,10 +45,12 @@ #include "openthread/netdiag.h" #define OT_REST_ACCEPT_HEADER "Accept" +#define OT_REST_ALLOW_HEADER "Allow" #define OT_REST_CONTENT_TYPE_HEADER "Content-Type" #define OT_REST_CONTENT_TYPE_JSON "application/json" #define OT_REST_CONTENT_TYPE_PLAIN "text/plain" +#define OT_REST_CONTENT_TYPE_JSONAPI "application/vnd.api+json" using std::chrono::steady_clock; @@ -68,15 +70,17 @@ enum class HttpMethod : std::uint8_t enum class HttpStatusCode : std::uint16_t { - kStatusOk = 200, - kStatusCreated = 201, - kStatusNoContent = 204, - kStatusBadRequest = 400, - kStatusResourceNotFound = 404, - kStatusMethodNotAllowed = 405, - kStatusRequestTimeout = 408, - kStatusConflict = 409, - kStatusInternalServerError = 500, + kStatusOk = 200, + kStatusCreated = 201, + kStatusNoContent = 204, + kStatusBadRequest = 400, + kStatusResourceNotFound = 404, + kStatusMethodNotAllowed = 405, + kStatusRequestTimeout = 408, + kStatusConflict = 409, + kStatusUnsupportedMediaType = 415, + kStatusInternalServerError = 500, + kStatusServiceUnavailable = 503, }; enum class PostError : std::uint8_t diff --git a/tests/restjsonapi/actions/Delete_Actions_Collection.bru b/tests/restjsonapi/actions/Delete_Actions_Collection.bru new file mode 100644 index 00000000000..7cd696a7218 --- /dev/null +++ b/tests/restjsonapi/actions/Delete_Actions_Collection.bru @@ -0,0 +1,19 @@ +meta { + name: Delete Actions Collection + type: http + seq: 1 +} + +delete { + url: {{protocol}}://{{host}}:{{port}}{{base_path}}/actions + body: none + auth: none +} + +assert { + res.status: eq 204 +} + +docs { + Delete all items stored in the Collection. +} diff --git a/tests/restjsonapi/actions/Get_ActionItem_by_ItemId_404.bru b/tests/restjsonapi/actions/Get_ActionItem_by_ItemId_404.bru new file mode 100644 index 00000000000..dc6ab2aefa2 --- /dev/null +++ b/tests/restjsonapi/actions/Get_ActionItem_by_ItemId_404.bru @@ -0,0 +1,24 @@ +meta { + name: Get Action Item by ActionId + type: http + seq: 1 +} + +get { + url: {{protocol}}://{{host}}:{{port}}{{base_path}}/actions/{{actionId}} + body: none + auth: none +} + +headers { + Accept: application/vnd.api+json + ~Accept: application/json +} + +vars:pre-request { + actionId: 9ecae480-07a0-4b72-869d-15858196144f +} + +assert { + res.status: eq 404 +} diff --git a/tests/restjsonapi/actions/Get_Actions_Collection.bru b/tests/restjsonapi/actions/Get_Actions_Collection.bru new file mode 100644 index 00000000000..543e70673a2 --- /dev/null +++ b/tests/restjsonapi/actions/Get_Actions_Collection.bru @@ -0,0 +1,47 @@ +meta { + name: Get Actions Collection + type: http + seq: 1 +} + +get { + url: {{protocol}}://{{host}}:{{port}}{{base_path}}/actions + body: none + auth: none +} + +headers { + Accept: application/vnd.api+json +} + +assert { + res.headers['content-type']: eq application/vnd.api+json + res.body: isJson + res.status: eq 200 + res.body.meta.collection: isDefined + res.body.meta.collection.total: gte 1 + res.body.meta.collection.pending: isDefined + res.body.data: isDefined + res.body.data[0].id: isDefined + res.body.data[0].attributes.status: isDefined +} + +tests { + + test("Data contains fields requested", function() { + const data = res.getBody().data; + const size = res.getBody().data.size; + expect(data).to.be.an.instanceOf(Array); + for (let i=0; i < size; i++){ + item = data[i]; + expect(item).to.have.property("id"); + expect(item).to.have.property("type"); + expect(item).to.have.property("attributes"); + expect(item.attributes).to.have.property("status"); + } + }); +} + +docs { + Return all items stored in the Collection. +} diff --git a/tests/restjsonapi/actions/Options.bru b/tests/restjsonapi/actions/Options.bru new file mode 100644 index 00000000000..5126ff7429e --- /dev/null +++ b/tests/restjsonapi/actions/Options.bru @@ -0,0 +1,15 @@ +meta { + name: Options + type: http + seq: 5 +} + +options { + url: {{protocol}}://{{host}}:{{port}}{{base_path}}/actions + body: none + auth: none +} + +assert { + res.headers['allow']: contains OPTIONS, GET, POST, DELETE +} diff --git a/tests/restjsonapi/actions/Post_Add_ThreadDevice.bru b/tests/restjsonapi/actions/Post_Add_ThreadDevice.bru new file mode 100644 index 00000000000..c175520334d --- /dev/null +++ b/tests/restjsonapi/actions/Post_Add_ThreadDevice.bru @@ -0,0 +1,39 @@ +meta { + name: Post Add ThreadDevice Joiner + type: http + seq: 1 +} + +post { + url: {{protocol}}://{{host}}:{{port}}{{base_path}}/actions + body: json + auth: none +} + +headers { + Content-Type: application/vnd.api+json +} + +body:json { + { + "data": [ + { + "type": "addThreadDeviceTask", + "attributes": { + "eui": "fedcba9876543210", + "pskd": "J01NME", + "timeout": 3600 + } + } + ] + } +} + +assert { + res.headers['content-type']: eq application/vnd.api+json + res.body: isJson + res.status: eq 200 + res.body.data: isDefined + res.body.data[0].id: isDefined + res.body.data[0].attributes.status: isDefined +} diff --git a/tests/restjsonapi/bruno.json b/tests/restjsonapi/bruno.json new file mode 100644 index 00000000000..f488367e6ae --- /dev/null +++ b/tests/restjsonapi/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "OpenThread REST JSON:API", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/tests/restjsonapi/environments/localhost.bru b/tests/restjsonapi/environments/localhost.bru new file mode 100644 index 00000000000..82b672ce7b7 --- /dev/null +++ b/tests/restjsonapi/environments/localhost.bru @@ -0,0 +1,6 @@ +vars { + host: localhost + base_path: /api + port: 8081 + protocol: http +} diff --git a/tests/restjsonapi/install_bruno_cli b/tests/restjsonapi/install_bruno_cli new file mode 100755 index 00000000000..96cbfdb3c10 --- /dev/null +++ b/tests/restjsonapi/install_bruno_cli @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Copyright (c) 2024, 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. +# +# Install Bruno CLI +# + +# installs fnm (Fast Node Manager) +curl -fsSL https://fnm.vercel.app/install | bash +# activate fnm +source ~/.bashrc +# download and install Node.js +fnm use --install-if-missing 20 +# verifies the right Node.js version is in the environment +node -v # should print `v20.17.0` +# verifies the right npm version is in the environment +npm -v # should print `10.8.2` +# install Bruno CLI +npm install -g @usebruno/cli diff --git a/tests/restjsonapi/test-restjsonapi-server b/tests/restjsonapi/test-restjsonapi-server new file mode 100755 index 00000000000..7ab8d048e83 --- /dev/null +++ b/tests/restjsonapi/test-restjsonapi-server @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Copyright (c) 2024, 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. +# +# Run Bruno tests +# +# Note: The border router is expected to be connected to a network and not in detached state. + +# Check DELETE collections +bru run actions/Delete_Actions_Collection.bru --env localhost --output results_$LINENO.json + +############################################################## +## Actions ## +############################################################## +# POST Actions +# add a Thread Joiner Device. Note: we will not check success of joining in this script. +bru run actions/Post_Add_ThreadDevice.bru --env localhost --output results_$LINENO.json + +# GET Actions +bru run actions/Get_Actions_Collection.bru --env localhost --output results_$LINENO.json +# GET by itemId +# GET a non-existing item +bru run actions/Get_ActionItem_by_ItemId_404.bru --env localhost --output results_$LINENO.json + +# Check valid options +bru run actions/Options.bru --env localhost --output results_$LINENO.json diff --git a/third_party/openthread/CMakeLists.txt b/third_party/openthread/CMakeLists.txt index eab6e7c6007..8cb7a198920 100644 --- a/third_party/openthread/CMakeLists.txt +++ b/third_party/openthread/CMakeLists.txt @@ -61,6 +61,7 @@ set(OT_NAT64_BORDER_ROUTING ${OTBR_NAT64} CACHE STRING "enable NAT64 in border r set(OT_NAT64_TRANSLATOR ${OTBR_NAT64} CACHE STRING "enable NAT64 translator" FORCE) set(OT_NETDATA_PUBLISHER ON CACHE STRING "enable netdata publisher" FORCE) set(OT_NETDIAG_CLIENT ON CACHE STRING "enable Network Diagnostic client" FORCE) +set(OT_MESH_DIAG ON CACHE STRING "enable Mesh Diagnostics" FORCE) set(OT_PLATFORM "posix" CACHE STRING "use posix platform" FORCE) set(OT_PLATFORM_NETIF ON CACHE STRING "enable platform netif" FORCE) set(OT_PLATFORM_UDP ON CACHE STRING "enable platform UDP" FORCE) From 3c372962baccade2e80001c4a2f1ebf573da6188 Mon Sep 17 00:00:00 2001 From: Martin Zimmermann <30142883+martinzi@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:12:55 +0100 Subject: [PATCH 2/2] [rest] add api/actions 'addThreadDeviceTask' in openapi.yaml specification also includes bug fixes. --- .../extensions/commissioner_allow_list.cpp | 64 +++--- .../extensions/commissioner_allow_list.hpp | 24 ++- src/rest/extensions/rest_server_common.cpp | 9 +- src/rest/extensions/rest_server_common.hpp | 10 +- .../rest_task_add_thread_device.cpp | 28 ++- .../rest_task_add_thread_device.hpp | 1 + src/rest/extensions/rest_task_handler.cpp | 6 +- src/rest/extensions/rest_task_handler.hpp | 9 +- src/rest/extensions/rest_task_queue.cpp | 16 +- src/rest/extensions/rest_task_queue.hpp | 9 +- src/rest/extensions/timestamp.cpp | 6 + src/rest/extensions/timestamp.hpp | 11 + src/rest/extensions/uuid.cpp | 6 + src/rest/extensions/uuid.hpp | 6 + src/rest/openapi.yaml | 190 ++++++++++++++++++ src/rest/resource.cpp | 44 ++-- src/rest/rest_web_server.cpp | 7 - src/rest/types.hpp | 1 + tests/restjsonapi/install_bruno_cli | 3 +- 19 files changed, 345 insertions(+), 105 deletions(-) diff --git a/src/rest/extensions/commissioner_allow_list.cpp b/src/rest/extensions/commissioner_allow_list.cpp index 00c69979ff4..0407945396d 100644 --- a/src/rest/extensions/commissioner_allow_list.cpp +++ b/src/rest/extensions/commissioner_allow_list.cpp @@ -57,6 +57,7 @@ extern "C" { #define COMMISSIONER_START_WAIT_TIME_MS 100 #define COMMISSIONER_START_MAX_ATTEMPTS 5 +static otInstance *mInstance; static allow_list::LinkedList AllowListEntryList; static void consoleEntryPrint(AllowListEntry *aEntry); @@ -104,11 +105,11 @@ AllowListEntry *entryEui64Find(const otExtAddress *aEui64) return entry; } -otError allowListCommissionerJoinerAdd(otExtAddress aEui64, - uint32_t aTimeout, - char *aPskd, - otInstance *aInstance, - uuid_t uuid) +otError allowListCommissionerJoinerAdd(otExtAddress aEui64, + uint32_t aTimeout, + char *aPskd, + otInstance *aInstance, + otbr::rest::uuid_t uuid) { otError error; AllowListEntry *entry = nullptr; @@ -197,8 +198,8 @@ AllowListEntry *parse_buf_as_json(char *aBuf) otExtAddress eui64; uint32_t timeout = 0; AllowListEntry::AllowListEntryState state = AllowListEntry::kAllowListEntryNew; - UUID uuid_obj; - uuid_t uuid; + otbr::rest::UUID uuid_obj; + otbr::rest::uuid_t uuid; char *uuid_str = nullptr; char *eui64_str = nullptr; char *pskdValue = nullptr; @@ -274,10 +275,10 @@ AllowListEntry *parse_buf_as_json(char *aBuf) return pEntry; } -void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, uuid_t aUuid) +void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, otbr::rest::uuid_t aUuid) { assert(nullptr != aPskd); - + otError error; AllowListEntry *pEntry = entryEui64Find(&aEui64); int pskd_len = strlen(aPskd); @@ -294,19 +295,18 @@ void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, uui } else { - // TODO if (aUuid == NULL) - //{ - // // this may not be needed - // uuid_generate_random(&aUuid); - // } pEntry = new AllowListEntry(aEui64, aUuid, aTimeout, pskd_new); if (nullptr == pEntry) { - // otbrLogErr("%s: Err creating a new AllowListEntry", __func__); + otbrLogErr("%s: Err creating a new AllowListEntry", __func__); free(pskd_new); return; } - AllowListEntryList.Add(*pEntry); + error = AllowListEntryList.Add(*pEntry); + if (error != OT_ERROR_NONE) + { + otbrLogWarning("%s: already have AllowListEntry", __func__); + } } consoleEntryPrint(pEntry); @@ -322,7 +322,7 @@ static void consoleEntryPrint(AllowListEntry *aEntry) assert(nullptr != aEntry); // char uuidStr[UUID_STR_LEN] = {0}; - UUID uuid_obj = UUID(); + otbr::rest::UUID uuid_obj = otbr::rest::UUID(); uuid_obj.setUuid(aEntry->muuid); // uuid_unparse(aEntry->muuid, uuidStr); @@ -432,7 +432,7 @@ uint8_t allowListGetPendingJoinersCount(void) while (entry) { - if ((AllowListEntry::kAllowListEntryJoined != entry->mstate) || + if ((AllowListEntry::kAllowListEntryJoined != entry->mstate) && (AllowListEntry::kAllowListEntryJoinFailed != entry->mstate)) { pendingJoinersCount++; @@ -454,6 +454,8 @@ void HandleJoinerEvent(otCommissionerJoinerEvent aEvent, AllowListEntry *entry = nullptr; uint8_t pendingDevicesCount = 0; + otError error = OT_ERROR_NONE; + // @note: Thread may call this for joiners that we are not supposed to join // do not assume `entry` is not null in the rest of the code. entry = entryEui64Find(&aJoinerInfo->mSharedId.mEui64); @@ -510,7 +512,8 @@ void HandleJoinerEvent(otCommissionerJoinerEvent aEvent, // If all entries have been attempted and nothing is pending, stop the commissioner if (0 == pendingDevicesCount) { - allowListCommissionerStopPost(); + error = allowListCommissionerStopPost(); + otbrLogWarning("Commissioner Stop: %s", otThreadErrorToString(error)); } else { @@ -525,6 +528,7 @@ void HandleJoinerEvent(otCommissionerJoinerEvent aEvent, otError allowListCommissionerStart(otInstance *aInstance) { otError error = OT_ERROR_FAILED; + mInstance = aInstance; error = otCommissionerStart(aInstance, &HandleStateChanged, &HandleJoinerEvent, NULL); @@ -533,33 +537,29 @@ otError allowListCommissionerStart(otInstance *aInstance) otError allowListCommissionerStopPost(void) { - return OT_ERROR_NONE; + otError error = OT_ERROR_FAILED; + + error = otCommissionerStop(mInstance); + + return error; } cJSON *AllowListEntry::Allow_list_entry_as_CJSON(const char *entryType) { assert(nullptr != entryType); - cJSON *entry_json_obj = nullptr; - // cJSON *hasActivationKey = nullptr; + cJSON *entry_json_obj = nullptr; cJSON *attributes_json_obj = nullptr; char eui64_str[17] = {0}; - // char uuid_str[UUID_STR_LEN] = {0}; - - UUID uuid_obj = UUID(); - // hasActivationKey = cJSON_CreateObject(); + otbr::rest::UUID uuid_obj = otbr::rest::UUID(); - memset(eui64_str, 0, sizeof(eui64_str)); - sprintf(eui64_str, "%02x%02x%02x%02x%02x%02x%02x%02x", meui64.m8[0], meui64.m8[1], meui64.m8[2], meui64.m8[3], - meui64.m8[4], meui64.m8[5], meui64.m8[6], meui64.m8[7]); + snprintf(eui64_str, sizeof(eui64_str), "%02x%02x%02x%02x%02x%02x%02x%02x", meui64.m8[0], meui64.m8[1], meui64.m8[2], + meui64.m8[3], meui64.m8[4], meui64.m8[5], meui64.m8[6], meui64.m8[7]); - // memset(uuid_str, 0, sizeof(uuid_str)); - // uuid_unparse(muuid, uuid_str); uuid_obj.setUuid(muuid); attributes_json_obj = cJSON_CreateObject(); - // cJSON_AddItemToObject(attributes_json_obj, "hasActivationKey", hasActivationKey); cJSON_AddItemToObject(attributes_json_obj, "eui", cJSON_CreateString(eui64_str)); cJSON_AddItemToObject(attributes_json_obj, "pskd", cJSON_CreateString(mPSKd)); diff --git a/src/rest/extensions/commissioner_allow_list.hpp b/src/rest/extensions/commissioner_allow_list.hpp index b588b74dc46..34647588269 100644 --- a/src/rest/extensions/commissioner_allow_list.hpp +++ b/src/rest/extensions/commissioner_allow_list.hpp @@ -91,7 +91,7 @@ class AllowListEntry : public allow_list::LinkedListEntry /** * This constructor creates an AllowListEntry. */ - AllowListEntry(otExtAddress aEui64, uuid_t uuid, uint32_t aTimeout, char *aPskd) + AllowListEntry(otExtAddress aEui64, otbr::rest::uuid_t uuid, uint32_t aTimeout, char *aPskd) { meui64 = aEui64; muuid = uuid; @@ -101,7 +101,11 @@ class AllowListEntry : public allow_list::LinkedListEntry mNext = nullptr; } - AllowListEntry(otExtAddress aEui64, uuid_t uuid, uint32_t aTimeout, AllowListEntryState state, char *aPskd) + AllowListEntry(otExtAddress aEui64, + otbr::rest::uuid_t uuid, + uint32_t aTimeout, + AllowListEntryState state, + char *aPskd) { meui64 = aEui64; muuid = uuid; @@ -136,7 +140,7 @@ class AllowListEntry : public allow_list::LinkedListEntry // Members otExtAddress meui64; - uuid_t muuid; + otbr::rest::uuid_t muuid; uint32_t mTimeout; char *mPSKd; AllowListEntryState mstate; @@ -180,11 +184,11 @@ bool eui64IsNull(const otExtAddress aEui64); * is not set. OT_ERROR_INVALID_STATE On-Mesh commissioner is not active. * */ -otError allowListCommissionerJoinerAdd(otExtAddress aEui64, - uint32_t aTimeout, - char *aPskd, - otInstance *aInstance, - uuid_t uuid); +otError allowListCommissionerJoinerAdd(otExtAddress aEui64, + uint32_t aTimeout, + char *aPskd, + otInstance *aInstance, + otbr::rest::uuid_t uuid); /** * @brief Remove a single entry from On-Mesh commissioner joiner table @@ -216,7 +220,7 @@ otError allowListEntryErase(otExtAddress aEui64); * automatically removed, in seconds. * @param[in] aPskd Pskd to use when joinig the device */ -void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, uuid_t uuid); +void allowListAddDevice(otExtAddress aEui64, uint32_t aTimeout, char *aPskd, otbr::rest::uuid_t uuid); /** * @brief Print Allow List Entries available in memory to console @@ -270,4 +274,4 @@ otError allowListEntryJoinStatusGet(const otExtAddress *eui64); } #endif -#endif /* EXTENSIONS_COMMISSIONER_ALLOW_LIST_HPP_ */ +#endif // EXTENSIONS_COMMISSIONER_ALLOW_LIST_HPP_ diff --git a/src/rest/extensions/rest_server_common.cpp b/src/rest/extensions/rest_server_common.cpp index bf5baf9de07..e36c0777867 100644 --- a/src/rest/extensions/rest_server_common.cpp +++ b/src/rest/extensions/rest_server_common.cpp @@ -32,11 +32,13 @@ */ #include "rest_server_common.hpp" +namespace otbr { +namespace rest { + #ifdef __cplusplus extern "C" { #endif -#include #include #include @@ -59,7 +61,7 @@ void combineMeshLocalPrefixAndIID(const otMeshLocalPrefix *meshLocalPrefi } // count number of 1s in bitmask -int my_count_ones(uint32_t bitmask) +int count_ones(uint32_t bitmask) { int count = 0; while (bitmask) @@ -166,3 +168,6 @@ bool is_hex_string(char *str) #ifdef __cplusplus } #endif + +} // namespace rest +} // namespace otbr diff --git a/src/rest/extensions/rest_server_common.hpp b/src/rest/extensions/rest_server_common.hpp index be2ec1911dd..37fd4d54d9c 100644 --- a/src/rest/extensions/rest_server_common.hpp +++ b/src/rest/extensions/rest_server_common.hpp @@ -35,6 +35,9 @@ #include "utils/thread_helper.hpp" +namespace otbr { +namespace rest { + #ifdef __cplusplus extern "C" { #endif @@ -64,7 +67,7 @@ void combineMeshLocalPrefixAndIID(const otMeshLocalPrefix *meshLocalPrefi otIp6Address *ip6Address); // count number of 1s in bitmask -int my_count_ones(uint32_t bitmask); +int count_ones(uint32_t bitmask); uint8_t joiner_verify_pskd(char *pskd); @@ -94,4 +97,7 @@ bool is_hex_string(char *str); } // end of extern "C" #endif -#endif +} // namespace rest +} // namespace otbr + +#endif // REST_SERVER_COMMON_HPP_ diff --git a/src/rest/extensions/rest_task_add_thread_device.cpp b/src/rest/extensions/rest_task_add_thread_device.cpp index f60ede8ccc0..8f67c4b29f7 100644 --- a/src/rest/extensions/rest_task_add_thread_device.cpp +++ b/src/rest/extensions/rest_task_add_thread_device.cpp @@ -57,6 +57,7 @@ uint32_t getJoinerExpirationTime(otExtAddress *aEui); cJSON *jsonify_add_thread_device_task(task_node_t *task_node) { otExtAddress eui64 = {0}; + otError error = OT_ERROR_NONE; cJSON *task_json = task_node_to_json(task_node); cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task_json, "attributes"); @@ -66,7 +67,7 @@ cJSON *jsonify_add_thread_device_task(task_node_t *task_node) if ((task_node->status > ACTIONS_TASK_STATUS_PENDING) && (task_node->status != ACTIONS_TASK_STATUS_UNIMPLEMENTED)) { // find allowListEntry and get more detailed status - str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + SuccessOrExit(error = otbr::rest::str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); if (entryEui64Find(&eui64) != nullptr) { @@ -81,6 +82,12 @@ cJSON *jsonify_add_thread_device_task(task_node_t *task_node) cJSON_Print(attributes)); } } +exit: + if (error != OT_ERROR_NONE) + { + otbrLogWarning("%s:%d - %s - missing or bad value in a field: %s", __FILE__, __LINE__, __func__, + cJSON_Print(attributes)); + } return task_json; } @@ -95,14 +102,15 @@ uint8_t validate_add_thread_device_task(cJSON *attributes) VerifyOrExit((NULL != timeout && cJSON_IsNumber(timeout)), error = OT_ERROR_FAILED); - VerifyOrExit( - (NULL != eui && cJSON_IsString(eui) && 16 == strlen(eui->valuestring) && is_hex_string(eui->valuestring)), - error = OT_ERROR_FAILED); + VerifyOrExit((NULL != eui && cJSON_IsString(eui) && 16 == strlen(eui->valuestring) && + otbr::rest::is_hex_string(eui->valuestring)), + error = OT_ERROR_FAILED); // check eui is convertable - SuccessOrExit(error = str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); + SuccessOrExit(error = otbr::rest::str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); - VerifyOrExit((NULL != pskd && cJSON_IsString(pskd) && (WPANSTATUS_OK == joiner_verify_pskd(pskd->valuestring))), - error = OT_ERROR_FAILED); + VerifyOrExit( + (NULL != pskd && cJSON_IsString(pskd) && (WPANSTATUS_OK == otbr::rest::joiner_verify_pskd(pskd->valuestring))), + error = OT_ERROR_FAILED); exit: if (error != OT_ERROR_NONE) @@ -128,7 +136,7 @@ otError addJoiner(task_node_t *task_node, otInstance *aInstance) cJSON *pskd = cJSON_GetObjectItemCaseSensitive(attributes, ATTRIBUTE_PSKD); cJSON *timeout = cJSON_GetObjectItemCaseSensitive(attributes, "timeout"); - str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + SuccessOrExit(error = otbr::rest::str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); if ((entryEui64Find(&eui64) != NULL) && (entryEui64Find(&eui64)->mstate < AllowListEntry::kAllowListEntryJoinFailed)) @@ -231,7 +239,7 @@ rest_actions_task_result_t evaluate_add_thread_device_task(task_node_t *task_nod cJSON *attributes = cJSON_GetObjectItemCaseSensitive(task, "attributes"); cJSON *eui = cJSON_GetObjectItemCaseSensitive(attributes, "eui"); - str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + SuccessOrExit(error = otbr::rest::str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); addrPtr = &eui64; SuccessOrExit(error = allowListEntryJoinStatusGet(addrPtr)); @@ -259,7 +267,7 @@ rest_actions_task_result_t clean_add_thread_device_task(task_node_t *task_node, otError error = OT_ERROR_NONE; otExtAddress eui64 = {0}; - str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE); + SuccessOrExit(error = otbr::rest::str_to_m8(eui64.m8, eui->valuestring, OT_EXT_ADDRESS_SIZE)); SuccessOrExit(error = allowListCommissionerJoinerRemove(eui64, aInstance)); SuccessOrExit(error = allowListEntryErase(eui64)); diff --git a/src/rest/extensions/rest_task_add_thread_device.hpp b/src/rest/extensions/rest_task_add_thread_device.hpp index 187fafdeeb5..7e922b2699c 100644 --- a/src/rest/extensions/rest_task_add_thread_device.hpp +++ b/src/rest/extensions/rest_task_add_thread_device.hpp @@ -37,6 +37,7 @@ #include "rest_task_handler.hpp" #include "rest_task_queue.hpp" +// Forward declare cJSON in the global scope struct cJSON; #ifdef __cplusplus diff --git a/src/rest/extensions/rest_task_handler.cpp b/src/rest/extensions/rest_task_handler.cpp index 8e1f6ec909f..79d99aa007e 100644 --- a/src/rest/extensions/rest_task_handler.cpp +++ b/src/rest/extensions/rest_task_handler.cpp @@ -31,6 +31,8 @@ * update task status and conversion of `task_node` to `JSON` format. * */ +#include + #include "rest_task_handler.hpp" #include "rest_task_queue.hpp" #include "uuid.hpp" @@ -61,7 +63,7 @@ task_node_t *task_node_new(cJSON *task) task_node_t *task_node = (task_node_t *)calloc(1, sizeof(task_node_t)); // free in rest_task_queue_handle on delete assert(NULL != task_node); - UUID uuid = UUID(); + otbr::rest::UUID uuid = otbr::rest::UUID(); // Duplicate the client data associated with this task task_node->task = cJSON_Duplicate(task, cJSON_True); @@ -75,7 +77,7 @@ task_node_t *task_node_new(cJSON *task) // Populate UUID uuid.generateRandom(); uuid.getUuid(task_node->id); - snprintf(task_node->id_str, sizeof(task_node->id_str), uuid.toString().c_str()); + snprintf(task_node->id_str, sizeof(task_node->id_str), "%s", uuid.toString().c_str()); otbrLogWarning("creating new task with id %s", task_node->id_str); cJSON_AddStringToObject(task_node->task, "id", task_node->id_str); diff --git a/src/rest/extensions/rest_task_handler.hpp b/src/rest/extensions/rest_task_handler.hpp index 2bf41e5b767..1f39e935d5f 100644 --- a/src/rest/extensions/rest_task_handler.hpp +++ b/src/rest/extensions/rest_task_handler.hpp @@ -36,6 +36,7 @@ #include "uuid.hpp" +// Forward declare cJSON in the global scope struct cJSON; #ifdef __cplusplus @@ -98,14 +99,14 @@ typedef enum typedef struct relationship { char mType[MAX_TYPELENGTH]; - char mId[UUID_STR_LEN]; + char mId[otbr::rest::UUID_STR_LEN]; } relationship_t; typedef struct task_node_s { cJSON *task; - uuid_t id; - char id_str[UUID_STR_LEN]; + otbr::rest::uuid_t id; + char id_str[otbr::rest::UUID_STR_LEN]; rest_actions_task_t type; rest_actions_task_status_t status; int created; @@ -159,4 +160,4 @@ bool can_remove_task(task_node_t *aTaskNode); } // end of extern "C" #endif -#endif +#endif // REST_TASK_HANDLER_HPP_ diff --git a/src/rest/extensions/rest_task_queue.cpp b/src/rest/extensions/rest_task_queue.cpp index d1f0019f04a..51bfd0b8cd2 100644 --- a/src/rest/extensions/rest_task_queue.cpp +++ b/src/rest/extensions/rest_task_queue.cpp @@ -115,12 +115,12 @@ cJSON *task_to_json(task_node_t *aTaskNode) return handlers->jsonify(aTaskNode); } -task_node_t *task_node_find_by_id(uuid_t uuid) +task_node_t *task_node_find_by_id(otbr::rest::uuid_t uuid) { task_node_t *head = task_queue; while (NULL != head) { - if (uuid_equals(uuid, head->id)) + if (otbr::rest::uuid_equals(uuid, head->id)) { return head; } @@ -226,7 +226,7 @@ uint8_t validate_task(cJSON *task) return ACTIONS_TASK_INVALID; } -bool queue_task(cJSON *task, uuid_t *task_id) +bool queue_task(cJSON *task, otbr::rest::uuid_t *task_id) { otbrLogWarning("Queueing task: %s", cJSON_PrintUnformatted(task)); if (TASK_QUEUE_MAX <= task_queue_len) @@ -244,7 +244,7 @@ bool queue_task(cJSON *task, uuid_t *task_id) } // Generate the task object, and copy the ID to the output task_node_t *task_node = task_node_new(task); - memcpy(task_id, &(task_node->id), sizeof(uuid_t)); + memcpy(task_id, &(task_node->id), sizeof(otbr::rest::uuid_t)); if (NULL == task_queue) { @@ -485,10 +485,6 @@ void rest_task_queue_handle(void) // Get ready to process the next task in the queue head = head->next; } - // otbrLogWarning("EXITING rest_task_queue_task"); - // pthread_exit(NULL); - - // return NULL; } void rest_task_queue_task_init(otInstance *aInstance) @@ -506,11 +502,9 @@ void rest_task_queue_task_init(otInstance *aInstance) // // This check iterates over the list an ensures that each entry has a // type_id which is exactly 1 greater than the previous entry. - rest_actions_task_t previous_id = handlers[0].type_id; for (size_t idx = 1; idx < ARRAY_SIZE(handlers); idx++) { - assert(previous_id + 1 == handlers[idx].type_id); - previous_id = handlers[idx].type_id; + assert(handlers[idx - 1].type_id + 1 == handlers[idx].type_id); } } diff --git a/src/rest/extensions/rest_task_queue.hpp b/src/rest/extensions/rest_task_queue.hpp index adeb3fed8a7..5804ea44cad 100644 --- a/src/rest/extensions/rest_task_queue.hpp +++ b/src/rest/extensions/rest_task_queue.hpp @@ -37,6 +37,7 @@ #include "rest_task_handler.hpp" #include "utils/thread_helper.hpp" +// Forward declare cJSON in the global scope struct cJSON; #ifdef __cplusplus @@ -141,13 +142,13 @@ uint8_t validate_task(cJSON *task); * be proccessed on different thread. * * @param task A pointer to JSON array item. - * @param uuid_t *task_id A reference to get the task_id + * @param task_id A reference to get the task_id * @return true Task queued * @return false Not able to queue task */ -bool queue_task(cJSON *task, uuid_t *task_id); +bool queue_task(cJSON *task, otbr::rest::uuid_t *task_id); cJSON *task_to_json(task_node_t *task_node); -task_node_t *task_node_find_by_id(uuid_t uuid); +task_node_t *task_node_find_by_id(otbr::rest::uuid_t uuid); /** * @brief When called, I generate a CJSON object for the task metadata @@ -203,4 +204,4 @@ bool task_type_id_from_name(const char *task_name, rest_actions_task_t *type_id) } // end of extern "C" #endif -#endif +#endif // REST_TASK_QUEUE_HPP_ diff --git a/src/rest/extensions/timestamp.cpp b/src/rest/extensions/timestamp.cpp index 3fe9ad38efa..cbad58e95b7 100644 --- a/src/rest/extensions/timestamp.cpp +++ b/src/rest/extensions/timestamp.cpp @@ -37,6 +37,9 @@ using namespace std; using namespace std::chrono; +namespace otbr { +namespace rest { + std::string now_rfc3339() { return toRfc3339(system_clock::now()); @@ -81,3 +84,6 @@ std::string toRfc3339(system_clock::time_point timepoint) return std::string(updated_str); } + +} // namespace rest +} // namespace otbr diff --git a/src/rest/extensions/timestamp.hpp b/src/rest/extensions/timestamp.hpp index 6d93b8069f9..1ed8998624b 100644 --- a/src/rest/extensions/timestamp.hpp +++ b/src/rest/extensions/timestamp.hpp @@ -26,11 +26,22 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#ifndef TIMESTAMP_HPP +#define TIMESTAMP_HPP + #include #include +namespace otbr { +namespace rest { + std::string now_rfc3339(); // std::string toRfc3339Utc(std::chrono::system_clock::time_point timepoint); std::string toRfc3339(std::chrono::system_clock::time_point timepoint); + +} // namespace rest +} // namespace otbr + +#endif // TIMESTAMP_HPP diff --git a/src/rest/extensions/uuid.cpp b/src/rest/extensions/uuid.cpp index a78868a5e47..994ae051c1a 100644 --- a/src/rest/extensions/uuid.cpp +++ b/src/rest/extensions/uuid.cpp @@ -36,6 +36,9 @@ #include #include +namespace otbr { +namespace rest { + UUID::UUID() { std::memset(&uuid, 0, sizeof(uuid)); @@ -122,3 +125,6 @@ int uuid_equals(uuid_t uuid1, uuid_t uuid2) uuid1.node[1] == uuid2.node[1] && uuid1.node[2] == uuid2.node[2] && uuid1.node[3] == uuid2.node[3] && uuid1.node[4] == uuid2.node[4] && uuid1.node[5] == uuid2.node[5]; } + +} // namespace rest +} // namespace otbr diff --git a/src/rest/extensions/uuid.hpp b/src/rest/extensions/uuid.hpp index dc377ed7f52..8912ebd9c55 100644 --- a/src/rest/extensions/uuid.hpp +++ b/src/rest/extensions/uuid.hpp @@ -33,6 +33,9 @@ #include #include +namespace otbr { +namespace rest { + const int UUID_LEN = 16; const int UUID_STR_LEN = 37; @@ -112,4 +115,7 @@ class UUID */ int uuid_equals(uuid_t uuid1, uuid_t uuid2); +} // namespace rest +} // namespace otbr + #endif // UUID_HPP diff --git a/src/rest/openapi.yaml b/src/rest/openapi.yaml index 2ba2a4dd56f..70e5a3a2598 100644 --- a/src/rest/openapi.yaml +++ b/src/rest/openapi.yaml @@ -14,11 +14,201 @@ info: servers: - url: http://localhost:8081 tags: + - name: Actions + description: Task queue. - name: node description: Thread parameters of this node. - name: diagnostics description: Thread network diagnostic. paths: + /api/actions: + get: + tags: + - Actions + summary: Read Actions collection. + parameters: + - name: Accept + description: Must be set to `application/vnd.api+json` + in: header + required: true + schema: + type: string + example: application/vnd.api+json + responses: + "200": + description: List of actions. + content: + application/vnd.api+json: + schema: + type: object + example: + data: + - id: 9ecae480-07a0-4b72-869d-15858196144e + type: addThreadDeviceTask + attributes: + eui: "fedcba9876543210" + pskd: "J01NME" + timeout: 888 + status: "pending" + - id: 9ecae480-07a0-4b72-869d-15858196144f + type: addThreadDeviceTask + attributes: + eui: "fedcba9876543211" + pskd: "J01NME2" + timeout: 333 + status: "pending" + post: + tags: + - Actions + summary: Add task(s) to the Actions collection. + description: | + Input to add one or more *Task* items to the *Actions* collection (i.e., enqueue). + + ## Condition + - requires all items in the list to be valid *Task* items with all required attributes; + otherwise rejects whole list and returns 422 Conflict. + - requires the *Actions* collection to have free memory slots to enqueue the new items; + completed, stopped, or failed items in the collection may be removed (oldest first); + otherwise, + - if number of items send by client exceeds maximum number of items in collection, rejects whole list and returns 409 Conflict + - else if not enough items can be freed, rejects whole list and returns 503 Service Unavailable + + ## On Success + - enqueues the tasks given in the `data` array + - each *Task* item is given a unique `id` + - each *Task* item is given a `status` attribute that is initialized with `pending` + - returns 200 OK listing all items enqueued with ID and status + + ## On Failure + Returns one of the following: + - 409 Conflict, when the request contains more items than the maximum total number of items for the collection + - 415 Unsupported Media Type, when the request content type is not `application/vnd.api+json`, TODO: allow `application/json` or empty (tolerant server) + - 422 Unprocessable Content, when invalid items are included or required task-specific attributes are missing + - 503 Service Unavailable, when no more items can be enqueued (but the number of items in the request does not exceed the maximum) + + ## General Background Logic (amended by task-specific background logic) + - when a *Task* item is executed, its `status` attribute changes to `active` + - when a *Task* item completes successfully, its `status` attribute changes to `completed` + - when a *Task* item completes unsuccessfully, its `status` attribute changes to `stopped` + - when a *Task* item fails, its `status` attribute changes to `failed` + + parameters: + - name: Accept + description: Must be set to `application/vnd.api+json` + in: header + required: true + schema: + type: string + example: application/vnd.api+json + responses: + "200": + description: Task accepted and queue for execution. + content: + application/vnd.api+json: + schema: + type: object + example: + data: + - id: 9ecae480-07a0-4b72-869d-15858196144f + type: addThreadDeviceTask + attributes: + eui: "fedcba9876543210" + pskd: "J01NME" + timeout: 900 + status: "pending" + "400": + description: The client sent an invalid request. The client SHOULD perform action(s) to provide valid syntax before retrying the request. + "409": + description: Conflict parsing the task. The client SHOULD provide less tasks in the request. + "415": + description: Unsupported Media Type. The client SHOULD use a supported content type `application/vnd.api+json` or (TODO) `application/json`. + "422": + description: Unprocessable task. The client SHOULD perform action(s) to provide valid attributes or required task-specific attributes before retrying the request. + "503": + description: Service unavailable. Too many tasks pending. The client SHOULD retry later. + requestBody: + description: Creates a new task. + required: true + content: + application/vnd.api+json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + type: + type: string + enum: [addThreadDeviceTask] + description: Type of task + attributes: + type: object + required: + - data + examples: + addThreadDeviceTask: + summary: Add new Thread Device to the Network + description: | + ### Background Logic + - when the task status becomes `active`: + - the *On-Mesh Commissioner* is started, if not already running + - the given *EUI-64*, *Joining Device Credential (pskd)*, and timeout are added to the *Commissioner Joiner Table* + - the task status is updated to `undiscovered`, before first attempts from *Joiner*, and `attempted` waiting for retries from *Joiner* + - when the identified *Joiner* successfully joins the network, the task completes successfully by: + - stopping the *On-Mesh Commissioner*, if it is the last active `addThreadDeviceTask` + - setting the task status to `completed` + - when the task times out without any *Joiner* attempts, the task status is set to `stopped` + - when an error occurs, the task status is set to `failed` + + value: + data: + - type: addThreadDeviceTask + attributes: + eui: "fedcba9876543210" + pskd: "J01NME" + timeout: 900 + + delete: + tags: + - Actions + summary: Remove all tasks from the queue of actions. + responses: + "204": + description: Tasks deleted. + options: + tags: + - Actions + summary: List of allowed operations. + + /api/actions/{actionId}: + get: + tags: + - Actions + summary: Read Actions item + description: | + *Task* item in the *Actions* collection selected by its `id`. + + ## Condition + Requires a *Task* item with the given `id` to exist; + otherwise returns 404 Not Found. + parameters: + - name: actionId + description: ID of the requested Action item (Task). + in: path + required: true + schema: + type: string + format: uuid + - name: Accept + description: Must be set to `application/vnd.api+json` + in: header + required: true + schema: + type: string + example: application/vnd.api+json + /diagnostics: get: tags: diff --git a/src/rest/resource.cpp b/src/rest/resource.cpp index 7b89a10eba7..80619cbbc74 100644 --- a/src/rest/resource.cpp +++ b/src/rest/resource.cpp @@ -37,15 +37,10 @@ #include "common/logging.hpp" #include "common/task_runner.hpp" +#include "rest/extensions/rest_task_queue.hpp" #include "rest/resource.hpp" #include "utils/string_utils.hpp" -extern "C" { -#include -extern task_node_t *task_queue; -extern uint8_t task_queue_len; -} - #define OT_PSKC_MAX_LENGTH 16 #define OT_EXTENDED_PANID_LENGTH 8 @@ -76,6 +71,7 @@ extern uint8_t task_queue_len; #define OT_REST_HTTP_STATUS_408 "408 Request Timeout" #define OT_REST_HTTP_STATUS_409 "409 Conflict" #define OT_REST_HTTP_STATUS_415 "415 Unsupported Media Type" +#define OT_REST_HTTP_STATUS_422 "422 Unprocessable Content" #define OT_REST_HTTP_STATUS_500 "500 Internal Server Error" #define OT_REST_HTTP_STATUS_503 "503 Service Unavailable" @@ -87,6 +83,12 @@ using std::chrono::steady_clock; using std::placeholders::_1; using std::placeholders::_2; +extern "C" { +#include +extern task_node_t *task_queue; +extern uint8_t task_queue_len; +} + namespace otbr { namespace rest { @@ -135,6 +137,9 @@ static std::string GetHttpStatus(HttpStatusCode aErrorCode) case HttpStatusCode::kStatusUnsupportedMediaType: httpStatus = OT_REST_HTTP_STATUS_415; break; + case HttpStatusCode::kStatusUnprocessable: + httpStatus = OT_REST_HTTP_STATUS_422; + break; case HttpStatusCode::kStatusInternalServerError: httpStatus = OT_REST_HTTP_STATUS_500; break; @@ -1008,7 +1013,7 @@ void Resource::ApiActionPostHandler(const Request &aRequest, Response &aResponse std::string responseMessage; std::string errorCode; HttpStatusCode statusCode = HttpStatusCode::kStatusOk; - cJSON *root; + cJSON *root = nullptr; cJSON *dataArray; cJSON *resp_data; cJSON *datum; @@ -1021,27 +1026,28 @@ void Resource::ApiActionPostHandler(const Request &aRequest, Response &aResponse statusCode = HttpStatusCode::kStatusUnsupportedMediaType); root = cJSON_Parse(aRequest.GetBody().c_str()); - VerifyOrExit(root != NULL, statusCode = HttpStatusCode::kStatusBadRequest); + VerifyOrExit(root != nullptr, statusCode = HttpStatusCode::kStatusBadRequest); // perform general validation before we attempt to // perform any task specific validation dataArray = cJSON_GetObjectItemCaseSensitive(root, "data"); - VerifyOrExit((dataArray != NULL) && cJSON_IsArray(dataArray), statusCode = HttpStatusCode::kStatusConflict); + VerifyOrExit((dataArray != nullptr) && cJSON_IsArray(dataArray), statusCode = HttpStatusCode::kStatusUnprocessable); // validate the form and arguments of all tasks // before we attempt to perform processing on any of the tasks. for (int idx = 0; idx < cJSON_GetArraySize(dataArray); idx++) { // Require all items in the list to be valid Task items with all required attributes; - // otherwise rejects whole list and returns 409 Conflict. + // otherwise rejects whole list and returns 422 Unprocessable. // Unimplemented tasks counted as failed / invalid tasks VerifyOrExit(ACTIONS_TASK_VALID == validate_task(cJSON_GetArrayItem(dataArray, idx)), - statusCode = HttpStatusCode::kStatusConflict); + statusCode = HttpStatusCode::kStatusUnprocessable); } // Check queueing all tasks does not exceed the max number of tasks we can have queued + VerifyOrExit((TASK_QUEUE_MAX > cJSON_GetArraySize(dataArray)), statusCode = HttpStatusCode::kStatusConflict); VerifyOrExit((TASK_QUEUE_MAX - task_queue_len + can_remove_task_max()) > cJSON_GetArraySize(dataArray), - statusCode = HttpStatusCode::kStatusConflict); + statusCode = HttpStatusCode::kStatusServiceUnavailable); // Queue the tasks and prepare response data resp_data = cJSON_CreateArray(); @@ -1079,12 +1085,12 @@ void Resource::ApiActionPostHandler(const Request &aRequest, Response &aResponse // Clear the 'root' JSON object and release its memory (this should also delete 'data') cJSON_Delete(root); - root = NULL; + root = nullptr; exit: if (statusCode != HttpStatusCode::kStatusOk) { - if (root != NULL) + if (root != nullptr) { cJSON_Delete(root); } @@ -1112,6 +1118,9 @@ void Resource::ApiActionGetHandler(const Request &aRequest, Response &aResponse) // and another attempt after 2s ApiActionRepeatedTaskRunner(2000); + resp = cJSON_CreateObject(); + task_node = task_queue; + if (aRequest.GetHeaderValue(OT_REST_ACCEPT_HEADER).compare(OT_REST_CONTENT_TYPE_JSONAPI) == 0) { aResponse.SetContentType(OT_REST_CONTENT_TYPE_JSONAPI); @@ -1119,7 +1128,6 @@ void Resource::ApiActionGetHandler(const Request &aRequest, Response &aResponse) if (!itemId.empty()) { // return the item - task_node = task_queue; while (std::string(task_node->id_str) != itemId) { VerifyOrExit(task_node->next != nullptr, statusCode = HttpStatusCode::kStatusResourceNotFound); @@ -1128,18 +1136,14 @@ void Resource::ApiActionGetHandler(const Request &aRequest, Response &aResponse) } evaluate_task(task_node); - resp = cJSON_CreateObject(); cJSON_AddItemToObject(resp, "data", task_to_json(task_node)); resp_body = std::string(cJSON_PrintUnformatted(resp)); } else { // return all items - task_node = task_queue; - resp = cJSON_CreateObject(); - resp_data = cJSON_CreateArray(); - while (task_node != NULL) + while (task_node != nullptr) { evaluate_task(task_node); cJSON_AddItemToObject(resp_data, "data", task_to_json(task_node)); diff --git a/src/rest/rest_web_server.cpp b/src/rest/rest_web_server.cpp index 92aad2d6581..e23d6ece97d 100644 --- a/src/rest/rest_web_server.cpp +++ b/src/rest/rest_web_server.cpp @@ -81,13 +81,6 @@ void RestWebServer::Init(void) mInstance = threadHelper->GetInstance(); if (mInstance != NULL) { - // Initialize the mutex and creates a thread to process the 'api/actions' - // Pass the openthread instance that can be used to call openthread apis. - - // removed 'task' (pthread) in the sense of a task for multithreading and do not create such - // parallel task objects that consequently require complicated lock and semaphore mechanisms. - // TODO: stick with term 'action' for an activity that requires indirect response. - // Initialize the instance pointer rest_task_queue_task_init(mInstance); } diff --git a/src/rest/types.hpp b/src/rest/types.hpp index fb74dca86ce..b4e51d83f06 100644 --- a/src/rest/types.hpp +++ b/src/rest/types.hpp @@ -79,6 +79,7 @@ enum class HttpStatusCode : std::uint16_t kStatusRequestTimeout = 408, kStatusConflict = 409, kStatusUnsupportedMediaType = 415, + kStatusUnprocessable = 422, kStatusInternalServerError = 500, kStatusServiceUnavailable = 503, }; diff --git a/tests/restjsonapi/install_bruno_cli b/tests/restjsonapi/install_bruno_cli index 96cbfdb3c10..a8ef2e23891 100755 --- a/tests/restjsonapi/install_bruno_cli +++ b/tests/restjsonapi/install_bruno_cli @@ -31,8 +31,9 @@ # installs fnm (Fast Node Manager) curl -fsSL https://fnm.vercel.app/install | bash -# activate fnm +# shellcheck source=/dev/null source ~/.bashrc +# activate fnm # download and install Node.js fnm use --install-if-missing 20 # verifies the right Node.js version is in the environment