diff --git a/subsys/net/lib/aws_fota/include/aws_fota_json.h b/subsys/net/lib/aws_fota/include/aws_fota_json.h index 3e44cb3cf128..7ccf5615ddc6 100644 --- a/subsys/net/lib/aws_fota/include/aws_fota_json.h +++ b/subsys/net/lib/aws_fota/include/aws_fota_json.h @@ -37,6 +37,16 @@ extern "C" { */ #define EXECUTION_OBJ_DECODED_BIT 2 +/** @brief AWS FOTA JSON parse result. */ +enum aws_fota_json_result { + AWS_FOTA_JSON_RES_SKIPPED = 1, /*!< Not a FOTA job document, skipped */ + AWS_FOTA_JSON_RES_SUCCESS = 0, /*!< Job document parsed successfully */ + AWS_FOTA_JSON_RES_INVALID_PARAMS = -1, /*!< Input parameters invalid */ + AWS_FOTA_JSON_RES_INVALID_JOB = -2, /*!< Job document invalid, could not get job id */ + AWS_FOTA_JSON_RES_INVALID_DOCUMENT = -3,/*!< FOTA update data invalid */ + AWS_FOTA_JSON_RES_URL_TOO_LONG = -4, /*!< Parts of URL too large for buffer */ +}; + /** * @brief Parse a given AWS IoT DescribeJobExecution response JSON object. * More information on this object can be found at https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-describejobexecution @@ -54,9 +64,7 @@ extern "C" { * @param[in] file_path_buf_size Size of the output buffer for the "file" field * @param[out] version_number Version number from the Job Execution data type. * - * @return 0 if the Job Execution object is empty, 1 if Job Execution object was - * correctly decoded, otherwise a negative error code is returned - * identicating reason of failure. + * @return aws_fota_json_result specifying the result of the parse operation **/ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, uint32_t payload_len, diff --git a/subsys/net/lib/aws_fota/src/aws_fota.c b/subsys/net/lib/aws_fota/src/aws_fota.c index 21d37142c8f8..480f7452d263 100644 --- a/subsys/net/lib/aws_fota/src/aws_fota.c +++ b/subsys/net/lib/aws_fota/src/aws_fota.c @@ -23,6 +23,7 @@ static enum internal_state { STATE_INIT, STATE_DOWNLOADING, STATE_DOWNLOAD_COMPLETE, + STATE_ERROR, } internal_state = STATE_UNINIT; /* Enum used when parsing AWS jobs topics messages are received on. */ @@ -78,6 +79,8 @@ static char *state2str(enum internal_state state) return "STATE_DOWNLOADING"; case STATE_DOWNLOAD_COMPLETE: return "STATE_DOWNLOAD_COMPLETE"; + case STATE_ERROR: + return "STATE_ERROR"; default: return "Unknown"; } @@ -95,13 +98,18 @@ static void internal_state_set(enum internal_state new_state) internal_state = new_state; } +static void set_current_job_id(uint8_t *job_id) +{ + strncpy(job_id_handling, job_id, sizeof(job_id_handling)); + job_id_handling[sizeof(job_id_handling) - 1] = '\0'; +} + static void reset_library(void) { internal_state_set(STATE_INIT); execution_status = AWS_JOBS_QUEUED; download_progress = 0; - strncpy(job_id_handling, AWS_JOB_ID_DEFAULT, sizeof(job_id_handling)); - job_id_handling[sizeof(job_id_handling) - 1] = '\0'; + set_current_job_id(AWS_JOB_ID_DEFAULT); LOG_DBG("Library reset"); } @@ -142,8 +150,7 @@ static enum jobs_topic topic_type_get(const char *incoming_topic, size_t topic_l * * @return 0 If successful otherwise a negative error code is returned. */ -static int get_published_payload(struct mqtt_client *client, uint8_t *write_buf, - size_t length) +static int get_published_payload(struct mqtt_client *client, uint8_t *write_buf, size_t length) { uint8_t *buf = write_buf; uint8_t *end = buf + length; @@ -208,6 +215,25 @@ static int update_job_execution(struct mqtt_client *const client, return 0; } +/** + * @brief Update the job document of the current job to AWS_JOBS_FAILED and move to ERROR state. + * + * @param client Connected MQTT client instance + * @return 0 If successful otherwise a negative error code is returned. + */ +static int set_current_job_failed(struct mqtt_client *const client) +{ + struct aws_fota_event aws_fota_evt; + + aws_fota_evt.id = AWS_FOTA_EVT_ERROR; + callback(&aws_fota_evt); + internal_state_set(STATE_ERROR); + return update_job_execution(client, + job_id_handling, + sizeof(job_id_handling), + AWS_JOBS_FAILED, ""); +} + /** * @brief Parsing an AWS IoT Job Execution response received on $next/get MQTT * topic or notify-next. If it is a valid response the program state is @@ -220,8 +246,7 @@ static int update_job_execution(struct mqtt_client *const client, * * @return 0 If successful otherwise a negative error code is returned. */ -static int get_job_execution(struct mqtt_client *const client, - uint32_t payload_len) +static int parse_job_execution(struct mqtt_client *const client, uint32_t payload_len) { int err; int execution_version_number_prev = execution_version_number; @@ -248,13 +273,16 @@ static int get_job_execution(struct mqtt_client *const client, file_path, sizeof(file_path), &execution_version_number); - if (err < 0) { - LOG_ERR("Error when parsing the json: %d", err); - goto cleanup; - } else if (err == 0) { + if (err == AWS_FOTA_JSON_RES_SKIPPED) { LOG_DBG("Got only one field"); LOG_DBG("No queued jobs for this device"); return 0; + } else if (err < 0 + && err != AWS_FOTA_JSON_RES_INVALID_DOCUMENT + && err != AWS_FOTA_JSON_RES_URL_TOO_LONG) { + LOG_ERR("Error when parsing the json: %d", err); + err = -ENODATA; + goto cleanup; } /* Check if the incoming job is already being handled. */ @@ -262,26 +290,35 @@ static int get_job_execution(struct mqtt_client *const client, LOG_WRN("Job already being handled, ignore message"); err = 0; goto cleanup; - } else { - strncpy(job_id_handling, job_id_incoming, sizeof(job_id_handling)); - job_id_handling[sizeof(job_id_handling) - 1] = '\0'; } + set_current_job_id(job_id_incoming); + + /* Check if the update data is valid */ + if (err == AWS_FOTA_JSON_RES_INVALID_DOCUMENT) { + LOG_ERR("Invalid FOTA update document: %d", err); + return set_current_job_failed(client); + } else if (err == AWS_FOTA_JSON_RES_URL_TOO_LONG) { + LOG_ERR("URL elements too long for buffer: %d", err); + return set_current_job_failed(client); + } + + /* Valid update */ LOG_DBG("Job ID: %s", (char *)job_id_handling); LOG_DBG("hostname: %s", (char *)hostname); LOG_DBG("file_path %s", (char *)file_path); LOG_DBG("execution_version_number: %d ", execution_version_number); - /* Subscribe to update topic to receive feedback on whether an - * update is accepted or not. - */ - err = aws_jobs_subscribe_topic_update(client, job_id_handling, update_topic); + err = update_job_execution(client, + job_id_handling, + sizeof(job_id_handling), + AWS_JOBS_IN_PROGRESS, + ""); if (err) { - LOG_ERR("Error when subscribing job_id_update: %d", err); - goto cleanup; + LOG_ERR("update_job_execution failed, error: %d", err); + return err; } - LOG_DBG("Subscribed to FOTA update topic %s", (char *)update_topic); return 0; cleanup: @@ -298,8 +335,7 @@ static int get_job_execution(struct mqtt_client *const client, * * @return 0 If successful otherwise a negative error code is returned. */ -static int job_update_accepted(struct mqtt_client *const client, - uint32_t payload_len) +static int job_update_accepted(struct mqtt_client *const client, uint32_t payload_len) { int err; int sec_tag = -1; @@ -334,12 +370,7 @@ static int job_update_accepted(struct mqtt_client *const client, err = fota_download_start(hostname, file_path, sec_tag, 0, 0); if (err) { LOG_ERR("Error (%d) when trying to start firmware download", err); - aws_fota_evt.id = AWS_FOTA_EVT_ERROR; - callback(&aws_fota_evt); - return update_job_execution(client, - job_id_handling, - sizeof(job_id_handling), - AWS_JOBS_FAILED, ""); + return set_current_job_failed(client); } internal_state_set(STATE_DOWNLOADING); @@ -361,6 +392,12 @@ static int job_update_accepted(struct mqtt_client *const client, reset_library(); } break; + case AWS_JOBS_FAILED: { + LOG_DBG("Job document was updated with status FAILED"); + reset_library(); + (void)aws_jobs_get_job_execution(client_internal, "$next", get_topic); + } + break; default: LOG_ERR("Invalid execution status"); return -EINVAL; @@ -378,8 +415,7 @@ static int job_update_accepted(struct mqtt_client *const client, * * @return A negative error code is returned. */ -static int job_update_rejected(struct mqtt_client *const client, - uint32_t payload_len) +static int job_update_rejected(struct mqtt_client *const client, uint32_t payload_len) { struct aws_fota_event aws_fota_evt = { .id = AWS_FOTA_EVT_ERROR }; LOG_ERR("Job document update was rejected"); @@ -449,10 +485,11 @@ static int on_publish_evt(struct mqtt_client *const client, } LOG_DBG("Checking for an available job"); - return get_job_execution(client, payload_len); + return parse_job_execution(client, payload_len); case TOPIC_UPDATE_ACCEPTED: if (internal_state != STATE_INIT && - internal_state != STATE_DOWNLOAD_COMPLETE) { + internal_state != STATE_DOWNLOAD_COMPLETE && + internal_state != STATE_ERROR) { goto read_payload; } @@ -464,7 +501,8 @@ static int on_publish_evt(struct mqtt_client *const client, return job_update_accepted(client, payload_len); case TOPIC_UPDATE_REJECTED: if (internal_state != STATE_INIT && - internal_state != STATE_DOWNLOAD_COMPLETE) { + internal_state != STATE_DOWNLOAD_COMPLETE && + internal_state != STATE_ERROR) { goto read_payload; } @@ -500,6 +538,7 @@ static int on_publish_evt(struct mqtt_client *const client, static int on_connack_evt(struct mqtt_client *const client) { int err; + enum execution_status status; switch (internal_state) { case STATE_INIT: @@ -514,24 +553,31 @@ static int on_connack_evt(struct mqtt_client *const client) LOG_ERR("Unable to subscribe to jobs/$next/get"); return err; } + + status = AWS_JOBS_IN_PROGRESS; break; - case STATE_DOWNLOADING: - /* Fall through */ case STATE_DOWNLOAD_COMPLETE: - if (strncmp(job_id_handling, AWS_JOB_ID_DEFAULT, sizeof(job_id_handling)) == 0) { - return -ECANCELED; - } - - err = aws_jobs_subscribe_topic_update(client, job_id_handling, update_topic); - if (err) { - LOG_ERR("Error when subscribing job_id_update: %d", err); - return err; - } - - LOG_DBG("Subscribed to FOTA update topic %s", (char *)update_topic); + status = AWS_JOBS_SUCCEEDED; break; - default: + case STATE_ERROR: + status = AWS_JOBS_FAILED; break; + default: + return 0; + } + + + /* If we have just reconnected, we might already be handling a job. + * Check if this is the case and update the job status accordingly. + */ + if (strncmp(job_id_handling, AWS_JOB_ID_DEFAULT, sizeof(job_id_handling)) == 0) { + return 0; + } + + err = update_job_execution(client, job_id_handling, sizeof(job_id_handling), status, ""); + if (err) { + LOG_ERR("update_job_execution failed, error: %d", err); + return err; } return 0; @@ -560,34 +606,7 @@ static int on_suback_evt(struct mqtt_client *const client, uint16_t message_id) break; case SUBSCRIBE_JOB_ID_UPDATE: LOG_DBG("Subscribed to job ID update accepted/rejected topics"); - - enum execution_status status; - - switch (internal_state) { - case STATE_INIT: - status = AWS_JOBS_IN_PROGRESS; - break; - case STATE_DOWNLOAD_COMPLETE: - status = AWS_JOBS_SUCCEEDED; break; - case STATE_DOWNLOADING: - return 0; - default: - LOG_WRN("Invalid state"); - return -ECANCELED; - } - - err = update_job_execution(client, - job_id_handling, - sizeof(job_id_handling), - status, - ""); - if (err) { - LOG_ERR("update_job_execution failed, error: %d", err); - return err; - } - - break; default: /* Message ID not related to AWS FOTA. */ break; @@ -596,8 +615,7 @@ static int on_suback_evt(struct mqtt_client *const client, uint16_t message_id) return 0; } -int aws_fota_mqtt_evt_handler(struct mqtt_client *const client, - const struct mqtt_evt *evt) +int aws_fota_mqtt_evt_handler(struct mqtt_client *const client, const struct mqtt_evt *evt) { int err; @@ -739,21 +757,7 @@ static void http_fota_handler(const struct fota_download_evt *evt) case FOTA_DOWNLOAD_EVT_ERROR: LOG_ERR("FOTA_DOWNLOAD_EVT_ERROR"); - (void)update_job_execution(client_internal, - job_id_handling, - sizeof(job_id_handling), - AWS_JOBS_FAILED, - ""); - - aws_fota_evt.id = AWS_FOTA_EVT_ERROR; - - callback(&aws_fota_evt); - reset_library(); - - /* If the FOTA download fails it might be due to the image being deleted. - * Try to get the next job if any exist. - */ - (void)aws_jobs_get_job_execution(client_internal, "$next", get_topic); + (void) set_current_job_failed(client_internal); break; case FOTA_DOWNLOAD_EVT_PROGRESS: @@ -772,8 +776,7 @@ static void http_fota_handler(const struct fota_download_evt *evt) } } -int aws_fota_init(struct mqtt_client *const client, - aws_fota_callback_t evt_handler) +int aws_fota_init(struct mqtt_client *const client, aws_fota_callback_t evt_handler) { int err; diff --git a/subsys/net/lib/aws_fota/src/aws_fota_json.c b/subsys/net/lib/aws_fota/src/aws_fota_json.c index 396fe18d3913..0020ab94fca9 100644 --- a/subsys/net/lib/aws_fota/src/aws_fota_json.c +++ b/subsys/net/lib/aws_fota/src/aws_fota_json.c @@ -35,14 +35,12 @@ int aws_fota_parse_UpdateJobExecution_rsp(const char *update_rsp_document, int ret; cJSON *update_response = cJSON_Parse(update_rsp_document); - if (update_response == NULL) { ret = -ENODATA; goto cleanup; } - cJSON *status = cJSON_GetObjectItemCaseSensitive(update_response, - "status"); + cJSON *status = cJSON_GetObjectItemCaseSensitive(update_response, "status"); if (cJSON_IsString(status) && status->valuestring != NULL) { strncpy_nullterm(status_buf, status->valuestring, STATUS_MAX_LEN); @@ -71,7 +69,7 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, || hostname_buf == NULL || file_path_buf == NULL || execution_version_number == NULL) { - return -EINVAL; + return AWS_FOTA_JSON_RES_INVALID_PARAMS; } int ret; @@ -79,40 +77,46 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, cJSON *json_data = cJSON_Parse(job_document); if (json_data == NULL) { - ret = -ENODATA; + ret = AWS_FOTA_JSON_RES_INVALID_JOB; goto cleanup; } - cJSON *execution = cJSON_GetObjectItemCaseSensitive(json_data, - "execution"); + cJSON *execution = cJSON_GetObjectItemCaseSensitive(json_data, "execution"); + if (execution == NULL) { - ret = 0; + ret = AWS_FOTA_JSON_RES_SKIPPED; goto cleanup; } cJSON *job_id = cJSON_GetObjectItemCaseSensitive(execution, "jobId"); if (cJSON_GetStringValue(job_id) != NULL) { - strncpy_nullterm(job_id_buf, job_id->valuestring, - AWS_JOBS_JOB_ID_MAX_LEN); + strncpy_nullterm(job_id_buf, job_id->valuestring, AWS_JOBS_JOB_ID_MAX_LEN); } else { - ret = -ENODATA; + ret = AWS_FOTA_JSON_RES_INVALID_JOB; goto cleanup; } - cJSON *job_data = cJSON_GetObjectItemCaseSensitive(execution, - "jobDocument"); + cJSON *version_number = cJSON_GetObjectItemCaseSensitive(execution, "versionNumber"); + + if (cJSON_IsNumber(version_number)) { + *execution_version_number = version_number->valueint; + } else { + ret = AWS_FOTA_JSON_RES_INVALID_JOB; + goto cleanup; + } + + cJSON *job_data = cJSON_GetObjectItemCaseSensitive(execution, "jobDocument"); if (!cJSON_IsObject(job_data)) { - ret = -ENODATA; + ret = AWS_FOTA_JSON_RES_INVALID_DOCUMENT; goto cleanup; } - cJSON *location = cJSON_GetObjectItemCaseSensitive(job_data, - "location"); + cJSON *location = cJSON_GetObjectItemCaseSensitive(job_data, "location"); if (!cJSON_IsObject(location)) { - ret = -ENODATA; + ret = AWS_FOTA_JSON_RES_INVALID_DOCUMENT; goto cleanup; } @@ -126,46 +130,45 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, http_parser_url_init(&u); http_parser_parse_url(url->valuestring, strlen(url->valuestring), false, &u); - /* Determine size of hostname and clamp to buffer size. + /* Determine size of hostname and path (consisting of path + query). * Length increased by one to get null termination at right spot when copying. */ - uint16_t parsed_host_len = - MIN(hostname_buf_size, u.field_data[UF_HOST].len + 1); + uint16_t parsed_host_len = u.field_data[UF_HOST].len + 1; + uint16_t parsed_file_len = u.field_data[UF_PATH].len + + u.field_data[UF_QUERY].len + 1; + + if (parsed_host_len > hostname_buf_size || parsed_file_len > file_path_buf_size) { + ret = AWS_FOTA_JSON_RES_URL_TOO_LONG; + goto cleanup; + } + + /* Copy slices of the hostname and path (url path + query) to the + * respective buffers. Increase path offset by one to omit initial slash. + */ strncpy_nullterm(hostname_buf, url->valuestring + u.field_data[UF_HOST].off, parsed_host_len); - - /* Determine size of file path, consisting of the path and query parts of the URL. - * Length increased by one for proper null termination and clamped to buffer size. - * Copy starting from the path offset, increased by one to omit the initial slash. - */ - uint16_t parsed_file_len = - MIN(file_path_buf_size, - u.field_data[UF_PATH].len + u.field_data[UF_QUERY].len + 1); strncpy_nullterm(file_path_buf, url->valuestring + u.field_data[UF_PATH].off + 1, parsed_file_len); } else if ((cJSON_GetStringValue(hostname) != NULL) && (cJSON_GetStringValue(path) != NULL)) { - strncpy_nullterm(hostname_buf, hostname->valuestring, hostname_buf_size); - strncpy_nullterm(file_path_buf, path->valuestring, file_path_buf_size); - } else { - ret = -ENODATA; - goto cleanup; - } - cJSON *version_number = cJSON_GetObjectItemCaseSensitive( - execution, "versionNumber"); + if (strlen(hostname->valuestring) >= hostname_buf_size + || strlen(path->valuestring) >= file_path_buf_size) { + ret = AWS_FOTA_JSON_RES_URL_TOO_LONG; + goto cleanup; + } - if (cJSON_IsNumber(version_number)) { - *execution_version_number = version_number->valueint; + strncpy_nullterm(hostname_buf, hostname->valuestring, hostname_buf_size); + strncpy_nullterm(file_path_buf, path->valuestring, file_path_buf_size); } else { - ret = -ENODATA; + ret = AWS_FOTA_JSON_RES_INVALID_DOCUMENT; goto cleanup; } - ret = 1; + ret = AWS_FOTA_JSON_RES_SUCCESS; cleanup: cJSON_Delete(json_data); return ret; diff --git a/tests/subsys/net/lib/aws_fota/aws_fota_json/src/main.c b/tests/subsys/net/lib/aws_fota/aws_fota_json/src/main.c index 2a5673dc6224..765c4b5317ed 100644 --- a/tests/subsys/net/lib/aws_fota/aws_fota_json/src/main.c +++ b/tests/subsys/net/lib/aws_fota/aws_fota_json/src/main.c @@ -34,7 +34,7 @@ void test_parse_job_execution(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(1, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_SUCCESS, ret); TEST_ASSERT_EQUAL_STRING(expected_job_id, job_id); TEST_ASSERT_EQUAL(expected_version_number, version_number); TEST_ASSERT_EQUAL_STRING(expected_hostname, hostname); @@ -76,7 +76,7 @@ void test_parse_job_execution_single_url(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(1, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_SUCCESS, ret); TEST_ASSERT_EQUAL_STRING(expected_job_id, job_id); TEST_ASSERT_EQUAL(expected_version_number, version_number); TEST_ASSERT_EQUAL_STRING(expected_hostname, hostname); @@ -98,7 +98,7 @@ void test_parse_malformed_job_execution(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(-ENODATA, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_INVALID_JOB, ret); } void test_parse_job_execution_missing_host_field(void) @@ -117,7 +117,7 @@ void test_parse_job_execution_missing_host_field(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(-ENODATA, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_INVALID_DOCUMENT, ret); } void test_parse_job_execution_missing_path_field(void) @@ -135,7 +135,7 @@ void test_parse_job_execution_missing_path_field(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(-ENODATA, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_INVALID_DOCUMENT, ret); } void test_parse_job_execution_missing_job_id_field(void) @@ -153,7 +153,7 @@ void test_parse_job_execution_missing_job_id_field(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(-ENODATA, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_INVALID_JOB, ret); } void test_parse_job_execution_missing_location_obj(void) @@ -171,7 +171,7 @@ void test_parse_job_execution_missing_location_obj(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(-ENODATA, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_INVALID_DOCUMENT, ret); } void test_update_job_longer_than_max(void) @@ -206,7 +206,7 @@ void test_timestamp_only(void) hostname, sizeof(hostname), file_path, sizeof(file_path), &version_number); - TEST_ASSERT_EQUAL(0, ret); + TEST_ASSERT_EQUAL(AWS_FOTA_JSON_RES_SKIPPED, ret); } void test_update_job_exec_rsp_minimal(void)