Skip to content

Commit

Permalink
net: lib: aws_fota: Add single url support to job parser
Browse files Browse the repository at this point in the history
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 <joshua.crawford@levno.com>
Signed-off-by: Fabian Nawratil <fabian.nawratil@nordicsemi.no>
  • Loading branch information
fnawratil and Crzyrndm committed Aug 7, 2023
1 parent d02e160 commit e9e21f6
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 24 deletions.
21 changes: 19 additions & 2 deletions doc/nrf/libraries/networking/aws_fota.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
============

Expand All @@ -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 <download_client_https>` 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.
Expand Down
1 change: 1 addition & 0 deletions doc/nrf/links.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions subsys/net/lib/aws_fota/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions subsys/net/lib/aws_fota/include/aws_fota_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

/**
Expand Down
5 changes: 3 additions & 2 deletions subsys/net/lib/aws_fota/src/aws_fota.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
40 changes: 34 additions & 6 deletions subsys/net/lib/aws_fota/src/aws_fota_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <string.h>
#include <cJSON.h>
#include <zephyr/sys/util.h>
#include <zephyr/net/http/parser_url.h>
#include <net/aws_jobs.h>

#include "aws_fota_json.h"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions tests/subsys/net/lib/aws_fota/aws_fota_json/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
76 changes: 62 additions & 14 deletions tests/subsys/net/lib/aws_fota/aws_fota_json/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit e9e21f6

Please sign in to comment.