diff --git a/doc/nrf/libraries/networking/aws_fota.rst b/doc/nrf/libraries/networking/aws_fota.rst index b44de922f4c4..8a02f7ce1812 100644 --- a/doc/nrf/libraries/networking/aws_fota.rst +++ b/doc/nrf/libraries/networking/aws_fota.rst @@ -82,6 +82,7 @@ Creating a FOTA job } } + To use a single URL, such as when using presigned AWS S3 URLs, see :ref:`aws_iot_jobs`. See `AWS IoT Developer Guide: Jobs`_ for more information about AWS jobs. #. In the `AWS S3 console`_ Select the bucket, click :guilabel:`Upload`, and upload your job document. You must now have two files in your bucket, the uploaded image and the job document. @@ -142,6 +143,8 @@ The following sequence diagram shows how a firmware over-the-air update is imple * The other device has valid (but different) certificates that use the same AWS IoT policy as the original device. * The other device is subscribed to the same MQTT topic as the original device. +.. _aws_iot_jobs: + AWS IoT jobs ============ @@ -161,12 +164,26 @@ The implementation uses a job document like the following (where *bucket_name* i } } -The current implementation uses information from the ``host`` and ``path`` fields only. +Alternatively, to use a single URL, a document like the following can be used: + +.. parsed-literal:: + :class: highlight + + { + "operation": "app_fw_update", + "fwversion": "v1.0.2", + "size": 181124, + "location": { + "url": "*url*" + } + } + +For information on how to use presigned AWS S3 URLs, refer to `AWS IoT Developer Guide: Managing Jobs`_. Limitations *********** -* Currently, the library uses HTTP for downloading the firmware. +* The current implementation ignores the value specified in the ``protocol`` field and uses HTTP by default. To use HTTPS instead, apply the changes described in :ref:`the HTTPS section of the download client documentation ` to the :ref:`lib_fota_download` library. * The library requires a Content-Range header to be present in the HTTP response from the server. This limitation is inherited from the :ref:`lib_download_client` library. diff --git a/doc/nrf/links.txt b/doc/nrf/links.txt index 00b429c261f4..d1915a436ebd 100644 --- a/doc/nrf/links.txt +++ b/doc/nrf/links.txt @@ -821,6 +821,7 @@ .. _`AWS IoT Developer Guide: Security Best Practices`: https://docs.aws.amazon.com/iot/latest/developerguide/security-best-practices.html .. _`AWS IoT jobs`: .. _`AWS IoT Developer Guide: Jobs`: https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html +.. _`AWS IoT Developer Guide: Managing Jobs`: https://docs.aws.amazon.com/iot/latest/developerguide/create-manage-jobs.html .. _`AWS Simple Storage Service (S3)`: https://docs.aws.amazon.com/s3/index.html .. _`AWS S3 Developer Guide: Using Bucket Policies and User Policies`: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-iam-policies.html diff --git a/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst b/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst index 69cb43ce3fd1..81a6ea1578a3 100644 --- a/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst +++ b/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst @@ -557,6 +557,11 @@ Libraries for networking * :ref:`lib_aws_fota` library: + * Added: + + * Support for a single ``url`` field in job documents. + Previously the host name and path of the download URL could only be specified separately. + * Updated: * The :kconfig:option:`CONFIG_AWS_FOTA_HOSTNAME_MAX_LEN` Kconfig option has been replaced by the :kconfig:option:`CONFIG_DOWNLOAD_CLIENT_MAX_HOSTNAME_SIZE` Kconfig option. diff --git a/subsys/net/lib/aws_fota/Kconfig b/subsys/net/lib/aws_fota/Kconfig index 0171758798c9..252485fc29c7 100644 --- a/subsys/net/lib/aws_fota/Kconfig +++ b/subsys/net/lib/aws_fota/Kconfig @@ -7,6 +7,7 @@ menuconfig AWS_FOTA bool "AWS Jobs FOTA library" select AWS_JOBS select CJSON_LIB + select HTTP_PARSER_URL depends on FOTA_DOWNLOAD depends on !BOARD_QEMU_X86 default y if AWS_IOT 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 880486c8c07a..3e44cb3cf128 100644 --- a/subsys/net/lib/aws_fota/include/aws_fota_json.h +++ b/subsys/net/lib/aws_fota/include/aws_fota_json.h @@ -48,8 +48,10 @@ extern "C" { * JobExecution data type. * @param[out] hostname_buf Output buffer for the "host" field from the Job * Document + * @param[in] hostname_buf_size Size of the output buffer for the "host" field * @param[out] file_path_buf Output buffer for the "file" field from the Job * Document + * @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 @@ -60,7 +62,9 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, uint32_t payload_len, char *job_id_buf, char *hostname_buf, + size_t hostname_buf_size, char *file_path_buf, + size_t file_path_buf_size, int *version_number); /** diff --git a/subsys/net/lib/aws_fota/src/aws_fota.c b/subsys/net/lib/aws_fota/src/aws_fota.c index 81d489685122..21d37142c8f8 100644 --- a/subsys/net/lib/aws_fota/src/aws_fota.c +++ b/subsys/net/lib/aws_fota/src/aws_fota.c @@ -243,8 +243,9 @@ static int get_job_execution(struct mqtt_client *const client, /* Check if message received is a job. */ err = aws_fota_parse_DescribeJobExecution_rsp(payload_buf, payload_len, - job_id_incoming, hostname, - file_path, + job_id_incoming, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &execution_version_number); if (err < 0) { 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 2b28f4747681..396fe18d3913 100644 --- a/subsys/net/lib/aws_fota/src/aws_fota_json.c +++ b/subsys/net/lib/aws_fota/src/aws_fota_json.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "aws_fota_json.h" @@ -60,7 +61,9 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, uint32_t payload_len, char *job_id_buf, char *hostname_buf, + size_t hostname_buf_size, char *file_path_buf, + size_t file_path_buf_size, int *execution_version_number) { if (job_document == NULL @@ -115,13 +118,38 @@ int aws_fota_parse_DescribeJobExecution_rsp(const char *job_document, cJSON *hostname = cJSON_GetObjectItemCaseSensitive(location, "host"); cJSON *path = cJSON_GetObjectItemCaseSensitive(location, "path"); - - if ((cJSON_GetStringValue(hostname) != NULL) + cJSON *url = cJSON_GetObjectItemCaseSensitive(location, "url"); + + if (cJSON_GetStringValue(url) != NULL) { + struct http_parser_url u; + + 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. + * 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); + 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, - CONFIG_DOWNLOAD_CLIENT_MAX_HOSTNAME_SIZE); - strncpy_nullterm(file_path_buf, path->valuestring, - CONFIG_DOWNLOAD_CLIENT_MAX_FILENAME_SIZE); + 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; diff --git a/tests/subsys/net/lib/aws_fota/aws_fota_json/CMakeLists.txt b/tests/subsys/net/lib/aws_fota/aws_fota_json/CMakeLists.txt index 02dc27eb243a..04f281620220 100644 --- a/tests/subsys/net/lib/aws_fota/aws_fota_json/CMakeLists.txt +++ b/tests/subsys/net/lib/aws_fota/aws_fota_json/CMakeLists.txt @@ -17,11 +17,13 @@ target_sources(app PRIVATE ${app_sources}) target_sources(app PRIVATE ${ZEPHYR_NRF_MODULE_DIR}/subsys/net/lib/aws_fota/src/aws_fota_json.c + ${ZEPHYR_BASE}/subsys/net/lib/http/http_parser_url.c ) target_include_directories(app PRIVATE ${ZEPHYR_NRF_MODULE_DIR}/subsys/net/lib/aws_fota/include/ + ${ZEPHYR_BASE}/include/zephyr/net/http/ ) # Do this in a non-standard way as the Kconfig options of "aws_jobs/Kconfig" 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 3bf9714b44dc..2a5673dc6224 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 @@ -30,8 +30,51 @@ void test_parse_job_execution(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), + &version_number); + TEST_ASSERT_EQUAL(1, 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); + TEST_ASSERT_EQUAL_STRING(expected_file_path, file_path); +} + +void test_parse_job_execution_single_url(void) +{ + int ret; + int version_number; + int expected_version_number = 1; + char expected_job_id[] = "9b5caac6-3e8a-45dd-9273-c1b995762f4a"; + char expected_hostname[] = "fota-update-bucket.s3.eu-central-1.amazonaws.com"; + char expected_file_path[] = "update.bin?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=" + "AKIAWXEL53DXIU7W72AE%2F20190606%2Feu-central-1%2Fs3%2Faws4_request" + "&X-Amz-Date=20190606T081505Z&X-Amz-Expires=604800&X-Amz-Signature=" + "913e00b97efe5565a901df4ff0b87e4878a406941d711f59d45915035989adcc" + "&X-Amz-SignedHeaders=host"; + char encoded[] = "{\"timestamp\":1559808907,\"execution\":{\"jobId\":" + "\"9b5caac6-3e8a-45dd-9273-c1b995762f4a\",\"status\":\"QUEUED\"," + "\"queuedAt\":1559808906,\"lastUpdatedAt\":1559808906,\"versionNumber\":1," + "\"executionNumber\":1,\"jobDocument\":{\"operation\":\"app_fw_update\"," + "\"fwversion\":\"2\",\"size\":181124,\"location\":{\"protocol\":\"https:\"," + "\"url\":\"https://fota-update-bucket.s3.eu-central-1.amazonaws.com/" + "update.bin?X-Amz-Algorithm=AWS4-HMAC-SHA256" + "&X-Amz-Credential=AKIAWXEL53DXIU7W72AE%2F20190606%2Feu" + "-central-1%2Fs3%2Faws4_request&X-Amz-Date=20190606T081505Z" + "&X-Amz-Expires=604800" + "&X-Amz-Signature=" + "913e00b97efe5565a901df4ff0b87e4878a406941d711f59d45915035989adcc" + "&X-Amz-SignedHeaders=host\"}}}}"; + char job_id[100]; + char hostname[100]; + char file_path[1000]; + + ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, + sizeof(encoded) - 1, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(1, ret); TEST_ASSERT_EQUAL_STRING(expected_job_id, job_id); @@ -51,8 +94,9 @@ void test_parse_malformed_job_execution(void) ret = aws_fota_parse_DescribeJobExecution_rsp(malformed, sizeof(malformed) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(-ENODATA, ret); } @@ -69,8 +113,9 @@ void test_parse_job_execution_missing_host_field(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(-ENODATA, ret); } @@ -86,8 +131,9 @@ void test_parse_job_execution_missing_path_field(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(-ENODATA, ret); } @@ -103,8 +149,9 @@ void test_parse_job_execution_missing_job_id_field(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(-ENODATA, ret); } @@ -120,8 +167,9 @@ void test_parse_job_execution_missing_location_obj(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, - job_id, hostname, - file_path, + job_id, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(-ENODATA, ret); } @@ -155,8 +203,8 @@ void test_timestamp_only(void) ret = aws_fota_parse_DescribeJobExecution_rsp(encoded, sizeof(encoded) - 1, job_id, - hostname, - file_path, + hostname, sizeof(hostname), + file_path, sizeof(file_path), &version_number); TEST_ASSERT_EQUAL(0, ret); }