From e9e21f6a785c49c96fac101f0f8c0aabe19a5b94 Mon Sep 17 00:00:00 2001 From: Fabian Nawratil Date: Wed, 2 Aug 2023 16:37:25 +0200 Subject: [PATCH] net: lib: aws_fota: Add single url support to job parser Previously the AWS FOTA job parser only supported passing firmware update URLs using split hostname and path fields. Now a combined URL field is supported alternatively. This is required for presigned AWS S3 URLs. Additionally the maximum lengths for the host and file path are now passed from the caller to the job parser. Co-authored-by: Joshua Crawford Signed-off-by: Fabian Nawratil --- doc/nrf/libraries/networking/aws_fota.rst | 21 ++++- doc/nrf/links.txt | 1 + .../releases/release-notes-changelog.rst | 5 ++ subsys/net/lib/aws_fota/Kconfig | 1 + .../net/lib/aws_fota/include/aws_fota_json.h | 4 + subsys/net/lib/aws_fota/src/aws_fota.c | 5 +- subsys/net/lib/aws_fota/src/aws_fota_json.c | 40 ++++++++-- .../lib/aws_fota/aws_fota_json/CMakeLists.txt | 2 + .../net/lib/aws_fota/aws_fota_json/src/main.c | 76 +++++++++++++++---- 9 files changed, 131 insertions(+), 24 deletions(-) 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); }