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] [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