Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net: lib: aws_fota: Add single url option to job parser #11938

Merged
merged 1 commit into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
fnawratil marked this conversation as resolved.
Show resolved Hide resolved
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,
fnawratil marked this conversation as resolved.
Show resolved Hide resolved
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 =
fnawratil marked this conversation as resolved.
Show resolved Hide resolved
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];
fnawratil marked this conversation as resolved.
Show resolved Hide resolved
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
Loading