From 2d4fe1c41162f46e24c6da1ba060a3d4c1dd62fa Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 4 Nov 2021 09:53:49 -0700 Subject: [PATCH 01/98] Add missing export macro (#205) --- include/aws/mqtt/client.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 06c8636c..9263f263 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -418,6 +418,7 @@ int aws_mqtt_client_connection_connect( * \returns AWS_OP_SUCCESS if the connection has been successfully initiated, * otherwise AWS_OP_ERR and aws_last_error() will be set. */ +AWS_MQTT_API int aws_mqtt_client_connection_reconnect( struct aws_mqtt_client_connection *connection, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, From 60f9a179c1a599cbf217edcccc9dd008c8e9b787 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 10 Nov 2021 12:39:54 -0800 Subject: [PATCH 02/98] Packet payload fixes (#206) * Relax overly strict condition on buffer encode * Allow empty publish payload in decode step --- source/packets.c | 4 +- tests/CMakeLists.txt | 2 + tests/packet_encoding_test.c | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/source/packets.c b/source/packets.c index 47f69457..7af2af26 100644 --- a/source/packets.c +++ b/source/packets.c @@ -20,7 +20,7 @@ static size_t s_sizeof_encoded_buffer(struct aws_byte_cursor *buf) { static int s_encode_buffer(struct aws_byte_buf *buf, const struct aws_byte_cursor cur) { AWS_PRECONDITION(buf); - AWS_PRECONDITION(cur.ptr && cur.len); + AWS_PRECONDITION(aws_byte_cursor_is_valid(&cur)); /* Make sure the buffer isn't too big */ if (cur.len > UINT16_MAX) { @@ -584,7 +584,7 @@ int aws_mqtt_packet_publish_decode(struct aws_byte_cursor *cur, struct aws_mqtt_ /*************************************************************************/ /* Payload */ packet->payload = aws_byte_cursor_advance(cur, payload_size); - if (packet->payload.len == 0) { + if (packet->payload.len != payload_size) { return aws_raise_error(AWS_ERROR_SHORT_BUFFER); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 52d376c7..21e961d7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,10 +15,12 @@ add_test_case(mqtt_packet_suback) add_test_case(mqtt_packet_unsuback) add_test_case(mqtt_packet_connect) add_test_case(mqtt_packet_connect_will) +add_test_case(mqtt_packet_connect_empty_payload_will) add_test_case(mqtt_packet_connect_password) add_test_case(mqtt_packet_connack) add_test_case(mqtt_packet_publish_qos0_dup) add_test_case(mqtt_packet_publish_qos2_retain) +add_test_case(mqtt_packet_publish_empty_payload) add_test_case(mqtt_packet_subscribe) add_test_case(mqtt_packet_unsubscribe) add_test_case(mqtt_packet_pingreq) diff --git a/tests/packet_encoding_test.c b/tests/packet_encoding_test.c index ecc56d3b..ba85ee89 100644 --- a/tests/packet_encoding_test.c +++ b/tests/packet_encoding_test.c @@ -302,6 +302,57 @@ static int s_test_connect_will_init(struct packet_test_fixture *fixture) { } PACKET_TEST_NAME(CONNECT, connect_will, connect, &s_test_connect_will_init, NULL, &s_test_connect_eq) +static uint8_t s_empty_payload[] = ""; +enum { EMPTY_PAYLOAD_LEN = 0 }; + +static int s_test_connect_empty_payload_will_init(struct packet_test_fixture *fixture) { + /* Init packet */ + ASSERT_SUCCESS(aws_mqtt_packet_connect_init( + fixture->in_packet, aws_byte_cursor_from_array(s_client_id, CLIENT_ID_LEN), false, 0)); + ASSERT_SUCCESS(aws_mqtt_packet_connect_add_will( + fixture->in_packet, + aws_byte_cursor_from_array(s_topic_name, TOPIC_NAME_LEN), + AWS_MQTT_QOS_EXACTLY_ONCE, + true /*retain*/, + aws_byte_cursor_from_array(s_empty_payload, EMPTY_PAYLOAD_LEN))); + + /* Init buffer */ + /* clang-format off */ + uint8_t header[] = { + AWS_MQTT_PACKET_CONNECT << 4, /* Packet type */ + 10 + (2 + CLIENT_ID_LEN) + (2 + TOPIC_NAME_LEN) + (2 + EMPTY_PAYLOAD_LEN), /* Remaining length */ + 0, 4, 'M', 'Q', 'T', 'T', /* Protocol name */ + 4, /* Protocol level */ + /* Connect Flags: */ + (1 << 2) /* Will flag, bit 2 */ + | (AWS_MQTT_QOS_EXACTLY_ONCE << 3)/* Will QoS, bits 4-3 */ + | (1 << 5), /* Will Retain, bit 5 */ + + 0, 0, /* Keep alive */ + }; + /* clang-format on */ + + aws_byte_buf_write(&fixture->buffer, header, sizeof(header)); + /* client identifier */ + aws_byte_buf_write_be16(&fixture->buffer, CLIENT_ID_LEN); + aws_byte_buf_write(&fixture->buffer, s_client_id, CLIENT_ID_LEN); + /* will topic */ + aws_byte_buf_write_be16(&fixture->buffer, TOPIC_NAME_LEN); + aws_byte_buf_write(&fixture->buffer, s_topic_name, TOPIC_NAME_LEN); + /* will payload */ + aws_byte_buf_write_be16(&fixture->buffer, EMPTY_PAYLOAD_LEN); + aws_byte_buf_write(&fixture->buffer, s_empty_payload, EMPTY_PAYLOAD_LEN); + + return AWS_OP_SUCCESS; +} +PACKET_TEST_NAME( + CONNECT, + connect_empty_payload_will, + connect, + &s_test_connect_empty_payload_will_init, + NULL, + &s_test_connect_eq) + static int s_test_connect_password_init(struct packet_test_fixture *fixture) { /* Init packet */ ASSERT_SUCCESS(aws_mqtt_packet_connect_init( @@ -440,6 +491,41 @@ static int s_test_publish_qos2_retain_init(struct packet_test_fixture *fixture) return AWS_OP_SUCCESS; } +static int s_test_publish_empty_payload_init(struct packet_test_fixture *fixture) { + + /* Init packet */ + ASSERT_SUCCESS(aws_mqtt_packet_publish_init( + fixture->in_packet, + false /* retain */, + AWS_MQTT_QOS_AT_MOST_ONCE, + true /* dup */, + aws_byte_cursor_from_array(s_topic_name, TOPIC_NAME_LEN), + 0, + aws_byte_cursor_from_array(s_empty_payload, EMPTY_PAYLOAD_LEN))); + + /* Init buffer */ + /* clang-format off */ + aws_byte_buf_write_u8( + &fixture->buffer, + (AWS_MQTT_PACKET_PUBLISH << 4) /* Packet type bits 7-4 */ + | (1 << 3) /* DUP bit 3 */ + | (AWS_MQTT_QOS_AT_MOST_ONCE << 1) /* QoS bits 2-1 */ + | 0 /* RETAIN bit 0 */); + aws_byte_buf_write_u8( + &fixture->buffer, 2 + TOPIC_NAME_LEN + EMPTY_PAYLOAD_LEN); /* Remaining length */ + aws_byte_buf_write_u8( + &fixture->buffer, 0); /* Topic name len byte 1 */ + aws_byte_buf_write_u8( + &fixture->buffer, TOPIC_NAME_LEN); /* Topic name len byte 2 */ + aws_byte_buf_write( + &fixture->buffer, s_topic_name, TOPIC_NAME_LEN); /* Topic name */ + aws_byte_buf_write( + &fixture->buffer, s_empty_payload, EMPTY_PAYLOAD_LEN); /* payload */ + /* clang-format on */ + + return AWS_OP_SUCCESS; +} + static bool s_test_publish_eq(void *a, void *b, size_t size) { (void)size; @@ -452,6 +538,7 @@ static bool s_test_publish_eq(void *a, void *b, size_t size) { } PACKET_TEST_NAME(PUBLISH, publish_qos0_dup, publish, &s_test_publish_qos0_dup_init, NULL, &s_test_publish_eq) PACKET_TEST_NAME(PUBLISH, publish_qos2_retain, publish, &s_test_publish_qos2_retain_init, NULL, &s_test_publish_eq) +PACKET_TEST_NAME(PUBLISH, publish_empty_payload, publish, &s_test_publish_empty_payload_init, NULL, &s_test_publish_eq) /*****************************************************************************/ /* Subscribe */ From 6168e32bf9f745dec40df633b78baa03420b7f83 Mon Sep 17 00:00:00 2001 From: Dengke Tang <815825145@qq.com> Date: Thu, 2 Dec 2021 15:07:55 -0800 Subject: [PATCH 03/98] remove try lock from debug build (#207) --- .github/workflows/ci.yml | 20 +++++++++----------- CMakeLists.txt | 5 +++++ include/aws/mqtt/private/client_impl.h | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c5da58f..c9a3b49e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: us-east-1 - + jobs: linux-compat: runs-on: ubuntu-latest @@ -36,7 +36,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} downstream + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON downstream linux-compiler-compat: runs-on: ubuntu-latest @@ -59,7 +59,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --spec downstream + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON --spec downstream clang-sanitizers: runs-on: ubuntu-latest @@ -71,7 +71,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=clang-11 --cmake-extra=-DENABLE_SANITIZERS=ON --cmake-extra=-DSANITIZERS="${{ matrix.sanitizers }}" + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=clang-11 --cmake-extra=-DENABLE_SANITIZERS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DSANITIZERS="${{ matrix.sanitizers }}" linux-shared-libs: runs-on: ubuntu-latest @@ -80,7 +80,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON windows: runs-on: windows-latest @@ -88,7 +88,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" - python builder.pyz build -p ${{ env.PACKAGE_NAME }} + python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON windows-vc14: runs-on: windows-latest @@ -99,7 +99,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" - python builder.pyz build -p ${{ env.PACKAGE_NAME }} --target windows-${{ matrix.arch }} --compiler msvc-14 + python builder.pyz build -p ${{ env.PACKAGE_NAME }} --target windows-${{ matrix.arch }} --compiler msvc-14 --cmake-extra=-DASSERT_LOCK_HELD=ON windows-shared-libs: runs-on: windows-latest @@ -107,7 +107,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" - python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON + python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON osx: runs-on: macos-latest @@ -116,6 +116,4 @@ jobs: run: | python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" chmod a+x builder - ./builder build -p ${{ env.PACKAGE_NAME }} default-downstream - - + ./builder build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON default-downstream diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d80abb1..45eab337 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,11 @@ if (POLICY CMP0069) cmake_policy(SET CMP0069 NEW) # Enable LTO/IPO if available in the compiler, see AwsCFlags endif() +option(ASSERT_LOCK_HELD "Enable ASSERT_SYNCED_DATA_LOCK_HELD for checking thread issue" OFF) +if (ASSERT_LOCK_HELD) + add_definitions(-DASSERT_LOCK_HELD) +endif() + if (DEFINED CMAKE_PREFIX_PATH) file(TO_CMAKE_PATH "${CMAKE_PREFIX_PATH}" CMAKE_PREFIX_PATH) endif() diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 387f39aa..f7b4be01 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -34,7 +34,7 @@ } \ } while (false) -#if DEBUG_BUILD +#if ASSERT_LOCK_HELD # define ASSERT_SYNCED_DATA_LOCK_HELD(object) \ { \ int cached_error = aws_last_error(); \ From a503e91d35a07675d8e2edb254d7bd075053ed66 Mon Sep 17 00:00:00 2001 From: Dengke Tang <815825145@qq.com> Date: Wed, 2 Feb 2022 14:37:33 -0800 Subject: [PATCH 04/98] not use latest containers for ci (#208) --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/clang-format.yml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9a3b49e..669181fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ env: jobs: linux-compat: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # latest strategy: matrix: image: @@ -39,7 +39,7 @@ jobs: ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON downstream linux-compiler-compat: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # latest strategy: matrix: compiler: @@ -62,7 +62,7 @@ jobs: ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON --spec downstream clang-sanitizers: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # latest strategy: matrix: sanitizers: [",thread", ",address,undefined"] @@ -74,7 +74,7 @@ jobs: ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=clang-11 --cmake-extra=-DENABLE_SANITIZERS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DSANITIZERS="${{ matrix.sanitizers }}" linux-shared-libs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # latest steps: # We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages - name: Build ${{ env.PACKAGE_NAME }} @@ -83,7 +83,7 @@ jobs: ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON windows: - runs-on: windows-latest + runs-on: windows-2022 # latest steps: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | @@ -91,7 +91,7 @@ jobs: python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON windows-vc14: - runs-on: windows-latest + runs-on: windows-2019 # windows-2019 is last env with Visual Studio 2015 (v14.0) strategy: matrix: arch: [x86, x64] @@ -102,7 +102,7 @@ jobs: python builder.pyz build -p ${{ env.PACKAGE_NAME }} --target windows-${{ matrix.arch }} --compiler msvc-14 --cmake-extra=-DASSERT_LOCK_HELD=ON windows-shared-libs: - runs-on: windows-latest + runs-on: windows-2022 # latest steps: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | @@ -110,7 +110,7 @@ jobs: python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON osx: - runs-on: macos-latest + runs-on: macos-11 # latest steps: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index c784771e..cca2eb7c 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -5,12 +5,12 @@ on: [push] jobs: clang-format: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # latest steps: - name: Checkout Sources uses: actions/checkout@v1 - + - name: clang-format lint uses: DoozyX/clang-format-lint-action@v0.3.1 with: From a6b3e05acb90091ab1208b8b2606b38ac2866c67 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 24 Mar 2022 07:17:58 -0700 Subject: [PATCH 05/98] Update to latest builder (#209) * Update to latest builder --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 669181fb..3867206e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ on: - '!main' env: - BUILDER_VERSION: v0.8.27 + BUILDER_VERSION: v0.9.15 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt - LINUX_BASE_IMAGE: ubuntu-16-x64 + LINUX_BASE_IMAGE: ubuntu-18-x64 RUN: ${{ github.run_id }}-${{ github.run_number }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 1d635a18ddb23fc2d6ac4b3751415ca8fe1213bb Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Fri, 15 Apr 2022 11:08:41 -0700 Subject: [PATCH 06/98] Fix it so CI runs on branches with '/' in name (#210) --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3867206e..3b55f8e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,8 @@ name: CI on: push: - branches: - - '*' - - '!main' + branches-ignore: + - 'main' env: BUILDER_VERSION: v0.9.15 From d2545e9118a43a267b96070134e2b77ac6b29658 Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Mon, 2 May 2022 13:26:29 -0700 Subject: [PATCH 07/98] fixed endpoint not being read in arguments (#211) * fixed endpoint not being read in arguments of elastipubsub * Explicitly added endpoint as an option to look for in arguments and modified instructions in help --- bin/elastipubsub/main.c | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/bin/elastipubsub/main.c b/bin/elastipubsub/main.c index 6c32110e..9ebadb36 100644 --- a/bin/elastipubsub/main.c +++ b/bin/elastipubsub/main.c @@ -63,8 +63,8 @@ struct app_ctx { static void s_usage(int exit_code) { - fprintf(stderr, "usage: elastipubsub [options] endpoint\n"); - fprintf(stderr, " endpoint: url to connect to\n"); + fprintf(stderr, "usage: elastipubsub [options] --endpoint\n"); + fprintf(stderr, " --endpoint: url to connect to \n"); fprintf(stderr, "\n Options:\n\n"); fprintf(stderr, " --cacert FILE: path to a CA certficate file.\n"); fprintf(stderr, " --cert FILE: path to a PEM encoded certificate to use with mTLS\n"); @@ -95,14 +95,16 @@ static struct aws_cli_option s_long_options[] = { {"pops", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'p'}, {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, + {"endpoint", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'E'}, /* Per getopt(3) the last element of the array has to be filled with all zeros */ {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, }; static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { + bool uri_found = false; while (true) { int option_index = 0; - int c = aws_cli_getopt_long(argc, argv, "a:c:C:e:f:i:k:l:n:p:v:h", s_long_options, &option_index); + int c = aws_cli_getopt_long(argc, argv, "a:c:C:e:f:i:k:l:n:p:v:h:E", s_long_options, &option_index); if (c == -1) { break; } @@ -158,24 +160,26 @@ static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { case 'h': s_usage(0); break; + case 'E': { + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(aws_cli_positional_arg); + if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { + fprintf( + stderr, + "Failed to parse uri %s with error %s\n", + (char *)uri_cursor.ptr, + aws_error_debug_str(aws_last_error())); + s_usage(1); + } + uri_found = true; + break; + } default: fprintf(stderr, "Unknown option\n"); s_usage(1); } } - if (aws_cli_optind < argc) { - struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(argv[aws_cli_optind++]); - - if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { - fprintf( - stderr, - "Failed to parse uri %s with error %s\n", - (char *)uri_cursor.ptr, - aws_error_debug_str(aws_last_error())); - s_usage(1); - }; - } else { + if (!uri_found) { fprintf(stderr, "A URI for the request must be supplied.\n"); s_usage(1); } From dd09527354ea435812bf4be4bf9b32ba6a41c6a0 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 8 Jun 2022 12:25:41 -0700 Subject: [PATCH 08/98] No resubscribe (#215) * Fix resubscribe segfault when there are no topics Co-authored-by: Octogonapus --- source/client.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/source/client.c b/source/client.c index 14c8f300..215ae565 100644 --- a/source/client.c +++ b/source/client.c @@ -2279,7 +2279,11 @@ static enum aws_mqtt_client_request_state s_resubscribe_send( bool initing_packet = task_arg->subscribe.fixed_header.packet_type == 0; struct aws_io_message *message = NULL; - size_t sub_count = aws_mqtt_topic_tree_get_sub_count(&task_arg->connection->thread_data.subscriptions); + const size_t sub_count = aws_mqtt_topic_tree_get_sub_count(&task_arg->connection->thread_data.subscriptions); + /* Init the topics list even if there are no topics because the s_resubscribe_complete callback will always run. */ + if (aws_array_list_init_dynamic(&task_arg->topics, task_arg->connection->allocator, sub_count, sizeof(void *))) { + goto handle_error; + } if (sub_count == 0) { AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, @@ -2287,9 +2291,6 @@ static enum aws_mqtt_client_request_state s_resubscribe_send( (void *)task_arg->connection); return AWS_MQTT_CLIENT_REQUEST_COMPLETE; } - if (aws_array_list_init_dynamic(&task_arg->topics, task_arg->connection->allocator, sub_count, sizeof(void *))) { - goto handle_error; - } aws_mqtt_topic_tree_iterate(&task_arg->connection->thread_data.subscriptions, s_reconnect_resub_iterator, task_arg); AWS_LOGF_TRACE( @@ -2358,6 +2359,11 @@ static void s_resubscribe_complete( struct subscribe_task_arg *task_arg = userdata; + const size_t list_len = aws_array_list_length(&task_arg->topics); + if (list_len <= 0) { + goto clean_up; + } + struct subscribe_task_topic *topic = NULL; aws_array_list_get_at(&task_arg->topics, &topic, 0); AWS_ASSUME(topic); @@ -2369,7 +2375,6 @@ static void s_resubscribe_complete( packet_id, error_code); - size_t list_len = aws_array_list_length(&task_arg->topics); if (task_arg->on_suback.multi) { /* create a list of aws_mqtt_topic_subscription pointers from topics for the callback */ AWS_VARIABLE_LENGTH_ARRAY(uint8_t, cb_list_buf, list_len * sizeof(void *)); @@ -2389,6 +2394,8 @@ static void s_resubscribe_complete( connection, packet_id, &topic->request.topic, topic->request.qos, error_code, task_arg->on_suback_ud); } +clean_up: + /* We need to cleanup the subscribe_task_topics, since they are not inserted into the topic tree by resubscribe. We * take the ownership to clean it up */ for (size_t i = 0; i < list_len; i++) { @@ -2405,7 +2412,7 @@ uint16_t aws_mqtt_resubscribe_existing_topics( aws_mqtt_suback_multi_fn *on_suback, void *on_suback_ud) { - struct subscribe_task_arg *task_arg = aws_mem_acquire(connection->allocator, sizeof(struct subscribe_task_arg)); + struct subscribe_task_arg *task_arg = aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_task_arg)); if (!task_arg) { AWS_LOGF_ERROR( AWS_LS_MQTT_CLIENT, "id=%p: failed to allocate storage for resubscribe arguments", (void *)connection); From 936b788b477fc7f3227bef2d86037bbfa462316a Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:14:25 -0700 Subject: [PATCH 09/98] User initiated disconnect wipes the client state including pending and ongoing requests. (#216) --- source/client.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/client.c b/source/client.c index 215ae565..de5cf363 100644 --- a/source/client.c +++ b/source/client.c @@ -241,7 +241,15 @@ static void s_mqtt_client_shutdown( /* disconnect requested by user */ /* Successfully shutdown, so clear the outstanding requests */ /* TODO: respect the cleansession, clear the table when needed */ + + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Discard ongoing requests and pending requests when a disconnect requested by user.", + (void *)connection); + aws_linked_list_move_all_back(&cancelling_requests, &connection->thread_data.ongoing_requests_list); + aws_linked_list_move_all_back(&cancelling_requests, &connection->synced_data.pending_requests_list); aws_hash_table_clear(&connection->synced_data.outstanding_requests_table); + disconnected_state = true; AWS_LOGF_DEBUG( AWS_LS_MQTT_CLIENT, From 6b9e58cfeda5b490cf8809cd46b41df922e29fc4 Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Tue, 16 Aug 2022 06:25:26 -0700 Subject: [PATCH 10/98] Minimum stable connection time before resetting reconnect timer (#212) Adjusts the reconnect timer so that it only resets if a connection has been stable for at least 10 seconds - preventing quick, infinite reconnect loops that can occur if you try to subscribe to an invalid topic, etc. Co-authored-by: Noah Beard --- include/aws/mqtt/private/client_impl.h | 9 +++++---- source/client.c | 13 ++++++++----- source/client_channel_handler.c | 16 ++++++++++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index f7b4be01..379efba3 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -156,10 +156,11 @@ struct aws_mqtt_client_connection { struct aws_byte_buf payload; } will; struct { - uint64_t current; /* seconds */ - uint64_t min; /* seconds */ - uint64_t max; /* seconds */ - uint64_t next_attempt; /* milliseconds */ + uint64_t current; /* seconds */ + uint64_t min; /* seconds */ + uint64_t max; /* seconds */ + uint64_t next_attempt; /* milliseconds */ + uint64_t next_attempt_reset_timer; /* nanoseconds */ } reconnect_timeouts; /* User connection callbacks */ diff --git a/source/client.c b/source/client.c index de5cf363..a11d39b2 100644 --- a/source/client.c +++ b/source/client.c @@ -345,10 +345,12 @@ static void s_mqtt_client_shutdown( } mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ + if (!stop_reconnect) { - /* Attempt the reconnect immediately, which will schedule a task to retry if it doesn't succeed */ - connection->reconnect_task->task.fn( - &connection->reconnect_task->task, connection->reconnect_task->task.arg, AWS_TASK_STATUS_RUN_READY); + struct aws_event_loop *el = + aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); + aws_event_loop_schedule_task_future( + el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt); } break; } @@ -628,7 +630,6 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (s_mqtt_client_connect( connection, connection->on_connection_complete, connection->on_connection_complete_ud)) { - /* If reconnect attempt failed, schedule the next attempt */ struct aws_event_loop *el = aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); @@ -1665,6 +1666,8 @@ int aws_mqtt_client_connection_disconnect( mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ + connection->reconnect_timeouts.next_attempt_reset_timer = 0; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Closing connection", (void *)connection); mqtt_disconnect_impl(connection, AWS_OP_SUCCESS); @@ -2537,7 +2540,7 @@ static enum aws_mqtt_client_request_state s_unsubscribe_send( /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly - * fire fire the on_completion callbacks. */ + * fire the on_completion callbacks. */ struct request_timeout_task_arg *timeout_task_arg = s_schedule_timeout_task(task_arg->connection, packet_id); if (!timeout_task_arg) { return AWS_MQTT_CLIENT_REQUEST_ERROR; diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 25e8d31d..f82ec4bc 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -107,8 +107,20 @@ static int s_packet_handler_connack( } /* END CRITICAL SECTION */ connection->connection_count++; - /* Reset the current timeout timer */ - connection->reconnect_timeouts.current = connection->reconnect_timeouts.min; + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + + /* + * Only reset the duration of the reconnect timer to min if this connect is happening past + * the previously set next_attempt_reset_timer value. The next reset value will be 10 seconds after the next + * connection attempt + */ + if (connection->reconnect_timeouts.next_attempt_reset_timer < now) { + connection->reconnect_timeouts.current = connection->reconnect_timeouts.min; + } + connection->reconnect_timeouts.next_attempt_reset_timer = + now + 10000000000 + + aws_timestamp_convert(connection->reconnect_timeouts.current, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); if (connack.connect_return_code == AWS_MQTT_CONNECT_ACCEPTED) { /* If successfully connected, schedule all pending tasks */ From b25558a3ab9bcd7ca2895ccd725e92f57a893829 Mon Sep 17 00:00:00 2001 From: Dengke Tang Date: Tue, 16 Aug 2022 16:48:29 -0700 Subject: [PATCH 11/98] load cmake target fallback (#218) --- cmake/aws-c-mqtt-config.cmake | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/cmake/aws-c-mqtt-config.cmake b/cmake/aws-c-mqtt-config.cmake index 4a4dcbb1..c7993376 100644 --- a/cmake/aws-c-mqtt-config.cmake +++ b/cmake/aws-c-mqtt-config.cmake @@ -6,9 +6,21 @@ if (@MQTT_WITH_WEBSOCKETS@) find_dependency(aws-c-http) endif() +macro(aws_load_targets type) + include(${CMAKE_CURRENT_LIST_DIR}/${type}/@PROJECT_NAME@-targets.cmake) +endmacro() + +# try to load the lib follow BUILD_SHARED_LIBS. Fall back if not exist. if (BUILD_SHARED_LIBS) - include(${CMAKE_CURRENT_LIST_DIR}/shared/@PROJECT_NAME@-targets.cmake) + if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/shared") + aws_load_targets(shared) + else() + aws_load_targets(static) + endif() else() - include(${CMAKE_CURRENT_LIST_DIR}/static/@PROJECT_NAME@-targets.cmake) + if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/static") + aws_load_targets(static) + else() + aws_load_targets(shared) + endif() endif() - From eea8afa08999d65ea664c2cde0ccfcdf8cf02d89 Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:56:01 -0700 Subject: [PATCH 12/98] Infinite loop (#219) * added _sec _ms _ns where applicable to reconnect_timeouts * Added updating of next_attempt_reset_timer on successive failed reconnect attempts --- include/aws/mqtt/private/client_impl.h | 10 +++--- source/client.c | 42 +++++++++++++++----------- source/client_channel_handler.c | 9 +++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 379efba3..ca69646c 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -156,11 +156,11 @@ struct aws_mqtt_client_connection { struct aws_byte_buf payload; } will; struct { - uint64_t current; /* seconds */ - uint64_t min; /* seconds */ - uint64_t max; /* seconds */ - uint64_t next_attempt; /* milliseconds */ - uint64_t next_attempt_reset_timer; /* nanoseconds */ + uint64_t current_sec; /* seconds */ + uint64_t min_sec; /* seconds */ + uint64_t max_sec; /* seconds */ + uint64_t next_attempt_ms; /* milliseconds */ + uint64_t next_attempt_reset_timer_ns; /* nanoseconds */ } reconnect_timeouts; /* User connection callbacks */ diff --git a/source/client.c b/source/client.c index a11d39b2..9e42e3a2 100644 --- a/source/client.c +++ b/source/client.c @@ -321,7 +321,7 @@ static void s_mqtt_client_shutdown( AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Reconnect failed, retrying", (void *)connection); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt); + el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); break; } case AWS_MQTT_CLIENT_STATE_CONNECTED: { @@ -350,7 +350,7 @@ static void s_mqtt_client_shutdown( struct aws_event_loop *el = aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt); + el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); } break; } @@ -611,35 +611,44 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (status == AWS_TASK_STATUS_RUN_READY && connection) { /* If the task is not cancelled and a connection has not succeeded, attempt reconnect */ - aws_high_res_clock_get_ticks(&connection->reconnect_timeouts.next_attempt); - connection->reconnect_timeouts.next_attempt += aws_timestamp_convert( - connection->reconnect_timeouts.current, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + aws_high_res_clock_get_ticks(&connection->reconnect_timeouts.next_attempt_ms); + connection->reconnect_timeouts.next_attempt_ms += aws_timestamp_convert( + connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Attempting reconnect, if it fails next attempt will be in %" PRIu64 " seconds", (void *)connection, - connection->reconnect_timeouts.current); + connection->reconnect_timeouts.current_sec); /* Check before multiplying to avoid potential overflow */ - if (connection->reconnect_timeouts.current > connection->reconnect_timeouts.max / 2) { - connection->reconnect_timeouts.current = connection->reconnect_timeouts.max; + if (connection->reconnect_timeouts.current_sec > connection->reconnect_timeouts.max_sec / 2) { + connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.max_sec; } else { - connection->reconnect_timeouts.current *= 2; + connection->reconnect_timeouts.current_sec *= 2; } + /* Apply updated reconnect_timeout to next_attempt_reset_timer_ns to prevent premature reset to min + * of min reconnect on a successful connect after a prolonged period of failed connections */ + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + connection->reconnect_timeouts.next_attempt_reset_timer_ns = + now + 10000000000 + + aws_timestamp_convert( + connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + if (s_mqtt_client_connect( connection, connection->on_connection_complete, connection->on_connection_complete_ud)) { /* If reconnect attempt failed, schedule the next attempt */ struct aws_event_loop *el = aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt); + el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Scheduling reconnect, for %" PRIu64 " on event-loop %p", (void *)connection, - connection->reconnect_timeouts.next_attempt, + connection->reconnect_timeouts.next_attempt_ms, (void *)el); } else { connection->reconnect_task->task.timestamp = 0; @@ -795,8 +804,8 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt connection->client = aws_mqtt_client_acquire(client); AWS_ZERO_STRUCT(connection->synced_data); connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; - connection->reconnect_timeouts.min = 1; - connection->reconnect_timeouts.max = 128; + connection->reconnect_timeouts.min_sec = 1; + connection->reconnect_timeouts.max_sec = 128; aws_linked_list_init(&connection->synced_data.pending_requests_list); aws_linked_list_init(&connection->thread_data.ongoing_requests_list); @@ -1044,8 +1053,8 @@ int aws_mqtt_client_connection_set_reconnect_timeout( (void *)connection, min_timeout, max_timeout); - connection->reconnect_timeouts.min = min_timeout; - connection->reconnect_timeouts.max = max_timeout; + connection->reconnect_timeouts.min_sec = min_timeout; + connection->reconnect_timeouts.max_sec = max_timeout; return AWS_OP_SUCCESS; } @@ -1663,11 +1672,10 @@ int aws_mqtt_client_connection_disconnect( (void *)connection); connection->on_disconnect = on_disconnect; connection->on_disconnect_ud = userdata; + connection->reconnect_timeouts.next_attempt_reset_timer_ns = 0; mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ - connection->reconnect_timeouts.next_attempt_reset_timer = 0; - AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Closing connection", (void *)connection); mqtt_disconnect_impl(connection, AWS_OP_SUCCESS); diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index f82ec4bc..ef0cbb22 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -115,12 +115,13 @@ static int s_packet_handler_connack( * the previously set next_attempt_reset_timer value. The next reset value will be 10 seconds after the next * connection attempt */ - if (connection->reconnect_timeouts.next_attempt_reset_timer < now) { - connection->reconnect_timeouts.current = connection->reconnect_timeouts.min; + if (connection->reconnect_timeouts.next_attempt_reset_timer_ns < now) { + connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.min_sec; } - connection->reconnect_timeouts.next_attempt_reset_timer = + connection->reconnect_timeouts.next_attempt_reset_timer_ns = now + 10000000000 + - aws_timestamp_convert(connection->reconnect_timeouts.current, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + aws_timestamp_convert( + connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); if (connack.connect_return_code == AWS_MQTT_CONNECT_ACCEPTED) { /* If successfully connected, schedule all pending tasks */ From cea176e7f3ec32d1465788e95ae9c652014aa135 Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Tue, 23 Aug 2022 08:52:38 -0700 Subject: [PATCH 13/98] removed forced cleansession behavior on a user called disconnect (#220) --- source/client.c | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/source/client.c b/source/client.c index 9e42e3a2..074d98c5 100644 --- a/source/client.c +++ b/source/client.c @@ -239,17 +239,7 @@ static void s_mqtt_client_shutdown( break; case AWS_MQTT_CLIENT_STATE_DISCONNECTING: /* disconnect requested by user */ - /* Successfully shutdown, so clear the outstanding requests */ - /* TODO: respect the cleansession, clear the table when needed */ - - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, - "id=%p: Discard ongoing requests and pending requests when a disconnect requested by user.", - (void *)connection); - aws_linked_list_move_all_back(&cancelling_requests, &connection->thread_data.ongoing_requests_list); - aws_linked_list_move_all_back(&cancelling_requests, &connection->synced_data.pending_requests_list); - aws_hash_table_clear(&connection->synced_data.outstanding_requests_table); - + /* Successfully shutdown, if cleansession is set, ongoing and pending requests will be cleared */ disconnected_state = true; AWS_LOGF_DEBUG( AWS_LS_MQTT_CLIENT, From bc82d543dde51716bab1fe91b07e074815c4b823 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Thu, 15 Sep 2022 16:51:56 -0400 Subject: [PATCH 14/98] Add AppVerifier to CI (#222) --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b55f8e1..5d84cdbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,17 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON + + windows-app-verifier: + runs-on: windows-2022 # latest + steps: + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" + python builder.pyz build -p ${{ env.PACKAGE_NAME }} run_tests=false --cmake-extra=-DBUILD_TESTING=ON + - name: Run and check AppVerifier + run: | + python .\aws-c-mqtt\build\deps\aws-c-common\scripts\appverifier_ctest.py --build_directory .\aws-c-mqtt\build\aws-c-mqtt osx: runs-on: macos-11 # latest From 9d5cfcef9498fa066d587f19924d6832faf26cd8 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 2 Nov 2022 12:27:02 -0700 Subject: [PATCH 15/98] Log the client id going into the connect packet (#223) * Log the client id going into the connect packet * Reschedule task time side effects should be lock-protected --- source/client.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/source/client.c b/source/client.c index 074d98c5..88836053 100644 --- a/source/client.c +++ b/source/client.c @@ -516,13 +516,17 @@ static void s_mqtt_client_init( now += connection->ping_timeout_ns; aws_channel_schedule_task_future(channel, connack_task, now); + struct aws_byte_cursor client_id_cursor = aws_byte_cursor_from_buf(&connection->client_id); + AWS_LOGF_DEBUG( + AWS_LS_MQTT_CLIENT, + "id=%p: MQTT Connection initializing CONNECT packet for client-id '" PRInSTR "'", + (void *)connection, + AWS_BYTE_CURSOR_PRI(client_id_cursor)); + /* Send the connect packet */ struct aws_mqtt_packet_connect connect; aws_mqtt_packet_connect_init( - &connect, - aws_byte_cursor_from_buf(&connection->client_id), - connection->clean_session, - connection->keep_alive_time_secs); + &connect, client_id_cursor, connection->clean_session, connection->keep_alive_time_secs); if (connection->will.topic.buffer) { /* Add will if present */ @@ -601,6 +605,8 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (status == AWS_TASK_STATUS_RUN_READY && connection) { /* If the task is not cancelled and a connection has not succeeded, attempt reconnect */ + mqtt_connection_lock_synced_data(connection); + aws_high_res_clock_get_ticks(&connection->reconnect_timeouts.next_attempt_ms); connection->reconnect_timeouts.next_attempt_ms += aws_timestamp_convert( connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); @@ -627,6 +633,8 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ aws_timestamp_convert( connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + mqtt_connection_unlock_synced_data(connection); + if (s_mqtt_client_connect( connection, connection->on_connection_complete, connection->on_connection_complete_ud)) { /* If reconnect attempt failed, schedule the next attempt */ From 882c689561a3db1466330ccfe3b63637e0a575d3 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 4 Nov 2022 08:30:51 -0700 Subject: [PATCH 16/98] Serialize mqtt (#224) * Serialize connections and reconnect task by binding to a fixed per-connection thread --- include/aws/mqtt/private/client_impl.h | 1 + source/client.c | 27 ++++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index ca69646c..833e147e 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -140,6 +140,7 @@ struct aws_mqtt_client_connection { struct aws_tls_connection_options tls_options; struct aws_socket_options socket_options; struct aws_http_proxy_config *http_proxy_config; + struct aws_event_loop *loop; /* Connect parameters */ struct aws_byte_buf client_id; diff --git a/source/client.c b/source/client.c index 88836053..88d886ea 100644 --- a/source/client.c +++ b/source/client.c @@ -305,13 +305,10 @@ static void s_mqtt_client_shutdown( switch (prev_state) { case AWS_MQTT_CLIENT_STATE_RECONNECTING: { /* If reconnect attempt failed, schedule the next attempt */ - struct aws_event_loop *el = - aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Reconnect failed, retrying", (void *)connection); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); + connection->loop, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); break; } case AWS_MQTT_CLIENT_STATE_CONNECTED: { @@ -337,10 +334,10 @@ static void s_mqtt_client_shutdown( } /* END CRITICAL SECTION */ if (!stop_reconnect) { - struct aws_event_loop *el = - aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); + connection->loop, + &connection->reconnect_task->task, + connection->reconnect_timeouts.next_attempt_ms); } break; } @@ -435,6 +432,8 @@ static void s_mqtt_client_init( return; } + AWS_FATAL_ASSERT(aws_channel_get_event_loop(channel) == connection->loop); + /* user requested disconnect before the channel has been set up. Stop installing the slot and sending CONNECT. */ bool failed_create_slot = false; @@ -638,17 +637,16 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (s_mqtt_client_connect( connection, connection->on_connection_complete, connection->on_connection_complete_ud)) { /* If reconnect attempt failed, schedule the next attempt */ - struct aws_event_loop *el = - aws_event_loop_group_get_next_loop(connection->client->bootstrap->event_loop_group); aws_event_loop_schedule_task_future( - el, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); + connection->loop, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Scheduling reconnect, for %" PRIu64 " on event-loop %p", (void *)connection, connection->reconnect_timeouts.next_attempt_ms, - (void *)el); + (void *)connection->loop); } else { + /* Ideally, it would be nice to move this inside the lock, but I'm unsure of the correctness */ connection->reconnect_task->task.timestamp = 0; } } else { @@ -858,6 +856,8 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt goto failed_init_outstanding_requests_table; } + connection->loop = aws_event_loop_group_get_next_loop(client->bootstrap->event_loop_group); + /* Initialize the handler */ connection->handler.alloc = connection->allocator; connection->handler.vtable = aws_mqtt_get_client_channel_vtable(); @@ -1182,7 +1182,8 @@ static void s_on_websocket_setup( if (websocket) { channel = aws_websocket_get_channel(websocket); - AWS_ASSERT(channel); + AWS_FATAL_ASSERT(channel); + AWS_FATAL_ASSERT(aws_channel_get_event_loop(channel) == connection->loop); /* Websocket must be "converted" before the MQTT handler can be installed next to it. */ if (aws_websocket_convert_to_midchannel_handler(websocket)) { @@ -1311,6 +1312,7 @@ static void s_websocket_handshake_transform_complete( .user_data = connection, .on_connection_setup = s_on_websocket_setup, .on_connection_shutdown = s_on_websocket_shutdown, + .requested_event_loop = connection->loop, }; struct aws_http_proxy_options proxy_options; @@ -1598,6 +1600,7 @@ static int s_mqtt_client_connect( channel_options.setup_callback = &s_mqtt_client_init; channel_options.shutdown_callback = &s_mqtt_client_shutdown; channel_options.user_data = connection; + channel_options.requested_event_loop = connection->loop; if (connection->http_proxy_config == NULL) { result = aws_client_bootstrap_new_socket_channel(&channel_options); From ccc34d2efe17db8a1ac0e7b0b2e976a4d57a3d62 Mon Sep 17 00:00:00 2001 From: Waqar Ahmed Khan Date: Wed, 9 Nov 2022 16:06:07 -0800 Subject: [PATCH 17/98] Update CI to fix downstream build (#225) --- .github/workflows/ci.yml | 23 +++++++++++++++++------ builder.json | 4 +++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d84cdbb..4acc0780 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - 'main' env: - BUILDER_VERSION: v0.9.15 + BUILDER_VERSION: v0.9.26 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt @@ -35,7 +35,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON downstream + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON linux-compiler-compat: runs-on: ubuntu-20.04 # latest @@ -58,7 +58,7 @@ jobs: - name: Build ${{ env.PACKAGE_NAME }} run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh - ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON --spec downstream + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON clang-sanitizers: runs-on: ubuntu-20.04 # latest @@ -107,7 +107,7 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON - + windows-app-verifier: runs-on: windows-2022 # latest steps: @@ -120,10 +120,21 @@ jobs: python .\aws-c-mqtt\build\deps\aws-c-common\scripts\appverifier_ctest.py --build_directory .\aws-c-mqtt\build\aws-c-mqtt osx: - runs-on: macos-11 # latest + runs-on: macos-12 # latest steps: - name: Build ${{ env.PACKAGE_NAME }} + consumers run: | python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" chmod a+x builder - ./builder build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON default-downstream + ./builder build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON + + # Test downstream repos. + # This should not be required because we can run into a chicken and egg problem if there is a change that needs some fix in a downstream repo. + downstream: + runs-on: ubuntu-20.04 # latest + steps: + # We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages + - name: Build ${{ env.PACKAGE_NAME }} + run: | + aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh + ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build downstream -p ${{ env.PACKAGE_NAME }} diff --git a/builder.json b/builder.json index 55dfb62a..d0771149 100644 --- a/builder.json +++ b/builder.json @@ -1,9 +1,11 @@ { "name": "aws-c-mqtt", "upstream": [ - { "name": "aws-c-http" } + { "name": "aws-c-http" }, + { "name": "aws-c-io" } ], "downstream": [ + { "name": "aws-c-iot" } ], "cmake_args": [ ] From ac31a0459ab5aea924cf466b591ab2993c63d477 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Tue, 22 Nov 2022 18:45:59 -0500 Subject: [PATCH 18/98] MQTT5 support (#227) Add MQTT5 support to aws-c-mqtt. Co-authored-by: Vera Xia Co-authored-by: Steve Kim Co-authored-by: Bret Ambrose Co-authored-by: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Co-authored-by: aws-crt-bot <72894184+aws-crt-bot@users.noreply.github.com> Co-authored-by: Michael Graeb --- .github/workflows/ci.yml | 9 - CMakeLists.txt | 13 + README.md | 4 +- bin/elastipubsub/main.c | 2 +- bin/elastipubsub5/CMakeLists.txt | 29 + bin/elastipubsub5/main.c | 769 +++ bin/mqtt5canary/CMakeLists.txt | 29 + bin/mqtt5canary/main.c | 983 +++ codebuild/.gitignore | 1 + codebuild/CanaryWrapper.py | 323 + codebuild/CanaryWrapper_24_7.py | 392 ++ codebuild/CanaryWrapper_Classes.py | 1270 ++++ codebuild/CanaryWrapper_MetricFunctions.py | 49 + codebuild/mqtt-canary-test.yml | 58 + include/aws/mqtt/mqtt.h | 24 + include/aws/mqtt/private/shared_constants.h | 18 + .../aws/mqtt/private/v5/mqtt5_client_impl.h | 616 ++ include/aws/mqtt/private/v5/mqtt5_decoder.h | 263 + include/aws/mqtt/private/v5/mqtt5_encoder.h | 357 + .../mqtt/private/v5/mqtt5_options_storage.h | 343 + .../aws/mqtt/private/v5/mqtt5_topic_alias.h | 66 + include/aws/mqtt/private/v5/mqtt5_utils.h | 355 + include/aws/mqtt/private/v5/rate_limiters.h | 110 + include/aws/mqtt/v5/mqtt5_client.h | 785 +++ include/aws/mqtt/v5/mqtt5_packet_storage.h | 328 + include/aws/mqtt/v5/mqtt5_types.h | 478 ++ source/client.c | 16 +- source/mqtt.c | 66 + source/shared_constants.c | 22 + source/v5/mqtt5_client.c | 3306 ++++++++++ source/v5/mqtt5_decoder.c | 1165 ++++ source/v5/mqtt5_encoder.c | 1283 ++++ source/v5/mqtt5_options_storage.c | 3893 +++++++++++ source/v5/mqtt5_topic_alias.c | 586 ++ source/v5/mqtt5_types.c | 333 + source/v5/mqtt5_utils.c | 534 ++ source/v5/rate_limiters.c | 217 + tests/CMakeLists.txt | 254 +- tests/{ => v3-client}/aws_iot_client_test.c | 12 +- tests/{ => v3-client}/paho_client_test.c | 10 +- tests/{ => v3}/connection_state_test.c | 0 tests/{ => v3}/mqtt_mock_server_handler.c | 0 tests/{ => v3}/mqtt_mock_server_handler.h | 0 tests/{ => v3}/packet_encoding_test.c | 0 tests/{ => v3}/topic_tree_test.c | 0 tests/v5/mqtt5_client_tests.c | 5825 +++++++++++++++++ tests/v5/mqtt5_encoding_tests.c | 1808 +++++ tests/v5/mqtt5_operation_and_storage_tests.c | 3303 ++++++++++ ...mqtt5_operation_validation_failure_tests.c | 1327 ++++ tests/v5/mqtt5_testing_utils.c | 1747 +++++ tests/v5/mqtt5_testing_utils.h | 148 + tests/v5/mqtt5_topic_alias_tests.c | 589 ++ tests/v5/mqtt5_utils_tests.c | 115 + tests/v5/rate_limiter_tests.c | 343 + 54 files changed, 34533 insertions(+), 43 deletions(-) create mode 100644 bin/elastipubsub5/CMakeLists.txt create mode 100644 bin/elastipubsub5/main.c create mode 100644 bin/mqtt5canary/CMakeLists.txt create mode 100644 bin/mqtt5canary/main.c create mode 100644 codebuild/.gitignore create mode 100644 codebuild/CanaryWrapper.py create mode 100644 codebuild/CanaryWrapper_24_7.py create mode 100644 codebuild/CanaryWrapper_Classes.py create mode 100644 codebuild/CanaryWrapper_MetricFunctions.py create mode 100644 codebuild/mqtt-canary-test.yml create mode 100644 include/aws/mqtt/private/shared_constants.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_client_impl.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_decoder.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_encoder.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_options_storage.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_topic_alias.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_utils.h create mode 100644 include/aws/mqtt/private/v5/rate_limiters.h create mode 100644 include/aws/mqtt/v5/mqtt5_client.h create mode 100644 include/aws/mqtt/v5/mqtt5_packet_storage.h create mode 100644 include/aws/mqtt/v5/mqtt5_types.h create mode 100644 source/shared_constants.c create mode 100644 source/v5/mqtt5_client.c create mode 100644 source/v5/mqtt5_decoder.c create mode 100644 source/v5/mqtt5_encoder.c create mode 100644 source/v5/mqtt5_options_storage.c create mode 100644 source/v5/mqtt5_topic_alias.c create mode 100644 source/v5/mqtt5_types.c create mode 100644 source/v5/mqtt5_utils.c create mode 100644 source/v5/rate_limiters.c rename tests/{ => v3-client}/aws_iot_client_test.c (97%) rename tests/{ => v3-client}/paho_client_test.c (97%) rename tests/{ => v3}/connection_state_test.c (100%) rename tests/{ => v3}/mqtt_mock_server_handler.c (100%) rename tests/{ => v3}/mqtt_mock_server_handler.h (100%) rename tests/{ => v3}/packet_encoding_test.c (100%) rename tests/{ => v3}/topic_tree_test.c (100%) create mode 100644 tests/v5/mqtt5_client_tests.c create mode 100644 tests/v5/mqtt5_encoding_tests.c create mode 100644 tests/v5/mqtt5_operation_and_storage_tests.c create mode 100644 tests/v5/mqtt5_operation_validation_failure_tests.c create mode 100644 tests/v5/mqtt5_testing_utils.c create mode 100644 tests/v5/mqtt5_testing_utils.h create mode 100644 tests/v5/mqtt5_topic_alias_tests.c create mode 100644 tests/v5/mqtt5_utils_tests.c create mode 100644 tests/v5/rate_limiter_tests.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4acc0780..44e8a9c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,6 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON - linux-compiler-compat: runs-on: ubuntu-20.04 # latest strategy: @@ -59,7 +58,6 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON - clang-sanitizers: runs-on: ubuntu-20.04 # latest strategy: @@ -71,7 +69,6 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=clang-11 --cmake-extra=-DENABLE_SANITIZERS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DSANITIZERS="${{ matrix.sanitizers }}" - linux-shared-libs: runs-on: ubuntu-20.04 # latest steps: @@ -80,7 +77,6 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON - windows: runs-on: windows-2022 # latest steps: @@ -88,7 +84,6 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON - windows-vc14: runs-on: windows-2019 # windows-2019 is last env with Visual Studio 2015 (v14.0) strategy: @@ -99,7 +94,6 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --target windows-${{ matrix.arch }} --compiler msvc-14 --cmake-extra=-DASSERT_LOCK_HELD=ON - windows-shared-libs: runs-on: windows-2022 # latest steps: @@ -107,7 +101,6 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON - windows-app-verifier: runs-on: windows-2022 # latest steps: @@ -118,7 +111,6 @@ jobs: - name: Run and check AppVerifier run: | python .\aws-c-mqtt\build\deps\aws-c-common\scripts\appverifier_ctest.py --build_directory .\aws-c-mqtt\build\aws-c-mqtt - osx: runs-on: macos-12 # latest steps: @@ -127,7 +119,6 @@ jobs: python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" chmod a+x builder ./builder build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON - # Test downstream repos. # This should not be required because we can run into a chicken and egg problem if there is a change that needs some fix in a downstream repo. downstream: diff --git a/CMakeLists.txt b/CMakeLists.txt index 45eab337..1e4b7cfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,8 +43,13 @@ file(GLOB AWS_MQTT_HEADERS "include/aws/mqtt/*.h" ) +file(GLOB AWS_MQTT5_HEADERS + "include/aws/mqtt/v5/*.h" + ) + file(GLOB AWS_MQTT_PRIV_HEADERS "include/aws/mqtt/private/*.h" + "include/aws/mqtt/private/v5/*.h" ) file(GLOB AWS_MQTT_PRIV_EXPOSED_HEADERS @@ -53,6 +58,7 @@ file(GLOB AWS_MQTT_PRIV_EXPOSED_HEADERS file(GLOB AWS_MQTT_SRC "source/*.c" + "source/v5/*.c" ) file(GLOB MQTT_HEADERS @@ -60,6 +66,10 @@ file(GLOB MQTT_HEADERS ${AWS_MQTT_PRIV_HEADERS} ) +file(GLOB AWS_MQTT5_HEADERS + ${AWS_MQTT5_HEADERS} + ) + file(GLOB MQTT_SRC ${AWS_MQTT_SRC} ) @@ -89,6 +99,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC ${DEP_AWS_LIBS}) aws_prepare_shared_lib_exports(${PROJECT_NAME}) install(FILES ${AWS_MQTT_HEADERS} DESTINATION "include/aws/mqtt" COMPONENT Development) +install(FILES ${AWS_MQTT5_HEADERS} DESTINATION "include/aws/mqtt/v5" COMPONENT Development) install(FILES ${AWS_MQTT_TESTING_HEADERS} DESTINATION "include/aws/testing/mqtt" COMPONENT Development) install(FILES ${AWS_MQTT_PRIV_EXPOSED_HEADERS} DESTINATION "include/aws/mqtt/private" COMPONENT Development) @@ -116,5 +127,7 @@ if (BUILD_TESTING) add_subdirectory(tests) if (NOT CMAKE_CROSSCOMPILING ) add_subdirectory(bin/elastipubsub) + add_subdirectory(bin/elastipubsub5) + add_subdirectory(bin/mqtt5canary) endif() endif () diff --git a/README.md b/README.md index e7f9ac0f..070aab53 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ C99 implementation of the MQTT 3.1.1 specification. ## License -This library is licensed under the Apache 2.0 License. +This library is licensed under the Apache 2.0 License. ## Usage @@ -203,6 +203,6 @@ the wire. For QoS 1, as soon as PUBACK comes back. For QoS 2, PUBCOMP. `topic` a ```c int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connection); ``` -Sends a PINGREQ packet to the server. +Sends a PINGREQ packet to the server. [aws-c-io]: https://github.com/awslabs/aws-c-io diff --git a/bin/elastipubsub/main.c b/bin/elastipubsub/main.c index 9ebadb36..04085169 100644 --- a/bin/elastipubsub/main.c +++ b/bin/elastipubsub/main.c @@ -160,7 +160,7 @@ static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { case 'h': s_usage(0); break; - case 'E': { + case 0x02: { struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(aws_cli_positional_arg); if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { fprintf( diff --git a/bin/elastipubsub5/CMakeLists.txt b/bin/elastipubsub5/CMakeLists.txt new file mode 100644 index 00000000..727fdbbe --- /dev/null +++ b/bin/elastipubsub5/CMakeLists.txt @@ -0,0 +1,29 @@ +project(elastipubsub5 C) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_INSTALL_PREFIX}/lib/cmake") + +file(GLOB ELASTIPUBSUB_SRC + "*.c" + ) + +set(ELASTIPUBSUB_MQTT5_PROJECT_NAME elastipubsub5) +add_executable(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} ${ELASTIPUBSUB_SRC}) +aws_set_common_properties(${ELASTIPUBSUB_MQTT5_PROJECT_NAME}) + + +target_include_directories(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} aws-c-mqtt) + +if (BUILD_SHARED_LIBS AND NOT WIN32) + message(INFO " elastiPUBSUB will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") +endif() + +install(TARGETS ${ELASTIPUBSUB_MQTT5_PROJECT_NAME} + EXPORT ${ELASTIPUBSUB_MQTT5_PROJECT_NAME}-targets + COMPONENT Runtime + RUNTIME + DESTINATION bin + COMPONENT Runtime) diff --git a/bin/elastipubsub5/main.c b/bin/elastipubsub5/main.c new file mode 100644 index 00000000..f3ac686b --- /dev/null +++ b/bin/elastipubsub5/main.c @@ -0,0 +1,769 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef _MSC_VER +# pragma warning(disable : 4996) /* Disable warnings about fopen() being insecure */ +# pragma warning(disable : 4204) /* Declared initializers */ +# pragma warning(disable : 4221) /* Local var in declared initializer */ +#endif + +#ifdef WIN32 +// Windows does not need specific imports +#else +# include +#endif + +struct app_ctx { + struct aws_allocator *allocator; + struct aws_mutex lock; + struct aws_condition_variable signal; + struct aws_uri uri; + uint16_t port; + const char *cacert; + const char *cert; + const char *key; + int connect_timeout; + bool use_websockets; + + struct aws_tls_connection_options tls_connection_options; + + const char *log_filename; + enum aws_log_level log_level; +}; + +static void s_usage(int exit_code) { + + fprintf(stderr, "usage: elastipubsub5 [options] endpoint\n"); + fprintf(stderr, " endpoint: url to connect to\n"); + fprintf(stderr, "\n Options:\n\n"); + fprintf(stderr, " --cacert FILE: path to a CA certficate file.\n"); + fprintf(stderr, " --cert FILE: path to a PEM encoded certificate to use with mTLS\n"); + fprintf(stderr, " --key FILE: Path to a PEM encoded private key that matches cert.\n"); + fprintf(stderr, " --connect-timeout INT: time in milliseconds to wait for a connection.\n"); + fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); + fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); + fprintf(stderr, " -w, --websockets: use mqtt-over-websockets rather than direct mqtt\n"); + fprintf(stderr, " -h, --help\n"); + fprintf(stderr, " Display this message and quit.\n"); + exit(exit_code); +} + +static struct aws_cli_option s_long_options[] = { + {"cacert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'a'}, + {"cert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'c'}, + {"key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'e'}, + {"connect-timeout", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'f'}, + {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, + {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, + {"websockets", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'w'}, + {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, + /* Per getopt(3) the last element of the array has to be filled with all zeros */ + {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, +}; + +static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { + bool uri_found = false; + + while (true) { + int option_index = 0; + int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wh", s_long_options, &option_index); + if (c == -1) { + break; + } + + switch (c) { + case 0: + /* getopt_long() returns 0 if an option.flag is non-null */ + break; + case 'a': + ctx->cacert = aws_cli_optarg; + break; + case 'c': + ctx->cert = aws_cli_optarg; + break; + case 'e': + ctx->key = aws_cli_optarg; + break; + case 'f': + ctx->connect_timeout = atoi(aws_cli_optarg); + break; + case 'l': + ctx->log_filename = aws_cli_optarg; + break; + case 'v': + if (!strcmp(aws_cli_optarg, "TRACE")) { + ctx->log_level = AWS_LL_TRACE; + } else if (!strcmp(aws_cli_optarg, "INFO")) { + ctx->log_level = AWS_LL_INFO; + } else if (!strcmp(aws_cli_optarg, "DEBUG")) { + ctx->log_level = AWS_LL_DEBUG; + } else if (!strcmp(aws_cli_optarg, "ERROR")) { + ctx->log_level = AWS_LL_ERROR; + } else { + fprintf(stderr, "unsupported log level %s.\n", aws_cli_optarg); + s_usage(1); + } + break; + case 'h': + s_usage(0); + break; + case 'w': + ctx->use_websockets = true; + break; + case 0x02: { + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(aws_cli_positional_arg); + if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { + fprintf( + stderr, + "Failed to parse uri %s with error %s\n", + (char *)uri_cursor.ptr, + aws_error_debug_str(aws_last_error())); + s_usage(1); + } + uri_found = true; + break; + } + + default: + fprintf(stderr, "Unknown option\n"); + s_usage(1); + } + } + + if (!uri_found) { + fprintf(stderr, "A URI for the request must be supplied.\n"); + s_usage(1); + } +} + +static void s_on_subscribe_complete_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + (void)error_code; + (void)complete_ctx; + + printf("SUBACK received!\n"); + for (size_t i = 0; i < suback->reason_code_count; ++i) { + printf("Subscription %d: %s\n", (int)i, aws_mqtt5_suback_reason_code_to_c_string(suback->reason_codes[i])); + } + fflush(stdout); +} + +static void s_on_unsubscribe_complete_fn( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + + (void)error_code; + (void)complete_ctx; + + printf("UNSUBACK received!\n"); + for (size_t i = 0; i < unsuback->reason_code_count; ++i) { + printf("Topic Filter %d: %s\n", (int)i, aws_mqtt5_unsuback_reason_code_to_c_string(unsuback->reason_codes[i])); + } + fflush(stdout); +} + +static void s_on_publish_complete_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + (void)complete_ctx; + + switch (error_code) { + case AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY: + printf("PUBLISH FAILED due to disconnect and offline queue policy"); + break; + case AWS_ERROR_MQTT_TIMEOUT: + printf("PUBLISH FAILED due to MQTT Timeout"); + break; + case AWS_ERROR_SUCCESS: + printf("PUBLISH SUCCESS\n"); + break; + default: + break; + } + + if (packet_type == AWS_MQTT5_PT_PUBACK) { + const struct aws_mqtt5_packet_puback_view *puback = packet; + printf("PUBACK received!\n"); + printf("PUBACK id:%d %s\n", puback->packet_id, aws_mqtt5_puback_reason_code_to_c_string(puback->reason_code)); + } else { + printf("PUBLISH Complete with no PUBACK\n"); + } + + fflush(stdout); +} + +static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; + + printf("PUBLISH received!\n"); + printf( + "Publish received to topic:'" PRInSTR "' payload '" PRInSTR "'\n", + AWS_BYTE_CURSOR_PRI(publish->topic), + AWS_BYTE_CURSOR_PRI(publish->payload)); +} + +static void s_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + + switch (event->event_type) { + case AWS_MQTT5_CLET_STOPPED: + printf("Lifecycle event: Stopped!\n"); + break; + + case AWS_MQTT5_CLET_ATTEMPTING_CONNECT: + printf("Lifecycle event: Attempting Connect!\n"); + break; + + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + printf("Lifecycle event: Connection Failure!\n"); + printf(" Error Code: %d(%s)\n", event->error_code, aws_error_debug_str(event->error_code)); + break; + + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + printf("Lifecycle event: Connection Success!\n"); + break; + + case AWS_MQTT5_CLET_DISCONNECTION: + printf("Lifecycle event: Disconnect!\n"); + printf(" Error Code: %d(%s)\n", event->error_code, aws_error_debug_str(event->error_code)); + break; + } + + fflush(stdout); +} + +static bool s_skip_whitespace(uint8_t value) { + return value == '\n' || value == '\r' || value == '\t' || value == ' '; +} + +static void s_split_command_line(struct aws_byte_cursor cursor, struct aws_array_list *words) { + struct aws_byte_cursor split_cursor; + AWS_ZERO_STRUCT(split_cursor); + + while (aws_byte_cursor_next_split(&cursor, ' ', &split_cursor)) { + struct aws_byte_cursor word_cursor = aws_byte_cursor_trim_pred(&split_cursor, &s_skip_whitespace); + if (word_cursor.len > 0) { + aws_array_list_push_back(words, &word_cursor); + } + } +} + +static void s_handle_subscribe( + struct aws_mqtt5_client *client, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + struct aws_mqtt5_subscribe_completion_options subscribe_completion_options = { + .completion_callback = &s_on_subscribe_complete_fn, + .completion_user_data = NULL, + }; + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count < 2) { + printf("invalid subscribe options:\n"); + printf(" subscribe topic1 topic2 ....\n"); + return; + } + + struct aws_byte_cursor qos_cursor; + AWS_ZERO_STRUCT(qos_cursor); + aws_array_list_get_at(arguments, &qos_cursor, 1); + + struct aws_string *qos_string = aws_string_new_from_cursor(allocator, &qos_cursor); + + int qos_value = atoi((const char *)qos_string->bytes); + enum aws_mqtt5_qos qos = qos_value; + + size_t topic_count = aws_array_list_length(arguments) - 2; + + struct aws_array_list subscriptions; + aws_array_list_init_dynamic(&subscriptions, allocator, topic_count, sizeof(struct aws_mqtt5_subscription_view)); + + printf("Subscribing to:\n"); + + for (size_t i = 0; i < topic_count; ++i) { + size_t topic_index = i + 2; + struct aws_byte_cursor topic_filter_cursor; + + aws_array_list_get_at(arguments, &topic_filter_cursor, topic_index); + + struct aws_mqtt5_subscription_view subscription = { + .topic_filter = topic_filter_cursor, + .qos = qos, + .no_local = false, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_DONT_SEND, + }; + + printf(" %d:" PRInSTR "\n", (int)i, AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + aws_array_list_push_back(&subscriptions, &subscription); + } + + struct aws_mqtt5_packet_subscribe_view packet_subscribe_view = { + .subscription_count = aws_array_list_length(&subscriptions), + .subscriptions = subscriptions.data, + }; + + aws_mqtt5_client_subscribe(client, &packet_subscribe_view, &subscribe_completion_options); + + aws_array_list_clean_up(&subscriptions); + aws_string_destroy(qos_string); +} + +static void s_handle_unsubscribe(struct aws_mqtt5_client *client, struct aws_array_list *arguments) { + struct aws_mqtt5_unsubscribe_completion_options unsubscribe_completion_options = { + .completion_callback = &s_on_unsubscribe_complete_fn, + .completion_user_data = NULL, + }; + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count < 1) { + printf("invalid unsubscribe options:\n"); + printf(" unsubscribe topic1 topic2 ....\n"); + return; + } + + size_t topic_count = aws_array_list_length(arguments) - 1; + + printf("Unsubscribing to:\n"); + + for (size_t i = 0; i < topic_count; ++i) { + size_t topic_index = i + 1; + struct aws_byte_cursor topic_filter_cursor; + + aws_array_list_get_at(arguments, &topic_filter_cursor, topic_index); + + printf(" %d:" PRInSTR "\n", (int)i, AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + } + + struct aws_mqtt5_packet_unsubscribe_view packet_unsubscribe_view = { + .topic_filter_count = topic_count, + .topic_filters = ((struct aws_byte_cursor *)arguments->data) + 1, + }; + + aws_mqtt5_client_unsubscribe(client, &packet_unsubscribe_view, &unsubscribe_completion_options); +} + +static void s_handle_publish( + struct aws_mqtt5_client *client, + struct aws_allocator *allocator, + struct aws_array_list *arguments, + struct aws_byte_cursor *full_argument) { + struct aws_mqtt5_publish_completion_options publish_completion_options = { + .completion_callback = &s_on_publish_complete_fn, + .completion_user_data = NULL, + }; + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count < 2) { + printf("invalid publish call:\n"); + printf(" publish topic \n"); + return; + } + + /* QoS */ + struct aws_byte_cursor qos_cursor; + AWS_ZERO_STRUCT(qos_cursor); + aws_array_list_get_at(arguments, &qos_cursor, 1); + struct aws_string *qos_string = aws_string_new_from_cursor(allocator, &qos_cursor); + int qos_value = atoi((const char *)qos_string->bytes); + enum aws_mqtt5_qos qos = qos_value; + + /* TOPIC */ + struct aws_byte_cursor topic_cursor; + AWS_ZERO_STRUCT(topic_cursor); + aws_array_list_get_at(arguments, &topic_cursor, 2); + + /* PAYLOAD */ + struct aws_byte_cursor payload_cursor; + AWS_ZERO_STRUCT(payload_cursor); + /* account for empty payload */ + + if (argument_count > 2) { + aws_array_list_get_at(arguments, &payload_cursor, 3); + payload_cursor.len = (size_t)(full_argument->ptr + full_argument->len - payload_cursor.ptr); + } + + printf( + "Publishing to Topic:'" PRInSTR "' Payload:'" PRInSTR "'\n", + AWS_BYTE_CURSOR_PRI(topic_cursor), + AWS_BYTE_CURSOR_PRI(payload_cursor)); + + struct aws_mqtt5_packet_publish_view packet_publish_view = { + .qos = qos, + .topic = topic_cursor, + .retain = false, + .duplicate = false, + .payload = payload_cursor, + }; + + aws_mqtt5_client_publish(client, &packet_publish_view, &publish_completion_options); + + aws_string_destroy(qos_string); +} + +static void s_on_disconnect_completion(int error_code, void *user_data) { + (void)user_data; + + printf("DISCONNECT complete with error code %d(%s)!", error_code, aws_error_debug_str(error_code)); + fflush(stdout); +} + +static void s_handle_stop( + struct aws_mqtt5_client *client, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + switch (argument_count) { + case 0: + printf("Stopping client by shutting down channel!\n"); + aws_mqtt5_client_stop(client, NULL, NULL); + break; + + case 1: { + struct aws_byte_cursor reason_code_cursor; + AWS_ZERO_STRUCT(reason_code_cursor); + aws_array_list_get_at(arguments, &reason_code_cursor, 1); + + struct aws_string *reason_code_string = aws_string_new_from_cursor(allocator, &reason_code_cursor); + + int reason_code_value = atoi((const char *)reason_code_string->bytes); + enum aws_mqtt5_disconnect_reason_code reason_code = reason_code_value; + aws_string_destroy(reason_code_string); + + struct aws_mqtt5_packet_disconnect_view disconnect_options = { + .reason_code = reason_code, + }; + + struct aws_mqtt5_disconnect_completion_options completion_options = { + .completion_callback = s_on_disconnect_completion, + .completion_user_data = client, + }; + + printf( + "Stopping client cleanly by sending DISCONNECT packet with reason code %d(%s)!\n", + reason_code_value, + aws_mqtt5_disconnect_reason_code_to_c_string(reason_code, NULL)); + aws_mqtt5_client_stop(client, &disconnect_options, &completion_options); + break; + } + + default: + printf("invalid stop options:\n"); + printf(" stop [optional int: reason_code]\n"); + break; + } +} + +static bool s_handle_input(struct aws_mqtt5_client *client, struct aws_allocator *allocator, const char *input_line) { + + struct aws_byte_cursor quit_cursor = aws_byte_cursor_from_c_str("quit"); + struct aws_byte_cursor start_cursor = aws_byte_cursor_from_c_str("start"); + struct aws_byte_cursor stop_cursor = aws_byte_cursor_from_c_str("stop"); + struct aws_byte_cursor subscribe_cursor = aws_byte_cursor_from_c_str("subscribe"); + struct aws_byte_cursor unsubscribe_cursor = aws_byte_cursor_from_c_str("unsubscribe"); + struct aws_byte_cursor publish_cursor = aws_byte_cursor_from_c_str("publish"); + + struct aws_array_list words; + aws_array_list_init_dynamic(&words, allocator, 10, sizeof(struct aws_byte_cursor)); + + struct aws_byte_cursor line_cursor = aws_byte_cursor_from_c_str(input_line); + line_cursor = aws_byte_cursor_trim_pred(&line_cursor, &s_skip_whitespace); + + bool done = false; + + s_split_command_line(line_cursor, &words); + if (aws_array_list_length(&words) == 0) { + printf("Empty command line\n"); + goto done; + } + + struct aws_byte_cursor command_cursor; + AWS_ZERO_STRUCT(command_cursor); + aws_array_list_get_at(&words, &command_cursor, 0); + + if (aws_byte_cursor_eq_ignore_case(&command_cursor, &quit_cursor)) { + printf("Quitting!\n"); + done = true; + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &start_cursor)) { + printf("Starting client!\n"); + aws_mqtt5_client_start(client); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &stop_cursor)) { + s_handle_stop(client, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &subscribe_cursor)) { + s_handle_subscribe(client, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &unsubscribe_cursor)) { + s_handle_unsubscribe(client, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &publish_cursor)) { + s_handle_publish(client, allocator, &words, &line_cursor); + } else { + printf("Unknown command: " PRInSTR "\n", AWS_BYTE_CURSOR_PRI(command_cursor)); + } + +done: + + aws_array_list_clean_up(&words); + return done; +} + +static void s_aws_mqtt5_transform_websocket_handshake_fn( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + + (void)user_data; + + (*complete_fn)(request, AWS_ERROR_SUCCESS, complete_ctx); +} + +AWS_STATIC_STRING_FROM_LITERAL(s_client_id, "HelloWorld"); + +int main(int argc, char **argv) { + struct aws_allocator *allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, AWS_MEMTRACE_STACKS, 15); + + aws_mqtt_library_init(allocator); + + struct app_ctx app_ctx; + AWS_ZERO_STRUCT(app_ctx); + app_ctx.allocator = allocator; + app_ctx.signal = (struct aws_condition_variable)AWS_CONDITION_VARIABLE_INIT; + app_ctx.connect_timeout = 3000; + aws_mutex_init(&app_ctx.lock); + app_ctx.port = 1883; + + s_parse_options(argc, argv, &app_ctx); + if (app_ctx.uri.port) { + app_ctx.port = app_ctx.uri.port; + } + + struct aws_logger logger; + AWS_ZERO_STRUCT(logger); + + struct aws_logger_standard_options options = { + .level = app_ctx.log_level, + }; + + if (app_ctx.log_level) { + if (app_ctx.log_filename) { + options.filename = app_ctx.log_filename; + } else { + options.file = stderr; + } + + if (aws_logger_init_standard(&logger, allocator, &options)) { + fprintf(stderr, "Failed to initialize logger with error %s\n", aws_error_debug_str(aws_last_error())); + exit(1); + } + + aws_logger_set(&logger); + } + + bool use_tls = false; + struct aws_tls_ctx *tls_ctx = NULL; + struct aws_tls_ctx_options tls_ctx_options; + AWS_ZERO_STRUCT(tls_ctx_options); + struct aws_tls_connection_options tls_connection_options; + AWS_ZERO_STRUCT(tls_connection_options); + + if (app_ctx.cert && app_ctx.key) { + if (aws_tls_ctx_options_init_client_mtls_from_path(&tls_ctx_options, allocator, app_ctx.cert, app_ctx.key)) { + fprintf( + stderr, + "Failed to load %s and %s with error %s.", + app_ctx.cert, + app_ctx.key, + aws_error_debug_str(aws_last_error())); + exit(1); + } + + if (app_ctx.cacert) { + if (aws_tls_ctx_options_override_default_trust_store_from_path(&tls_ctx_options, NULL, app_ctx.cacert)) { + fprintf( + stderr, "Failed to load %s with error %s", app_ctx.cacert, aws_error_debug_str(aws_last_error())); + exit(1); + } + } + + if (aws_tls_ctx_options_set_alpn_list(&tls_ctx_options, "x-amzn-mqtt-ca")) { + fprintf(stderr, "Failed to set alpn list with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + + if (!tls_ctx) { + fprintf(stderr, "Failed to initialize TLS context with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + aws_tls_connection_options_init_from_ctx(&tls_connection_options, tls_ctx); + if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &app_ctx.uri.host_name)) { + fprintf(stderr, "Failed to set servername with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + use_tls = true; + } + + struct aws_event_loop_group *el_group = aws_event_loop_group_new_default(allocator, 2, NULL); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = el_group, + .max_entries = 8, + }; + + struct aws_host_resolver *resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = el_group, + .host_resolver = resolver, + }; + + struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + struct aws_socket_options socket_options = { + .type = AWS_SOCKET_STREAM, + .connect_timeout_ms = (uint32_t)app_ctx.connect_timeout, + .keep_alive_timeout_sec = 0, + .keepalive = false, + .keep_alive_interval_sec = 0, + }; + + uint16_t receive_maximum = 9; + uint32_t maximum_packet_size = 128 * 1024; + + struct aws_mqtt5_packet_connect_view connect_options = { + .keep_alive_interval_seconds = 30, + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = true, + .maximum_packet_size_bytes = &maximum_packet_size, + .receive_maximum = &receive_maximum, + }; + + aws_mqtt5_transform_websocket_handshake_fn *websocket_handshake_transform = NULL; + void *websocket_handshake_transform_user_data = NULL; + if (app_ctx.use_websockets) { + websocket_handshake_transform = &s_aws_mqtt5_transform_websocket_handshake_fn; + } + + struct aws_mqtt5_client_options client_options = { + .host_name = app_ctx.uri.host_name, + .port = app_ctx.port, + .bootstrap = bootstrap, + .socket_options = &socket_options, + .tls_options = (use_tls) ? &tls_connection_options : NULL, + .connect_options = &connect_options, + .session_behavior = AWS_MQTT5_CSBT_CLEAN, + .lifecycle_event_handler = s_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE, + .min_reconnect_delay_ms = 1000, + .max_reconnect_delay_ms = 120000, + .min_connected_time_to_reset_reconnect_delay_ms = 30000, + .ping_timeout_ms = 10000, + .websocket_handshake_transform = websocket_handshake_transform, + .websocket_handshake_transform_user_data = websocket_handshake_transform_user_data, + .publish_received_handler = s_on_publish_received, + }; + + struct aws_mqtt5_client *client = aws_mqtt5_client_new(allocator, &client_options); + aws_mqtt5_client_start(client); + + bool done = false; + while (!done) { + printf("Enter command:\n"); + + char input_buffer[4096]; +#ifdef WIN32 + char *line = gets_s(input_buffer, AWS_ARRAY_SIZE(input_buffer)); +#else + char *line = fgets(input_buffer, AWS_ARRAY_SIZE(input_buffer), stdin); +#endif + done = s_handle_input(client, allocator, line); + } + + aws_mqtt5_client_release(client); + + aws_client_bootstrap_release(bootstrap); + aws_host_resolver_release(resolver); + aws_event_loop_group_release(el_group); + + if (tls_ctx) { + aws_tls_connection_options_clean_up(&tls_connection_options); + aws_tls_ctx_release(tls_ctx); + aws_tls_ctx_options_clean_up(&tls_ctx_options); + } + + aws_thread_join_all_managed(); + + const size_t outstanding_bytes = aws_mem_tracer_bytes(allocator); + printf("Summary:\n"); + printf(" Outstanding bytes: %zu\n\n", outstanding_bytes); + + if (app_ctx.log_level) { + aws_logger_set(NULL); + aws_logger_clean_up(&logger); + } + + aws_uri_clean_up(&app_ctx.uri); + + aws_mqtt_library_clean_up(); + + const size_t leaked_bytes = aws_mem_tracer_bytes(allocator); + if (leaked_bytes) { + struct aws_logger memory_logger; + AWS_ZERO_STRUCT(memory_logger); + + aws_logger_init_noalloc(&memory_logger, aws_default_allocator(), &options); + aws_logger_set(&memory_logger); + + aws_mqtt_library_init(aws_default_allocator()); + + printf("Writing memory leaks to log.\n"); + aws_mem_tracer_dump(allocator); + + aws_logger_set(NULL); + aws_logger_clean_up(&memory_logger); + + aws_mqtt_library_clean_up(); + } else { + printf("Finished, with no memory leaks\n"); + } + + aws_mem_tracer_destroy(allocator); + + return 0; +} diff --git a/bin/mqtt5canary/CMakeLists.txt b/bin/mqtt5canary/CMakeLists.txt new file mode 100644 index 00000000..c1d381b5 --- /dev/null +++ b/bin/mqtt5canary/CMakeLists.txt @@ -0,0 +1,29 @@ +project(mqtt5canary C) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_INSTALL_PREFIX}/lib/cmake") + +file(GLOB MQTT5CANARY_SRC + "*.c" + ) + +set(MQTT5CANARY_PROJECT_NAME mqtt5canary) +add_executable(${MQTT5CANARY_PROJECT_NAME} ${MQTT5CANARY_SRC}) +aws_set_common_properties(${MQTT5CANARY_PROJECT_NAME}) + + +target_include_directories(${MQTT5CANARY_PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${MQTT5CANARY_PROJECT_NAME} aws-c-mqtt) + +if (BUILD_SHARED_LIBS AND NOT WIN32) + message(INFO " mqtt5canary will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") +endif() + +install(TARGETS ${MQTT5CANARY_PROJECT_NAME} + EXPORT ${MQTT5CANARY_PROJECT_NAME}-targets + COMPONENT Runtime + RUNTIME + DESTINATION bin + COMPONENT Runtime) diff --git a/bin/mqtt5canary/main.c b/bin/mqtt5canary/main.c new file mode 100644 index 00000000..f20850ce --- /dev/null +++ b/bin/mqtt5canary/main.c @@ -0,0 +1,983 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef _MSC_VER +# pragma warning(disable : 4996) /* Disable warnings about fopen() being insecure */ +# pragma warning(disable : 4204) /* Declared initializers */ +# pragma warning(disable : 4221) /* Local var in declared initializer */ +#endif + +#define AWS_MQTT5_CANARY_CLIENT_CREATION_SLEEP_TIME 10000000 +#define AWS_MQTT5_CANARY_OPERATION_ARRAY_SIZE 10000 +#define AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE 256 +#define AWS_MQTT5_CANARY_CLIENT_MAX 50 +#define AWS_MQTT5_CANARY_PAYLOAD_SIZE_MAX UINT16_MAX + +struct app_ctx { + struct aws_allocator *allocator; + struct aws_mutex lock; + struct aws_condition_variable signal; + struct aws_uri uri; + uint16_t port; + const char *cacert; + const char *cert; + const char *key; + int connect_timeout; + bool use_websockets; + + struct aws_tls_connection_options tls_connection_options; + + const char *log_filename; + enum aws_log_level log_level; +}; + +enum aws_mqtt5_canary_operations { + AWS_MQTT5_CANARY_OPERATION_NULL = 0, + AWS_MQTT5_CANARY_OPERATION_START = 1, + AWS_MQTT5_CANARY_OPERATION_STOP = 2, + AWS_MQTT5_CANARY_OPERATION_DESTROY = 3, + AWS_MQTT5_CANARY_OPERATION_SUBSCRIBE = 4, + AWS_MQTT5_CANARY_OPERATION_UNSUBSCRIBE = 5, + AWS_MQTT5_CANARY_OPERATION_UNSUBSCRIBE_BAD = 6, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_QOS0 = 7, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_QOS1 = 8, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SUBSCRIBED_TOPIC_QOS0 = 9, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SUBSCRIBED_TOPIC_QOS1 = 10, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SHARED_TOPIC_QOS0 = 11, + AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SHARED_TOPIC_QOS1 = 12, + AWS_MQTT5_CANARY_OPERATION_COUNT = 13, +}; + +struct aws_mqtt5_canary_tester_options { + uint16_t elg_max_threads; + uint16_t client_count; + size_t tps; + uint64_t tps_sleep_time; + size_t distributions_total; + enum aws_mqtt5_canary_operations *operations; + size_t test_run_seconds; +}; + +static void s_usage(int exit_code) { + + fprintf(stderr, "usage: elastipubsub5 [options] endpoint\n"); + fprintf(stderr, " endpoint: url to connect to\n"); + fprintf(stderr, "\n Options:\n\n"); + fprintf(stderr, " --cacert FILE: path to a CA certficate file.\n"); + fprintf(stderr, " --cert FILE: path to a PEM encoded certificate to use with mTLS\n"); + fprintf(stderr, " --key FILE: Path to a PEM encoded private key that matches cert.\n"); + fprintf(stderr, " --connect-timeout INT: time in milliseconds to wait for a connection.\n"); + fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); + fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); + fprintf(stderr, " -w, --websockets: use mqtt-over-websockets rather than direct mqtt\n"); + + fprintf(stderr, " -t, --threads: number of eventloop group threads to use\n"); + fprintf(stderr, " -C, --clients: number of mqtt5 clients to use\n"); + fprintf(stderr, " -T, --tps: operations to run per second\n"); + fprintf(stderr, " -s, --seconds: seconds to run canary test\n"); + fprintf(stderr, " -h, --help\n"); + fprintf(stderr, " Display this message and quit.\n"); + exit(exit_code); +} + +static struct aws_cli_option s_long_options[] = { + {"cacert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'a'}, + {"cert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'c'}, + {"key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'e'}, + {"connect-timeout", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'f'}, + {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, + {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, + {"websockets", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'w'}, + {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, + + {"threads", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 't'}, + {"clients", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'C'}, + {"tps", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'T'}, + {"seconds", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 's'}, + /* Per getopt(3) the last element of the array has to be filled with all zeros */ + {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, +}; + +static void s_parse_options( + int argc, + char **argv, + struct app_ctx *ctx, + struct aws_mqtt5_canary_tester_options *tester_options) { + bool uri_found = false; + + while (true) { + int option_index = 0; + int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wht:C:T:s:", s_long_options, &option_index); + if (c == -1) { + break; + } + + switch (c) { + case 0: + /* getopt_long() returns 0 if an option.flag is non-null */ + break; + case 'a': + ctx->cacert = aws_cli_optarg; + break; + case 'c': + ctx->cert = aws_cli_optarg; + break; + case 'e': + ctx->key = aws_cli_optarg; + break; + case 'f': + ctx->connect_timeout = atoi(aws_cli_optarg); + break; + case 'l': + ctx->log_filename = aws_cli_optarg; + break; + case 'v': + if (!strcmp(aws_cli_optarg, "TRACE")) { + ctx->log_level = AWS_LL_TRACE; + } else if (!strcmp(aws_cli_optarg, "INFO")) { + ctx->log_level = AWS_LL_INFO; + } else if (!strcmp(aws_cli_optarg, "DEBUG")) { + ctx->log_level = AWS_LL_DEBUG; + } else if (!strcmp(aws_cli_optarg, "ERROR")) { + ctx->log_level = AWS_LL_ERROR; + } else { + fprintf(stderr, "unsupported log level %s.\n", aws_cli_optarg); + s_usage(1); + } + break; + case 'h': + s_usage(0); + break; + case 'w': + ctx->use_websockets = true; + break; + case 't': + tester_options->elg_max_threads = (uint16_t)atoi(aws_cli_optarg); + break; + case 'C': + tester_options->client_count = (uint16_t)atoi(aws_cli_optarg); + if (tester_options->client_count > AWS_MQTT5_CANARY_CLIENT_MAX) { + tester_options->client_count = AWS_MQTT5_CANARY_CLIENT_MAX; + } + break; + case 'T': + tester_options->tps = atoi(aws_cli_optarg); + break; + case 's': + tester_options->test_run_seconds = atoi(aws_cli_optarg); + break; + case 0x02: { + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(aws_cli_positional_arg); + if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { + fprintf( + stderr, + "Failed to parse uri %s with error %s\n", + (char *)uri_cursor.ptr, + aws_error_debug_str(aws_last_error())); + s_usage(1); + } + uri_found = true; + break; + } + + default: + fprintf(stderr, "Unknown option\n"); + s_usage(1); + } + } + + if (!uri_found) { + fprintf(stderr, "A URI for the request must be supplied.\n"); + s_usage(1); + } +} + +/********************************************************** + * MQTT5 CANARY OPTIONS + **********************************************************/ + +static void s_aws_mqtt5_canary_update_tps_sleep_time(struct aws_mqtt5_canary_tester_options *tester_options) { + tester_options->tps_sleep_time = + (aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL) / tester_options->tps); +} + +static void s_aws_mqtt5_canary_init_tester_options(struct aws_mqtt5_canary_tester_options *tester_options) { + /* number of eventloop group threads to use */ + tester_options->elg_max_threads = 3; + /* number of mqtt5 clients to use */ + tester_options->client_count = 10; + /* operations per second to run */ + tester_options->tps = 50; + /* How long to run the test before exiting */ + tester_options->test_run_seconds = 25200; +} + +struct aws_mqtt5_canary_test_client { + struct aws_mqtt5_client *client; + const struct aws_mqtt5_negotiated_settings *settings; + struct aws_byte_cursor shared_topic; + struct aws_byte_cursor client_id; + size_t subscription_count; + bool is_connected; +}; + +/********************************************************** + * OPERATION DISTRIBUTION + **********************************************************/ + +typedef int(aws_mqtt5_canary_operation_fn)(struct aws_mqtt5_canary_test_client *test_client); + +struct aws_mqtt5_canary_operations_function_table { + aws_mqtt5_canary_operation_fn *operation_by_operation_type[AWS_MQTT5_CANARY_OPERATION_COUNT]; +}; + +static void s_aws_mqtt5_canary_add_operation_to_array( + struct aws_mqtt5_canary_tester_options *tester_options, + enum aws_mqtt5_canary_operations operation_type, + size_t probability) { + for (size_t i = 0; i < probability; ++i) { + + tester_options->operations[tester_options->distributions_total] = operation_type; + tester_options->distributions_total += 1; + } +} + +/* Add operations and their weighted probability to the list of possible operations */ +static void s_aws_mqtt5_canary_init_weighted_operations(struct aws_mqtt5_canary_tester_options *tester_options) { + + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_STOP, 1); + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_SUBSCRIBE, 200); + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_UNSUBSCRIBE, 200); + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_UNSUBSCRIBE_BAD, 100); + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_QOS0, 300); + s_aws_mqtt5_canary_add_operation_to_array(tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_QOS1, 150); + s_aws_mqtt5_canary_add_operation_to_array( + tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SUBSCRIBED_TOPIC_QOS0, 100); + s_aws_mqtt5_canary_add_operation_to_array( + tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SUBSCRIBED_TOPIC_QOS1, 50); + s_aws_mqtt5_canary_add_operation_to_array( + tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SHARED_TOPIC_QOS0, 50); + s_aws_mqtt5_canary_add_operation_to_array( + tester_options, AWS_MQTT5_CANARY_OPERATION_PUBLISH_TO_SHARED_TOPIC_QOS1, 50); +} + +static enum aws_mqtt5_canary_operations s_aws_mqtt5_canary_get_random_operation( + struct aws_mqtt5_canary_tester_options *tester_options) { + size_t random_index = rand() % tester_options->distributions_total; + + return tester_options->operations[random_index]; +} + +/********************************************************** + * PACKET CALLBACKS + **********************************************************/ + +static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + + struct aws_mqtt5_canary_test_client *test_client = user_data; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish Received on topic " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(publish->topic)); +} + +/********************************************************** + * LIFECYCLE EVENTS + **********************************************************/ + +static void s_handle_lifecycle_event_connection_success( + struct aws_mqtt5_canary_test_client *test_client, + const struct aws_mqtt5_negotiated_settings *settings) { + AWS_ASSERT(test_client != NULL); + test_client->is_connected = true; + test_client->settings = settings; + test_client->client_id = aws_byte_cursor_from_buf(&settings->client_id_storage); + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Lifecycle Event: Connection Success", + AWS_BYTE_CURSOR_PRI(test_client->client_id)); +} + +static void s_handle_lifecycle_event_disconnection(struct aws_mqtt5_canary_test_client *test_client) { + AWS_ASSERT(test_client != NULL); + test_client->is_connected = false; + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Lifecycle Event: Disconnect", AWS_BYTE_CURSOR_PRI(test_client->client_id)); +} + +static void s_handle_lifecycle_event_stopped(struct aws_mqtt5_canary_test_client *test_client) { + AWS_ASSERT(test_client != NULL); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Lifecycle Event: Stopped", AWS_BYTE_CURSOR_PRI(test_client->client_id)); +} + +static void s_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + switch (event->event_type) { + case AWS_MQTT5_CLET_STOPPED: + s_handle_lifecycle_event_stopped(event->user_data); + break; + + case AWS_MQTT5_CLET_ATTEMPTING_CONNECT: + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "Lifecycle event: Attempting Connect!"); + break; + + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "Lifecycle event: Connection Failure!"); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, " Error Code: %d(%s)", event->error_code, aws_error_debug_str(event->error_code)); + break; + + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + s_handle_lifecycle_event_connection_success(event->user_data, event->settings); + break; + + case AWS_MQTT5_CLET_DISCONNECTION: + s_handle_lifecycle_event_disconnection(event->user_data); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, " Error Code: %d(%s)", event->error_code, aws_error_debug_str(event->error_code)); + break; + } +} + +static void s_aws_mqtt5_transform_websocket_handshake_fn( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + + (void)user_data; + + (*complete_fn)(request, AWS_ERROR_SUCCESS, complete_ctx); +} + +/********************************************************** + * OPERATION FUNCTIONS + **********************************************************/ + +static int s_aws_mqtt5_canary_operation_start(struct aws_mqtt5_canary_test_client *test_client) { + if (test_client->is_connected) { + return AWS_OP_SUCCESS; + } + aws_mqtt5_client_start(test_client->client); + struct aws_byte_cursor client_id; + if (test_client->client_id.len) { + client_id.ptr = test_client->client_id.ptr; + client_id.len = test_client->client_id.len; + } else { + client_id = aws_byte_cursor_from_c_str("Client ID not set"); + } + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Start", AWS_BYTE_CURSOR_PRI(client_id)); + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_canary_operation_stop(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return AWS_OP_SUCCESS; + } + aws_mqtt5_client_stop(test_client->client, NULL, NULL); + test_client->subscription_count = 0; + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Stop", AWS_BYTE_CURSOR_PRI(test_client->client_id)); + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_canary_operation_subscribe(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + char topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf( + topic_array, + sizeof topic_array, + PRInSTR "_%zu", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + test_client->subscription_count); + + struct aws_mqtt5_subscription_view subscriptions[] = { + { + .topic_filter = aws_byte_cursor_from_c_str(topic_array), + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + }, + { + .topic_filter = + { + .ptr = test_client->shared_topic.ptr, + .len = test_client->shared_topic.len, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + }, + }; + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = subscriptions, + .subscription_count = AWS_ARRAY_SIZE(subscriptions), + }; + + test_client->subscription_count++; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Subscribe to topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(subscriptions->topic_filter)); + return aws_mqtt5_client_subscribe(test_client->client, &subscribe_view, NULL); +} + +static int s_aws_mqtt5_canary_operation_unsubscribe_bad(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + char topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf( + topic_array, sizeof topic_array, PRInSTR "_non_existing_topic", AWS_BYTE_CURSOR_PRI(test_client->client_id)); + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str(topic_array); + struct aws_byte_cursor unsubscribes[] = { + { + .ptr = topic.ptr, + .len = topic.len, + }, + }; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = unsubscribes, + .topic_filter_count = AWS_ARRAY_SIZE(unsubscribes), + }; + + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Unsubscribe Bad", AWS_BYTE_CURSOR_PRI(test_client->client_id)); + return aws_mqtt5_client_unsubscribe(test_client->client, &unsubscribe_view, NULL); +} + +static int s_aws_mqtt5_canary_operation_unsubscribe(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + + if (test_client->subscription_count <= 0) { + return s_aws_mqtt5_canary_operation_unsubscribe_bad(test_client); + } + + test_client->subscription_count--; + char topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf( + topic_array, + sizeof topic_array, + PRInSTR "_%zu", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + test_client->subscription_count); + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str(topic_array); + struct aws_byte_cursor unsubscribes[] = { + { + .ptr = topic.ptr, + .len = topic.len, + }, + }; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = unsubscribes, + .topic_filter_count = AWS_ARRAY_SIZE(unsubscribes), + }; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Unsubscribe from topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(topic)); + return aws_mqtt5_client_unsubscribe(test_client->client, &unsubscribe_view, NULL); +} + +static int s_aws_mqtt5_canary_operation_publish( + struct aws_mqtt5_canary_test_client *test_client, + struct aws_byte_cursor topic_filter, + enum aws_mqtt5_qos qos) { + + struct aws_byte_cursor property_cursor = aws_byte_cursor_from_c_str("property"); + struct aws_mqtt5_user_property user_properties[] = { + { + .name = + { + .ptr = property_cursor.ptr, + .len = property_cursor.len, + }, + .value = + { + .ptr = property_cursor.ptr, + .len = property_cursor.len, + }, + }, + { + .name = + { + .ptr = property_cursor.ptr, + .len = property_cursor.len, + }, + .value = + { + .ptr = property_cursor.ptr, + .len = property_cursor.len, + }, + }, + }; + + uint16_t payload_size = (rand() % UINT16_MAX) + 1; + uint8_t payload_data[AWS_MQTT5_CANARY_PAYLOAD_SIZE_MAX]; + + struct aws_mqtt5_packet_publish_view packet_publish_view = { + .qos = qos, + .topic = topic_filter, + .retain = false, + .duplicate = false, + .payload = + { + .ptr = payload_data, + .len = payload_size, + }, + .user_properties = user_properties, + .user_property_count = AWS_ARRAY_SIZE(user_properties), + }; + + return aws_mqtt5_client_publish(test_client->client, &packet_publish_view, NULL); +} + +static int s_aws_mqtt5_canary_operation_publish_qos0(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + + struct aws_byte_cursor topic_cursor; + AWS_ZERO_STRUCT(topic_cursor); + topic_cursor = aws_byte_cursor_from_c_str("topic1"); + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Publish qos0", AWS_BYTE_CURSOR_PRI(test_client->client_id)); + return s_aws_mqtt5_canary_operation_publish(test_client, topic_cursor, AWS_MQTT5_QOS_AT_MOST_ONCE); +} + +static int s_aws_mqtt5_canary_operation_publish_qos1(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + struct aws_byte_cursor topic_cursor; + AWS_ZERO_STRUCT(topic_cursor); + topic_cursor = aws_byte_cursor_from_c_str("topic1"); + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Publish qos1", AWS_BYTE_CURSOR_PRI(test_client->client_id)); + return s_aws_mqtt5_canary_operation_publish(test_client, topic_cursor, AWS_MQTT5_QOS_AT_LEAST_ONCE); +} + +static int s_aws_mqtt5_canary_operation_publish_to_subscribed_topic_qos0( + struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + + if (test_client->subscription_count < 1) { + return s_aws_mqtt5_canary_operation_publish_qos0(test_client); + } + char topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf( + topic_array, + sizeof topic_array, + PRInSTR "_%zu", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + test_client->subscription_count - 1); + struct aws_byte_cursor topic_cursor = aws_byte_cursor_from_c_str(topic_array); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish qos 0 to subscribed topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(topic_cursor)); + return s_aws_mqtt5_canary_operation_publish(test_client, topic_cursor, AWS_MQTT5_QOS_AT_MOST_ONCE); +} + +static int s_aws_mqtt5_canary_operation_publish_to_subscribed_topic_qos1( + struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + + if (test_client->subscription_count < 1) { + return s_aws_mqtt5_canary_operation_publish_qos1(test_client); + } + + char topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf( + topic_array, + sizeof topic_array, + PRInSTR "_%zu", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + test_client->subscription_count - 1); + struct aws_byte_cursor topic_cursor = aws_byte_cursor_from_c_str(topic_array); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish qos 1 to subscribed topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(topic_cursor)); + return s_aws_mqtt5_canary_operation_publish(test_client, topic_cursor, AWS_MQTT5_QOS_AT_LEAST_ONCE); +} + +static int s_aws_mqtt5_canary_operation_publish_to_shared_topic_qos0(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish qos 0 to shared topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(test_client->shared_topic)); + return s_aws_mqtt5_canary_operation_publish(test_client, test_client->shared_topic, AWS_MQTT5_QOS_AT_MOST_ONCE); +} + +static int s_aws_mqtt5_canary_operation_publish_to_shared_topic_qos1(struct aws_mqtt5_canary_test_client *test_client) { + if (!test_client->is_connected) { + return s_aws_mqtt5_canary_operation_start(test_client); + } + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish qos 1 to shared topic: " PRInSTR, + AWS_BYTE_CURSOR_PRI(test_client->client_id), + AWS_BYTE_CURSOR_PRI(test_client->shared_topic)); + return s_aws_mqtt5_canary_operation_publish(test_client, test_client->shared_topic, AWS_MQTT5_QOS_AT_LEAST_ONCE); +} + +static struct aws_mqtt5_canary_operations_function_table s_aws_mqtt5_canary_operation_table = { + .operation_by_operation_type = + { + NULL, /* null */ + &s_aws_mqtt5_canary_operation_start, /* start */ + &s_aws_mqtt5_canary_operation_stop, /* stop */ + NULL, /* destroy */ + &s_aws_mqtt5_canary_operation_subscribe, /* subscribe */ + &s_aws_mqtt5_canary_operation_unsubscribe, /* unsubscribe */ + &s_aws_mqtt5_canary_operation_unsubscribe_bad, /* unsubscribe_bad */ + &s_aws_mqtt5_canary_operation_publish_qos0, /* publish_qos0 */ + &s_aws_mqtt5_canary_operation_publish_qos1, /* publish_qos1 */ + &s_aws_mqtt5_canary_operation_publish_to_subscribed_topic_qos0, /* publish_to_subscribed_topic_qos0 */ + &s_aws_mqtt5_canary_operation_publish_to_subscribed_topic_qos1, /* publish_to_subscribed_topic_qos1 */ + &s_aws_mqtt5_canary_operation_publish_to_shared_topic_qos0, /* publish_to_shared_topic_qos0 */ + &s_aws_mqtt5_canary_operation_publish_to_shared_topic_qos1, /* publish_to_shared_topic_qos1 */ + }, +}; + +/********************************************************** + * MAIN + **********************************************************/ + +int main(int argc, char **argv) { + + struct aws_allocator *allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, AWS_MEMTRACE_STACKS, 15); + aws_mqtt_library_init(allocator); + + struct app_ctx app_ctx; + AWS_ZERO_STRUCT(app_ctx); + app_ctx.allocator = allocator; + app_ctx.signal = (struct aws_condition_variable)AWS_CONDITION_VARIABLE_INIT; + app_ctx.connect_timeout = 3000; + aws_mutex_init(&app_ctx.lock); + app_ctx.port = 1883; + + struct aws_mqtt5_canary_tester_options tester_options; + AWS_ZERO_STRUCT(tester_options); + s_aws_mqtt5_canary_init_tester_options(&tester_options); + enum aws_mqtt5_canary_operations operations[AWS_MQTT5_CANARY_OPERATION_ARRAY_SIZE]; + AWS_ZERO_STRUCT(operations); + tester_options.operations = operations; + + s_parse_options(argc, argv, &app_ctx, &tester_options); + if (app_ctx.uri.port) { + app_ctx.port = app_ctx.uri.port; + } + + s_aws_mqtt5_canary_update_tps_sleep_time(&tester_options); + s_aws_mqtt5_canary_init_weighted_operations(&tester_options); + + /********************************************************** + * LOGGING + **********************************************************/ + struct aws_logger logger; + AWS_ZERO_STRUCT(logger); + + struct aws_logger_standard_options options = { + .level = app_ctx.log_level, + }; + + if (app_ctx.log_level) { + if (app_ctx.log_filename) { + options.filename = app_ctx.log_filename; + } else { + options.file = stderr; + } + if (aws_logger_init_standard(&logger, allocator, &options)) { + fprintf(stderr, "Faled to initialize logger with error %s", aws_error_debug_str(aws_last_error())); + exit(1); + } + aws_logger_set(&logger); + } else { + options.file = stderr; + } + + /********************************************************** + * TLS + **********************************************************/ + bool use_tls = false; + struct aws_tls_ctx *tls_ctx = NULL; + struct aws_tls_ctx_options tls_ctx_options; + AWS_ZERO_STRUCT(tls_ctx_options); + struct aws_tls_connection_options tls_connection_options; + AWS_ZERO_STRUCT(tls_connection_options); + + if (app_ctx.cert && app_ctx.key) { + if (aws_tls_ctx_options_init_client_mtls_from_path(&tls_ctx_options, allocator, app_ctx.cert, app_ctx.key)) { + fprintf( + stderr, + "Failed to load %s and %s with error %s.", + app_ctx.cert, + app_ctx.key, + aws_error_debug_str(aws_last_error())); + exit(1); + } + + if (app_ctx.cacert) { + if (aws_tls_ctx_options_override_default_trust_store_from_path(&tls_ctx_options, NULL, app_ctx.cacert)) { + fprintf( + stderr, "Failed to load %s with error %s", app_ctx.cacert, aws_error_debug_str(aws_last_error())); + exit(1); + } + } + + if (aws_tls_ctx_options_set_alpn_list(&tls_ctx_options, "x-amzn-mqtt-ca")) { + fprintf(stderr, "Failed to set alpn list with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + + if (!tls_ctx) { + fprintf(stderr, "Failed to initialize TLS context with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + aws_tls_connection_options_init_from_ctx(&tls_connection_options, tls_ctx); + if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &app_ctx.uri.host_name)) { + fprintf(stderr, "Failed to set servername with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + use_tls = true; + } + + /********************************************************** + * EVENT LOOP GROUP + **********************************************************/ + struct aws_event_loop_group *el_group = + aws_event_loop_group_new_default(allocator, tester_options.elg_max_threads, NULL); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = el_group, + .max_entries = 8, + }; + + struct aws_host_resolver *resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = el_group, + .host_resolver = resolver, + }; + + struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + struct aws_socket_options socket_options = { + .type = AWS_SOCKET_STREAM, + .connect_timeout_ms = (uint32_t)app_ctx.connect_timeout, + .keep_alive_timeout_sec = 0, + .keepalive = false, + .keep_alive_interval_sec = 0, + }; + + uint16_t receive_maximum = 9; + uint32_t maximum_packet_size = 128 * 1024; + + aws_mqtt5_transform_websocket_handshake_fn *websocket_handshake_transform = NULL; + void *websocket_handshake_transform_user_data = NULL; + if (app_ctx.use_websockets) { + websocket_handshake_transform = &s_aws_mqtt5_transform_websocket_handshake_fn; + } + + /********************************************************** + * MQTT5 CLIENT CREATION + **********************************************************/ + + struct aws_mqtt5_packet_connect_view connect_options = { + .keep_alive_interval_seconds = 30, + .clean_start = true, + .maximum_packet_size_bytes = &maximum_packet_size, + .receive_maximum = &receive_maximum, + }; + + struct aws_mqtt5_client_options client_options = { + .host_name = app_ctx.uri.host_name, + .port = app_ctx.port, + .bootstrap = bootstrap, + .socket_options = &socket_options, + .tls_options = (use_tls) ? &tls_connection_options : NULL, + .connect_options = &connect_options, + .session_behavior = AWS_MQTT5_CSBT_CLEAN, + .lifecycle_event_handler = s_lifecycle_event_callback, + .retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE, + .min_reconnect_delay_ms = 1000, + .max_reconnect_delay_ms = 120000, + .min_connected_time_to_reset_reconnect_delay_ms = 30000, + .ping_timeout_ms = 10000, + .websocket_handshake_transform = websocket_handshake_transform, + .websocket_handshake_transform_user_data = websocket_handshake_transform_user_data, + .publish_received_handler = s_on_publish_received, + }; + + struct aws_mqtt5_canary_test_client clients[AWS_MQTT5_CANARY_CLIENT_MAX]; + AWS_ZERO_STRUCT(clients); + + uint64_t start_time = 0; + aws_high_res_clock_get_ticks(&start_time); + char shared_topic_array[AWS_MQTT5_CANARY_TOPIC_ARRAY_SIZE] = ""; + snprintf(shared_topic_array, sizeof shared_topic_array, "%" PRIu64 "_shared_topic", start_time); + struct aws_byte_cursor shared_topic = aws_byte_cursor_from_c_str(shared_topic_array); + + for (size_t i = 0; i < tester_options.client_count; ++i) { + + client_options.lifecycle_event_handler_user_data = &clients[i]; + client_options.publish_received_handler_user_data = &clients[i]; + + clients[i].shared_topic = shared_topic; + clients[i].client = aws_mqtt5_client_new(allocator, &client_options); + + aws_mqtt5_canary_operation_fn *operation_fn = + s_aws_mqtt5_canary_operation_table.operation_by_operation_type[AWS_MQTT5_CANARY_OPERATION_START]; + (*operation_fn)(&clients[i]); + + aws_thread_current_sleep(AWS_MQTT5_CANARY_CLIENT_CREATION_SLEEP_TIME); + } + + fprintf(stderr, "Clients created\n"); + + /********************************************************** + * TESTING + **********************************************************/ + bool done = false; + size_t operations_executed = 0; + uint64_t time_test_finish = 0; + aws_high_res_clock_get_ticks(&time_test_finish); + time_test_finish += + aws_timestamp_convert(tester_options.test_run_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + + printf("Running test for %zu seconds\n", tester_options.test_run_seconds); + + while (!done) { + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + operations_executed++; + + enum aws_mqtt5_canary_operations next_operation = s_aws_mqtt5_canary_get_random_operation(&tester_options); + aws_mqtt5_canary_operation_fn *operation_fn = + s_aws_mqtt5_canary_operation_table.operation_by_operation_type[next_operation]; + + (*operation_fn)(&clients[rand() % tester_options.client_count]); + + if (now > time_test_finish) { + done = true; + } + + aws_thread_current_sleep(tester_options.tps_sleep_time); + } + + /********************************************************** + * CLEAN UP + **********************************************************/ + for (size_t i = 0; i < tester_options.client_count; ++i) { + struct aws_mqtt5_client *client = clients[i].client; + aws_mqtt5_client_release(client); + } + + aws_client_bootstrap_release(bootstrap); + aws_host_resolver_release(resolver); + aws_event_loop_group_release(el_group); + + if (tls_ctx) { + aws_tls_connection_options_clean_up(&tls_connection_options); + aws_tls_ctx_release(tls_ctx); + aws_tls_ctx_options_clean_up(&tls_ctx_options); + } + + aws_thread_join_all_managed(); + + const size_t outstanding_bytes = aws_mem_tracer_bytes(allocator); + printf("Summary:\n"); + printf(" Outstanding bytes: %zu\n", outstanding_bytes); + + if (app_ctx.log_level) { + aws_logger_set(NULL); + aws_logger_clean_up(&logger); + } + + aws_uri_clean_up(&app_ctx.uri); + + aws_mqtt_library_clean_up(); + + printf(" Operations executed: %zu\n", operations_executed); + printf(" Operating TPS average over test: %zu\n\n", operations_executed / tester_options.test_run_seconds); + + const size_t leaked_bytes = aws_mem_tracer_bytes(allocator); + if (leaked_bytes) { + struct aws_logger memory_logger; + AWS_ZERO_STRUCT(memory_logger); + + aws_logger_init_noalloc(&memory_logger, aws_default_allocator(), &options); + aws_logger_set(&memory_logger); + + aws_mqtt_library_init(aws_default_allocator()); + + printf("Writing memory leaks to log.\n"); + aws_mem_tracer_dump(allocator); + + aws_logger_set(NULL); + aws_logger_clean_up(&memory_logger); + + aws_mqtt_library_clean_up(); + } else { + printf("Finished, with no memory leaks\n"); + } + + aws_mem_tracer_destroy(allocator); + + return 0; +} diff --git a/codebuild/.gitignore b/codebuild/.gitignore new file mode 100644 index 00000000..bee8a64b --- /dev/null +++ b/codebuild/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/codebuild/CanaryWrapper.py b/codebuild/CanaryWrapper.py new file mode 100644 index 00000000..fe894d8b --- /dev/null +++ b/codebuild/CanaryWrapper.py @@ -0,0 +1,323 @@ +# Python wrapper script for collecting Canary metrics, setting-up/tearing-down alarms, reporting metrics to Cloudwatch, +# checking the alarms to ensure everything is correct at the end of the run, and pushing the log to S3 if successful. + +# Needs to be installed prior to running +# Part of standard packages in Python 3.4+ +import argparse +import time +import datetime +# Dependencies in project folder +from CanaryWrapper_Classes import * +from CanaryWrapper_MetricFunctions import * + +# Code for command line argument parsing +# ================================================================================ +command_parser = argparse.ArgumentParser("CanaryWrapper") +command_parser.add_argument("--canary_executable", type=str, required=True, + help="The path to the canary executable (or program - like 'python3')") +command_parser.add_argument("--canary_arguments", type=str, default="", + help="The arguments to pass/launch the canary executable with") +command_parser.add_argument("--git_hash", type=str, required=True, + help="The Git commit hash that we are running the canary with") +command_parser.add_argument("--git_repo_name", type=str, required=True, + help="The name of the Git repository") +command_parser.add_argument("--git_hash_as_namespace", type=bool, default=False, + help="(OPTIONAL, default=False) If true, the git hash will be used as the name of the Cloudwatch namespace") +command_parser.add_argument("--output_log_filepath", type=str, default="output.log", + help="(OPTIONAL, default=output.log) The file to output log info to. Set to 'None' to disable") +command_parser.add_argument("--output_to_console", type=bool, default=True, + help="(OPTIONAL, default=True) If true, info will be output to the console") +command_parser.add_argument("--cloudwatch_region", type=str, default="us-east-1", + help="(OPTIONAL, default=us-east-1) The AWS region for Cloudwatch") +command_parser.add_argument("--s3_bucket_name", type=str, default="canary-wrapper-folder", + help="(OPTIONAL, default=canary-wrapper-folder) The name of the S3 bucket where success logs will be stored") +command_parser.add_argument("--snapshot_wait_time", type=int, default=600, + help="(OPTIONAL, default=600) The number of seconds between gathering and sending snapshot reports") +command_parser.add_argument("--ticket_category", type=str, default="AWS", + help="(OPTIONAL, default=AWS) The category to register the ticket under") +command_parser.add_argument("--ticket_type", type=str, default="SDKs and Tools", + help="(OPTIONAL, default='SDKs and Tools') The type to register the ticket under") +command_parser.add_argument("--ticket_item", type=str, default="IoT SDK for CPP", + help="(OPTIONAL, default='IoT SDK for CPP') The item to register the ticket under") +command_parser.add_argument("--ticket_group", type=str, default="AWS IoT Device SDK", + help="(OPTIONAL, default='AWS IoT Device SDK') The group to register the ticket under") +command_parser.add_argument("--dependencies", type=str, default="", + help="(OPTIONAL, default='') Any dependencies and their commit hashes. \ + Current expected format is '(name or path);(hash);(next name or path);(hash);(etc...)'.") +command_parser.add_argument("--lambda_name", type=str, default="iot-send-email-lambda", + help="(OPTIONAL, default='CanarySendEmailLambda') The name of the Lambda used to send emails") +command_parser.add_argument("--codebuild_log_path", type=str, default="", + help="The CODEBUILD_LOG_PATH environment variable. Leave blank to ignore") +command_parser_arguments = command_parser.parse_args() + +if (command_parser_arguments.output_log_filepath == "None"): + command_parser_arguments.output_log_filepath = None +if (command_parser_arguments.snapshot_wait_time <= 0): + command_parser_arguments.snapshot_wait_time = 60 + +# Deal with possibly empty values in semi-critical commands/arguments +if (command_parser_arguments.canary_executable == ""): + print ("ERROR - required canary_executable is empty!", flush=True) + exit (1) # cannot run without a canary executable +if (command_parser_arguments.git_hash == ""): + print ("ERROR - required git_hash is empty!", flush=True) + exit (1) # cannot run without git hash +if (command_parser_arguments.git_repo_name == ""): + print ("ERROR - required git_repo_name is empty!", flush=True) + exit (1) # cannot run without git repo name +if (command_parser_arguments.git_hash_as_namespace is not True and command_parser_arguments.git_hash_as_namespace is not False): + command_parser_arguments.git_hash_as_namespace = False +if (command_parser_arguments.output_log_filepath == ""): + command_parser_arguments.output_log_filepath = None +if (command_parser_arguments.output_to_console != True and command_parser_arguments.output_to_console != False): + command_parser_arguments.output_to_console = True +if (command_parser_arguments.cloudwatch_region == ""): + command_parser_arguments.cloudwatch_region = "us-east-1" +if (command_parser_arguments.s3_bucket_name == ""): + command_parser_arguments.s3_bucket_name = "canary-wrapper-folder" +if (command_parser_arguments.ticket_category == ""): + command_parser_arguments.ticket_category = "AWS" +if (command_parser_arguments.ticket_type == ""): + command_parser_arguments.ticket_type = "SDKs and Tools" +if (command_parser_arguments.ticket_item == ""): + command_parser_arguments.ticket_item = "IoT SDK for CPP" +if (command_parser_arguments.ticket_group == ""): + command_parser_arguments.ticket_group = "AWS IoT Device SDK" + + + +# ================================================================================ + +datetime_now = datetime.datetime.now() +datetime_string = datetime_now.strftime("%d-%m-%Y/%H-%M-%S") +print("Datetime string is: " + datetime_string, flush=True) + +# Make the snapshot class +data_snapshot = DataSnapshot( + git_hash=command_parser_arguments.git_hash, + git_repo_name=command_parser_arguments.git_repo_name, + datetime_string=datetime_string, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + output_log_filepath="output.txt", + output_to_console=command_parser_arguments.output_to_console, + cloudwatch_region="us-east-1", + cloudwatch_make_dashboard=False, + cloudwatch_teardown_alarms_on_complete=True, + cloudwatch_teardown_dashboard_on_complete=True, + s3_bucket_name=command_parser_arguments.s3_bucket_name, + s3_bucket_upload_on_complete=True, + lambda_name=command_parser_arguments.lambda_name, + metric_frequency=command_parser_arguments.snapshot_wait_time) + +# Make sure nothing failed +if (data_snapshot.abort_due_to_internal_error == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again", flush=True) + exit(0) + +# Register metrics +data_snapshot.register_metric( + new_metric_name="total_cpu_usage", + new_metric_function=get_metric_total_cpu_usage, + new_metric_unit="Percent", + new_metric_alarm_threshold=70, + new_metric_reports_to_skip=1, + new_metric_alarm_severity=5) +data_snapshot.register_metric( + new_metric_name="total_memory_usage_value", + new_metric_function=get_metric_total_memory_usage_value, + new_metric_unit="Bytes") +data_snapshot.register_metric( + new_metric_name="total_memory_usage_percent", + new_metric_function=get_metric_total_memory_usage_percent, + new_metric_unit="Percent", + new_metric_alarm_threshold=70, + new_metric_reports_to_skip=0, + new_metric_alarm_severity=5) + +# Print diagnosis information +data_snapshot.output_diagnosis_information(command_parser_arguments.dependencies) + +# Make the snapshot (metrics) monitor +snapshot_monitor = SnapshotMonitor( + wrapper_data_snapshot=data_snapshot, + wrapper_metrics_wait_time=command_parser_arguments.snapshot_wait_time) + +# Make sure nothing failed +if (snapshot_monitor.had_internal_error == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again", flush=True) + exit(0) + +# Make the application monitor +application_monitor = ApplicationMonitor( + wrapper_application_path=command_parser_arguments.canary_executable, + wrapper_application_arguments=command_parser_arguments.canary_arguments, + wrapper_application_restart_on_finish=False, + data_snapshot=data_snapshot # pass the data_snapshot for printing to the log +) + +# Make sure nothing failed +if (application_monitor.error_has_occurred == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again", flush=True) + exit(0) + +# For tracking if we stopped due to a metric alarm +stopped_due_to_metric_alarm = False + +execution_sleep_time = 30 +def execution_loop(): + while True: + snapshot_monitor.monitor_loop_function( + time_passed=execution_sleep_time, psutil_process=application_monitor.application_process_psutil) + application_monitor.monitor_loop_function( + time_passed=execution_sleep_time) + + # Did a metric go into alarm? + if (snapshot_monitor.has_cut_ticket == True): + # Set that we had an 'internal error' so we go down the right code path + snapshot_monitor.had_internal_error = True + break + + # If an error has occurred or otherwise this thread needs to stop, then break the loop + if (application_monitor.error_has_occurred == True or snapshot_monitor.had_internal_error == True): + break + + time.sleep(execution_sleep_time) + + +def application_thread(): + + start_email_body = "MQTT5 Short Running Canary Wrapper has started for " + start_email_body += "\"" + command_parser_arguments.git_repo_name + "\" commit \"" + command_parser_arguments.git_hash + "\"" + start_email_body += "\nThe wrapper will run for the length the MQTT5 Canary application is set to run for, which is determined by " + start_email_body += "the arguments set. The arguments used for this run are listed below:" + start_email_body += "\n Arguments: " + command_parser_arguments.canary_arguments + snapshot_monitor.send_email(email_body=start_email_body, email_subject_text_append="Started") + + # Start the application going + snapshot_monitor.start_monitoring() + application_monitor.start_monitoring() + # Allow the snapshot monitor to cut tickets + snapshot_monitor.can_cut_ticket = True + + # Start the execution loop + execution_loop() + + # Make sure everything is stopped + snapshot_monitor.stop_monitoring() + application_monitor.stop_monitoring() + + # Track whether this counts as an error (and therefore we should cleanup accordingly) or not + wrapper_error_occurred = False + # Finished Email + send_finished_email = True + finished_email_body = "MQTT5 Short Running Canary Wrapper has stopped." + finished_email_body += "\n\n" + + # Find out why we stopped + if (snapshot_monitor.had_internal_error == True): + if (snapshot_monitor.has_cut_ticket == True): + # We do not need to cut a ticket here - it's cut by the snapshot monitor! + print ("ERROR - Snapshot monitor stopped due to metric in alarm!", flush=True) + finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" + finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) + wrapper_error_occurred = True + else: + print ("ERROR - Snapshot monitor stopped due to internal error!", flush=True) + cut_ticket_using_cloudwatch( + git_repo_name=command_parser_arguments.git_repo_name, + git_hash=command_parser_arguments.git_hash, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1", + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, + ticket_reason="Snapshot monitor stopped due to internal error", + ticket_allow_duplicates=True, + ticket_category=command_parser_arguments.ticket_category, + ticket_item=command_parser_arguments.ticket_item, + ticket_group=command_parser_arguments.ticket_group, + ticket_type=command_parser_arguments.ticket_type, + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason + + elif (application_monitor.error_has_occurred == True): + if (application_monitor.error_due_to_credentials == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again", flush=True) + wrapper_error_occurred = True + send_finished_email = False + else: + # Is the error something in the canary failed? + if (application_monitor.error_code != 0): + cut_ticket_using_cloudwatch( + git_repo_name=command_parser_arguments.git_repo_name, + git_hash=command_parser_arguments.git_hash, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1", + ticket_description="The Short Running Canary exited with a non-zero exit code! This likely means something in the canary failed.", + ticket_reason="The Short Running Canary exited with a non-zero exit code", + ticket_allow_duplicates=True, + ticket_category=command_parser_arguments.ticket_category, + ticket_item=command_parser_arguments.ticket_item, + ticket_group=command_parser_arguments.ticket_group, + ticket_type=command_parser_arguments.ticket_type, + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code! This means something in the Canary application itself failed" + else: + print ("INFO - Stopping application. No error has occurred, application has stopped normally", flush=True) + finished_email_body += "Short Running Canary finished successfully and run without errors!" + wrapper_error_occurred = False + else: + print ("ERROR - Short Running Canary stopped due to unknown reason!", flush=True) + cut_ticket_using_cloudwatch( + git_repo_name=command_parser_arguments.git_repo_name, + git_hash=command_parser_arguments.git_hash, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1", + ticket_description="The Short Running Canary stopped for an unknown reason!", + ticket_reason="The Short Running Canary stopped for unknown reason", + ticket_allow_duplicates=True, + ticket_category=command_parser_arguments.ticket_category, + ticket_item=command_parser_arguments.ticket_item, + ticket_group=command_parser_arguments.ticket_group, + ticket_type=command_parser_arguments.ticket_type, + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + + # Clean everything up and stop + snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) + application_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) + print ("Short Running Canary finished!", flush=True) + + finished_email_body += "\n\nYou can find the log file for this run at the following S3 location: " + finished_email_body += "https://s3.console.aws.amazon.com/s3/object/" + finished_email_body += command_parser_arguments.s3_bucket_name + finished_email_body += "?region=" + command_parser_arguments.cloudwatch_region + finished_email_body += "&prefix=" + command_parser_arguments.git_repo_name + "/" + datetime_string + "/" + if (wrapper_error_occurred == True): + finished_email_body += "Failed_Logs/" + finished_email_body += command_parser_arguments.git_hash + ".log" + if (command_parser_arguments.codebuild_log_path != ""): + print ("\n Codebuild log path: " + command_parser_arguments.codebuild_log_path + "\n") + + # Send the finish email + if (send_finished_email == True): + if (wrapper_error_occurred == True): + snapshot_monitor.send_email(email_body=finished_email_body, email_subject_text_append="Had an error") + else: + snapshot_monitor.send_email(email_body=finished_email_body, email_subject_text_append="Finished") + + exit (application_monitor.error_code) + + +# Start the application! +application_thread() diff --git a/codebuild/CanaryWrapper_24_7.py b/codebuild/CanaryWrapper_24_7.py new file mode 100644 index 00000000..d4fa3a0c --- /dev/null +++ b/codebuild/CanaryWrapper_24_7.py @@ -0,0 +1,392 @@ +# Python wrapper script for collecting Canary metrics, setting up alarms, reporting metrics to Cloudwatch, +# checking the alarms to ensure everything is correct at the end of the run, and checking for new +# builds in S3, downloading them, and launching them if they exist (24/7 operation) +# +# Will only stop running if the Canary application itself has an issue - in which case it Canary application will +# need to be fixed and then the wrapper script restarted + +# Needs to be installed prior to running +# Part of standard packages in Python 3.4+ +import argparse +import time +# Dependencies in project folder +from CanaryWrapper_Classes import * +from CanaryWrapper_MetricFunctions import * + +# TODO - Using subprocess may not work on Windows for starting/stopping the application thread. +# Canary will likely be running on Linux, so it's probably okay, but need to confirm/check at some point.... +# ================================================================================ +# Code for command line argument parsing + +command_parser = argparse.ArgumentParser("CanaryWrapper_24_7") +command_parser.add_argument("--canary_executable", type=str, required=True, + help="The path to the canary executable") +command_parser.add_argument("--canary_arguments", type=str, default="", + help="The arguments to pass/launch the canary executable with") +command_parser.add_argument("--s3_bucket_name", type=str, default="canary-wrapper-folder", + help="(OPTIONAL, default=canary-wrapper-folder) The name of the S3 bucket where success logs will be stored") +command_parser.add_argument("--s3_bucket_application", type=str, required=True, + help="(OPTIONAL, default=canary-wrapper-folder) The S3 URL to monitor for changes MINUS the bucket name") +command_parser.add_argument("--s3_bucket_application_in_zip", type=str, required=False, default="", + help="(OPTIONAL, default="") The file path in the zip folder where the application is stored. Will be ignored if set to empty string") +command_parser.add_argument("--lambda_name", type=str, default="iot-send-email-lambda", + help="(OPTIONAL, default='CanarySendEmailLambda') The name of the Lambda used to send emails") +command_parser_arguments = command_parser.parse_args() + +# ================================================================================ +# Global variables that both threads use to communicate. +# NOTE - These should likely be replaced with futures or similar for better thread safety. +# However, these variables are only either read or written to from a single thread, no +# thread should read and write to these variables. + +# The local file path (and extension) of the Canary application that the wrapper will manage +# (This will also be the filename and directory used when a new file is detected in S3) +# [THIS IS READ ONLY] +canary_local_application_path = command_parser_arguments.canary_executable +if (canary_local_application_path == ""): + print ("ERROR - required canary_executable is empty!") + exit (1) # cannot run without a canary executable +# This is the arguments passed to the local file path when starting +# [THIS IS READ ONLY] +canary_local_application_arguments = command_parser_arguments.canary_arguments +# The "Git Hash" to use for metrics and dimensions +# [THIS IS READ ONLY] +canary_local_git_hash_stub = "Canary" +# The "Git Repo" name to use for metrics and dimensions. Is hard-coded since this is a 24/7 canary that should only run for MQTT +# [THIS IS READ ONLY] +canary_local_git_repo_stub = "MQTT5_24_7" +# The Fixed Namespace name for the Canary +# [THIS IS READ ONLY] +canary_local_git_fixed_namespace = "MQTT5_24_7_Canary" +# The S3 bucket name to monitor for the application +# [THIS IS READ ONLY] +canary_s3_bucket_name = command_parser_arguments.s3_bucket_name +if (canary_s3_bucket_name == ""): + canary_s3_bucket_name = "canary-wrapper-folder" +# The file in the S3 bucket to monitor (The application filepath and file. Example: "canary/canary_application.exe") +# [THIS IS READ ONLY] +canary_s3_bucket_application_path = command_parser_arguments.s3_bucket_application +if (canary_s3_bucket_application_path == ""): + print ("ERROR - required s3_bucket_application is empty!") + exit (1) # cannot run without a s3_bucket_application to monitor +# The location of the file in the S3 zip, if the S3 file being monitored is a zip +# (THIS IS READ ONLY) +canary_s3_bucket_application_path_zip = command_parser_arguments.s3_bucket_application_in_zip +if (canary_s3_bucket_application_path_zip == ""): + canary_s3_bucket_application_path_zip = None +# The name of the email lambda. If an empty string is set, it defaults to 'iot-send-email-lambda' +if (command_parser_arguments.lambda_name == ""): + command_parser_arguments.lambda_name = "iot-send-email-lambda" +# The region the canary is running in +# (THIS IS READ ONLY) +canary_region_stub = "us-east-1" + +# How long (in seconds) to wait before gathering metrics and pushing them to Cloudwatch +canary_metrics_wait_time = 600 # 10 minutes +# How long (in seconds) to run the Application thread loop. Should be shorter or equal to the Canary Metrics time +canary_application_loop_wait_time = 300 # 5 minutes + +# For testing - set both to 30 seconds +# canary_metrics_wait_time = 30 +# canary_application_loop_wait_time = 30 + +# ================================================================================ + +# Make the snapshot class +data_snapshot = DataSnapshot( + git_hash=canary_local_git_hash_stub, + git_repo_name=canary_local_git_repo_stub, + git_hash_as_namespace=False, + datetime_string=None, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + output_log_filepath="output.txt", + output_to_console=True, + cloudwatch_region=canary_region_stub, + cloudwatch_make_dashboard=True, + cloudwatch_teardown_alarms_on_complete=True, + cloudwatch_teardown_dashboard_on_complete=False, + s3_bucket_name=canary_s3_bucket_name, + s3_bucket_upload_on_complete=True, + lambda_name=command_parser_arguments.lambda_name, + metric_frequency=canary_metrics_wait_time) + +# Make sure nothing failed +if (data_snapshot.abort_due_to_internal_error == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") + exit(0) + +# Register metrics +data_snapshot.register_metric( + new_metric_name="total_cpu_usage", + new_metric_function=get_metric_total_cpu_usage, + new_metric_unit="Percent", + new_metric_alarm_threshold=70, + new_metric_reports_to_skip=1, + new_metric_alarm_severity=5) +data_snapshot.register_metric( + new_metric_name="total_memory_usage_value", + new_metric_function=get_metric_total_memory_usage_value, + new_metric_unit="Bytes") +data_snapshot.register_metric( + new_metric_name="total_memory_usage_percent", + new_metric_function=get_metric_total_memory_usage_percent, + new_metric_unit="Percent", + new_metric_alarm_threshold=70, + new_metric_reports_to_skip=0, + new_metric_alarm_severity=5) + +data_snapshot.register_dashboard_widget("Process CPU Usage - Percentage", ["total_cpu_usage"], 60) +data_snapshot.register_dashboard_widget("Process Memory Usage - Percentage", ["total_memory_usage_percent"], 60) + +# Print diagnosis information +data_snapshot.output_diagnosis_information("24/7 Canary cannot show dependencies!") + +# Make the S3 class +s3_monitor = S3Monitor( + s3_bucket_name=canary_s3_bucket_name, + s3_file_name=canary_s3_bucket_application_path, + s3_file_name_in_zip=canary_s3_bucket_application_path_zip, + canary_local_application_path=canary_local_application_path, + data_snapshot=data_snapshot) + +if (s3_monitor.had_internal_error == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") + exit(0) + +# Make the snapshot (metrics) monitor +snapshot_monitor = SnapshotMonitor( + wrapper_data_snapshot=data_snapshot, + wrapper_metrics_wait_time=canary_metrics_wait_time) + +# Make sure nothing failed +if (snapshot_monitor.had_internal_error == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") + exit(0) + +# Make the application monitor +application_monitor = ApplicationMonitor( + wrapper_application_path=canary_local_application_path, + wrapper_application_arguments=canary_local_application_arguments, + wrapper_application_restart_on_finish=True, + data_snapshot=data_snapshot) + +# Make sure nothing failed +if (application_monitor.error_has_occurred == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") + exit(0) + +# For tracking if we stopped due to a metric alarm +stopped_due_to_metric_alarm = False + +def execution_loop(): + while True: + s3_monitor.monitor_loop_function(time_passed=canary_application_loop_wait_time) + + # Is there an error? + if (s3_monitor.had_internal_error == True): + print ("[Debug] S3 monitor had an internal error!") + break + + # Is there a new file? + if (s3_monitor.s3_file_needs_replacing == True): + # Stop the application + print ("[Debug] Stopping application monitor...") + application_monitor.stop_monitoring() + print ("[Debug] Getting S3 file...") + s3_monitor.replace_current_file_for_new_file() + # Start the application + print ("[Debug] Starting application monitor...") + application_monitor.start_monitoring() + # Allow the snapshot monitor to cut a ticket + snapshot_monitor.can_cut_ticket = True + + snapshot_monitor.monitor_loop_function( + time_passed=canary_application_loop_wait_time, psutil_process=application_monitor.application_process_psutil) + application_monitor.monitor_loop_function( + time_passed=canary_application_loop_wait_time) + + # Did a metric go into alarm? + if (snapshot_monitor.has_cut_ticket == True): + # Do not allow it to cut anymore tickets until it gets a new build + snapshot_monitor.can_cut_ticket = False + + # If an error has occurred or otherwise this thread needs to stop, then break the loop + if (application_monitor.error_has_occurred == True or snapshot_monitor.had_internal_error == True): + if (application_monitor.error_has_occurred == True): + print ("[Debug] Application monitor error occurred!") + else: + print ("[Debug] Snapshot monitor internal error ocurred!") + break + + time.sleep(canary_application_loop_wait_time) + + +def application_thread(): + # Start the application going + snapshot_monitor.start_monitoring() + application_monitor.start_monitoring() + # Allow the snapshot monitor to cut tickets + snapshot_monitor.can_cut_ticket = True + + start_email_body = "MQTT5 24/7 Canary Wrapper has started. This will run and continue to test new MQTT5 application builds as" + start_email_body += " they pass CodeBuild and are uploaded to S3." + snapshot_monitor.send_email(email_body=start_email_body, email_subject_text_append="Started") + + # Start the execution loop + execution_loop() + + # Make sure everything is stopped + snapshot_monitor.stop_monitoring() + application_monitor.stop_monitoring() + + # Track whether this counts as an error (and therefore we should cleanup accordingly) or not + wrapper_error_occurred = False + + send_finished_email = True + finished_email_body = "MQTT5 24/7 Canary Wrapper has stopped." + finished_email_body += "\n\n" + + # Find out why we stopped + # S3 Monitor + if (s3_monitor.had_internal_error == True): + if (s3_monitor.error_due_to_credentials == False): + print ("ERROR - S3 monitor stopped due to internal error!") + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + s3_monitor.internal_error_reason, + ticket_reason="S3 monitor stopped due to internal error", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=4) + finished_email_body += "Failure due to S3 monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + s3_monitor.internal_error_reason + wrapper_error_occurred = True + # Snapshot Monitor + elif (snapshot_monitor.had_internal_error == True): + if (snapshot_monitor.has_cut_ticket == True): + # We do not need to cut a ticket here - it's cut by the snapshot monitor! + print ("ERROR - Snapshot monitor stopped due to metric in alarm!") + finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" + finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) + finished_email_body += "\nNOTE - this shouldn't occur in the 24/7 Canary! If it does, then the wrapper needs adjusting." + wrapper_error_occurred = True + else: + print ("ERROR - Snapshot monitor stopped due to internal error!") + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, + ticket_reason="Snapshot monitor stopped due to internal error", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason + # Application Monitor + elif (application_monitor.error_has_occurred == True): + if (application_monitor.error_due_to_credentials == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") + wrapper_error_occurred = True + send_finished_email = False + else: + # Is the error something in the canary failed? + if (application_monitor.error_code != 0): + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary exited with a non-zero exit code! This likely means something in the canary failed.", + ticket_reason="The 24/7 Canary exited with a non-zero exit code", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code!" + finished_email_body += " This means something in the Canary application itself failed" + else: + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary exited with a zero exit code but did not restart!", + ticket_reason="The 24/7 Canary exited with a zero exit code but did not restart", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application stopping and not automatically restarting!" + finished_email_body += " This shouldn't occur and means something is wrong with the Canary wrapper!" + # Other + else: + print ("ERROR - 24/7 Canary stopped due to unknown reason!") + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary stopped for an unknown reason!", + ticket_reason="The 24/7 Canary stopped for unknown reason", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + + # Clean everything up and stop + snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) + application_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) + print ("24/7 Canary finished!") + + finished_email_body += "\n\nYou can find the log file for this run at the following S3 location: " + finished_email_body += "https://s3.console.aws.amazon.com/s3/object/" + finished_email_body += command_parser_arguments.s3_bucket_name + finished_email_body += "?region=" + canary_region_stub + finished_email_body += "&prefix=" + canary_local_git_repo_stub + "/" + if (wrapper_error_occurred == True): + finished_email_body += "Failed_Logs/" + finished_email_body += canary_local_git_hash_stub + ".log" + # Send the finish email + if (send_finished_email == True): + if (wrapper_error_occurred == True): + snapshot_monitor.send_email(email_body=finished_email_body, email_subject_text_append="Had an error") + else: + snapshot_monitor.send_email(email_body=finished_email_body, email_subject_text_append="Finished") + + exit (-1) + + +# Start the application! +application_thread() diff --git a/codebuild/CanaryWrapper_Classes.py b/codebuild/CanaryWrapper_Classes.py new file mode 100644 index 00000000..c31c0d5d --- /dev/null +++ b/codebuild/CanaryWrapper_Classes.py @@ -0,0 +1,1270 @@ +# Contains all of the classes that are shared across both the Canary Wrapper and the Persistent Canary Wrapper scripts +# If a class can/is reused, then it should be in this file. + +# Needs to be installed prior to running +import boto3 +import psutil +# Part of standard packages in Python 3.4+ +import time +import os +import json +import subprocess +import zipfile +import datetime + +# ================================================================================ + +# Class that holds metric data and has a few utility functions for getting that data in a format we can use for Cloudwatch +class DataSnapshot_Metric(): + def __init__(self, metric_name, metric_function, metric_dimensions=[], + metric_unit="None", metric_alarm_threshold=None, metric_alarm_severity=6, + git_hash="", git_repo_name="", reports_to_skip=0): + self.metric_name = metric_name + self.metric_function = metric_function + self.metric_dimensions = metric_dimensions + self.metric_unit = metric_unit + self.metric_alarm_threshold = metric_alarm_threshold + self.metric_alarm_name = self.metric_name + "-" + git_repo_name + "-" + git_hash + self.metric_alarm_description = 'Alarm for metric "' + self.metric_name + '" - git hash: ' + git_hash + self.metric_value = None + self.reports_to_skip = reports_to_skip + self.metric_alarm_severity = metric_alarm_severity + + # Gets the latest metric value from the metric_function callback + def get_metric_value(self, psutil_process : psutil.Process): + if not self.metric_function is None: + self.metric_value = self.metric_function(psutil_process) + return self.metric_value + + # Returns the data needed to send to Cloudwatch when posting metrics + def get_metric_cloudwatch_dictionary(self): + if (self.reports_to_skip > 0): + self.reports_to_skip -= 1 + return None # skips sending to Cloudwatch + + if (self.metric_value == None): + return None # skips sending to Cloudwatch + + return { + "MetricName": self.metric_name, + "Dimensions": self.metric_dimensions, + "Value": self.metric_value, + "Unit": self.metric_unit + } + +class DataSnapshot_Dashboard_Widget(): + def __init__(self, widget_name, metric_namespace, metric_dimension, cloudwatch_region="us-east-1", widget_period=60) -> None: + self.metric_list = [] + self.region = cloudwatch_region + self.widget_name = widget_name + self.metric_namespace = metric_namespace + self.metric_dimension = metric_dimension + self.widget_period = widget_period + + def add_metric_to_widget(self, new_metric_name): + try: + self.metric_list.append(new_metric_name) + except Exception as e: + print ("[DataSnapshot_Dashboard] ERROR - could not add metric to dashboard widget due to exception!") + print ("[DataSnapshot_Dashboard] Exception: " + str(e)) + + def remove_metric_from_widget(self, existing_metric_name): + try: + self.metric_list.remove(existing_metric_name) + except Exception as e: + print ("[DataSnapshot_Dashboard] ERROR - could not remove metric from dashboard widget due to exception!") + print ("[DataSnapshot_Dashboard] Exception: " + str(e)) + + def get_widget_dictionary(self): + metric_list_json = [] + for metric_name in self.metric_list: + metric_list_json.append([self.metric_namespace, metric_name, self.metric_dimension, metric_name]) + + return { + "type":"metric", + "properties" : { + "metrics" : metric_list_json, + "region": self.region, + "title": self.widget_name, + "period": self.widget_period, + }, + "width": 14, + "height": 10 + } + +# ================================================================================ + +# Class that keeps track of the metrics registered, sets up Cloudwatch and S3, and sends periodic reports +# Is the backbone of the reporting operation +class DataSnapshot(): + def __init__(self, + git_hash=None, + git_repo_name=None, + git_hash_as_namespace=False, + git_fixed_namespace_text="mqtt5_canary", + datetime_string=None, + output_log_filepath=None, + output_to_console=True, + cloudwatch_region="us-east-1", + cloudwatch_make_dashboard=False, + cloudwatch_teardown_alarms_on_complete=True, + cloudwatch_teardown_dashboard_on_complete=True, + s3_bucket_name="canary-wrapper-bucket", + s3_bucket_upload_on_complete=True, + lambda_name="CanarySendEmailLambda", + metric_frequency=None): + + # Setting initial values + # ================== + self.first_metric_call = True + self.metrics = [] + self.metrics_numbers = [] + self.metric_report_number = 0 + self.metric_report_non_zero_count = 4 + + # Needed so we can initialize Cloudwatch alarms, etc, outside of the init function + # but before we start sending data. + # This boolean tracks whether we have done the post-initialization prior to sending the first report. + self.perform_final_initialization = True + + # Watched by the thread creating the snapshot. Will cause the thread(s) to abort and return an error. + self.abort_due_to_internal_error = False + self.abort_due_to_internal_error_reason = "" + self.abort_due_to_internal_error_due_to_credentials = False + + self.git_hash = None + self.git_repo_name = None + self.git_hash_as_namespace = git_hash_as_namespace + self.git_fixed_namespace_text = git_fixed_namespace_text + self.git_metric_namespace = None + + self.cloudwatch_region = cloudwatch_region + self.cloudwatch_client = None + self.cloudwatch_make_dashboard = cloudwatch_make_dashboard + self.cloudwatch_teardown_alarms_on_complete = cloudwatch_teardown_alarms_on_complete + self.cloudwatch_teardown_dashboard_on_complete = cloudwatch_teardown_dashboard_on_complete + self.cloudwatch_dashboard_name = "" + self.cloudwatch_dashboard_widgets = [] + + self.s3_bucket_name = s3_bucket_name + self.s3_client = None + self.s3_bucket_upload_on_complete = s3_bucket_upload_on_complete + + self.output_to_file_filepath = output_log_filepath + self.output_to_file = False + self.output_file = None + self.output_to_console = output_to_console + + self.lambda_client = None + self.lambda_name = lambda_name + + self.datetime_string = datetime_string + self.metric_frequency = metric_frequency + # ================== + + # Check for valid credentials + # ================== + try: + tmp_sts_client = boto3.client('sts') + tmp_sts_client.get_caller_identity() + except Exception as e: + print ("[DataSnapshot] ERROR - AWS credentials are NOT valid!") + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "AWS credentials are NOT valid!" + self.abort_due_to_internal_error_due_to_credentials = True + return + # ================== + + # Git related stuff + # ================== + if (git_hash == None or git_repo_name == None): + print("[DataSnapshot] ERROR - a Git hash and repository name are REQUIRED for the canary wrapper to run!") + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "No Git hash and repository passed!" + return + + self.git_hash = git_hash + self.git_repo_name = git_repo_name + + if (self.git_hash_as_namespace == False): + self.git_metric_namespace = self.git_fixed_namespace_text + else: + if (self.datetime_string == None): + git_namespace_prepend_text = self.git_repo_name + "-" + self.git_hash + else: + git_namespace_prepend_text = self.git_repo_name + "/" + self.datetime_string + "-" + self.git_hash + self.git_metric_namespace = git_namespace_prepend_text + # ================== + + # Cloudwatch related stuff + # ================== + try: + self.cloudwatch_client = boto3.client('cloudwatch', self.cloudwatch_region) + self.cloudwatch_dashboard_name = self.git_metric_namespace + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not make Cloudwatch client due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.cloudwatch_client = None + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not make Cloudwatch client!" + return + # ================== + + # S3 related stuff + # ================== + try: + self.s3_client = boto3.client("s3") + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not make S3 client due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.s3_client = None + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not make S3 client!" + return + # ================== + + # Lambda related stuff + # ================== + try: + self.lambda_client = boto3.client("lambda", self.cloudwatch_region) + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not make Lambda client due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.lambda_client = None + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not make Lambda client!" + return + # ================== + + # File output (logs) related stuff + # ================== + if (not output_log_filepath is None): + self.output_to_file = True + self.output_file = open(self.output_to_file_filepath, "w") + else: + self.output_to_file = False + self.output_file = None + # ================== + + self.print_message("[DataSnapshot] Data snapshot created!") + + # Cleans the class - closing any files, removing alarms, and sending data to S3. + # Should be called at the end when you are totally finished shadowing metrics + def cleanup(self, error_occurred=False): + if (self.s3_bucket_upload_on_complete == True): + self.export_result_to_s3_bucket(copy_output_log=True, log_is_error=error_occurred) + + self._cleanup_cloudwatch_alarms() + if (self.cloudwatch_make_dashboard == True): + self._cleanup_cloudwatch_dashboard() + + self.print_message("[DataSnapshot] Data snapshot cleaned!") + + if (self.output_file is not None): + self.output_file.close() + self.output_file = None + + # Utility function for printing messages + def print_message(self, message): + if self.output_to_file == True: + self.output_file.write(message + "\n") + if self.output_to_console == True: + print(message, flush=True) + + # Utility function - adds the metric alarms to Cloudwatch. We do run this right before the first + # collection of metrics so we can register metrics before we initialize Cloudwatch + def _init_cloudwatch_pre_first_run(self): + for metric in self.metrics: + if (not metric.metric_alarm_threshold is None): + self._add_cloudwatch_metric_alarm(metric) + + if (self.cloudwatch_make_dashboard == True): + self._init_cloudwatch_pre_first_run_dashboard() + + # Utility function - adds the Cloudwatch Dashboard for the currently running data snapshot + def _init_cloudwatch_pre_first_run_dashboard(self): + try: + # Remove the old dashboard if it exists before adding a new one + self._cleanup_cloudwatch_dashboard() + + new_dashboard_widgets_array = [] + for widget in self.cloudwatch_dashboard_widgets: + new_dashboard_widgets_array.append(widget.get_widget_dictionary()) + + new_dashboard_body = { + "start": "-PT1H", + "widgets": new_dashboard_widgets_array, + } + new_dashboard_body_json = json.dumps(new_dashboard_body) + + self.cloudwatch_client.put_dashboard( + DashboardName=self.cloudwatch_dashboard_name, + DashboardBody= new_dashboard_body_json) + self.print_message("[DataSnapshot] Added Cloudwatch dashboard successfully") + except Exception as e: + self.print_message("[DataSnapshot] ERROR - Cloudwatch client could not make dashboard due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Cloudwatch client could not make dashboard due to exception" + return + + # Utility function - The function that adds each individual metric alarm. + def _add_cloudwatch_metric_alarm(self, metric): + if self.cloudwatch_client is None: + self.print_message("[DataSnapshot] ERROR - Cloudwatch client not setup. Cannot register alarm") + return + + try: + self.cloudwatch_client.put_metric_alarm( + AlarmName=metric.metric_alarm_name, + AlarmDescription=metric.metric_alarm_description, + MetricName=metric.metric_name, + Namespace=self.git_metric_namespace, + Statistic="Maximum", + Dimensions=metric.metric_dimensions, + Period=60, # How long (in seconds) is an evaluation period? + EvaluationPeriods=120, # How many periods does it need to be invalid for? + DatapointsToAlarm=1, # How many data points need to be invalid? + Threshold=metric.metric_alarm_threshold, + ComparisonOperator="GreaterThanOrEqualToThreshold", + ) + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not register alarm for metric due to exception: " + metric.metric_name) + self.print_message("[DataSnapshot] Exception: " + str(e)) + + # Utility function - removes all the Cloudwatch alarms for the metrics + def _cleanup_cloudwatch_alarms(self): + if (self.cloudwatch_teardown_alarms_on_complete == True): + try: + for metric in self.metrics: + if (not metric.metric_alarm_threshold is None): + self.cloudwatch_client.delete_alarms(AlarmNames=[metric.metric_alarm_name]) + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not delete alarms due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + + # Utility function - removes all Cloudwatch dashboards created + def _cleanup_cloudwatch_dashboard(self): + if (self.cloudwatch_teardown_dashboard_on_complete == True): + try: + self.cloudwatch_client.delete_dashboards(DashboardNames=[self.cloudwatch_dashboard_name]) + self.print_message("[DataSnapshot] Cloudwatch Dashboards deleted successfully!") + except Exception as e: + self.print_message("[DataSnapshot] ERROR - dashboard cleaning function failed due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Cloudwatch dashboard cleaning function failed due to exception" + return + + # Returns the results of the metric alarms. Will return a list containing tuples with the following structure: + # [Boolean (False = the alarm is in the ALARM state), String (Name of the alarm that is in the ALARM state), int (severity of alarm)] + # Currently this function will only return a list of failed alarms, so if the returned list is empty, then it means all + # alarms did not get to the ALARM state in Cloudwatch for the registered metrics + def get_cloudwatch_alarm_results(self): + return self._check_cloudwatch_alarm_states() + + # Utility function - collects the metric alarm results and returns them in a list. + def _check_cloudwatch_alarm_states(self): + return_result_list = [] + + tmp = None + for metric in self.metrics: + tmp = self._check_cloudwatch_alarm_state_metric(metric) + if (tmp[1] != None): + # Do not cut a ticket for the "Alive_Alarm" that we use to check if the Canary is running + if ("Alive_Alarm" in tmp[1] == False): + if (tmp[0] != True): + return_result_list.append(tmp) + + return return_result_list + + # Utility function - checks each individual alarm and returns a tuple with the following format: + # [Boolean (False if the alarm is in the ALARM state, otherwise it is true), String (name of the alarm), Int (severity of alarm)] + def _check_cloudwatch_alarm_state_metric(self, metric): + alarms_response = self.cloudwatch_client.describe_alarms_for_metric( + MetricName=metric.metric_name, + Namespace=self.git_metric_namespace, + Dimensions=metric.metric_dimensions) + + return_result = [True, None, metric.metric_alarm_severity] + + for metric_alarm_dict in alarms_response["MetricAlarms"]: + if metric_alarm_dict["StateValue"] == "ALARM": + return_result[0] = False + return_result[1] = metric_alarm_dict["AlarmName"] + break + + return return_result + + # Exports a file with the same name as the commit Git hash to an S3 bucket in a folder with the Git repo name. + # By default, this file will only contain the Git hash. + # If copy_output_log is true, then the output log will be copied into this file, which may be useful for debugging. + def export_result_to_s3_bucket(self, copy_output_log=False, log_is_error=False): + if (self.s3_client is None): + self.print_message("[DataSnapshot] ERROR - No S3 client initialized! Cannot send log to S3") + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "S3 client not initialized and therefore cannot send log to S3" + return + + s3_file = open(self.git_hash + ".log", "w") + s3_file.write(self.git_hash) + + # Might be useful for debugging? + if (copy_output_log == True and self.output_to_file == True): + # Are we still writing? If so, then we need to close the file first so everything is written to it + is_output_file_open_previously = False + if (self.output_file != None): + self.output_file.close() + is_output_file_open_previously = True + self.output_file = open(self.output_to_file_filepath, "r") + + s3_file.write("\n\nOUTPUT LOG\n") + s3_file.write("==========================================================================================\n") + output_file_lines = self.output_file.readlines() + for line in output_file_lines: + s3_file.write(line) + + self.output_file.close() + + # If we were writing to the output previously, then we need to open in RW mode so we can continue to write to it + if (is_output_file_open_previously == True): + self.output_to_file = open(self.output_to_file_filepath, "a") + + s3_file.close() + + # Upload to S3 + try: + if (log_is_error == False): + if (self.datetime_string == None): + self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/" + self.git_hash + ".log") + else: + self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/" + self.datetime_string + "/" + self.git_hash + ".log") + else: + if (self.datetime_string == None): + self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/Failed_Logs/" + self.git_hash + ".log") + else: + self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/Failed_Logs/" + self.datetime_string + "/" + self.git_hash + ".log") + self.print_message("[DataSnapshot] Uploaded to S3!") + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not upload to S3 due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "S3 client had exception and therefore could not upload log!" + os.remove(self.git_hash + ".log") + return + + # Delete the file when finished + os.remove(self.git_hash + ".log") + + # Sends an email via a special lambda. The payload has to contain a message and a subject + # * (REQUIRED) message is the message you want to send in the body of the email + # * (REQUIRED) subject is the subject that the email will be sent with + def lambda_send_email(self, message, subject): + + payload = {"Message":message, "Subject":subject} + payload_string = json.dumps(payload) + + try: + self.lambda_client.invoke( + FunctionName=self.lambda_name, + InvocationType="Event", + ClientContext="MQTT Wrapper Script", + Payload=payload_string + ) + except Exception as e: + self.print_message("[DataSnapshot] ERROR - could not send email via Lambda due to exception!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Lambda email function had an exception!" + return + + # Registers a metric to be polled by the Snapshot. + # * (REQUIRED) new_metric_name is the name of the metric. Cloudwatch will use this name + # * (REQUIRED) new_metric_function is expected to be a pointer to a Python function and will not work if you pass a value/object + # * (OPTIONAL) new_metric_unit is the metric unit. There is a list of possible metric unit types on the Boto3 documentation for Cloudwatch + # * (OPTIONAL) new_metric_alarm_threshold is the value that the metric has to exceed in order to be registered as an alarm + # * (OPTIONAL) new_reports_to_skip is the number of reports this metric will return nothing, but will get it's value. + # * Useful for CPU calculations that require deltas + # * (OPTIONAL) new_metric_alarm_severity is the severity of the ticket if this alarm is triggered. A severity of 6+ means no ticket. + def register_metric(self, new_metric_name, new_metric_function, new_metric_unit="None", + new_metric_alarm_threshold=None, new_metric_reports_to_skip=0, new_metric_alarm_severity=6): + + new_metric_dimensions = [] + + if (self.git_hash_as_namespace == False): + git_namespace_prepend_text = self.git_repo_name + "-" + self.git_hash + new_metric_dimensions.append( + {"Name": git_namespace_prepend_text, "Value": new_metric_name}) + else: + new_metric_dimensions.append( + {"Name": "System_Metrics", "Value": new_metric_name}) + + new_metric = DataSnapshot_Metric( + metric_name=new_metric_name, + metric_function=new_metric_function, + metric_dimensions=new_metric_dimensions, + metric_unit=new_metric_unit, + metric_alarm_threshold=new_metric_alarm_threshold, + metric_alarm_severity=new_metric_alarm_severity, + git_hash=self.git_hash, + git_repo_name=self.git_repo_name, + reports_to_skip=new_metric_reports_to_skip + ) + self.metrics.append(new_metric) + # append an empty list so we can track it's metrics over time + self.metrics_numbers.append([]) + + def register_dashboard_widget(self, new_widget_name, metrics_to_add=[], new_widget_period=60): + + # We need to know what metric dimension to get the metric(s) from + metric_dimension_string = "" + if (self.git_hash_as_namespace == False): + metric_dimension_string = self.git_repo_name + "-" + self.git_hash + else: + metric_dimension_string = "System_Metrics" + + widget = self._find_cloudwatch_widget(name=new_widget_name) + if (widget == None): + widget = DataSnapshot_Dashboard_Widget( + widget_name=new_widget_name, metric_namespace=self.git_metric_namespace, + metric_dimension=metric_dimension_string, + cloudwatch_region=self.cloudwatch_region, + widget_period=new_widget_period) + self.cloudwatch_dashboard_widgets.append(widget) + + for metric in metrics_to_add: + self.register_metric_to_dashboard_widget(widget_name=new_widget_name, metric_name=metric) + + def register_metric_to_dashboard_widget(self, widget_name, metric_name, widget=None): + if widget is None: + widget = self._find_cloudwatch_widget(name=widget_name) + if widget is None: + print ("[DataSnapshot] ERROR - could not find widget with name: " + widget_name, flush=True) + return + + # Adjust metric name so it has the git hash, repo, etc + metric_name_formatted = metric_name + + widget.add_metric_to_widget(new_metric_name=metric_name_formatted) + return + + def remove_metric_from_dashboard_widget(self, widget_name, metric_name, widget=None): + if widget is None: + widget = self._find_cloudwatch_widget(name=widget_name) + if widget is None: + print ("[DataSnapshot] ERROR - could not find widget with name: " + widget_name, flush=True) + return + widget.remove_metric_from_widget(existing_metric_name=metric_name) + return + + def _find_cloudwatch_widget(self, name): + result = None + for widget in self.cloudwatch_dashboard_widgets: + if widget.widget_name == name: + return widget + return result + + # Prints the metrics to the console + def export_metrics_console(self): + datetime_now = datetime.datetime.now() + datetime_string = datetime_now.strftime("%d-%m-%Y/%H-%M-%S") + + self.print_message("\n[DataSnapshot] Metric report: " + str(self.metric_report_number) + " (" + datetime_string + ")") + for metric in self.metrics: + self.print_message(" " + metric.metric_name + + " - value: " + str(metric.metric_value)) + self.print_message("") + + # Sends all registered metrics to Cloudwatch. + # Does NOT need to called on loop. Call post_metrics on loop to send all the metrics as expected. + # This is just the Cloudwatch part of that loop. + def export_metrics_cloudwatch(self): + if (self.cloudwatch_client == None): + self.print_message("[DataSnapshot] Error - cannot export Cloudwatch metrics! Cloudwatch was not initiallized.") + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not export Cloudwatch metrics due to no Cloudwatch client initialized!" + return + + self.print_message("[DataSnapshot] Preparing to send to Cloudwatch...") + metrics_data = [] + metric_data_tmp = None + for metric in self.metrics: + metric_data_tmp = metric.get_metric_cloudwatch_dictionary() + if (not metric_data_tmp is None): + metrics_data.append(metric_data_tmp) + + if (len(metrics_data) == 0): + self.print_message("[DataSnapshot] INFO - no metric data to send. Skipping...") + return + + try: + self.cloudwatch_client.put_metric_data( + Namespace=self.git_metric_namespace, + MetricData=metrics_data) + self.print_message("[DataSnapshot] Metrics sent to Cloudwatch.") + except Exception as e: + self.print_message("[DataSnapshot] Error - something when wrong posting cloudwatch metrics!") + self.print_message("[DataSnapshot] Exception: " + str(e)) + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not export Cloudwatch metrics due to exception in Cloudwatch client!" + return + + # Call this at a set interval to post the metrics to Cloudwatch, etc. + # This is the function you want to call repeatedly after you have everything setup. + def post_metrics(self, psutil_process : psutil.Process): + if (self.perform_final_initialization == True): + self.perform_final_initialization = False + self._init_cloudwatch_pre_first_run() + + # Update the metric values internally + for i in range(0, len(self.metrics)): + metric_value = self.metrics[i].get_metric_value(psutil_process) + self.metrics_numbers[i].insert(0, metric_value) + + # Only keep the last metric_report_non_zero_count results + if (len(self.metrics_numbers[i]) > self.metric_report_non_zero_count): + amount_to_delete = len(self.metrics_numbers[i]) - self.metric_report_non_zero_count + del self.metrics_numbers[i][-amount_to_delete:] + # If we have metric_report_non_zero_count amount of metrics, make sure there is at least one + # non-zero. If it is all zero, then print a log so we can easily find it + if (len(self.metrics_numbers[i]) == self.metric_report_non_zero_count): + non_zero_found = False + for j in range(0, len(self.metrics_numbers[i])): + if (self.metrics_numbers[i][j] != 0.0 and self.metrics_numbers[i][j] != None): + non_zero_found = True + break + if (non_zero_found == False): + self.print_message("\n[DataSnapshot] METRIC ZERO ERROR!") + self.print_message(f"[DataSnapshot] Metric index {i} has been zero for last {self.metric_report_non_zero_count} reports!") + self.print_message("\n") + + self.metric_report_number += 1 + + self.export_metrics_console() + self.export_metrics_cloudwatch() + + def output_diagnosis_information(self, dependencies_list): + + # Print general diagnosis information + self.print_message("\n========== Canary Wrapper diagnosis information ==========") + self.print_message("\nRunning Canary for repository: " + self.git_repo_name) + self.print_message("\t Commit hash: " + self.git_hash) + + if not dependencies_list == "": + self.print_message("\nDependencies:") + dependencies_list = dependencies_list.split(";") + dependencies_list_found_hash = False + for i in range(0, len(dependencies_list)): + # There's probably a better way to do this... + if (dependencies_list_found_hash == True): + dependencies_list_found_hash = False + continue + self.print_message("* " + dependencies_list[i]) + if (i+1 < len(dependencies_list)): + self.print_message("\t Commit hash: " + dependencies_list[i+1]) + dependencies_list_found_hash = True + else: + self.print_message("\t Commit hash: Unknown") + + if (self.metric_frequency != None): + self.print_message("\nMetric Snapshot Frequency: " + str(self.metric_frequency) + " seconds") + self.print_message("\nMetrics:") + for metric in self.metrics: + self.print_message("* " + metric.metric_name) + if metric.metric_alarm_threshold is not None: + self.print_message("\t Alarm Threshold: " + str(metric.metric_alarm_threshold)) + self.print_message("\t Alarm Severity: " + str(metric.metric_alarm_severity)) + else: + self.print_message("\t No alarm set for metric.") + + self.print_message("\n") + self.print_message("==========================================================") + self.print_message("\n") + +# ================================================================================ + +class SnapshotMonitor(): + def __init__(self, wrapper_data_snapshot, wrapper_metrics_wait_time) -> None: + + self.data_snapshot = wrapper_data_snapshot + self.had_internal_error = False + self.error_due_to_credentials = False + self.internal_error_reason = "" + self.error_due_to_alarm = False + + self.can_cut_ticket = False + self.has_cut_ticket = False + + # A list of all the alarms triggered in the last check, cached for later + # NOTE - this is only the alarm names! Not the severity. This just makes it easier to process + self.cloudwatch_current_alarms_triggered = [] + + # Check for errors + if (self.data_snapshot.abort_due_to_internal_error == True): + self.had_internal_error = True + self.internal_error_reason = "Could not initialize DataSnapshot. Likely credentials are not setup!" + if (self.data_snapshot.abort_due_to_internal_error_due_to_credentials == True): + self.error_due_to_credentials = True + self.data_snapshot.cleanup() + return + + # How long to wait before posting a metric + self.metric_post_timer = 0 + self.metric_post_timer_time = wrapper_metrics_wait_time + + + def register_metric(self, new_metric_name, new_metric_function, new_metric_unit="None", new_metric_alarm_threshold=None, + new_metric_reports_to_skip=0, new_metric_alarm_severity=6): + + try: + self.data_snapshot.register_metric( + new_metric_name=new_metric_name, + new_metric_function=new_metric_function, + new_metric_unit=new_metric_unit, + new_metric_alarm_threshold=new_metric_alarm_threshold, + new_metric_reports_to_skip=new_metric_reports_to_skip, + new_metric_alarm_severity=new_metric_alarm_severity) + except Exception as e: + self.print_message("[SnaptshotMonitor] ERROR - could not register metric in data snapshot due to exception!") + self.print_message("[SnaptshotMonitor] Exception: " + str(e)) + self.had_internal_error = True + self.internal_error_reason = "Could not register metric in data snapshot due to exception" + return + + def register_dashboard_widget(self, new_widget_name, metrics_to_add=[], widget_period=60): + self.data_snapshot.register_dashboard_widget(new_widget_name=new_widget_name, metrics_to_add=metrics_to_add, new_widget_period=widget_period) + + def output_diagnosis_information(self, dependencies=""): + self.data_snapshot.output_diagnosis_information(dependencies_list=dependencies) + + def check_alarms_for_new_alarms(self, triggered_alarms): + + if len(triggered_alarms) > 0: + self.data_snapshot.print_message( + "WARNING - One or more alarms are in state of ALARM") + + old_alarms_still_active = [] + new_alarms = [] + new_alarms_highest_severity = 6 + new_alarm_found = True + new_alarm_ticket_description = "Canary has metrics in ALARM state!\n\nMetrics in alarm:\n" + + for triggered_alarm in triggered_alarms: + new_alarm_found = True + + # Is this a new alarm? + for old_alarm_name in self.cloudwatch_current_alarms_triggered: + if (old_alarm_name == triggered_alarm[1]): + new_alarm_found = False + old_alarms_still_active.append(triggered_alarm[1]) + + new_alarm_ticket_description += "* (STILL IN ALARM) " + triggered_alarm[1] + "\n" + new_alarm_ticket_description += "\tSeverity: " + str(triggered_alarm[2]) + new_alarm_ticket_description += "\n" + break + + # If it is a new alarm, then add it to our list so we can cut a new ticket + if (new_alarm_found == True): + self.data_snapshot.print_message(' (NEW) Alarm with name "' + triggered_alarm[1] + '" is in the ALARM state!') + new_alarms.append(triggered_alarm[1]) + if (triggered_alarm[2] < new_alarms_highest_severity): + new_alarms_highest_severity = triggered_alarm[2] + new_alarm_ticket_description += "* " + triggered_alarm[1] + "\n" + new_alarm_ticket_description += "\tSeverity: " + str(triggered_alarm[2]) + new_alarm_ticket_description += "\n" + + + if len(new_alarms) > 0: + if (self.can_cut_ticket == True): + cut_ticket_using_cloudwatch( + git_repo_name=self.data_snapshot.git_repo_name, + git_hash=self.data_snapshot.git_hash, + git_hash_as_namespace=False, + git_fixed_namespace_text=self.data_snapshot.git_fixed_namespace_text, + cloudwatch_region="us-east-1", + ticket_description="New metric(s) went into alarm for the Canary! Metrics in alarm: " + str(new_alarms), + ticket_reason="New metric(s) went into alarm", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_type="SDKs and Tools", + ticket_severity=4) + self.has_cut_ticket = True + + # Cache the new alarms and the old alarms + self.cloudwatch_current_alarms_triggered = old_alarms_still_active + new_alarms + + else: + self.cloudwatch_current_alarms_triggered.clear() + + + def monitor_loop_function(self, psutil_process : psutil.Process, time_passed=30): + # Check for internal errors + if (self.data_snapshot.abort_due_to_internal_error == True): + self.had_internal_error = True + self.internal_error_reason = "Data Snapshot internal error: " + self.data_snapshot.abort_due_to_internal_error_reason + return + + try: + # Poll the metric alarms + if (self.had_internal_error == False): + # Get a report of all the alarms that might have been set to an alarm state + triggered_alarms = self.data_snapshot.get_cloudwatch_alarm_results() + self.check_alarms_for_new_alarms(triggered_alarms) + except Exception as e: + self.print_message("[SnaptshotMonitor] ERROR - exception occurred checking metric alarms!") + self.print_message("[SnaptshotMonitor] (Likely session credentials expired)") + self.had_internal_error = True + self.internal_error_reason = "Exception occurred checking metric alarms! Likely session credentials expired" + return + + if (self.metric_post_timer <= 0): + if (self.had_internal_error == False): + try: + self.data_snapshot.post_metrics(psutil_process) + except Exception as e: + self.print_message("[SnaptshotMonitor] ERROR - exception occurred posting metrics!") + self.print_message("[SnaptshotMonitor] (Likely session credentials expired)") + + print (e, flush=True) + + self.had_internal_error = True + self.internal_error_reason = "Exception occurred posting metrics! Likely session credentials expired" + return + + # reset the timer + self.metric_post_timer += self.metric_post_timer_time + + # Gather and post the metrics + self.metric_post_timer -= time_passed + + + def send_email(self, email_body, email_subject_text_append=None): + if (email_subject_text_append != None): + self.data_snapshot.lambda_send_email(email_body, "Canary: " + self.data_snapshot.git_repo_name + ":" + self.data_snapshot.git_hash + " - " + email_subject_text_append) + else: + self.data_snapshot.lambda_send_email(email_body, "Canary: " + self.data_snapshot.git_repo_name + ":" + self.data_snapshot.git_hash) + + + def stop_monitoring(self): + # Stub - just added for consistency + pass + + + def start_monitoring(self): + # Stub - just added for consistency + pass + + + def restart_monitoring(self): + # Stub - just added for consistency + pass + + + def cleanup_monitor(self, error_occurred=False): + self.data_snapshot.cleanup(error_occurred=error_occurred) + + def print_message(self, message): + if (self.data_snapshot != None): + self.data_snapshot.print_message(message) + else: + print(message, flush=True) + +# ================================================================================ + +class ApplicationMonitor(): + def __init__(self, wrapper_application_path, wrapper_application_arguments, wrapper_application_restart_on_finish=True, data_snapshot=None) -> None: + self.application_process = None + self.application_process_psutil = None + self.error_has_occurred = False + self.error_due_to_credentials = False + self.error_reason = "" + self.error_code = 0 + self.wrapper_application_path = wrapper_application_path + self.wrapper_application_arguments = wrapper_application_arguments + self.wrapper_application_restart_on_finish = wrapper_application_restart_on_finish + self.data_snapshot=data_snapshot + + def start_monitoring(self): + self.print_message("[ApplicationMonitor] Starting to monitor application...") + + if (self.application_process == None): + try: + canary_command = self.wrapper_application_path + " " + self.wrapper_application_arguments + self.application_process = subprocess.Popen(canary_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8") + self.application_process_psutil = psutil.Process(self.application_process.pid) + self.print_message ("[ApplicationMonitor] Application started...") + except Exception as e: + self.print_message ("[ApplicationMonitor] ERROR - Could not launch Canary/Application due to exception!") + self.print_message ("[ApplicationMonitor] Exception: " + str(e)) + self.error_has_occurred = True + self.error_reason = "Could not launch Canary/Application due to exception" + self.error_code = 1 + return + else: + self.print_message("[ApplicationMonitor] ERROR - Monitor already has an application process! Cannot monitor two applications with one monitor class!") + + def restart_monitoring(self): + self.print_message ("[ApplicationMonitor] Restarting monitor application...") + + if (self.application_process != None): + try: + self.stop_monitoring() + self.start_monitoring() + self.print_message("[ApplicationMonitor] Restarted monitor application!") + except Exception as e: + self.print_message("[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception!") + self.print_message("[ApplicationMonitor] Exception: " + str(e)) + self.error_has_occurred = True + self.error_reason = "Could not restart Canary/Application due to exception" + self.error_code = 1 + return + else: + self.print_message("[ApplicationMonitor] ERROR - Application process restart called but process is/was not running!") + self.error_has_occurred = True + self.error_reason = "Could not restart Canary/Application due to application process not being started initially" + self.error_code = 1 + return + + + def stop_monitoring(self): + self.print_message ("[ApplicationMonitor] Stopping monitor application...") + if (not self.application_process == None): + self.application_process.terminate() + self.application_process.wait() + self.print_message ("[ApplicationMonitor] Stopped monitor application!") + + if self.application_process.stdout != None: + self.print_message("\nApplication STDOUT:\n") + self.print_message("=========================================\n") + for line in self.application_process.stdout: + self.print_message(line) + self.application_process.stdout.close() + self.print_message("\n=========================================\n") + self.application_process = None + else: + self.print_message ("[ApplicationMonitor] ERROR - cannot stop monitor application because no process is found!") + + + def monitor_loop_function(self, time_passed=30): + if (self.application_process != None): + + application_process_return_code = None + try: + application_process_return_code = self.application_process.poll() + except Exception as e: + self.print_message("[ApplicationMonitor] ERROR - exception occurred while trying to poll application status!") + self.print_message("[ApplicationMonitor] Exception: " + str(e)) + self.error_has_occurred = True + self.error_reason = "Exception when polling application status" + self.error_code = 1 + return + + # If it is not none, then the application finished + if (application_process_return_code != None): + + self.print_message("[ApplicationMonitor] Monitor application has stopped! Processing result...") + + if (application_process_return_code != 0): + self.print_message("[ApplicationMonitor] ERROR - Something Crashed in Canary/Application!") + self.print_message("[ApplicationMonitor] Error code: " + str(application_process_return_code)) + + self.error_has_occurred = True + self.error_reason = "Canary application crashed!" + self.error_code = application_process_return_code + else: + # Should we restart? + if (self.wrapper_application_restart_on_finish == True): + self.print_message("[ApplicationMonitor] NOTE - Canary finished running and is restarting...") + self.restart_monitoring() + else: + self.print_message("[ApplicationMonitor] Monitor application has stopped and monitor is not supposed to restart... Finishing...") + self.error_has_occurred = True + self.error_reason = "Canary Application Finished" + self.error_code = 0 + else: + self.print_message("[ApplicationMonitor] Monitor application is still running...") + + def cleanup_monitor(self, error_occurred=False): + pass + + def print_message(self, message): + if (self.data_snapshot != None): + self.data_snapshot.print_message(message) + else: + print(message, flush=True) + +# ================================================================================ + +class S3Monitor(): + + def __init__(self, s3_bucket_name, s3_file_name, s3_file_name_in_zip, canary_local_application_path, data_snapshot) -> None: + self.s3_client = None + self.s3_current_object_version_id = None + self.s3_current_object_last_modified = None + self.s3_bucket_name = s3_bucket_name + self.s3_file_name = s3_file_name + self.s3_file_name_only_path, self.s3_file_name_only_extension = os.path.splitext(s3_file_name) + self.data_snapshot = data_snapshot + + self.canary_local_application_path = canary_local_application_path + + self.s3_file_name_in_zip = s3_file_name_in_zip + self.s3_file_name_in_zip_only_path = None + self.s3_file_name_in_zip_only_extension = None + if (self.s3_file_name_in_zip != None): + self.s3_file_name_in_zip_only_path, self.s3_file_name_in_zip_only_extension = os.path.splitext(s3_file_name_in_zip) + + self.s3_file_needs_replacing = False + + self.had_internal_error = False + self.error_due_to_credentials = False + self.internal_error_reason = "" + + # Check for valid credentials + # ================== + try: + tmp_sts_client = boto3.client('sts') + tmp_sts_client.get_caller_identity() + except Exception as e: + self.print_message("[S3Monitor] ERROR - (S3 Check) AWS credentials are NOT valid!") + self.had_internal_error = True + self.error_due_to_credentials = True + self.internal_error_reason = "AWS credentials are NOT valid!" + return + # ================== + + try: + self.s3_client = boto3.client("s3") + except Exception as e: + self.print_message("[S3Monitor] ERROR - (S3 Check) Could not make S3 client") + self.had_internal_error = True + self.internal_error_reason = "Could not make S3 client for S3 Monitor" + return + + + def check_for_file_change(self): + try: + version_check_response = self.s3_client.list_object_versions( + Bucket=self.s3_bucket_name, + Prefix=self.s3_file_name_only_path) + if "Versions" in version_check_response: + for version in version_check_response["Versions"]: + if (version["IsLatest"] == True): + if (version["VersionId"] != self.s3_current_object_version_id or + version["LastModified"] != self.s3_current_object_last_modified): + + self.print_message("[S3Monitor] Found new version of Canary/Application in S3!") + self.print_message("[S3Monitor] Changing running Canary/Application to new one...") + + # Will be checked by thread to trigger replacing the file + self.s3_file_needs_replacing = True + + self.s3_current_object_version_id = version["VersionId"] + self.s3_current_object_last_modified = version["LastModified"] + return + + except Exception as e: + self.print_message("[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception!") + self.print_message("[S3Monitor] Exception: " + str(e)) + self.had_internal_error = True + self.internal_error_reason = "Could not check for S3 file due to exception in S3 client" + + + def replace_current_file_for_new_file(self): + try: + self.print_message("[S3Monitor] Making directory...") + if not os.path.exists("tmp"): + os.makedirs("tmp") + except Exception as e: + self.print_message ("[S3Monitor] ERROR - could not make tmp directory to place S3 file into!") + self.had_internal_error = True + self.internal_error_reason = "Could not make TMP folder for S3 file download" + return + + # Download the file + new_file_path = "tmp/new_file" + self.s3_file_name_only_extension + try: + self.print_message("[S3Monitor] Downloading file...") + s3_resource = boto3.resource("s3") + s3_resource.meta.client.download_file(self.s3_bucket_name, self.s3_file_name, new_file_path) + except Exception as e: + self.print_message("[S3Monitor] ERROR - could not download latest S3 file into TMP folder!") + self.had_internal_error = True + self.internal_error_reason = "Could not download latest S3 file into TMP folder" + return + + # Is it a zip file? + if (self.s3_file_name_in_zip != None): + self.print_message("[S3Monitor] New file is zip file. Unzipping...") + # Unzip it! + with zipfile.ZipFile(new_file_path, 'r') as zip_file: + zip_file.extractall("tmp/new_file_zip") + new_file_path = "tmp/new_file_zip/" + self.s3_file_name_in_zip_only_path + self.s3_file_name_in_zip_only_extension + + try: + # is there a file already present there? + if os.path.exists(self.canary_local_application_path) == True: + os.remove(self.canary_local_application_path) + + self.print_message("[S3Monitor] Moving file...") + os.replace(new_file_path, self.canary_local_application_path) + self.print_message("[S3Monitor] Getting execution rights...") + os.system("chmod u+x " + self.canary_local_application_path) + + except Exception as e: + self.print_message("[S3Monitor] ERROR - could not move file into local application path due to exception!") + self.print_message("[S3Monitor] Exception: " + str(e)) + self.had_internal_error = True + self.internal_error_reason = "Could not move file into local application path" + return + + self.print_message("[S3Monitor] New file downloaded and moved into correct location!") + self.s3_file_needs_replacing = False + + + def stop_monitoring(self): + # Stub - just added for consistency + pass + + + def start_monitoring(self): + # Stub - just added for consistency + pass + + + def restart_monitoring(self): + # Stub - just added for consistency + pass + + + def cleanup_monitor(self): + # Stub - just added for consistency + pass + + def monitor_loop_function(self, time_passed=30): + self.check_for_file_change() + + def print_message(self, message): + if (self.data_snapshot != None): + self.data_snapshot.print_message(message) + else: + print(message, flush=True) + +# ================================================================================ + + +# Cuts a ticket to SIM using a temporary Cloudwatch metric that is quickly created, triggered, and destroyed. +# Can be called in any thread - creates its own Cloudwatch client and any data it needs is passed in. +# +# See (https://w.amazon.com/bin/view/CloudWatchAlarms/Internal/CloudWatchAlarmsSIMTicketing) for more details +# on how the alarm is sent using Cloudwatch. +def cut_ticket_using_cloudwatch( + ticket_description="Description here!", + ticket_reason="Reason here!", + ticket_severity=5, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_allow_duplicates=False, + git_repo_name="REPO NAME", + git_hash="HASH", + git_hash_as_namespace=False, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1"): + + git_metric_namespace = "" + if (git_hash_as_namespace == False): + git_metric_namespace = git_fixed_namespace_text + else: + git_namespace_prepend_text = git_repo_name + "-" + git_hash + git_metric_namespace = git_namespace_prepend_text + + cloudwatch_client = boto3.client('cloudwatch', cloudwatch_region) + ticket_alarm_name = git_repo_name + "-" + git_hash + "-AUTO-TICKET" + + new_metric_dimensions = [] + if (git_hash_as_namespace == False): + git_namespace_prepend_text = git_repo_name + "-" + git_hash + new_metric_dimensions.append( + {"Name": git_namespace_prepend_text, "Value": ticket_alarm_name}) + else: + new_metric_dimensions.append( + {"Name": "System_Metrics", "Value": ticket_alarm_name}) + + ticket_arn = f"arn:aws:cloudwatch::cwa-internal:ticket:{ticket_severity}:{ticket_category}:{ticket_type}:{ticket_item}:{ticket_group}:" + if (ticket_allow_duplicates == True): + # use "DO-NOT-DEDUPE" so we can run the same commit again and it will cut another ticket. + ticket_arn += "DO-NOT-DEDUPE" + # In the ticket ARN, all spaces need to be replaced with + + ticket_arn = ticket_arn.replace(" ", "+") + + ticket_alarm_description = f"AUTO CUT CANARY WRAPPER TICKET\n\nREASON: {ticket_reason}\n\nDESCRIPTION: {ticket_description}\n\n" + + # Regsiter a metric alarm so it can auto-cut a ticket for us + cloudwatch_client.put_metric_alarm( + AlarmName=ticket_alarm_name, + AlarmDescription=ticket_alarm_description, + MetricName=ticket_alarm_name, + Namespace=git_metric_namespace, + Statistic="Maximum", + Dimensions=new_metric_dimensions, + Period=60, # How long (in seconds) is an evaluation period? + EvaluationPeriods=1, # How many periods does it need to be invalid for? + DatapointsToAlarm=1, # How many data points need to be invalid? + Threshold=1, + ComparisonOperator="GreaterThanOrEqualToThreshold", + # The data above does not really matter - it just needs to be valid input data. + # This is the part that tells Cloudwatch to cut the ticket + AlarmActions=[ticket_arn] + ) + + # Trigger the alarm so it cuts the ticket + try: + cloudwatch_client.set_alarm_state( + AlarmName=ticket_alarm_name, + StateValue="ALARM", + StateReason="AUTO TICKET CUT") + except Exception as e: + print ("ERROR - could not cut ticket due to exception!") + print ("Exception: " + str(e), flush=True) + return + + print("Waiting for ticket metric to trigger...", flush=True) + # Wait a little bit (2 seconds)... + time.sleep(2) + + # Remove the metric + print("Removing ticket metric...", flush=True) + cloudwatch_client.delete_alarms(AlarmNames=[ticket_alarm_name]) + + print ("Finished cutting ticket via Cloudwatch!", flush=True) + return + +# A helper function that gets the majority of the ticket information from the arguments result from argparser. +def cut_ticket_using_cloudwatch_from_args( + ticket_description="", + ticket_reason="", + ticket_severity=6, + arguments=None): + + # Do not cut a ticket for a severity of 6+ + if (ticket_severity >= 6): + return + + cut_ticket_using_cloudwatch( + ticket_description=ticket_description, + ticket_reason=ticket_reason, + ticket_severity=ticket_severity, + ticket_category=arguments.ticket_category, + ticket_type=arguments.ticket_type, + ticket_item=arguments.ticket_item, + ticket_group=arguments.ticket_group, + ticket_allow_duplicates=False, + git_repo_name=arguments.git_repo_name, + git_hash=arguments.git_hash, + git_hash_as_namespace=arguments.git_hash_as_namespace) + +# ================================================================================ diff --git a/codebuild/CanaryWrapper_MetricFunctions.py b/codebuild/CanaryWrapper_MetricFunctions.py new file mode 100644 index 00000000..a53d896d --- /dev/null +++ b/codebuild/CanaryWrapper_MetricFunctions.py @@ -0,0 +1,49 @@ +# Contains all of the metric reporting functions for the Canary Wrappers + +# Needs to be installed prior to running +import psutil + + +cache_cpu_psutil_process = None +def get_metric_total_cpu_usage(psutil_process : psutil.Process): + global cache_cpu_psutil_process + + try: + if (psutil_process == None): + print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) + return None + # We always need to skip the first CPU poll on a new process + if (cache_cpu_psutil_process != psutil_process): + psutil_process.cpu_percent(interval=None) + cache_cpu_psutil_process = psutil_process + return None + return psutil_process.cpu_percent(interval=None) + except Exception as e: + print ("ERROR - exception occurred gathering metrics!") + print ("Exception: " + str(e), flush=True) + return None + + +def get_metric_total_memory_usage_value(psutil_process : psutil.Process): + try: + if (psutil_process == None): + print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) + return None + return psutil_process.memory_info().rss + except Exception as e: + print ("ERROR - exception occurred gathering metrics!") + print ("Exception: " + str(e), flush=True) + return None + + +def get_metric_total_memory_usage_percent(psutil_process : psutil.Process): + try: + if (psutil_process == None): + print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) + return None + return psutil_process.memory_percent() + except Exception as e: + print ("ERROR - exception occurred gathering metrics!") + print ("Exception: " + str(e), flush=True) + return None + diff --git a/codebuild/mqtt-canary-test.yml b/codebuild/mqtt-canary-test.yml new file mode 100644 index 00000000..4f4bcbcc --- /dev/null +++ b/codebuild/mqtt-canary-test.yml @@ -0,0 +1,58 @@ +version: 0.2 +env: + shell: bash + variables: + CANARY_DURATION: 7200 + CANARY_THREADS: 3 + CANARY_TPS: 50 + CANARY_CLIENT_COUNT: 10 + CANARY_LOG_FILE: 'canary_log.txt' + CANARY_LOG_LEVEL: 'ERROR' + BUILDER_VERSION: v0.9.15 + BUILDER_SOURCE: releases + BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net + PACKAGE_NAME: 'aws-c-mqtt' + CANARY_TEST_EXE_PATH: build/install/bin/mqtt5canary + CANARY_SERVER_ARN: Mqtt5MosquittoSever + CANARY_BUILD_S3_DST: mqtt5-canary/s3 + +phases: + install: + commands: + # install cmake for codebuild environment. + - apt-get install build-essential libssl-dev -y -f + - apt-get update -y + - apt-get install cmake -y -f + # Install necessary lib for canary wrapper + - sudo apt-get install gcc python3-dev -y -f + - sudo apt-get install pip -y -f + - python3 -m pip install psutil + - python3 -m pip install boto3 + + build: + commands: + - export CANNARY_TEST_EXE=$CODEBUILD_SRC_DIR/$CANARY_TEST_EXE_PATH + - echo $CANNARY_TEST_EXE + - export ENDPOINT=$(aws secretsmanager get-secret-value --secret-id "$CANARY_SERVER_ARN" --query "SecretString" | cut -f2 -d":" | sed -e 's/[\\\"\}]//g') + - export S3_DST=$(aws secretsmanager get-secret-value --secret-id "$CANARY_BUILD_S3_DST" --query "SecretString" | cut -f2,3 -d":" | sed -e 's/[\\\"\}]//g') + - export GIT_HASH=$(git rev-parse HEAD) + # Build library and test + - python3 -c "from urllib.request import urlretrieve; urlretrieve('$BUILDER_HOST/$BUILDER_SOURCE/$BUILDER_VERSION/builder.pyz?run=$CODEBUILD_BUILD_ID', 'builder.pyz')" + - python3 builder.pyz build -p aws-c-mqtt + # Canary related: + # ========== + - echo run canary test through wrapper + # start canary + - python3 codebuild/CanaryWrapper.py --canary_executable $CANNARY_TEST_EXE --canary_arguments "-s ${CANARY_DURATION} -t ${CANARY_THREADS} -T ${CANARY_TPS} -C ${CANARY_CLIENT_COUNT} -l ${CANARY_LOG_FILE} -v ${CANARY_LOG_LEVEL} endpoint ${ENDPOINT}" --git_hash ${GIT_HASH} --git_repo_name $PACKAGE_NAME --codebuild_log_path $CODEBUILD_LOG_PATH + - aws s3 cp ./${CANARY_LOG_FILE} ${S3_DST}log/${GIT_HASH}/ + # upload built canary test build result to s3 bucket + - zip -r latestBuild.zip build/install + - aws s3 cp ./latestBuild.zip ${S3_DST}build/latest + # upload latest source to S3 bucket + - zip -r latestSnapshot.zip **/*(.r) + - aws s3 cp ./latestSnapshot.zip ${S3_DST}source/latest + # ========== + + post_build: + commands: + - echo Build completed on `date` diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 84701a3f..8a86c2ed 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -54,6 +54,27 @@ enum aws_mqtt_error { AWS_ERROR_MQTT_CONNECTION_DISCONNECTING, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION, AWS_ERROR_MQTT_QUEUE_FULL, + AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION, + AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION, + AWS_ERROR_MQTT5_PACKET_VALIDATION, + AWS_ERROR_MQTT5_ENCODE_FAILURE, + AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR, + AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + AWS_ERROR_MQTT5_CONNACK_TIMEOUT, + AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT, + AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + AWS_ERROR_MQTT5_CLIENT_TERMINATED, + AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, + AWS_ERROR_MQTT5_ENCODE_SIZE_UNSUPPORTED_PACKET_TYPE, + AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE, + AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS, + AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; @@ -62,6 +83,9 @@ enum aws_mqtt_log_subject { AWS_LS_MQTT_GENERAL = AWS_LOG_SUBJECT_BEGIN_RANGE(AWS_C_MQTT_PACKAGE_ID), AWS_LS_MQTT_CLIENT, AWS_LS_MQTT_TOPIC_TREE, + AWS_LS_MQTT5_GENERAL, + AWS_LS_MQTT5_CLIENT, + AWS_LS_MQTT5_CANARY, }; /** Function called on cleanup of a userdata. */ diff --git a/include/aws/mqtt/private/shared_constants.h b/include/aws/mqtt/private/shared_constants.h new file mode 100644 index 00000000..0a835942 --- /dev/null +++ b/include/aws/mqtt/private/shared_constants.h @@ -0,0 +1,18 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_SHARED_CONSTANTS_H +#define AWS_MQTT_SHARED_CONSTANTS_H + +#include + +AWS_EXTERN_C_BEGIN + +AWS_MQTT_API extern const struct aws_byte_cursor *g_websocket_handshake_default_path; +AWS_MQTT_API extern const struct aws_http_header *g_websocket_handshake_default_protocol_header; + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_SHARED_CONSTANTS_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h new file mode 100644 index 00000000..38651647 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -0,0 +1,616 @@ +#ifndef AWS_MQTT_MQTT5_CLIENT_IMPL_H +#define AWS_MQTT_MQTT5_CLIENT_IMPL_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct aws_event_loop; +struct aws_http_message; +struct aws_http_proxy_options; +struct aws_mqtt5_client_options_storage; +struct aws_mqtt5_operation; +struct aws_websocket_client_connection_options; + +/** + * The various states that the client can be in. A client has both a current state and a desired state. + * Desired state is only allowed to be one of {STOPPED, CONNECTED, TERMINATED}. The client transitions states + * based on either + * (1) changes in desired state, or + * (2) external events. + * + * Most states are interruptible (in the sense of a change in desired state causing an immediate change in state) but + * CONNECTING and CHANNEL_SHUTDOWN cannot be interrupted due to waiting for an asynchronous callback (that has no + * cancel) to complete. + */ +enum aws_mqtt5_client_state { + + /* + * The client is not connected and not waiting for anything to happen. + * + * Next States: + * CONNECTING - if the user invokes Start() on the client + * TERMINATED - if the user releases the last ref count on the client + */ + AWS_MCS_STOPPED, + + /* + * The client is attempting to connect to a remote endpoint, and is waiting for channel setup to complete. This + * state is not interruptible by any means other than channel setup completion. + * + * Next States: + * MQTT_CONNECT - if the channel completes setup with no error and desired state is still CONNECTED + * CHANNEL_SHUTDOWN - if the channel completes setup with no error, but desired state is not CONNECTED + * PENDING_RECONNECT - if the channel fails to complete setup and desired state is still CONNECTED + * STOPPED - if the channel fails to complete setup and desired state is not CONNECTED + */ + AWS_MCS_CONNECTING, + + /* + * The client is sending a CONNECT packet and waiting on a CONNACK packet. + * + * Next States: + * CONNECTED - if a successful CONNACK is received and desired state is still CONNECTED + * CHANNEL_SHUTDOWN - On send/encode errors, read/decode errors, unsuccessful CONNACK, timeout to receive + * CONNACK, desired state is no longer CONNECTED + * PENDING_RECONNECT - unexpected channel shutdown completion and desired state still CONNECTED + * STOPPED - unexpected channel shutdown completion and desired state no longer CONNECTED + */ + AWS_MCS_MQTT_CONNECT, + + /* + * The client is ready to perform user-requested mqtt operations. + * + * Next States: + * CHANNEL_SHUTDOWN - On send/encode errors, read/decode errors, DISCONNECT packet received, desired state + * no longer CONNECTED, PINGRESP timeout + * PENDING_RECONNECT - unexpected channel shutdown completion and desired state still CONNECTED + * STOPPED - unexpected channel shutdown completion and desired state no longer CONNECTED + */ + AWS_MCS_CONNECTED, + + /* + * The client is attempt to shut down a connection cleanly by finishing the current operation and then + * transmitting an outbound DISCONNECT. + * + * Next States: + * CHANNEL_SHUTDOWN - on successful (or unsuccessful) send of the DISCONNECT + * PENDING_RECONNECT - unexpected channel shutdown completion and desired state still CONNECTED + * STOPPED - unexpected channel shutdown completion and desired state no longer CONNECTED + */ + AWS_MCS_CLEAN_DISCONNECT, + + /* + * The client is waiting for the io channel to completely shut down. This state is not interruptible. + * + * Next States: + * PENDING_RECONNECT - the io channel has shut down and desired state is still CONNECTED + * STOPPED - the io channel has shut down and desired state is not CONNECTED + */ + AWS_MCS_CHANNEL_SHUTDOWN, + + /* + * The client is waiting for the reconnect timer to expire before attempting to connect again. + * + * Next States: + * CONNECTING - the reconnect timer has expired and desired state is still CONNECTED + * STOPPED - desired state is no longer CONNECTED + */ + AWS_MCS_PENDING_RECONNECT, + + /* + * The client is performing final shutdown and release of all resources. This state is only realized for + * a non-observable instant of time (transition out of STOPPED). + */ + AWS_MCS_TERMINATED, +}; + +/** + * Table of overridable external functions to allow mocking and monitoring of the client. + */ +struct aws_mqtt5_client_vtable { + /* aws_high_res_clock_get_ticks */ + uint64_t (*get_current_time_fn)(void); + + /* aws_channel_shutdown */ + int (*channel_shutdown_fn)(struct aws_channel *channel, int error_code); + + /* aws_websocket_client_connect */ + int (*websocket_connect_fn)(const struct aws_websocket_client_connection_options *options); + + /* aws_client_bootstrap_new_socket_channel */ + int (*client_bootstrap_new_socket_channel_fn)(struct aws_socket_channel_bootstrap_options *options); + + /* aws_http_proxy_new_socket_channel */ + int (*http_proxy_new_socket_channel_fn)( + struct aws_socket_channel_bootstrap_options *channel_options, + const struct aws_http_proxy_options *proxy_options); + + /* This doesn't replace anything, it's just for test verification of state changes */ + void (*on_client_state_change_callback_fn)( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state old_state, + enum aws_mqtt5_client_state new_state, + void *vtable_user_data); + + /* This doesn't replace anything, it's just for test verification of statistic changes */ + void (*on_client_statistics_changed_callback_fn)( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + void *vtable_user_data); + + /* aws_channel_acquire_message_from_pool */ + struct aws_io_message *(*aws_channel_acquire_message_from_pool_fn)( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data); + + /* aws_channel_slot_send_message */ + int (*aws_channel_slot_send_message_fn)( + struct aws_channel_slot *slot, + struct aws_io_message *message, + enum aws_channel_direction dir, + void *user_data); + + void *vtable_user_data; +}; + +/* + * In order to make it easier to guarantee the lifecycle events are properly paired and emitted, we track + * a separate state (from aws_mqtt5_client_state) and emit lifecycle events based on it. + * + * For example, if our lifecycle event is state CONNECTING, than anything going wrong becomes a CONNECTION_FAILED event + * whereas if we were in CONNECTED, it must be a DISCONNECTED event. By setting the state to NONE after emitting + * a CONNECTION_FAILED or DISCONNECTED event, then emission spots further down the execution pipeline will not + * accidentally emit an additional event. This also allows us to emit immediately when an event happens, if + * appropriate, without having to persist additional event data (like packet views) until some singular point. + * + * For example: + * + * If I'm in CONNECTING and the channel shuts down, I want to emit a CONNECTION_FAILED event with the error code. + * If I'm in CONNECTING and I receive a failed CONNACK, I want to emit a CONNECTION_FAILED event immediately with + * the CONNACK view in it and then invoke channel shutdown (and channel shutdown completing later should not emit an + * event). + * If I'm in CONNECTED and the channel shuts down, I want to emit a DISCONNECTED event with the error code. + * If I'm in CONNECTED and get a DISCONNECT packet from the server, I want to emit a DISCONNECTED event with + * the DISCONNECT packet in it, invoke channel shutdown, and then I *don't* want to emit a DISCONNECTED event + * when the channel finishes shutting down. + */ +enum aws_mqtt5_lifecycle_state { + AWS_MQTT5_LS_NONE, + AWS_MQTT5_LS_CONNECTING, + AWS_MQTT5_LS_CONNECTED, +}; + +/* + * Operation-related state notes + * + * operation flow: + * (qos 0 publish, disconnect, connect) + * user (via cross thread task) -> + * queued_operations -> (on front of queue) + * current_operation -> (on completely encoded and passed to next handler) + * write_completion_operations -> (on socket write complete) + * release + * + * (qos 1+ publish, sub/unsub) + * user (via cross thread task) -> + * queued_operations -> (on front of queue) + * current_operation (allocate packet id if necessary) -> (on completely encoded and passed to next handler) + * unacked_operations && unacked_operations_table -> (on ack received) + * release + * + * QoS 1+ requires both a table and a list holding the same operations in order to support fast lookups by + * mqtt packet id and in-order re-queueing in the case of a disconnection (required by spec) + * + * On Qos 1 PUBLISH completely received (and final callback invoked): + * Add PUBACK at head of queued_operations + * + * On disconnect (on transition to PENDING_RECONNECT or STOPPED): + * If current_operation, move current_operation to head of queued_operations + * Fail all operations in the pending write completion list + * Fail, remove, and release operations in queued_operations where + * (1) They fail the offline queue policy OR + * (2) They are a PUBACK, PINGREQ, or DISCONNECT + * Fail, remove, and release unacked_operations if: + * (1) They fail the offline queue policy AND + * (2) operation is not Qos 1+ publish + * + * On reconnect (post CONNACK): + * if rejoined_session: + * Move-and-append all non-qos1+-publishes in unacked_operations to the front of queued_operations + * Move-and-append remaining operations (qos1+ publishes) to the front of queued_operations + * else: + * Fail, remove, and release unacked_operations that fail the offline queue policy + * Move and append unacked operations to front of queued_operations + * + * Clear unacked_operations_table + */ +struct aws_mqtt5_client_operational_state { + + /* back pointer to the client */ + struct aws_mqtt5_client *client; + + /* + * One more than the most recently used packet id. This is the best starting point for a forward search through + * the id space for a free id. + */ + aws_mqtt5_packet_id_t next_mqtt_packet_id; + + struct aws_linked_list queued_operations; + struct aws_mqtt5_operation *current_operation; + struct aws_hash_table unacked_operations_table; + struct aws_linked_list unacked_operations; + struct aws_linked_list write_completion_operations; + + /* + * Is there an io message in transit (to the socket) that has not invoked its write completion callback yet? + * The client implementation only allows one in-transit message at a time, and so if this is true, we don't + * send additional ones/ + */ + bool pending_write_completion; +}; + +/* + * State related to flow-control rules for the mqtt5 client + * + * Includes: + * (1) Mqtt5 ReceiveMaximum support + * (2) AWS IoT Core limit support: + * (a) Publish TPS rate limit + * (b) Total outbound throughput limit + */ +struct aws_mqtt5_client_flow_control_state { + + /* + * Mechanically follows the mqtt5 suggested implementation: + * + * Starts at the server's receive maximum. + * 1. Decrement every time we send a QoS1+ publish + * 2. Increment every time we receive a PUBACK + * + * Qos1+ publishes (and all operations behind them in the queue) are blocked while this value is zero. + * + * Qos 2 support will require additional work here to match the spec. + */ + uint32_t unacked_publish_token_count; + + /* + * Optional throttle (extended validation) that prevents the client from exceeding Iot Core's default throughput + * limit + */ + struct aws_rate_limiter_token_bucket throughput_throttle; + + /* + * Optional throttle (extended validation) that prevents the client from exceeding Iot Core's default publish + * rate limit. + */ + struct aws_rate_limiter_token_bucket publish_throttle; +}; + +struct aws_mqtt5_client { + + struct aws_allocator *allocator; + struct aws_ref_count ref_count; + + const struct aws_mqtt5_client_vtable *vtable; + + /* + * Client configuration + */ + const struct aws_mqtt5_client_options_storage *config; + + /* + * The recurrent task that runs all client logic outside of external event callbacks. Bound to the client's + * event loop. + */ + struct aws_task service_task; + + /* + * Tracks when the client's service task is next schedule to run. Is zero if the task is not scheduled to run or + * we are in the middle of a service (so technically not scheduled too). + */ + uint64_t next_service_task_run_time; + + /* + * True if the client's service task is running. Used to skip service task reevaluation due to state changes + * while running the service task. Reevaluation will occur at the very end of the service. + */ + bool in_service; + + /* + * The final mqtt5 settings negotiated between defaults, CONNECT, and CONNACK. Only valid while in + * CONNECTED or CLEAN_DISCONNECT states. + */ + struct aws_mqtt5_negotiated_settings negotiated_settings; + + /* + * Event loop all the client's connections and any related tasks will be pinned to, ensuring serialization and + * concurrency safety. + */ + struct aws_event_loop *loop; + + /* Channel handler information */ + struct aws_channel_handler handler; + struct aws_channel_slot *slot; + + /* + * What state is the client working towards? + */ + enum aws_mqtt5_client_state desired_state; + + /* + * What is the client's current state? + */ + enum aws_mqtt5_client_state current_state; + + /* + * The client's lifecycle state. Used to correctly emit lifecycle events in spite of the complicated + * async execution pathways that are possible. + */ + enum aws_mqtt5_lifecycle_state lifecycle_state; + + /* + * The client's MQTT packet encoder + */ + struct aws_mqtt5_encoder encoder; + + /* + * The client's MQTT packet decoder + */ + struct aws_mqtt5_decoder decoder; + + /* + * Cache of inbound topic aliases + */ + struct aws_mqtt5_inbound_topic_alias_resolver inbound_topic_alias_resolver; + + /* + * Cache of outbound topic aliases + */ + struct aws_mqtt5_outbound_topic_alias_resolver *outbound_topic_alias_resolver; + + /* + * Temporary state-related data. + * + * clean_disconnect_error_code - the CLEAN_DISCONNECT state takes time to complete and we want to be able + * to pass an error code from a prior event to the channel shutdown. This holds the "override" error code + * that we'd like to shutdown the channel with while CLEAN_DISCONNECT is processed. + * + * handshake exists on websocket-configured clients between the transform completion timepoint and the + * websocket setup callback. + */ + int clean_disconnect_error_code; + struct aws_http_message *handshake; + + /* + * Wraps all state related to pending and in-progress MQTT operations within the client. + */ + struct aws_mqtt5_client_operational_state operational_state; + + /* + * TODO: topic alias mappings, from-server and to-server have independent mappings + * + * From-server requires a single table + * To-server requires both a table and a list (for LRU) + */ + + /* Statistics tracking operational state */ + struct aws_mutex operation_statistics_lock; + struct aws_mqtt5_client_operation_statistics operation_statistics; + + /* + * Wraps all state related to outbound flow control. + */ + struct aws_mqtt5_client_flow_control_state flow_control_state; + + /* + * When should the next PINGREQ be sent? + */ + uint64_t next_ping_time; + + /* + * When should we shut down the channel due to failure to receive a PINGRESP? Only non-zero when an outstanding + * PINGREQ has not been answered. + */ + uint64_t next_ping_timeout_time; + + /* + * When should the client next attempt to reconnect? Only used by PENDING_RECONNECT state. + */ + uint64_t next_reconnect_time_ns; + + /* + * How many consecutive reconnect failures have we experienced? + */ + uint64_t reconnect_count; + + /* + * How much should we wait before our next reconnect attempt? + */ + uint64_t current_reconnect_delay_ms; + + /* + * When should the client reset current_reconnect_delay_interval_ms to the minimum value? Only relevant to the + * CONNECTED state. + */ + uint64_t next_reconnect_delay_reset_time_ns; + + /* + * When should we shut down the channel due to failure to receive a CONNACK? Only relevant during the MQTT_CONNECT + * state. + */ + uint64_t next_mqtt_connect_packet_timeout_time; + + /* + * Starts false and set to true as soon as a successful connection is established. If the session resumption + * behavior is AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS then this must be true before the client sends CONNECT packets + * with clean start set to false. + */ + bool has_connected_successfully; +}; + +AWS_EXTERN_C_BEGIN + +/* + * A number of private APIs which are either set up for mocking parts of the client or testing subsystems within it by + * exposing what would normally be static functions internal to the implementation. + */ + +/* + * Override the vtable used by the client; useful for mocking certain scenarios. + */ +AWS_MQTT_API void aws_mqtt5_client_set_vtable( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_client_vtable *vtable); + +/* + * Gets the default vtable used by the client. In order to mock something, we start with the default and then + * mutate it selectively to achieve the scenario we're interested in. + */ +AWS_MQTT_API const struct aws_mqtt5_client_vtable *aws_mqtt5_client_get_default_vtable(void); + +/* + * Sets the packet id, if necessary, on an operation based on the current pending acks table. The caller is + * responsible for adding the operation to the unacked table when the packet has been encoding in an io message. + * + * There is an argument that the operation should go into the table only on socket write completion, but that breaks + * allocation unless an additional, independent table is added, which I'd prefer not to do presently. Also, socket + * write completion callbacks can be a bit delayed which could lead to a situation where the response from a local + * server could arrive before the write completion runs which would be a disaster. + */ +AWS_MQTT_API int aws_mqtt5_operation_bind_packet_id( + struct aws_mqtt5_operation *operation, + struct aws_mqtt5_client_operational_state *client_operational_state); + +/* + * Initialize and clean up of the client operational state. Exposed (privately) to enabled tests to reuse the + * init/cleanup used by the client itself. + */ +AWS_MQTT_API int aws_mqtt5_client_operational_state_init( + struct aws_mqtt5_client_operational_state *client_operational_state, + struct aws_allocator *allocator, + struct aws_mqtt5_client *client); + +AWS_MQTT_API void aws_mqtt5_client_operational_state_clean_up( + struct aws_mqtt5_client_operational_state *client_operational_state); + +/* + * Resets the client's operational state based on a disconnection (from above comment): + * + * If current_operation + * move current_operation to head of queued_operations + * Fail all operations in the pending write completion list + * Fail, remove, and release operations in queued_operations where they fail the offline queue policy + * Iterate unacked_operations: + * If qos1+ publish + * set dup flag + * else + * unset/release packet id + * Fail, remove, and release unacked_operations if: + * (1) They fail the offline queue policy AND + * (2) the operation is not Qos 1+ publish + */ +AWS_MQTT_API void aws_mqtt5_client_on_disconnection_update_operational_state(struct aws_mqtt5_client *client); + +/* + * Updates the client's operational state based on a successfully established connection event: + * + * if rejoined_session: + * Move-and-append all non-qos1+-publishes in unacked_operations to the front of queued_operations + * Move-and-append remaining operations (qos1+ publishes) to the front of queued_operations + * else: + * Fail, remove, and release unacked_operations that fail the offline queue policy + * Move and append unacked operations to front of queued_operations + */ +AWS_MQTT_API void aws_mqtt5_client_on_connection_update_operational_state(struct aws_mqtt5_client *client); + +/* + * Processes the pending operation queue based on the current state of the associated client + */ +AWS_MQTT_API int aws_mqtt5_client_service_operational_state( + struct aws_mqtt5_client_operational_state *client_operational_state); + +/* + * Updates the client's operational state based on the receipt of an ACK packet from the server. In general this + * means looking up the original operation in the pending ack table, completing it, removing it from both the + * pending ack table and list, and then destroying it. + */ +AWS_MQTT_API void aws_mqtt5_client_operational_state_handle_ack( + struct aws_mqtt5_client_operational_state *client_operational_state, + aws_mqtt5_packet_id_t packet_id, + enum aws_mqtt5_packet_type packet_type, + const void *packet_view, + int error_code); + +/* + * Helper function that returns whether or not the current value of the negotiated settings can be used. Primarily + * a client state check (received CONNACK, not yet disconnected) + */ +AWS_MQTT_API bool aws_mqtt5_client_are_negotiated_settings_valid(const struct aws_mqtt5_client *client); + +/* + * Initializes the client's flow control state. This state governs the rates and delays between processing + * operations and sending packets. + */ +AWS_MQTT_API void aws_mqtt5_client_flow_control_state_init(struct aws_mqtt5_client *client); + +/* + * Resets the client's flow control state to a known baseline. Invoked right after entering the connected state. + */ +AWS_MQTT_API void aws_mqtt5_client_flow_control_state_reset(struct aws_mqtt5_client *client); + +/* + * Updates the client's flow control state based on the receipt of a PUBACK for a Qos1 publish. + */ +AWS_MQTT_API void aws_mqtt5_client_flow_control_state_on_puback(struct aws_mqtt5_client *client); + +/* + * Updates the client's flow control state based on successfully encoding an operation into a channel message. + */ +AWS_MQTT_API void aws_mqtt5_client_flow_control_state_on_outbound_operation( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation); + +/* + * Given the next operation in the queue, examines the flow control state to determine when is the earliest time + * it should be processed. + */ +AWS_MQTT_API uint64_t aws_mqtt5_client_flow_control_state_get_next_operation_service_time( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + uint64_t now); + +/* + * Updates the client's operation statistics based on a change in the state of an operation. + */ +AWS_MQTT_API void aws_mqtt5_client_statistics_change_operation_statistic_state( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + enum aws_mqtt5_operation_statistic_state_flags new_state_flags); + +/** + * Converts a client state type to a readable description. + * + * @param state client state + * @return short string describing the client state + */ +AWS_MQTT_API const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_client_state state); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_decoder.h b/include/aws/mqtt/private/v5/mqtt5_decoder.h new file mode 100644 index 00000000..e2294b24 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_decoder.h @@ -0,0 +1,263 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_DECODER_H +#define AWS_MQTT_MQTT5_DECODER_H + +#include + +#include +#include +#include + +struct aws_mqtt5_client; +struct aws_mqtt5_decoder; +struct aws_mqtt5_inbound_topic_alias_resolver; + +/** + * Overall decoder state. We read the packet type and the remaining length, and then buffer the + * entire packet before decoding. + */ +enum aws_mqtt5_decoder_state { + AWS_MQTT5_DS_READ_PACKET_TYPE, + AWS_MQTT5_DS_READ_REMAINING_LENGTH, + AWS_MQTT5_DS_READ_PACKET, + AWS_MQTT5_DS_FATAL_ERROR, +}; + +/* + * Basic return value for a number of different decoding operations. Error is always fatal and implies the + * connection needs to be torn down. + */ +enum aws_mqtt5_decode_result_type { + AWS_MQTT5_DRT_MORE_DATA, + AWS_MQTT5_DRT_SUCCESS, + AWS_MQTT5_DRT_ERROR, +}; + +/* + * Callbacks the decoder should invoke. We don't invoke functions directly on the client because + * we want to test the decoder's correctness in isolation. + */ +typedef int(aws_mqtt5_on_packet_received_fn)( + enum aws_mqtt5_packet_type type, + void *packet_view, + void *decoder_callback_user_data); + +typedef int(aws_mqtt5_on_publish_payload_data_fn)( + struct aws_mqtt5_packet_publish_view *publish_view, + struct aws_byte_cursor payload, + void *decoder_callback_user_data); + +/** + * per-packet-type decoding function signature + */ +typedef int(aws_mqtt5_decoding_fn)(struct aws_mqtt5_decoder *decoder); + +/** + * table of decoding functions. Tests use an augmented version that includes decoders for packet types normally + * only decoded by an mqtt server. + */ +struct aws_mqtt5_decoder_function_table { + aws_mqtt5_decoding_fn *decoders_by_packet_type[16]; +}; + +/** + * Basic decoder configuration. + */ +struct aws_mqtt5_decoder_options { + void *callback_user_data; + aws_mqtt5_on_packet_received_fn *on_packet_received; + const struct aws_mqtt5_decoder_function_table *decoder_table; +}; + +struct aws_mqtt5_decoder { + struct aws_allocator *allocator; + struct aws_mqtt5_decoder_options options; + + enum aws_mqtt5_decoder_state state; + + /* + * decode scratch space: packets may get fully buffered here before decode + * Exceptions: + * when the incoming io message buffer contains the entire packet, we decode directly from it instead + */ + struct aws_byte_buf scratch_space; + + /* + * packet type and flags + */ + uint8_t packet_first_byte; + + uint32_t remaining_length; + + /* + * Packet decoders work from this cursor. It may point to scratch_space (for packets that were delivered + * in more than one fragment) or to an io message buffer that contains the entire packet. + */ + struct aws_byte_cursor packet_cursor; + + struct aws_mqtt5_inbound_topic_alias_resolver *topic_alias_resolver; +}; + +AWS_EXTERN_C_BEGIN + +/** + * One-time initialization for an mqtt5 decoder + * + * @param decoder decoder to initialize + * @param allocator allocator to use for memory allocation + * @param options configuration options + * @return success/failure + */ +AWS_MQTT_API int aws_mqtt5_decoder_init( + struct aws_mqtt5_decoder *decoder, + struct aws_allocator *allocator, + struct aws_mqtt5_decoder_options *options); + +/** + * Cleans up an mqtt5 decoder + * + * @param decoder decoder to clean up + */ +AWS_MQTT_API void aws_mqtt5_decoder_clean_up(struct aws_mqtt5_decoder *decoder); + +/** + * Resets the state of an mqtt5 decoder. Used whenever a new connection is established + * + * @param decoder decoder to reset state for + */ +AWS_MQTT_API void aws_mqtt5_decoder_reset(struct aws_mqtt5_decoder *decoder); + +/** + * Basic entry point for all incoming mqtt5 data once the basic connection has been established + * + * @param decoder decoder to decode data with + * @param data the data to decode + * @return success/failure - failure implies a need to shut down the connection + */ +AWS_MQTT_API int aws_mqtt5_decoder_on_data_received(struct aws_mqtt5_decoder *decoder, struct aws_byte_cursor data); + +/** + * Sets the optional inbound alias resolver that the decoder should use during the lifetime of a connection + * + * @param decoder decoder to apply inbound topic alias resolution to + * @param resolver inbound topic alias resolver + */ +AWS_MQTT_API void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( + struct aws_mqtt5_decoder *decoder, + struct aws_mqtt5_inbound_topic_alias_resolver *resolver); + +/** + * Default decoding table; tests use an augmented version with decoders for packets that only the server needs to + * decode. + */ +AWS_MQTT_API extern const struct aws_mqtt5_decoder_function_table *g_aws_mqtt5_default_decoder_table; + +AWS_EXTERN_C_END + +/* Decode helpers */ + +AWS_EXTERN_C_BEGIN + +/** + * Decodes, if possible, a variable length integer from a cursor. If the decode is successful, the cursor is advanced + * past the variable length integer encoding. This can be used both for streaming and non-streaming decode operations. + * + * @param cursor data to decode from + * @param dest where to put a successfully decoded variable length integer + * @return the result of attempting the decode: {success, error, not enough data} Does not set aws_last_error. + */ +AWS_MQTT_API enum aws_mqtt5_decode_result_type aws_mqtt5_decode_vli(struct aws_byte_cursor *cursor, uint32_t *dest); + +/** + * Decodes an MQTT5 user property from a cursor + * + * @param packet_cursor data to decode from + * @param properties property set to add the decoded property to + * @return success/failure - failures implies connection termination + */ +AWS_MQTT_API int aws_mqtt5_decode_user_property( + struct aws_byte_cursor *packet_cursor, + struct aws_mqtt5_user_property_set *properties); + +AWS_EXTERN_C_END + +/* Decode helper macros operating on a cursor */ + +/* + * u8 and u16 decode are a little different in order to support encoded values that are widened to larger storage. + * To make that safe, we decode to a local and then assign the local to the final spot. There should be no + * complaints as long as the implicit conversion is the same size or wider. + * + * Some u8 examples include qos (one byte encode -> int-based enum) and various reason codes + * Some u16 examples include cursor lengths decoded directly into a cursor's len field (u16 -> size_t) + */ +#define AWS_MQTT5_DECODE_U8(cursor_ptr, u8_ptr, error_label) \ + { \ + uint8_t decoded_value = 0; \ + if (!aws_byte_cursor_read_u8((cursor_ptr), (&decoded_value))) { \ + goto error_label; \ + } \ + *u8_ptr = decoded_value; \ + } + +#define AWS_MQTT5_DECODE_U8_OPTIONAL(cursor_ptr, u8_ptr, u8_ptr_ptr, error_label) \ + AWS_MQTT5_DECODE_U8(cursor_ptr, u8_ptr, error_label); \ + *(u8_ptr_ptr) = (u8_ptr); + +#define AWS_MQTT5_DECODE_U16(cursor_ptr, u16_ptr, error_label) \ + { \ + uint16_t decoded_value = 0; \ + if (!aws_byte_cursor_read_be16((cursor_ptr), (&decoded_value))) { \ + goto error_label; \ + } \ + *u16_ptr = decoded_value; \ + } + +/* + * In addition to decoding a length prefix, this also verifies that the length prefix does not exceed the source + * cursor length. + */ +#define AWS_MQTT5_DECODE_U16_PREFIX(cursor_ptr, u16_ptr, error_label) \ + AWS_MQTT5_DECODE_U16((cursor_ptr), (u16_ptr), error_label); \ + if (cursor_ptr->len < *(u16_ptr)) { \ + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); \ + goto error_label; \ + } + +#define AWS_MQTT5_DECODE_U16_OPTIONAL(cursor_ptr, u16_ptr, u16_ptr_ptr, error_label) \ + AWS_MQTT5_DECODE_U16((cursor_ptr), u16_ptr, error_label); \ + *(u16_ptr_ptr) = (u16_ptr); + +#define AWS_MQTT5_DECODE_U32(cursor_ptr, u32_ptr, error_label) \ + if (!aws_byte_cursor_read_be32((cursor_ptr), (u32_ptr))) { \ + goto error_label; \ + } + +#define AWS_MQTT5_DECODE_U32_OPTIONAL(cursor_ptr, u32_ptr, u32_ptr_ptr, error_label) \ + AWS_MQTT5_DECODE_U32((cursor_ptr), u32_ptr, error_label); \ + *(u32_ptr_ptr) = (u32_ptr); + +#define AWS_MQTT5_DECODE_VLI(cursor_ptr, u32_ptr, error_label) \ + if (AWS_MQTT5_DRT_SUCCESS != aws_mqtt5_decode_vli((cursor_ptr), (u32_ptr))) { \ + goto error_label; \ + } + +/* decodes both the length prefix and the following cursor field */ +#define AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(cursor_ptr, dest_cursor_ptr, error_label) \ + { \ + uint16_t prefix_length = 0; \ + AWS_MQTT5_DECODE_U16_PREFIX((cursor_ptr), &prefix_length, error_label) \ + \ + *(dest_cursor_ptr) = aws_byte_cursor_advance((cursor_ptr), prefix_length); \ + } + +#define AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( \ + cursor_ptr, dest_cursor_ptr, dest_cursor_ptr_ptr, error_label) \ + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR((cursor_ptr), (dest_cursor_ptr), error_label) \ + *(dest_cursor_ptr_ptr) = (dest_cursor_ptr); + +#endif /* AWS_MQTT_MQTT5_DECODER_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_encoder.h b/include/aws/mqtt/private/v5/mqtt5_encoder.h new file mode 100644 index 00000000..80dfbc20 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_encoder.h @@ -0,0 +1,357 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_ENCODER_H +#define AWS_MQTT_MQTT5_ENCODER_H + +#include + +#include +#include + +#include + +struct aws_mqtt5_client; +struct aws_mqtt5_encoder; +struct aws_mqtt5_outbound_topic_alias_resolver; + +/** + * We encode packets by looking at all of the packet's values/properties and building a sequence of encoding steps. + * Each encoding step is a simple, primitive operation of which there are two types: + * (1) encode an integer in some fashion (fixed width or variable length) + * (2) encode a raw sequence of bytes (either a cursor or a stream) + * + * Once the encoding step sequence is constructed, we do the actual encoding by iterating the sequence, performing + * the steps. This is interruptible/resumable, so we can perform encodings that span multiple buffers easily. + */ +enum aws_mqtt5_encoding_step_type { + /* encode a single byte */ + AWS_MQTT5_EST_U8, + + /* encode a 16 bit unsigned integer in network order */ + AWS_MQTT5_EST_U16, + + /* encode a 32 bit unsigned integer in network order */ + AWS_MQTT5_EST_U32, + + /* + * encode a 32 bit unsigned integer using MQTT variable length encoding. It is assumed that the 32 bit value has + * already been checked against the maximum allowed value for variable length encoding. + */ + AWS_MQTT5_EST_VLI, + + /* + * encode an array of bytes as referenced by a cursor. Most of the time this step is paired with either a prefix + * specifying the number of bytes or a preceding variable length integer from which the data length can be + * computed. + */ + AWS_MQTT5_EST_CURSOR, + + /* encode a stream of bytes. The same context that applies to cursor encoding above also applies here. */ + AWS_MQTT5_EST_STREAM, +}; + +/** + * Elemental unit of packet encoding. + */ +struct aws_mqtt5_encoding_step { + enum aws_mqtt5_encoding_step_type type; + union { + uint8_t value_u8; + uint16_t value_u16; + uint32_t value_u32; + struct aws_byte_cursor value_cursor; + struct aws_input_stream *value_stream; + } value; +}; + +/** + * signature of a function that can takes a view assumed to be a specific packet type and appends the encoding + * steps necessary to encode that packet into the encoder + */ +typedef int(aws_mqtt5_encode_begin_packet_type_fn)(struct aws_mqtt5_encoder *encoder, const void *view); + +/** + * Per-packet-type table of encoding functions + */ +struct aws_mqtt5_encoder_function_table { + aws_mqtt5_encode_begin_packet_type_fn *encoders_by_packet_type[16]; +}; + +/** + * Configuration options for an mqtt5 encoder. Everything is optional at this time. + */ +struct aws_mqtt5_encoder_options { + struct aws_mqtt5_client *client; + const struct aws_mqtt5_encoder_function_table *encoders; +}; + +/** + * An encoder is just a list of steps and a current location for the encoding process within that list. + */ +struct aws_mqtt5_encoder { + struct aws_mqtt5_encoder_options config; + + struct aws_array_list encoding_steps; + size_t current_encoding_step_index; + + struct aws_mqtt5_outbound_topic_alias_resolver *topic_alias_resolver; +}; + +/** + * Encoding proceeds until either + * (1) a fatal error is reached + * (2) the steps are done + * (3) no room is left in the buffer + */ +enum aws_mqtt5_encoding_result { + /* + * A fatal error state was reached during encoding. This forces a connection shut down with no DISCONNECT. + * An error can arise from several sources: + * (1) Bug in the encoder (length calculations, step calculations) + * (2) Bug in the view validation logic that is assumed to have caught any illegal/forbidden situations like + * values-too-big, etc... + * (3) System error when reading from a stream that is more than just a memory buffer + * + * Regardless of the origin, the connection is in an unusable state once this happens. + * + * If the encode function returns this value, aws last error will have an error value in it + */ + AWS_MQTT5_ER_ERROR, + + /* All encoding steps in the encoder have been completed. The encoder is ready for a new packet. */ + AWS_MQTT5_ER_FINISHED, + + /* + * The buffer has been filled as closely to full as possible and there are still encoding steps remaining that + * have not been completed. It is technically possible to hit a permanent out-of-room state if the buffer size + * is less than 4. Don't do that. + */ + AWS_MQTT5_ER_OUT_OF_ROOM, +}; + +AWS_EXTERN_C_BEGIN + +/** + * Initializes an mqtt5 encoder + * + * @param encoder encoder to initialize + * @param allocator allocator to use for all memory allocation + * @param options encoder configuration options to use + * @return + */ +AWS_MQTT_API int aws_mqtt5_encoder_init( + struct aws_mqtt5_encoder *encoder, + struct aws_allocator *allocator, + struct aws_mqtt5_encoder_options *options); + +/** + * Cleans up an mqtt5 encoder + * + * @param encoder encoder to free up all resources for + */ +AWS_MQTT_API void aws_mqtt5_encoder_clean_up(struct aws_mqtt5_encoder *encoder); + +/** + * Resets the state on an mqtt5 encoder. Ok to call after a failure to a packet _begin_packet() function. Not ok to + * call after a failed call to aws_mqtt5_encoder_encode_to_buffer() + * + * @param encoder encoder to reset + * @return + */ +AWS_MQTT_API void aws_mqtt5_encoder_reset(struct aws_mqtt5_encoder *encoder); + +/** + * Adds all of the primitive encoding steps necessary to encode an MQTT5 packet + * + * @param encoder encoder to add encoding steps to + * @param packet_type type of packet to encode + * @param packet_view view into the corresponding packet type + * @return success/failure + */ +AWS_MQTT_API int aws_mqtt5_encoder_append_packet_encoding( + struct aws_mqtt5_encoder *encoder, + enum aws_mqtt5_packet_type packet_type, + const void *packet_view); + +/* + * We intend that the client implementation only submits one packet at a time to the encoder, corresponding to the + * current operation of the client. This is an important property to maintain to allow us to correlate socket + * completions with packets/operations sent. It's the client's responsibility though; the encoder is dumb. + * + * The client will greedily use as much of an iomsg's buffer as it can if there are multiple operations (packets) + * queued and there is sufficient room. + */ + +/** + * Asks the encoder to encode as much as it possibly can into the supplied buffer. + * + * @param encoder encoder to do the encoding + * @param buffer where to encode into + * @return result of the encoding process. aws last error will be set appropriately. + */ +AWS_MQTT_API enum aws_mqtt5_encoding_result aws_mqtt5_encoder_encode_to_buffer( + struct aws_mqtt5_encoder *encoder, + struct aws_byte_buf *buffer); + +/** + * Sets the outbound alias resolver that the encoder should use during the lifetime of a connection + * + * @param encoder encoder to apply outbound topic alias resolution to + * @param resolver outbound topic alias resolver + */ +AWS_MQTT_API void aws_mqtt5_encoder_set_outbound_topic_alias_resolver( + struct aws_mqtt5_encoder *encoder, + struct aws_mqtt5_outbound_topic_alias_resolver *resolver); + +/** + * Default encoder table. Tests copy it and augment with additional functions in order to do round-trip encode-decode + * tests for packets that are only encoded on the server. + */ +AWS_MQTT_API extern const struct aws_mqtt5_encoder_function_table *g_aws_mqtt5_encoder_default_function_table; + +AWS_EXTERN_C_END + +/****************************************************************************************************************** + * Encoding helper functions and macros - placed in header so that test-only encoding has access + ******************************************************************************************************************/ + +AWS_EXTERN_C_BEGIN + +/** + * Utility function to calculate the encoded packet size of a given packet view. Used to validate operations + * against the server's maximum packet size. + * + * @param packet_type type of packet the view represents + * @param packet_view packet view + * @param packet_size output parameter, set if the size was successfully calculated + * @return success/failure + */ +AWS_MQTT_API int aws_mqtt5_packet_view_get_encoded_size( + enum aws_mqtt5_packet_type packet_type, + const void *packet_view, + size_t *packet_size); + +/** + * Encodes a variable length integer to a buffer. Assumes the buffer has been checked for sufficient room (this + * is not a streaming/resumable operation) + * + * @param buf buffer to encode to + * @param value value to encode + * @return success/failure + */ +AWS_MQTT_API int aws_mqtt5_encode_variable_length_integer(struct aws_byte_buf *buf, uint32_t value); + +/** + * Computes how many bytes are necessary to encode a value as a variable length integer + * @param value value to encode + * @param encode_size output parameter for the encoding size + * @return success/failure where failure is exclusively value-is-illegal-and-too-large-to-encode + */ +AWS_MQTT_API int aws_mqtt5_get_variable_length_encode_size(size_t value, size_t *encode_size); + +AWS_MQTT_API void aws_mqtt5_encoder_push_step_u8(struct aws_mqtt5_encoder *encoder, uint8_t value); + +AWS_MQTT_API void aws_mqtt5_encoder_push_step_u16(struct aws_mqtt5_encoder *encoder, uint16_t value); + +AWS_MQTT_API void aws_mqtt5_encoder_push_step_u32(struct aws_mqtt5_encoder *encoder, uint32_t value); + +AWS_MQTT_API int aws_mqtt5_encoder_push_step_vli(struct aws_mqtt5_encoder *encoder, uint32_t value); + +AWS_MQTT_API void aws_mqtt5_encoder_push_step_cursor(struct aws_mqtt5_encoder *encoder, struct aws_byte_cursor value); + +AWS_MQTT_API size_t aws_mqtt5_compute_user_property_encode_length( + const struct aws_mqtt5_user_property *properties, + size_t user_property_count); + +AWS_MQTT_API void aws_mqtt5_add_user_property_encoding_steps( + struct aws_mqtt5_encoder *encoder, + const struct aws_mqtt5_user_property *user_properties, + size_t user_property_count); + +AWS_EXTERN_C_END + +/* macros to simplify encoding step list construction */ + +#define ADD_ENCODE_STEP_U8(encoder, value) aws_mqtt5_encoder_push_step_u8(encoder, (uint8_t)(value)) +#define ADD_ENCODE_STEP_U16(encoder, value) aws_mqtt5_encoder_push_step_u16(encoder, (uint16_t)(value)) +#define ADD_ENCODE_STEP_U32(encoder, value) aws_mqtt5_encoder_push_step_u32(encoder, (uint32_t)(value)) +#define ADD_ENCODE_STEP_CURSOR(encoder, cursor) aws_mqtt5_encoder_push_step_cursor(encoder, (cursor)) + +#define ADD_ENCODE_STEP_VLI(encoder, value) \ + if (aws_mqtt5_encoder_push_step_vli(encoder, (value))) { \ + return AWS_OP_ERR; \ + } + +#define ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, cursor) \ + { \ + aws_mqtt5_encoder_push_step_u16(encoder, (uint16_t)((cursor).len)); \ + aws_mqtt5_encoder_push_step_cursor(encoder, (cursor)); \ + } + +#define ADD_ENCODE_STEP_OPTIONAL_LENGTH_PREFIXED_CURSOR(encoder, cursor_ptr) \ + if (cursor_ptr != NULL) { \ + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, *cursor_ptr); \ + } + +/* Property-oriented macros for encode steps. Properties have an additional prefix byte saying what their type is. */ + +#define ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY(encoder, property_value, value_ptr) \ + if ((value_ptr) != NULL) { \ + ADD_ENCODE_STEP_U8(encoder, property_value); \ + ADD_ENCODE_STEP_U8(encoder, *(value_ptr)); \ + } + +#define ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY(encoder, property_value, value_ptr) \ + if ((value_ptr) != NULL) { \ + ADD_ENCODE_STEP_U8(encoder, property_value); \ + ADD_ENCODE_STEP_U16(encoder, *(value_ptr)); \ + } + +#define ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY(encoder, property_value, value_ptr) \ + if ((value_ptr) != NULL) { \ + ADD_ENCODE_STEP_U8(encoder, property_value); \ + ADD_ENCODE_STEP_U32(encoder, *(value_ptr)); \ + } + +#define ADD_ENCODE_STEP_OPTIONAL_VLI_PROPERTY(encoder, property_value, value_ptr) \ + if ((value_ptr) != NULL) { \ + ADD_ENCODE_STEP_U8(encoder, property_value); \ + ADD_ENCODE_STEP_VLI(encoder, *(value_ptr)); \ + } + +#define ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY(encoder, property_type, cursor_ptr) \ + if ((cursor_ptr) != NULL) { \ + ADD_ENCODE_STEP_U8(encoder, property_type); \ + ADD_ENCODE_STEP_U16(encoder, (cursor_ptr)->len); \ + ADD_ENCODE_STEP_CURSOR(encoder, *(cursor_ptr)); \ + } + +/* + * Macros to simplify packet size calculations, which are significantly complicated by mqtt5's many optional + * properties. + */ + +#define ADD_OPTIONAL_U8_PROPERTY_LENGTH(property_ptr, length) \ + if ((property_ptr) != NULL) { \ + (length) += 2; \ + } + +#define ADD_OPTIONAL_U16_PROPERTY_LENGTH(property_ptr, length) \ + if ((property_ptr) != NULL) { \ + (length) += 3; \ + } + +#define ADD_OPTIONAL_U32_PROPERTY_LENGTH(property_ptr, length) \ + if ((property_ptr) != NULL) { \ + (length) += 5; \ + } + +#define ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(property_ptr, length) \ + if ((property_ptr) != NULL) { \ + (length) += 3 + ((property_ptr)->len); \ + } + +#endif /* AWS_MQTT_MQTT5_ENCODER_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h new file mode 100644 index 00000000..dcc07d39 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -0,0 +1,343 @@ +#ifndef AWS_MQTT_MQTT5_OPERATION_H +#define AWS_MQTT_MQTT5_OPERATION_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct aws_client_bootstrap; +struct aws_mqtt5_client; +struct aws_mqtt5_client_options; +struct aws_mqtt5_operation; +struct aws_string; + +/* Basic vtable for all mqtt operations. Implementations are per-packet type */ +struct aws_mqtt5_operation_vtable { + void (*aws_mqtt5_operation_completion_fn)( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *completion_view); + + void ( + *aws_mqtt5_operation_set_packet_id_fn)(struct aws_mqtt5_operation *operation, aws_mqtt5_packet_id_t packet_id); + + aws_mqtt5_packet_id_t *(*aws_mqtt5_operation_get_packet_id_address_fn)(const struct aws_mqtt5_operation *operation); + + int (*aws_mqtt5_operation_validate_vs_connection_settings_fn)( + const void *operation_packet_view, + const struct aws_mqtt5_client *client); +}; + +/* Flags that indicate the way in which an operation is currently affecting the statistics of the client */ +enum aws_mqtt5_operation_statistic_state_flags { + /* The operation is not affecting the client's statistics at all */ + AWS_MQTT5_OSS_NONE = 0, + + /* The operation is affecting the client's "incomplete operation" statistics */ + AWS_MQTT5_OSS_INCOMPLETE = 1 << 0, + + /* The operation is affecting the client's "unacked operation" statistics */ + AWS_MQTT5_OSS_UNACKED = 1 << 1, +}; + +/** + * This is the base structure for all mqtt5 operations. It includes the type, a ref count, timeout timepoint, + * and list management. + */ +struct aws_mqtt5_operation { + const struct aws_mqtt5_operation_vtable *vtable; + struct aws_ref_count ref_count; + uint64_t ack_timeout_timepoint_ns; + struct aws_linked_list_node node; + + enum aws_mqtt5_packet_type packet_type; + const void *packet_view; + + /* How this operation is currently affecting the statistics of the client */ + enum aws_mqtt5_operation_statistic_state_flags statistic_state_flags; + + /* Size of the MQTT packet this operation represents */ + size_t packet_size; + + void *impl; +}; + +struct aws_mqtt5_operation_connect { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_connect_storage options_storage; +}; + +struct aws_mqtt5_operation_publish { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_publish_storage options_storage; + + struct aws_mqtt5_publish_completion_options completion_options; +}; + +struct aws_mqtt5_operation_puback { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_puback_storage options_storage; +}; + +struct aws_mqtt5_operation_disconnect { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_disconnect_storage options_storage; + + struct aws_mqtt5_disconnect_completion_options external_completion_options; + struct aws_mqtt5_disconnect_completion_options internal_completion_options; +}; + +struct aws_mqtt5_operation_subscribe { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_subscribe_storage options_storage; + + struct aws_mqtt5_subscribe_completion_options completion_options; +}; + +struct aws_mqtt5_operation_unsubscribe { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_unsubscribe_storage options_storage; + + struct aws_mqtt5_unsubscribe_completion_options completion_options; +}; + +struct aws_mqtt5_operation_pingreq { + struct aws_mqtt5_operation base; + struct aws_allocator *allocator; +}; + +struct aws_mqtt5_client_options_storage { + struct aws_allocator *allocator; + + struct aws_string *host_name; + uint16_t port; + struct aws_client_bootstrap *bootstrap; + struct aws_socket_options socket_options; + + struct aws_tls_connection_options tls_options; + struct aws_tls_connection_options *tls_options_ptr; + + struct aws_http_proxy_options http_proxy_options; + struct aws_http_proxy_config *http_proxy_config; + + aws_mqtt5_transform_websocket_handshake_fn *websocket_handshake_transform; + void *websocket_handshake_transform_user_data; + + aws_mqtt5_publish_received_fn *publish_received_handler; + void *publish_received_handler_user_data; + + enum aws_mqtt5_client_session_behavior_type session_behavior; + enum aws_mqtt5_extended_validation_and_flow_control_options extended_validation_and_flow_control_options; + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior; + + enum aws_exponential_backoff_jitter_mode retry_jitter_mode; + uint64_t min_reconnect_delay_ms; + uint64_t max_reconnect_delay_ms; + uint64_t min_connected_time_to_reset_reconnect_delay_ms; + + uint64_t ack_timeout_seconds; + + uint32_t ping_timeout_ms; + uint32_t connack_timeout_ms; + + struct aws_mqtt5_client_topic_alias_options topic_aliasing_options; + + struct aws_mqtt5_packet_connect_storage connect; + + aws_mqtt5_client_connection_event_callback_fn *lifecycle_event_handler; + void *lifecycle_event_handler_user_data; + + aws_mqtt5_client_termination_completion_fn *client_termination_handler; + void *client_termination_handler_user_data; +}; + +AWS_EXTERN_C_BEGIN + +/* Operation base */ + +AWS_MQTT_API struct aws_mqtt5_operation *aws_mqtt5_operation_acquire(struct aws_mqtt5_operation *operation); + +AWS_MQTT_API struct aws_mqtt5_operation *aws_mqtt5_operation_release(struct aws_mqtt5_operation *operation); + +AWS_MQTT_API void aws_mqtt5_operation_complete( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *associated_view); + +AWS_MQTT_API void aws_mqtt5_operation_set_packet_id( + struct aws_mqtt5_operation *operation, + aws_mqtt5_packet_id_t packet_id); + +AWS_MQTT_API aws_mqtt5_packet_id_t aws_mqtt5_operation_get_packet_id(const struct aws_mqtt5_operation *operation); + +AWS_MQTT_API aws_mqtt5_packet_id_t *aws_mqtt5_operation_get_packet_id_address( + const struct aws_mqtt5_operation *operation); + +AWS_MQTT_API int aws_mqtt5_operation_validate_vs_connection_settings( + const struct aws_mqtt5_operation *operation, + const struct aws_mqtt5_client *client); + +/* Connect */ + +AWS_MQTT_API struct aws_mqtt5_operation_connect *aws_mqtt5_operation_connect_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connect_view *connect_options); + +AWS_MQTT_API int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect_view *connect_view); + +AWS_MQTT_API void aws_mqtt5_packet_connect_view_log( + const struct aws_mqtt5_packet_connect_view *connect_view, + enum aws_log_level level); + +/* Connack */ + +AWS_MQTT_API void aws_mqtt5_packet_connack_view_log( + const struct aws_mqtt5_packet_connack_view *connack_view, + enum aws_log_level level); + +/* Disconnect */ + +AWS_MQTT_API struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_disconnect_view *disconnect_options, + const struct aws_mqtt5_disconnect_completion_options *external_completion_options, + const struct aws_mqtt5_disconnect_completion_options *internal_completion_options); + +AWS_MQTT_API struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_acquire( + struct aws_mqtt5_operation_disconnect *disconnect_op); + +AWS_MQTT_API struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_release( + struct aws_mqtt5_operation_disconnect *disconnect_op); + +AWS_MQTT_API int aws_mqtt5_packet_disconnect_view_validate( + const struct aws_mqtt5_packet_disconnect_view *disconnect_view); + +AWS_MQTT_API void aws_mqtt5_packet_disconnect_view_log( + const struct aws_mqtt5_packet_disconnect_view *disconnect_view, + enum aws_log_level level); + +/* Publish */ + +AWS_MQTT_API struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_publish_view *publish_options, + const struct aws_mqtt5_publish_completion_options *completion_options); + +AWS_MQTT_API int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish_view *publish_view); + +AWS_MQTT_API int aws_mqtt5_packet_publish_view_validate_vs_iot_core( + const struct aws_mqtt5_packet_publish_view *publish_view); + +AWS_MQTT_API void aws_mqtt5_packet_publish_view_log( + const struct aws_mqtt5_packet_publish_view *publish_view, + enum aws_log_level level); + +/* Puback */ + +AWS_MQTT_API struct aws_mqtt5_operation_puback *aws_mqtt5_operation_puback_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_puback_view *puback_options); + +AWS_MQTT_API void aws_mqtt5_packet_puback_view_log( + const struct aws_mqtt5_packet_puback_view *puback_view, + enum aws_log_level level); + +/* Subscribe */ + +AWS_MQTT_API struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options, + const struct aws_mqtt5_subscribe_completion_options *completion_options); + +AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate(const struct aws_mqtt5_packet_subscribe_view *subscribe_view); + +AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate_vs_iot_core( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view); + +AWS_MQTT_API void aws_mqtt5_packet_subscribe_view_log( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view, + enum aws_log_level level); + +/* Suback */ + +AWS_MQTT_API void aws_mqtt5_packet_suback_view_log( + const struct aws_mqtt5_packet_suback_view *suback_view, + enum aws_log_level level); + +/* Unsubscribe */ + +AWS_MQTT_API struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubscribe_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options, + const struct aws_mqtt5_unsubscribe_completion_options *completion_options); + +AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view); + +AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view); + +AWS_MQTT_API void aws_mqtt5_packet_unsubscribe_view_log( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view, + enum aws_log_level level); + +/* Unsuback */ + +AWS_MQTT_API void aws_mqtt5_packet_unsuback_view_log( + const struct aws_mqtt5_packet_unsuback_view *unsuback_view, + enum aws_log_level level); + +/* PINGREQ */ + +AWS_MQTT_API struct aws_mqtt5_operation_pingreq *aws_mqtt5_operation_pingreq_new(struct aws_allocator *allocator); + +/* client */ + +AWS_MQTT_API +struct aws_mqtt5_client_options_storage *aws_mqtt5_client_options_storage_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client_options *options); + +AWS_MQTT_API +void aws_mqtt5_client_options_storage_destroy(struct aws_mqtt5_client_options_storage *options_storage); + +AWS_MQTT_API int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *client_options); + +AWS_MQTT_API void aws_mqtt5_client_options_storage_log( + const struct aws_mqtt5_client_options_storage *options_storage, + enum aws_log_level level); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_OPERATION_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_topic_alias.h b/include/aws/mqtt/private/v5/mqtt5_topic_alias.h new file mode 100644 index 00000000..8c044d4e --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_topic_alias.h @@ -0,0 +1,66 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_TOPIC_ALIAS_H +#define AWS_MQTT_MQTT5_TOPIC_ALIAS_H + +#include + +#include +#include + +/* outbound resolvers are polymorphic; implementations are completely internal */ +struct aws_mqtt5_outbound_topic_alias_resolver; + +/* there are only two possibilities for inbound resolution: on or off */ +struct aws_mqtt5_inbound_topic_alias_resolver { + struct aws_allocator *allocator; + + struct aws_array_list topic_aliases; +}; + +AWS_EXTERN_C_BEGIN + +AWS_MQTT_API int aws_mqtt5_inbound_topic_alias_resolver_init( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_inbound_topic_alias_resolver_clean_up( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver); + +AWS_MQTT_API int aws_mqtt5_inbound_topic_alias_resolver_reset( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t cache_size); + +AWS_MQTT_API int aws_mqtt5_inbound_topic_alias_resolver_resolve_alias( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t alias, + struct aws_byte_cursor *topic_out); + +AWS_MQTT_API int aws_mqtt5_inbound_topic_alias_resolver_register_alias( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t alias, + struct aws_byte_cursor topic); + +AWS_MQTT_API struct aws_mqtt5_outbound_topic_alias_resolver *aws_mqtt5_outbound_topic_alias_resolver_new( + struct aws_allocator *allocator, + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_alias_behavior); + +AWS_MQTT_API void aws_mqtt5_outbound_topic_alias_resolver_destroy( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver); + +AWS_MQTT_API int aws_mqtt5_outbound_topic_alias_resolver_reset( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + uint16_t topic_alias_maximum); + +AWS_MQTT_API int aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + const struct aws_mqtt5_packet_publish_view *publish_view, + uint16_t *topic_alias_out, + struct aws_byte_cursor *topic_out); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_TOPIC_ALIAS_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_utils.h b/include/aws/mqtt/private/v5/mqtt5_utils.h new file mode 100644 index 00000000..0aacfba7 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_utils.h @@ -0,0 +1,355 @@ +#ifndef AWS_MQTT_MQTT5_UTILS_H +#define AWS_MQTT_MQTT5_UTILS_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +struct aws_byte_buf; +struct aws_mqtt5_negotiated_settings; + +#define AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER 268435455 +#define AWS_MQTT5_MAXIMUM_PACKET_SIZE (5 + AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER) +#define AWS_MQTT5_RECEIVE_MAXIMUM 65535 +#define AWS_MQTT5_PINGREQ_ENCODED_SIZE 2 + +/* property type codes */ +#define AWS_MQTT5_PROPERTY_TYPE_PAYLOAD_FORMAT_INDICATOR ((uint8_t)1) +#define AWS_MQTT5_PROPERTY_TYPE_MESSAGE_EXPIRY_INTERVAL ((uint8_t)2) +#define AWS_MQTT5_PROPERTY_TYPE_CONTENT_TYPE ((uint8_t)3) +#define AWS_MQTT5_PROPERTY_TYPE_RESPONSE_TOPIC ((uint8_t)8) +#define AWS_MQTT5_PROPERTY_TYPE_CORRELATION_DATA ((uint8_t)9) +#define AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIER ((uint8_t)11) +#define AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL ((uint8_t)17) +#define AWS_MQTT5_PROPERTY_TYPE_ASSIGNED_CLIENT_IDENTIFIER ((uint8_t)18) +#define AWS_MQTT5_PROPERTY_TYPE_SERVER_KEEP_ALIVE ((uint8_t)19) +#define AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_METHOD ((uint8_t)21) +#define AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_DATA ((uint8_t)22) +#define AWS_MQTT5_PROPERTY_TYPE_REQUEST_PROBLEM_INFORMATION ((uint8_t)23) +#define AWS_MQTT5_PROPERTY_TYPE_WILL_DELAY_INTERVAL ((uint8_t)24) +#define AWS_MQTT5_PROPERTY_TYPE_REQUEST_RESPONSE_INFORMATION ((uint8_t)25) +#define AWS_MQTT5_PROPERTY_TYPE_RESPONSE_INFORMATION ((uint8_t)26) +#define AWS_MQTT5_PROPERTY_TYPE_SERVER_REFERENCE ((uint8_t)28) +#define AWS_MQTT5_PROPERTY_TYPE_REASON_STRING ((uint8_t)31) +#define AWS_MQTT5_PROPERTY_TYPE_RECEIVE_MAXIMUM ((uint8_t)33) +#define AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS_MAXIMUM ((uint8_t)34) +#define AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS ((uint8_t)35) +#define AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_QOS ((uint8_t)36) +#define AWS_MQTT5_PROPERTY_TYPE_RETAIN_AVAILABLE ((uint8_t)37) +#define AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY ((uint8_t)38) +#define AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_PACKET_SIZE ((uint8_t)39) +#define AWS_MQTT5_PROPERTY_TYPE_WILDCARD_SUBSCRIPTIONS_AVAILABLE ((uint8_t)40) +#define AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIERS_AVAILABLE ((uint8_t)41) +#define AWS_MQTT5_PROPERTY_TYPE_SHARED_SUBSCRIPTIONS_AVAILABLE ((uint8_t)42) + +/* decode/encode bit masks and positions */ +#define AWS_MQTT5_CONNECT_FLAGS_WILL_BIT (1U << 2) +#define AWS_MQTT5_CONNECT_FLAGS_CLEAN_START_BIT (1U << 1) +#define AWS_MQTT5_CONNECT_FLAGS_USER_NAME_BIT (1U << 7) +#define AWS_MQTT5_CONNECT_FLAGS_PASSWORD_BIT (1U << 6) +#define AWS_MQTT5_CONNECT_FLAGS_WILL_RETAIN_BIT (1U << 5) + +#define AWS_MQTT5_CONNECT_FLAGS_WILL_QOS_BIT_POSITION 3 +#define AWS_MQTT5_CONNECT_FLAGS_WILL_QOS_BIT_MASK 0x03 + +#define AWS_MQTT5_SUBSCRIBE_FLAGS_NO_LOCAL (1U << 2) +#define AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_AS_PUBLISHED (1U << 3) + +#define AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_HANDLING_TYPE_BIT_POSITION 4 +#define AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_HANDLING_TYPE_BIT_MASK 0x03 +#define AWS_MQTT5_SUBSCRIBE_FLAGS_QOS_BIT_POSITION 0 +#define AWS_MQTT5_SUBSCRIBE_FLAGS_QOS_BIT_MASK 0x03 + +/* Static AWS IoT Core Limit/Quota Values */ +#define AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH 128 +#define AWS_IOT_CORE_MAXIMUM_TOPIC_LENGTH 256 +#define AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS 8 +#define AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE 8 + +/* Dynamic IoT Core Limits */ +#define AWS_IOT_CORE_PUBLISH_PER_SECOND_LIMIT 100 +#define AWS_IOT_CORE_THROUGHPUT_LIMIT (512 * 1024) + +/* Client configuration defaults when parameter left zero */ +#define AWS_MQTT5_DEFAULT_SOCKET_CONNECT_TIMEOUT_MS 10000 +#define AWS_MQTT5_CLIENT_DEFAULT_MIN_RECONNECT_DELAY_MS 1000 +#define AWS_MQTT5_CLIENT_DEFAULT_MAX_RECONNECT_DELAY_MS 120000 +#define AWS_MQTT5_CLIENT_DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS 30000 +#define AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS 30000 +#define AWS_MQTT5_CLIENT_DEFAULT_CONNACK_TIMEOUT_MS 20000 +#define AWS_MQTT5_CLIENT_DEFAULT_OPERATION_TIMEOUNT_SECONDS 60 +#define AWS_MQTT5_CLIENT_DEFAULT_INBOUND_TOPIC_ALIAS_CACHE_SIZE 25 +#define AWS_MQTT5_CLIENT_DEFAULT_OUTBOUND_TOPIC_ALIAS_CACHE_SIZE 25 + +AWS_EXTERN_C_BEGIN + +/** + * CONNECT packet MQTT5 prefix which includes "MQTT" encoded as a utf-8 string followed by the protocol number (5) + * + * {0x00, 0x04, "MQTT", 0x05} + */ +AWS_MQTT_API extern struct aws_byte_cursor g_aws_mqtt5_connect_protocol_cursor; + +/** + * Simple helper function to compute the first byte of an MQTT packet encoding as a function of 4 bit flags + * and the packet type. + * + * @param packet_type type of MQTT packet + * @param flags 4-bit wide flags, specific to each packet type, 0-valued for most + * @return the expected/required first byte of a packet of that type with flags set + */ +AWS_MQTT_API uint8_t aws_mqtt5_compute_fixed_header_byte1(enum aws_mqtt5_packet_type packet_type, uint8_t flags); + +AWS_MQTT_API void aws_mqtt5_negotiated_settings_log( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + enum aws_log_level level); + +/** + * Assigns and stores a client id for use on CONNECT + * + * @param negotiated_settings settings to apply client id to + * @param client_id client id to set + */ +AWS_MQTT_API int aws_mqtt5_negotiated_settings_apply_client_id( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_byte_cursor *client_id); + +/** + * Resets negotiated_settings to defaults reconciled with client set properties. + * Called on init of mqtt5 Client and just prior to a CONNECT. + * + * @param negotiated_settings struct containing settings to be set + * @param packet_connect_view Read-only snapshot of a CONNECT packet + * @return void + */ +AWS_MQTT_API void aws_mqtt5_negotiated_settings_reset( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_mqtt5_packet_connect_view *packet_connect_view); + +/** + * Checks properties received from Server CONNACK and reconcile with negotiated_settings + * + * @param negotiated_settings struct containing settings to be set + * @param connack_data Read-only snapshot of a CONNACK packet + * @return void + */ +AWS_MQTT_API void aws_mqtt5_negotiated_settings_apply_connack( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_mqtt5_packet_connack_view *connack_data); + +/** + * Converts a disconnect reason code into the Reason Code Name, as it appears in the mqtt5 spec. + * + * @param reason_code a disconnect reason code + * @return name associated with the reason code + */ +AWS_MQTT_API const char *aws_mqtt5_disconnect_reason_code_to_c_string( + enum aws_mqtt5_disconnect_reason_code reason_code, + bool *is_valid); + +/** + * Converts a connect reason code into the Reason Code Name, as it appears in the mqtt5 spec. + * + * @param reason_code a connect reason code + * @return name associated with the reason code + */ +AWS_MQTT_API const char *aws_mqtt5_connect_reason_code_to_c_string(enum aws_mqtt5_connect_reason_code reason_code); + +/** + * Converts a publish reason code into the Reason Code Name, as it appears in the mqtt5 spec. + * + * @param reason_code a publish reason code + * @return name associated with the reason code + */ +AWS_MQTT_API const char *aws_mqtt5_puback_reason_code_to_c_string(enum aws_mqtt5_puback_reason_code reason_code); + +/** + * Converts a subscribe reason code into the Reason Code Name, as it appears in the mqtt5 spec. + * + * @param reason_code a subscribe reason code + * @return name associated with the reason code + */ +AWS_MQTT_API const char *aws_mqtt5_suback_reason_code_to_c_string(enum aws_mqtt5_suback_reason_code reason_code); + +/** + * Converts a unsubscribe reason code into the Reason Code Name, as it appears in the mqtt5 spec. + * + * @param reason_code an unsubscribe reason code + * @return name associated with the reason code + */ +AWS_MQTT_API const char *aws_mqtt5_unsuback_reason_code_to_c_string(enum aws_mqtt5_unsuback_reason_code reason_code); + +/** + * Converts a session behavior type value to a readable description. + * + * @param session_behavior type of session behavior + * @return short string describing the session behavior + */ +AWS_MQTT_API const char *aws_mqtt5_client_session_behavior_type_to_c_string( + enum aws_mqtt5_client_session_behavior_type session_behavior); + +/** + * Converts a session behavior type value to a final non-default value. + * + * @param session_behavior type of session behavior + * @return session behavior value where default has been mapped to its intended meaning + */ +AWS_MQTT_API enum aws_mqtt5_client_session_behavior_type aws_mqtt5_client_session_behavior_type_to_non_default( + enum aws_mqtt5_client_session_behavior_type session_behavior); + +/** + * Converts an outbound topic aliasing behavior type value to a readable description. + * + * @param outbound_aliasing_behavior type of outbound topic aliasing behavior + * @return short string describing the outbound topic aliasing behavior + */ +AWS_MQTT_API const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior); + +/** + * Converts an outbound topic aliasing behavior type value to a final non-default value. + * + * @param outbound_aliasing_behavior type of outbound topic aliasing behavior + * @return outbound topic aliasing value where default has been mapped to its intended meaning + */ +AWS_MQTT_API enum aws_mqtt5_client_outbound_topic_alias_behavior_type + aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior); + +/** + * Converts an inbound topic aliasing behavior type value to a readable description. + * + * @param inbound_aliasing_behavior type of inbound topic aliasing behavior + * @return short string describing the inbound topic aliasing behavior + */ +AWS_MQTT_API const char *aws_mqtt5_inbound_topic_alias_behavior_type_to_c_string( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior); + +/** + * Converts an inbound topic aliasing behavior type value to a final non-default value. + * + * @param inbound_aliasing_behavior type of inbound topic aliasing behavior + * @return inbound topic aliasing value where default has been mapped to its intended meaning + */ +AWS_MQTT_API enum aws_mqtt5_client_inbound_topic_alias_behavior_type + aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior); + +/** + * Converts an extended validation and flow control options value to a readable description. + * + * @param extended_validation_behavior type of extended validation and flow control + * @return short string describing the extended validation and flow control behavior + */ +AWS_MQTT_API const char *aws_mqtt5_extended_validation_and_flow_control_options_to_c_string( + enum aws_mqtt5_extended_validation_and_flow_control_options extended_validation_behavior); + +/** + * Converts an offline queue behavior type value to a readable description. + * + * @param offline_queue_behavior type of offline queue behavior + * @return short string describing the offline queue behavior + */ +AWS_MQTT_API const char *aws_mqtt5_client_operation_queue_behavior_type_to_c_string( + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior); + +/** + * Converts an offline queue behavior type value to a final non-default value. + * + * @param offline_queue_behavior type of offline queue behavior + * @return offline queue behavior value where default has been mapped to its intended meaning + */ +AWS_MQTT_API enum aws_mqtt5_client_operation_queue_behavior_type + aws_mqtt5_client_operation_queue_behavior_type_to_non_default( + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior); + +/** + * Converts a lifecycle event type value to a readable description. + * + * @param lifecycle_event type of lifecycle event + * @return short string describing the lifecycle event type + */ +AWS_MQTT_API const char *aws_mqtt5_client_lifecycle_event_type_to_c_string( + enum aws_mqtt5_client_lifecycle_event_type lifecycle_event); + +/** + * Converts a payload format indicator value to a readable description. + * + * @param format_indicator type of payload format indicator + * @return short string describing the payload format indicator + */ +AWS_MQTT_API const char *aws_mqtt5_payload_format_indicator_to_c_string( + enum aws_mqtt5_payload_format_indicator format_indicator); + +/** + * Converts a retain handling type value to a readable description. + * + * @param retain_handling_type type of retain handling + * @return short string describing the retain handling type + */ +AWS_MQTT_API const char *aws_mqtt5_retain_handling_type_to_c_string( + enum aws_mqtt5_retain_handling_type retain_handling_type); + +/** + * Converts a packet type value to a readable description. + * + * @param packet_type type of packet + * @return short string describing the packet type + */ +AWS_MQTT_API const char *aws_mqtt5_packet_type_to_c_string(enum aws_mqtt5_packet_type packet_type); + +/** + * Computes a uniformly-distributed random number in the specified range. Not intended for cryptographic purposes. + * + * @param from one end of the range to sample from + * @param to other end of the range to sample from + * @return a random number from the supplied range, with roughly a uniform distribution + */ +AWS_MQTT_API uint64_t aws_mqtt5_client_random_in_range(uint64_t from, uint64_t to); + +/** + * Utility function to skip the "$aws/rules//" prefix of a topic. Technically this works for topic + * filters too. + * + * @param topic_cursor topic to get the non-rules suffix for + * @return remaining part of the topic after the leading AWS IoT Rules prefix has been skipped, if present + */ +AWS_MQTT_API struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte_cursor topic_cursor); + +/** + * Computes the number of topic segments in a topic or topic filter + * @param topic_cursor topic or topic filter + * @return number of topic segments in the topic or topic filter + */ +AWS_MQTT_API size_t aws_mqtt5_topic_get_segment_count(struct aws_byte_cursor topic_cursor); + +/** + * Checks a topic filter for validity against AWS IoT Core rules + * @param topic_filter_cursor topic filter to check + * @return true if valid, false otherwise + */ +AWS_MQTT_API bool aws_mqtt_is_valid_topic_filter_for_iot_core(struct aws_byte_cursor topic_filter_cursor); + +/** + * Checks a topic for validity against AWS IoT Core rules + * @param topic_cursor topic to check + * @return true if valid, false otherwise + */ +AWS_MQTT_API bool aws_mqtt_is_valid_topic_for_iot_core(struct aws_byte_cursor topic_cursor); + +/** + * Checks if a topic filter matches a shared subscription according to the mqtt5 spec + * @param topic_cursor topic to check + * @return true if this matches the definition of a shared subscription, false otherwise + */ +AWS_MQTT_API bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_cursor); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_UTILS_H */ diff --git a/include/aws/mqtt/private/v5/rate_limiters.h b/include/aws/mqtt/private/v5/rate_limiters.h new file mode 100644 index 00000000..23cb1880 --- /dev/null +++ b/include/aws/mqtt/private/v5/rate_limiters.h @@ -0,0 +1,110 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_RATE_LIMITERS_H +#define AWS_RATE_LIMITERS_H + +#include + +#include + +struct aws_rate_limiter_token_bucket_options { + /* Clock function override. If left null, the high resolution clock will be used */ + aws_io_clock_fn *clock_fn; + + /* How many tokens regenerate per second? */ + uint64_t tokens_per_second; + + /* Initial amount of tokens the limiter will start with */ + uint64_t initial_token_count; + + /* + * Maximum amount of tokens the limiter can hold. Regenerated tokens that exceed this maximum are + * discarded + */ + uint64_t maximum_token_count; +}; + +/** + * A token-bucket based rate limiter. + * + * Has an unusually complex implementation due to implementer-desired constraints: + * + * (1) Model regeneration as an integral rate per second. This is for ease-of-use. A regeneration interval would + * be a much simpler implementation, but not as intuitive (or accurate for non-integral rates). + * (2) Integer math only. Not comfortable falling back on doubles and not having a good understanding of the + * accuracy issues, over time, that doing so would create. + * (3) Minimize as much as possible the dangers of multiplication saturation and integer division round-down. + * (4) No integer division round-off "error" accumulation allowed. Arguments could be made that it might be small + * enough to never make a difference but I'd rather not even have the argument at all. + * (5) A perfectly accurate how-long-must-I-wait query. Not just a safe over-estimate. + */ +struct aws_rate_limiter_token_bucket { + uint64_t last_service_time; + uint64_t current_token_count; + + uint64_t fractional_nanos; + uint64_t fractional_nano_tokens; + + struct aws_rate_limiter_token_bucket_options config; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Initializes a token-bucket-based rate limiter + * + * @param limiter rate limiter to intiialize + * @param options configuration values for the token bucket rate limiter + * @return AWS_OP_SUCCESS/AWS_OP_ERR + */ +AWS_MQTT_API int aws_rate_limiter_token_bucket_init( + struct aws_rate_limiter_token_bucket *limiter, + const struct aws_rate_limiter_token_bucket_options *options); + +/** + * Resets a token-bucket-based rate limiter + * + * @param limiter rate limiter to reset + */ +AWS_MQTT_API void aws_rate_limiter_token_bucket_reset(struct aws_rate_limiter_token_bucket *limiter); + +/** + * Queries if the token bucket has a number of tokens currently available + * + * @param limiter token bucket rate limiter to query, non-const because token count is lazily updated + * @param token_count how many tokens to check for + * @return true if that many tokens are available, false otherwise + */ +AWS_MQTT_API bool aws_rate_limiter_token_bucket_can_take_tokens( + struct aws_rate_limiter_token_bucket *limiter, + uint64_t token_count); + +/** + * Takes a number of tokens from the token bucket rate limiter + * + * @param limiter token bucket rate limiter to take from + * @param token_count how many tokens to take + * @return AWS_OP_SUCCESS if there were that many tokens available, AWS_OP_ERR otherwise + */ +AWS_MQTT_API int aws_rate_limiter_token_bucket_take_tokens( + struct aws_rate_limiter_token_bucket *limiter, + uint64_t token_count); + +/** + * Queries a token-bucket-based rate limiter for how long, in nanoseconds, until a specified amount of tokens will + * be available. + * + * @param limiter token-bucket-based rate limiter to query + * @param token_count how many tokens need to be avilable + * @return how long the caller must wait, in nanoseconds, before that many tokens are available + */ +AWS_MQTT_API uint64_t aws_rate_limiter_token_bucket_compute_wait_for_tokens( + struct aws_rate_limiter_token_bucket *limiter, + uint64_t token_count); + +AWS_EXTERN_C_END + +#endif /* AWS_RATE_LIMITERS_H */ diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h new file mode 100644 index 00000000..247d9d2d --- /dev/null +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -0,0 +1,785 @@ +#ifndef AWS_MQTT_MQTT5_CLIENT_H +#define AWS_MQTT_MQTT5_CLIENT_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +struct aws_allocator; +struct aws_client_bootstrap; +struct aws_http_message; +struct aws_input_stream; +struct aws_mqtt5_client; +struct aws_mqtt5_client_lifecycle_event; +struct aws_tls_connection_options; +struct aws_socket_options; + +/* public client-related enums */ + +/** + * Controls how the mqtt client should behave with respect to mqtt sessions. + */ +enum aws_mqtt5_client_session_behavior_type { + /** + * Maps to AWS_MQTT5_CSBT_CLEAN + */ + AWS_MQTT5_CSBT_DEFAULT, + + /** + * Always join a new, clean session + */ + AWS_MQTT5_CSBT_CLEAN, + + /** + * Always attempt to rejoin an existing session after an initial connection success. + */ + AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS, +}; + +/** + * Outbound topic aliasing behavior is controlled by this type. + * + * Topic alias behavior is described in https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901113 + * + * If the server allows topic aliasing, this setting controls how topic aliases are used on PUBLISH packets sent + * from the client to the server. + * + * If topic aliasing is not supported by the server, this setting has no effect and any attempts to directly + * manipulate the topic alias id in outbound publishes will be ignored. + */ +enum aws_mqtt5_client_outbound_topic_alias_behavior_type { + /** + * Maps to AWS_MQTT5_COTABT_LRU at the moment + */ + AWS_MQTT5_COTABT_DEFAULT, + + /** + * Outbound aliasing is the user's responsibility. Client will cache and use + * previously-established aliases if they fall within the negotiated limits of the connection. + * + * The user must still always submit a full topic in their publishes because disconnections disrupt + * topic alias mappings unpredictably. The client will properly use the alias when the current connection + * has seen the alias binding already. + */ + AWS_MQTT5_COTABT_USER, + + /** + * Client fails any user-specified topic aliasing and acts on the outbound alias set as an LRU cache. + */ + AWS_MQTT5_COTABT_LRU, + + /** + * Completely disable outbound topic aliasing. Attempting to set a topic alias on a PUBLISH results in + * an error. + */ + AWS_MQTT5_COTABT_DISABLED +}; + +/** + * Inbound topic aliasing behavior is controlled by this type. + * + * Topic alias behavior is described in https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901113 + * + * This setting controls whether or not the client will send a positive topic alias maximum to the server + * in its CONNECT packets. + * + * If topic aliasing is not supported by the server, this setting has no net effect. + */ +enum aws_mqtt5_client_inbound_topic_alias_behavior_type { + /** + * Maps to AWS_MQTT5_CITABT_DISABLED + */ + AWS_MQTT5_CITABT_DEFAULT, + + /** + * Allow the server to send PUBLISH packets to the client that use topic aliasing + */ + AWS_MQTT5_CITABT_ENABLED, + + /** + * Forbid the server from sending PUBLISH packets to the client that use topic aliasing + */ + AWS_MQTT5_CITABT_DISABLED +}; + +/** + * Configuration struct for all client topic aliasing behavior. If this is left null, then all default options + * (as it zeroed) will be used. + */ +struct aws_mqtt5_client_topic_alias_options { + + /** + * Controls what kind of outbound topic aliasing behavior the client should attempt to use. + */ + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_topic_alias_behavior; + + /** + * If outbound topic aliasing is set to LRU, this controls the maximum size of the cache. If outbound topic + * aliasing is set to LRU and this is zero, a sensible default is used (25). If outbound topic aliasing is not + * set to LRU, then this setting has no effect. + * + * The final size of the cache is determined by the minimum of this setting and the value of the + * topic_alias_maximum property of the received CONNACK. If the received CONNACK does not have an explicit + * positive value for that field, outbound topic aliasing is disabled for the duration of that connection. + */ + uint16_t outbound_alias_cache_max_size; + + /** + * Controls what kind of inbound topic aliasing behavior the client should use. + * + * Even if inbound topic aliasing is enabled, it is up to the server to choose whether or not to use it. + */ + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_topic_alias_behavior; + + /** + * If inbound topic aliasing is enabled, this will control the size of the inbound alias cache. If inbound + * aliases are enabled and this is zero, then a sensible default will be used (25). If inbound aliases are + * disabled, this setting has no effect. + * + * Behaviorally, this value overrides anything present in the topic_alias_maximum field of + * the CONNECT packet options. We intentionally don't bind that field to managed clients to reduce + */ + uint16_t inbound_alias_cache_size; +}; + +/** + * Extended validation and flow control options + * + * Potentially a point of expansion in the future. We could add custom controls letting people override + * the Aws IOT Core limits based on their account properties. We could, with IoT Core support, add dynamic + * limit recognition via user properties as well. + */ +enum aws_mqtt5_extended_validation_and_flow_control_options { + /** + * Do not do any additional validation or flow control outside of the MQTT5 spec + */ + AWS_MQTT5_EVAFCO_NONE, + + /** + * Apply additional client-side validation and operational flow control that respects the + * default AWS IoT Core limits. + * + * Currently applies the following additional validation: + * (1) No more than 8 subscriptions per SUBSCRIBE packet + * (2) Topics and topic filters have a maximum of 7 slashes (8 segments), not counting any AWS rules prefix + * (3) Topics must be <= 256 bytes in length + * (4) Client id must be <= 128 bytes in length + * + * Also applies the following flow control: + * (1) Outbound throughput throttled to 512KB/s + * (2) Outbound publish TPS throttled to 100 + */ + AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS, +}; + +/** + * Controls how disconnects affect the queued and in-progress operations submitted to the client. Also controls + * how operations are handled while the client is not connected. In particular, if the client is not connected, + * then any operation that would be failed on disconnect (according to these rules) will be rejected. + */ +enum aws_mqtt5_client_operation_queue_behavior_type { + + /* + * Maps to AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT + */ + AWS_MQTT5_COQBT_DEFAULT, + + /* + * Requeues QoS 1+ publishes on disconnect; unacked publishes go to the front, unprocessed publishes stay + * in place. All other operations (QoS 0 publishes, subscribe, unsubscribe) are failed. + */ + AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT, + + /* + * Qos 0 publishes that are not complete at the time of disconnection are failed. Unacked QoS 1+ publishes are + * requeued at the head of the line for immediate retransmission on a session resumption. All other operations + * are requeued in original order behind any retransmissions. + */ + AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT, + + /* + * All operations that are not complete at the time of disconnection are failed, except those operations that + * the mqtt 5 spec requires to be retransmitted (unacked qos1+ publishes). + */ + AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT, +}; + +/** + * Type of a client lifecycle event + */ +enum aws_mqtt5_client_lifecycle_event_type { + /** + * Emitted when the client begins an attempt to connect to the remote endpoint. + * + * Mandatory event fields: client, user_data + */ + AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + + /** + * Emitted after the client connects to the remote endpoint and receives a successful CONNACK. + * Every ATTEMPTING_CONNECT will be followed by exactly one CONNECTION_SUCCESS or one CONNECTION_FAILURE. + * + * Mandatory event fields: client, user_data, connack_data, settings + */ + AWS_MQTT5_CLET_CONNECTION_SUCCESS, + + /** + * Emitted at any point during the connection process when it has conclusively failed. + * Every ATTEMPTING_CONNECT will be followed by exactly one CONNECTION_SUCCESS or one CONNECTION_FAILURE. + * + * Mandatory event fields: client, user_data, error_code + * Conditional event fields: connack_data + */ + AWS_MQTT5_CLET_CONNECTION_FAILURE, + + /** + * Lifecycle event containing information about a disconnect. Every CONNECTION_SUCCESS will eventually be + * followed by one and only one DISCONNECTION. + * + * Mandatory event fields: client, user_data, error_code + * Conditional event fields: disconnect_data + */ + AWS_MQTT5_CLET_DISCONNECTION, + + /** + * Lifecycle event notifying the user that the client has entered the STOPPED state. Entering this state will + * cause the client to wipe all MQTT session state. + * + * Mandatory event fields: client, user_data + */ + AWS_MQTT5_CLET_STOPPED, +}; + +/* client-related callback function signatures */ + +/** + * Signature of the continuation function to be called after user-code transforms a websocket handshake request + */ +typedef void(aws_mqtt5_transform_websocket_handshake_complete_fn)( + struct aws_http_message *request, + int error_code, + void *complete_ctx); + +/** + * Signature of the websocket handshake request transformation function. After transformation, the completion + * function must be invoked to send the request. + */ +typedef void(aws_mqtt5_transform_websocket_handshake_fn)( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx); + +/** + * Callback signature for mqtt5 client lifecycle events. + */ +typedef void(aws_mqtt5_client_connection_event_callback_fn)(const struct aws_mqtt5_client_lifecycle_event *event); + +/** + * Signature of callback to invoke on Publish success/failure. + */ +typedef void(aws_mqtt5_publish_completion_fn)( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx); + +/** + * Signature of callback to invoke on Subscribe success/failure. + */ +typedef void(aws_mqtt5_subscribe_completion_fn)( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx); + +/** + * Signature of callback to invoke on Unsubscribe success/failure. + */ +typedef void(aws_mqtt5_unsubscribe_completion_fn)( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx); + +/** + * Signature of callback to invoke on Publish received + */ +typedef void(aws_mqtt5_publish_received_fn)(const struct aws_mqtt5_packet_publish_view *publish, void *user_data); + +/** + * Signature of callback to invoke when a DISCONNECT is fully written to the socket (or fails to be) + */ +typedef void(aws_mqtt5_disconnect_completion_fn)(int error_code, void *complete_ctx); + +/** + * Signature of callback invoked when a client has completely destroyed itself + */ +typedef void(aws_mqtt5_client_termination_completion_fn)(void *complete_ctx); + +/* operation completion options structures */ + +/** + * Completion callback options for the Publish operation + */ +struct aws_mqtt5_publish_completion_options { + aws_mqtt5_publish_completion_fn *completion_callback; + void *completion_user_data; +}; + +/** + * Completion callback options for the Subscribe operation + */ +struct aws_mqtt5_subscribe_completion_options { + aws_mqtt5_subscribe_completion_fn *completion_callback; + void *completion_user_data; +}; + +/** + * Completion callback options for the Unsubscribe operation + */ +struct aws_mqtt5_unsubscribe_completion_options { + aws_mqtt5_unsubscribe_completion_fn *completion_callback; + void *completion_user_data; +}; + +/** + * Public completion callback options for the a DISCONNECT operation + */ +struct aws_mqtt5_disconnect_completion_options { + aws_mqtt5_disconnect_completion_fn *completion_callback; + void *completion_user_data; +}; + +/** + * Mqtt behavior settings that are dynamically negotiated as part of the CONNECT/CONNACK exchange. + */ +struct aws_mqtt5_negotiated_settings { + /** + * The maximum QoS used between the server and client. + */ + enum aws_mqtt5_qos maximum_qos; + + /** + * the amount of time in seconds the server will retain the session after a disconnect. + */ + uint32_t session_expiry_interval; + + /** + * the number of QoS 1 and QoS2 publications the server is willing to process concurrently. + */ + uint16_t receive_maximum_from_server; + + /** + * the maximum packet size the server is willing to accept. + */ + uint32_t maximum_packet_size_to_server; + + /** + * the highest value that the server will accept as a Topic Alias sent by the client. + */ + uint16_t topic_alias_maximum_to_server; + + /** + * the highest value that the client will accept as a Topic Alias sent by the server. + */ + uint16_t topic_alias_maximum_to_client; + + /** + * the amount of time in seconds before the server will disconnect the client for inactivity. + */ + uint16_t server_keep_alive; + + /** + * whether the server supports retained messages. + */ + bool retain_available; + + /** + * whether the server supports wildcard subscriptions. + */ + bool wildcard_subscriptions_available; + + /** + * whether the server supports subscription identifiers + */ + bool subscription_identifiers_available; + + /** + * whether the server supports shared subscriptions + */ + bool shared_subscriptions_available; + + /** + * whether the client has rejoined an existing session. + */ + bool rejoined_session; + + struct aws_byte_buf client_id_storage; +}; + +/** + * Contains some simple statistics about the current state of the client's queue of operations + */ +struct aws_mqtt5_client_operation_statistics { + /* + * total number of operations submitted to the client that have not yet been completed. Unacked operations + * are a subset of this. + */ + uint64_t incomplete_operation_count; + + /* + * total packet size of operations submitted to the client that have not yet been completed. Unacked operations + * are a subset of this. + */ + uint64_t incomplete_operation_size; + + /* + * total number of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + uint64_t unacked_operation_count; + + /* + * total packet size of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + uint64_t unacked_operation_size; +}; + +/** + * Details about a client lifecycle event. + */ +struct aws_mqtt5_client_lifecycle_event { + + /** + * Type of event this is. + */ + enum aws_mqtt5_client_lifecycle_event_type event_type; + + /** + * Client this event corresponds to. Necessary (can't be replaced with user data) because the client + * doesn't exist at the time the event callback user data is configured. + */ + struct aws_mqtt5_client *client; + + /** + * Aws-c-* error code associated with the event + */ + int error_code; + + /** + * User data associated with the client's lifecycle event handler. Set with client configuration. + */ + void *user_data; + + /** + * If this event was caused by receiving a CONNACK, this will be a view of that packet. + */ + const struct aws_mqtt5_packet_connack_view *connack_data; + + /** + * If this is a successful connection establishment, this will contain the negotiated mqtt5 behavioral settings + */ + const struct aws_mqtt5_negotiated_settings *settings; + + /** + * If this event was caused by receiving a DISCONNECT, this will be a view of that packet. + */ + const struct aws_mqtt5_packet_disconnect_view *disconnect_data; +}; + +/** + * Basic mqtt5 client configuration struct. + * + * Contains desired connection properties + * Configuration that represents properties of the mqtt5 CONNECT packet go in the connect view (connect_options) + */ +struct aws_mqtt5_client_options { + + /** + * Host to establish mqtt connections to + */ + struct aws_byte_cursor host_name; + + /** + * Port to establish mqtt connections to + */ + uint16_t port; + + /** + * Client bootstrap to use whenever this client establishes a connection + */ + struct aws_client_bootstrap *bootstrap; + + /** + * Socket options to use whenever this client establishes a connection + */ + const struct aws_socket_options *socket_options; + + /** + * (Optional) Tls options to use whenever this client establishes a connection + */ + const struct aws_tls_connection_options *tls_options; + + /** + * (Optional) Http proxy options to use whenever this client establishes a connection + */ + const struct aws_http_proxy_options *http_proxy_options; + + /** + * (Optional) Websocket handshake transformation function and user data. Websockets are used if the + * transformation function is non-null. + */ + aws_mqtt5_transform_websocket_handshake_fn *websocket_handshake_transform; + void *websocket_handshake_transform_user_data; + + /** + * All CONNECT-related options, includes the will configuration, if desired + */ + const struct aws_mqtt5_packet_connect_view *connect_options; + + /** + * Controls session rejoin behavior + */ + enum aws_mqtt5_client_session_behavior_type session_behavior; + + /** + * Controls if any additional AWS-specific validation or flow control should be performed by the client. + */ + enum aws_mqtt5_extended_validation_and_flow_control_options extended_validation_and_flow_control_options; + + /** + * Controls how the client treats queued/in-progress operations when the connection drops for any reason. + */ + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior; + + /** + * Controls the exponential backoff behavior when the client is waiting to reconnect. + * + * See: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + */ + enum aws_exponential_backoff_jitter_mode retry_jitter_mode; + + /** + * Minimum amount of time in ms to wait before attempting to reconnect. If this is zero, a default of 1000 ms will + * be used. + */ + uint64_t min_reconnect_delay_ms; + + /** + * Maximum amount of time in ms to wait before attempting to reconnect. If this is zero, a default of 120000 ms + * will be used. + */ + uint64_t max_reconnect_delay_ms; + + /** + * Amount of time that must elapse with a good connection before the reconnect delay is reset to the minimum. If + * this zero, a default of 30000 ms will be used. + */ + uint64_t min_connected_time_to_reset_reconnect_delay_ms; + + /** + * Time interval to wait after sending a PINGREQ for a PINGRESP to arrive. If one does not arrive, the connection + * will be shut down. If this is zero, a default of 30000 ms will be used. + */ + uint32_t ping_timeout_ms; + + /** + * Time interval to wait after sending a CONNECT request for a CONNACK to arrive. If one does not arrive, the + * connection will be shut down. If this zero, a default of 20000 ms will be used. + */ + uint32_t connack_timeout_ms; + + /** + * Time interval to wait for an ack after sending a SUBSCRIBE, UNSUBSCRIBE, or PUBLISH with QoS 1+ before + * failing the packet, notifying the client of failure, and removing it. If this is zero, a default of 60 seconds + * will be used. + */ + uint32_t ack_timeout_seconds; + + /** + * Controls how the client uses mqtt5 topic aliasing. If NULL, zero-based defaults will be used. + */ + struct aws_mqtt5_client_topic_alias_options *topic_aliasing_options; + + /** + * Callback for received publish packets + */ + aws_mqtt5_publish_received_fn *publish_received_handler; + void *publish_received_handler_user_data; + + /** + * Callback and user data for all client lifecycle events. + * Life cycle events include: + * ConnectionSuccess + * ConnectionFailure, + * Disconnect + * (client) Stopped + * + * Disconnect lifecycle events are 1-1 with -- strictly after -- ConnectionSuccess events. + */ + aws_mqtt5_client_connection_event_callback_fn *lifecycle_event_handler; + void *lifecycle_event_handler_user_data; + + /** + * Callback for when the client has completely destroyed itself + */ + aws_mqtt5_client_termination_completion_fn *client_termination_handler; + void *client_termination_handler_user_data; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Creates a new mqtt5 client using the supplied configuration + * + * @param allocator allocator to use with all memory operations related to this client's creation and operation + * @param options mqtt5 client configuration + * @return a new mqtt5 client or NULL + */ +AWS_MQTT_API +struct aws_mqtt5_client *aws_mqtt5_client_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client_options *options); + +/** + * Acquires a reference to an mqtt5 client + * + * @param client client to acquire a reference to. May be NULL. + * @return what was passed in as the client (a client or NULL) + */ +AWS_MQTT_API +struct aws_mqtt5_client *aws_mqtt5_client_acquire(struct aws_mqtt5_client *client); + +/** + * Release a reference to an mqtt5 client. When the client ref count drops to zero, the client will automatically + * trigger a stop and once the stop completes, the client will delete itself. + * + * @param client client to release a reference to. May be NULL. + * @return NULL + */ +AWS_MQTT_API +struct aws_mqtt5_client *aws_mqtt5_client_release(struct aws_mqtt5_client *client); + +/** + * Asynchronous notify to the mqtt5 client that you want it to attempt to connect to the configured endpoint. + * The client will attempt to stay connected using the properties of the reconnect-related parameters + * in the mqtt5 client configuration. + * + * @param client mqtt5 client to start + * @return success/failure in the synchronous logic that kicks off the start process + */ +AWS_MQTT_API +int aws_mqtt5_client_start(struct aws_mqtt5_client *client); + +/** + * Asynchronous notify to the mqtt5 client that you want it to transition to the stopped state. When the client + * reaches the stopped state, all session state is erased. + * + * @param client mqtt5 client to stop + * @param disconnect_options (optional) properties of a DISCONNECT packet to send as part of the shutdown process + * @return success/failure in the synchronous logic that kicks off the stop process + */ +AWS_MQTT_API +int aws_mqtt5_client_stop( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_disconnect_view *disconnect_options, + const struct aws_mqtt5_disconnect_completion_options *completion_options); + +/** + * Queues a Publish operation in an mqtt5 client + * + * @param client mqtt5 client to queue a Publish for + * @param publish_options configuration options for the Publish operation + * @param completion_options completion callback configuration. Successful QoS 0 publishes invoke the callback when + * the data has been written to the socket. Successful QoS1+ publishes invoke the callback when the corresponding ack + * is received. Unsuccessful publishes invoke the callback at the point in time a failure condition is reached. + * @return success/failure in the synchronous logic that kicks off the publish operation + */ +AWS_MQTT_API +int aws_mqtt5_client_publish( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_publish_view *publish_options, + const struct aws_mqtt5_publish_completion_options *completion_options); + +/** + * Queues a Subscribe operation in an mqtt5 client + * + * @param client mqtt5 client to queue a Subscribe for + * @param subscribe_options configuration options for the Subscribe operation + * @param completion_options Completion callback configuration. Invoked when the corresponding SUBACK is received or + * a failure condition is reached. An error code implies complete failure of the subscribe, while a success code + * implies the user must still check all of the SUBACK's reason codes for per-subscription feedback. + * @return success/failure in the synchronous logic that kicks off the Subscribe operation + */ +AWS_MQTT_API +int aws_mqtt5_client_subscribe( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options, + const struct aws_mqtt5_subscribe_completion_options *completion_options); + +/** + * Queues an Unsubscribe operation in an mqtt5 client + * + * @param client mqtt5 client to queue an Unsubscribe for + * @param unsubscribe_options configuration options for the Unsubscribe operation + * @param completion_options Completion callback configuration. Invoked when the corresponding UNSUBACK is received or + * a failure condition is reached. An error code implies complete failure of the unsubscribe, while a success code + * implies the user must still check all of the UNSUBACK's reason codes for per-topic-filter feedback. + * @return success/failure in the synchronous logic that kicks off the Unsubscribe operation + */ +AWS_MQTT_API +int aws_mqtt5_client_unsubscribe( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options, + const struct aws_mqtt5_unsubscribe_completion_options *completion_options); + +/** + * Queries the client's internal statistics for incomplete operations. + * @param client client to get statistics for + * @param stats set of incomplete operation statistics + */ +AWS_MQTT_API +void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt5_client_operation_statistics *stats); + +/* Misc related type APIs */ + +/** + * Initializes the Client ID byte buf in negotiated settings + * + * @param allocator allocator to use for memory allocation + * @param negotiated_settings settings to apply client id to + * @param client_id client id to set + */ +AWS_MQTT_API int aws_mqtt5_negotiated_settings_init( + struct aws_allocator *allocator, + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_byte_cursor *client_id); + +/** + * Makes an owning copy of a negotiated settings structure + * + * @param source settings to copy from + * @param dest settings to copy into. Must be in a zeroed or initialized state because it gets clean up + * called on it as the first step of the copy process. + * @return success/failure + */ +AWS_MQTT_API int aws_mqtt5_negotiated_settings_copy( + const struct aws_mqtt5_negotiated_settings *source, + struct aws_mqtt5_negotiated_settings *dest); + +/** + * Clean up owned memory in negotiated_settings + * + * @param negotiated_settings settings to clean up + */ +AWS_MQTT_API void aws_mqtt5_negotiated_settings_clean_up(struct aws_mqtt5_negotiated_settings *negotiated_settings); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_CLIENT_H */ diff --git a/include/aws/mqtt/v5/mqtt5_packet_storage.h b/include/aws/mqtt/v5/mqtt5_packet_storage.h new file mode 100644 index 00000000..44e7f633 --- /dev/null +++ b/include/aws/mqtt/v5/mqtt5_packet_storage.h @@ -0,0 +1,328 @@ +#ifndef AWS_MQTT_MQTT5_PACKET_STORAGE_H +#define AWS_MQTT_MQTT5_PACKET_STORAGE_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +struct aws_mqtt5_user_property_set { + struct aws_array_list properties; +}; + +struct aws_mqtt5_packet_connect_storage { + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_connect_view storage_view; + + struct aws_byte_cursor username; + + struct aws_byte_cursor password; + + uint32_t session_expiry_interval_seconds; + + uint8_t request_response_information; + + uint8_t request_problem_information; + + uint16_t receive_maximum; + + uint16_t topic_alias_maximum; + + uint32_t maximum_packet_size_bytes; + + struct aws_mqtt5_packet_publish_storage *will; + + uint32_t will_delay_interval_seconds; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_cursor authentication_method; + + struct aws_byte_cursor authentication_data; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_connack_storage { + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_connack_view storage_view; + + uint32_t session_expiry_interval; + + uint16_t receive_maximum; + + enum aws_mqtt5_qos maximum_qos; + + bool retain_available; + + uint32_t maximum_packet_size; + + struct aws_byte_cursor assigned_client_identifier; + + uint16_t topic_alias_maximum; + + struct aws_byte_cursor reason_string; + + bool wildcard_subscriptions_available; + + bool subscription_identifiers_available; + + bool shared_subscriptions_available; + + uint16_t server_keep_alive; + + struct aws_byte_cursor response_information; + + struct aws_byte_cursor server_reference; + + struct aws_byte_cursor authentication_method; + + struct aws_byte_cursor authentication_data; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_suback_storage { + + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_suback_view storage_view; + + struct aws_byte_cursor reason_string; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_array_list reason_codes; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_unsuback_storage { + + struct aws_allocator *allocator; + + struct aws_mqtt5_packet_unsuback_view storage_view; + + struct aws_byte_cursor reason_string; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_array_list reason_codes; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_publish_storage { + struct aws_mqtt5_packet_publish_view storage_view; + + enum aws_mqtt5_payload_format_indicator payload_format; + + uint32_t message_expiry_interval_seconds; + + uint16_t topic_alias; + + struct aws_byte_cursor response_topic; + + struct aws_byte_cursor correlation_data; + + struct aws_byte_cursor content_type; + + struct aws_mqtt5_user_property_set user_properties; + struct aws_array_list subscription_identifiers; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_puback_storage { + struct aws_mqtt5_packet_puback_view storage_view; + + struct aws_byte_cursor reason_string; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_disconnect_storage { + struct aws_mqtt5_packet_disconnect_view storage_view; + + uint32_t session_expiry_interval_seconds; + + struct aws_byte_cursor reason_string; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_cursor server_reference; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_subscribe_storage { + struct aws_mqtt5_packet_subscribe_view storage_view; + + uint32_t subscription_identifier; + + struct aws_array_list subscriptions; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_buf storage; +}; + +struct aws_mqtt5_packet_unsubscribe_storage { + struct aws_mqtt5_packet_unsubscribe_view storage_view; + + struct aws_array_list topic_filters; + + struct aws_mqtt5_user_property_set user_properties; + + struct aws_byte_buf storage; +}; + +AWS_EXTERN_C_BEGIN + +/* User properties */ + +AWS_MQTT_API int aws_mqtt5_user_property_set_init_with_storage( + struct aws_mqtt5_user_property_set *property_set, + struct aws_allocator *allocator, + struct aws_byte_buf *storage_buffer, + size_t property_count, + const struct aws_mqtt5_user_property *properties); + +AWS_MQTT_API void aws_mqtt5_user_property_set_clean_up(struct aws_mqtt5_user_property_set *property_set); + +AWS_MQTT_API size_t aws_mqtt5_user_property_set_size(const struct aws_mqtt5_user_property_set *property_set); + +/* Connect */ + +AWS_MQTT_API int aws_mqtt5_packet_connect_storage_init( + struct aws_mqtt5_packet_connect_storage *connect_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connect_view *connect_options); + +AWS_MQTT_API int aws_mqtt5_packet_connect_storage_init_from_external_storage( + struct aws_mqtt5_packet_connect_storage *connect_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_connect_storage_clean_up(struct aws_mqtt5_packet_connect_storage *connect_storage); + +/* Connack */ + +AWS_MQTT_API int aws_mqtt5_packet_connack_storage_init( + struct aws_mqtt5_packet_connack_storage *connack_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connack_view *connack_options); + +AWS_MQTT_API int aws_mqtt5_packet_connack_storage_init_from_external_storage( + struct aws_mqtt5_packet_connack_storage *connack_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_connack_storage_clean_up(struct aws_mqtt5_packet_connack_storage *connack_storage); + +/* Disconnect */ + +AWS_MQTT_API int aws_mqtt5_packet_disconnect_storage_init( + struct aws_mqtt5_packet_disconnect_storage *disconnect_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_disconnect_view *disconnect_options); + +AWS_MQTT_API int aws_mqtt5_packet_disconnect_storage_init_from_external_storage( + struct aws_mqtt5_packet_disconnect_storage *disconnect_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_disconnect_storage_clean_up( + struct aws_mqtt5_packet_disconnect_storage *disconnect_storage); + +/* Publish */ + +AWS_MQTT_API int aws_mqtt5_packet_publish_storage_init( + struct aws_mqtt5_packet_publish_storage *publish_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_publish_view *publish_options); + +AWS_MQTT_API int aws_mqtt5_packet_publish_storage_init_from_external_storage( + struct aws_mqtt5_packet_publish_storage *publish_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_publish_storage_clean_up(struct aws_mqtt5_packet_publish_storage *publish_storage); + +/* Puback */ + +AWS_MQTT_API int aws_mqtt5_packet_puback_storage_init( + struct aws_mqtt5_packet_puback_storage *puback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_puback_view *puback_view); + +AWS_MQTT_API int aws_mqtt5_packet_puback_storage_init_from_external_storage( + struct aws_mqtt5_packet_puback_storage *puback_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_puback_storage_clean_up(struct aws_mqtt5_packet_puback_storage *puback_storage); + +/* Subscribe */ + +AWS_MQTT_API int aws_mqtt5_packet_subscribe_storage_init( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options); + +AWS_MQTT_API int aws_mqtt5_packet_subscribe_storage_init_from_external_storage( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_subscribe_storage_clean_up( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage); + +/* Suback */ + +AWS_MQTT_API int aws_mqtt5_packet_suback_storage_init( + struct aws_mqtt5_packet_suback_storage *suback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_suback_view *suback_view); + +AWS_MQTT_API int aws_mqtt5_packet_suback_storage_init_from_external_storage( + struct aws_mqtt5_packet_suback_storage *suback_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_suback_storage_clean_up(struct aws_mqtt5_packet_suback_storage *suback_storage); + +/* Unsubscribe */ + +AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_storage_init( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options); + +AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_storage_init_from_external_storage( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_unsubscribe_storage_clean_up( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage); + +/* Unsuback */ + +AWS_MQTT_API int aws_mqtt5_packet_unsuback_storage_init( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_unsuback_view *unsuback_view); + +AWS_MQTT_API int aws_mqtt5_packet_unsuback_storage_init_from_external_storage( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_packet_unsuback_storage_clean_up( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_PACKET_STORAGE_H */ diff --git a/include/aws/mqtt/v5/mqtt5_types.h b/include/aws/mqtt/v5/mqtt5_types.h new file mode 100644 index 00000000..5f13e867 --- /dev/null +++ b/include/aws/mqtt/v5/mqtt5_types.h @@ -0,0 +1,478 @@ +#ifndef AWS_MQTT_MQTT5_TYPES_H +#define AWS_MQTT_MQTT5_TYPES_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +/** + * Some artificial (non-MQTT spec specified) limits that we place on input packets (publish, subscribe, unsubscibe) + * which lets us safely do the various packet size calculations with a bare minimum of checked arithmetic. + * + * I don't see any conceivable use cases why you'd want more than this, but they are relaxable to some degree. + * + * TODO: Add some static assert calculations that verify that we can't possibly overflow against the maximum value + * of a variable length integer for relevant packet size encodings that are absolute worst-case against these limits. + */ +#define AWS_MQTT5_CLIENT_MAXIMUM_USER_PROPERTIES 1024 +#define AWS_MQTT5_CLIENT_MAXIMUM_SUBSCRIPTIONS_PER_SUBSCRIBE 1024 +#define AWS_MQTT5_CLIENT_MAXIMUM_TOPIC_FILTERS_PER_UNSUBSCRIBE 1024 + +/** + * Over-the-wire packet id as defined in the mqtt spec. Allocated at the point in time when the packet is + * is next to go down the channel and about to be encoded into an io message buffer. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901026 + */ +typedef uint16_t aws_mqtt5_packet_id_t; + +/** + * MQTT Message delivery quality of service. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901234 + */ +enum aws_mqtt5_qos { + + /** https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901235 */ + AWS_MQTT5_QOS_AT_MOST_ONCE = 0x0, + + /** https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901236 */ + AWS_MQTT5_QOS_AT_LEAST_ONCE = 0x1, + + /** https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901237 */ + AWS_MQTT5_QOS_EXACTLY_ONCE = 0x2, +}; + +/** + * Server return code for CONNECT attempts. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901079 + */ +enum aws_mqtt5_connect_reason_code { + AWS_MQTT5_CRC_SUCCESS = 0, + AWS_MQTT5_CRC_UNSPECIFIED_ERROR = 128, + AWS_MQTT5_CRC_MALFORMED_PACKET = 129, + AWS_MQTT5_CRC_PROTOCOL_ERROR = 130, + AWS_MQTT5_CRC_IMPLEMENTATION_SPECIFIC_ERROR = 131, + AWS_MQTT5_CRC_UNSUPPORTED_PROTOCOL_VERSION = 132, + AWS_MQTT5_CRC_CLIENT_IDENTIFIER_NOT_VALID = 133, + AWS_MQTT5_CRC_BAD_USERNAME_OR_PASSWORD = 134, + AWS_MQTT5_CRC_NOT_AUTHORIZED = 135, + AWS_MQTT5_CRC_SERVER_UNAVAILABLE = 136, + AWS_MQTT5_CRC_SERVER_BUSY = 137, + AWS_MQTT5_CRC_BANNED = 138, + AWS_MQTT5_CRC_BAD_AUTHENTICATION_METHOD = 140, + AWS_MQTT5_CRC_TOPIC_NAME_INVALID = 144, + AWS_MQTT5_CRC_PACKET_TOO_LARGE = 149, + AWS_MQTT5_CRC_QUOTA_EXCEEDED = 151, + AWS_MQTT5_CRC_PAYLOAD_FORMAT_INVALID = 153, + AWS_MQTT5_CRC_RETAIN_NOT_SUPPORTED = 154, + AWS_MQTT5_CRC_QOS_NOT_SUPPORTED = 155, + AWS_MQTT5_CRC_USE_ANOTHER_SERVER = 156, + AWS_MQTT5_CRC_SERVER_MOVED = 157, + AWS_MQTT5_CRC_CONNECTION_RATE_EXCEEDED = 159, +}; + +/** + * Reason code inside DISCONNECT packets. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901208 + */ +enum aws_mqtt5_disconnect_reason_code { + AWS_MQTT5_DRC_NORMAL_DISCONNECTION = 0, + AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE = 4, + AWS_MQTT5_DRC_UNSPECIFIED_ERROR = 128, + AWS_MQTT5_DRC_MALFORMED_PACKET = 129, + AWS_MQTT5_DRC_PROTOCOL_ERROR = 130, + AWS_MQTT5_DRC_IMPLEMENTATION_SPECIFIC_ERROR = 131, + AWS_MQTT5_DRC_NOT_AUTHORIZED = 135, + AWS_MQTT5_DRC_SERVER_BUSY = 137, + AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN = 139, + AWS_MQTT5_DRC_KEEP_ALIVE_TIMEOUT = 141, + AWS_MQTT5_DRC_SESSION_TAKEN_OVER = 142, + AWS_MQTT5_DRC_TOPIC_FILTER_INVALID = 143, + AWS_MQTT5_DRC_TOPIC_NAME_INVALID = 144, + AWS_MQTT5_DRC_RECEIVE_MAXIMUM_EXCEEDED = 147, + AWS_MQTT5_DRC_TOPIC_ALIAS_INVALID = 148, + AWS_MQTT5_DRC_PACKET_TOO_LARGE = 149, + AWS_MQTT5_DRC_MESSAGE_RATE_TOO_HIGH = 150, + AWS_MQTT5_DRC_QUOTA_EXCEEDED = 151, + AWS_MQTT5_DRC_ADMINISTRATIVE_ACTION = 152, + AWS_MQTT5_DRC_PAYLOAD_FORMAT_INVALID = 153, + AWS_MQTT5_DRC_RETAIN_NOT_SUPPORTED = 154, + AWS_MQTT5_DRC_QOS_NOT_SUPPORTED = 155, + AWS_MQTT5_DRC_USE_ANOTHER_SERVER = 156, + AWS_MQTT5_DRC_SERVER_MOVED = 157, + AWS_MQTT5_DRC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED = 158, + AWS_MQTT5_DRC_CONNECTION_RATE_EXCEEDED = 159, + AWS_MQTT5_DRC_MAXIMUM_CONNECT_TIME = 160, + AWS_MQTT5_DRC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED = 161, + AWS_MQTT5_DRC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED = 162, +}; + +/** + * Reason code inside PUBACK packets. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901124 + */ +enum aws_mqtt5_puback_reason_code { + AWS_MQTT5_PARC_SUCCESS = 0, + AWS_MQTT5_PARC_NO_MATCHING_SUBSCRIBERS = 16, + AWS_MQTT5_PARC_UNSPECIFIED_ERROR = 128, + AWS_MQTT5_PARC_IMPLEMENTATION_SPECIFIC_ERROR = 131, + AWS_MQTT5_PARC_NOT_AUTHORIZED = 135, + AWS_MQTT5_PARC_TOPIC_NAME_INVALID = 144, + AWS_MQTT5_PARC_PACKET_IDENTIFIER_IN_USE = 145, + AWS_MQTT5_PARC_QUOTA_EXCEEDED = 151, + AWS_MQTT5_PARC_PAYLOAD_FORMAT_INVALID = 153, +}; + +/** + * Reason code inside SUBACK packet payloads. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901178 + */ +enum aws_mqtt5_suback_reason_code { + AWS_MQTT5_SARC_GRANTED_QOS_0 = 0, + AWS_MQTT5_SARC_GRANTED_QOS_1 = 1, + AWS_MQTT5_SARC_GRANTED_QOS_2 = 2, + AWS_MQTT5_SARC_UNSPECIFIED_ERROR = 128, + AWS_MQTT5_SARC_IMPLEMENTATION_SPECIFIC_ERROR = 131, + AWS_MQTT5_SARC_NOT_AUTHORIZED = 135, + AWS_MQTT5_SARC_TOPIC_FILTER_INVALID = 143, + AWS_MQTT5_SARC_PACKET_IDENTIFIER_IN_USE = 145, + AWS_MQTT5_SARC_QUOTA_EXCEEDED = 151, + AWS_MQTT5_SARC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED = 158, + AWS_MQTT5_SARC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED = 161, + AWS_MQTT5_SARC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED = 162, +}; + +/** + * Reason code inside UNSUBACK packet payloads. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901194 + */ +enum aws_mqtt5_unsuback_reason_code { + AWS_MQTT5_UARC_SUCCESS = 0, + AWS_MQTT5_UARC_NO_SUBSCRIPTION_EXISTED = 17, + AWS_MQTT5_UARC_UNSPECIFIED_ERROR = 128, + AWS_MQTT5_UARC_IMPLEMENTATION_SPECIFIC_ERROR = 131, + AWS_MQTT5_UARC_NOT_AUTHORIZED = 135, + AWS_MQTT5_UARC_TOPIC_FILTER_INVALID = 143, + AWS_MQTT5_UARC_PACKET_IDENTIFIER_IN_USE = 145, +}; + +/** + * Type of mqtt packet. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901022 + */ +enum aws_mqtt5_packet_type { + /* internal indicator that the associated packet is null */ + AWS_MQTT5_PT_NONE = -1, + AWS_MQTT5_PT_RESERVED = 0, + AWS_MQTT5_PT_CONNECT = 1, + AWS_MQTT5_PT_CONNACK = 2, + AWS_MQTT5_PT_PUBLISH = 3, + AWS_MQTT5_PT_PUBACK = 4, + AWS_MQTT5_PT_PUBREC = 5, + AWS_MQTT5_PT_PUBREL = 6, + AWS_MQTT5_PT_PUBCOMP = 7, + AWS_MQTT5_PT_SUBSCRIBE = 8, + AWS_MQTT5_PT_SUBACK = 9, + AWS_MQTT5_PT_UNSUBSCRIBE = 10, + AWS_MQTT5_PT_UNSUBACK = 11, + AWS_MQTT5_PT_PINGREQ = 12, + AWS_MQTT5_PT_PINGRESP = 13, + AWS_MQTT5_PT_DISCONNECT = 14, + AWS_MQTT5_PT_AUTH = 15, +}; + +/** + * Non-persistent representation of an mqtt5 user property. + */ +struct aws_mqtt5_user_property { + struct aws_byte_cursor name; + struct aws_byte_cursor value; +}; + +/** + * Optional property describing a message's payload format. + * Enum values match mqtt spec encoding values. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901063 + */ +enum aws_mqtt5_payload_format_indicator { + AWS_MQTT5_PFI_BYTES = 0, + AWS_MQTT5_PFI_UTF8 = 1, +}; + +/** + * Configures how retained messages should be handled when subscribing with a topic filter that matches topics with + * associated retained messages. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901169 + */ +enum aws_mqtt5_retain_handling_type { + + /** + * Server should send all retained messages on topics that match the subscription's filter. + */ + AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE = 0x00, + + /** + * Server should send all retained messages on topics that match the subscription's filter, where this is the + * first (relative to connection) subscription filter that matches the topic with a retained message. + */ + AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE_IF_NEW = 0x01, + + /** + * Subscribe must not trigger any retained message publishes from the server. + */ + AWS_MQTT5_RHT_DONT_SEND = 0x02, +}; + +/** + * Configures a single subscription within a Subscribe operation + */ +struct aws_mqtt5_subscription_view { + /** + * Topic filter to subscribe to + */ + struct aws_byte_cursor topic_filter; + + /** + * Maximum QOS that the subscriber will accept messages for. Negotiated QoS may be different. + */ + enum aws_mqtt5_qos qos; + + /** + * Should the server not send publishes to a client when that client was the one who sent the publish? + */ + bool no_local; + + /** + * Should messages sent due to this subscription keep the retain flag preserved on the message? + */ + bool retain_as_published; + + /** + * Should retained messages on matching topics be sent in reaction to this subscription? + */ + enum aws_mqtt5_retain_handling_type retain_handling_type; +}; + +/** + * Read-only snapshot of a DISCONNECT packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901205 + */ +struct aws_mqtt5_packet_disconnect_view { + enum aws_mqtt5_disconnect_reason_code reason_code; + const uint32_t *session_expiry_interval_seconds; + const struct aws_byte_cursor *reason_string; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; + + const struct aws_byte_cursor *server_reference; +}; + +/** + * Read-only snapshot of a SUBSCRIBE packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901161 + */ +struct aws_mqtt5_packet_subscribe_view { + aws_mqtt5_packet_id_t packet_id; + + size_t subscription_count; + const struct aws_mqtt5_subscription_view *subscriptions; + + const uint32_t *subscription_identifier; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; +}; + +/** + * Read-only snapshot of an UNSUBSCRIBE packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901179 + */ +struct aws_mqtt5_packet_unsubscribe_view { + aws_mqtt5_packet_id_t packet_id; + + size_t topic_filter_count; + const struct aws_byte_cursor *topic_filters; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; +}; + +/** + * Read-only snapshot of a PUBLISH packet. Used both in configuration of a publish operation and callback + * data in message receipt. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901100 + */ +struct aws_mqtt5_packet_publish_view { + struct aws_byte_cursor payload; + + /* packet_id is only set for QoS 1 and QoS 2 */ + aws_mqtt5_packet_id_t packet_id; + + enum aws_mqtt5_qos qos; + + /* + * Used to set the duplicate flag on QoS 1+ re-delivery attempts. + * Set to false on all first attempts or QoS 0. Set to true on any re-delivery. + */ + bool duplicate; + bool retain; + struct aws_byte_cursor topic; + const enum aws_mqtt5_payload_format_indicator *payload_format; + const uint32_t *message_expiry_interval_seconds; + const uint16_t *topic_alias; + const struct aws_byte_cursor *response_topic; + const struct aws_byte_cursor *correlation_data; + + /* These are ignored when building publish operations */ + size_t subscription_identifier_count; + const uint32_t *subscription_identifiers; + + const struct aws_byte_cursor *content_type; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; +}; + +/** + * Read-only snapshot of a CONNECT packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901033 + */ +struct aws_mqtt5_packet_connect_view { + uint16_t keep_alive_interval_seconds; + + struct aws_byte_cursor client_id; + + const struct aws_byte_cursor *username; + const struct aws_byte_cursor *password; + + bool clean_start; + + const uint32_t *session_expiry_interval_seconds; + + const uint8_t *request_response_information; + const uint8_t *request_problem_information; + const uint16_t *receive_maximum; + const uint16_t *topic_alias_maximum; + const uint32_t *maximum_packet_size_bytes; + + const uint32_t *will_delay_interval_seconds; + const struct aws_mqtt5_packet_publish_view *will; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; + + /* Do not bind these. We don't support AUTH packets yet. For decode/encade testing purposes only. */ + const struct aws_byte_cursor *authentication_method; + const struct aws_byte_cursor *authentication_data; +}; + +/** + * Read-only snapshot of a CONNACK packet. + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901074 + */ +struct aws_mqtt5_packet_connack_view { + bool session_present; + enum aws_mqtt5_connect_reason_code reason_code; + + const uint32_t *session_expiry_interval; + const uint16_t *receive_maximum; + const enum aws_mqtt5_qos *maximum_qos; + const bool *retain_available; + const uint32_t *maximum_packet_size; + const struct aws_byte_cursor *assigned_client_identifier; + const uint16_t *topic_alias_maximum; + const struct aws_byte_cursor *reason_string; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; + + const bool *wildcard_subscriptions_available; + const bool *subscription_identifiers_available; + const bool *shared_subscriptions_available; + + const uint16_t *server_keep_alive; + const struct aws_byte_cursor *response_information; + const struct aws_byte_cursor *server_reference; + const struct aws_byte_cursor *authentication_method; + const struct aws_byte_cursor *authentication_data; +}; + +/** + * Read-only snapshot of a PUBACK packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901121 + */ +struct aws_mqtt5_packet_puback_view { + aws_mqtt5_packet_id_t packet_id; + + enum aws_mqtt5_puback_reason_code reason_code; + const struct aws_byte_cursor *reason_string; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; +}; + +/** + * Read-only snapshot of a SUBACK packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901171 + */ +struct aws_mqtt5_packet_suback_view { + aws_mqtt5_packet_id_t packet_id; + + const struct aws_byte_cursor *reason_string; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; + + size_t reason_code_count; + const enum aws_mqtt5_suback_reason_code *reason_codes; +}; + +/** + * Read-only snapshot of an UNSUBACK packet + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901187 + */ +struct aws_mqtt5_packet_unsuback_view { + aws_mqtt5_packet_id_t packet_id; + + const struct aws_byte_cursor *reason_string; + + size_t user_property_count; + const struct aws_mqtt5_user_property *user_properties; + + size_t reason_code_count; + const enum aws_mqtt5_unsuback_reason_code *reason_codes; +}; + +#endif /* AWS_MQTT_MQTT5_TYPES_H */ diff --git a/source/client.c b/source/client.c index 88d886ea..ba8a8ae9 100644 --- a/source/client.c +++ b/source/client.c @@ -7,13 +7,13 @@ #include #include #include +#include #include #include #include #include -#include #include #include #include @@ -24,7 +24,6 @@ #include #ifdef AWS_MQTT_WITH_WEBSOCKETS -# include # include # include #endif @@ -1233,24 +1232,17 @@ static aws_mqtt_transform_websocket_handshake_complete_fn s_websocket_handshake_ static int s_websocket_connect(struct aws_mqtt_client_connection *connection) { AWS_ASSERT(connection->websocket.enabled); - /* These defaults were chosen because they're commmon in other MQTT libraries. - * The user can modify the request in their transform callback if they need to. */ - const struct aws_byte_cursor default_path = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("/mqtt"); - const struct aws_http_header default_protocol_header = { - .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Sec-WebSocket-Protocol"), - .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("mqtt"), - }; - /* Build websocket handshake request */ connection->websocket.handshake_request = aws_http_message_new_websocket_handshake_request( - connection->allocator, default_path, aws_byte_cursor_from_string(connection->host_name)); + connection->allocator, *g_websocket_handshake_default_path, aws_byte_cursor_from_string(connection->host_name)); if (!connection->websocket.handshake_request) { AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Failed to generate websocket handshake request", (void *)connection); goto error; } - if (aws_http_message_add_header(connection->websocket.handshake_request, default_protocol_header)) { + if (aws_http_message_add_header( + connection->websocket.handshake_request, *g_websocket_handshake_default_protocol_header)) { AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Failed to generate websocket handshake request", (void *)connection); goto error; } diff --git a/source/mqtt.c b/source/mqtt.c index c7713476..fd39a827 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -153,6 +153,69 @@ bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter) AWS_DEFINE_ERROR_INFO_MQTT( AWS_ERROR_MQTT_QUEUE_FULL, "MQTT request queue is full."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION, + "Invalid mqtt5 client options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION, + "Invalid mqtt5 connect packet options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION, + "Invalid mqtt5 disconnect packet options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION, + "Invalid mqtt5 publish packet options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION, + "Invalid mqtt5 subscribe packet options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION, + "Invalid mqtt5 unsubscribe packet options value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION, + "Invalid mqtt5 user property value."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_PACKET_VALIDATION, + "General mqtt5 packet validation error"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_ENCODE_FAILURE, + "Error occurred while encoding an outgoing mqtt5 packet"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR, + "Mqtt5 decoder received an invalid packet that broke mqtt5 protocol rules"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + "Remote endpoint rejected the CONNECT attempt by returning an unsuccessful CONNACK"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_CONNACK_TIMEOUT, + "Remote endpoint did not respond to a CONNECT request before timeout exceeded"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT, + "Remote endpoint did not respond to a PINGREQ before timeout exceeded"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + "Mqtt5 client connection interrupted by user request."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + "Mqtt5 client connection interrupted by server DISCONNECT."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_CLIENT_TERMINATED, + "Mqtt5 client terminated by user request."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, + "Mqtt5 operation failed due to a disconnection event in conjunction with the client's offline queue retention policy."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_ENCODE_SIZE_UNSUPPORTED_PACKET_TYPE, + "Unsupported packet type for encode size calculation"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE, + "Error while processing mqtt5 operational state"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS, + "Incoming publish contained an invalid (too large or unknown) topic alias"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, + "Outgoing publish contained an invalid (too large or unknown) topic alias"), }; /* clang-format on */ #undef AWS_DEFINE_ERROR_INFO_MQTT @@ -167,6 +230,9 @@ static struct aws_error_info_list s_error_list = { DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT_GENERAL, "mqtt", "Misc MQTT logging"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT_CLIENT, "mqtt-client", "MQTT client and connections"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT_TOPIC_TREE, "mqtt-topic-tree", "MQTT subscription tree"), + DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_GENERAL, "mqtt5-general", "Misc MQTT5 logging"), + DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CLIENT, "mqtt5-client", "MQTT5 client and connections"), + DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CANARY, "mqtt5-canary", "MQTT5 canary logging"), }; /* clang-format on */ diff --git a/source/shared_constants.c b/source/shared_constants.c new file mode 100644 index 00000000..8d260799 --- /dev/null +++ b/source/shared_constants.c @@ -0,0 +1,22 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +/* + * These defaults were chosen because they're commmon in other MQTT libraries. + * The user can modify the request in their transform callback if they need to. + */ +static const struct aws_byte_cursor s_websocket_handshake_default_path = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("/mqtt"); +const struct aws_byte_cursor *g_websocket_handshake_default_path = &s_websocket_handshake_default_path; + +static const struct aws_http_header s_websocket_handshake_default_protocol_header = { + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Sec-WebSocket-Protocol"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("mqtt"), +}; +const struct aws_http_header *g_websocket_handshake_default_protocol_header = + &s_websocket_handshake_default_protocol_header; diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c new file mode 100644 index 00000000..bd55b4bf --- /dev/null +++ b/source/v5/mqtt5_client.c @@ -0,0 +1,3306 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable : 4232) /* function pointer to dll symbol */ +#endif + +#define AWS_MQTT5_IO_MESSAGE_DEFAULT_LENGTH 4096 +#define AWS_MQTT5_DEFAULT_CONNACK_PACKET_TIMEOUT_MS 10000 + +const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_client_state state) { + switch (state) { + case AWS_MCS_STOPPED: + return "STOPPED"; + + case AWS_MCS_CONNECTING: + return "CONNECTING"; + + case AWS_MCS_MQTT_CONNECT: + return "MQTT_CONNECT"; + + case AWS_MCS_CONNECTED: + return "CONNECTED"; + + case AWS_MCS_CLEAN_DISCONNECT: + return "CLEAN_DISCONNECT"; + + case AWS_MCS_CHANNEL_SHUTDOWN: + return "CHANNEL_SHUTDOWN"; + + case AWS_MCS_PENDING_RECONNECT: + return "PENDING_RECONNECT"; + + case AWS_MCS_TERMINATED: + return "TERMINATED"; + + default: + return "UNKNOWN"; + } +} + +static bool s_aws_mqtt5_operation_is_retainable(struct aws_mqtt5_operation *operation) { + switch (operation->packet_type) { + case AWS_MQTT5_PT_PUBLISH: + case AWS_MQTT5_PT_SUBSCRIBE: + case AWS_MQTT5_PT_UNSUBSCRIBE: + return true; + + default: + return false; + } +} + +static bool s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy( + struct aws_mqtt5_operation *operation, + enum aws_mqtt5_client_operation_queue_behavior_type queue_behavior) { + switch (aws_mqtt5_client_operation_queue_behavior_type_to_non_default(queue_behavior)) { + case AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT: + return false; + + case AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT: + if (!s_aws_mqtt5_operation_is_retainable(operation)) { + return false; + } + + if (operation->packet_type == AWS_MQTT5_PT_PUBLISH) { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos == AWS_MQTT5_QOS_AT_MOST_ONCE) { + return false; + } + } + + return true; + + case AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT: + if (!s_aws_mqtt5_operation_is_retainable(operation)) { + return false; + } + + if (operation->packet_type == AWS_MQTT5_PT_PUBLISH) { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + return true; + } + } + + return false; + + default: + return false; + } +} + +typedef bool(mqtt5_operation_filter)(struct aws_mqtt5_operation *operation, void *filter_context); + +static void s_filter_operation_list( + struct aws_linked_list *source_operations, + mqtt5_operation_filter *filter_fn, + struct aws_linked_list *filtered_operations, + void *filter_context) { + struct aws_linked_list_node *node = aws_linked_list_begin(source_operations); + while (node != aws_linked_list_end(source_operations)) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + node = aws_linked_list_next(node); + + if (filter_fn(operation, filter_context)) { + aws_linked_list_remove(&operation->node); + aws_linked_list_push_back(filtered_operations, &operation->node); + } + } +} + +typedef void(mqtt5_operation_applicator)(struct aws_mqtt5_operation *operation, void *applicator_context); + +static void s_apply_to_operation_list( + struct aws_linked_list *operations, + mqtt5_operation_applicator *applicator_fn, + void *applicator_context) { + struct aws_linked_list_node *node = aws_linked_list_begin(operations); + while (node != aws_linked_list_end(operations)) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + node = aws_linked_list_next(node); + + applicator_fn(operation, applicator_context); + } +} + +static int s_aws_mqtt5_client_change_desired_state( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state desired_state, + struct aws_mqtt5_operation_disconnect *disconnect_operation); + +static uint64_t s_hash_uint16_t(const void *item) { + return *(uint16_t *)item; +} + +static bool s_uint16_t_eq(const void *a, const void *b) { + return *(uint16_t *)a == *(uint16_t *)b; +} + +static uint64_t s_aws_mqtt5_client_compute_operational_state_service_time( + const struct aws_mqtt5_client_operational_state *client_operational_state, + uint64_t now); + +static int s_submit_operation(struct aws_mqtt5_client *client, struct aws_mqtt5_operation *operation); + +static void s_complete_operation( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *view) { + if (client != NULL) { + aws_mqtt5_client_statistics_change_operation_statistic_state(client, operation, AWS_MQTT5_OSS_NONE); + } + + aws_mqtt5_operation_complete(operation, error_code, packet_type, view); + aws_mqtt5_operation_release(operation); +} + +static void s_complete_operation_list( + struct aws_mqtt5_client *client, + struct aws_linked_list *operation_list, + int error_code) { + + struct aws_linked_list_node *node = aws_linked_list_begin(operation_list); + while (node != aws_linked_list_end(operation_list)) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + + node = aws_linked_list_next(node); + + s_complete_operation(client, operation, error_code, AWS_MQTT5_PT_NONE, NULL); + } + + /* we've released everything, so reset the list to empty */ + aws_linked_list_init(operation_list); +} + +static void s_check_timeouts(struct aws_mqtt5_client *client, uint64_t now) { + if (client->config->ack_timeout_seconds == 0) { + return; + } + + struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); + while (node != aws_linked_list_end(&client->operational_state.unacked_operations)) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + node = aws_linked_list_next(node); + if (operation->ack_timeout_timepoint_ns < now) { + /* Timeout for this packet has been reached */ + aws_mqtt5_packet_id_t packet_id = aws_mqtt5_operation_get_packet_id(operation); + + switch (operation->packet_type) { + case AWS_MQTT5_PT_SUBSCRIBE: + /* SUBSCRIBE has timed out. */ + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: SUBSCRIBE packet with id:%d has timed out", + (void *)client, + packet_id); + break; + + case AWS_MQTT5_PT_UNSUBSCRIBE: + /* UNSUBSCRIBE has timed out. */ + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: UNSUBSCRIBE packet with id:%d has timed out", + (void *)client, + packet_id); + break; + + case AWS_MQTT5_PT_PUBLISH: + /* PUBLISH has timed out. */ + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: PUBLISH packet with id:%d has timed out", + (void *)client, + packet_id); + + aws_mqtt5_client_flow_control_state_on_puback(client); + break; + + default: + /* something is wrong, there should be no other packet type in this linked list */ + break; + } + + struct aws_hash_element *elem = NULL; + aws_hash_table_find(&client->operational_state.unacked_operations_table, &packet_id, &elem); + + if (elem == NULL || elem->value == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: timeout for unknown operation with id %d", + (void *)client, + (int)packet_id); + return; + } + + aws_linked_list_remove(&operation->node); + aws_hash_table_remove(&client->operational_state.unacked_operations_table, &packet_id, NULL, NULL); + + s_complete_operation(client, operation, AWS_ERROR_MQTT_TIMEOUT, AWS_MQTT5_PT_NONE, NULL); + } else { + break; + } + } +} + +static void s_mqtt5_client_final_destroy(struct aws_mqtt5_client *client) { + if (client == NULL) { + return; + } + + aws_mqtt5_client_termination_completion_fn *client_termination_handler = NULL; + void *client_termination_handler_user_data = NULL; + if (client->config != NULL) { + client_termination_handler = client->config->client_termination_handler; + client_termination_handler_user_data = client->config->client_termination_handler_user_data; + } + + aws_mqtt5_client_operational_state_clean_up(&client->operational_state); + + aws_mqtt5_client_options_storage_destroy((struct aws_mqtt5_client_options_storage *)client->config); + + aws_mqtt5_negotiated_settings_clean_up(&client->negotiated_settings); + + aws_http_message_release(client->handshake); + + aws_mqtt5_encoder_clean_up(&client->encoder); + aws_mqtt5_decoder_clean_up(&client->decoder); + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&client->inbound_topic_alias_resolver); + aws_mqtt5_outbound_topic_alias_resolver_destroy(client->outbound_topic_alias_resolver); + + aws_mutex_clean_up(&client->operation_statistics_lock); + + aws_mem_release(client->allocator, client); + + if (client_termination_handler != NULL) { + (*client_termination_handler)(client_termination_handler_user_data); + } +} + +static void s_on_mqtt5_client_zero_ref_count(void *user_data) { + struct aws_mqtt5_client *client = user_data; + + s_aws_mqtt5_client_change_desired_state(client, AWS_MCS_TERMINATED, NULL); +} + +static void s_aws_mqtt5_client_emit_stopped_lifecycle_event(struct aws_mqtt5_client *client) { + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: emitting stopped lifecycle event", (void *)client); + + if (client->config->lifecycle_event_handler != NULL) { + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.event_type = AWS_MQTT5_CLET_STOPPED; + event.client = client; + event.user_data = client->config->lifecycle_event_handler_user_data; + + (*client->config->lifecycle_event_handler)(&event); + } +} + +static void s_aws_mqtt5_client_emit_connecting_lifecycle_event(struct aws_mqtt5_client *client) { + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: emitting connecting lifecycle event", (void *)client); + + client->lifecycle_state = AWS_MQTT5_LS_CONNECTING; + + if (client->config->lifecycle_event_handler != NULL) { + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT; + event.client = client; + event.user_data = client->config->lifecycle_event_handler_user_data; + + (*client->config->lifecycle_event_handler)(&event); + } +} + +static void s_aws_mqtt5_client_emit_connection_success_lifecycle_event( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_connack_view *connack_view) { + + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: emitting connection success lifecycle event", (void *)client); + + client->lifecycle_state = AWS_MQTT5_LS_CONNECTED; + + if (client->config->lifecycle_event_handler != NULL) { + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS; + event.client = client; + event.user_data = client->config->lifecycle_event_handler_user_data; + event.settings = &client->negotiated_settings; + event.connack_data = connack_view; + + (*client->config->lifecycle_event_handler)(&event); + } +} + +/* + * Emits either a CONNECTION_FAILED or DISCONNECT event based on the current life cycle state. Once a "final" + * event is emitted by the client, it must attempt to reconnect before another one will be emitted, since the + * lifecycle state check will early out until then. It is expected that this function may get called unnecessarily + * often during various channel shutdown or disconnection/failure flows. This will not affect overall correctness. + */ +static void s_aws_mqtt5_client_emit_final_lifecycle_event( + struct aws_mqtt5_client *client, + int error_code, + const struct aws_mqtt5_packet_connack_view *connack_view, + const struct aws_mqtt5_packet_disconnect_view *disconnect_view) { + + if (client->lifecycle_state == AWS_MQTT5_LS_NONE) { + /* we already emitted a final event earlier */ + return; + } + + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + if (client->lifecycle_state == AWS_MQTT5_LS_CONNECTING) { + AWS_FATAL_ASSERT(disconnect_view == NULL); + event.event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: emitting connection failure lifecycle event with error code %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + } else { + AWS_FATAL_ASSERT(client->lifecycle_state == AWS_MQTT5_LS_CONNECTED); + AWS_FATAL_ASSERT(connack_view == NULL); + event.event_type = AWS_MQTT5_CLET_DISCONNECTION; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: emitting disconnection lifecycle event with error code %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + } + + event.error_code = error_code; + event.user_data = client->config->lifecycle_event_handler_user_data; + event.connack_data = connack_view; + event.disconnect_data = disconnect_view; + + client->lifecycle_state = AWS_MQTT5_LS_NONE; + + if (client->config->lifecycle_event_handler != NULL) { + (*client->config->lifecycle_event_handler)(&event); + } +} + +/* + * next_service_time == 0 means to not service the client, i.e. a state that only cares about external events + * + * This includes connecting and channel shutdown. Terminated is also included, but it's a state that only exists + * instantaneously before final destruction. + */ +static uint64_t s_compute_next_service_time_client_stopped(struct aws_mqtt5_client *client, uint64_t now) { + /* have we been told to connect or terminate? */ + if (client->desired_state != AWS_MCS_STOPPED) { + return now; + } + + return 0; +} + +static uint64_t s_compute_next_service_time_client_connecting(struct aws_mqtt5_client *client, uint64_t now) { + (void)client; + (void)now; + + return 0; +} + +static uint64_t s_compute_next_service_time_client_mqtt_connect(struct aws_mqtt5_client *client, uint64_t now) { + /* This state is interruptable by a stop/terminate */ + if (client->desired_state != AWS_MCS_CONNECTED) { + return now; + } + + uint64_t operation_processing_time = + s_aws_mqtt5_client_compute_operational_state_service_time(&client->operational_state, now); + if (operation_processing_time == 0) { + return client->next_mqtt_connect_packet_timeout_time; + } + + return aws_min_u64(client->next_mqtt_connect_packet_timeout_time, operation_processing_time); +} + +static uint64_t s_min_non_0_64(uint64_t a, uint64_t b) { + if (a == 0) { + return b; + } + + if (b == 0) { + return a; + } + + return aws_min_u64(a, b); +} + +static uint64_t s_compute_next_service_time_client_connected(struct aws_mqtt5_client *client, uint64_t now) { + + /* ping and ping timeout */ + uint64_t next_service_time = client->next_ping_time; + if (client->next_ping_timeout_time != 0) { + next_service_time = aws_min_u64(next_service_time, client->next_ping_timeout_time); + } + + /* unacked operations timeout */ + if (client->config->ack_timeout_seconds != 0 && + !aws_linked_list_empty(&client->operational_state.unacked_operations)) { + struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + next_service_time = aws_min_u64(next_service_time, operation->ack_timeout_timepoint_ns); + } + + if (client->desired_state != AWS_MCS_CONNECTED) { + next_service_time = now; + } + + uint64_t operation_processing_time = + s_aws_mqtt5_client_compute_operational_state_service_time(&client->operational_state, now); + + next_service_time = s_min_non_0_64(operation_processing_time, next_service_time); + + /* reset reconnect delay interval */ + next_service_time = s_min_non_0_64(client->next_reconnect_delay_reset_time_ns, next_service_time); + + return next_service_time; +} + +static uint64_t s_compute_next_service_time_client_clean_disconnect(struct aws_mqtt5_client *client, uint64_t now) { + uint64_t ack_timeout_time = 0; + + /* unacked operations timeout */ + if (client->config->ack_timeout_seconds != 0 && + !aws_linked_list_empty(&client->operational_state.unacked_operations)) { + struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + ack_timeout_time = operation->ack_timeout_timepoint_ns; + } + + uint64_t operation_processing_time = + s_aws_mqtt5_client_compute_operational_state_service_time(&client->operational_state, now); + + return s_min_non_0_64(ack_timeout_time, operation_processing_time); +} + +static uint64_t s_compute_next_service_time_client_channel_shutdown(struct aws_mqtt5_client *client, uint64_t now) { + (void)client; + (void)now; + + return 0; +} + +static uint64_t s_compute_next_service_time_client_pending_reconnect(struct aws_mqtt5_client *client, uint64_t now) { + if (client->desired_state != AWS_MCS_CONNECTED) { + return now; + } + + return client->next_reconnect_time_ns; +} + +static uint64_t s_compute_next_service_time_client_terminated(struct aws_mqtt5_client *client, uint64_t now) { + (void)client; + (void)now; + + return 0; +} + +static uint64_t s_compute_next_service_time_by_current_state(struct aws_mqtt5_client *client, uint64_t now) { + switch (client->current_state) { + case AWS_MCS_STOPPED: + return s_compute_next_service_time_client_stopped(client, now); + case AWS_MCS_CONNECTING: + return s_compute_next_service_time_client_connecting(client, now); + case AWS_MCS_MQTT_CONNECT: + return s_compute_next_service_time_client_mqtt_connect(client, now); + case AWS_MCS_CONNECTED: + return s_compute_next_service_time_client_connected(client, now); + case AWS_MCS_CLEAN_DISCONNECT: + return s_compute_next_service_time_client_clean_disconnect(client, now); + case AWS_MCS_CHANNEL_SHUTDOWN: + return s_compute_next_service_time_client_channel_shutdown(client, now); + case AWS_MCS_PENDING_RECONNECT: + return s_compute_next_service_time_client_pending_reconnect(client, now); + case AWS_MCS_TERMINATED: + return s_compute_next_service_time_client_terminated(client, now); + } + + return 0; +} + +static void s_reevaluate_service_task(struct aws_mqtt5_client *client) { + /* + * This causes the client to only reevaluate service schedule time at the end of the service call or in + * a callback from an external event. + */ + if (client->in_service) { + return; + } + + uint64_t now = (*client->vtable->get_current_time_fn)(); + uint64_t next_service_time = s_compute_next_service_time_by_current_state(client, now); + + /* + * This catches both the case when there's an existing service schedule and we either want to not + * perform it (next_service_time == 0) or need to run service at a different time than the current scheduled time. + */ + if (next_service_time != client->next_service_task_run_time && client->next_service_task_run_time > 0) { + aws_event_loop_cancel_task(client->loop, &client->service_task); + client->next_service_task_run_time = 0; + + AWS_LOGF_TRACE(AWS_LS_MQTT5_CLIENT, "id=%p: cancelling previously scheduled service task", (void *)client); + } + + if (next_service_time > 0 && + (next_service_time < client->next_service_task_run_time || client->next_service_task_run_time == 0)) { + aws_event_loop_schedule_task_future(client->loop, &client->service_task, next_service_time); + + AWS_LOGF_TRACE( + AWS_LS_MQTT5_CLIENT, "id=%p: scheduled service task for time %" PRIu64, (void *)client, next_service_time); + } + + client->next_service_task_run_time = next_service_time; +} + +static void s_enqueue_operation_back(struct aws_mqtt5_client *client, struct aws_mqtt5_operation *operation) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: enqueuing %s operation to back", + (void *)client, + aws_mqtt5_packet_type_to_c_string(operation->packet_type)); + + aws_linked_list_push_back(&client->operational_state.queued_operations, &operation->node); + + s_reevaluate_service_task(client); +} + +static void s_enqueue_operation_front(struct aws_mqtt5_client *client, struct aws_mqtt5_operation *operation) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: enqueuing %s operation to front", + (void *)client, + aws_mqtt5_packet_type_to_c_string(operation->packet_type)); + + aws_linked_list_push_front(&client->operational_state.queued_operations, &operation->node); + + s_reevaluate_service_task(client); +} + +static void s_aws_mqtt5_client_operational_state_reset( + struct aws_mqtt5_client_operational_state *client_operational_state, + int completion_error_code, + bool is_final) { + + struct aws_mqtt5_client *client = client_operational_state->client; + + s_complete_operation_list(client, &client_operational_state->queued_operations, completion_error_code); + s_complete_operation_list(client, &client_operational_state->write_completion_operations, completion_error_code); + s_complete_operation_list(client, &client_operational_state->unacked_operations, completion_error_code); + + if (is_final) { + aws_hash_table_clean_up(&client_operational_state->unacked_operations_table); + } else { + aws_hash_table_clear(&client_operational_state->unacked_operations_table); + } +} + +static void s_change_current_state(struct aws_mqtt5_client *client, enum aws_mqtt5_client_state next_state); + +static void s_change_current_state_to_stopped(struct aws_mqtt5_client *client) { + client->current_state = AWS_MCS_STOPPED; + + s_aws_mqtt5_client_operational_state_reset(&client->operational_state, AWS_ERROR_MQTT5_USER_REQUESTED_STOP, false); + + /* Stop works as a complete session wipe, and so the next time we connect, we want it to be clean */ + client->has_connected_successfully = false; + + s_aws_mqtt5_client_emit_stopped_lifecycle_event(client); +} + +static void s_aws_mqtt5_client_shutdown_channel(struct aws_mqtt5_client *client, int error_code) { + if (error_code == AWS_ERROR_SUCCESS) { + error_code = AWS_ERROR_UNKNOWN; + } + + s_aws_mqtt5_client_emit_final_lifecycle_event(client, error_code, NULL, NULL); + + if (client->current_state != AWS_MCS_MQTT_CONNECT && client->current_state != AWS_MCS_CONNECTED && + client->current_state != AWS_MCS_CLEAN_DISCONNECT) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: client channel shutdown invoked from unexpected state %d(%s)", + (void *)client, + (int)client->current_state, + aws_mqtt5_client_state_to_c_string(client->current_state)); + return; + } + + if (client->slot == NULL || client->slot->channel == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: client channel shutdown invoked without a channel", (void *)client); + return; + } + + s_change_current_state(client, AWS_MCS_CHANNEL_SHUTDOWN); + (*client->vtable->channel_shutdown_fn)(client->slot->channel, error_code); +} + +static void s_aws_mqtt5_client_shutdown_channel_with_disconnect( + struct aws_mqtt5_client *client, + int error_code, + struct aws_mqtt5_operation_disconnect *disconnect_op) { + if (client->current_state != AWS_MCS_CONNECTED && client->current_state != AWS_MCS_MQTT_CONNECT) { + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + aws_linked_list_push_front(&client->operational_state.queued_operations, &disconnect_op->base.node); + aws_mqtt5_operation_disconnect_acquire(disconnect_op); + client->clean_disconnect_error_code = error_code; + + s_change_current_state(client, AWS_MCS_CLEAN_DISCONNECT); +} + +static void s_on_disconnect_operation_complete(int error_code, void *user_data) { + struct aws_mqtt5_client *client = user_data; + + s_aws_mqtt5_client_shutdown_channel( + client, (error_code != AWS_ERROR_SUCCESS) ? error_code : client->clean_disconnect_error_code); +} + +static void s_aws_mqtt5_client_shutdown_channel_clean( + struct aws_mqtt5_client *client, + int error_code, + enum aws_mqtt5_disconnect_reason_code reason_code) { + struct aws_mqtt5_packet_disconnect_view disconnect_options = { + .reason_code = reason_code, + }; + + struct aws_mqtt5_disconnect_completion_options internal_completion_options = { + .completion_callback = s_on_disconnect_operation_complete, + .completion_user_data = client, + }; + + struct aws_mqtt5_operation_disconnect *disconnect_op = + aws_mqtt5_operation_disconnect_new(client->allocator, &disconnect_options, NULL, &internal_completion_options); + if (disconnect_op == NULL) { + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + s_aws_mqtt5_client_shutdown_channel_with_disconnect(client, error_code, disconnect_op); + aws_mqtt5_operation_disconnect_release(disconnect_op); +} + +static void s_mqtt5_client_shutdown( + struct aws_client_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + + (void)bootstrap; + (void)channel; + + struct aws_mqtt5_client *client = user_data; + + if (error_code == AWS_ERROR_SUCCESS) { + error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP; + } + + s_aws_mqtt5_client_emit_final_lifecycle_event(client, error_code, NULL, NULL); + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: channel tore down with error code %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + if (client->slot) { + aws_channel_slot_remove(client->slot); + AWS_LOGF_TRACE(AWS_LS_MQTT5_CLIENT, "id=%p: slot removed successfully", (void *)client); + client->slot = NULL; + } + + aws_mqtt5_client_on_disconnection_update_operational_state(client); + + if (client->desired_state == AWS_MCS_CONNECTED) { + s_change_current_state(client, AWS_MCS_PENDING_RECONNECT); + } else { + s_change_current_state(client, AWS_MCS_STOPPED); + } +} + +static void s_mqtt5_client_setup( + struct aws_client_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + + (void)bootstrap; + + /* Setup callback contract is: if error_code is non-zero then channel is NULL. */ + AWS_FATAL_ASSERT((error_code != 0) == (channel == NULL)); + struct aws_mqtt5_client *client = user_data; + + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_CONNECTING); + + if (error_code != AWS_OP_SUCCESS) { + /* client shutdown already handles this case, so just call that. */ + s_mqtt5_client_shutdown(bootstrap, error_code, channel, user_data); + return; + } + + if (client->desired_state != AWS_MCS_CONNECTED) { + aws_raise_error(AWS_ERROR_MQTT5_USER_REQUESTED_STOP); + goto error; + } + + client->slot = aws_channel_slot_new(channel); /* allocs or crashes */ + + if (aws_channel_slot_insert_end(channel, client->slot)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: Failed to insert slot into channel %p, error %d (%s).", + (void *)client, + (void *)channel, + aws_last_error(), + aws_error_name(aws_last_error())); + goto error; + } + + if (aws_channel_slot_set_handler(client->slot, &client->handler)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: Failed to set MQTT handler into slot on channel %p, error %d (%s).", + (void *)client, + (void *)channel, + aws_last_error(), + aws_error_name(aws_last_error())); + + goto error; + } + + s_change_current_state(client, AWS_MCS_MQTT_CONNECT); + + return; + +error: + + s_change_current_state(client, AWS_MCS_CHANNEL_SHUTDOWN); + (*client->vtable->channel_shutdown_fn)(channel, aws_last_error()); +} + +static void s_on_websocket_shutdown(struct aws_websocket *websocket, int error_code, void *user_data) { + struct aws_mqtt5_client *client = user_data; + + struct aws_channel *channel = client->slot ? client->slot->channel : NULL; + + s_mqtt5_client_shutdown(client->config->bootstrap, error_code, channel, client); + + if (websocket) { + aws_websocket_release(websocket); + } +} + +static void s_on_websocket_setup( + struct aws_websocket *websocket, + int error_code, + int handshake_response_status, + const struct aws_http_header *handshake_response_header_array, + size_t num_handshake_response_headers, + void *user_data) { + + (void)handshake_response_status; + (void)handshake_response_header_array; + (void)num_handshake_response_headers; + + struct aws_mqtt5_client *client = user_data; + client->handshake = aws_http_message_release(client->handshake); + + /* Setup callback contract is: if error_code is non-zero then websocket is NULL. */ + AWS_FATAL_ASSERT((error_code != 0) == (websocket == NULL)); + + struct aws_channel *channel = NULL; + + if (websocket) { + channel = aws_websocket_get_channel(websocket); + AWS_ASSERT(channel); + + /* Websocket must be "converted" before the MQTT handler can be installed next to it. */ + if (aws_websocket_convert_to_midchannel_handler(websocket)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: Failed converting websocket, error %d (%s)", + (void *)client, + aws_last_error(), + aws_error_name(aws_last_error())); + + (*client->vtable->channel_shutdown_fn)(channel, aws_last_error()); + return; + } + } + + /* Call into the channel-setup callback, the rest of the logic is the same. */ + s_mqtt5_client_setup(client->config->bootstrap, error_code, channel, client); +} + +struct aws_mqtt5_websocket_transform_complete_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt5_client *client; + int error_code; + struct aws_http_message *handshake; +}; + +void s_websocket_transform_complete_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt5_websocket_transform_complete_task *websocket_transform_complete_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt5_client *client = websocket_transform_complete_task->client; + + aws_http_message_release(client->handshake); + client->handshake = aws_http_message_acquire(websocket_transform_complete_task->handshake); + + int error_code = websocket_transform_complete_task->error_code; + if (error_code == 0 && client->desired_state == AWS_MCS_CONNECTED) { + + struct aws_websocket_client_connection_options websocket_options = { + .allocator = client->allocator, + .bootstrap = client->config->bootstrap, + .socket_options = &client->config->socket_options, + .tls_options = client->config->tls_options_ptr, + .host = aws_byte_cursor_from_string(client->config->host_name), + .port = client->config->port, + .handshake_request = websocket_transform_complete_task->handshake, + .initial_window_size = 0, /* Prevent websocket data from arriving before the MQTT handler is installed */ + .user_data = client, + .on_connection_setup = s_on_websocket_setup, + .on_connection_shutdown = s_on_websocket_shutdown, + .requested_event_loop = client->loop, + }; + + if (client->config->http_proxy_config != NULL) { + websocket_options.proxy_options = &client->config->http_proxy_options; + } + + if (client->vtable->websocket_connect_fn(&websocket_options)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: Failed to initiate websocket connection.", (void *)client); + error_code = aws_last_error(); + goto error; + } + + goto done; + + } else { + if (error_code == AWS_ERROR_SUCCESS) { + AWS_ASSERT(client->desired_state != AWS_MCS_CONNECTED); + error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; + } + } + +error: + + s_on_websocket_setup(NULL, error_code, -1, NULL, 0, client); + +done: + + aws_http_message_release(websocket_transform_complete_task->handshake); + aws_mqtt5_client_release(websocket_transform_complete_task->client); + + aws_mem_release(websocket_transform_complete_task->allocator, websocket_transform_complete_task); +} + +static void s_websocket_handshake_transform_complete( + struct aws_http_message *handshake_request, + int error_code, + void *complete_ctx) { + + struct aws_mqtt5_client *client = complete_ctx; + + struct aws_mqtt5_websocket_transform_complete_task *task = + aws_mem_calloc(client->allocator, 1, sizeof(struct aws_mqtt5_websocket_transform_complete_task)); + + aws_task_init( + &task->task, s_websocket_transform_complete_task_fn, (void *)task, "WebsocketHandshakeTransformComplete"); + + task->allocator = client->allocator; + task->client = aws_mqtt5_client_acquire(client); + task->error_code = error_code; + task->handshake = handshake_request; + + aws_event_loop_schedule_task_now(client->loop, &task->task); + + aws_mqtt5_client_release(client); +} + +static int s_websocket_connect(struct aws_mqtt5_client *client) { + AWS_ASSERT(client); + AWS_ASSERT(client->config->websocket_handshake_transform); + + /* Build websocket handshake request */ + struct aws_http_message *handshake = aws_http_message_new_websocket_handshake_request( + client->allocator, *g_websocket_handshake_default_path, aws_byte_cursor_from_string(client->config->host_name)); + + if (handshake == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: Failed to generate websocket handshake request", (void *)client); + return AWS_OP_ERR; + } + + if (aws_http_message_add_header(handshake, *g_websocket_handshake_default_protocol_header)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, "id=%p: Failed to add default header to websocket handshake request", (void *)client); + goto on_error; + } + + AWS_LOGF_TRACE(AWS_LS_MQTT5_CLIENT, "id=%p: Transforming websocket handshake request.", (void *)client); + + aws_mqtt5_client_acquire(client); + client->config->websocket_handshake_transform( + handshake, + client->config->websocket_handshake_transform_user_data, + s_websocket_handshake_transform_complete, + client); + + return AWS_OP_SUCCESS; + +on_error: + + aws_http_message_release(handshake); + + return AWS_OP_ERR; +} + +static void s_change_current_state_to_connecting(struct aws_mqtt5_client *client) { + AWS_ASSERT(client->current_state == AWS_MCS_STOPPED || client->current_state == AWS_MCS_PENDING_RECONNECT); + + client->current_state = AWS_MCS_CONNECTING; + client->clean_disconnect_error_code = AWS_ERROR_SUCCESS; + + s_aws_mqtt5_client_emit_connecting_lifecycle_event(client); + + int result = 0; + if (client->config->websocket_handshake_transform != NULL) { + result = s_websocket_connect(client); + } else { + struct aws_socket_channel_bootstrap_options channel_options; + AWS_ZERO_STRUCT(channel_options); + channel_options.bootstrap = client->config->bootstrap; + channel_options.host_name = aws_string_c_str(client->config->host_name); + channel_options.port = client->config->port; + channel_options.socket_options = &client->config->socket_options; + channel_options.tls_options = client->config->tls_options_ptr; + channel_options.setup_callback = &s_mqtt5_client_setup; + channel_options.shutdown_callback = &s_mqtt5_client_shutdown; + channel_options.user_data = client; + channel_options.requested_event_loop = client->loop; + + if (client->config->http_proxy_config == NULL) { + result = (*client->vtable->client_bootstrap_new_socket_channel_fn)(&channel_options); + } else { + result = (*client->vtable->http_proxy_new_socket_channel_fn)( + &channel_options, &client->config->http_proxy_options); + } + } + + if (result) { + int error_code = aws_last_error(); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to kick off connection with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_emit_final_lifecycle_event(client, aws_last_error(), NULL, NULL); + + s_change_current_state(client, AWS_MCS_PENDING_RECONNECT); + } +} + +static int s_aws_mqtt5_client_set_current_operation( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation) { + + if (aws_mqtt5_operation_bind_packet_id(operation, &client->operational_state)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to bind mqtt packet id for current operation, with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + return AWS_OP_ERR; + } + + if (aws_mqtt5_encoder_append_packet_encoding(&client->encoder, operation->packet_type, operation->packet_view)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to append packet encoding sequence for current operation with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + return AWS_OP_ERR; + } + + client->operational_state.current_operation = operation; + + return AWS_OP_SUCCESS; +} + +static void s_reset_ping(struct aws_mqtt5_client *client) { + uint64_t now = (*client->vtable->get_current_time_fn)(); + uint16_t keep_alive_seconds = client->negotiated_settings.server_keep_alive; + + uint64_t keep_alive_interval_nanos = + aws_timestamp_convert(keep_alive_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + client->next_ping_time = aws_add_u64_saturating(now, keep_alive_interval_nanos); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: next PINGREQ scheduled for time %" PRIu64, (void *)client, client->next_ping_time); +} + +static void s_aws_mqtt5_on_socket_write_completion_mqtt_connect(struct aws_mqtt5_client *client, int error_code) { + if (error_code != AWS_ERROR_SUCCESS) { + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + s_reevaluate_service_task(client); +} + +static void s_aws_mqtt5_on_socket_write_completion_connected(struct aws_mqtt5_client *client, int error_code) { + if (error_code != AWS_ERROR_SUCCESS) { + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + s_reevaluate_service_task(client); +} + +static void s_aws_mqtt5_on_socket_write_completion( + struct aws_channel *channel, + struct aws_io_message *message, + int error_code, + void *user_data) { + + (void)channel; + (void)message; + + struct aws_mqtt5_client *client = user_data; + client->operational_state.pending_write_completion = false; + + if (error_code != AWS_ERROR_SUCCESS) { + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: socket write completion invoked with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + } + + switch (client->current_state) { + case AWS_MCS_MQTT_CONNECT: + s_aws_mqtt5_on_socket_write_completion_mqtt_connect(client, error_code); + break; + + case AWS_MCS_CONNECTED: + s_aws_mqtt5_on_socket_write_completion_connected(client, error_code); + break; + + case AWS_MCS_CLEAN_DISCONNECT: + /* the CONNECTED callback works just fine for CLEAN_DISCONNECT */ + s_aws_mqtt5_on_socket_write_completion_connected(client, error_code); + break; + + default: + break; + } + + s_complete_operation_list(client, &client->operational_state.write_completion_operations, error_code); +} + +static bool s_should_resume_session(const struct aws_mqtt5_client *client) { + enum aws_mqtt5_client_session_behavior_type session_behavior = + aws_mqtt5_client_session_behavior_type_to_non_default(client->config->session_behavior); + + return session_behavior == AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS && client->has_connected_successfully; +} + +static void s_change_current_state_to_mqtt_connect(struct aws_mqtt5_client *client) { + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_CONNECTING); + AWS_FATAL_ASSERT(client->operational_state.current_operation == NULL); + + client->current_state = AWS_MCS_MQTT_CONNECT; + client->operational_state.pending_write_completion = false; + + aws_mqtt5_encoder_reset(&client->encoder); + aws_mqtt5_decoder_reset(&client->decoder); + + bool resume_session = s_should_resume_session(client); + struct aws_mqtt5_packet_connect_view connect_view = client->config->connect.storage_view; + connect_view.clean_start = !resume_session; + + if (aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default( + client->config->topic_aliasing_options.inbound_topic_alias_behavior) == AWS_MQTT5_CITABT_ENABLED) { + connect_view.topic_alias_maximum = &client->config->topic_aliasing_options.inbound_alias_cache_size; + } + + aws_mqtt5_negotiated_settings_reset(&client->negotiated_settings, &connect_view); + connect_view.client_id = aws_byte_cursor_from_buf(&client->negotiated_settings.client_id_storage); + + struct aws_mqtt5_operation_connect *connect_op = aws_mqtt5_operation_connect_new(client->allocator, &connect_view); + if (connect_op == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to create CONNECT operation with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + s_enqueue_operation_front(client, &connect_op->base); + + uint32_t timeout_ms = client->config->connack_timeout_ms; + if (timeout_ms == 0) { + timeout_ms = AWS_MQTT5_DEFAULT_CONNACK_PACKET_TIMEOUT_MS; + } + + uint64_t now = (*client->vtable->get_current_time_fn)(); + client->next_mqtt_connect_packet_timeout_time = + now + aws_timestamp_convert(timeout_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: setting CONNECT timeout to %" PRIu64, + (void *)client, + client->next_mqtt_connect_packet_timeout_time); +} + +static void s_reset_reconnection_delay_time(struct aws_mqtt5_client *client) { + uint64_t now = (*client->vtable->get_current_time_fn)(); + uint64_t reset_reconnection_delay_time_nanos = aws_timestamp_convert( + client->config->min_connected_time_to_reset_reconnect_delay_ms, + AWS_TIMESTAMP_MILLIS, + AWS_TIMESTAMP_NANOS, + NULL); + client->next_reconnect_delay_reset_time_ns = aws_add_u64_saturating(now, reset_reconnection_delay_time_nanos); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: reconnection delay reset time set to %" PRIu64, + (void *)client, + client->next_reconnect_delay_reset_time_ns); +} + +static void s_change_current_state_to_connected(struct aws_mqtt5_client *client) { + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_MQTT_CONNECT); + + client->current_state = AWS_MCS_CONNECTED; + + aws_mqtt5_client_on_connection_update_operational_state(client); + + client->has_connected_successfully = true; + client->next_ping_timeout_time = 0; + s_reset_ping(client); + s_reset_reconnection_delay_time(client); +} + +static void s_change_current_state_to_clean_disconnect(struct aws_mqtt5_client *client) { + (void)client; + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_MQTT_CONNECT || client->current_state == AWS_MCS_CONNECTED); + + client->current_state = AWS_MCS_CLEAN_DISCONNECT; +} + +static void s_change_current_state_to_channel_shutdown(struct aws_mqtt5_client *client) { + enum aws_mqtt5_client_state current_state = client->current_state; + AWS_FATAL_ASSERT( + current_state == AWS_MCS_MQTT_CONNECT || current_state == AWS_MCS_CONNECTING || + current_state == AWS_MCS_CONNECTED || current_state == AWS_MCS_CLEAN_DISCONNECT); + + client->current_state = AWS_MCS_CHANNEL_SHUTDOWN; + + /* + * Critical requirement: The caller must invoke the channel shutdown function themselves (with the desired error + * code) *after* changing state. + * + * The caller is the only one with the error context and we want to be safe and avoid the possibility of a + * synchronous channel shutdown (mocks) leading to a situation where we get the shutdown callback before we've + * transitioned into the CHANNEL_SHUTDOWN state. + * + * We could relax this if a synchronous channel shutdown is literally impossible even with mocked channels. + */ +} + +/* TODO: refactor and reunify with internals of retry strategy to expose these as usable functions in aws-c-io */ + +static uint64_t s_aws_mqtt5_compute_reconnect_backoff_no_jitter(struct aws_mqtt5_client *client) { + uint64_t retry_count = aws_min_u64(client->reconnect_count, 63); + return aws_mul_u64_saturating((uint64_t)1 << retry_count, client->config->min_reconnect_delay_ms); +} + +static uint64_t s_aws_mqtt5_compute_reconnect_backoff_full_jitter(struct aws_mqtt5_client *client) { + uint64_t non_jittered = s_aws_mqtt5_compute_reconnect_backoff_no_jitter(client); + return aws_mqtt5_client_random_in_range(0, non_jittered); +} + +static uint64_t s_compute_deccorelated_jitter(struct aws_mqtt5_client *client) { + uint64_t last_backoff_val = client->current_reconnect_delay_ms; + + if (!last_backoff_val) { + return s_aws_mqtt5_compute_reconnect_backoff_full_jitter(client); + } + + return aws_mqtt5_client_random_in_range( + client->config->min_reconnect_delay_ms, aws_mul_u64_saturating(last_backoff_val, 3)); +} + +static void s_update_reconnect_delay_for_pending_reconnect(struct aws_mqtt5_client *client) { + uint64_t delay_ms = 0; + + switch (client->config->retry_jitter_mode) { + case AWS_EXPONENTIAL_BACKOFF_JITTER_DECORRELATED: + delay_ms = s_compute_deccorelated_jitter(client); + break; + + case AWS_EXPONENTIAL_BACKOFF_JITTER_NONE: + delay_ms = s_aws_mqtt5_compute_reconnect_backoff_no_jitter(client); + break; + + case AWS_EXPONENTIAL_BACKOFF_JITTER_FULL: + case AWS_EXPONENTIAL_BACKOFF_JITTER_DEFAULT: + default: + delay_ms = s_aws_mqtt5_compute_reconnect_backoff_full_jitter(client); + break; + } + + delay_ms = aws_min_u64(delay_ms, client->config->max_reconnect_delay_ms); + uint64_t now = (*client->vtable->get_current_time_fn)(); + + client->next_reconnect_time_ns = + aws_add_u64_saturating(now, aws_timestamp_convert(delay_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL)); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: next connection attempt in %" PRIu64 " milliseconds", (void *)client, delay_ms); + + client->reconnect_count++; +} + +static void s_change_current_state_to_pending_reconnect(struct aws_mqtt5_client *client) { + client->current_state = AWS_MCS_PENDING_RECONNECT; + + s_update_reconnect_delay_for_pending_reconnect(client); +} + +static void s_change_current_state_to_terminated(struct aws_mqtt5_client *client) { + client->current_state = AWS_MCS_TERMINATED; + + s_mqtt5_client_final_destroy(client); +} + +static void s_change_current_state(struct aws_mqtt5_client *client, enum aws_mqtt5_client_state next_state) { + AWS_ASSERT(next_state != client->current_state); + if (next_state == client->current_state) { + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: switching current state from %s to %s", + (void *)client, + aws_mqtt5_client_state_to_c_string(client->current_state), + aws_mqtt5_client_state_to_c_string(next_state)); + + if (client->vtable->on_client_state_change_callback_fn != NULL) { + (*client->vtable->on_client_state_change_callback_fn)( + client, client->current_state, next_state, client->vtable->vtable_user_data); + } + + switch (next_state) { + case AWS_MCS_STOPPED: + s_change_current_state_to_stopped(client); + break; + case AWS_MCS_CONNECTING: + s_change_current_state_to_connecting(client); + break; + case AWS_MCS_MQTT_CONNECT: + s_change_current_state_to_mqtt_connect(client); + break; + case AWS_MCS_CONNECTED: + s_change_current_state_to_connected(client); + break; + case AWS_MCS_CLEAN_DISCONNECT: + s_change_current_state_to_clean_disconnect(client); + break; + case AWS_MCS_CHANNEL_SHUTDOWN: + s_change_current_state_to_channel_shutdown(client); + break; + case AWS_MCS_PENDING_RECONNECT: + s_change_current_state_to_pending_reconnect(client); + break; + case AWS_MCS_TERMINATED: + s_change_current_state_to_terminated(client); + return; + } + + s_reevaluate_service_task(client); +} + +static bool s_service_state_stopped(struct aws_mqtt5_client *client) { + enum aws_mqtt5_client_state desired_state = client->desired_state; + if (desired_state == AWS_MCS_CONNECTED) { + s_change_current_state(client, AWS_MCS_CONNECTING); + } else if (desired_state == AWS_MCS_TERMINATED) { + s_change_current_state(client, AWS_MCS_TERMINATED); + return true; + } + + return false; +} + +static void s_service_state_connecting(struct aws_mqtt5_client *client) { + (void)client; +} + +static void s_service_state_mqtt_connect(struct aws_mqtt5_client *client, uint64_t now) { + enum aws_mqtt5_client_state desired_state = client->desired_state; + if (desired_state != AWS_MCS_CONNECTED) { + s_aws_mqtt5_client_emit_final_lifecycle_event(client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP, NULL, NULL); + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP); + return; + } + + if (now >= client->next_mqtt_connect_packet_timeout_time) { + s_aws_mqtt5_client_emit_final_lifecycle_event(client, AWS_ERROR_MQTT5_CONNACK_TIMEOUT, NULL, NULL); + + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: shutting down channel due to CONNACK timeout", (void *)client); + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT5_CONNACK_TIMEOUT); + return; + } + + if (aws_mqtt5_client_service_operational_state(&client->operational_state)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to service outgoing CONNECT packet to channel with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } +} + +static int s_aws_mqtt5_client_queue_ping(struct aws_mqtt5_client *client) { + s_reset_ping(client); + + AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: queuing PINGREQ", (void *)client); + + struct aws_mqtt5_operation_pingreq *pingreq_op = aws_mqtt5_operation_pingreq_new(client->allocator); + s_enqueue_operation_front(client, &pingreq_op->base); + + return AWS_OP_SUCCESS; +} + +static void s_service_state_connected(struct aws_mqtt5_client *client, uint64_t now) { + enum aws_mqtt5_client_state desired_state = client->desired_state; + if (desired_state != AWS_MCS_CONNECTED) { + s_aws_mqtt5_client_emit_final_lifecycle_event(client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP, NULL, NULL); + + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: channel shutdown due to user Stop request", (void *)client); + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP); + return; + } + + if (now >= client->next_ping_timeout_time && client->next_ping_timeout_time != 0) { + s_aws_mqtt5_client_emit_final_lifecycle_event(client, AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT, NULL, NULL); + + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: channel shutdown due to PINGRESP timeout", (void *)client); + + s_aws_mqtt5_client_shutdown_channel_clean( + client, AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT, AWS_MQTT5_DRC_KEEP_ALIVE_TIMEOUT); + return; + } + + if (now >= client->next_ping_time) { + if (s_aws_mqtt5_client_queue_ping(client)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to queue PINGREQ with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + } + + if (now >= client->next_reconnect_delay_reset_time_ns && client->next_reconnect_delay_reset_time_ns != 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: connected sufficiently long that reconnect backoff delay has been reset back to " + "minimum value", + (void *)client); + + client->reconnect_count = 0; + client->current_reconnect_delay_ms = 0; + client->next_reconnect_delay_reset_time_ns = 0; + } + + s_check_timeouts(client, now); + + if (aws_mqtt5_client_service_operational_state(&client->operational_state)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to service CONNECTED operation queue with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } +} + +static void s_service_state_clean_disconnect(struct aws_mqtt5_client *client, uint64_t now) { + if (aws_mqtt5_client_service_operational_state(&client->operational_state)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to service CLEAN_DISCONNECT operation queue with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + return; + } + + s_check_timeouts(client, now); +} + +static void s_service_state_channel_shutdown(struct aws_mqtt5_client *client) { + (void)client; +} + +static void s_service_state_pending_reconnect(struct aws_mqtt5_client *client, uint64_t now) { + if (client->desired_state != AWS_MCS_CONNECTED) { + s_change_current_state(client, AWS_MCS_STOPPED); + return; + } + + if (now >= client->next_reconnect_time_ns) { + s_change_current_state(client, AWS_MCS_CONNECTING); + return; + } +} + +static void s_mqtt5_service_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + if (status != AWS_TASK_STATUS_RUN_READY) { + return; + } + + struct aws_mqtt5_client *client = arg; + client->next_service_task_run_time = 0; + client->in_service = true; + + uint64_t now = (*client->vtable->get_current_time_fn)(); + bool terminated = false; + switch (client->current_state) { + case AWS_MCS_STOPPED: + terminated = s_service_state_stopped(client); + break; + case AWS_MCS_CONNECTING: + s_service_state_connecting(client); + break; + case AWS_MCS_MQTT_CONNECT: + s_service_state_mqtt_connect(client, now); + break; + case AWS_MCS_CONNECTED: + s_service_state_connected(client, now); + break; + case AWS_MCS_CLEAN_DISCONNECT: + s_service_state_clean_disconnect(client, now); + break; + case AWS_MCS_CHANNEL_SHUTDOWN: + s_service_state_channel_shutdown(client); + break; + case AWS_MCS_PENDING_RECONNECT: + s_service_state_pending_reconnect(client, now); + break; + default: + break; + } + + /* + * We can only enter the terminated state from stopped. If we do so, the client memory is now freed and we + * will crash if we access anything anymore. + */ + if (terminated) { + return; + } + + /* we're not scheduled anymore, reschedule as needed */ + client->in_service = false; + s_reevaluate_service_task(client); +} + +static bool s_should_client_disconnect_cleanly(struct aws_mqtt5_client *client) { + enum aws_mqtt5_client_state current_state = client->current_state; + + return current_state == AWS_MCS_CONNECTED; +} + +static int s_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + + struct aws_mqtt5_client *client = handler->impl; + + if (message->message_type != AWS_IO_MESSAGE_APPLICATION_DATA) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: unexpected io message data", (void *)client); + return AWS_OP_ERR; + } + + AWS_LOGF_TRACE( + AWS_LS_MQTT5_CLIENT, "id=%p: processing read message of size %zu", (void *)client, message->message_data.len); + + struct aws_byte_cursor message_cursor = aws_byte_cursor_from_buf(&message->message_data); + + int result = aws_mqtt5_decoder_on_data_received(&client->decoder, message_cursor); + if (result != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: decode failure with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + if (error_code == AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR && s_should_client_disconnect_cleanly(client)) { + s_aws_mqtt5_client_shutdown_channel_clean(client, error_code, AWS_MQTT5_DRC_PROTOCOL_ERROR); + } else { + s_aws_mqtt5_client_shutdown_channel(client, error_code); + } + + goto done; + } + + aws_channel_slot_increment_read_window(slot, message->message_data.len); + +done: + + aws_mem_release(message->allocator, message); + + return AWS_OP_SUCCESS; +} + +static int s_shutdown( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + enum aws_channel_direction dir, + int error_code, + bool free_scarce_resources_immediately) { + + (void)handler; + + return aws_channel_slot_on_handler_shutdown_complete(slot, dir, error_code, free_scarce_resources_immediately); +} + +static size_t s_initial_window_size(struct aws_channel_handler *handler) { + (void)handler; + + return SIZE_MAX; +} + +static void s_destroy(struct aws_channel_handler *handler) { + (void)handler; +} + +static size_t s_message_overhead(struct aws_channel_handler *handler) { + (void)handler; + + return 0; +} + +static struct aws_channel_handler_vtable s_mqtt5_channel_handler_vtable = { + .process_read_message = &s_process_read_message, + .process_write_message = NULL, + .increment_read_window = NULL, + .shutdown = &s_shutdown, + .initial_window_size = &s_initial_window_size, + .message_overhead = &s_message_overhead, + .destroy = &s_destroy, +}; + +static bool s_aws_is_successful_reason_code(int value) { + return value < 128; +} + +static void s_aws_mqtt5_client_on_connack( + struct aws_mqtt5_client *client, + struct aws_mqtt5_packet_connack_view *connack_view) { + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_MQTT_CONNECT); + + bool is_successful = s_aws_is_successful_reason_code((int)connack_view->reason_code); + if (!is_successful) { + s_aws_mqtt5_client_emit_final_lifecycle_event( + client, AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, connack_view, NULL); + + enum aws_mqtt5_connect_reason_code reason_code = connack_view->reason_code; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: connection refused (via failed CONNACK) by remote host with reason code %d(%s)", + (void *)client, + (int)reason_code, + aws_mqtt5_connect_reason_code_to_c_string(reason_code)); + + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED); + return; + } + + aws_mqtt5_negotiated_settings_apply_connack(&client->negotiated_settings, connack_view); + + /* Check if a session is being rejoined and perform associated rejoin connect logic here */ + if (client->negotiated_settings.rejoined_session) { + /* Disconnect if the server is attempting to connect the client to an unexpected session */ + if (aws_mqtt5_client_session_behavior_type_to_non_default(client->config->session_behavior) == + AWS_MQTT5_CSBT_CLEAN || + client->has_connected_successfully == false) { + s_aws_mqtt5_client_emit_final_lifecycle_event( + client, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION, connack_view, NULL); + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); + return; + } + } + + s_change_current_state(client, AWS_MCS_CONNECTED); + s_aws_mqtt5_client_emit_connection_success_lifecycle_event(client, connack_view); +} + +static void s_aws_mqtt5_client_log_received_packet( + struct aws_mqtt5_client *client, + enum aws_mqtt5_packet_type type, + void *packet_view) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: Received %s packet", (void *)client, aws_mqtt5_packet_type_to_c_string(type)); + + switch (type) { + case AWS_MQTT5_PT_CONNACK: + aws_mqtt5_packet_connack_view_log(packet_view, AWS_LL_DEBUG); + break; + + case AWS_MQTT5_PT_PUBLISH: + aws_mqtt5_packet_publish_view_log(packet_view, AWS_LL_DEBUG); + break; + + case AWS_MQTT5_PT_PUBACK: + aws_mqtt5_packet_puback_view_log(packet_view, AWS_LL_DEBUG); + break; + + case AWS_MQTT5_PT_SUBACK: + aws_mqtt5_packet_suback_view_log(packet_view, AWS_LL_DEBUG); + break; + + case AWS_MQTT5_PT_UNSUBACK: + aws_mqtt5_packet_unsuback_view_log(packet_view, AWS_LL_DEBUG); + break; + + case AWS_MQTT5_PT_PINGRESP: + break; /* nothing to log */ + + case AWS_MQTT5_PT_DISCONNECT: + aws_mqtt5_packet_disconnect_view_log(packet_view, AWS_LL_DEBUG); + break; + + default: + break; + } +} + +static void s_aws_mqtt5_client_mqtt_connect_on_packet_received( + struct aws_mqtt5_client *client, + enum aws_mqtt5_packet_type type, + void *packet_view) { + if (type == AWS_MQTT5_PT_CONNACK) { + s_aws_mqtt5_client_on_connack(client, (struct aws_mqtt5_packet_connack_view *)packet_view); + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, "id=%p: Invalid packet type received while in MQTT_CONNECT state", (void *)client); + + s_aws_mqtt5_client_shutdown_channel_clean( + client, AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR, AWS_MQTT5_DRC_PROTOCOL_ERROR); + } +} + +typedef bool(aws_linked_list_node_predicate_fn)(struct aws_linked_list_node *); + +/* + * This predicate finds the first (if any) operation in the queue that is not a PUBACK or a PINGREQ. + */ +static bool s_is_ping_or_puback(struct aws_linked_list_node *operation_node) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(operation_node, struct aws_mqtt5_operation, node); + + return operation->packet_type == AWS_MQTT5_PT_PUBACK || operation->packet_type == AWS_MQTT5_PT_PINGREQ; +} + +/* + * Helper function to insert a node (operation) into a list (operation queue) in the correct spot. Currently, this + * is only used to enqueue PUBACKs after existing PUBACKs and PINGREQs. This ensure that PUBACKs go out in the order + * the corresponding PUBLISH was received, regardless of whether or not there was an intervening service call. + */ +static void s_insert_node_before_predicate_failure( + struct aws_linked_list *list, + struct aws_linked_list_node *node, + aws_linked_list_node_predicate_fn predicate) { + struct aws_linked_list_node *current_node = NULL; + for (current_node = aws_linked_list_begin(list); current_node != aws_linked_list_end(list); + current_node = aws_linked_list_next(current_node)) { + if (!predicate(current_node)) { + break; + } + } + + AWS_FATAL_ASSERT(current_node != NULL); + + aws_linked_list_insert_before(current_node, node); +} + +static int s_aws_mqtt5_client_queue_puback(struct aws_mqtt5_client *client, uint16_t packet_id) { + AWS_PRECONDITION(client != NULL); + + const struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = packet_id, + .reason_code = AWS_MQTT5_PARC_SUCCESS, + }; + + struct aws_mqtt5_operation_puback *puback_op = aws_mqtt5_operation_puback_new(client->allocator, &puback_view); + + if (puback_op == NULL) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: enqueuing PUBACK operation to first position in queue that is not a PUBACK or PINGREQ", + (void *)client); + + /* + * Put the PUBACK ahead of all user-submitted operations (PUBLISH, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT), but behind + * all pre-existing "internal" operations (PINGREQ, PUBACK). + * + * Qos 2 support will need to extend the predicate to include Qos 2 publish packets. + */ + s_insert_node_before_predicate_failure( + &client->operational_state.queued_operations, &puback_op->base.node, s_is_ping_or_puback); + + s_reevaluate_service_task(client); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_client_connected_on_packet_received( + struct aws_mqtt5_client *client, + enum aws_mqtt5_packet_type type, + void *packet_view) { + + switch (type) { + case AWS_MQTT5_PT_PINGRESP: + AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: resetting PINGREQ timer", (void *)client); + client->next_ping_timeout_time = 0; + break; + + case AWS_MQTT5_PT_DISCONNECT: + s_aws_mqtt5_client_emit_final_lifecycle_event( + client, AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, NULL, packet_view); + + AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: shutting down channel due to DISCONNECT", (void *)client); + + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT5_DISCONNECT_RECEIVED); + break; + + case AWS_MQTT5_PT_SUBACK: { + uint16_t packet_id = ((const struct aws_mqtt5_packet_suback_view *)packet_view)->packet_id; + aws_mqtt5_client_operational_state_handle_ack( + &client->operational_state, packet_id, AWS_MQTT5_PT_SUBACK, packet_view, AWS_ERROR_SUCCESS); + break; + } + + case AWS_MQTT5_PT_UNSUBACK: { + uint16_t packet_id = ((const struct aws_mqtt5_packet_unsuback_view *)packet_view)->packet_id; + aws_mqtt5_client_operational_state_handle_ack( + &client->operational_state, packet_id, AWS_MQTT5_PT_UNSUBACK, packet_view, AWS_ERROR_SUCCESS); + break; + } + + case AWS_MQTT5_PT_PUBLISH: { + const struct aws_mqtt5_packet_publish_view *publish_view = packet_view; + + client->config->publish_received_handler(publish_view, client->config->publish_received_handler_user_data); + + /* Send a puback if QoS 1+ */ + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + + int result = s_aws_mqtt5_client_queue_puback(client, publish_view->packet_id); + if (result != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: decode failure with error %d(%s)", + (void *)client, + error_code, + aws_error_debug_str(error_code)); + + s_aws_mqtt5_client_shutdown_channel(client, error_code); + } + } + break; + } + + case AWS_MQTT5_PT_PUBACK: { + uint16_t packet_id = ((const struct aws_mqtt5_packet_puback_view *)packet_view)->packet_id; + aws_mqtt5_client_operational_state_handle_ack( + &client->operational_state, packet_id, AWS_MQTT5_PT_PUBACK, packet_view, AWS_ERROR_SUCCESS); + break; + } + + default: + break; + } +} + +static int s_aws_mqtt5_client_on_packet_received( + enum aws_mqtt5_packet_type type, + void *packet_view, + void *decoder_callback_user_data) { + + struct aws_mqtt5_client *client = decoder_callback_user_data; + + s_aws_mqtt5_client_log_received_packet(client, type, packet_view); + + switch (client->current_state) { + case AWS_MCS_MQTT_CONNECT: + s_aws_mqtt5_client_mqtt_connect_on_packet_received(client, type, packet_view); + break; + + case AWS_MCS_CONNECTED: + case AWS_MCS_CLEAN_DISCONNECT: + s_aws_mqtt5_client_connected_on_packet_received(client, type, packet_view); + break; + + default: + break; + } + + s_reevaluate_service_task(client); + + return AWS_OP_SUCCESS; +} + +static uint64_t s_aws_high_res_clock_get_ticks_proxy(void) { + uint64_t current_time = 0; + AWS_FATAL_ASSERT(aws_high_res_clock_get_ticks(¤t_time) == AWS_OP_SUCCESS); + + return current_time; +} + +struct aws_io_message *s_aws_channel_acquire_message_from_pool_default( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data) { + (void)user_data; + + return aws_channel_acquire_message_from_pool(channel, message_type, size_hint); +} + +static int s_aws_channel_slot_send_message_default( + struct aws_channel_slot *slot, + struct aws_io_message *message, + enum aws_channel_direction dir, + void *user_data) { + (void)user_data; + + return aws_channel_slot_send_message(slot, message, dir); +} + +static struct aws_mqtt5_client_vtable s_default_client_vtable = { + .get_current_time_fn = s_aws_high_res_clock_get_ticks_proxy, + .channel_shutdown_fn = aws_channel_shutdown, + .websocket_connect_fn = aws_websocket_client_connect, + .client_bootstrap_new_socket_channel_fn = aws_client_bootstrap_new_socket_channel, + .http_proxy_new_socket_channel_fn = aws_http_proxy_new_socket_channel, + .on_client_state_change_callback_fn = NULL, + .aws_channel_acquire_message_from_pool_fn = s_aws_channel_acquire_message_from_pool_default, + .aws_channel_slot_send_message_fn = s_aws_channel_slot_send_message_default, + + .vtable_user_data = NULL, +}; + +void aws_mqtt5_client_set_vtable(struct aws_mqtt5_client *client, const struct aws_mqtt5_client_vtable *vtable) { + client->vtable = vtable; +} + +const struct aws_mqtt5_client_vtable *aws_mqtt5_client_get_default_vtable(void) { + return &s_default_client_vtable; +} + +struct aws_mqtt5_client *aws_mqtt5_client_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client_options *options) { + AWS_FATAL_ASSERT(allocator != NULL); + AWS_FATAL_ASSERT(options != NULL); + + struct aws_mqtt5_client *client = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_client)); + if (client == NULL) { + return NULL; + } + + aws_task_init(&client->service_task, s_mqtt5_service_task_fn, client, "Mqtt5Service"); + + client->allocator = allocator; + client->vtable = &s_default_client_vtable; + + aws_ref_count_init(&client->ref_count, client, s_on_mqtt5_client_zero_ref_count); + + if (aws_mqtt5_client_operational_state_init(&client->operational_state, allocator, client)) { + goto on_error; + } + + client->config = aws_mqtt5_client_options_storage_new(allocator, options); + if (client->config == NULL) { + goto on_error; + } + + aws_mqtt5_client_flow_control_state_init(client); + + /* all client activity will take place on this event loop, serializing things like reconnect, ping, etc... */ + client->loop = aws_event_loop_group_get_next_loop(client->config->bootstrap->event_loop_group); + if (client->loop == NULL) { + goto on_error; + } + + client->desired_state = AWS_MCS_STOPPED; + client->current_state = AWS_MCS_STOPPED; + client->lifecycle_state = AWS_MQTT5_LS_NONE; + + struct aws_mqtt5_decoder_options decoder_options = { + .callback_user_data = client, + .on_packet_received = s_aws_mqtt5_client_on_packet_received, + }; + + if (aws_mqtt5_decoder_init(&client->decoder, allocator, &decoder_options)) { + goto on_error; + } + + struct aws_mqtt5_encoder_options encoder_options = { + .client = client, + }; + + if (aws_mqtt5_encoder_init(&client->encoder, allocator, &encoder_options)) { + goto on_error; + } + + if (aws_mqtt5_inbound_topic_alias_resolver_init(&client->inbound_topic_alias_resolver, allocator)) { + goto on_error; + } + + client->outbound_topic_alias_resolver = aws_mqtt5_outbound_topic_alias_resolver_new( + allocator, client->config->topic_aliasing_options.outbound_topic_alias_behavior); + if (client->outbound_topic_alias_resolver == NULL) { + goto on_error; + } + + if (aws_mqtt5_negotiated_settings_init( + allocator, &client->negotiated_settings, &options->connect_options->client_id)) { + goto on_error; + } + + client->current_reconnect_delay_ms = 0; + + client->handler.alloc = client->allocator; + client->handler.vtable = &s_mqtt5_channel_handler_vtable; + client->handler.impl = client; + + aws_mqtt5_client_options_storage_log(client->config, AWS_LL_DEBUG); + + aws_mutex_init(&client->operation_statistics_lock); + + return client; + +on_error: + + /* release isn't usable here since we may not even have an event loop */ + s_mqtt5_client_final_destroy(client); + + return NULL; +} + +struct aws_mqtt5_client *aws_mqtt5_client_acquire(struct aws_mqtt5_client *client) { + if (client != NULL) { + aws_ref_count_acquire(&client->ref_count); + } + + return client; +} + +struct aws_mqtt5_client *aws_mqtt5_client_release(struct aws_mqtt5_client *client) { + if (client != NULL) { + aws_ref_count_release(&client->ref_count); + } + + return NULL; +} + +struct aws_mqtt_change_desired_state_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt5_client *client; + enum aws_mqtt5_client_state desired_state; + struct aws_mqtt5_operation_disconnect *disconnect_operation; +}; + +static void s_change_state_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_change_desired_state_task *change_state_task = arg; + struct aws_mqtt5_client *client = change_state_task->client; + enum aws_mqtt5_client_state desired_state = change_state_task->desired_state; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + if (client->desired_state != desired_state) { + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: changing desired client state from %s to %s", + (void *)client, + aws_mqtt5_client_state_to_c_string(client->desired_state), + aws_mqtt5_client_state_to_c_string(desired_state)); + + client->desired_state = desired_state; + + struct aws_mqtt5_operation_disconnect *disconnect_op = change_state_task->disconnect_operation; + if (desired_state == AWS_MCS_STOPPED && disconnect_op != NULL) { + s_aws_mqtt5_client_shutdown_channel_with_disconnect( + client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP, disconnect_op); + } + + s_reevaluate_service_task(client); + } + +done: + + aws_mqtt5_operation_disconnect_release(change_state_task->disconnect_operation); + if (desired_state != AWS_MCS_TERMINATED) { + aws_mqtt5_client_release(client); + } + + aws_mem_release(change_state_task->allocator, change_state_task); +} + +static struct aws_mqtt_change_desired_state_task *s_aws_mqtt_change_desired_state_task_new( + struct aws_allocator *allocator, + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state desired_state, + struct aws_mqtt5_operation_disconnect *disconnect_operation) { + + struct aws_mqtt_change_desired_state_task *change_state_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_change_desired_state_task)); + if (change_state_task == NULL) { + return NULL; + } + + aws_task_init(&change_state_task->task, s_change_state_task_fn, (void *)change_state_task, "ChangeStateTask"); + change_state_task->allocator = client->allocator; + change_state_task->client = (desired_state == AWS_MCS_TERMINATED) ? client : aws_mqtt5_client_acquire(client); + change_state_task->desired_state = desired_state; + change_state_task->disconnect_operation = aws_mqtt5_operation_disconnect_acquire(disconnect_operation); + + return change_state_task; +} + +static bool s_is_valid_desired_state(enum aws_mqtt5_client_state desired_state) { + switch (desired_state) { + case AWS_MCS_STOPPED: + case AWS_MCS_CONNECTED: + case AWS_MCS_TERMINATED: + return true; + + default: + return false; + } +} + +static int s_aws_mqtt5_client_change_desired_state( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state desired_state, + struct aws_mqtt5_operation_disconnect *disconnect_operation) { + AWS_FATAL_ASSERT(client != NULL); + AWS_FATAL_ASSERT(client->loop != NULL); + AWS_FATAL_ASSERT(disconnect_operation == NULL || desired_state == AWS_MCS_STOPPED); + + if (!s_is_valid_desired_state(desired_state)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: invalid desired state argument %d(%s)", + (void *)client, + (int)desired_state, + aws_mqtt5_client_state_to_c_string(desired_state)); + + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_mqtt_change_desired_state_task *task = + s_aws_mqtt_change_desired_state_task_new(client->allocator, client, desired_state, disconnect_operation); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: failed to create change desired state task", (void *)client); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(client->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_client_start(struct aws_mqtt5_client *client) { + return s_aws_mqtt5_client_change_desired_state(client, AWS_MCS_CONNECTED, NULL); +} + +int aws_mqtt5_client_stop( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_disconnect_view *options, + const struct aws_mqtt5_disconnect_completion_options *completion_options) { + struct aws_mqtt5_operation_disconnect *disconnect_op = NULL; + if (options != NULL) { + struct aws_mqtt5_disconnect_completion_options internal_completion_options = { + .completion_callback = s_on_disconnect_operation_complete, + .completion_user_data = client, + }; + + disconnect_op = aws_mqtt5_operation_disconnect_new( + client->allocator, options, completion_options, &internal_completion_options); + if (disconnect_op == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, "id=%p: failed to create requested DISCONNECT operation", (void *)client); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: Stopping client via DISCONNECT operation (%p)", + (void *)client, + (void *)disconnect_op); + aws_mqtt5_packet_disconnect_view_log(disconnect_op->base.packet_view, AWS_LL_DEBUG); + } else { + AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: Stopping client immediately", (void *)client); + } + + int result = s_aws_mqtt5_client_change_desired_state(client, AWS_MCS_STOPPED, disconnect_op); + + aws_mqtt5_operation_disconnect_release(disconnect_op); + + return result; +} + +struct aws_mqtt5_submit_operation_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt5_client *client; + struct aws_mqtt5_operation *operation; +}; + +static void s_mqtt5_submit_operation_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + int completion_error_code = AWS_ERROR_MQTT5_CLIENT_TERMINATED; + struct aws_mqtt5_submit_operation_task *submit_operation_task = arg; + + /* + * Take a ref to the operation that represents the client taking ownership + * If we subsequently reject it (task cancel or offline queue policy), then the operation completion + * will undo this ref acquisition. + */ + aws_mqtt5_operation_acquire(submit_operation_task->operation); + + if (status != AWS_TASK_STATUS_RUN_READY) { + goto error; + } + + /* + * If we're offline and this operation doesn't meet the requirements of the offline queue retention policy, + * fail it immediately. + */ + struct aws_mqtt5_client *client = submit_operation_task->client; + struct aws_mqtt5_operation *operation = submit_operation_task->operation; + if (client->current_state != AWS_MCS_CONNECTED) { + if (!s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy( + operation, client->config->offline_queue_behavior)) { + completion_error_code = AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY; + goto error; + } + } + + /* newly-submitted operations must have a 0 packet id */ + aws_mqtt5_operation_set_packet_id(submit_operation_task->operation, 0); + + s_enqueue_operation_back(submit_operation_task->client, submit_operation_task->operation); + aws_mqtt5_client_statistics_change_operation_statistic_state( + submit_operation_task->client, submit_operation_task->operation, AWS_MQTT5_OSS_INCOMPLETE); + + goto done; + +error: + + s_complete_operation(NULL, submit_operation_task->operation, completion_error_code, AWS_MQTT5_PT_NONE, NULL); + +done: + + aws_mqtt5_operation_release(submit_operation_task->operation); + aws_mqtt5_client_release(submit_operation_task->client); + + aws_mem_release(submit_operation_task->allocator, submit_operation_task); +} + +static int s_submit_operation(struct aws_mqtt5_client *client, struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_submit_operation_task *submit_task = + aws_mem_calloc(client->allocator, 1, sizeof(struct aws_mqtt5_submit_operation_task)); + if (submit_task == NULL) { + return AWS_OP_ERR; + } + + aws_task_init(&submit_task->task, s_mqtt5_submit_operation_task_fn, submit_task, "Mqtt5SubmitOperation"); + submit_task->allocator = client->allocator; + submit_task->client = aws_mqtt5_client_acquire(client); + submit_task->operation = operation; + + aws_event_loop_schedule_task_now(client->loop, &submit_task->task); + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_client_publish( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_publish_view *publish_options, + const struct aws_mqtt5_publish_completion_options *completion_options) { + + AWS_PRECONDITION(client != NULL); + AWS_PRECONDITION(publish_options != NULL); + + struct aws_mqtt5_operation_publish *publish_op = + aws_mqtt5_operation_publish_new(client->allocator, client, publish_options, completion_options); + + if (publish_op == NULL) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: Submitting PUBLISH operation (%p)", (void *)client, (void *)publish_op); + aws_mqtt5_packet_publish_view_log(publish_op->base.packet_view, AWS_LL_DEBUG); + + if (s_submit_operation(client, &publish_op->base)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + aws_mqtt5_operation_release(&publish_op->base); + + return AWS_OP_ERR; +} + +int aws_mqtt5_client_subscribe( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options, + const struct aws_mqtt5_subscribe_completion_options *completion_options) { + + AWS_PRECONDITION(client != NULL); + AWS_PRECONDITION(subscribe_options != NULL); + + struct aws_mqtt5_operation_subscribe *subscribe_op = + aws_mqtt5_operation_subscribe_new(client->allocator, client, subscribe_options, completion_options); + + if (subscribe_op == NULL) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: Submitting SUBSCRIBE operation (%p)", (void *)client, (void *)subscribe_op); + aws_mqtt5_packet_subscribe_view_log(subscribe_op->base.packet_view, AWS_LL_DEBUG); + + if (s_submit_operation(client, &subscribe_op->base)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + aws_mqtt5_operation_release(&subscribe_op->base); + + return AWS_OP_ERR; +} + +int aws_mqtt5_client_unsubscribe( + struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options, + const struct aws_mqtt5_unsubscribe_completion_options *completion_options) { + + AWS_PRECONDITION(client != NULL); + AWS_PRECONDITION(unsubscribe_options != NULL); + + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = + aws_mqtt5_operation_unsubscribe_new(client->allocator, client, unsubscribe_options, completion_options); + + if (unsubscribe_op == NULL) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: Submitting UNSUBSCRIBE operation (%p)", (void *)client, (void *)unsubscribe_op); + aws_mqtt5_packet_unsubscribe_view_log(unsubscribe_op->base.packet_view, AWS_LL_DEBUG); + + if (s_submit_operation(client, &unsubscribe_op->base)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + aws_mqtt5_operation_release(&unsubscribe_op->base); + + return AWS_OP_ERR; +} + +static bool s_needs_packet_id(const struct aws_mqtt5_operation *operation) { + switch (operation->packet_type) { + case AWS_MQTT5_PT_SUBSCRIBE: + case AWS_MQTT5_PT_UNSUBSCRIBE: + return aws_mqtt5_operation_get_packet_id(operation) == 0; + + case AWS_MQTT5_PT_PUBLISH: { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos == AWS_MQTT5_QOS_AT_MOST_ONCE) { + return false; + } + + return aws_mqtt5_operation_get_packet_id(operation) == 0; + } + + default: + return false; + } +} + +static uint16_t s_next_packet_id(uint16_t current_id) { + if (++current_id == 0) { + current_id = 1; + } + + return current_id; +} + +int aws_mqtt5_operation_bind_packet_id( + struct aws_mqtt5_operation *operation, + struct aws_mqtt5_client_operational_state *client_operational_state) { + if (!s_needs_packet_id(operation)) { + return AWS_OP_SUCCESS; + } + + uint16_t current_id = client_operational_state->next_mqtt_packet_id; + struct aws_hash_element *elem = NULL; + for (uint16_t i = 0; i < UINT16_MAX; ++i) { + aws_hash_table_find(&client_operational_state->unacked_operations_table, ¤t_id, &elem); + + if (elem == NULL) { + aws_mqtt5_operation_set_packet_id(operation, current_id); + client_operational_state->next_mqtt_packet_id = s_next_packet_id(current_id); + + return AWS_OP_SUCCESS; + } + + current_id = s_next_packet_id(current_id); + } + + aws_raise_error(AWS_ERROR_INVALID_STATE); + return AWS_OP_ERR; +} + +int aws_mqtt5_client_operational_state_init( + struct aws_mqtt5_client_operational_state *client_operational_state, + struct aws_allocator *allocator, + struct aws_mqtt5_client *client) { + + aws_linked_list_init(&client_operational_state->queued_operations); + aws_linked_list_init(&client_operational_state->write_completion_operations); + aws_linked_list_init(&client_operational_state->unacked_operations); + + if (aws_hash_table_init( + &client_operational_state->unacked_operations_table, + allocator, + sizeof(struct aws_mqtt5_operation *), + s_hash_uint16_t, + s_uint16_t_eq, + NULL, + NULL)) { + return AWS_OP_ERR; + } + + client_operational_state->next_mqtt_packet_id = 1; + client_operational_state->current_operation = NULL; + client_operational_state->client = client; + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_client_operational_state_clean_up(struct aws_mqtt5_client_operational_state *client_operational_state) { + AWS_ASSERT(client_operational_state->current_operation == NULL); + + s_aws_mqtt5_client_operational_state_reset(client_operational_state, AWS_ERROR_MQTT5_CLIENT_TERMINATED, true); +} + +static bool s_filter_queued_operations_for_offline(struct aws_mqtt5_operation *operation, void *context) { + struct aws_mqtt5_client *client = context; + enum aws_mqtt5_client_operation_queue_behavior_type queue_behavior = client->config->offline_queue_behavior; + + return !s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy(operation, queue_behavior); +} + +static void s_process_unacked_operations_for_disconnect(struct aws_mqtt5_operation *operation, void *context) { + (void)context; + + if (operation->packet_type == AWS_MQTT5_PT_PUBLISH) { + struct aws_mqtt5_packet_publish_view *publish_view = + (struct aws_mqtt5_packet_publish_view *)operation->packet_view; + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + publish_view->duplicate = true; + return; + } + } + + aws_mqtt5_operation_set_packet_id(operation, 0); +} + +static bool s_filter_unacked_operations_for_offline(struct aws_mqtt5_operation *operation, void *context) { + struct aws_mqtt5_client *client = context; + enum aws_mqtt5_client_operation_queue_behavior_type queue_behavior = client->config->offline_queue_behavior; + + if (operation->packet_type == AWS_MQTT5_PT_PUBLISH) { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + return false; + } + } + + return !s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy(operation, queue_behavior); +} + +/* + * Resets the client's operational state based on a disconnection (from above comment): + * + * If current_operation + * move current_operation to head of queued_operations + * Fail all operations in the pending write completion list + * Fail, remove, and release operations in queued_operations where they fail the offline queue policy + * Iterate unacked_operations: + * If qos1+ publish + * set dup flag + * else + * unset/release packet id + * Fail, remove, and release unacked_operations if: + * (1) They fail the offline queue policy AND + * (2) the operation is not Qos 1+ publish + * + * Clears the unacked_operations table + */ +void aws_mqtt5_client_on_disconnection_update_operational_state(struct aws_mqtt5_client *client) { + struct aws_mqtt5_client_operational_state *client_operational_state = &client->operational_state; + + /* move current operation to the head of the queue */ + if (client_operational_state->current_operation != NULL) { + aws_linked_list_push_front( + &client_operational_state->queued_operations, &client_operational_state->current_operation->node); + client_operational_state->current_operation = NULL; + } + + /* fail everything in pending write completion */ + s_complete_operation_list( + client, + &client_operational_state->write_completion_operations, + AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY); + + struct aws_linked_list operations_to_fail; + AWS_ZERO_STRUCT(operations_to_fail); + aws_linked_list_init(&operations_to_fail); + + /* fail everything in the pending queue that doesn't meet the offline queue behavior retention requirements */ + s_filter_operation_list( + &client_operational_state->queued_operations, + s_filter_queued_operations_for_offline, + &operations_to_fail, + client); + s_complete_operation_list( + client, &operations_to_fail, AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY); + + /* Mark unacked qos1+ publishes as duplicate and release packet ids for non qos1+ publish */ + s_apply_to_operation_list( + &client_operational_state->unacked_operations, s_process_unacked_operations_for_disconnect, NULL); + + /* + * fail everything in the pending queue that + * (1) isn't a qos1+ publish AND + * (2) doesn't meet the offline queue behavior retention requirements + */ + s_filter_operation_list( + &client_operational_state->unacked_operations, + s_filter_unacked_operations_for_offline, + &operations_to_fail, + client); + s_complete_operation_list( + client, &operations_to_fail, AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY); + + aws_hash_table_clear(&client->operational_state.unacked_operations_table); + + /* + * Prevents inbound resolution on the highly unlikely, illegal server behavior of sending a PUBLISH before + * a CONNACK on next connection establishment. + */ + aws_mqtt5_decoder_set_inbound_topic_alias_resolver(&client->decoder, NULL); +} + +static void s_set_operation_list_statistic_state( + struct aws_mqtt5_client *client, + struct aws_linked_list *operation_list, + enum aws_mqtt5_operation_statistic_state_flags new_state_flags) { + struct aws_linked_list_node *node = aws_linked_list_begin(operation_list); + while (node != aws_linked_list_end(operation_list)) { + struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + node = aws_linked_list_next(node); + + aws_mqtt5_client_statistics_change_operation_statistic_state(client, operation, new_state_flags); + } +} + +static bool s_filter_unacked_operations_for_session_rejoin(struct aws_mqtt5_operation *operation, void *context) { + (void)context; + + if (operation->packet_type == AWS_MQTT5_PT_PUBLISH) { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + return false; + } + } + + return true; +} + +/* + * Updates the client's operational state based on a successfully established connection event: + * + * if rejoined_session: + * Move-and-append all non-qos1+-publishes in unacked_operations to the front of queued_operations + * Move-and-append remaining operations (qos1+ publishes) to the front of queued_operations + * else: + * Fail, remove, and release unacked_operations that fail the offline queue policy + * Move and append unacked operations to front of queued_operations + */ +void aws_mqtt5_client_on_connection_update_operational_state(struct aws_mqtt5_client *client) { + + struct aws_mqtt5_client_operational_state *client_operational_state = &client->operational_state; + + if (client->negotiated_settings.rejoined_session) { + struct aws_linked_list requeued_operations; + AWS_ZERO_STRUCT(requeued_operations); + aws_linked_list_init(&requeued_operations); + + /* + * qos1+ publishes must go out first, so split the unacked operation list into two sets: qos1+ publishes and + * everything else. + */ + s_filter_operation_list( + &client_operational_state->unacked_operations, + s_filter_unacked_operations_for_session_rejoin, + &requeued_operations, + client); + + /* + * Put non-qos1+ publishes on the front of the pending queue + */ + aws_linked_list_move_all_front(&client->operational_state.queued_operations, &requeued_operations); + + /* + * Put qos1+ publishes on the front of the pending queue + */ + aws_linked_list_move_all_front( + &client->operational_state.queued_operations, &client_operational_state->unacked_operations); + } else { + struct aws_linked_list failed_operations; + AWS_ZERO_STRUCT(failed_operations); + aws_linked_list_init(&failed_operations); + + s_filter_operation_list( + &client_operational_state->unacked_operations, + s_filter_queued_operations_for_offline, + &failed_operations, + client); + + /* + * fail operations that we aren't going to requeue. In this particular case it's only qos1+ publishes + * that we didn't fail because we didn't know if we were going to rejoin a sesison or not. + */ + s_complete_operation_list( + client, &failed_operations, AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY); + + /* requeue operations that we are going to perform again */ + aws_linked_list_move_all_front( + &client->operational_state.queued_operations, &client->operational_state.unacked_operations); + } + + /* set everything remaining to incomplete */ + s_set_operation_list_statistic_state( + client, &client->operational_state.queued_operations, AWS_MQTT5_OSS_INCOMPLETE); + + aws_mqtt5_client_flow_control_state_reset(client); + + uint16_t inbound_alias_maximum = client->negotiated_settings.topic_alias_maximum_to_client; + + if (aws_mqtt5_inbound_topic_alias_resolver_reset(&client->inbound_topic_alias_resolver, inbound_alias_maximum)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: client unable to reset inbound alias resolver", + (void *)client_operational_state->client); + goto on_error; + } + + if (inbound_alias_maximum > 0) { + aws_mqtt5_decoder_set_inbound_topic_alias_resolver(&client->decoder, &client->inbound_topic_alias_resolver); + } else { + aws_mqtt5_decoder_set_inbound_topic_alias_resolver(&client->decoder, NULL); + } + + uint16_t outbound_alias_maximum = client->negotiated_settings.topic_alias_maximum_to_server; + if (aws_mqtt5_outbound_topic_alias_resolver_reset(client->outbound_topic_alias_resolver, outbound_alias_maximum)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: client unable to reset outbound alias resolver", + (void *)client_operational_state->client); + goto on_error; + } + + aws_mqtt5_encoder_set_outbound_topic_alias_resolver(&client->encoder, client->outbound_topic_alias_resolver); + + return; + +on_error: + + s_aws_mqtt5_client_shutdown_channel(client, aws_last_error()); +} + +static bool s_aws_mqtt5_client_has_pending_operational_work( + const struct aws_mqtt5_client_operational_state *client_operational_state, + enum aws_mqtt5_client_state client_state) { + if (aws_linked_list_empty(&client_operational_state->queued_operations)) { + return false; + } + + struct aws_linked_list_node *next_operation_node = + aws_linked_list_front(&client_operational_state->queued_operations); + struct aws_mqtt5_operation *next_operation = + AWS_CONTAINER_OF(next_operation_node, struct aws_mqtt5_operation, node); + + switch (client_state) { + case AWS_MCS_MQTT_CONNECT: + /* Only allowed to send a CONNECT packet in this state */ + return next_operation->packet_type == AWS_MQTT5_PT_CONNECT; + + case AWS_MCS_CLEAN_DISCONNECT: + /* Except for finishing the current operation, only allowed to send a DISCONNECT packet in this state */ + return next_operation->packet_type == AWS_MQTT5_PT_DISCONNECT; + + case AWS_MCS_CONNECTED: + return true; + + default: + return false; + } +} + +static uint64_t s_aws_mqtt5_client_compute_next_operation_flow_control_service_time( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + uint64_t now) { + (void)operation; + + switch (client->current_state) { + case AWS_MCS_MQTT_CONNECT: + case AWS_MCS_CLEAN_DISCONNECT: + return now; + + case AWS_MCS_CONNECTED: + return aws_mqtt5_client_flow_control_state_get_next_operation_service_time(client, operation, now); + + default: + /* no outbound traffic is allowed outside of the above states */ + return 0; + } +} + +/* + * We don't presently know if IoT Core's throughput limit is on the plaintext or encrypted data stream. Assume + * it's on the encrypted stream for now and make a reasonable guess at the additional cost TLS imposes on data size: + * + * This calculation is intended to be a reasonable default but will not be accurate in all cases + * + * Estimate the # of ethernet frames (max 1444 bytes) and add in potential TLS framing and padding values per. + * + * TODO: query IoT Core to determine if this calculation is needed after all + * TODO: may eventually want to expose the ethernet frame size here as a configurable option for networks that have a + * lower MTU + * + * References: + * https://tools.ietf.org/id/draft-mattsson-uta-tls-overhead-01.xml#rfc.section.3 + * + */ + +#define ETHERNET_FRAME_MAX_PAYLOAD_SIZE 1500 +#define TCP_SIZE_OVERESTIMATE 72 +#define TLS_FRAMING_AND_PADDING_OVERESTIMATE 64 +#define AVAILABLE_ETHERNET_FRAME_SIZE \ + (ETHERNET_FRAME_MAX_PAYLOAD_SIZE - (TCP_SIZE_OVERESTIMATE + TLS_FRAMING_AND_PADDING_OVERESTIMATE)) +#define ETHERNET_FRAMES_PER_IO_MESSAGE_ESTIMATE \ + ((AWS_MQTT5_IO_MESSAGE_DEFAULT_LENGTH + AVAILABLE_ETHERNET_FRAME_SIZE - 1) / AVAILABLE_ETHERNET_FRAME_SIZE) +#define THROUGHPUT_TOKENS_PER_IO_MESSAGE_OVERESTIMATE \ + (AWS_MQTT5_IO_MESSAGE_DEFAULT_LENGTH + \ + ETHERNET_FRAMES_PER_IO_MESSAGE_ESTIMATE * TLS_FRAMING_AND_PADDING_OVERESTIMATE) + +static uint64_t s_compute_throughput_throttle_wait(const struct aws_mqtt5_client *client, uint64_t now) { + + /* flow control only applies during CONNECTED/CLEAN_DISCONNECT */ + if (!aws_mqtt5_client_are_negotiated_settings_valid(client)) { + return now; + } + + uint64_t throughput_wait = 0; + if (client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + throughput_wait = aws_rate_limiter_token_bucket_compute_wait_for_tokens( + (struct aws_rate_limiter_token_bucket *)&client->flow_control_state.throughput_throttle, + THROUGHPUT_TOKENS_PER_IO_MESSAGE_OVERESTIMATE); + } + + return aws_add_u64_saturating(now, throughput_wait); +} + +static uint64_t s_aws_mqtt5_client_compute_operational_state_service_time( + const struct aws_mqtt5_client_operational_state *client_operational_state, + uint64_t now) { + /* If an io message is in transit down the channel, then wait for it to complete */ + if (client_operational_state->pending_write_completion) { + return 0; + } + + /* Throughput flow control check */ + uint64_t next_throttled_time = s_compute_throughput_throttle_wait(client_operational_state->client, now); + if (next_throttled_time > now) { + return next_throttled_time; + } + + /* If we're in the middle of something, keep going */ + if (client_operational_state->current_operation != NULL) { + return now; + } + + /* If nothing is queued, there's nothing to do */ + enum aws_mqtt5_client_state client_state = client_operational_state->client->current_state; + if (!s_aws_mqtt5_client_has_pending_operational_work(client_operational_state, client_state)) { + return 0; + } + + AWS_FATAL_ASSERT(!aws_linked_list_empty(&client_operational_state->queued_operations)); + + struct aws_linked_list_node *next_operation_node = + aws_linked_list_front(&client_operational_state->queued_operations); + struct aws_mqtt5_operation *next_operation = + AWS_CONTAINER_OF(next_operation_node, struct aws_mqtt5_operation, node); + + AWS_FATAL_ASSERT(next_operation != NULL); + + /* + * Check the head of the pending operation queue against flow control and client state restrictions + */ + return s_aws_mqtt5_client_compute_next_operation_flow_control_service_time( + client_operational_state->client, next_operation, now); +} + +static bool s_aws_mqtt5_client_should_service_operational_state( + const struct aws_mqtt5_client_operational_state *client_operational_state, + uint64_t now) { + + return now == s_aws_mqtt5_client_compute_operational_state_service_time(client_operational_state, now); +} + +static bool s_operation_requires_ack(const struct aws_mqtt5_operation *operation) { + switch (operation->packet_type) { + case AWS_MQTT5_PT_SUBSCRIBE: + case AWS_MQTT5_PT_UNSUBSCRIBE: + return true; + + case AWS_MQTT5_PT_PUBLISH: { + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + return publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE; + } + + default: + return false; + } +} + +static void s_on_pingreq_send(struct aws_mqtt5_client *client) { + uint64_t now = client->vtable->get_current_time_fn(); + uint64_t ping_timeout_nanos = + aws_timestamp_convert(client->config->ping_timeout_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + client->next_ping_timeout_time = aws_add_u64_saturating(now, ping_timeout_nanos); +} + +static int s_apply_throughput_flow_control(struct aws_mqtt5_client *client) { + /* flow control only applies during CONNECTED/CLEAN_DISCONNECT */ + if (!aws_mqtt5_client_are_negotiated_settings_valid(client)) { + return AWS_OP_SUCCESS; + } + + if (client->config->extended_validation_and_flow_control_options == AWS_MQTT5_EVAFCO_NONE) { + return AWS_OP_SUCCESS; + } + + return aws_rate_limiter_token_bucket_take_tokens( + (struct aws_rate_limiter_token_bucket *)&client->flow_control_state.throughput_throttle, + THROUGHPUT_TOKENS_PER_IO_MESSAGE_OVERESTIMATE); +} + +static int s_apply_publish_tps_flow_control(struct aws_mqtt5_client *client, struct aws_mqtt5_operation *operation) { + if (client->config->extended_validation_and_flow_control_options == AWS_MQTT5_EVAFCO_NONE) { + return AWS_OP_SUCCESS; + } + + if (operation->packet_type != AWS_MQTT5_PT_PUBLISH) { + return AWS_OP_SUCCESS; + } + + return aws_rate_limiter_token_bucket_take_tokens( + (struct aws_rate_limiter_token_bucket *)&client->flow_control_state.publish_throttle, 1); +} + +int aws_mqtt5_client_service_operational_state(struct aws_mqtt5_client_operational_state *client_operational_state) { + struct aws_mqtt5_client *client = client_operational_state->client; + struct aws_channel_slot *slot = client->slot; + const struct aws_mqtt5_client_vtable *vtable = client->vtable; + uint64_t now = (*vtable->get_current_time_fn)(); + + /* Should we write data? */ + bool should_service = s_aws_mqtt5_client_should_service_operational_state(client_operational_state, now); + if (!should_service) { + return AWS_OP_SUCCESS; + } + + if (s_apply_throughput_flow_control(client)) { + return AWS_OP_SUCCESS; + } + + /* If we're going to write data, we need something to write to */ + struct aws_io_message *io_message = (*vtable->aws_channel_acquire_message_from_pool_fn)( + slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, AWS_MQTT5_IO_MESSAGE_DEFAULT_LENGTH, vtable->vtable_user_data); + if (io_message == NULL) { + return AWS_OP_ERR; + } + + int operational_error_code = AWS_ERROR_SUCCESS; + + do { + /* if no current operation, pull one in and setup encode */ + if (client_operational_state->current_operation == NULL) { + + /* + * Loop through queued operations, discarding ones that fail validation, until we run out or find + * a good one. Failing validation against negotiated settings is expected to be a rare event. + */ + struct aws_mqtt5_operation *next_operation = NULL; + while (!aws_linked_list_empty(&client_operational_state->queued_operations)) { + struct aws_linked_list_node *next_operation_node = + aws_linked_list_pop_front(&client_operational_state->queued_operations); + struct aws_mqtt5_operation *operation = + AWS_CONTAINER_OF(next_operation_node, struct aws_mqtt5_operation, node); + + if (s_apply_publish_tps_flow_control(client, operation)) { + break; + } + + if (!aws_mqtt5_operation_validate_vs_connection_settings(operation, client)) { + next_operation = operation; + break; + } + + enum aws_mqtt5_packet_type packet_type = operation->packet_type; + int validation_error_code = aws_last_error(); + s_complete_operation(client, operation, validation_error_code, AWS_MQTT5_PT_NONE, NULL); + + /* A DISCONNECT packet failing dynamic validation should shut down the whole channel */ + if (packet_type == AWS_MQTT5_PT_DISCONNECT) { + operational_error_code = AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE; + break; + } + } + + if (next_operation != NULL && s_aws_mqtt5_client_set_current_operation(client, next_operation)) { + operational_error_code = AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE; + break; + } + } + + struct aws_mqtt5_operation *current_operation = client_operational_state->current_operation; + if (current_operation == NULL) { + break; + } + + /* write current operation to message, handle errors */ + enum aws_mqtt5_encoding_result encoding_result = + aws_mqtt5_encoder_encode_to_buffer(&client->encoder, &io_message->message_data); + if (encoding_result == AWS_MQTT5_ER_ERROR) { + operational_error_code = AWS_ERROR_MQTT5_ENCODE_FAILURE; + break; + } + + /* if encoding finished: + * push to write completion or unacked + * clear current + * else (message full) + * break + */ + if (encoding_result == AWS_MQTT5_ER_FINISHED) { + aws_mqtt5_client_flow_control_state_on_outbound_operation(client, current_operation); + + if (s_operation_requires_ack(current_operation)) { + /* track the operation in the unacked data structures by packet id */ + AWS_FATAL_ASSERT(aws_mqtt5_operation_get_packet_id(current_operation) != 0); + + if (aws_hash_table_put( + &client_operational_state->unacked_operations_table, + aws_mqtt5_operation_get_packet_id_address(current_operation), + current_operation, + NULL)) { + operational_error_code = aws_last_error(); + break; + } + + if (client->config->ack_timeout_seconds != 0) { + current_operation->ack_timeout_timepoint_ns = + now + aws_timestamp_convert( + client->config->ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + } + + aws_linked_list_push_back(&client_operational_state->unacked_operations, ¤t_operation->node); + aws_mqtt5_client_statistics_change_operation_statistic_state( + client, current_operation, AWS_MQTT5_OSS_INCOMPLETE | AWS_MQTT5_OSS_UNACKED); + } else { + /* no ack is necessary, just add to socket write completion list */ + aws_linked_list_push_back( + &client_operational_state->write_completion_operations, ¤t_operation->node); + + /* + * We special-case setting the ping timeout here. Other possible places are not appropriate: + * + * (1) Socket write completion - this leads to a race condition where our domain socket tests can + * sporadically fail because the PINGRESP is processed before the write completion callback is + * invoked. + * + * (2) Enqueue the ping - if the current operation is a large payload over a poor connection, it may + * be an arbitrarily long time before the current operation completes and the ping even has a chance + * to go out, meaning we will trigger a ping time out before it's even sent. + * + * Given a reasonable io message size, this is the best place to set the timeout. + */ + if (current_operation->packet_type == AWS_MQTT5_PT_PINGREQ) { + s_on_pingreq_send(client); + } + } + + client->operational_state.current_operation = NULL; + } else { + AWS_FATAL_ASSERT(encoding_result == AWS_MQTT5_ER_OUT_OF_ROOM); + break; + } + + now = (*vtable->get_current_time_fn)(); + should_service = s_aws_mqtt5_client_should_service_operational_state(client_operational_state, now); + } while (should_service); + + if (operational_error_code != AWS_ERROR_SUCCESS) { + aws_mem_release(io_message->allocator, io_message); + return aws_raise_error(operational_error_code); + } + + /* It's possible for there to be no data if we serviced operations that failed validation */ + if (io_message->message_data.len == 0) { + aws_mem_release(io_message->allocator, io_message); + return AWS_OP_SUCCESS; + } + + /* send io_message down channel in write direction, handle errors */ + io_message->on_completion = s_aws_mqtt5_on_socket_write_completion; + io_message->user_data = client_operational_state->client; + client_operational_state->pending_write_completion = true; + + if ((*vtable->aws_channel_slot_send_message_fn)( + slot, io_message, AWS_CHANNEL_DIR_WRITE, vtable->vtable_user_data)) { + client_operational_state->pending_write_completion = false; + aws_mem_release(io_message->allocator, io_message); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_client_operational_state_handle_ack( + struct aws_mqtt5_client_operational_state *client_operational_state, + aws_mqtt5_packet_id_t packet_id, + enum aws_mqtt5_packet_type packet_type, + const void *packet_view, + int error_code) { + + if (packet_type == AWS_MQTT5_PT_PUBACK) { + aws_mqtt5_client_flow_control_state_on_puback(client_operational_state->client); + } + + struct aws_hash_element *elem = NULL; + aws_hash_table_find(&client_operational_state->unacked_operations_table, &packet_id, &elem); + + if (elem == NULL || elem->value == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: received an ACK for an unknown operation with id %d", + (void *)client_operational_state->client, + (int)packet_id); + return; + } else { + AWS_LOGF_TRACE( + AWS_LS_MQTT5_CLIENT, + "id=%p: Processing ACK with id %d", + (void *)client_operational_state->client, + (int)packet_id); + } + + struct aws_mqtt5_operation *operation = elem->value; + + aws_linked_list_remove(&operation->node); + aws_hash_table_remove(&client_operational_state->unacked_operations_table, &packet_id, NULL, NULL); + + s_complete_operation(client_operational_state->client, operation, error_code, packet_type, packet_view); +} + +bool aws_mqtt5_client_are_negotiated_settings_valid(const struct aws_mqtt5_client *client) { + return client->current_state == AWS_MCS_CONNECTED || client->current_state == AWS_MCS_CLEAN_DISCONNECT; +} + +void aws_mqtt5_client_flow_control_state_init(struct aws_mqtt5_client *client) { + struct aws_mqtt5_client_flow_control_state *flow_control = &client->flow_control_state; + + struct aws_rate_limiter_token_bucket_options publish_throttle_config = { + .tokens_per_second = AWS_IOT_CORE_PUBLISH_PER_SECOND_LIMIT, + .maximum_token_count = AWS_IOT_CORE_PUBLISH_PER_SECOND_LIMIT, + .initial_token_count = 0, + }; + aws_rate_limiter_token_bucket_init(&flow_control->publish_throttle, &publish_throttle_config); + + struct aws_rate_limiter_token_bucket_options throughput_throttle_config = { + .tokens_per_second = AWS_IOT_CORE_THROUGHPUT_LIMIT, + .maximum_token_count = AWS_IOT_CORE_THROUGHPUT_LIMIT, + .initial_token_count = 0, + }; + aws_rate_limiter_token_bucket_init(&flow_control->throughput_throttle, &throughput_throttle_config); +} + +void aws_mqtt5_client_flow_control_state_reset(struct aws_mqtt5_client *client) { + struct aws_mqtt5_client_flow_control_state *flow_control = &client->flow_control_state; + + AWS_FATAL_ASSERT(aws_mqtt5_client_are_negotiated_settings_valid(client)); + + flow_control->unacked_publish_token_count = client->negotiated_settings.receive_maximum_from_server; + + aws_rate_limiter_token_bucket_reset(&client->flow_control_state.publish_throttle); + aws_rate_limiter_token_bucket_reset(&client->flow_control_state.throughput_throttle); +} + +void aws_mqtt5_client_flow_control_state_on_puback(struct aws_mqtt5_client *client) { + struct aws_mqtt5_client_flow_control_state *flow_control = &client->flow_control_state; + + bool was_zero = flow_control->unacked_publish_token_count == 0; + flow_control->unacked_publish_token_count = aws_min_u32( + client->negotiated_settings.receive_maximum_from_server, flow_control->unacked_publish_token_count + 1); + + if (was_zero) { + s_reevaluate_service_task(client); + } +} + +void aws_mqtt5_client_flow_control_state_on_outbound_operation( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation) { + if (operation->packet_type != AWS_MQTT5_PT_PUBLISH) { + return; + } + + const struct aws_mqtt5_packet_publish_view *publish_view = operation->packet_view; + if (publish_view->qos == AWS_MQTT5_QOS_AT_MOST_ONCE) { + return; + } + + struct aws_mqtt5_client_flow_control_state *flow_control = &client->flow_control_state; + + AWS_FATAL_ASSERT(flow_control->unacked_publish_token_count > 0); + --flow_control->unacked_publish_token_count; +} + +uint64_t aws_mqtt5_client_flow_control_state_get_next_operation_service_time( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *next_operation, + uint64_t now) { + + if (next_operation->packet_type != AWS_MQTT5_PT_PUBLISH) { + return now; + } + + /* publish tps check */ + if (client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + uint64_t publish_wait = + aws_rate_limiter_token_bucket_compute_wait_for_tokens(&client->flow_control_state.publish_throttle, 1); + if (publish_wait > 0) { + return now + publish_wait; + } + } + + /* receive maximum check */ + const struct aws_mqtt5_packet_publish_view *publish_view = next_operation->packet_view; + if (publish_view->qos == AWS_MQTT5_QOS_AT_MOST_ONCE) { + return now; + } + + if (client->flow_control_state.unacked_publish_token_count > 0) { + return now; + } + + return 0; +} + +void aws_mqtt5_client_statistics_change_operation_statistic_state( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + enum aws_mqtt5_operation_statistic_state_flags new_state_flags) { + enum aws_mqtt5_packet_type packet_type = operation->packet_type; + if (packet_type != AWS_MQTT5_PT_PUBLISH && packet_type != AWS_MQTT5_PT_SUBSCRIBE && + packet_type != AWS_MQTT5_PT_UNSUBSCRIBE) { + return; + } + + if (operation->packet_size == 0) { + if (aws_mqtt5_packet_view_get_encoded_size(packet_type, operation->packet_view, &operation->packet_size)) { + return; + } + } + + AWS_FATAL_ASSERT(operation->packet_size > 0); + uint64_t packet_size = (uint64_t)operation->packet_size; + + enum aws_mqtt5_operation_statistic_state_flags old_state_flags = operation->statistic_state_flags; + if (new_state_flags == old_state_flags) { + return; + } + + aws_mutex_lock(&client->operation_statistics_lock); + struct aws_mqtt5_client_operation_statistics *stats = &client->operation_statistics; + + if ((old_state_flags & AWS_MQTT5_OSS_INCOMPLETE) != (new_state_flags & AWS_MQTT5_OSS_INCOMPLETE)) { + if ((new_state_flags & AWS_MQTT5_OSS_INCOMPLETE) != 0) { + ++stats->incomplete_operation_count; + stats->incomplete_operation_size += packet_size; + } else { + AWS_FATAL_ASSERT(stats->incomplete_operation_count > 0 && stats->incomplete_operation_size >= packet_size); + + --stats->incomplete_operation_count; + stats->incomplete_operation_size -= packet_size; + } + } + + if ((old_state_flags & AWS_MQTT5_OSS_UNACKED) != (new_state_flags & AWS_MQTT5_OSS_UNACKED)) { + if ((new_state_flags & AWS_MQTT5_OSS_UNACKED) != 0) { + ++stats->unacked_operation_count; + stats->unacked_operation_size += packet_size; + } else { + AWS_FATAL_ASSERT(stats->unacked_operation_count > 0 && stats->unacked_operation_size >= packet_size); + + --stats->unacked_operation_count; + stats->unacked_operation_size -= packet_size; + } + } + + aws_mutex_unlock(&client->operation_statistics_lock); + + operation->statistic_state_flags = new_state_flags; + + if (client->vtable != NULL && client->vtable->on_client_statistics_changed_callback_fn != NULL) { + (*client->vtable->on_client_statistics_changed_callback_fn)( + client, operation, client->vtable->vtable_user_data); + } +} + +void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt5_client_operation_statistics *stats) { + aws_mutex_lock(&client->operation_statistics_lock); + *stats = client->operation_statistics; + aws_mutex_unlock(&client->operation_statistics_lock); +} diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c new file mode 100644 index 00000000..c1840025 --- /dev/null +++ b/source/v5/mqtt5_decoder.c @@ -0,0 +1,1165 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +#define AWS_MQTT5_DECODER_BUFFER_START_SIZE 2048 +#define PUBLISH_PACKET_FIXED_HEADER_DUPLICATE_FLAG 8 +#define PUBLISH_PACKET_FIXED_HEADER_RETAIN_FLAG 1 +#define PUBLISH_PACKET_FIXED_HEADER_QOS_FLAG 3 + +static void s_reset_decoder_for_new_packet(struct aws_mqtt5_decoder *decoder) { + aws_byte_buf_reset(&decoder->scratch_space, false); + + decoder->packet_first_byte = 0; + decoder->remaining_length = 0; + AWS_ZERO_STRUCT(decoder->packet_cursor); +} + +static void s_enter_state(struct aws_mqtt5_decoder *decoder, enum aws_mqtt5_decoder_state state) { + decoder->state = state; + + if (state == AWS_MQTT5_DS_READ_PACKET_TYPE) { + s_reset_decoder_for_new_packet(decoder); + } else { + aws_byte_buf_reset(&decoder->scratch_space, false); + } +} + +static bool s_is_decodable_packet_type(struct aws_mqtt5_decoder *decoder, enum aws_mqtt5_packet_type type) { + return (uint32_t)type < AWS_ARRAY_SIZE(decoder->options.decoder_table->decoders_by_packet_type) && + decoder->options.decoder_table->decoders_by_packet_type[type] != NULL; +} + +/* + * Every mqtt packet has a first byte that, amongst other things, determines the packet type + */ +static int s_aws_mqtt5_decoder_read_packet_type_on_data( + struct aws_mqtt5_decoder *decoder, + struct aws_byte_cursor *data) { + + if (data->len == 0) { + return AWS_MQTT5_DRT_MORE_DATA; + } + + uint8_t byte = *data->ptr; + aws_byte_cursor_advance(data, 1); + aws_byte_buf_append_byte_dynamic(&decoder->scratch_space, byte); + + enum aws_mqtt5_packet_type packet_type = (byte >> 4); + + if (!s_is_decodable_packet_type(decoder, packet_type)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: unsupported or illegal packet type value: %d", + decoder->options.callback_user_data, + (int)packet_type); + return AWS_MQTT5_DRT_ERROR; + } + + decoder->packet_first_byte = byte; + + s_enter_state(decoder, AWS_MQTT5_DS_READ_REMAINING_LENGTH); + + return AWS_MQTT5_DRT_SUCCESS; +} + +/* + * non-streaming variable length integer decode. cursor is updated only if the value was successfully read + */ +enum aws_mqtt5_decode_result_type aws_mqtt5_decode_vli(struct aws_byte_cursor *cursor, uint32_t *dest) { + uint32_t value = 0; + bool more_data = false; + size_t bytes_used = 0; + + uint32_t shift = 0; + + struct aws_byte_cursor cursor_copy = *cursor; + for (; bytes_used < 4; ++bytes_used) { + uint8_t byte = 0; + if (!aws_byte_cursor_read_u8(&cursor_copy, &byte)) { + return AWS_MQTT5_DRT_MORE_DATA; + } + + value |= ((byte & 0x7F) << shift); + shift += 7; + + more_data = (byte & 0x80) != 0; + if (!more_data) { + break; + } + } + + if (more_data) { + /* A variable length integer with the 4th byte high bit set is not valid */ + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "(static) aws_mqtt5_decoder - illegal variable length integer encoding"); + return AWS_MQTT5_DRT_ERROR; + } + + aws_byte_cursor_advance(cursor, bytes_used + 1); + *dest = value; + + return AWS_MQTT5_DRT_SUCCESS; +} + +/* "streaming" variable length integer decode */ +static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_vli_on_data( + struct aws_mqtt5_decoder *decoder, + uint32_t *vli_dest, + struct aws_byte_cursor *data) { + + enum aws_mqtt5_decode_result_type decode_vli_result = AWS_MQTT5_DRT_MORE_DATA; + + /* try to decode the vli integer one byte at a time */ + while (data->len > 0 && decode_vli_result == AWS_MQTT5_DRT_MORE_DATA) { + /* append a single byte to the scratch buffer */ + struct aws_byte_cursor byte_cursor = aws_byte_cursor_advance(data, 1); + aws_byte_buf_append_dynamic(&decoder->scratch_space, &byte_cursor); + + /* now try and decode a vli integer based on the range implied by the offset into the buffer */ + struct aws_byte_cursor vli_cursor = { + .ptr = decoder->scratch_space.buffer, + .len = decoder->scratch_space.len, + }; + + decode_vli_result = aws_mqtt5_decode_vli(&vli_cursor, vli_dest); + } + + return decode_vli_result; +} + +/* attempts to read the variable length integer that is always the second piece of data in an mqtt packet */ +static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_remaining_length_on_data( + struct aws_mqtt5_decoder *decoder, + struct aws_byte_cursor *data) { + + enum aws_mqtt5_decode_result_type result = + s_aws_mqtt5_decoder_read_vli_on_data(decoder, &decoder->remaining_length, data); + if (result != AWS_MQTT5_DRT_SUCCESS) { + return result; + } + + s_enter_state(decoder, AWS_MQTT5_DS_READ_PACKET); + + return AWS_MQTT5_DRT_SUCCESS; +} + +/* non-streaming decode of a user property; failure implies connection termination */ +int aws_mqtt5_decode_user_property( + struct aws_byte_cursor *packet_cursor, + struct aws_mqtt5_user_property_set *properties) { + struct aws_mqtt5_user_property property; + + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(packet_cursor, &property.name, error); + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(packet_cursor, &property.value, error); + + if (aws_array_list_push_back(&properties->properties, &property)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; + +error: + + return AWS_OP_ERR; +} + +/* decode function for all CONNACK properties */ +static int s_read_connack_property( + struct aws_mqtt5_packet_connack_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_connack_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, &storage->session_expiry_interval, &storage_view->session_expiry_interval, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RECEIVE_MAXIMUM: + AWS_MQTT5_DECODE_U16_OPTIONAL( + packet_cursor, &storage->receive_maximum, &storage_view->receive_maximum, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_QOS: + AWS_MQTT5_DECODE_U8_OPTIONAL(packet_cursor, &storage->maximum_qos, &storage_view->maximum_qos, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RETAIN_AVAILABLE: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, &storage->retain_available, &storage_view->retain_available, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_PACKET_SIZE: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, &storage->maximum_packet_size, &storage_view->maximum_packet_size, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_ASSIGNED_CLIENT_IDENTIFIER: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->assigned_client_identifier, &storage_view->assigned_client_identifier, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS_MAXIMUM: + AWS_MQTT5_DECODE_U16_OPTIONAL( + packet_cursor, &storage->topic_alias_maximum, &storage_view->topic_alias_maximum, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_REASON_STRING: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->reason_string, &storage_view->reason_string, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_WILDCARD_SUBSCRIPTIONS_AVAILABLE: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, + &storage->wildcard_subscriptions_available, + &storage_view->wildcard_subscriptions_available, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIERS_AVAILABLE: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, + &storage->subscription_identifiers_available, + &storage_view->subscription_identifiers_available, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_SHARED_SUBSCRIPTIONS_AVAILABLE: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, + &storage->shared_subscriptions_available, + &storage_view->shared_subscriptions_available, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_SERVER_KEEP_ALIVE: + AWS_MQTT5_DECODE_U16_OPTIONAL( + packet_cursor, &storage->server_keep_alive, &storage_view->server_keep_alive, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RESPONSE_INFORMATION: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->response_information, &storage_view->response_information, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_SERVER_REFERENCE: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->server_reference, &storage_view->server_reference, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_METHOD: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->authentication_method, &storage_view->authentication_method, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_DATA: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->authentication_data, &storage_view->authentication_data, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes a CONNACK packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_connack(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_connack_storage storage; + if (aws_mqtt5_packet_connack_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + + int result = AWS_OP_ERR; + + uint8_t first_byte = decoder->packet_first_byte; + /* CONNACK flags must be zero by protocol */ + if ((first_byte & 0x0F) != 0) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + uint32_t remaining_length = decoder->remaining_length; + if (remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + uint8_t connect_flags = 0; + AWS_MQTT5_DECODE_U8(&packet_cursor, &connect_flags, done); + + /* everything but the 0-bit must be 0 */ + if ((connect_flags & 0xFE) != 0) { + goto done; + } + + struct aws_mqtt5_packet_connack_view *storage_view = &storage.storage_view; + + storage_view->session_present = (connect_flags & 0x01) != 0; + + uint8_t reason_code = 0; + AWS_MQTT5_DECODE_U8(&packet_cursor, &reason_code, done); + storage_view->reason_code = reason_code; + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length != (uint32_t)packet_cursor.len) { + goto done; + } + + while (packet_cursor.len > 0) { + if (s_read_connack_property(&storage, &packet_cursor)) { + goto done; + } + } + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_CONNACK, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: CONNACK decode failure", decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_connack_storage_clean_up(&storage); + + return result; +} + +/* decode function for all PUBLISH properties */ +static int s_read_publish_property( + struct aws_mqtt5_packet_publish_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_publish_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_PAYLOAD_FORMAT_INDICATOR: + AWS_MQTT5_DECODE_U8_OPTIONAL(packet_cursor, &storage->payload_format, &storage_view->payload_format, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_MESSAGE_EXPIRY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, + &storage->message_expiry_interval_seconds, + &storage_view->message_expiry_interval_seconds, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS: + AWS_MQTT5_DECODE_U16_OPTIONAL(packet_cursor, &storage->topic_alias, &storage_view->topic_alias, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RESPONSE_TOPIC: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->response_topic, &storage_view->response_topic, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_CORRELATION_DATA: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->correlation_data, &storage_view->correlation_data, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + case AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIER: { + uint32_t subscription_identifier = 0; + AWS_MQTT5_DECODE_VLI(packet_cursor, &subscription_identifier, done); + aws_array_list_push_back(&storage->subscription_identifiers, &subscription_identifier); + break; + } + + case AWS_MQTT5_PROPERTY_TYPE_CONTENT_TYPE: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->content_type, &storage_view->content_type, done); + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes a PUBLISH packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_publish(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_publish_storage storage; + if (aws_mqtt5_packet_publish_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + + int result = AWS_OP_ERR; + struct aws_mqtt5_packet_publish_view *storage_view = &storage.storage_view; + + /* + * Fixed Header + * byte 1: + * bits 4-7: MQTT Control Packet Type + * bit 3: DUP flag + * bit 1-2: QoS level + * bit 0: RETAIN + * byte 2-x: Remaining Length as Variable Byte Integer (1-4 bytes) + */ + + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & PUBLISH_PACKET_FIXED_HEADER_DUPLICATE_FLAG) != 0) { + storage_view->duplicate = true; + } + if ((first_byte & PUBLISH_PACKET_FIXED_HEADER_RETAIN_FLAG) != 0) { + storage_view->retain = true; + } + storage_view->qos = (enum aws_mqtt5_qos)((first_byte >> 1) & PUBLISH_PACKET_FIXED_HEADER_QOS_FLAG); + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + uint32_t remaining_length = decoder->remaining_length; + if (remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + /* + * Topic Name + * Packet Identifier (only present for > QoS 0) + * Properties + * - Property Length + * - Properties + * Payload + */ + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &storage_view->topic, done); + + if (storage_view->qos > 0) { + AWS_MQTT5_DECODE_U16(&packet_cursor, &storage_view->packet_id, done); + } + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length > (uint32_t)packet_cursor.len) { + goto done; + } + struct aws_byte_cursor properties_cursor = aws_byte_cursor_advance(&packet_cursor, property_length); + + while (properties_cursor.len > 0) { + if (s_read_publish_property(&storage, &properties_cursor)) { + goto done; + } + } + + storage_view->subscription_identifier_count = aws_array_list_length(&storage.subscription_identifiers); + storage_view->subscription_identifiers = storage.subscription_identifiers.data; + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + storage_view->payload = packet_cursor; + + if (storage_view->topic_alias != NULL) { + if (decoder->topic_alias_resolver == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: PUBLISH packet contained topic alias when not allowed", + decoder->options.callback_user_data); + goto done; + } + + uint16_t topic_alias_id = *storage_view->topic_alias; + if (topic_alias_id == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: PUBLISH packet contained illegal topic alias", + decoder->options.callback_user_data); + goto done; + } + + if (storage_view->topic.len > 0) { + if (aws_mqtt5_inbound_topic_alias_resolver_register_alias( + decoder->topic_alias_resolver, topic_alias_id, storage_view->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, "id=%p: unable to register topic alias", decoder->options.callback_user_data); + goto done; + } + } else { + if (aws_mqtt5_inbound_topic_alias_resolver_resolve_alias( + decoder->topic_alias_resolver, topic_alias_id, &storage_view->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: PUBLISH packet contained unknown topic alias", + decoder->options.callback_user_data); + goto done; + } + } + } + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_PUBLISH, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: PUBLISH decode failure", decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_publish_storage_clean_up(&storage); + + return result; +} + +/* decode function for all PUBACK properties */ +static int s_read_puback_property( + struct aws_mqtt5_packet_puback_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_puback_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_REASON_STRING: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->reason_string, &storage_view->reason_string, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes a PUBACK packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_puback(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_puback_storage storage; + if (aws_mqtt5_packet_puback_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + int result = AWS_OP_ERR; + + uint8_t first_byte = decoder->packet_first_byte; + /* PUBACK flags must be zero by protocol */ + if ((first_byte & 0x0F) != 0) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + uint32_t remaining_length = decoder->remaining_length; + if (remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + struct aws_mqtt5_packet_puback_view *storage_view = &storage.storage_view; + + AWS_MQTT5_DECODE_U16(&packet_cursor, &storage_view->packet_id, done); + + /* Packet can end immediately following packet id with default success reason code */ + uint8_t reason_code = 0; + if (packet_cursor.len > 0) { + AWS_MQTT5_DECODE_U8(&packet_cursor, &reason_code, done); + + /* Packet can end immediately following reason code */ + if (packet_cursor.len > 0) { + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length != (uint32_t)packet_cursor.len) { + goto done; + } + while (packet_cursor.len > 0) { + if (s_read_puback_property(&storage, &packet_cursor)) { + goto done; + } + } + } + } + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + storage_view->reason_code = reason_code; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_PUBACK, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - PUBACK decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_puback_storage_clean_up(&storage); + + return result; +} + +/* decode function for all SUBACK properties */ +static int s_read_suback_property( + struct aws_mqtt5_packet_suback_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_suback_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_REASON_STRING: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->reason_string, &storage_view->reason_string, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes a SUBACK packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_suback(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_suback_storage storage; + if (aws_mqtt5_packet_suback_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + int result = AWS_OP_ERR; + + struct aws_mqtt5_packet_suback_view *storage_view = &storage.storage_view; + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + + AWS_MQTT5_DECODE_U16(&packet_cursor, &storage_view->packet_id, done); + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + struct aws_byte_cursor properties_cursor = aws_byte_cursor_advance(&packet_cursor, property_length); + while (properties_cursor.len > 0) { + if (s_read_suback_property(&storage, &properties_cursor)) { + goto done; + } + } + + aws_array_list_init_dynamic( + &storage.reason_codes, decoder->allocator, packet_cursor.len, sizeof(enum aws_mqtt5_suback_reason_code)); + + while (packet_cursor.len > 0) { + uint8_t reason_code; + AWS_MQTT5_DECODE_U8(&packet_cursor, &reason_code, done); + enum aws_mqtt5_suback_reason_code reason_code_enum = reason_code; + aws_array_list_push_back(&storage.reason_codes, &reason_code_enum); + } + + storage_view->reason_code_count = aws_array_list_length(&storage.reason_codes); + storage_view->reason_codes = storage.reason_codes.data; + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_SUBACK, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - SUBACK decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_suback_storage_clean_up(&storage); + + return result; +} + +/* decode function for all UNSUBACK properties */ +static int s_read_unsuback_property( + struct aws_mqtt5_packet_unsuback_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_unsuback_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_REASON_STRING: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->reason_string, &storage_view->reason_string, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes an UNSUBACK packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_unsuback(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_unsuback_storage storage; + + /* + * Fixed Header + * byte 1: MQTT5 Control Packet - Reserved 0 + * byte 2 - x: VLI Remaining Length + * + * Variable Header + * byte 1-2: Packet Identifier + * Byte 3 - x: VLI Property Length + * + * Properties + * byte 1: Idenfier + * bytes 2 - x: Property content + * + * Payload + * 1 byte per reason code in order of unsub requests + */ + + if (aws_mqtt5_packet_unsuback_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + int result = AWS_OP_ERR; + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + + struct aws_mqtt5_packet_unsuback_view *storage_view = &storage.storage_view; + + AWS_MQTT5_DECODE_U16(&packet_cursor, &storage_view->packet_id, done); + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + struct aws_byte_cursor properties_cursor = aws_byte_cursor_advance(&packet_cursor, property_length); + while (properties_cursor.len > 0) { + if (s_read_unsuback_property(&storage, &properties_cursor)) { + goto done; + } + } + + aws_array_list_init_dynamic( + &storage.reason_codes, decoder->allocator, packet_cursor.len, sizeof(enum aws_mqtt5_unsuback_reason_code)); + + while (packet_cursor.len > 0) { + uint8_t reason_code; + AWS_MQTT5_DECODE_U8(&packet_cursor, &reason_code, done); + enum aws_mqtt5_unsuback_reason_code reason_code_enum = reason_code; + aws_array_list_push_back(&storage.reason_codes, &reason_code_enum); + } + + storage_view->reason_code_count = aws_array_list_length(&storage.reason_codes); + storage_view->reason_codes = storage.reason_codes.data; + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_UNSUBACK, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - UNSUBACK decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_unsuback_storage_clean_up(&storage); + + return result; +} + +/* decode function for all DISCONNECT properties */ +static int s_read_disconnect_property( + struct aws_mqtt5_packet_disconnect_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_disconnect_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, + &storage->session_expiry_interval_seconds, + &storage_view->session_expiry_interval_seconds, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_SERVER_REFERENCE: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->server_reference, &storage_view->server_reference, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_REASON_STRING: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->reason_string, &storage_view->reason_string, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_ERR) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decodes a DISCONNECT packet whose data must be in the scratch buffer */ +static int s_aws_mqtt5_decoder_decode_disconnect(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_disconnect_storage storage; + if (aws_mqtt5_packet_disconnect_storage_init_from_external_storage(&storage, decoder->allocator)) { + return AWS_OP_ERR; + } + + int result = AWS_OP_ERR; + + uint8_t first_byte = decoder->packet_first_byte; + /* DISCONNECT flags must be zero by protocol */ + if ((first_byte & 0x0F) != 0) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + uint32_t remaining_length = decoder->remaining_length; + if (remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + struct aws_mqtt5_packet_disconnect_view *storage_view = &storage.storage_view; + if (remaining_length > 0) { + uint8_t reason_code = 0; + AWS_MQTT5_DECODE_U8(&packet_cursor, &reason_code, done); + storage_view->reason_code = reason_code; + if (packet_cursor.len == 0) { + result = AWS_OP_SUCCESS; + goto done; + } + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length != (uint32_t)packet_cursor.len) { + goto done; + } + + while (packet_cursor.len > 0) { + if (s_read_disconnect_property(&storage, &packet_cursor)) { + goto done; + } + } + } + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage.user_properties); + storage_view->user_properties = storage.user_properties.properties.data; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_DISCONNECT, &storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: DISCONNECT decode failure", decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_disconnect_storage_clean_up(&storage); + + return result; +} + +static int s_aws_mqtt5_decoder_decode_pingresp(struct aws_mqtt5_decoder *decoder) { + if (decoder->packet_cursor.len != 0) { + goto error; + } + + uint8_t expected_first_byte = aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PINGRESP, 0); + if (decoder->packet_first_byte != expected_first_byte || decoder->remaining_length != 0) { + goto error; + } + + int result = AWS_OP_SUCCESS; + if (decoder->options.on_packet_received != NULL) { + result = + (*decoder->options.on_packet_received)(AWS_MQTT5_PT_PINGRESP, NULL, decoder->options.callback_user_data); + } + + return result; + +error: + + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: PINGRESP decode failure", decoder->options.callback_user_data); + return aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); +} + +static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) { + enum aws_mqtt5_packet_type packet_type = (enum aws_mqtt5_packet_type)(decoder->packet_first_byte >> 4); + aws_mqtt5_decoding_fn *decoder_fn = decoder->options.decoder_table->decoders_by_packet_type[packet_type]; + if (decoder_fn == NULL) { + return aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return (*decoder_fn)(decoder); +} + +/* + * (Streaming) Given a packet type and a variable length integer specifying the packet length, this state either + * (1) decodes directly from the cursor if possible + * (2) reads the packet into the scratch buffer and then decodes it once it is completely present + * + */ +static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_packet_on_data( + struct aws_mqtt5_decoder *decoder, + struct aws_byte_cursor *data) { + + /* Are we able to decode directly from the channel message data buffer? */ + if (decoder->scratch_space.len == 0 && decoder->remaining_length <= data->len) { + /* The cursor contains the entire packet, so decode directly from the backing io message buffer */ + decoder->packet_cursor = aws_byte_cursor_advance(data, decoder->remaining_length); + } else { + /* If the packet is fragmented across multiple io messages, then we buffer it internally */ + size_t unread_length = decoder->remaining_length - decoder->scratch_space.len; + size_t copy_length = aws_min_size(unread_length, data->len); + + struct aws_byte_cursor copy_cursor = aws_byte_cursor_advance(data, copy_length); + if (aws_byte_buf_append_dynamic(&decoder->scratch_space, ©_cursor)) { + return AWS_MQTT5_DRT_ERROR; + } + + if (copy_length < unread_length) { + return AWS_MQTT5_DRT_MORE_DATA; + } + + decoder->packet_cursor = aws_byte_cursor_from_buf(&decoder->scratch_space); + } + + if (s_aws_mqtt5_decoder_decode_packet(decoder)) { + return AWS_MQTT5_DRT_ERROR; + } + + s_enter_state(decoder, AWS_MQTT5_DS_READ_PACKET_TYPE); + + return AWS_MQTT5_DRT_SUCCESS; +} + +/* top-level entry function for all new data received from the remote mqtt endpoint */ +int aws_mqtt5_decoder_on_data_received(struct aws_mqtt5_decoder *decoder, struct aws_byte_cursor data) { + enum aws_mqtt5_decode_result_type result = AWS_MQTT5_DRT_SUCCESS; + while (result == AWS_MQTT5_DRT_SUCCESS) { + switch (decoder->state) { + case AWS_MQTT5_DS_READ_PACKET_TYPE: + result = s_aws_mqtt5_decoder_read_packet_type_on_data(decoder, &data); + break; + + case AWS_MQTT5_DS_READ_REMAINING_LENGTH: + result = s_aws_mqtt5_decoder_read_remaining_length_on_data(decoder, &data); + break; + + case AWS_MQTT5_DS_READ_PACKET: + result = s_aws_mqtt5_decoder_read_packet_on_data(decoder, &data); + break; + + default: + result = AWS_MQTT5_DRT_ERROR; + break; + } + } + + if (result == AWS_MQTT5_DRT_ERROR) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + decoder->state = AWS_MQTT5_DS_FATAL_ERROR; + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_decoder_function_table s_aws_mqtt5_decoder_default_function_table = { + .decoders_by_packet_type = + { + NULL, /* RESERVED = 0 */ + NULL, /* CONNECT */ + &s_aws_mqtt5_decoder_decode_connack, /* CONNACK */ + &s_aws_mqtt5_decoder_decode_publish, /* PUBLISH */ + &s_aws_mqtt5_decoder_decode_puback, /* PUBACK */ + NULL, /* PUBREC */ + NULL, /* PUBREL */ + NULL, /* PUBCOMP */ + NULL, /* SUBSCRIBE */ + &s_aws_mqtt5_decoder_decode_suback, /* SUBACK */ + NULL, /* UNSUBSCRIBE */ + &s_aws_mqtt5_decoder_decode_unsuback, /* UNSUBACK */ + NULL, /* PINGREQ */ + &s_aws_mqtt5_decoder_decode_pingresp, /* PINGRESP */ + &s_aws_mqtt5_decoder_decode_disconnect, /* DISCONNECT */ + NULL /* AUTH */ + }, +}; + +const struct aws_mqtt5_decoder_function_table *g_aws_mqtt5_default_decoder_table = + &s_aws_mqtt5_decoder_default_function_table; + +int aws_mqtt5_decoder_init( + struct aws_mqtt5_decoder *decoder, + struct aws_allocator *allocator, + struct aws_mqtt5_decoder_options *options) { + AWS_ZERO_STRUCT(*decoder); + + decoder->options = *options; + + if (decoder->options.decoder_table == NULL) { + decoder->options.decoder_table = g_aws_mqtt5_default_decoder_table; + } + + decoder->allocator = allocator; + + decoder->state = AWS_MQTT5_DS_READ_PACKET_TYPE; + + if (aws_byte_buf_init(&decoder->scratch_space, allocator, AWS_MQTT5_DECODER_BUFFER_START_SIZE)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_decoder_reset(struct aws_mqtt5_decoder *decoder) { + s_reset_decoder_for_new_packet(decoder); +} + +void aws_mqtt5_decoder_clean_up(struct aws_mqtt5_decoder *decoder) { + aws_byte_buf_clean_up(&decoder->scratch_space); +} + +void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( + struct aws_mqtt5_decoder *decoder, + struct aws_mqtt5_inbound_topic_alias_resolver *resolver) { + decoder->topic_alias_resolver = resolver; +} diff --git a/source/v5/mqtt5_encoder.c b/source/v5/mqtt5_encoder.c new file mode 100644 index 00000000..b9a3ec56 --- /dev/null +++ b/source/v5/mqtt5_encoder.c @@ -0,0 +1,1283 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include + +#include + +#define INITIAL_ENCODING_STEP_COUNT 64 +#define SUBSCRIBE_PACKET_FIXED_HEADER_RESERVED_BITS 2 +#define UNSUBSCRIBE_PACKET_FIXED_HEADER_RESERVED_BITS 2 + +int aws_mqtt5_encode_variable_length_integer(struct aws_byte_buf *buf, uint32_t value) { + AWS_PRECONDITION(buf); + + if (value > AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + do { + uint8_t encoded_byte = value % 128; + value /= 128; + if (value) { + encoded_byte |= 128; + } + if (!aws_byte_buf_write_u8(buf, encoded_byte)) { + return aws_raise_error(AWS_ERROR_SHORT_BUFFER); + } + } while (value); + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_get_variable_length_encode_size(size_t value, size_t *encode_size) { + if (value > AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (value < 128) { + *encode_size = 1; + } else if (value < 16384) { + *encode_size = 2; + } else if (value < 2097152) { + *encode_size = 3; + } else { + *encode_size = 4; + } + + return AWS_OP_SUCCESS; +} + +/* helper functions that add a single type of encoding step to the list of steps in an encoder */ + +void aws_mqtt5_encoder_push_step_u8(struct aws_mqtt5_encoder *encoder, uint8_t value) { + struct aws_mqtt5_encoding_step step; + AWS_ZERO_STRUCT(step); + + step.type = AWS_MQTT5_EST_U8; + step.value.value_u8 = value; + + aws_array_list_push_back(&encoder->encoding_steps, &step); +} + +void aws_mqtt5_encoder_push_step_u16(struct aws_mqtt5_encoder *encoder, uint16_t value) { + struct aws_mqtt5_encoding_step step; + AWS_ZERO_STRUCT(step); + + step.type = AWS_MQTT5_EST_U16; + step.value.value_u16 = value; + + aws_array_list_push_back(&encoder->encoding_steps, &step); +} + +void aws_mqtt5_encoder_push_step_u32(struct aws_mqtt5_encoder *encoder, uint32_t value) { + struct aws_mqtt5_encoding_step step; + AWS_ZERO_STRUCT(step); + + step.type = AWS_MQTT5_EST_U32; + step.value.value_u32 = value; + + aws_array_list_push_back(&encoder->encoding_steps, &step); +} + +int aws_mqtt5_encoder_push_step_vli(struct aws_mqtt5_encoder *encoder, uint32_t value) { + if (value > AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER) { + return aws_raise_error(AWS_ERROR_MQTT5_ENCODE_FAILURE); + } + + struct aws_mqtt5_encoding_step step; + AWS_ZERO_STRUCT(step); + + step.type = AWS_MQTT5_EST_VLI; + step.value.value_u32 = value; + + aws_array_list_push_back(&encoder->encoding_steps, &step); + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_encoder_push_step_cursor(struct aws_mqtt5_encoder *encoder, struct aws_byte_cursor value) { + struct aws_mqtt5_encoding_step step; + AWS_ZERO_STRUCT(step); + + step.type = AWS_MQTT5_EST_CURSOR; + step.value.value_cursor = value; + + aws_array_list_push_back(&encoder->encoding_steps, &step); +} + +/* + * All size calculations are done with size_t. We assume that view validation will catch and fail all packets + * that violate length constraints either from the MQTT5 spec or additional constraints that we impose on packets + * to ensure that the size calculations do not need to perform checked arithmetic. The only place where we need + * to use checked arithmetic is a PUBLISH packet when combining the payload size and "sizeof everything else" + * + * The additional beyond-spec constraints we apply to view validation ensure our results actually fit in 32 bits. + */ +size_t aws_mqtt5_compute_user_property_encode_length( + const struct aws_mqtt5_user_property *properties, + size_t user_property_count) { + /* + * for each user property, in addition to the raw name-value bytes, we also have 5 bytes of prefix required: + * 1 byte for the property type + * 2 bytes for the name length + * 2 bytes for the value length + */ + size_t length = 5 * user_property_count; + + for (size_t i = 0; i < user_property_count; ++i) { + const struct aws_mqtt5_user_property *property = &properties[i]; + + length += property->name.len; + length += property->value.len; + } + + return length; +} + +void aws_mqtt5_add_user_property_encoding_steps( + struct aws_mqtt5_encoder *encoder, + const struct aws_mqtt5_user_property *user_properties, + size_t user_property_count) { + for (size_t i = 0; i < user_property_count; ++i) { + const struct aws_mqtt5_user_property *property = &user_properties[i]; + + /* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901054 */ + ADD_ENCODE_STEP_U8(encoder, AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY); + ADD_ENCODE_STEP_U16(encoder, (uint16_t)property->name.len); + ADD_ENCODE_STEP_CURSOR(encoder, property->name); + ADD_ENCODE_STEP_U16(encoder, (uint16_t)property->value.len); + ADD_ENCODE_STEP_CURSOR(encoder, property->value); + } +} + +static int s_aws_mqtt5_encoder_begin_pingreq(struct aws_mqtt5_encoder *encoder, const void *view) { + (void)view; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: setting up encode for a PINGREQ packet", (void *)encoder->config.client); + + /* A ping is just a fixed header with a 0-valued remaining length which we encode as a 0 u8 rather than a 0 vli */ + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PINGREQ, 0)); + ADD_ENCODE_STEP_U8(encoder, 0); + + return AWS_OP_SUCCESS; +} + +static int s_compute_disconnect_variable_length_fields( + const struct aws_mqtt5_packet_disconnect_view *disconnect_view, + size_t *total_remaining_length, + size_t *property_length) { + size_t local_property_length = aws_mqtt5_compute_user_property_encode_length( + disconnect_view->user_properties, disconnect_view->user_property_count); + + ADD_OPTIONAL_U32_PROPERTY_LENGTH(disconnect_view->session_expiry_interval_seconds, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(disconnect_view->server_reference, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(disconnect_view->reason_string, local_property_length); + + *property_length = local_property_length; + + size_t property_length_encoding_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &property_length_encoding_length)) { + return AWS_OP_ERR; + } + + /* reason code is the only other thing to worry about */ + *total_remaining_length = 1 + *property_length + property_length_encoding_length; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_encoder_begin_disconnect(struct aws_mqtt5_encoder *encoder, const void *view) { + + const struct aws_mqtt5_packet_disconnect_view *disconnect_view = view; + + size_t total_remaining_length = 0; + size_t property_length = 0; + if (s_compute_disconnect_variable_length_fields(disconnect_view, &total_remaining_length, &property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to compute variable length values for DISCONNECT packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t property_length_u32 = (uint32_t)property_length; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: setting up encode for a DISCONNECT packet with remaining length %" PRIu32, + (void *)encoder->config.client, + total_remaining_length_u32); + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_DISCONNECT, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + ADD_ENCODE_STEP_U8(encoder, (uint8_t)disconnect_view->reason_code); + ADD_ENCODE_STEP_VLI(encoder, property_length_u32); + + if (property_length > 0) { + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL, disconnect_view->session_expiry_interval_seconds); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, disconnect_view->reason_string); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SERVER_REFERENCE, disconnect_view->server_reference); + + aws_mqtt5_add_user_property_encoding_steps( + encoder, disconnect_view->user_properties, disconnect_view->user_property_count); + } + + return AWS_OP_SUCCESS; +} + +static int s_compute_connect_variable_length_fields( + const struct aws_mqtt5_packet_connect_view *connect_view, + size_t *total_remaining_length, + size_t *connect_property_length, + size_t *will_property_length) { + + size_t connect_property_section_length = + aws_mqtt5_compute_user_property_encode_length(connect_view->user_properties, connect_view->user_property_count); + + ADD_OPTIONAL_U32_PROPERTY_LENGTH(connect_view->session_expiry_interval_seconds, connect_property_section_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(connect_view->receive_maximum, connect_property_section_length); + ADD_OPTIONAL_U32_PROPERTY_LENGTH(connect_view->maximum_packet_size_bytes, connect_property_section_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(connect_view->topic_alias_maximum, connect_property_section_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connect_view->request_response_information, connect_property_section_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connect_view->request_problem_information, connect_property_section_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connect_view->authentication_method, connect_property_section_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connect_view->authentication_data, connect_property_section_length); + + *connect_property_length = (uint32_t)connect_property_section_length; + + /* variable header length = + * 10 bytes (6 for mqtt string, 1 for protocol version, 1 for flags, 2 for keep alive) + * + # bytes(variable_length_encoding(connect_property_section_length)) + * + connect_property_section_length + */ + size_t variable_header_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(connect_property_section_length, &variable_header_length)) { + return AWS_OP_ERR; + } + + variable_header_length += 10 + connect_property_section_length; + + size_t payload_length = 2 + connect_view->client_id.len; + + *will_property_length = 0; + if (connect_view->will != NULL) { + const struct aws_mqtt5_packet_publish_view *publish_view = connect_view->will; + + *will_property_length = aws_mqtt5_compute_user_property_encode_length( + publish_view->user_properties, publish_view->user_property_count); + + ADD_OPTIONAL_U32_PROPERTY_LENGTH(connect_view->will_delay_interval_seconds, *will_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(publish_view->payload_format, *will_property_length); + ADD_OPTIONAL_U32_PROPERTY_LENGTH(publish_view->message_expiry_interval_seconds, *will_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->content_type, *will_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->response_topic, *will_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->correlation_data, *will_property_length); + + size_t will_properties_length_encode_size = 0; + if (aws_mqtt5_get_variable_length_encode_size( + (uint32_t)*will_property_length, &will_properties_length_encode_size)) { + return AWS_OP_ERR; + } + + payload_length += *will_property_length; + payload_length += will_properties_length_encode_size; + + payload_length += 2 + publish_view->topic.len; + payload_length += 2 + publish_view->payload.len; + } + + /* Can't use the optional property macros because these don't have a leading property type byte */ + if (connect_view->username != NULL) { + payload_length += connect_view->username->len + 2; + } + + if (connect_view->password != NULL) { + payload_length += connect_view->password->len + 2; + } + + *total_remaining_length = payload_length + variable_header_length; + + return AWS_OP_SUCCESS; +} + +static uint8_t s_aws_mqtt5_connect_compute_connect_flags(const struct aws_mqtt5_packet_connect_view *connect_view) { + uint8_t flags = 0; + + if (connect_view->clean_start) { + flags |= 1 << 1; + } + + const struct aws_mqtt5_packet_publish_view *will = connect_view->will; + if (will != NULL) { + flags |= 1 << 2; + flags |= ((uint8_t)will->qos) << 3; + + if (will->retain) { + flags |= 1 << 5; + } + } + + if (connect_view->password != NULL) { + flags |= 1 << 6; + } + + if (connect_view->username != NULL) { + flags |= 1 << 7; + } + + return flags; +} + +static int s_aws_mqtt5_encoder_begin_connect(struct aws_mqtt5_encoder *encoder, const void *view) { + + const struct aws_mqtt5_packet_connect_view *connect_view = view; + const struct aws_mqtt5_packet_publish_view *will = connect_view->will; + + size_t total_remaining_length = 0; + size_t connect_property_length = 0; + size_t will_property_length = 0; + if (s_compute_connect_variable_length_fields( + connect_view, &total_remaining_length, &connect_property_length, &will_property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to compute variable length values for CONNECT packet with error %d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: setting up encode for a CONNECT packet with remaining length %zu", + (void *)encoder->config.client, + total_remaining_length); + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t connect_property_length_u32 = (uint32_t)connect_property_length; + uint32_t will_property_length_u32 = (uint32_t)will_property_length; + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_CONNECT, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + ADD_ENCODE_STEP_CURSOR(encoder, g_aws_mqtt5_connect_protocol_cursor); + ADD_ENCODE_STEP_U8(encoder, s_aws_mqtt5_connect_compute_connect_flags(connect_view)); + ADD_ENCODE_STEP_U16(encoder, connect_view->keep_alive_interval_seconds); + + ADD_ENCODE_STEP_VLI(encoder, connect_property_length_u32); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL, connect_view->session_expiry_interval_seconds); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_RECEIVE_MAXIMUM, connect_view->receive_maximum); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_PACKET_SIZE, connect_view->maximum_packet_size_bytes); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS_MAXIMUM, connect_view->topic_alias_maximum); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REQUEST_RESPONSE_INFORMATION, connect_view->request_response_information); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REQUEST_PROBLEM_INFORMATION, connect_view->request_problem_information); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_METHOD, connect_view->authentication_method); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_DATA, connect_view->authentication_data); + + aws_mqtt5_add_user_property_encoding_steps( + encoder, connect_view->user_properties, connect_view->user_property_count); + + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, connect_view->client_id); + + if (will != NULL) { + ADD_ENCODE_STEP_VLI(encoder, will_property_length_u32); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_WILL_DELAY_INTERVAL, connect_view->will_delay_interval_seconds); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_PAYLOAD_FORMAT_INDICATOR, will->payload_format); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_MESSAGE_EXPIRY_INTERVAL, will->message_expiry_interval_seconds); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY(encoder, AWS_MQTT5_PROPERTY_TYPE_CONTENT_TYPE, will->content_type); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY(encoder, AWS_MQTT5_PROPERTY_TYPE_RESPONSE_TOPIC, will->response_topic); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_CORRELATION_DATA, will->correlation_data); + + aws_mqtt5_add_user_property_encoding_steps(encoder, will->user_properties, will->user_property_count); + + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, will->topic); + ADD_ENCODE_STEP_U16(encoder, (uint16_t)will->payload.len); + ADD_ENCODE_STEP_CURSOR(encoder, will->payload); + } + + ADD_ENCODE_STEP_OPTIONAL_LENGTH_PREFIXED_CURSOR(encoder, connect_view->username); + ADD_ENCODE_STEP_OPTIONAL_LENGTH_PREFIXED_CURSOR(encoder, connect_view->password); + + return AWS_OP_SUCCESS; +} + +static uint8_t s_aws_mqtt5_subscribe_compute_subscription_flags( + const struct aws_mqtt5_subscription_view *subscription_view) { + + uint8_t flags = (uint8_t)subscription_view->qos; + + if (subscription_view->no_local) { + flags |= 1 << 2; + } + + if (subscription_view->retain_as_published) { + flags |= 1 << 3; + } + + flags |= ((uint8_t)subscription_view->retain_handling_type) << 4; + + return flags; +} + +static void aws_mqtt5_add_subscribe_topic_filter_encoding_steps( + struct aws_mqtt5_encoder *encoder, + const struct aws_mqtt5_subscription_view *subscriptions, + size_t subscription_count) { + /* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901169 */ + for (size_t i = 0; i < subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *subscription = &subscriptions[i]; + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, subscription->topic_filter); + ADD_ENCODE_STEP_U8(encoder, s_aws_mqtt5_subscribe_compute_subscription_flags(subscription)); + } +} + +static void aws_mqtt5_add_unsubscribe_topic_filter_encoding_steps( + struct aws_mqtt5_encoder *encoder, + const struct aws_byte_cursor *topics, + size_t unsubscription_count) { + /* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901185 */ + for (size_t i = 0; i < unsubscription_count; ++i) { + const struct aws_byte_cursor topic_filter = topics[i]; + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, topic_filter); + } +} + +static int s_compute_subscribe_variable_length_fields( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view, + size_t *total_remaining_length, + size_t *subscribe_properties_length) { + + size_t subscribe_variable_header_property_length = aws_mqtt5_compute_user_property_encode_length( + subscribe_view->user_properties, subscribe_view->user_property_count); + + /* + * Add the length of 1 byte for the identifier of a Subscription Identifier property + * and the VLI of the subscription_identifier itself + */ + if (subscribe_view->subscription_identifier != 0) { + size_t subscription_identifier_length = 0; + aws_mqtt5_get_variable_length_encode_size( + *subscribe_view->subscription_identifier, &subscription_identifier_length); + subscribe_variable_header_property_length += subscription_identifier_length + 1; + } + + *subscribe_properties_length = subscribe_variable_header_property_length; + + /* variable header total length = + * 2 bytes for Packet Identifier + * + # bytes (variable_length_encoding(subscribe_variable_header_property_length)) + * + subscribe_variable_header_property_length + */ + size_t variable_header_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(subscribe_variable_header_property_length, &variable_header_length)) { + return AWS_OP_ERR; + } + variable_header_length += 2 + subscribe_variable_header_property_length; + + size_t payload_length = 0; + + /* + * for each subscription view, in addition to the raw name-value bytes, we also have 2 bytes of + * prefix and one byte suffix required. + * 2 bytes for the Topic Filter length + * 1 byte for the Subscription Options Flags + */ + + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *subscription = &subscribe_view->subscriptions[i]; + payload_length += subscription->topic_filter.len; + } + payload_length += (3 * subscribe_view->subscription_count); + + *total_remaining_length = variable_header_length + payload_length; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_encoder_begin_subscribe(struct aws_mqtt5_encoder *encoder, const void *view) { + + const struct aws_mqtt5_packet_subscribe_view *subscription_view = view; + + size_t total_remaining_length = 0; + size_t subscribe_properties_length = 0; + + if (s_compute_subscribe_variable_length_fields( + subscription_view, &total_remaining_length, &subscribe_properties_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for SUBSCRIBE packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a SUBSCRIBE packet with remaining length %zu", + (void *)encoder->config.client, + total_remaining_length); + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t subscribe_property_length_u32 = (uint32_t)subscribe_properties_length; + + /* + * Fixed Header + * byte 1: + * bits 7-4 MQTT Control Packet Type + * bits 3-0 Reserved, must be set to 0, 0, 1, 0 + * byte 2-x: Remaining Length as Variable Byte Integer (1-4 bytes) + */ + ADD_ENCODE_STEP_U8( + encoder, + aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_SUBSCRIBE, SUBSCRIBE_PACKET_FIXED_HEADER_RESERVED_BITS)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + + /* + * Variable Header + * byte 1-2: Packet Identifier + * byte 3-x: Property Length as Variable Byte Integer (1-4 bytes) + */ + ADD_ENCODE_STEP_U16(encoder, (uint16_t)subscription_view->packet_id); + ADD_ENCODE_STEP_VLI(encoder, subscribe_property_length_u32); + + /* + * Subscribe Properties + * (optional) Subscription Identifier + * (optional) User Properties + */ + if (subscription_view->subscription_identifier != 0) { + ADD_ENCODE_STEP_U8(encoder, AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIER); + ADD_ENCODE_STEP_VLI(encoder, *subscription_view->subscription_identifier); + } + + aws_mqtt5_add_user_property_encoding_steps( + encoder, subscription_view->user_properties, subscription_view->user_property_count); + + /* + * Payload + * n Topic Filters + * byte 1-2: Length + * byte 3..N: UTF-8 encoded Topic Filter + * byte N+1: + * bits 7-6 Reserved + * bits 5-4 Retain Handling + * bit 3 Retain as Published + * bit 2 No Local + * bits 1-0 Maximum QoS + */ + aws_mqtt5_add_subscribe_topic_filter_encoding_steps( + encoder, subscription_view->subscriptions, subscription_view->subscription_count); + + return AWS_OP_SUCCESS; +} + +static int s_compute_unsubscribe_variable_length_fields( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view, + size_t *total_remaining_length, + size_t *unsubscribe_properties_length) { + + size_t unsubscribe_variable_header_property_length = aws_mqtt5_compute_user_property_encode_length( + unsubscribe_view->user_properties, unsubscribe_view->user_property_count); + + *unsubscribe_properties_length = unsubscribe_variable_header_property_length; + + /* variable header total length = + * 2 bytes for Packet Identifier + * + # bytes (variable_length_encoding(subscribe_variable_header_property_length)) + * + unsubscribe_variable_header_property_length + */ + size_t variable_header_length = 0; + if (aws_mqtt5_get_variable_length_encode_size( + unsubscribe_variable_header_property_length, &variable_header_length)) { + return AWS_OP_ERR; + } + variable_header_length += 2 + unsubscribe_variable_header_property_length; + + size_t payload_length = 0; + + /* + * for each unsubscribe topic filter + * 2 bytes for the Topic Filter length + * n bytes for Topic Filter + */ + + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + const struct aws_byte_cursor topic_filter = unsubscribe_view->topic_filters[i]; + payload_length += topic_filter.len; + } + + payload_length += (2 * unsubscribe_view->topic_filter_count); + + *total_remaining_length = variable_header_length + payload_length; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_encoder_begin_unsubscribe(struct aws_mqtt5_encoder *encoder, const void *view) { + + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = view; + + size_t total_remaining_length = 0; + size_t unsubscribe_properties_length = 0; + + if (s_compute_unsubscribe_variable_length_fields( + unsubscribe_view, &total_remaining_length, &unsubscribe_properties_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for UNSUBSCRIBE packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a UNSUBSCRIBE packet with remaining length %zu", + (void *)encoder->config.client, + total_remaining_length); + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t unsubscribe_property_length_u32 = (uint32_t)unsubscribe_properties_length; + + /* + * Fixed Header + * byte 1: + * bits 7-4 MQTT Control Packet type (10) + * bits 3-0 Reserved, must be set to 0, 0, 1, 0 + * byte 2-x: Remaining Length as Variable Byte Integer (1-4 bytes) + */ + ADD_ENCODE_STEP_U8( + encoder, + aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_UNSUBSCRIBE, UNSUBSCRIBE_PACKET_FIXED_HEADER_RESERVED_BITS)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + + /* + * Variable Header + * byte 1-2: Packet Identifier + * byte 3-x: Properties length as Variable Byte Integer (1-4 bytes) + */ + ADD_ENCODE_STEP_U16(encoder, (uint16_t)unsubscribe_view->packet_id); + ADD_ENCODE_STEP_VLI(encoder, unsubscribe_property_length_u32); + + /* + * (optional) User Properties + */ + aws_mqtt5_add_user_property_encoding_steps( + encoder, unsubscribe_view->user_properties, unsubscribe_view->user_property_count); + + /* + * Payload + * n Topic Filters + * byte 1-2: Length + * byte 3..N: UTF-8 encoded Topic Filter + */ + + aws_mqtt5_add_unsubscribe_topic_filter_encoding_steps( + encoder, unsubscribe_view->topic_filters, unsubscribe_view->topic_filter_count); + + return AWS_OP_SUCCESS; +} + +static int s_compute_publish_variable_length_fields( + const struct aws_mqtt5_packet_publish_view *publish_view, + size_t *total_remaining_length, + size_t *publish_properties_length) { + + size_t publish_property_section_length = + aws_mqtt5_compute_user_property_encode_length(publish_view->user_properties, publish_view->user_property_count); + + ADD_OPTIONAL_U8_PROPERTY_LENGTH(publish_view->payload_format, publish_property_section_length); + ADD_OPTIONAL_U32_PROPERTY_LENGTH(publish_view->message_expiry_interval_seconds, publish_property_section_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(publish_view->topic_alias, publish_property_section_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->response_topic, publish_property_section_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->correlation_data, publish_property_section_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(publish_view->content_type, publish_property_section_length); + + for (size_t i = 0; i < publish_view->subscription_identifier_count; ++i) { + size_t encoding_size = 0; + if (aws_mqtt5_get_variable_length_encode_size(publish_view->subscription_identifiers[i], &encoding_size)) { + return AWS_OP_ERR; + } + publish_property_section_length += 1 + encoding_size; + } + + *publish_properties_length = (uint32_t)publish_property_section_length; + + /* + * Remaining Length: + * Variable Header + * - Topic Name + * - Packet Identifier + * - Property Length as VLI x + * - All Properties x + * Payload + */ + + size_t remaining_length = 0; + + /* Property Length VLI size */ + if (aws_mqtt5_get_variable_length_encode_size(publish_property_section_length, &remaining_length)) { + return AWS_OP_ERR; + } + + /* Topic name */ + remaining_length += 2 + publish_view->topic.len; + + /* Optional packet id */ + if (publish_view->packet_id != 0) { + remaining_length += 2; + } + + /* Properties */ + remaining_length += publish_property_section_length; + + /* Payload */ + remaining_length += publish_view->payload.len; + + *total_remaining_length = remaining_length; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_encoder_begin_publish(struct aws_mqtt5_encoder *encoder, const void *view) { + + /* We do a shallow copy of the stored view in order to temporarily side affect it for topic aliasing */ + struct aws_mqtt5_packet_publish_view local_publish_view = *((const struct aws_mqtt5_packet_publish_view *)view); + + uint16_t outbound_topic_alias = 0; + struct aws_byte_cursor outbound_topic; + + if (encoder->topic_alias_resolver != NULL) { + AWS_ZERO_STRUCT(outbound_topic); + if (aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + encoder->topic_alias_resolver, &local_publish_view, &outbound_topic_alias, &outbound_topic)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to perform outbound topic alias resolution on PUBLISH packet with " + "error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + local_publish_view.topic = outbound_topic; + if (outbound_topic_alias != 0) { + local_publish_view.topic_alias = &outbound_topic_alias; + } + } + + /* + * We're going to encode the local mutated view copy, not the stored view. This lets the original packet stay + * unchanged for the entire time it is owned by the client. Otherwise, events that disrupt the alias cache + * (like disconnections) would make correct aliasing impossible (because we'd have mutated and potentially lost + * topic information). + */ + const struct aws_mqtt5_packet_publish_view *publish_view = &local_publish_view; + + size_t total_remaining_length = 0; + size_t publish_properties_length = 0; + + if (s_compute_publish_variable_length_fields(publish_view, &total_remaining_length, &publish_properties_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for PUBLISH packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a PUBLISH packet with remaining length %zu", + (void *)encoder->config.client, + total_remaining_length); + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t publish_property_length_u32 = (uint32_t)publish_properties_length; + + /* + * Fixed Header + * byte 1: + * bits 4-7: MQTT Control Packet Type + * bit 3: DUP flag + * bit 1-2: QoS level + * bit 0: RETAIN + * byte 2-x: Remaining Length as Variable Byte Integer (1-4 bytes) + */ + + uint8_t flags = 0; + + if (publish_view->duplicate) { + flags |= 1 << 3; + } + + flags |= ((uint8_t)publish_view->qos) << 1; + + if (publish_view->retain) { + flags |= 1; + } + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PUBLISH, flags)); + + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + + /* + * Variable Header + * UTF-8 Encoded Topic Name + * 2 byte Packet Identifier + * 1-4 byte Property Length as Variable Byte Integer + * n bytes Properties + */ + + ADD_ENCODE_STEP_LENGTH_PREFIXED_CURSOR(encoder, publish_view->topic); + if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + ADD_ENCODE_STEP_U16(encoder, (uint16_t)publish_view->packet_id); + } + ADD_ENCODE_STEP_VLI(encoder, publish_property_length_u32); + + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_PAYLOAD_FORMAT_INDICATOR, publish_view->payload_format); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_MESSAGE_EXPIRY_INTERVAL, publish_view->message_expiry_interval_seconds); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY(encoder, AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS, publish_view->topic_alias); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_RESPONSE_TOPIC, publish_view->response_topic); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_CORRELATION_DATA, publish_view->correlation_data); + + for (size_t i = 0; i < publish_view->subscription_identifier_count; ++i) { + ADD_ENCODE_STEP_OPTIONAL_VLI_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIER, &publish_view->subscription_identifiers[i]); + } + + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY(encoder, AWS_MQTT5_PROPERTY_TYPE_CONTENT_TYPE, publish_view->content_type); + + aws_mqtt5_add_user_property_encoding_steps( + encoder, publish_view->user_properties, publish_view->user_property_count); + + /* + * Payload + * Content and format of data is application specific + */ + if (publish_view->payload.len > 0) { + ADD_ENCODE_STEP_CURSOR(encoder, publish_view->payload); + } + + return AWS_OP_SUCCESS; +} + +static int s_compute_puback_variable_length_fields( + const struct aws_mqtt5_packet_puback_view *puback_view, + size_t *total_remaining_length, + size_t *puback_properties_length) { + + size_t local_property_length = + aws_mqtt5_compute_user_property_encode_length(puback_view->user_properties, puback_view->user_property_count); + + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(puback_view->reason_string, local_property_length); + + *puback_properties_length = (uint32_t)local_property_length; + + /* variable header total length = + * 2 bytes for Packet Identifier + * + 1 byte for PUBACK reason code if it exists + * + subscribe_variable_header_property_length + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901124 + * If there are no properties and Reason Code is success, PUBACK ends with the packet id + * + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901124 + * If there are no properties and Reason Code is not success, PUBACK ends with the reason code + */ + if (local_property_length == 0) { + if (puback_view->reason_code == AWS_MQTT5_PARC_SUCCESS) { + *total_remaining_length = 2; + } else { + *total_remaining_length = 3; + } + return AWS_OP_SUCCESS; + } + + size_t variable_property_length_size = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &variable_property_length_size)) { + return AWS_OP_ERR; + } + /* vli of property length + packet id + reason code + properties length */ + *total_remaining_length = variable_property_length_size + 3 + local_property_length; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_encoder_begin_puback(struct aws_mqtt5_encoder *encoder, const void *view) { + const struct aws_mqtt5_packet_puback_view *puback_view = view; + + size_t total_remaining_length = 0; + size_t puback_properties_length = 0; + + if (s_compute_puback_variable_length_fields(puback_view, &total_remaining_length, &puback_properties_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for PUBACK packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a PUBACK packet with remaining length %zu", + (void *)encoder->config.client, + total_remaining_length); + + uint32_t total_remaining_length_u32 = (uint32_t)total_remaining_length; + uint32_t puback_property_length_u32 = (uint32_t)puback_properties_length; + + /* + * Fixed Header + * byte 1: + * bits 7-4 MQTT Control Packet Type + * bits 3-0 Reserved, bust be set to 0, 0, 0, 0 + * byte 2-x: Remaining Length as a Variable Byte Integer (1-4 bytes) + */ + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PUBACK, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length_u32); + + /* + * Variable Header + * byte 1-2: Packet Identifier + * byte 3: PUBACK Reason Code + * byte 4-x: Property Length + * Properties + */ + ADD_ENCODE_STEP_U16(encoder, (uint16_t)puback_view->packet_id); + /* + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901124 + * If Reason Code is success and there are no properties, PUBACK ends with the packet id + */ + if (total_remaining_length == 2) { + return AWS_OP_SUCCESS; + } + + ADD_ENCODE_STEP_U8(encoder, puback_view->reason_code); + + /* + * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901126 + * If remaining length < 4 there is no property length + */ + if (total_remaining_length < 4) { + return AWS_OP_SUCCESS; + } + + ADD_ENCODE_STEP_VLI(encoder, puback_property_length_u32); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, puback_view->reason_string); + aws_mqtt5_add_user_property_encoding_steps(encoder, puback_view->user_properties, puback_view->user_property_count); + + return AWS_OP_SUCCESS; +} + +static enum aws_mqtt5_encoding_result s_execute_encode_step( + struct aws_mqtt5_encoder *encoder, + struct aws_mqtt5_encoding_step *step, + struct aws_byte_buf *buffer) { + size_t buffer_room = buffer->capacity - buffer->len; + + switch (step->type) { + case AWS_MQTT5_EST_U8: + if (buffer_room < 1) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + aws_byte_buf_write_u8(buffer, step->value.value_u8); + + return AWS_MQTT5_ER_FINISHED; + + case AWS_MQTT5_EST_U16: + if (buffer_room < 2) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + aws_byte_buf_write_be16(buffer, step->value.value_u16); + + return AWS_MQTT5_ER_FINISHED; + + case AWS_MQTT5_EST_U32: + if (buffer_room < 4) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + aws_byte_buf_write_be32(buffer, step->value.value_u32); + + return AWS_MQTT5_ER_FINISHED; + + case AWS_MQTT5_EST_VLI: + /* being lazy here and just assuming the worst case */ + if (buffer_room < 4) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + /* This can't fail. We've already validated the vli value when we made the step */ + aws_mqtt5_encode_variable_length_integer(buffer, step->value.value_u32); + + return AWS_MQTT5_ER_FINISHED; + + case AWS_MQTT5_EST_CURSOR: + if (buffer_room < 1) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + aws_byte_buf_write_to_capacity(buffer, &step->value.value_cursor); + + return (step->value.value_cursor.len == 0) ? AWS_MQTT5_ER_FINISHED : AWS_MQTT5_ER_OUT_OF_ROOM; + + case AWS_MQTT5_EST_STREAM: + while (buffer->len < buffer->capacity) { + if (aws_input_stream_read(step->value.value_stream, buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to read from stream with error %d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_MQTT5_ER_ERROR; + } + + struct aws_stream_status status; + if (aws_input_stream_get_status(step->value.value_stream, &status)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: failed to query stream status with error %d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_MQTT5_ER_ERROR; + } + + if (status.is_end_of_stream) { + return AWS_MQTT5_ER_FINISHED; + } + } + + if (buffer->len == buffer->capacity) { + return AWS_MQTT5_ER_OUT_OF_ROOM; + } + + /* fall through intentional */ + } + + /* shouldn't be reachable */ + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: encoder reached an unreachable state", (void *)encoder->config.client); + aws_raise_error(AWS_ERROR_INVALID_STATE); + return AWS_MQTT5_ER_ERROR; +} + +enum aws_mqtt5_encoding_result aws_mqtt5_encoder_encode_to_buffer( + struct aws_mqtt5_encoder *encoder, + struct aws_byte_buf *buffer) { + + enum aws_mqtt5_encoding_result result = AWS_MQTT5_ER_FINISHED; + size_t step_count = aws_array_list_length(&encoder->encoding_steps); + while (result == AWS_MQTT5_ER_FINISHED && encoder->current_encoding_step_index < step_count) { + struct aws_mqtt5_encoding_step *step = NULL; + aws_array_list_get_at_ptr(&encoder->encoding_steps, (void **)&step, encoder->current_encoding_step_index); + + result = s_execute_encode_step(encoder, step, buffer); + if (result == AWS_MQTT5_ER_FINISHED) { + encoder->current_encoding_step_index++; + } + } + + if (result == AWS_MQTT5_ER_FINISHED) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: finished encoding current operation", (void *)encoder->config.client); + aws_mqtt5_encoder_reset(encoder); + } + + return result; +} + +static struct aws_mqtt5_encoder_function_table s_aws_mqtt5_encoder_default_function_table = { + .encoders_by_packet_type = + { + NULL, /* RESERVED = 0 */ + &s_aws_mqtt5_encoder_begin_connect, /* CONNECT */ + NULL, /* CONNACK */ + &s_aws_mqtt5_encoder_begin_publish, /* PUBLISH */ + &s_aws_mqtt5_encoder_begin_puback, /* PUBACK */ + NULL, /* PUBREC */ + NULL, /* PUBREL */ + NULL, /* PUBCOMP */ + &s_aws_mqtt5_encoder_begin_subscribe, /* SUBSCRIBE */ + NULL, /* SUBACK */ + &s_aws_mqtt5_encoder_begin_unsubscribe, /* UNSUBSCRIBE */ + NULL, /* UNSUBACK */ + &s_aws_mqtt5_encoder_begin_pingreq, /* PINGREQ */ + NULL, /* PINGRESP */ + &s_aws_mqtt5_encoder_begin_disconnect, /* DISCONNECT */ + NULL /* AUTH */ + }, +}; + +const struct aws_mqtt5_encoder_function_table *g_aws_mqtt5_encoder_default_function_table = + &s_aws_mqtt5_encoder_default_function_table; + +int aws_mqtt5_encoder_init( + struct aws_mqtt5_encoder *encoder, + struct aws_allocator *allocator, + struct aws_mqtt5_encoder_options *options) { + AWS_ZERO_STRUCT(*encoder); + + encoder->config = *options; + if (encoder->config.encoders == NULL) { + encoder->config.encoders = &s_aws_mqtt5_encoder_default_function_table; + } + + if (aws_array_list_init_dynamic( + &encoder->encoding_steps, allocator, INITIAL_ENCODING_STEP_COUNT, sizeof(struct aws_mqtt5_encoding_step))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_encoder_clean_up(struct aws_mqtt5_encoder *encoder) { + aws_array_list_clean_up(&encoder->encoding_steps); +} + +void aws_mqtt5_encoder_reset(struct aws_mqtt5_encoder *encoder) { + aws_array_list_clear(&encoder->encoding_steps); + encoder->current_encoding_step_index = 0; +} + +int aws_mqtt5_encoder_append_packet_encoding( + struct aws_mqtt5_encoder *encoder, + enum aws_mqtt5_packet_type packet_type, + const void *packet_view) { + aws_mqtt5_encode_begin_packet_type_fn *encoding_fn = encoder->config.encoders->encoders_by_packet_type[packet_type]; + if (encoding_fn == NULL) { + return aws_raise_error(AWS_ERROR_MQTT5_ENCODE_FAILURE); + } + + return (*encoding_fn)(encoder, packet_view); +} + +static int s_compute_packet_size(size_t total_remaining_length, size_t *packet_size) { + /* 1 (packet type + flags) + vli_length(total_remaining_length) + total_remaining_length */ + size_t encode_size = 0; + if (aws_mqtt5_get_variable_length_encode_size(total_remaining_length, &encode_size)) { + return AWS_OP_ERR; + } + + size_t prefix = (size_t)1 + encode_size; + + if (aws_add_size_checked(prefix, total_remaining_length, packet_size)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_view_get_encoded_size( + enum aws_mqtt5_packet_type packet_type, + const void *packet_view, + size_t *packet_size) { + size_t total_remaining_length = 0; + size_t properties_length = 0; + + if (packet_type == AWS_MQTT5_PT_PINGREQ) { + *packet_size = AWS_MQTT5_PINGREQ_ENCODED_SIZE; + return AWS_OP_SUCCESS; + } + + switch (packet_type) { + case AWS_MQTT5_PT_PUBLISH: + if (s_compute_publish_variable_length_fields(packet_view, &total_remaining_length, &properties_length)) { + return AWS_OP_ERR; + } + break; + + case AWS_MQTT5_PT_SUBSCRIBE: + if (s_compute_subscribe_variable_length_fields(packet_view, &total_remaining_length, &properties_length)) { + return AWS_OP_ERR; + } + break; + + case AWS_MQTT5_PT_UNSUBSCRIBE: + if (s_compute_unsubscribe_variable_length_fields( + packet_view, &total_remaining_length, &properties_length)) { + return AWS_OP_ERR; + } + break; + + case AWS_MQTT5_PT_DISCONNECT: + if (s_compute_disconnect_variable_length_fields(packet_view, &total_remaining_length, &properties_length)) { + return AWS_OP_ERR; + } + break; + + case AWS_MQTT5_PT_PUBACK: + if (s_compute_puback_variable_length_fields(packet_view, &total_remaining_length, &properties_length)) { + return AWS_OP_ERR; + } + break; + + default: + return aws_raise_error(AWS_ERROR_MQTT5_ENCODE_SIZE_UNSUPPORTED_PACKET_TYPE); + } + + return s_compute_packet_size(total_remaining_length, packet_size); +} + +void aws_mqtt5_encoder_set_outbound_topic_alias_resolver( + struct aws_mqtt5_encoder *encoder, + struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { + + encoder->topic_alias_resolver = resolver; +} diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c new file mode 100644 index 00000000..493649c5 --- /dev/null +++ b/source/v5/mqtt5_options_storage.c @@ -0,0 +1,3893 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/********************************************************************************************************************* + * Property set + ********************************************************************************************************************/ + +int aws_mqtt5_user_property_set_init( + struct aws_mqtt5_user_property_set *property_set, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*property_set); + + if (aws_array_list_init_dynamic(&property_set->properties, allocator, 0, sizeof(struct aws_mqtt5_user_property))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_user_property_set_init_with_storage( + struct aws_mqtt5_user_property_set *property_set, + struct aws_allocator *allocator, + struct aws_byte_buf *storage, + size_t property_count, + const struct aws_mqtt5_user_property *properties) { + AWS_ZERO_STRUCT(*property_set); + + if (aws_array_list_init_dynamic( + &property_set->properties, allocator, property_count, sizeof(struct aws_mqtt5_user_property))) { + goto error; + } + + for (size_t i = 0; i < property_count; ++i) { + const struct aws_mqtt5_user_property *property = &properties[i]; + struct aws_mqtt5_user_property property_clone = *property; + + if (aws_byte_buf_append_and_update(storage, &property_clone.name)) { + goto error; + } + + if (aws_byte_buf_append_and_update(storage, &property_clone.value)) { + goto error; + } + + if (aws_array_list_push_back(&property_set->properties, &property_clone)) { + goto error; + } + } + + return AWS_OP_SUCCESS; + +error: + + aws_mqtt5_user_property_set_clean_up(property_set); + + return AWS_OP_ERR; +} + +void aws_mqtt5_user_property_set_clean_up(struct aws_mqtt5_user_property_set *property_set) { + aws_array_list_clean_up(&property_set->properties); +} + +size_t aws_mqtt5_user_property_set_size(const struct aws_mqtt5_user_property_set *property_set) { + return aws_array_list_length(&property_set->properties); +} + +int aws_mqtt5_user_property_set_get_property( + const struct aws_mqtt5_user_property_set *property_set, + size_t index, + struct aws_mqtt5_user_property *property_out) { + return aws_array_list_get_at(&property_set->properties, property_out, index); +} + +int aws_mqtt5_user_property_set_add_stored_property( + struct aws_mqtt5_user_property_set *property_set, + struct aws_mqtt5_user_property *property) { + return aws_array_list_push_back(&property_set->properties, property); +} + +static void s_aws_mqtt5_user_property_set_log( + struct aws_logger *log_handle, + const struct aws_mqtt5_user_property *properties, + size_t property_count, + void *log_context, + enum aws_log_level level, + const char *log_prefix) { + + if (property_count == 0) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: %s with %zu user properties:", + log_context, + log_prefix, + property_count); + + for (size_t i = 0; i < property_count; ++i) { + const struct aws_mqtt5_user_property *property = &properties[i]; + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: %s user property %zu with name \"" PRInSTR "\", value \"" PRInSTR "\"", + log_context, + log_prefix, + i, + AWS_BYTE_CURSOR_PRI(property->name), + AWS_BYTE_CURSOR_PRI(property->value)); + } +} + +static size_t s_aws_mqtt5_user_property_set_compute_storage_size( + const struct aws_mqtt5_user_property *properties, + size_t property_count) { + size_t storage_size = 0; + for (size_t i = 0; i < property_count; ++i) { + const struct aws_mqtt5_user_property *property = &properties[i]; + storage_size += property->name.len; + storage_size += property->value.len; + } + + return storage_size; +} + +static int s_aws_mqtt5_user_property_set_validate( + const struct aws_mqtt5_user_property *properties, + size_t property_count, + const char *log_prefix, + void *log_context) { + if (properties == NULL) { + if (property_count == 0) { + return AWS_OP_SUCCESS; + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: %s - Invalid user property configuration, null properties, non-zero property count", + log_context, + log_prefix); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } + } + + if (property_count > AWS_MQTT5_CLIENT_MAXIMUM_USER_PROPERTIES) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: %s - user property limit (%d) exceeded (%zu)", + log_context, + log_prefix, + (int)AWS_MQTT5_CLIENT_MAXIMUM_USER_PROPERTIES, + property_count); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } + + for (size_t i = 0; i < property_count; ++i) { + const struct aws_mqtt5_user_property *property = &properties[i]; + if (property->name.len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: %s - user property #%zu name too long (%zu)", + log_context, + log_prefix, + i, + property->name.len); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } + if (property->value.len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: %s - user property #%zu value too long (%zu)", + log_context, + log_prefix, + i, + property->value.len); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +/********************************************************************************************************************* + * Operation base + ********************************************************************************************************************/ + +struct aws_mqtt5_operation *aws_mqtt5_operation_acquire(struct aws_mqtt5_operation *operation) { + if (operation == NULL) { + return NULL; + } + + aws_ref_count_acquire(&operation->ref_count); + + return operation; +} + +struct aws_mqtt5_operation *aws_mqtt5_operation_release(struct aws_mqtt5_operation *operation) { + if (operation != NULL) { + aws_ref_count_release(&operation->ref_count); + } + + return NULL; +} + +void aws_mqtt5_operation_complete( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *associated_view) { + AWS_FATAL_ASSERT(operation->vtable != NULL); + if (operation->vtable->aws_mqtt5_operation_completion_fn != NULL) { + (*operation->vtable->aws_mqtt5_operation_completion_fn)(operation, error_code, packet_type, associated_view); + } +} + +void aws_mqtt5_operation_set_packet_id(struct aws_mqtt5_operation *operation, aws_mqtt5_packet_id_t packet_id) { + AWS_FATAL_ASSERT(operation->vtable != NULL); + if (operation->vtable->aws_mqtt5_operation_set_packet_id_fn != NULL) { + (*operation->vtable->aws_mqtt5_operation_set_packet_id_fn)(operation, packet_id); + } +} + +aws_mqtt5_packet_id_t aws_mqtt5_operation_get_packet_id(const struct aws_mqtt5_operation *operation) { + AWS_FATAL_ASSERT(operation->vtable != NULL); + if (operation->vtable->aws_mqtt5_operation_get_packet_id_address_fn != NULL) { + aws_mqtt5_packet_id_t *packet_id_ptr = + (*operation->vtable->aws_mqtt5_operation_get_packet_id_address_fn)(operation); + if (packet_id_ptr != NULL) { + return *packet_id_ptr; + } + } + + return 0; +} + +aws_mqtt5_packet_id_t *aws_mqtt5_operation_get_packet_id_address(const struct aws_mqtt5_operation *operation) { + AWS_FATAL_ASSERT(operation->vtable != NULL); + if (operation->vtable->aws_mqtt5_operation_get_packet_id_address_fn != NULL) { + return (*operation->vtable->aws_mqtt5_operation_get_packet_id_address_fn)(operation); + } + + return NULL; +} + +int aws_mqtt5_operation_validate_vs_connection_settings( + const struct aws_mqtt5_operation *operation, + const struct aws_mqtt5_client *client) { + + AWS_FATAL_ASSERT(operation->vtable != NULL); + AWS_FATAL_ASSERT(client->loop == NULL || aws_event_loop_thread_is_callers_thread(client->loop)); + + /* If we have valid negotiated settings, check against them as well */ + if (aws_mqtt5_client_are_negotiated_settings_valid(client)) { + const struct aws_mqtt5_negotiated_settings *settings = &client->negotiated_settings; + + size_t packet_size_in_bytes = 0; + if (aws_mqtt5_packet_view_get_encoded_size( + operation->packet_type, operation->packet_view, &packet_size_in_bytes)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: error %d (%s) computing %s packet size", + (void *)client, + error_code, + aws_error_debug_str(error_code), + aws_mqtt5_packet_type_to_c_string(operation->packet_type)); + return aws_raise_error(AWS_ERROR_MQTT5_PACKET_VALIDATION); + } + + if (packet_size_in_bytes > settings->maximum_packet_size_to_server) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, + "id=%p: encoded %s packet size (%zu) exceeds server's maximum " + "packet size (%" PRIu32 ")", + (void *)client, + aws_mqtt5_packet_type_to_c_string(operation->packet_type), + packet_size_in_bytes, + settings->maximum_packet_size_to_server); + return aws_raise_error(AWS_ERROR_MQTT5_PACKET_VALIDATION); + } + } + + if (operation->vtable->aws_mqtt5_operation_validate_vs_connection_settings_fn != NULL) { + return (*operation->vtable->aws_mqtt5_operation_validate_vs_connection_settings_fn)( + operation->packet_view, client); + } + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_operation_vtable s_empty_operation_vtable = { + .aws_mqtt5_operation_completion_fn = NULL, + .aws_mqtt5_operation_set_packet_id_fn = NULL, + .aws_mqtt5_operation_get_packet_id_address_fn = NULL, + .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, +}; + +/********************************************************************************************************************* + * Connect + ********************************************************************************************************************/ + +int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect_view *connect_options) { + if (connect_options == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "Null CONNECT options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (connect_options->client_id.len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_connect_view - client id too long", (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + + if (connect_options->username != NULL) { + if (connect_options->username->len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - username too long", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->password != NULL) { + if (connect_options->password->len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - password too long", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->receive_maximum != NULL) { + if (*connect_options->receive_maximum == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - receive maximum property of CONNECT packet may not be zero.", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->maximum_packet_size_bytes != NULL) { + if (*connect_options->maximum_packet_size_bytes == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - maximum packet size property of CONNECT packet may not be " + "zero.", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->will != NULL) { + const struct aws_mqtt5_packet_publish_view *will_options = connect_options->will; + if (aws_mqtt5_packet_publish_view_validate(will_options)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - CONNECT packet Will message failed validation", + (void *)connect_options); + return AWS_OP_ERR; + } + + if (will_options->payload.len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - will payload larger than %d", + (void *)connect_options, + (int)UINT16_MAX); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->request_problem_information != NULL) { + if (*connect_options->request_problem_information > 1) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - CONNECT packet request problem information has invalid value", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (connect_options->request_response_information != NULL) { + if (*connect_options->request_response_information > 1) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - CONNECT packet request response information has invalid value", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + } + + if (s_aws_mqtt5_user_property_set_validate( + connect_options->user_properties, + connect_options->user_property_count, + "aws_mqtt5_packet_connect_view", + (void *)connect_options)) { + return AWS_OP_ERR; + } + + if (connect_options->authentication_method != NULL || connect_options->authentication_data != NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - CONNECT packet has unsupported authentication fields set.", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_connect_view_log( + const struct aws_mqtt5_packet_connect_view *connect_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view keep alive interval set to %" PRIu16, + (void *)connect_view, + connect_view->keep_alive_interval_seconds); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view client id set to \"" PRInSTR "\"", + (void *)connect_view, + AWS_BYTE_CURSOR_PRI(connect_view->client_id)); + + if (connect_view->username != NULL) { + /* Intentionally do not log username since it too can contain sensitive information */ + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view username set", + (void *)connect_view); + } + + if (connect_view->password != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view password set", + (void *)connect_view); + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view clean start set to %d", + (void *)connect_view, + (int)(connect_view->clean_start ? 1 : 0)); + + if (connect_view->session_expiry_interval_seconds != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view session expiry interval set to %" PRIu32, + (void *)connect_view, + *connect_view->session_expiry_interval_seconds); + } + + if (connect_view->request_response_information != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view request response information set to %d", + (void *)connect_view, + (int)*connect_view->request_response_information); + } + + if (connect_view->request_problem_information) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view request problem information set to %d", + (void *)connect_view, + (int)*connect_view->request_problem_information); + } + + if (connect_view->receive_maximum != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view receive maximum set to %" PRIu16, + (void *)connect_view, + *connect_view->receive_maximum); + } + + if (connect_view->topic_alias_maximum != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view topic alias maximum set to %" PRIu16, + (void *)connect_view, + *connect_view->topic_alias_maximum); + } + + if (connect_view->maximum_packet_size_bytes != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view maximum packet size set to %" PRIu32, + (void *)connect_view, + *connect_view->maximum_packet_size_bytes); + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view set will to (%p)", + (void *)connect_view, + (void *)connect_view->will); + + if (connect_view->will != NULL) { + aws_mqtt5_packet_publish_view_log(connect_view->will, level); + } + + if (connect_view->will_delay_interval_seconds != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view will delay interval set to %" PRIu32, + (void *)connect_view, + *connect_view->will_delay_interval_seconds); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + connect_view->user_properties, + connect_view->user_property_count, + (void *)connect_view, + level, + "aws_mqtt5_packet_connect_view"); + + if (connect_view->authentication_method != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view authentication method set", + (void *)connect_view); + } + + if (connect_view->password != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view authentication data set", + (void *)connect_view); + } +} + +void aws_mqtt5_packet_connect_storage_clean_up(struct aws_mqtt5_packet_connect_storage *storage) { + if (storage == NULL) { + return; + } + + if (storage->will != NULL) { + aws_mqtt5_packet_publish_storage_clean_up(storage->will); + aws_mem_release(storage->allocator, storage->will); + } + + aws_mqtt5_user_property_set_clean_up(&storage->user_properties); + + aws_byte_buf_clean_up_secure(&storage->storage); +} + +static size_t s_aws_mqtt5_packet_connect_compute_storage_size(const struct aws_mqtt5_packet_connect_view *view) { + if (view == NULL) { + return 0; + } + + size_t storage_size = 0; + + storage_size += view->client_id.len; + if (view->username != NULL) { + storage_size += view->username->len; + } + if (view->password != NULL) { + storage_size += view->password->len; + } + + storage_size += + s_aws_mqtt5_user_property_set_compute_storage_size(view->user_properties, view->user_property_count); + + if (view->authentication_method != NULL) { + storage_size += view->authentication_method->len; + } + + if (view->authentication_data != NULL) { + storage_size += view->authentication_data->len; + } + + return storage_size; +} + +int aws_mqtt5_packet_connect_storage_init( + struct aws_mqtt5_packet_connect_storage *storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connect_view *view) { + AWS_ZERO_STRUCT(*storage); + + struct aws_mqtt5_packet_connect_view *storage_view = &storage->storage_view; + + size_t storage_capacity = s_aws_mqtt5_packet_connect_compute_storage_size(view); + if (aws_byte_buf_init(&storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + storage->allocator = allocator; + storage_view->keep_alive_interval_seconds = view->keep_alive_interval_seconds; + + storage_view->client_id = view->client_id; + if (aws_byte_buf_append_and_update(&storage->storage, &storage_view->client_id)) { + return AWS_OP_ERR; + } + + if (view->username != NULL) { + storage->username = *view->username; + if (aws_byte_buf_append_and_update(&storage->storage, &storage->username)) { + return AWS_OP_ERR; + } + + storage_view->username = &storage->username; + } + + if (view->password != NULL) { + storage->password = *view->password; + if (aws_byte_buf_append_and_update(&storage->storage, &storage->password)) { + return AWS_OP_ERR; + } + + storage_view->password = &storage->password; + } + + storage_view->clean_start = view->clean_start; + + if (view->session_expiry_interval_seconds != NULL) { + storage->session_expiry_interval_seconds = *view->session_expiry_interval_seconds; + storage_view->session_expiry_interval_seconds = &storage->session_expiry_interval_seconds; + } + + if (view->request_response_information != NULL) { + storage->request_response_information = *view->request_response_information; + storage_view->request_response_information = &storage->request_response_information; + } + + if (view->request_problem_information != NULL) { + storage->request_problem_information = *view->request_problem_information; + storage_view->request_problem_information = &storage->request_problem_information; + } + + if (view->receive_maximum != NULL) { + storage->receive_maximum = *view->receive_maximum; + storage_view->receive_maximum = &storage->receive_maximum; + } + + if (view->topic_alias_maximum != NULL) { + storage->topic_alias_maximum = *view->topic_alias_maximum; + storage_view->topic_alias_maximum = &storage->topic_alias_maximum; + } + + if (view->maximum_packet_size_bytes != NULL) { + storage->maximum_packet_size_bytes = *view->maximum_packet_size_bytes; + storage_view->maximum_packet_size_bytes = &storage->maximum_packet_size_bytes; + } + + if (view->will != NULL) { + storage->will = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); + if (storage->will == NULL) { + return AWS_OP_ERR; + } + + if (aws_mqtt5_packet_publish_storage_init(storage->will, allocator, view->will)) { + return AWS_OP_ERR; + } + + storage_view->will = &storage->will->storage_view; + } + + if (view->will_delay_interval_seconds != 0) { + storage->will_delay_interval_seconds = *view->will_delay_interval_seconds; + storage_view->will_delay_interval_seconds = &storage->will_delay_interval_seconds; + } + + if (aws_mqtt5_user_property_set_init_with_storage( + &storage->user_properties, + allocator, + &storage->storage, + view->user_property_count, + view->user_properties)) { + return AWS_OP_ERR; + } + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&storage->user_properties); + storage_view->user_properties = storage->user_properties.properties.data; + + if (view->authentication_method != NULL) { + storage->authentication_method = *view->authentication_method; + if (aws_byte_buf_append_and_update(&storage->storage, &storage->authentication_method)) { + return AWS_OP_ERR; + } + + storage_view->authentication_method = &storage->authentication_method; + } + + if (view->authentication_data != NULL) { + storage->authentication_data = *view->authentication_data; + if (aws_byte_buf_append_and_update(&storage->storage, &storage->authentication_data)) { + return AWS_OP_ERR; + } + + storage_view->authentication_data = &storage->authentication_data; + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_connect_storage_init_from_external_storage( + struct aws_mqtt5_packet_connect_storage *connect_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*connect_storage); + + if (aws_mqtt5_user_property_set_init(&connect_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_destroy_operation_connect(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_connect *connect_op = object; + + aws_mqtt5_packet_connect_storage_clean_up(&connect_op->options_storage); + + aws_mem_release(connect_op->allocator, connect_op); +} + +struct aws_mqtt5_operation_connect *aws_mqtt5_operation_connect_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connect_view *connect_options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(connect_options != NULL); + + if (aws_mqtt5_packet_connect_view_validate(connect_options)) { + return NULL; + } + + struct aws_mqtt5_operation_connect *connect_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_connect)); + if (connect_op == NULL) { + return NULL; + } + + connect_op->allocator = allocator; + connect_op->base.vtable = &s_empty_operation_vtable; + connect_op->base.packet_type = AWS_MQTT5_PT_CONNECT; + aws_ref_count_init(&connect_op->base.ref_count, connect_op, s_destroy_operation_connect); + connect_op->base.impl = connect_op; + + if (aws_mqtt5_packet_connect_storage_init(&connect_op->options_storage, allocator, connect_options)) { + goto error; + } + + connect_op->base.packet_view = &connect_op->options_storage.storage_view; + + return connect_op; + +error: + + aws_mqtt5_operation_release(&connect_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Connack + ********************************************************************************************************************/ + +static size_t s_aws_mqtt5_packet_connack_compute_storage_size(const struct aws_mqtt5_packet_connack_view *view) { + if (view == NULL) { + return 0; + } + + size_t storage_size = 0; + + if (view->assigned_client_identifier != NULL) { + storage_size += view->assigned_client_identifier->len; + } + + if (view->reason_string != NULL) { + storage_size += view->reason_string->len; + } + + if (view->response_information != NULL) { + storage_size += view->response_information->len; + } + + if (view->server_reference != NULL) { + storage_size += view->server_reference->len; + } + + if (view->authentication_method != NULL) { + storage_size += view->authentication_method->len; + } + + if (view->authentication_data != NULL) { + storage_size += view->authentication_data->len; + } + + storage_size += + s_aws_mqtt5_user_property_set_compute_storage_size(view->user_properties, view->user_property_count); + + return storage_size; +} + +int aws_mqtt5_packet_connack_storage_init( + struct aws_mqtt5_packet_connack_storage *connack_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_connack_view *connack_view) { + + AWS_ZERO_STRUCT(*connack_storage); + size_t storage_capacity = s_aws_mqtt5_packet_connack_compute_storage_size(connack_view); + if (aws_byte_buf_init(&connack_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_connack_view *stored_view = &connack_storage->storage_view; + + connack_storage->allocator = allocator; + + stored_view->session_present = connack_view->session_present; + stored_view->reason_code = connack_view->reason_code; + + if (connack_view->session_expiry_interval != NULL) { + connack_storage->session_expiry_interval = *connack_view->session_expiry_interval; + stored_view->session_expiry_interval = &connack_storage->session_expiry_interval; + } + + if (connack_view->receive_maximum != NULL) { + connack_storage->receive_maximum = *connack_view->receive_maximum; + stored_view->receive_maximum = &connack_storage->receive_maximum; + } + + if (connack_view->maximum_qos != NULL) { + connack_storage->maximum_qos = *connack_view->maximum_qos; + stored_view->maximum_qos = &connack_storage->maximum_qos; + } + + if (connack_view->retain_available != NULL) { + connack_storage->retain_available = *connack_view->retain_available; + stored_view->retain_available = &connack_storage->retain_available; + } + + if (connack_view->maximum_packet_size != NULL) { + connack_storage->maximum_packet_size = *connack_view->maximum_packet_size; + stored_view->maximum_packet_size = &connack_storage->maximum_packet_size; + } + + if (connack_view->assigned_client_identifier != NULL) { + connack_storage->assigned_client_identifier = *connack_view->assigned_client_identifier; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->assigned_client_identifier)) { + return AWS_OP_ERR; + } + + stored_view->assigned_client_identifier = &connack_storage->assigned_client_identifier; + } + + if (connack_view->topic_alias_maximum != NULL) { + connack_storage->topic_alias_maximum = *connack_view->topic_alias_maximum; + stored_view->topic_alias_maximum = &connack_storage->topic_alias_maximum; + } + + if (connack_view->reason_string != NULL) { + connack_storage->reason_string = *connack_view->reason_string; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->reason_string)) { + return AWS_OP_ERR; + } + + stored_view->reason_string = &connack_storage->reason_string; + } + + if (connack_view->wildcard_subscriptions_available != NULL) { + connack_storage->wildcard_subscriptions_available = *connack_view->wildcard_subscriptions_available; + stored_view->wildcard_subscriptions_available = &connack_storage->wildcard_subscriptions_available; + } + + if (connack_view->subscription_identifiers_available != NULL) { + connack_storage->subscription_identifiers_available = *connack_view->subscription_identifiers_available; + stored_view->subscription_identifiers_available = &connack_storage->subscription_identifiers_available; + } + + if (connack_view->shared_subscriptions_available != NULL) { + connack_storage->shared_subscriptions_available = *connack_view->shared_subscriptions_available; + stored_view->shared_subscriptions_available = &connack_storage->shared_subscriptions_available; + } + + if (connack_view->server_keep_alive != NULL) { + connack_storage->server_keep_alive = *connack_view->server_keep_alive; + stored_view->server_keep_alive = &connack_storage->server_keep_alive; + } + + if (connack_view->response_information != NULL) { + connack_storage->response_information = *connack_view->response_information; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->response_information)) { + return AWS_OP_ERR; + } + + stored_view->response_information = &connack_storage->response_information; + } + + if (connack_view->server_reference != NULL) { + connack_storage->server_reference = *connack_view->server_reference; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->server_reference)) { + return AWS_OP_ERR; + } + + stored_view->server_reference = &connack_storage->server_reference; + } + + if (connack_view->authentication_method != NULL) { + connack_storage->authentication_method = *connack_view->authentication_method; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->authentication_method)) { + return AWS_OP_ERR; + } + + stored_view->authentication_method = &connack_storage->authentication_method; + } + + if (connack_view->authentication_data != NULL) { + connack_storage->authentication_data = *connack_view->authentication_data; + if (aws_byte_buf_append_and_update(&connack_storage->storage, &connack_storage->authentication_data)) { + return AWS_OP_ERR; + } + + stored_view->authentication_data = &connack_storage->authentication_data; + } + + if (aws_mqtt5_user_property_set_init_with_storage( + &connack_storage->user_properties, + allocator, + &connack_storage->storage, + connack_view->user_property_count, + connack_view->user_properties)) { + return AWS_OP_ERR; + } + + stored_view->user_property_count = aws_mqtt5_user_property_set_size(&connack_storage->user_properties); + stored_view->user_properties = connack_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_connack_storage_init_from_external_storage( + struct aws_mqtt5_packet_connack_storage *connack_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*connack_storage); + + if (aws_mqtt5_user_property_set_init(&connack_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_connack_storage_clean_up(struct aws_mqtt5_packet_connack_storage *connack_storage) { + if (connack_storage == NULL) { + return; + } + + aws_mqtt5_user_property_set_clean_up(&connack_storage->user_properties); + aws_byte_buf_clean_up(&connack_storage->storage); +} + +void aws_mqtt5_packet_connack_view_log( + const struct aws_mqtt5_packet_connack_view *connack_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view reason code set to %d (%s)", + (void *)connack_view, + (int)connack_view->reason_code, + aws_mqtt5_connect_reason_code_to_c_string(connack_view->reason_code)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view session present set to %d", + (void *)connack_view, + (int)connack_view->session_present); + + if (connack_view->session_expiry_interval != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view session expiry interval set to %" PRIu32, + (void *)connack_view, + *connack_view->session_expiry_interval); + } + + if (connack_view->receive_maximum != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view receive maximum set to %" PRIu16, + (void *)connack_view, + *connack_view->receive_maximum); + } + + if (connack_view->maximum_qos != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view maximum qos set to %d", + (void *)connack_view, + (int)(*connack_view->maximum_qos)); + } + + if (connack_view->retain_available != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view retain available set to %d", + (void *)connack_view, + (int)(*connack_view->retain_available)); + } + + if (connack_view->maximum_packet_size != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view maximum packet size set to %" PRIu32, + (void *)connack_view, + *connack_view->maximum_packet_size); + } + + if (connack_view->assigned_client_identifier != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view assigned client identifier set to \"" PRInSTR "\"", + (void *)connack_view, + AWS_BYTE_CURSOR_PRI(*connack_view->assigned_client_identifier)); + } + + if (connack_view->topic_alias_maximum != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view topic alias maximum set to %" PRIu16, + (void *)connack_view, + *connack_view->topic_alias_maximum); + } + + if (connack_view->reason_string != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view reason string set to \"" PRInSTR "\"", + (void *)connack_view, + AWS_BYTE_CURSOR_PRI(*connack_view->reason_string)); + } + + if (connack_view->wildcard_subscriptions_available != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view wildcard subscriptions available set to %d", + (void *)connack_view, + (int)(*connack_view->wildcard_subscriptions_available)); + } + + if (connack_view->subscription_identifiers_available != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view subscription identifiers available set to %d", + (void *)connack_view, + (int)(*connack_view->subscription_identifiers_available)); + } + + if (connack_view->shared_subscriptions_available != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view shared subscriptions available set to %d", + (void *)connack_view, + (int)(*connack_view->shared_subscriptions_available)); + } + + if (connack_view->server_keep_alive != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view server keep alive set to %" PRIu16, + (void *)connack_view, + *connack_view->server_keep_alive); + } + + if (connack_view->response_information != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view response information set to \"" PRInSTR "\"", + (void *)connack_view, + AWS_BYTE_CURSOR_PRI(*connack_view->response_information)); + } + + if (connack_view->server_reference != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view server reference set to \"" PRInSTR "\"", + (void *)connack_view, + AWS_BYTE_CURSOR_PRI(*connack_view->server_reference)); + } + + if (connack_view->authentication_method != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view authentication method set", + (void *)connack_view); + } + + if (connack_view->authentication_data != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connack_view authentication data set", + (void *)connack_view); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + connack_view->user_properties, + connack_view->user_property_count, + (void *)connack_view, + level, + "aws_mqtt5_packet_connack_view"); +} + +/********************************************************************************************************************* + * Disconnect + ********************************************************************************************************************/ + +int aws_mqtt5_packet_disconnect_view_validate(const struct aws_mqtt5_packet_disconnect_view *disconnect_view) { + + if (disconnect_view == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "null DISCONNECT packet options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + bool is_valid_reason_code = true; + aws_mqtt5_disconnect_reason_code_to_c_string(disconnect_view->reason_code, &is_valid_reason_code); + if (!is_valid_reason_code) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view - invalid DISCONNECT reason code:%d", + (void *)disconnect_view, + (int)disconnect_view->reason_code); + return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); + } + + if (disconnect_view->reason_string != NULL) { + if (disconnect_view->reason_string->len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view - reason string too long", + (void *)disconnect_view); + return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); + } + } + + if (disconnect_view->server_reference != NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view - sending a server reference with a client-sourced DISCONNECT is " + "not allowed", + (void *)disconnect_view); + return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); + } + + if (s_aws_mqtt5_user_property_set_validate( + disconnect_view->user_properties, + disconnect_view->user_property_count, + "aws_mqtt5_packet_disconnect_view", + (void *)disconnect_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_packet_disconnect_view_validate_vs_connection_settings( + const void *packet_view, + const struct aws_mqtt5_client *client) { + + const struct aws_mqtt5_packet_disconnect_view *disconnect_view = packet_view; + + if (disconnect_view->session_expiry_interval_seconds != NULL) { + /* + * By spec (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901211), you + * cannot set a non-zero value here if you sent a 0-value or no value in the CONNECT (presumably allows + * the server to skip tracking session state, and we can't undo that now) + */ + const uint32_t *session_expiry_ptr = client->config->connect.storage_view.session_expiry_interval_seconds; + if (*disconnect_view->session_expiry_interval_seconds > 0 && + (session_expiry_ptr == NULL || *session_expiry_ptr == 0)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view - cannot specify a positive session expiry after " + "committing " + "to 0-valued session expiry in CONNECT", + (void *)disconnect_view); + return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_disconnect_view_log( + const struct aws_mqtt5_packet_disconnect_view *disconnect_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view reason code set to %d (%s)", + (void *)disconnect_view, + (int)disconnect_view->reason_code, + aws_mqtt5_disconnect_reason_code_to_c_string(disconnect_view->reason_code, NULL)); + + if (disconnect_view->session_expiry_interval_seconds != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view session expiry interval set to %" PRIu32, + (void *)disconnect_view, + *disconnect_view->session_expiry_interval_seconds); + } + + if (disconnect_view->reason_string != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view reason string set to \"" PRInSTR "\"", + (void *)disconnect_view, + AWS_BYTE_CURSOR_PRI(*disconnect_view->reason_string)); + } + + if (disconnect_view->server_reference != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view server reference set to \"" PRInSTR "\"", + (void *)disconnect_view, + AWS_BYTE_CURSOR_PRI(*disconnect_view->server_reference)); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + disconnect_view->user_properties, + disconnect_view->user_property_count, + (void *)disconnect_view, + level, + "aws_mqtt5_packet_disconnect_view"); +} + +void aws_mqtt5_packet_disconnect_storage_clean_up(struct aws_mqtt5_packet_disconnect_storage *disconnect_storage) { + if (disconnect_storage == NULL) { + return; + } + + aws_mqtt5_user_property_set_clean_up(&disconnect_storage->user_properties); + aws_byte_buf_clean_up(&disconnect_storage->storage); +} + +static size_t s_aws_mqtt5_packet_disconnect_compute_storage_size( + const struct aws_mqtt5_packet_disconnect_view *disconnect_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + disconnect_view->user_properties, disconnect_view->user_property_count); + + if (disconnect_view->reason_string != NULL) { + storage_size += disconnect_view->reason_string->len; + } + + if (disconnect_view->server_reference != NULL) { + storage_size += disconnect_view->server_reference->len; + } + + return storage_size; +} + +int aws_mqtt5_packet_disconnect_storage_init( + struct aws_mqtt5_packet_disconnect_storage *disconnect_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_disconnect_view *disconnect_options) { + + AWS_ZERO_STRUCT(*disconnect_storage); + size_t storage_capacity = s_aws_mqtt5_packet_disconnect_compute_storage_size(disconnect_options); + if (aws_byte_buf_init(&disconnect_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_disconnect_view *storage_view = &disconnect_storage->storage_view; + + storage_view->reason_code = disconnect_options->reason_code; + + if (disconnect_options->session_expiry_interval_seconds != NULL) { + disconnect_storage->session_expiry_interval_seconds = *disconnect_options->session_expiry_interval_seconds; + storage_view->session_expiry_interval_seconds = &disconnect_storage->session_expiry_interval_seconds; + } + + if (disconnect_options->reason_string != NULL) { + disconnect_storage->reason_string = *disconnect_options->reason_string; + if (aws_byte_buf_append_and_update(&disconnect_storage->storage, &disconnect_storage->reason_string)) { + return AWS_OP_ERR; + } + + storage_view->reason_string = &disconnect_storage->reason_string; + } + + if (disconnect_options->server_reference != NULL) { + disconnect_storage->server_reference = *disconnect_options->server_reference; + if (aws_byte_buf_append_and_update(&disconnect_storage->storage, &disconnect_storage->server_reference)) { + return AWS_OP_ERR; + } + + storage_view->server_reference = &disconnect_storage->server_reference; + } + + if (aws_mqtt5_user_property_set_init_with_storage( + &disconnect_storage->user_properties, + allocator, + &disconnect_storage->storage, + disconnect_options->user_property_count, + disconnect_options->user_properties)) { + return AWS_OP_ERR; + } + + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&disconnect_storage->user_properties); + storage_view->user_properties = disconnect_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_disconnect_storage_init_from_external_storage( + struct aws_mqtt5_packet_disconnect_storage *disconnect_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*disconnect_storage); + + if (aws_mqtt5_user_property_set_init(&disconnect_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_destroy_operation_disconnect(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_disconnect *disconnect_op = object; + + aws_mqtt5_packet_disconnect_storage_clean_up(&disconnect_op->options_storage); + + aws_mem_release(disconnect_op->allocator, disconnect_op); +} + +static void s_aws_mqtt5_disconnect_operation_completion( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *completion_view) { + + (void)completion_view; + (void)packet_type; + + struct aws_mqtt5_operation_disconnect *disconnect_op = operation->impl; + + if (disconnect_op->internal_completion_options.completion_callback != NULL) { + (*disconnect_op->internal_completion_options.completion_callback)( + error_code, disconnect_op->internal_completion_options.completion_user_data); + } + + if (disconnect_op->external_completion_options.completion_callback != NULL) { + (*disconnect_op->external_completion_options.completion_callback)( + error_code, disconnect_op->external_completion_options.completion_user_data); + } +} + +static struct aws_mqtt5_operation_vtable s_disconnect_operation_vtable = { + .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_disconnect_operation_completion, + .aws_mqtt5_operation_set_packet_id_fn = NULL, + .aws_mqtt5_operation_get_packet_id_address_fn = NULL, + .aws_mqtt5_operation_validate_vs_connection_settings_fn = + s_aws_mqtt5_packet_disconnect_view_validate_vs_connection_settings, +}; + +struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_disconnect_view *disconnect_options, + const struct aws_mqtt5_disconnect_completion_options *external_completion_options, + const struct aws_mqtt5_disconnect_completion_options *internal_completion_options) { + AWS_PRECONDITION(allocator != NULL); + + if (aws_mqtt5_packet_disconnect_view_validate(disconnect_options)) { + return NULL; + } + + struct aws_mqtt5_operation_disconnect *disconnect_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_disconnect)); + if (disconnect_op == NULL) { + return NULL; + } + + disconnect_op->allocator = allocator; + disconnect_op->base.vtable = &s_disconnect_operation_vtable; + disconnect_op->base.packet_type = AWS_MQTT5_PT_DISCONNECT; + aws_ref_count_init(&disconnect_op->base.ref_count, disconnect_op, s_destroy_operation_disconnect); + disconnect_op->base.impl = disconnect_op; + + if (aws_mqtt5_packet_disconnect_storage_init(&disconnect_op->options_storage, allocator, disconnect_options)) { + goto error; + } + + disconnect_op->base.packet_view = &disconnect_op->options_storage.storage_view; + if (external_completion_options != NULL) { + disconnect_op->external_completion_options = *external_completion_options; + } + + if (internal_completion_options != NULL) { + disconnect_op->internal_completion_options = *internal_completion_options; + } + + return disconnect_op; + +error: + + aws_mqtt5_operation_release(&disconnect_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Publish + ********************************************************************************************************************/ + +int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish_view *publish_view) { + + if (publish_view == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "null PUBLISH packet options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (publish_view->qos < AWS_MQTT5_QOS_AT_MOST_ONCE || publish_view->qos > AWS_MQTT5_QOS_EXACTLY_ONCE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - unsupported QoS value in PUBLISH packet options: %d", + (void *)publish_view, + (int)publish_view->qos); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (publish_view->qos == AWS_MQTT5_QOS_AT_MOST_ONCE) { + if (publish_view->duplicate) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - duplicate flag must be set to 0 for QoS 0 messages", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + if (publish_view->packet_id != 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - Packet ID must not be set for QoS 0 messages", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + /* 0-length topic is never valid, even with user-controlled outbound aliasing */ + if (publish_view->topic.len == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - missing topic", (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } else if (!aws_mqtt_is_valid_topic(&publish_view->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - invalid topic: \"" PRInSTR "\"", + (void *)publish_view, + AWS_BYTE_CURSOR_PRI(publish_view->topic)); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (publish_view->topic_alias != NULL) { + if (*publish_view->topic_alias == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - topic alias may not be zero", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + if (publish_view->payload_format != NULL) { + if (*publish_view->payload_format < AWS_MQTT5_PFI_BYTES || *publish_view->payload_format > AWS_MQTT5_PFI_UTF8) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - invalid payload format value: %d", + (void *)publish_view, + (int)*publish_view->payload_format); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + if (publish_view->response_topic != NULL) { + if (publish_view->response_topic->len >= UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - response topic too long", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (!aws_mqtt_is_valid_topic(publish_view->response_topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - response topic must be a valid mqtt topic", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + if (publish_view->correlation_data != NULL) { + if (publish_view->correlation_data->len >= UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - correlation data too long", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + /* + * validate is done from a client perspective and clients should never generate subscription identifier in a + * publish message + */ + if (publish_view->subscription_identifier_count != 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "Client-initiated PUBLISH packets may not contain subscription identifiers"); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (publish_view->content_type != NULL) { + if (publish_view->content_type->len >= UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - content type too long", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + if (s_aws_mqtt5_user_property_set_validate( + publish_view->user_properties, + publish_view->user_property_count, + "aws_mqtt5_packet_publish_view", + (void *)publish_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_publish_view_validate_vs_iot_core(const struct aws_mqtt5_packet_publish_view *publish_view) { + if (!aws_mqtt_is_valid_topic_for_iot_core(publish_view->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - topic not valid for AWS Iot Core limits: \"" PRInSTR "\"", + (void *)publish_view, + AWS_BYTE_CURSOR_PRI(publish_view->topic)); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_packet_publish_view_validate_vs_connection_settings( + const void *packet_view, + const struct aws_mqtt5_client *client) { + const struct aws_mqtt5_packet_publish_view *publish_view = packet_view; + + /* If we have valid negotiated settings, check against them as well */ + if (aws_mqtt5_client_are_negotiated_settings_valid(client)) { + const struct aws_mqtt5_negotiated_settings *settings = &client->negotiated_settings; + + if (publish_view->qos > settings->maximum_qos) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - QoS value %d exceeds negotiated maximum qos %d", + (void *)publish_view, + (int)publish_view->qos, + (int)settings->maximum_qos); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (publish_view->topic_alias != NULL) { + const struct aws_mqtt5_client_options_storage *client_options = client->config; + if (client_options->topic_aliasing_options.outbound_topic_alias_behavior != AWS_MQTT5_COTABT_USER) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - topic alias set but outbound topic alias behavior has not " + "been set to user controlled", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + if (*publish_view->topic_alias > settings->topic_alias_maximum_to_server) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - outbound topic alias (%d) exceeds server's topic alias " + "maximum " + "(%d)", + (void *)publish_view, + (int)(*publish_view->topic_alias), + (int)settings->topic_alias_maximum_to_server); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + if (publish_view->retain && settings->retain_available == false) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - server does not support Retain", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_publish_view_log( + const struct aws_mqtt5_packet_publish_view *publish_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view packet id set to %d", + (void *)publish_view, + (int)publish_view->packet_id); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view payload set containing %zu bytes", + (void *)publish_view, + publish_view->payload.len); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view qos set to %d", + (void *)publish_view, + (int)publish_view->qos); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view retain set to %d", + (void *)publish_view, + (int)publish_view->retain); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view topic set to \"" PRInSTR "\"", + (void *)publish_view, + AWS_BYTE_CURSOR_PRI(publish_view->topic)); + + if (publish_view->payload_format != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view payload format indicator set to %d (%s)", + (void *)publish_view, + (int)*publish_view->payload_format, + aws_mqtt5_payload_format_indicator_to_c_string(*publish_view->payload_format)); + } + + if (publish_view->message_expiry_interval_seconds != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view message expiry interval set to %" PRIu32, + (void *)publish_view, + *publish_view->message_expiry_interval_seconds); + } + + if (publish_view->topic_alias != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view topic alias set to %" PRIu16, + (void *)publish_view, + *publish_view->topic_alias); + } + + if (publish_view->response_topic != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view response topic set to \"" PRInSTR "\"", + (void *)publish_view, + AWS_BYTE_CURSOR_PRI(*publish_view->response_topic)); + } + + if (publish_view->correlation_data != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - set correlation data", + (void *)publish_view); + } + + if (publish_view->content_type != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view content type set to \"" PRInSTR "\"", + (void *)publish_view, + AWS_BYTE_CURSOR_PRI(*publish_view->content_type)); + } + + for (size_t i = 0; i < publish_view->subscription_identifier_count; ++i) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view subscription identifier %d: %" PRIu32, + (void *)publish_view, + (int)i, + publish_view->subscription_identifiers[i]); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + publish_view->user_properties, + publish_view->user_property_count, + (void *)publish_view, + level, + "aws_mqtt5_packet_publish_view"); +} + +static size_t s_aws_mqtt5_packet_publish_compute_storage_size( + const struct aws_mqtt5_packet_publish_view *publish_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + publish_view->user_properties, publish_view->user_property_count); + + storage_size += publish_view->topic.len; + storage_size += publish_view->payload.len; + + if (publish_view->response_topic != NULL) { + storage_size += publish_view->response_topic->len; + } + + if (publish_view->correlation_data != NULL) { + storage_size += publish_view->correlation_data->len; + } + + if (publish_view->content_type != NULL) { + storage_size += publish_view->content_type->len; + } + + return storage_size; +} + +int aws_mqtt5_packet_publish_storage_init( + struct aws_mqtt5_packet_publish_storage *publish_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_publish_view *publish_options) { + + AWS_ZERO_STRUCT(*publish_storage); + size_t storage_capacity = s_aws_mqtt5_packet_publish_compute_storage_size(publish_options); + if (aws_byte_buf_init(&publish_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic(&publish_storage->subscription_identifiers, allocator, 0, sizeof(uint32_t))) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_publish_view *storage_view = &publish_storage->storage_view; + + storage_view->packet_id = publish_options->packet_id; + + storage_view->payload = publish_options->payload; + if (aws_byte_buf_append_and_update(&publish_storage->storage, &storage_view->payload)) { + return AWS_OP_ERR; + } + + storage_view->qos = publish_options->qos; + storage_view->retain = publish_options->retain; + storage_view->duplicate = publish_options->duplicate; + + storage_view->topic = publish_options->topic; + if (aws_byte_buf_append_and_update(&publish_storage->storage, &storage_view->topic)) { + return AWS_OP_ERR; + } + + if (publish_options->payload_format != NULL) { + publish_storage->payload_format = *publish_options->payload_format; + storage_view->payload_format = &publish_storage->payload_format; + } + + if (publish_options->message_expiry_interval_seconds != NULL) { + publish_storage->message_expiry_interval_seconds = *publish_options->message_expiry_interval_seconds; + storage_view->message_expiry_interval_seconds = &publish_storage->message_expiry_interval_seconds; + } + + if (publish_options->topic_alias != NULL) { + publish_storage->topic_alias = *publish_options->topic_alias; + storage_view->topic_alias = &publish_storage->topic_alias; + } + + if (publish_options->response_topic != NULL) { + publish_storage->response_topic = *publish_options->response_topic; + if (aws_byte_buf_append_and_update(&publish_storage->storage, &publish_storage->response_topic)) { + return AWS_OP_ERR; + } + + storage_view->response_topic = &publish_storage->response_topic; + } + + if (publish_options->correlation_data != NULL) { + publish_storage->correlation_data = *publish_options->correlation_data; + if (aws_byte_buf_append_and_update(&publish_storage->storage, &publish_storage->correlation_data)) { + return AWS_OP_ERR; + } + + storage_view->correlation_data = &publish_storage->correlation_data; + } + + for (size_t i = 0; i < publish_options->subscription_identifier_count; ++i) { + aws_array_list_push_back( + &publish_storage->subscription_identifiers, &publish_options->subscription_identifiers[i]); + } + + storage_view->subscription_identifier_count = aws_array_list_length(&publish_storage->subscription_identifiers); + storage_view->subscription_identifiers = publish_storage->subscription_identifiers.data; + + if (publish_options->content_type != NULL) { + publish_storage->content_type = *publish_options->content_type; + if (aws_byte_buf_append_and_update(&publish_storage->storage, &publish_storage->content_type)) { + return AWS_OP_ERR; + } + + storage_view->content_type = &publish_storage->content_type; + } + + if (aws_mqtt5_user_property_set_init_with_storage( + &publish_storage->user_properties, + allocator, + &publish_storage->storage, + publish_options->user_property_count, + publish_options->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&publish_storage->user_properties); + storage_view->user_properties = publish_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_publish_storage_init_from_external_storage( + struct aws_mqtt5_packet_publish_storage *publish_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*publish_storage); + + if (aws_mqtt5_user_property_set_init(&publish_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic(&publish_storage->subscription_identifiers, allocator, 0, sizeof(uint32_t))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_publish_storage_clean_up(struct aws_mqtt5_packet_publish_storage *publish_storage) { + aws_mqtt5_user_property_set_clean_up(&publish_storage->user_properties); + aws_array_list_clean_up(&publish_storage->subscription_identifiers); + aws_byte_buf_clean_up(&publish_storage->storage); +} + +static void s_aws_mqtt5_operation_publish_complete( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *completion_view) { + struct aws_mqtt5_operation_publish *publish_op = operation->impl; + + if (publish_op->completion_options.completion_callback != NULL) { + (*publish_op->completion_options.completion_callback)( + packet_type, completion_view, error_code, publish_op->completion_options.completion_user_data); + } +} + +static void s_aws_mqtt5_operation_publish_set_packet_id( + struct aws_mqtt5_operation *operation, + aws_mqtt5_packet_id_t packet_id) { + struct aws_mqtt5_operation_publish *publish_op = operation->impl; + publish_op->options_storage.storage_view.packet_id = packet_id; +} + +static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_publish_get_packet_id_address( + const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_publish *publish_op = operation->impl; + return &publish_op->options_storage.storage_view.packet_id; +} + +static struct aws_mqtt5_operation_vtable s_publish_operation_vtable = { + .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_publish_complete, + .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_publish_set_packet_id, + .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_publish_get_packet_id_address, + .aws_mqtt5_operation_validate_vs_connection_settings_fn = + s_aws_mqtt5_packet_publish_view_validate_vs_connection_settings, +}; + +static void s_destroy_operation_publish(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_publish *publish_op = object; + + aws_mqtt5_packet_publish_storage_clean_up(&publish_op->options_storage); + + aws_mem_release(publish_op->allocator, publish_op); +} + +struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_publish_view *publish_options, + const struct aws_mqtt5_publish_completion_options *completion_options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(publish_options != NULL); + + if (aws_mqtt5_packet_publish_view_validate(publish_options)) { + return NULL; + } + + if (publish_options->packet_id != 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view packet id must be zero", + (void *)publish_options); + aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + return NULL; + } + + if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + if (aws_mqtt5_packet_publish_view_validate_vs_iot_core(publish_options)) { + return NULL; + } + } + + struct aws_mqtt5_operation_publish *publish_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_publish)); + if (publish_op == NULL) { + return NULL; + } + + publish_op->allocator = allocator; + publish_op->base.vtable = &s_publish_operation_vtable; + publish_op->base.packet_type = AWS_MQTT5_PT_PUBLISH; + aws_ref_count_init(&publish_op->base.ref_count, publish_op, s_destroy_operation_publish); + publish_op->base.impl = publish_op; + + if (aws_mqtt5_packet_publish_storage_init(&publish_op->options_storage, allocator, publish_options)) { + goto error; + } + + publish_op->base.packet_view = &publish_op->options_storage.storage_view; + + if (completion_options != NULL) { + publish_op->completion_options = *completion_options; + } + + return publish_op; + +error: + + aws_mqtt5_operation_release(&publish_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Puback + ********************************************************************************************************************/ + +static size_t s_aws_mqtt5_packet_puback_compute_storage_size(const struct aws_mqtt5_packet_puback_view *puback_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + puback_view->user_properties, puback_view->user_property_count); + + if (puback_view->reason_string != NULL) { + storage_size += puback_view->reason_string->len; + } + + return storage_size; +} + +AWS_MQTT_API int aws_mqtt5_packet_puback_storage_init( + struct aws_mqtt5_packet_puback_storage *puback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_puback_view *puback_view) { + AWS_ZERO_STRUCT(*puback_storage); + size_t storage_capacity = s_aws_mqtt5_packet_puback_compute_storage_size(puback_view); + if (aws_byte_buf_init(&puback_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_puback_view *storage_view = &puback_storage->storage_view; + + storage_view->packet_id = puback_view->packet_id; + storage_view->reason_code = puback_view->reason_code; + + if (puback_view->reason_string != NULL) { + puback_storage->reason_string = *puback_view->reason_string; + if (aws_byte_buf_append_and_update(&puback_storage->storage, &puback_storage->reason_string)) { + return AWS_OP_ERR; + } + + storage_view->reason_string = &puback_storage->reason_string; + } + + if (aws_mqtt5_user_property_set_init_with_storage( + &puback_storage->user_properties, + allocator, + &puback_storage->storage, + puback_view->user_property_count, + puback_view->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&puback_storage->user_properties); + storage_view->user_properties = puback_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_puback_storage_init_from_external_storage( + struct aws_mqtt5_packet_puback_storage *puback_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*puback_storage); + + if (aws_mqtt5_user_property_set_init(&puback_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_puback_storage_clean_up(struct aws_mqtt5_packet_puback_storage *puback_storage) { + + if (puback_storage == NULL) { + return; + } + + aws_mqtt5_user_property_set_clean_up(&puback_storage->user_properties); + + aws_byte_buf_clean_up(&puback_storage->storage); +} + +void aws_mqtt5_packet_puback_view_log( + const struct aws_mqtt5_packet_puback_view *puback_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_puback_view packet id set to %d", + (void *)puback_view, + (int)puback_view->packet_id); + + enum aws_mqtt5_puback_reason_code reason_code = puback_view->reason_code; + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: puback %d reason code: %s", + (void *)puback_view, + (int)reason_code, + aws_mqtt5_puback_reason_code_to_c_string(reason_code)); + + if (puback_view->reason_string != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_puback_view reason string set to \"" PRInSTR "\"", + (void *)puback_view, + AWS_BYTE_CURSOR_PRI(*puback_view->reason_string)); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + puback_view->user_properties, + puback_view->user_property_count, + (void *)puback_view, + level, + "aws_mqtt5_packet_puback_view"); +} + +static void s_destroy_operation_puback(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_puback *puback_op = object; + + aws_mqtt5_packet_puback_storage_clean_up(&puback_op->options_storage); + + aws_mem_release(puback_op->allocator, puback_op); +} + +struct aws_mqtt5_operation_puback *aws_mqtt5_operation_puback_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_puback_view *puback_options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(puback_options != NULL); + + struct aws_mqtt5_operation_puback *puback_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_puback)); + if (puback_op == NULL) { + return NULL; + } + + puback_op->allocator = allocator; + puback_op->base.vtable = &s_empty_operation_vtable; + puback_op->base.packet_type = AWS_MQTT5_PT_PUBACK; + aws_ref_count_init(&puback_op->base.ref_count, puback_op, s_destroy_operation_puback); + puback_op->base.impl = puback_op; + + if (aws_mqtt5_packet_puback_storage_init(&puback_op->options_storage, allocator, puback_options)) { + goto error; + } + + puback_op->base.packet_view = &puback_op->options_storage.storage_view; + + return puback_op; + +error: + + aws_mqtt5_operation_release(&puback_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Unsubscribe + ********************************************************************************************************************/ + +int aws_mqtt5_packet_unsubscribe_view_validate(const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view) { + + if (unsubscribe_view == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "null UNSUBSCRIBE packet options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (unsubscribe_view->topic_filter_count == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view - must contain at least one topic", + (void *)unsubscribe_view); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } + + if (unsubscribe_view->topic_filter_count > AWS_MQTT5_CLIENT_MAXIMUM_TOPIC_FILTERS_PER_UNSUBSCRIBE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view - contains too many topics (%zu)", + (void *)unsubscribe_view, + unsubscribe_view->topic_filter_count); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } + + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + const struct aws_byte_cursor *topic_filter = &unsubscribe_view->topic_filters[i]; + if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view - invalid topic filter: \"" PRInSTR "\"", + (void *)unsubscribe_view, + AWS_BYTE_CURSOR_PRI(*topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } + } + + if (s_aws_mqtt5_user_property_set_validate( + unsubscribe_view->user_properties, + unsubscribe_view->user_property_count, + "aws_mqtt5_packet_unsubscribe_view", + (void *)unsubscribe_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view) { + + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + const struct aws_byte_cursor *topic_filter = &unsubscribe_view->topic_filters[i]; + if (!aws_mqtt_is_valid_topic_filter_for_iot_core(*topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view - topic filter not valid for AWS Iot Core limits: \"" PRInSTR + "\"", + (void *)unsubscribe_view, + AWS_BYTE_CURSOR_PRI(*topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_unsubscribe_view_log( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + size_t topic_count = unsubscribe_view->topic_filter_count; + for (size_t i = 0; i < topic_count; ++i) { + const struct aws_byte_cursor *topic_cursor = &unsubscribe_view->topic_filters[i]; + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view topic #%zu: \"" PRInSTR "\"", + (void *)unsubscribe_view, + i, + AWS_BYTE_CURSOR_PRI(*topic_cursor)); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + unsubscribe_view->user_properties, + unsubscribe_view->user_property_count, + (void *)unsubscribe_view, + level, + "aws_mqtt5_packet_unsubscribe_view"); +} + +void aws_mqtt5_packet_unsubscribe_storage_clean_up(struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage) { + if (unsubscribe_storage == NULL) { + return; + } + + aws_array_list_clean_up(&unsubscribe_storage->topic_filters); + aws_mqtt5_user_property_set_clean_up(&unsubscribe_storage->user_properties); + aws_byte_buf_clean_up(&unsubscribe_storage->storage); +} + +static int s_aws_mqtt5_packet_unsubscribe_build_topic_list( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_allocator *allocator, + size_t topic_count, + const struct aws_byte_cursor *topics) { + + if (aws_array_list_init_dynamic( + &unsubscribe_storage->topic_filters, allocator, topic_count, sizeof(struct aws_byte_cursor))) { + return AWS_OP_ERR; + } + + for (size_t i = 0; i < topic_count; ++i) { + const struct aws_byte_cursor *topic_cursor_ptr = &topics[i]; + struct aws_byte_cursor topic_cursor = *topic_cursor_ptr; + + if (aws_byte_buf_append_and_update(&unsubscribe_storage->storage, &topic_cursor)) { + return AWS_OP_ERR; + } + + if (aws_array_list_push_back(&unsubscribe_storage->topic_filters, &topic_cursor)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +static size_t s_aws_mqtt5_packet_unsubscribe_compute_storage_size( + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + unsubscribe_view->user_properties, unsubscribe_view->user_property_count); + + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + const struct aws_byte_cursor *topic = &unsubscribe_view->topic_filters[i]; + storage_size += topic->len; + } + + return storage_size; +} + +int aws_mqtt5_packet_unsubscribe_storage_init( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options) { + + AWS_ZERO_STRUCT(*unsubscribe_storage); + size_t storage_capacity = s_aws_mqtt5_packet_unsubscribe_compute_storage_size(unsubscribe_options); + if (aws_byte_buf_init(&unsubscribe_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_unsubscribe_view *storage_view = &unsubscribe_storage->storage_view; + + if (s_aws_mqtt5_packet_unsubscribe_build_topic_list( + unsubscribe_storage, + allocator, + unsubscribe_options->topic_filter_count, + unsubscribe_options->topic_filters)) { + return AWS_OP_ERR; + } + storage_view->topic_filter_count = aws_array_list_length(&unsubscribe_storage->topic_filters); + storage_view->topic_filters = unsubscribe_storage->topic_filters.data; + + if (aws_mqtt5_user_property_set_init_with_storage( + &unsubscribe_storage->user_properties, + allocator, + &unsubscribe_storage->storage, + unsubscribe_options->user_property_count, + unsubscribe_options->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&unsubscribe_storage->user_properties); + storage_view->user_properties = unsubscribe_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_unsubscribe_storage_init_from_external_storage( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*unsubscribe_storage); + + if (aws_mqtt5_user_property_set_init(&unsubscribe_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic( + &unsubscribe_storage->topic_filters, allocator, 0, sizeof(struct aws_byte_cursor))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_operation_unsubscribe_complete( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *completion_view) { + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = operation->impl; + (void)packet_type; + + if (unsubscribe_op->completion_options.completion_callback != NULL) { + (*unsubscribe_op->completion_options.completion_callback)( + completion_view, error_code, unsubscribe_op->completion_options.completion_user_data); + } +} + +static void s_aws_mqtt5_operation_unsubscribe_set_packet_id( + struct aws_mqtt5_operation *operation, + aws_mqtt5_packet_id_t packet_id) { + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = operation->impl; + unsubscribe_op->options_storage.storage_view.packet_id = packet_id; +} + +static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_unsubscribe_get_packet_id_address( + const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = operation->impl; + return &unsubscribe_op->options_storage.storage_view.packet_id; +} + +static struct aws_mqtt5_operation_vtable s_unsubscribe_operation_vtable = { + .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_unsubscribe_complete, + .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_unsubscribe_set_packet_id, + .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_unsubscribe_get_packet_id_address, + .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, +}; + +static void s_destroy_operation_unsubscribe(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = object; + + aws_mqtt5_packet_unsubscribe_storage_clean_up(&unsubscribe_op->options_storage); + + aws_mem_release(unsubscribe_op->allocator, unsubscribe_op); +} + +struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubscribe_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options, + const struct aws_mqtt5_unsubscribe_completion_options *completion_options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(unsubscribe_options != NULL); + + if (aws_mqtt5_packet_unsubscribe_view_validate(unsubscribe_options)) { + return NULL; + } + + if (unsubscribe_options->packet_id != 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view packet id must be zero", + (void *)unsubscribe_options); + aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + return NULL; + } + + if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + if (aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core(unsubscribe_options)) { + return NULL; + } + } + + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_unsubscribe)); + if (unsubscribe_op == NULL) { + return NULL; + } + + unsubscribe_op->allocator = allocator; + unsubscribe_op->base.vtable = &s_unsubscribe_operation_vtable; + unsubscribe_op->base.packet_type = AWS_MQTT5_PT_UNSUBSCRIBE; + aws_ref_count_init(&unsubscribe_op->base.ref_count, unsubscribe_op, s_destroy_operation_unsubscribe); + unsubscribe_op->base.impl = unsubscribe_op; + + if (aws_mqtt5_packet_unsubscribe_storage_init(&unsubscribe_op->options_storage, allocator, unsubscribe_options)) { + goto error; + } + + unsubscribe_op->base.packet_view = &unsubscribe_op->options_storage.storage_view; + + if (completion_options != NULL) { + unsubscribe_op->completion_options = *completion_options; + } + + return unsubscribe_op; + +error: + + aws_mqtt5_operation_release(&unsubscribe_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Subscribe + ********************************************************************************************************************/ + +static int s_aws_mqtt5_validate_subscription( + const struct aws_mqtt5_subscription_view *subscription, + void *log_context) { + + if (!aws_mqtt_is_valid_topic_filter(&subscription->topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - invalid topic filter \"" PRInSTR "\" in subscription", + log_context, + AWS_BYTE_CURSOR_PRI(subscription->topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + if (subscription->topic_filter.len > UINT16_MAX) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - subscription contains too-long topic filter", + log_context); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + if (subscription->qos < AWS_MQTT5_QOS_AT_MOST_ONCE || subscription->qos > AWS_MQTT5_QOS_AT_LEAST_ONCE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - unsupported QoS value: %d", + log_context, + (int)subscription->qos); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + if (subscription->retain_handling_type < AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE || + subscription->retain_handling_type > AWS_MQTT5_RHT_DONT_SEND) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - unsupported retain handling value: %d", + log_context, + (int)subscription->retain_handling_type); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + /* mqtt5 forbids no_local to be set to 1 if the topic filter represents a shared subscription */ + if (subscription->no_local) { + if (aws_mqtt_is_topic_filter_shared_subscription(subscription->topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - no_local cannot be 1 if the topic filter is a shared" + "subscription", + log_context); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_subscribe_view_validate(const struct aws_mqtt5_packet_subscribe_view *subscribe_view) { + + if (subscribe_view == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "null SUBSCRIBE packet options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (subscribe_view->subscription_count == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - must contain at least one subscription", + (void *)subscribe_view); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + if (subscribe_view->subscription_count > AWS_MQTT5_CLIENT_MAXIMUM_SUBSCRIPTIONS_PER_SUBSCRIBE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - too many subscriptions", + (void *)subscribe_view); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *subscription = &subscribe_view->subscriptions[i]; + if (s_aws_mqtt5_validate_subscription(subscription, (void *)subscribe_view)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - invalid subscription", + (void *)subscribe_view); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + } + + if (subscribe_view->subscription_identifier != NULL) { + if (*subscribe_view->subscription_identifier > AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - subscription identifier (%" PRIu32 ") too large", + (void *)subscribe_view, + *subscribe_view->subscription_identifier); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + } + + if (s_aws_mqtt5_user_property_set_validate( + subscribe_view->user_properties, + subscribe_view->user_property_count, + "aws_mqtt5_packet_subscribe_view", + (void *)subscribe_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate_vs_iot_core( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view) { + + if (subscribe_view->subscription_count > AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - number of subscriptions (%zu) exceeds default AWS IoT Core limit " + "(%d)", + (void *)subscribe_view, + subscribe_view->subscription_count, + (int)AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE); + return AWS_OP_ERR; + } + + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *subscription = &subscribe_view->subscriptions[i]; + const struct aws_byte_cursor *topic_filter = &subscription->topic_filter; + if (!aws_mqtt_is_valid_topic_filter_for_iot_core(*topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - topic filter not valid for AWS Iot Core limits: \"" PRInSTR + "\"", + (void *)subscribe_view, + AWS_BYTE_CURSOR_PRI(*topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_subscribe_view_log( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + size_t subscription_count = subscribe_view->subscription_count; + for (size_t i = 0; i < subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *view = &subscribe_view->subscriptions[i]; + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view subscription #%zu, topic filter \"" PRInSTR + "\", qos %d, no local %d, retain as " + "published %d, retain handling %d (%s)", + (void *)subscribe_view, + i, + AWS_BYTE_CURSOR_PRI(view->topic_filter), + (int)view->qos, + (int)view->no_local, + (int)view->retain_as_published, + (int)view->retain_handling_type, + aws_mqtt5_retain_handling_type_to_c_string(view->retain_handling_type)); + } + + if (subscribe_view->subscription_identifier != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view subscription identifier set to %" PRIu32, + (void *)subscribe_view, + *subscribe_view->subscription_identifier); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + subscribe_view->user_properties, + subscribe_view->user_property_count, + (void *)subscribe_view, + level, + "aws_mqtt5_packet_subscribe_view"); +} + +void aws_mqtt5_packet_subscribe_storage_clean_up(struct aws_mqtt5_packet_subscribe_storage *subscribe_storage) { + if (subscribe_storage == NULL) { + return; + } + + aws_array_list_clean_up(&subscribe_storage->subscriptions); + + aws_mqtt5_user_property_set_clean_up(&subscribe_storage->user_properties); + aws_byte_buf_clean_up(&subscribe_storage->storage); +} + +static int s_aws_mqtt5_packet_subscribe_storage_init_subscriptions( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_allocator *allocator, + size_t subscription_count, + const struct aws_mqtt5_subscription_view *subscriptions) { + + if (aws_array_list_init_dynamic( + &subscribe_storage->subscriptions, + allocator, + subscription_count, + sizeof(struct aws_mqtt5_subscription_view))) { + return AWS_OP_ERR; + } + + for (size_t i = 0; i < subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *source = &subscriptions[i]; + struct aws_mqtt5_subscription_view copy = *source; + + if (aws_byte_buf_append_and_update(&subscribe_storage->storage, ©.topic_filter)) { + return AWS_OP_ERR; + } + + if (aws_array_list_push_back(&subscribe_storage->subscriptions, ©)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +static size_t s_aws_mqtt5_packet_subscribe_compute_storage_size( + const struct aws_mqtt5_packet_subscribe_view *subscribe_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + subscribe_view->user_properties, subscribe_view->user_property_count); + + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *subscription = &subscribe_view->subscriptions[i]; + storage_size += subscription->topic_filter.len; + } + + return storage_size; +} + +int aws_mqtt5_packet_subscribe_storage_init( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options) { + + AWS_ZERO_STRUCT(*subscribe_storage); + size_t storage_capacity = s_aws_mqtt5_packet_subscribe_compute_storage_size(subscribe_options); + if (aws_byte_buf_init(&subscribe_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_subscribe_view *storage_view = &subscribe_storage->storage_view; + storage_view->packet_id = subscribe_options->packet_id; + + if (subscribe_options->subscription_identifier != NULL) { + subscribe_storage->subscription_identifier = *subscribe_options->subscription_identifier; + storage_view->subscription_identifier = &subscribe_storage->subscription_identifier; + } + + if (s_aws_mqtt5_packet_subscribe_storage_init_subscriptions( + subscribe_storage, allocator, subscribe_options->subscription_count, subscribe_options->subscriptions)) { + return AWS_OP_ERR; + } + storage_view->subscription_count = aws_array_list_length(&subscribe_storage->subscriptions); + storage_view->subscriptions = subscribe_storage->subscriptions.data; + + if (aws_mqtt5_user_property_set_init_with_storage( + &subscribe_storage->user_properties, + allocator, + &subscribe_storage->storage, + subscribe_options->user_property_count, + subscribe_options->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&subscribe_storage->user_properties); + storage_view->user_properties = subscribe_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_subscribe_storage_init_from_external_storage( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*subscribe_storage); + + if (aws_mqtt5_user_property_set_init(&subscribe_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic( + &subscribe_storage->subscriptions, allocator, 0, sizeof(struct aws_mqtt5_subscription_view))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_operation_subscribe_complete( + struct aws_mqtt5_operation *operation, + int error_code, + enum aws_mqtt5_packet_type packet_type, + const void *completion_view) { + (void)packet_type; + + struct aws_mqtt5_operation_subscribe *subscribe_op = operation->impl; + + if (subscribe_op->completion_options.completion_callback != NULL) { + (*subscribe_op->completion_options.completion_callback)( + completion_view, error_code, subscribe_op->completion_options.completion_user_data); + } +} + +static void s_aws_mqtt5_operation_subscribe_set_packet_id( + struct aws_mqtt5_operation *operation, + aws_mqtt5_packet_id_t packet_id) { + struct aws_mqtt5_operation_subscribe *subscribe_op = operation->impl; + subscribe_op->options_storage.storage_view.packet_id = packet_id; +} + +static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_subscribe_get_packet_id_address( + const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_subscribe *subscribe_op = operation->impl; + return &subscribe_op->options_storage.storage_view.packet_id; +} + +static struct aws_mqtt5_operation_vtable s_subscribe_operation_vtable = { + .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_subscribe_complete, + .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_subscribe_set_packet_id, + .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_subscribe_get_packet_id_address, + .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, +}; + +static void s_destroy_operation_subscribe(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_subscribe *subscribe_op = object; + + aws_mqtt5_packet_subscribe_storage_clean_up(&subscribe_op->options_storage); + + aws_mem_release(subscribe_op->allocator, subscribe_op); +} + +struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client *client, + const struct aws_mqtt5_packet_subscribe_view *subscribe_options, + const struct aws_mqtt5_subscribe_completion_options *completion_options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(subscribe_options != NULL); + + if (aws_mqtt5_packet_subscribe_view_validate(subscribe_options)) { + return NULL; + } + + if (subscribe_options->packet_id != 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view packet id must be zero", + (void *)subscribe_options); + aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + return NULL; + } + + if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + if (aws_mqtt5_packet_subscribe_view_validate_vs_iot_core(subscribe_options)) { + return NULL; + } + } + + struct aws_mqtt5_operation_subscribe *subscribe_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_subscribe)); + if (subscribe_op == NULL) { + return NULL; + } + + subscribe_op->allocator = allocator; + subscribe_op->base.vtable = &s_subscribe_operation_vtable; + subscribe_op->base.packet_type = AWS_MQTT5_PT_SUBSCRIBE; + aws_ref_count_init(&subscribe_op->base.ref_count, subscribe_op, s_destroy_operation_subscribe); + subscribe_op->base.impl = subscribe_op; + + if (aws_mqtt5_packet_subscribe_storage_init(&subscribe_op->options_storage, allocator, subscribe_options)) { + goto error; + } + + subscribe_op->base.packet_view = &subscribe_op->options_storage.storage_view; + + if (completion_options != NULL) { + subscribe_op->completion_options = *completion_options; + } + + return subscribe_op; + +error: + + aws_mqtt5_operation_release(&subscribe_op->base); + + return NULL; +} + +/********************************************************************************************************************* + * Suback + ********************************************************************************************************************/ + +static size_t s_aws_mqtt5_packet_suback_compute_storage_size(const struct aws_mqtt5_packet_suback_view *suback_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + suback_view->user_properties, suback_view->user_property_count); + + if (suback_view->reason_string != NULL) { + storage_size += suback_view->reason_string->len; + } + + return storage_size; +} + +static int s_aws_mqtt5_packet_suback_storage_init_reason_codes( + struct aws_mqtt5_packet_suback_storage *suback_storage, + struct aws_allocator *allocator, + size_t reason_code_count, + const enum aws_mqtt5_suback_reason_code *reason_codes) { + + if (aws_array_list_init_dynamic( + &suback_storage->reason_codes, allocator, reason_code_count, sizeof(enum aws_mqtt5_suback_reason_code))) { + return AWS_OP_ERR; + } + + for (size_t i = 0; i < reason_code_count; ++i) { + aws_array_list_push_back(&suback_storage->reason_codes, &reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +AWS_MQTT_API int aws_mqtt5_packet_suback_storage_init( + struct aws_mqtt5_packet_suback_storage *suback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_suback_view *suback_view) { + AWS_ZERO_STRUCT(*suback_storage); + size_t storage_capacity = s_aws_mqtt5_packet_suback_compute_storage_size(suback_view); + if (aws_byte_buf_init(&suback_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_suback_view *storage_view = &suback_storage->storage_view; + + storage_view->packet_id = suback_view->packet_id; + + if (suback_view->reason_string != NULL) { + suback_storage->reason_string = *suback_view->reason_string; + if (aws_byte_buf_append_and_update(&suback_storage->storage, &suback_storage->reason_string)) { + return AWS_OP_ERR; + } + + storage_view->reason_string = &suback_storage->reason_string; + } + + if (s_aws_mqtt5_packet_suback_storage_init_reason_codes( + suback_storage, allocator, suback_view->reason_code_count, suback_view->reason_codes)) { + return AWS_OP_ERR; + } + storage_view->reason_code_count = aws_array_list_length(&suback_storage->reason_codes); + storage_view->reason_codes = suback_storage->reason_codes.data; + + if (aws_mqtt5_user_property_set_init_with_storage( + &suback_storage->user_properties, + allocator, + &suback_storage->storage, + suback_view->user_property_count, + suback_view->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&suback_storage->user_properties); + storage_view->user_properties = suback_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_suback_storage_init_from_external_storage( + struct aws_mqtt5_packet_suback_storage *suback_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*suback_storage); + + if (aws_mqtt5_user_property_set_init(&suback_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic( + &suback_storage->reason_codes, allocator, 0, sizeof(enum aws_mqtt5_suback_reason_code))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_suback_storage_clean_up(struct aws_mqtt5_packet_suback_storage *suback_storage) { + if (suback_storage == NULL) { + return; + } + aws_mqtt5_user_property_set_clean_up(&suback_storage->user_properties); + + aws_array_list_clean_up(&suback_storage->reason_codes); + + aws_byte_buf_clean_up(&suback_storage->storage); +} + +void aws_mqtt5_packet_suback_view_log( + const struct aws_mqtt5_packet_suback_view *suback_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_suback_view packet id set to %d", + (void *)suback_view, + (int)suback_view->packet_id); + + for (size_t i = 0; i < suback_view->reason_code_count; ++i) { + enum aws_mqtt5_suback_reason_code reason_code = suback_view->reason_codes[i]; + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_suback_view topic #%zu, reason code %d (%s)", + (void *)suback_view, + i, + (int)reason_code, + aws_mqtt5_suback_reason_code_to_c_string(reason_code)); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + suback_view->user_properties, + suback_view->user_property_count, + (void *)suback_view, + level, + "aws_mqtt5_packet_suback_view"); +} + +/********************************************************************************************************************* + * Unsuback + ********************************************************************************************************************/ + +static size_t s_aws_mqtt5_packet_unsuback_compute_storage_size( + const struct aws_mqtt5_packet_unsuback_view *unsuback_view) { + size_t storage_size = s_aws_mqtt5_user_property_set_compute_storage_size( + unsuback_view->user_properties, unsuback_view->user_property_count); + + if (unsuback_view->reason_string != NULL) { + storage_size += unsuback_view->reason_string->len; + } + + return storage_size; +} + +static int s_aws_mqtt5_packet_unsuback_storage_init_reason_codes( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + struct aws_allocator *allocator, + size_t reason_code_count, + const enum aws_mqtt5_unsuback_reason_code *reason_codes) { + + if (aws_array_list_init_dynamic( + &unsuback_storage->reason_codes, + allocator, + reason_code_count, + sizeof(enum aws_mqtt5_unsuback_reason_code))) { + return AWS_OP_ERR; + } + + for (size_t i = 0; i < reason_code_count; ++i) { + aws_array_list_push_back(&unsuback_storage->reason_codes, &reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +AWS_MQTT_API int aws_mqtt5_packet_unsuback_storage_init( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + struct aws_allocator *allocator, + const struct aws_mqtt5_packet_unsuback_view *unsuback_view) { + AWS_ZERO_STRUCT(*unsuback_storage); + size_t storage_capacity = s_aws_mqtt5_packet_unsuback_compute_storage_size(unsuback_view); + if (aws_byte_buf_init(&unsuback_storage->storage, allocator, storage_capacity)) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_unsuback_view *storage_view = &unsuback_storage->storage_view; + + storage_view->packet_id = unsuback_view->packet_id; + + if (unsuback_view->reason_string != NULL) { + unsuback_storage->reason_string = *unsuback_view->reason_string; + if (aws_byte_buf_append_and_update(&unsuback_storage->storage, &unsuback_storage->reason_string)) { + return AWS_OP_ERR; + } + + storage_view->reason_string = &unsuback_storage->reason_string; + } + + if (s_aws_mqtt5_packet_unsuback_storage_init_reason_codes( + unsuback_storage, allocator, unsuback_view->reason_code_count, unsuback_view->reason_codes)) { + return AWS_OP_ERR; + } + storage_view->reason_code_count = aws_array_list_length(&unsuback_storage->reason_codes); + storage_view->reason_codes = unsuback_storage->reason_codes.data; + + if (aws_mqtt5_user_property_set_init_with_storage( + &unsuback_storage->user_properties, + allocator, + &unsuback_storage->storage, + unsuback_view->user_property_count, + unsuback_view->user_properties)) { + return AWS_OP_ERR; + } + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&unsuback_storage->user_properties); + storage_view->user_properties = unsuback_storage->user_properties.properties.data; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_packet_unsuback_storage_init_from_external_storage( + struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*unsuback_storage); + + if (aws_mqtt5_user_property_set_init(&unsuback_storage->user_properties, allocator)) { + return AWS_OP_ERR; + } + + if (aws_array_list_init_dynamic( + &unsuback_storage->reason_codes, allocator, 0, sizeof(enum aws_mqtt5_unsuback_reason_code))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_packet_unsuback_storage_clean_up(struct aws_mqtt5_packet_unsuback_storage *unsuback_storage) { + if (unsuback_storage == NULL) { + return; + } + aws_mqtt5_user_property_set_clean_up(&unsuback_storage->user_properties); + + aws_array_list_clean_up(&unsuback_storage->reason_codes); + + aws_byte_buf_clean_up(&unsuback_storage->storage); +} + +void aws_mqtt5_packet_unsuback_view_log( + const struct aws_mqtt5_packet_unsuback_view *unsuback_view, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsuback_view packet id set to %d", + (void *)unsuback_view, + (int)unsuback_view->packet_id); + + for (size_t i = 0; i < unsuback_view->reason_code_count; ++i) { + enum aws_mqtt5_unsuback_reason_code reason_code = unsuback_view->reason_codes[i]; + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsuback_view topic #%zu, reason code %d (%s)", + (void *)unsuback_view, + i, + (int)reason_code, + aws_mqtt5_unsuback_reason_code_to_c_string(reason_code)); + } + + s_aws_mqtt5_user_property_set_log( + log_handle, + unsuback_view->user_properties, + unsuback_view->user_property_count, + (void *)unsuback_view, + level, + "aws_mqtt5_packet_unsuback_view"); +} + +/********************************************************************************************************************* + * PINGREQ + ********************************************************************************************************************/ + +static void s_destroy_operation_pingreq(void *object) { + if (object == NULL) { + return; + } + + struct aws_mqtt5_operation_pingreq *pingreq_op = object; + aws_mem_release(pingreq_op->allocator, pingreq_op); +} + +struct aws_mqtt5_operation_pingreq *aws_mqtt5_operation_pingreq_new(struct aws_allocator *allocator) { + struct aws_mqtt5_operation_pingreq *pingreq_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_pingreq)); + if (pingreq_op == NULL) { + return NULL; + } + + pingreq_op->allocator = allocator; + pingreq_op->base.vtable = &s_empty_operation_vtable; + pingreq_op->base.packet_type = AWS_MQTT5_PT_PINGREQ; + aws_ref_count_init(&pingreq_op->base.ref_count, pingreq_op, s_destroy_operation_pingreq); + pingreq_op->base.impl = pingreq_op; + + return pingreq_op; +} + +/********************************************************************************************************************* + * Client storage options + ********************************************************************************************************************/ + +int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *options) { + if (options == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "null mqtt5 client configuration options"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (options->host_name.len == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "host name not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (options->bootstrap == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "client bootstrap not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + /* forbid no-timeout until someone convinces me otherwise */ + if (options->socket_options != NULL) { + if (options->socket_options->type == AWS_SOCKET_DGRAM || options->socket_options->connect_timeout_ms == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid socket options in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + + if (options->http_proxy_options != NULL) { + if (options->http_proxy_options->host.len == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "proxy host name not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (options->http_proxy_options->port == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "proxy port not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + + /* can't think of why you'd ever want an MQTT client without lifecycle event notifications */ + if (options->lifecycle_event_handler == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "lifecycle event handler not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (options->publish_received_handler == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "publish received not set in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (aws_mqtt5_packet_connect_view_validate(options->connect_options)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid CONNECT options in mqtt5 client configuration"); + return AWS_OP_ERR; + } + + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (options->connect_options->keep_alive_interval_seconds > 0) { + uint64_t keep_alive_ms = aws_timestamp_convert( + options->connect_options->keep_alive_interval_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); + uint64_t one_second_ms = aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); + + uint64_t ping_timeout_ms = options->ping_timeout_ms; + if (ping_timeout_ms == 0) { + ping_timeout_ms = AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS; + } + + if (ping_timeout_ms + one_second_ms > keep_alive_ms) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); + return AWS_OP_ERR; + } + } + + if (options->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { + if (options->connect_options->client_id.len > AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "AWS IoT Core limits client_id to be less than or equal to %d bytes in length", + (int)AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + + return AWS_OP_SUCCESS; +} + +static void s_log_tls_connection_options( + struct aws_logger *log_handle, + const struct aws_mqtt5_client_options_storage *options_storage, + const struct aws_tls_connection_options *tls_options, + enum aws_log_level level, + const char *log_text) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options set:", + (void *)options_storage, + log_text); + + if (tls_options->advertise_alpn_message && tls_options->alpn_list) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options alpn protocol list set to \"%s\"", + (void *)options_storage, + log_text, + aws_string_c_str(tls_options->alpn_list)); + } else { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options alpn not used", + (void *)options_storage, + log_text); + } + + if (tls_options->server_name) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options SNI value set to \"%s\"", + (void *)options_storage, + log_text, + aws_string_c_str(tls_options->server_name)); + } else { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options SNI not used", + (void *)options_storage, + log_text); + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options tls context set to (%p)", + (void *)options_storage, + log_text, + (void *)(tls_options->ctx)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage %s tls options handshake timeout set to %" PRIu32, + (void *)options_storage, + log_text, + tls_options->timeout_ms); +} + +static void s_log_topic_aliasing_options( + struct aws_logger *log_handle, + const struct aws_mqtt5_client_options_storage *options_storage, + const struct aws_mqtt5_client_topic_alias_options *topic_aliasing_options, + enum aws_log_level level) { + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage outbound topic aliasing behavior set to %d (%s)", + (void *)options_storage, + (int)topic_aliasing_options->outbound_topic_alias_behavior, + aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( + topic_aliasing_options->outbound_topic_alias_behavior)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage maximum outbound topic alias cache size set to %" PRIu16, + (void *)options_storage, + topic_aliasing_options->outbound_alias_cache_max_size); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage inbound topic aliasing behavior set to %d (%s)", + (void *)options_storage, + (int)topic_aliasing_options->inbound_topic_alias_behavior, + aws_mqtt5_inbound_topic_alias_behavior_type_to_c_string(topic_aliasing_options->inbound_topic_alias_behavior)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage inbound topic alias cache size set to %" PRIu16, + (void *)options_storage, + topic_aliasing_options->inbound_alias_cache_size); +} + +void aws_mqtt5_client_options_storage_log( + const struct aws_mqtt5_client_options_storage *options_storage, + enum aws_log_level level) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT5_GENERAL, level); + if (log_handle == NULL) { + return; + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage host name set to %s", + (void *)options_storage, + aws_string_c_str(options_storage->host_name)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage port set to %" PRIu16, + (void *)options_storage, + options_storage->port); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage client bootstrap set to (%p)", + (void *)options_storage, + (void *)options_storage->bootstrap); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage socket options set to: type = %d, domain = %d, connect_timeout_ms = " + "%" PRIu32, + (void *)options_storage, + (int)options_storage->socket_options.type, + (int)options_storage->socket_options.domain, + options_storage->socket_options.connect_timeout_ms); + + if (options_storage->socket_options.keepalive) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage socket keepalive options set to: keep_alive_interval_sec = " + "%" PRIu16 ", " + "keep_alive_timeout_sec = %" PRIu16 ", keep_alive_max_failed_probes = %" PRIu16, + (void *)options_storage, + options_storage->socket_options.keep_alive_interval_sec, + options_storage->socket_options.keep_alive_timeout_sec, + options_storage->socket_options.keep_alive_max_failed_probes); + } + + if (options_storage->tls_options_ptr != NULL) { + s_log_tls_connection_options(log_handle, options_storage, options_storage->tls_options_ptr, level, ""); + } + + if (options_storage->http_proxy_config != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage using http proxy:", + (void *)options_storage); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage http proxy host name set to " PRInSTR, + (void *)options_storage, + AWS_BYTE_CURSOR_PRI(options_storage->http_proxy_options.host)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage http proxy port set to %" PRIu16, + (void *)options_storage, + options_storage->http_proxy_options.port); + + if (options_storage->http_proxy_options.tls_options != NULL) { + s_log_tls_connection_options( + log_handle, options_storage, options_storage->tls_options_ptr, level, "http proxy"); + } + + /* ToDo: add (and use) an API to proxy strategy that returns a debug string (Basic, Adaptive, etc...) */ + if (options_storage->http_proxy_options.proxy_strategy != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage http proxy strategy set to (%p)", + (void *)options_storage, + (void *)options_storage->http_proxy_options.proxy_strategy); + } + } + + if (options_storage->websocket_handshake_transform != NULL) { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage enabling websockets", + (void *)options_storage); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage websocket handshake transform user data set to (%p)", + (void *)options_storage, + options_storage->websocket_handshake_transform_user_data); + } else { + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: mqtt5_client_options_storage disabling websockets", + (void *)options_storage); + } + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage session behavior set to %d (%s)", + (void *)options_storage, + (int)options_storage->session_behavior, + aws_mqtt5_client_session_behavior_type_to_c_string(options_storage->session_behavior)); + + s_log_topic_aliasing_options(log_handle, options_storage, &options_storage->topic_aliasing_options, level); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage extended validation and flow control options set to %d (%s)", + (void *)options_storage, + (int)options_storage->extended_validation_and_flow_control_options, + aws_mqtt5_extended_validation_and_flow_control_options_to_c_string( + options_storage->extended_validation_and_flow_control_options)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage operation queue behavior set to %d (%s)", + (void *)options_storage, + (int)options_storage->offline_queue_behavior, + aws_mqtt5_client_operation_queue_behavior_type_to_c_string(options_storage->offline_queue_behavior)); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage reconnect jitter mode set to %d", + (void *)options_storage, + (int)options_storage->retry_jitter_mode); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: mqtt5_client_options_storage reconnect delay min set to %" PRIu64 " ms, max set to %" PRIu64 " ms", + (void *)options_storage, + options_storage->min_reconnect_delay_ms, + options_storage->max_reconnect_delay_ms); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage minimum necessary connection time in order to reset the reconnect " + "delay " + "set " + "to %" PRIu64 " ms", + (void *)options_storage, + options_storage->min_connected_time_to_reset_reconnect_delay_ms); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage ping timeout interval set to %" PRIu32 " ms", + (void *)options_storage, + options_storage->ping_timeout_ms); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage connack timeout interval set to %" PRIu32 " ms", + (void *)options_storage, + options_storage->connack_timeout_ms); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage connect options:", + (void *)options_storage); + + aws_mqtt5_packet_connect_view_log(&options_storage->connect.storage_view, level); + + AWS_LOGUF( + log_handle, + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_client_options_storage lifecycle event handler user data set to (%p)", + (void *)options_storage, + options_storage->lifecycle_event_handler_user_data); +} + +void aws_mqtt5_client_options_storage_destroy(struct aws_mqtt5_client_options_storage *options_storage) { + if (options_storage == NULL) { + return; + } + + aws_string_destroy(options_storage->host_name); + aws_client_bootstrap_release(options_storage->bootstrap); + + aws_tls_connection_options_clean_up(&options_storage->tls_options); + aws_http_proxy_config_destroy(options_storage->http_proxy_config); + + aws_mqtt5_packet_connect_storage_clean_up(&options_storage->connect); + + aws_mem_release(options_storage->allocator, options_storage); +} + +static void s_apply_zero_valued_defaults_to_client_options_storage( + struct aws_mqtt5_client_options_storage *options_storage) { + if (options_storage->min_reconnect_delay_ms == 0) { + options_storage->min_reconnect_delay_ms = AWS_MQTT5_CLIENT_DEFAULT_MIN_RECONNECT_DELAY_MS; + } + + if (options_storage->max_reconnect_delay_ms == 0) { + options_storage->max_reconnect_delay_ms = AWS_MQTT5_CLIENT_DEFAULT_MAX_RECONNECT_DELAY_MS; + } + + if (options_storage->min_connected_time_to_reset_reconnect_delay_ms == 0) { + options_storage->min_connected_time_to_reset_reconnect_delay_ms = + AWS_MQTT5_CLIENT_DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS; + } + + if (options_storage->ping_timeout_ms == 0) { + options_storage->ping_timeout_ms = AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS; + } + + if (options_storage->connack_timeout_ms == 0) { + options_storage->connack_timeout_ms = AWS_MQTT5_CLIENT_DEFAULT_CONNACK_TIMEOUT_MS; + } + + if (options_storage->ack_timeout_seconds == 0) { + options_storage->ack_timeout_seconds = AWS_MQTT5_CLIENT_DEFAULT_OPERATION_TIMEOUNT_SECONDS; + } + + if (options_storage->topic_aliasing_options.inbound_alias_cache_size == 0) { + options_storage->topic_aliasing_options.inbound_alias_cache_size = + AWS_MQTT5_CLIENT_DEFAULT_INBOUND_TOPIC_ALIAS_CACHE_SIZE; + } + + if (options_storage->topic_aliasing_options.outbound_alias_cache_max_size == 0) { + options_storage->topic_aliasing_options.outbound_alias_cache_max_size = + AWS_MQTT5_CLIENT_DEFAULT_OUTBOUND_TOPIC_ALIAS_CACHE_SIZE; + } +} + +struct aws_mqtt5_client_options_storage *aws_mqtt5_client_options_storage_new( + struct aws_allocator *allocator, + const struct aws_mqtt5_client_options *options) { + AWS_PRECONDITION(allocator != NULL); + AWS_PRECONDITION(options != NULL); + + if (aws_mqtt5_client_options_validate(options)) { + return NULL; + } + + struct aws_mqtt5_client_options_storage *options_storage = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_client_options_storage)); + if (options_storage == NULL) { + return NULL; + } + + options_storage->allocator = allocator; + options_storage->host_name = aws_string_new_from_cursor(allocator, &options->host_name); + if (options_storage->host_name == NULL) { + goto error; + } + + options_storage->port = options->port; + options_storage->bootstrap = aws_client_bootstrap_acquire(options->bootstrap); + + if (options->socket_options != NULL) { + options_storage->socket_options = *options->socket_options; + } else { + options_storage->socket_options.type = AWS_SOCKET_STREAM; + options_storage->socket_options.connect_timeout_ms = AWS_MQTT5_DEFAULT_SOCKET_CONNECT_TIMEOUT_MS; + } + + if (options->tls_options != NULL) { + if (aws_tls_connection_options_copy(&options_storage->tls_options, options->tls_options)) { + goto error; + } + options_storage->tls_options_ptr = &options_storage->tls_options; + + if (!options_storage->tls_options.server_name) { + struct aws_byte_cursor host_name_cur = aws_byte_cursor_from_string(options_storage->host_name); + if (aws_tls_connection_options_set_server_name(&options_storage->tls_options, allocator, &host_name_cur)) { + + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "Failed to set TLS Connection Options server name"); + goto error; + } + } + } + + if (options->http_proxy_options != NULL) { + options_storage->http_proxy_config = + aws_http_proxy_config_new_from_proxy_options(allocator, options->http_proxy_options); + if (options_storage->http_proxy_config == NULL) { + goto error; + } + + aws_http_proxy_options_init_from_config( + &options_storage->http_proxy_options, options_storage->http_proxy_config); + } + + options_storage->websocket_handshake_transform = options->websocket_handshake_transform; + options_storage->websocket_handshake_transform_user_data = options->websocket_handshake_transform_user_data; + + options_storage->publish_received_handler = options->publish_received_handler; + options_storage->publish_received_handler_user_data = options->publish_received_handler_user_data; + + options_storage->session_behavior = options->session_behavior; + options_storage->extended_validation_and_flow_control_options = + options->extended_validation_and_flow_control_options; + options_storage->offline_queue_behavior = options->offline_queue_behavior; + + options_storage->retry_jitter_mode = options->retry_jitter_mode; + options_storage->min_reconnect_delay_ms = options->min_reconnect_delay_ms; + options_storage->max_reconnect_delay_ms = options->max_reconnect_delay_ms; + options_storage->min_connected_time_to_reset_reconnect_delay_ms = + options->min_connected_time_to_reset_reconnect_delay_ms; + + options_storage->ping_timeout_ms = options->ping_timeout_ms; + options_storage->connack_timeout_ms = options->connack_timeout_ms; + + options_storage->ack_timeout_seconds = options->ack_timeout_seconds; + + if (options->topic_aliasing_options != NULL) { + options_storage->topic_aliasing_options = *options->topic_aliasing_options; + } + + if (aws_mqtt5_packet_connect_storage_init(&options_storage->connect, allocator, options->connect_options)) { + goto error; + } + + options_storage->lifecycle_event_handler = options->lifecycle_event_handler; + options_storage->lifecycle_event_handler_user_data = options->lifecycle_event_handler_user_data; + + options_storage->client_termination_handler = options->client_termination_handler; + options_storage->client_termination_handler_user_data = options->client_termination_handler_user_data; + + s_apply_zero_valued_defaults_to_client_options_storage(options_storage); + + return options_storage; + +error: + + aws_mqtt5_client_options_storage_destroy(options_storage); + + return NULL; +} + +struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_acquire( + struct aws_mqtt5_operation_disconnect *disconnect_op) { + if (disconnect_op != NULL) { + aws_mqtt5_operation_acquire(&disconnect_op->base); + } + + return disconnect_op; +} + +struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_release( + struct aws_mqtt5_operation_disconnect *disconnect_op) { + if (disconnect_op != NULL) { + aws_mqtt5_operation_release(&disconnect_op->base); + } + + return NULL; +} diff --git a/source/v5/mqtt5_topic_alias.c b/source/v5/mqtt5_topic_alias.c new file mode 100644 index 00000000..928c7719 --- /dev/null +++ b/source/v5/mqtt5_topic_alias.c @@ -0,0 +1,586 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include + +int aws_mqtt5_inbound_topic_alias_resolver_init( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*resolver); + resolver->allocator = allocator; + + if (aws_array_list_init_dynamic(&resolver->topic_aliases, allocator, 0, sizeof(struct aws_string *))) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_release_aliases(struct aws_mqtt5_inbound_topic_alias_resolver *resolver) { + if (!aws_array_list_is_valid(&resolver->topic_aliases)) { + return; + } + + size_t cache_size = aws_array_list_length(&resolver->topic_aliases); + for (size_t i = 0; i < cache_size; ++i) { + struct aws_string *topic = NULL; + + aws_array_list_get_at(&resolver->topic_aliases, &topic, i); + aws_string_destroy(topic); + } +} + +void aws_mqtt5_inbound_topic_alias_resolver_clean_up(struct aws_mqtt5_inbound_topic_alias_resolver *resolver) { + s_release_aliases(resolver); + aws_array_list_clean_up(&resolver->topic_aliases); +} + +int aws_mqtt5_inbound_topic_alias_resolver_reset( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t cache_size) { + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(resolver); + AWS_ZERO_STRUCT(resolver->topic_aliases); + + if (aws_array_list_init_dynamic( + &resolver->topic_aliases, resolver->allocator, cache_size, sizeof(struct aws_string *))) { + return AWS_OP_ERR; + } + + for (size_t i = 0; i < cache_size; ++i) { + struct aws_string *topic = NULL; + aws_array_list_push_back(&resolver->topic_aliases, &topic); + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_inbound_topic_alias_resolver_resolve_alias( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t alias, + struct aws_byte_cursor *topic_out) { + size_t cache_size = aws_array_list_length(&resolver->topic_aliases); + + if (alias > cache_size || alias == 0) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS); + } + + size_t alias_index = alias - 1; + struct aws_string *topic = NULL; + aws_array_list_get_at(&resolver->topic_aliases, &topic, alias_index); + + if (topic == NULL) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS); + } + + *topic_out = aws_byte_cursor_from_string(topic); + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_inbound_topic_alias_resolver_register_alias( + struct aws_mqtt5_inbound_topic_alias_resolver *resolver, + uint16_t alias, + struct aws_byte_cursor topic) { + size_t cache_size = aws_array_list_length(&resolver->topic_aliases); + + if (alias > cache_size || alias == 0) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS); + } + + struct aws_string *new_entry = aws_string_new_from_cursor(resolver->allocator, &topic); + if (new_entry == NULL) { + return AWS_OP_ERR; + } + + size_t alias_index = alias - 1; + struct aws_string *existing_entry = NULL; + aws_array_list_get_at(&resolver->topic_aliases, &existing_entry, alias_index); + aws_string_destroy(existing_entry); + + aws_array_list_set_at(&resolver->topic_aliases, &new_entry, alias_index); + + return AWS_OP_SUCCESS; +} + +/****************************************************************************************************************/ + +struct aws_mqtt5_outbound_topic_alias_resolver_vtable { + void (*destroy_fn)(struct aws_mqtt5_outbound_topic_alias_resolver *); + int (*reset_fn)(struct aws_mqtt5_outbound_topic_alias_resolver *, uint16_t); + int (*resolve_outbound_publish_fn)( + struct aws_mqtt5_outbound_topic_alias_resolver *, + const struct aws_mqtt5_packet_publish_view *, + uint16_t *, + struct aws_byte_cursor *); +}; + +struct aws_mqtt5_outbound_topic_alias_resolver { + struct aws_allocator *allocator; + + struct aws_mqtt5_outbound_topic_alias_resolver_vtable *vtable; + void *impl; +}; + +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_disabled_new( + struct aws_allocator *allocator); +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_lru_new( + struct aws_allocator *allocator); +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_user_new( + struct aws_allocator *allocator); + +struct aws_mqtt5_outbound_topic_alias_resolver *aws_mqtt5_outbound_topic_alias_resolver_new( + struct aws_allocator *allocator, + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_alias_behavior) { + + switch (aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default(outbound_alias_behavior)) { + case AWS_MQTT5_COTABT_USER: + return s_aws_mqtt5_outbound_topic_alias_resolver_user_new(allocator); + + case AWS_MQTT5_COTABT_LRU: + return s_aws_mqtt5_outbound_topic_alias_resolver_lru_new(allocator); + + case AWS_MQTT5_COTABT_DISABLED: + return s_aws_mqtt5_outbound_topic_alias_resolver_disabled_new(allocator); + + default: + return NULL; + } +} + +void aws_mqtt5_outbound_topic_alias_resolver_destroy(struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { + if (resolver == NULL) { + return; + } + + (*resolver->vtable->destroy_fn)(resolver); +} + +int aws_mqtt5_outbound_topic_alias_resolver_reset( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + uint16_t topic_alias_maximum) { + + if (resolver == NULL) { + return AWS_OP_ERR; + } + + return (*resolver->vtable->reset_fn)(resolver, topic_alias_maximum); +} + +int aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + const struct aws_mqtt5_packet_publish_view *publish_view, + uint16_t *topic_alias_out, + struct aws_byte_cursor *topic_out) { + if (resolver == NULL) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + return (*resolver->vtable->resolve_outbound_publish_fn)(resolver, publish_view, topic_alias_out, topic_out); +} + +/* + * Disabled resolver + */ + +static void s_aws_mqtt5_outbound_topic_alias_resolver_disabled_destroy( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { + if (resolver == NULL) { + return; + } + + aws_mem_release(resolver->allocator, resolver); +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_disabled_reset( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + uint16_t topic_alias_maximum) { + (void)resolver; + (void)topic_alias_maximum; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_disabled_resolve_outbound_publish_fn( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + const struct aws_mqtt5_packet_publish_view *publish_view, + uint16_t *topic_alias_out, + struct aws_byte_cursor *topic_out) { + (void)resolver; + + if (publish_view->topic.len == 0) { + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + + *topic_alias_out = 0; + *topic_out = publish_view->topic; + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_outbound_topic_alias_resolver_vtable s_aws_mqtt5_outbound_topic_alias_resolver_disabled_vtable = + { + .destroy_fn = s_aws_mqtt5_outbound_topic_alias_resolver_disabled_destroy, + .reset_fn = s_aws_mqtt5_outbound_topic_alias_resolver_disabled_reset, + .resolve_outbound_publish_fn = s_aws_mqtt5_outbound_topic_alias_resolver_disabled_resolve_outbound_publish_fn, +}; + +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_disabled_new( + struct aws_allocator *allocator) { + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_outbound_topic_alias_resolver)); + + resolver->allocator = allocator; + resolver->vtable = &s_aws_mqtt5_outbound_topic_alias_resolver_disabled_vtable; + + return resolver; +} + +/* + * User resolver + * + * User resolution implies the user is controlling the topic alias assignments, but we still want to validate their + * actions. In particular, we track the currently valid set of aliases (based on previous outbound publishes) + * and only use an alias when the submitted publish is an exact match for the current assignment. + */ + +struct aws_mqtt5_outbound_topic_alias_resolver_user { + struct aws_mqtt5_outbound_topic_alias_resolver base; + + struct aws_array_list aliases; +}; + +static void s_cleanup_user_aliases(struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver) { + for (size_t i = 0; i < aws_array_list_length(&user_resolver->aliases); ++i) { + struct aws_string *alias = NULL; + aws_array_list_get_at(&user_resolver->aliases, &alias, i); + + aws_string_destroy(alias); + } + + aws_array_list_clean_up(&user_resolver->aliases); + AWS_ZERO_STRUCT(user_resolver->aliases); +} + +static void s_aws_mqtt5_outbound_topic_alias_resolver_user_destroy( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { + if (resolver == NULL) { + return; + } + + struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; + s_cleanup_user_aliases(user_resolver); + + aws_mem_release(resolver->allocator, user_resolver); +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_user_reset( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + uint16_t topic_alias_maximum) { + struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; + s_cleanup_user_aliases(user_resolver); + + aws_array_list_init_dynamic( + &user_resolver->aliases, resolver->allocator, topic_alias_maximum, sizeof(struct aws_string *)); + for (size_t i = 0; i < topic_alias_maximum; ++i) { + struct aws_string *invalid_alias = NULL; + aws_array_list_push_back(&user_resolver->aliases, &invalid_alias); + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publish_fn( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + const struct aws_mqtt5_packet_publish_view *publish_view, + uint16_t *topic_alias_out, + struct aws_byte_cursor *topic_out) { + + if (publish_view->topic_alias == NULL) { + /* not using a topic alias, nothing to do */ + *topic_alias_out = 0; + *topic_out = publish_view->topic; + + return AWS_OP_SUCCESS; + } + + uint16_t user_alias = *publish_view->topic_alias; + if (user_alias == 0) { + /* should have been caught by publish validation */ + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS); + } + + struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; + uint16_t user_alias_index = user_alias - 1; + if (user_alias_index >= aws_array_list_length(&user_resolver->aliases)) { + /* should have been caught by dynamic publish validation */ + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS); + } + + struct aws_string *current_assignment = NULL; + aws_array_list_get_at(&user_resolver->aliases, ¤t_assignment, user_alias_index); + + *topic_alias_out = user_alias; + + bool can_use_alias = false; + if (current_assignment != NULL) { + struct aws_byte_cursor assignment_cursor = aws_byte_cursor_from_string(current_assignment); + if (aws_byte_cursor_eq(&assignment_cursor, &publish_view->topic)) { + can_use_alias = true; + } + } + + if (can_use_alias) { + AWS_ZERO_STRUCT(*topic_out); + } else { + *topic_out = publish_view->topic; + } + + /* mark this alias as seen */ + if (!can_use_alias) { + aws_string_destroy(current_assignment); + current_assignment = aws_string_new_from_cursor(resolver->allocator, &publish_view->topic); + aws_array_list_set_at(&user_resolver->aliases, ¤t_assignment, user_alias_index); + } + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_outbound_topic_alias_resolver_vtable s_aws_mqtt5_outbound_topic_alias_resolver_user_vtable = { + .destroy_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_destroy, + .reset_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_reset, + .resolve_outbound_publish_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publish_fn, +}; + +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_user_new( + struct aws_allocator *allocator) { + struct aws_mqtt5_outbound_topic_alias_resolver_user *resolver = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_outbound_topic_alias_resolver_user)); + + resolver->base.allocator = allocator; + resolver->base.vtable = &s_aws_mqtt5_outbound_topic_alias_resolver_user_vtable; + resolver->base.impl = resolver; + + aws_array_list_init_dynamic(&resolver->aliases, allocator, 0, sizeof(struct aws_string *)); + + return &resolver->base; +} + +/* + * LRU resolver + * + * This resolver uses an LRU cache to automatically create topic alias assignments for the user. With a reasonable + * cache size, this should perform well for the majority of MQTT workloads. For workloads it does not perform well + * with, the user should control the assignment (or disable entirely). Even for workloads where the LRU cache fails + * to reuse an assignment every single time, the overall cost is 3 extra bytes per publish. As a rough estimate, this + * means that LRU topic aliasing is "worth it" if an existing alias can be used at least once every + * (AverageTopicLength / 3) publishes. + */ + +struct aws_mqtt5_outbound_topic_alias_resolver_lru { + struct aws_mqtt5_outbound_topic_alias_resolver base; + + struct aws_cache *lru_cache; + size_t max_aliases; +}; + +static void s_aws_mqtt5_outbound_topic_alias_resolver_lru_destroy( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { + if (resolver == NULL) { + return; + } + + struct aws_mqtt5_outbound_topic_alias_resolver_lru *lru_resolver = resolver->impl; + + if (lru_resolver->lru_cache != NULL) { + aws_cache_destroy(lru_resolver->lru_cache); + } + + aws_mem_release(resolver->allocator, lru_resolver); +} + +struct aws_topic_alias_assignment { + struct aws_byte_cursor topic_cursor; + struct aws_byte_buf topic; + uint16_t alias; + struct aws_allocator *allocator; +}; + +static void s_aws_topic_alias_assignment_destroy(struct aws_topic_alias_assignment *alias_assignment) { + if (alias_assignment == NULL) { + return; + } + + aws_byte_buf_clean_up(&alias_assignment->topic); + + aws_mem_release(alias_assignment->allocator, alias_assignment); +} + +static struct aws_topic_alias_assignment *s_aws_topic_alias_assignment_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic, + uint16_t alias) { + struct aws_topic_alias_assignment *assignment = + aws_mem_calloc(allocator, 1, sizeof(struct aws_topic_alias_assignment)); + + assignment->allocator = allocator; + assignment->alias = alias; + + if (aws_byte_buf_init_copy_from_cursor(&assignment->topic, allocator, topic)) { + goto on_error; + } + + assignment->topic_cursor = aws_byte_cursor_from_buf(&assignment->topic); + + return assignment; + +on_error: + + s_aws_topic_alias_assignment_destroy(assignment); + + return NULL; +} + +static void s_destroy_assignment_value(void *value) { + s_aws_topic_alias_assignment_destroy(value); +} + +static bool s_topic_hash_equality_fn(const void *a, const void *b) { + const struct aws_byte_cursor *a_cursor = a; + const struct aws_byte_cursor *b_cursor = b; + + return aws_byte_cursor_eq(a_cursor, b_cursor); +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_lru_reset( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + uint16_t topic_alias_maximum) { + struct aws_mqtt5_outbound_topic_alias_resolver_lru *lru_resolver = resolver->impl; + + if (lru_resolver->lru_cache != NULL) { + aws_cache_destroy(lru_resolver->lru_cache); + lru_resolver->lru_cache = NULL; + } + + if (topic_alias_maximum > 0) { + lru_resolver->lru_cache = aws_cache_new_lru( + lru_resolver->base.allocator, + aws_hash_byte_cursor_ptr, + s_topic_hash_equality_fn, + NULL, + s_destroy_assignment_value, + topic_alias_maximum); + } + + lru_resolver->max_aliases = topic_alias_maximum; + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_outbound_topic_alias_resolver_lru_resolve_outbound_publish_fn( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + const struct aws_mqtt5_packet_publish_view *publish_view, + uint16_t *topic_alias_out, + struct aws_byte_cursor *topic_out) { + + /* No cache => no aliasing done */ + struct aws_mqtt5_outbound_topic_alias_resolver_lru *lru_resolver = resolver->impl; + if (lru_resolver->lru_cache == NULL || lru_resolver->max_aliases == 0) { + *topic_alias_out = 0; + *topic_out = publish_view->topic; + return AWS_OP_SUCCESS; + } + + /* Look for the topic in the cache */ + struct aws_byte_cursor topic = publish_view->topic; + void *existing_element = NULL; + if (aws_cache_find(lru_resolver->lru_cache, &topic, &existing_element)) { + return AWS_OP_ERR; + } + + struct aws_topic_alias_assignment *existing_assignment = existing_element; + if (existing_assignment != NULL) { + /* + * Topic exists, so use the assignment. The LRU cache find implementation has already promoted the element + * to MRU. + */ + *topic_alias_out = existing_assignment->alias; + AWS_ZERO_STRUCT(*topic_out); + + return AWS_OP_SUCCESS; + } + + /* Topic doesn't exist in the cache. */ + uint16_t new_alias_id = 0; + size_t assignment_count = aws_cache_get_element_count(lru_resolver->lru_cache); + if (assignment_count == lru_resolver->max_aliases) { + /* + * The cache is full. Get the LRU element to figure out what id we're going to reuse. There's no way to get + * the LRU element without promoting it. So we get the element, save the discovered alias id, then remove + * the element. + */ + void *lru_element = aws_lru_cache_use_lru_element(lru_resolver->lru_cache); + + struct aws_topic_alias_assignment *replaced_assignment = lru_element; + new_alias_id = replaced_assignment->alias; + struct aws_byte_cursor replaced_topic = replaced_assignment->topic_cursor; + + /* + * This is a little uncomfortable but valid. The cursor we're passing in will get invalidated (and the backing + * memory deleted) as part of the removal process but it is only used to find the element to remove. Once + * destruction begins it is no longer accessed. + */ + aws_cache_remove(lru_resolver->lru_cache, &replaced_topic); + } else { + /* + * The cache never shrinks and the first N adds are the N valid topic aliases. Since the cache isn't full, + * we know the next alias that hasn't been used. This invariant only holds given that we will tear down + * the connection (invalidating the cache) on errors from this function (ie, continuing on from a put + * error would break the invariant and create duplicated ids). + */ + new_alias_id = (uint16_t)(assignment_count + 1); + } + + /* + * We have a topic alias to use. Add our new assignment. + */ + struct aws_topic_alias_assignment *new_assignment = + s_aws_topic_alias_assignment_new(resolver->allocator, topic, new_alias_id); + if (new_assignment == NULL) { + return AWS_OP_ERR; + } + + /* the LRU cache put implementation automatically makes the newly added element MRU */ + if (aws_cache_put(lru_resolver->lru_cache, &new_assignment->topic_cursor, new_assignment)) { + s_aws_topic_alias_assignment_destroy(new_assignment); + return AWS_OP_ERR; + } + + *topic_alias_out = new_assignment->alias; + *topic_out = topic; /* this is a new assignment so topic must go out too */ + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_outbound_topic_alias_resolver_vtable s_aws_mqtt5_outbound_topic_alias_resolver_lru_vtable = { + .destroy_fn = s_aws_mqtt5_outbound_topic_alias_resolver_lru_destroy, + .reset_fn = s_aws_mqtt5_outbound_topic_alias_resolver_lru_reset, + .resolve_outbound_publish_fn = s_aws_mqtt5_outbound_topic_alias_resolver_lru_resolve_outbound_publish_fn, +}; + +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_lru_new( + struct aws_allocator *allocator) { + struct aws_mqtt5_outbound_topic_alias_resolver_lru *resolver = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_outbound_topic_alias_resolver_lru)); + + resolver->base.allocator = allocator; + resolver->base.vtable = &s_aws_mqtt5_outbound_topic_alias_resolver_lru_vtable; + resolver->base.impl = resolver; + + return &resolver->base; +} diff --git a/source/v5/mqtt5_types.c b/source/v5/mqtt5_types.c new file mode 100644 index 00000000..c1eb1655 --- /dev/null +++ b/source/v5/mqtt5_types.c @@ -0,0 +1,333 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +/* disconnect and shared reason codes */ +static const char *s_normal_disconnection = "Normal Disconnection"; +static const char *s_disconnect_with_will_message = "Disconnect With Will Message"; +static const char *s_unspecified_error = "Unspecified Error"; +static const char *s_malformed_packet = "Malformed Packet"; +static const char *s_protocol_error = "Protocol Error"; +static const char *s_implementation_specific_error = "Implementation Specific Error"; +static const char *s_not_authorized = "Not Authorized"; +static const char *s_server_busy = "Server Busy"; +static const char *s_server_shutting_down = "Server Shutting Down"; +static const char *s_keep_alive_timeout = "Keep Alive Timeout"; +static const char *s_session_taken_over = "Session Taken Over"; +static const char *s_topic_filter_invalid = "Topic Filter Invalid"; +static const char *s_topic_name_invalid = "Topic Name Invalid"; +static const char *s_receive_maximum_exceeded = "Receive Maximum Exceeded"; +static const char *s_topic_alias_invalid = "Topic Alias Invalid"; +static const char *s_packet_too_large = "Packet Too Large"; +static const char *s_message_rate_too_high = "Message Rate Too High"; +static const char *s_quota_exceeded = "Quota Exceeded"; +static const char *s_administrative_action = "Administrative Action"; +static const char *s_payload_format_invalid = "Payload Format Invalid"; +static const char *s_retain_not_supported = "Retain Not Supported"; +static const char *s_qos_not_supported = "QoS Not Supported"; +static const char *s_use_another_server = "Use Another Server"; +static const char *s_server_moved = "Server Moved"; +static const char *s_shared_subscriptions_not_supported = "Shared Subscriptions Not Supported"; +static const char *s_connection_rate_exceeded = "Connection Rate Exceeded"; +static const char *s_maximum_connect_time = "Maximum Connect Time"; +static const char *s_subscription_identifiers_not_supported = "Subscription Identifiers Not Supported"; +static const char *s_wildcard_subscriptions_not_supported = "Wildcard Subscriptions Not Supported"; +static const char *s_success = "Success"; +static const char *s_unsupported_protocol_version = "Unsupported Protocol Version"; +static const char *s_client_identifier_not_valid = "Client Identifier Not Valid"; +static const char *s_bad_username_or_password = "Bad Username Or Password"; +static const char *s_server_unavailable = "Server Unavailable"; +static const char *s_banned = "Banned"; +static const char *s_bad_authentication_method = "Bad Authentication Method"; +static const char *s_unknown_reason = "Unknown Reason"; +static const char *s_no_subscription_existed = "No Subscription Existed"; +static const char *s_packet_identifier_in_use = "Packet Identifier In Use"; +static const char *s_granted_qos_0 = "Granted QoS 0"; +static const char *s_granted_qos_1 = "Granted QoS 1"; +static const char *s_granted_qos_2 = "Granted QoS 2"; +static const char *s_no_matching_subscribers = "No Matching Subscribers"; + +const char *aws_mqtt5_connect_reason_code_to_c_string(enum aws_mqtt5_connect_reason_code reason_code) { + switch (reason_code) { + case AWS_MQTT5_CRC_SUCCESS: + return s_success; + case AWS_MQTT5_CRC_UNSPECIFIED_ERROR: + return s_unspecified_error; + case AWS_MQTT5_CRC_MALFORMED_PACKET: + return s_malformed_packet; + case AWS_MQTT5_CRC_PROTOCOL_ERROR: + return s_protocol_error; + case AWS_MQTT5_CRC_IMPLEMENTATION_SPECIFIC_ERROR: + return s_implementation_specific_error; + case AWS_MQTT5_CRC_UNSUPPORTED_PROTOCOL_VERSION: + return s_unsupported_protocol_version; + case AWS_MQTT5_CRC_CLIENT_IDENTIFIER_NOT_VALID: + return s_client_identifier_not_valid; + case AWS_MQTT5_CRC_BAD_USERNAME_OR_PASSWORD: + return s_bad_username_or_password; + case AWS_MQTT5_CRC_NOT_AUTHORIZED: + return s_not_authorized; + case AWS_MQTT5_CRC_SERVER_UNAVAILABLE: + return s_server_unavailable; + case AWS_MQTT5_CRC_SERVER_BUSY: + return s_server_busy; + case AWS_MQTT5_CRC_BANNED: + return s_banned; + case AWS_MQTT5_CRC_BAD_AUTHENTICATION_METHOD: + return s_bad_authentication_method; + case AWS_MQTT5_CRC_TOPIC_NAME_INVALID: + return s_topic_name_invalid; + case AWS_MQTT5_CRC_PACKET_TOO_LARGE: + return s_packet_too_large; + case AWS_MQTT5_CRC_QUOTA_EXCEEDED: + return s_quota_exceeded; + case AWS_MQTT5_CRC_PAYLOAD_FORMAT_INVALID: + return s_payload_format_invalid; + case AWS_MQTT5_CRC_RETAIN_NOT_SUPPORTED: + return s_retain_not_supported; + case AWS_MQTT5_CRC_QOS_NOT_SUPPORTED: + return s_qos_not_supported; + case AWS_MQTT5_CRC_USE_ANOTHER_SERVER: + return s_use_another_server; + case AWS_MQTT5_CRC_SERVER_MOVED: + return s_server_moved; + case AWS_MQTT5_CRC_CONNECTION_RATE_EXCEEDED: + return s_connection_rate_exceeded; + } + + return s_unknown_reason; +} + +const char *aws_mqtt5_disconnect_reason_code_to_c_string( + enum aws_mqtt5_disconnect_reason_code reason_code, + bool *is_valid) { + if (is_valid != NULL) { + *is_valid = true; + } + + switch (reason_code) { + case AWS_MQTT5_DRC_NORMAL_DISCONNECTION: + return s_normal_disconnection; + case AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE: + return s_disconnect_with_will_message; + case AWS_MQTT5_DRC_UNSPECIFIED_ERROR: + return s_unspecified_error; + case AWS_MQTT5_DRC_MALFORMED_PACKET: + return s_malformed_packet; + case AWS_MQTT5_DRC_PROTOCOL_ERROR: + return s_protocol_error; + case AWS_MQTT5_DRC_IMPLEMENTATION_SPECIFIC_ERROR: + return s_implementation_specific_error; + case AWS_MQTT5_DRC_NOT_AUTHORIZED: + return s_not_authorized; + case AWS_MQTT5_DRC_SERVER_BUSY: + return s_server_busy; + case AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN: + return s_server_shutting_down; + case AWS_MQTT5_DRC_KEEP_ALIVE_TIMEOUT: + return s_keep_alive_timeout; + case AWS_MQTT5_DRC_SESSION_TAKEN_OVER: + return s_session_taken_over; + case AWS_MQTT5_DRC_TOPIC_FILTER_INVALID: + return s_topic_filter_invalid; + case AWS_MQTT5_DRC_TOPIC_NAME_INVALID: + return s_topic_name_invalid; + case AWS_MQTT5_DRC_RECEIVE_MAXIMUM_EXCEEDED: + return s_receive_maximum_exceeded; + case AWS_MQTT5_DRC_TOPIC_ALIAS_INVALID: + return s_topic_alias_invalid; + case AWS_MQTT5_DRC_PACKET_TOO_LARGE: + return s_packet_too_large; + case AWS_MQTT5_DRC_MESSAGE_RATE_TOO_HIGH: + return s_message_rate_too_high; + case AWS_MQTT5_DRC_QUOTA_EXCEEDED: + return s_quota_exceeded; + case AWS_MQTT5_DRC_ADMINISTRATIVE_ACTION: + return s_administrative_action; + case AWS_MQTT5_DRC_PAYLOAD_FORMAT_INVALID: + return s_payload_format_invalid; + case AWS_MQTT5_DRC_RETAIN_NOT_SUPPORTED: + return s_retain_not_supported; + case AWS_MQTT5_DRC_QOS_NOT_SUPPORTED: + return s_qos_not_supported; + case AWS_MQTT5_DRC_USE_ANOTHER_SERVER: + return s_use_another_server; + case AWS_MQTT5_DRC_SERVER_MOVED: + return s_server_moved; + case AWS_MQTT5_DRC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED: + return s_shared_subscriptions_not_supported; + case AWS_MQTT5_DRC_CONNECTION_RATE_EXCEEDED: + return s_connection_rate_exceeded; + case AWS_MQTT5_DRC_MAXIMUM_CONNECT_TIME: + return s_maximum_connect_time; + case AWS_MQTT5_DRC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED: + return s_subscription_identifiers_not_supported; + case AWS_MQTT5_DRC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED: + return s_wildcard_subscriptions_not_supported; + } + + if (is_valid != NULL) { + *is_valid = false; + } + + return s_unknown_reason; +} + +const char *aws_mqtt5_puback_reason_code_to_c_string(enum aws_mqtt5_puback_reason_code reason_code) { + switch (reason_code) { + case AWS_MQTT5_PARC_SUCCESS: + return s_success; + case AWS_MQTT5_PARC_NO_MATCHING_SUBSCRIBERS: + return s_no_matching_subscribers; + case AWS_MQTT5_PARC_UNSPECIFIED_ERROR: + return s_unspecified_error; + case AWS_MQTT5_PARC_IMPLEMENTATION_SPECIFIC_ERROR: + return s_implementation_specific_error; + case AWS_MQTT5_PARC_NOT_AUTHORIZED: + return s_not_authorized; + case AWS_MQTT5_PARC_TOPIC_NAME_INVALID: + return s_topic_name_invalid; + case AWS_MQTT5_PARC_PACKET_IDENTIFIER_IN_USE: + return s_packet_identifier_in_use; + case AWS_MQTT5_PARC_QUOTA_EXCEEDED: + return s_quota_exceeded; + case AWS_MQTT5_PARC_PAYLOAD_FORMAT_INVALID: + return s_payload_format_invalid; + } + + return s_unknown_reason; +} + +const char *aws_mqtt5_suback_reason_code_to_c_string(enum aws_mqtt5_suback_reason_code reason_code) { + switch (reason_code) { + case AWS_MQTT5_SARC_GRANTED_QOS_0: + return s_granted_qos_0; + case AWS_MQTT5_SARC_GRANTED_QOS_1: + return s_granted_qos_1; + case AWS_MQTT5_SARC_GRANTED_QOS_2: + return s_granted_qos_2; + case AWS_MQTT5_SARC_UNSPECIFIED_ERROR: + return s_unspecified_error; + case AWS_MQTT5_SARC_IMPLEMENTATION_SPECIFIC_ERROR: + return s_implementation_specific_error; + case AWS_MQTT5_SARC_NOT_AUTHORIZED: + return s_not_authorized; + case AWS_MQTT5_SARC_TOPIC_FILTER_INVALID: + return s_topic_filter_invalid; + case AWS_MQTT5_SARC_PACKET_IDENTIFIER_IN_USE: + return s_packet_identifier_in_use; + case AWS_MQTT5_SARC_QUOTA_EXCEEDED: + return s_quota_exceeded; + case AWS_MQTT5_SARC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED: + return s_shared_subscriptions_not_supported; + case AWS_MQTT5_SARC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED: + return s_subscription_identifiers_not_supported; + case AWS_MQTT5_SARC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED: + return s_wildcard_subscriptions_not_supported; + } + + return s_unknown_reason; +} + +const char *aws_mqtt5_unsuback_reason_code_to_c_string(enum aws_mqtt5_unsuback_reason_code reason_code) { + switch (reason_code) { + case AWS_MQTT5_UARC_SUCCESS: + return s_success; + case AWS_MQTT5_UARC_NO_SUBSCRIPTION_EXISTED: + return s_no_subscription_existed; + case AWS_MQTT5_UARC_UNSPECIFIED_ERROR: + return s_unspecified_error; + case AWS_MQTT5_UARC_IMPLEMENTATION_SPECIFIC_ERROR: + return s_implementation_specific_error; + case AWS_MQTT5_UARC_NOT_AUTHORIZED: + return s_not_authorized; + case AWS_MQTT5_UARC_TOPIC_FILTER_INVALID: + return s_topic_filter_invalid; + case AWS_MQTT5_UARC_PACKET_IDENTIFIER_IN_USE: + return s_packet_identifier_in_use; + } + + return s_unknown_reason; +} + +const char *aws_mqtt5_payload_format_indicator_to_c_string(enum aws_mqtt5_payload_format_indicator format_indicator) { + switch (format_indicator) { + case AWS_MQTT5_PFI_BYTES: + return "Bytes"; + case AWS_MQTT5_PFI_UTF8: + return "Utf-8"; + } + + return "Unknown Payload Format"; +} + +const char *aws_mqtt5_retain_handling_type_to_c_string(enum aws_mqtt5_retain_handling_type retain_handling_type) { + switch (retain_handling_type) { + case AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE: + return "Send retained on any subscribe"; + case AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE_IF_NEW: + return "Send retained on subscribe if not already subscribed"; + case AWS_MQTT5_RHT_DONT_SEND: + return "Dont send retained at all"; + } + + return "Unknown Retain Handling Type"; +} + +const char *aws_mqtt5_packet_type_to_c_string(enum aws_mqtt5_packet_type packet_type) { + switch (packet_type) { + case AWS_MQTT5_PT_RESERVED: + return "RESERVED(INVALID)"; + + case AWS_MQTT5_PT_CONNECT: + return "CONNECT"; + + case AWS_MQTT5_PT_CONNACK: + return "CONNACK"; + + case AWS_MQTT5_PT_PUBLISH: + return "PUBLISH"; + + case AWS_MQTT5_PT_PUBACK: + return "PUBACK"; + + case AWS_MQTT5_PT_PUBREC: + return "PUBREC"; + + case AWS_MQTT5_PT_PUBREL: + return "PUBREL"; + + case AWS_MQTT5_PT_PUBCOMP: + return "PUBCOMP"; + + case AWS_MQTT5_PT_SUBSCRIBE: + return "SUBSCRIBE"; + + case AWS_MQTT5_PT_SUBACK: + return "SUBACK"; + + case AWS_MQTT5_PT_UNSUBSCRIBE: + return "UNSUBSCRIBE"; + + case AWS_MQTT5_PT_UNSUBACK: + return "UNSUBACK"; + + case AWS_MQTT5_PT_PINGREQ: + return "PINGREQ"; + + case AWS_MQTT5_PT_PINGRESP: + return "PINGRESP"; + + case AWS_MQTT5_PT_DISCONNECT: + return "DISCONNECT"; + + case AWS_MQTT5_PT_AUTH: + return "AUTH"; + + default: + return "UNKNOWN"; + } +} diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c new file mode 100644 index 00000000..5c2ac2de --- /dev/null +++ b/source/v5/mqtt5_utils.c @@ -0,0 +1,534 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include + +uint8_t aws_mqtt5_compute_fixed_header_byte1(enum aws_mqtt5_packet_type packet_type, uint8_t flags) { + return flags | ((uint8_t)packet_type << 4); +} + +/* encodes a utf8-string (2 byte length + "MQTT") + the version value (5) */ +static uint8_t s_connect_variable_length_header_prefix[7] = {0x00, 0x04, 0x4D, 0x51, 0x54, 0x54, 0x05}; + +struct aws_byte_cursor g_aws_mqtt5_connect_protocol_cursor = { + .ptr = &s_connect_variable_length_header_prefix[0], + .len = AWS_ARRAY_SIZE(s_connect_variable_length_header_prefix), +}; + +void aws_mqtt5_negotiated_settings_log( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + enum aws_log_level level) { + + struct aws_logger *temp_logger = aws_logger_get(); + if (temp_logger == NULL || temp_logger->vtable->get_log_level(temp_logger, AWS_LS_MQTT5_GENERAL) < level) { + return; + } + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings maxiumum qos set to %d", + (void *)negotiated_settings, + negotiated_settings->maximum_qos); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings session expiry interval set to %" PRIu32, + (void *)negotiated_settings, + negotiated_settings->session_expiry_interval); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings receive maximum from server set to %" PRIu16, + (void *)negotiated_settings, + negotiated_settings->receive_maximum_from_server); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings maximum packet size to server set to %" PRIu32, + (void *)negotiated_settings, + negotiated_settings->maximum_packet_size_to_server); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings topic alias maximum to server set to %" PRIu16, + (void *)negotiated_settings, + negotiated_settings->topic_alias_maximum_to_server); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings topic alias maximum to client set to %" PRIu16, + (void *)negotiated_settings, + negotiated_settings->topic_alias_maximum_to_client); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings server keep alive set to %" PRIu16, + (void *)negotiated_settings, + negotiated_settings->server_keep_alive); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings retain available set to %s", + (void *)negotiated_settings, + negotiated_settings->retain_available ? "true" : "false"); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings wildcard subscriptions available set to %s", + (void *)negotiated_settings, + negotiated_settings->wildcard_subscriptions_available ? "true" : "false"); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings subscription identifiers available set to %s", + (void *)negotiated_settings, + negotiated_settings->subscription_identifiers_available ? "true" : "false"); + + AWS_LOGF( + level, + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_negotiated_settings shared subscriptions available set to %s", + (void *)negotiated_settings, + negotiated_settings->shared_subscriptions_available ? "true" : "false"); +} + +int aws_mqtt5_negotiated_settings_init( + struct aws_allocator *allocator, + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_byte_cursor *client_id) { + if (aws_byte_buf_init(&negotiated_settings->client_id_storage, allocator, client_id->len)) { + return AWS_OP_ERR; + } + + if (aws_byte_buf_append_dynamic(&negotiated_settings->client_id_storage, client_id)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_negotiated_settings_copy( + const struct aws_mqtt5_negotiated_settings *source, + struct aws_mqtt5_negotiated_settings *dest) { + aws_mqtt5_negotiated_settings_clean_up(dest); + + *dest = *source; + AWS_ZERO_STRUCT(dest->client_id_storage); + + if (source->client_id_storage.allocator != NULL) { + return aws_byte_buf_init_copy_from_cursor( + &dest->client_id_storage, + source->client_id_storage.allocator, + aws_byte_cursor_from_buf(&source->client_id_storage)); + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_negotiated_settings_apply_client_id( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_byte_cursor *client_id) { + + if (negotiated_settings->client_id_storage.len == 0) { + if (aws_byte_buf_append_dynamic(&negotiated_settings->client_id_storage, client_id)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_negotiated_settings_clean_up(struct aws_mqtt5_negotiated_settings *negotiated_settings) { + aws_byte_buf_clean_up(&negotiated_settings->client_id_storage); +} + +/** Assign defaults values to negotiated_settings */ +void aws_mqtt5_negotiated_settings_reset( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_mqtt5_packet_connect_view *packet_connect_view) { + AWS_PRECONDITION(negotiated_settings != NULL); + AWS_PRECONDITION(packet_connect_view != NULL); + + /* Properties that may be sent in CONNECT to Server. These should only be sent if Client + changes them from their default values. + */ + negotiated_settings->server_keep_alive = packet_connect_view->keep_alive_interval_seconds; + negotiated_settings->session_expiry_interval = 0; + negotiated_settings->receive_maximum_from_server = AWS_MQTT5_RECEIVE_MAXIMUM; + negotiated_settings->maximum_packet_size_to_server = AWS_MQTT5_MAXIMUM_PACKET_SIZE; + negotiated_settings->topic_alias_maximum_to_client = 0; + + // Default for Client is QoS 1. Server default is 2. + // This should only be changed if server returns a 0 in the CONNACK + negotiated_settings->maximum_qos = AWS_MQTT5_QOS_AT_LEAST_ONCE; + negotiated_settings->topic_alias_maximum_to_server = 0; + + // Default is true for following settings but can be changed by Server on CONNACK + negotiated_settings->retain_available = true; + negotiated_settings->wildcard_subscriptions_available = true; + negotiated_settings->subscription_identifiers_available = true; + negotiated_settings->shared_subscriptions_available = true; + + negotiated_settings->rejoined_session = false; + + /** + * Apply user set properties to negotiated_settings + * NULL pointers indicate user has not set a property and it should remain the default value. + */ + + if (packet_connect_view->session_expiry_interval_seconds != NULL) { + negotiated_settings->session_expiry_interval = *packet_connect_view->session_expiry_interval_seconds; + } + + if (packet_connect_view->topic_alias_maximum != NULL) { + negotiated_settings->topic_alias_maximum_to_client = *packet_connect_view->topic_alias_maximum; + } +} + +void aws_mqtt5_negotiated_settings_apply_connack( + struct aws_mqtt5_negotiated_settings *negotiated_settings, + const struct aws_mqtt5_packet_connack_view *connack_data) { + AWS_PRECONDITION(negotiated_settings != NULL); + AWS_PRECONDITION(connack_data != NULL); + + /** + * Reconcile CONNACK set properties with current negotiated_settings values + * NULL pointers indicate Server has not set a property + */ + + if (connack_data->session_expiry_interval != NULL) { + negotiated_settings->session_expiry_interval = *connack_data->session_expiry_interval; + } + + if (connack_data->receive_maximum != NULL) { + negotiated_settings->receive_maximum_from_server = *connack_data->receive_maximum; + } + + // NULL = Maximum QoS of 2. + if (connack_data->maximum_qos != NULL) { + if (*connack_data->maximum_qos < negotiated_settings->maximum_qos) { + negotiated_settings->maximum_qos = *connack_data->maximum_qos; + } + } + + if (connack_data->retain_available != NULL) { + negotiated_settings->retain_available = *connack_data->retain_available; + } + + if (connack_data->maximum_packet_size != NULL) { + negotiated_settings->maximum_packet_size_to_server = *connack_data->maximum_packet_size; + } + + // If a value is not sent by Server, the Client must not send any Topic Aliases to the Server. + if (connack_data->topic_alias_maximum != NULL) { + negotiated_settings->topic_alias_maximum_to_server = *connack_data->topic_alias_maximum; + } + + if (connack_data->wildcard_subscriptions_available != NULL) { + negotiated_settings->wildcard_subscriptions_available = *connack_data->wildcard_subscriptions_available; + } + + if (connack_data->subscription_identifiers_available != NULL) { + negotiated_settings->subscription_identifiers_available = *connack_data->subscription_identifiers_available; + } + + if (connack_data->shared_subscriptions_available != NULL) { + negotiated_settings->shared_subscriptions_available = *connack_data->shared_subscriptions_available; + } + + if (connack_data->server_keep_alive != NULL) { + negotiated_settings->server_keep_alive = *connack_data->server_keep_alive; + } + + if (connack_data->assigned_client_identifier != NULL) { + aws_mqtt5_negotiated_settings_apply_client_id(negotiated_settings, connack_data->assigned_client_identifier); + } + + negotiated_settings->rejoined_session = connack_data->session_present; +} + +const char *aws_mqtt5_client_session_behavior_type_to_c_string( + enum aws_mqtt5_client_session_behavior_type session_behavior) { + switch (aws_mqtt5_client_session_behavior_type_to_non_default(session_behavior)) { + case AWS_MQTT5_CSBT_CLEAN: + return "Clean session always"; + case AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS: + return "Attempt to resume a session after initial connection success"; + default: + return "Unknown session behavior"; + } +} + +enum aws_mqtt5_client_session_behavior_type aws_mqtt5_client_session_behavior_type_to_non_default( + enum aws_mqtt5_client_session_behavior_type session_behavior) { + if (session_behavior == AWS_MQTT5_CSBT_DEFAULT) { + return AWS_MQTT5_CSBT_CLEAN; + } + + return session_behavior; +} + +const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { + switch (aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default(outbound_aliasing_behavior)) { + case AWS_MQTT5_COTABT_USER: + return "User-controlled outbound topic aliasing behavior"; + case AWS_MQTT5_COTABT_LRU: + return "LRU caching outbound topic aliasing behavior"; + default: + return "Unknown outbound topic aliasing behavior"; + } +} + +enum aws_mqtt5_client_outbound_topic_alias_behavior_type aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { + if (outbound_aliasing_behavior == AWS_MQTT5_COTABT_DEFAULT) { + return AWS_MQTT5_COTABT_LRU; + } + + return outbound_aliasing_behavior; +} + +const char *aws_mqtt5_inbound_topic_alias_behavior_type_to_c_string( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior) { + switch (aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default(inbound_aliasing_behavior)) { + case AWS_MQTT5_CITABT_ENABLED: + return "Inbound topic aliasing behavior enabled"; + case AWS_MQTT5_CITABT_DISABLED: + return "Inbound topic aliasing behavior disabled"; + default: + return "Unknown inbound topic aliasing behavior"; + } +} + +enum aws_mqtt5_client_inbound_topic_alias_behavior_type aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior) { + if (inbound_aliasing_behavior == AWS_MQTT5_CITABT_DEFAULT) { + return AWS_MQTT5_CITABT_DISABLED; + } + + return inbound_aliasing_behavior; +} + +const char *aws_mqtt5_extended_validation_and_flow_control_options_to_c_string( + enum aws_mqtt5_extended_validation_and_flow_control_options extended_validation_behavior) { + switch (extended_validation_behavior) { + case AWS_MQTT5_EVAFCO_NONE: + return "No additional flow control or packet validation"; + case AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS: + return "AWS IoT Core flow control and packet validation"; + default: + return "Unknown extended validation behavior"; + } +} + +const char *aws_mqtt5_client_operation_queue_behavior_type_to_c_string( + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior) { + switch (aws_mqtt5_client_operation_queue_behavior_type_to_non_default(offline_queue_behavior)) { + case AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT: + return "Fail all incomplete operations except QoS 1 publishes"; + case AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT: + return "Fail incomplete QoS 0 publishes"; + case AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT: + return "Fail all incomplete operations"; + default: + return "Unknown operation queue behavior type"; + } +} + +enum aws_mqtt5_client_operation_queue_behavior_type aws_mqtt5_client_operation_queue_behavior_type_to_non_default( + enum aws_mqtt5_client_operation_queue_behavior_type offline_queue_behavior) { + if (offline_queue_behavior == AWS_MQTT5_COQBT_DEFAULT) { + return AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + } + + return offline_queue_behavior; +} + +const char *aws_mqtt5_client_lifecycle_event_type_to_c_string( + enum aws_mqtt5_client_lifecycle_event_type lifecycle_event) { + switch (lifecycle_event) { + case AWS_MQTT5_CLET_ATTEMPTING_CONNECT: + return "Connection establishment attempt"; + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + return "Connection establishment success"; + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + return "Connection establishment failure"; + case AWS_MQTT5_CLET_DISCONNECTION: + return "Disconnection"; + case AWS_MQTT5_CLET_STOPPED: + return "Client stopped"; + } + + return "Unknown lifecycle event"; +} + +uint64_t aws_mqtt5_client_random_in_range(uint64_t from, uint64_t to) { + uint64_t max = aws_max_u64(from, to); + uint64_t min = aws_min_u64(from, to); + + /* Note: this contains several changes to the corresponding function in aws-c-io. Don't throw them away. + * + * 1. random range is now inclusive/closed: [from, to] rather than half-open [from, to) + * 2. as a corollary, diff == 0 => return min, not 0 + */ + uint64_t diff = max - min; + if (!diff) { + return min; + } + + uint64_t random_value = 0; + if (aws_device_random_u64(&random_value)) { + return min; + } + + if (diff == UINT64_MAX) { + return random_value; + } + + return min + random_value % (diff + 1); /* + 1 is safe due to previous check */ +} + +static uint8_t s_aws_iot_core_rules_prefix[] = "$aws/rules/"; + +struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte_cursor topic_cursor) { + size_t prefix_length = AWS_ARRAY_SIZE(s_aws_iot_core_rules_prefix) - 1; /* skip 0-terminator */ + + struct aws_byte_cursor rules_prefix = { + .ptr = s_aws_iot_core_rules_prefix, + .len = prefix_length, + }; + + if (topic_cursor.len < rules_prefix.len) { + return topic_cursor; + } + + struct aws_byte_cursor topic_cursor_copy = topic_cursor; + struct aws_byte_cursor topic_prefix = topic_cursor; + topic_prefix.len = rules_prefix.len; + + if (!aws_byte_cursor_eq_ignore_case(&rules_prefix, &topic_prefix)) { + return topic_cursor; + } + + aws_byte_cursor_advance(&topic_cursor_copy, prefix_length); + if (topic_cursor_copy.len == 0) { + return topic_cursor; + } + + struct aws_byte_cursor rule_name_segment_cursor; + AWS_ZERO_STRUCT(rule_name_segment_cursor); + + if (!aws_byte_cursor_next_split(&topic_cursor_copy, '/', &rule_name_segment_cursor)) { + return topic_cursor; + } + + if (topic_cursor_copy.len < rule_name_segment_cursor.len + 1) { + return topic_cursor; + } + + aws_byte_cursor_advance(&topic_cursor_copy, rule_name_segment_cursor.len + 1); + + return topic_cursor_copy; +} + +size_t aws_mqtt5_topic_get_segment_count(struct aws_byte_cursor topic_cursor) { + size_t segment_count = 0; + + struct aws_byte_cursor segment_cursor; + AWS_ZERO_STRUCT(segment_cursor); + + while (aws_byte_cursor_next_split(&topic_cursor, '/', &segment_cursor)) { + ++segment_count; + } + + return segment_count; +} + +bool aws_mqtt_is_valid_topic_filter_for_iot_core(struct aws_byte_cursor topic_filter_cursor) { + struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_rules_prefix(topic_filter_cursor); + return aws_mqtt5_topic_get_segment_count(post_rule_suffix) <= AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS; +} + +bool aws_mqtt_is_valid_topic_for_iot_core(struct aws_byte_cursor topic_cursor) { + struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_rules_prefix(topic_cursor); + if (aws_mqtt5_topic_get_segment_count(post_rule_suffix) > AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS) { + return false; + } + + return post_rule_suffix.len <= AWS_IOT_CORE_MAXIMUM_TOPIC_LENGTH; +} + +static uint8_t s_shared_subscription_prefix[] = "$share"; + +static bool s_is_not_hash_or_plus(uint8_t byte) { + return byte != '+' && byte != '#'; +} + +/* $share/{ShareName}/{filter} */ +bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_cursor) { + + /* shared subscription filters must have an initial segment of "$share" */ + struct aws_byte_cursor first_segment_cursor; + AWS_ZERO_STRUCT(first_segment_cursor); + if (!aws_byte_cursor_next_split(&topic_cursor, '/', &first_segment_cursor)) { + return false; + } + + struct aws_byte_cursor share_prefix_cursor = { + .ptr = s_shared_subscription_prefix, + .len = AWS_ARRAY_SIZE(s_shared_subscription_prefix) - 1, /* skip null terminator */ + }; + + if (!aws_byte_cursor_eq_ignore_case(&share_prefix_cursor, &first_segment_cursor)) { + return false; + } + + /* + * The next segment must be non-empty and cannot include '#', '/', or '+'. In this case we know it already + * does not include '/' + */ + struct aws_byte_cursor second_segment_cursor = first_segment_cursor; + if (!aws_byte_cursor_next_split(&topic_cursor, '/', &second_segment_cursor)) { + return false; + } + + if (second_segment_cursor.len == 0 || + !aws_byte_cursor_satisfies_pred(&second_segment_cursor, s_is_not_hash_or_plus)) { + return false; + } + + /* + * Everything afterwards must form a normal, valid topic filter. + */ + struct aws_byte_cursor remaining_cursor = topic_cursor; + size_t remaining_length = + topic_cursor.ptr + topic_cursor.len - (second_segment_cursor.len + second_segment_cursor.ptr); + if (remaining_length == 0) { + return false; + } + + aws_byte_cursor_advance(&remaining_cursor, topic_cursor.len - remaining_length + 1); + + if (!aws_mqtt_is_valid_topic_filter(&remaining_cursor)) { + return false; + } + + return true; +} diff --git a/source/v5/rate_limiters.c b/source/v5/rate_limiters.c new file mode 100644 index 00000000..03e27906 --- /dev/null +++ b/source/v5/rate_limiters.c @@ -0,0 +1,217 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +static int s_rate_limit_time_fn(const struct aws_rate_limiter_token_bucket_options *options, uint64_t *current_time) { + if (options->clock_fn != NULL) { + return (*options->clock_fn)(current_time); + } + + return aws_high_res_clock_get_ticks(current_time); +} + +int aws_rate_limiter_token_bucket_init( + struct aws_rate_limiter_token_bucket *limiter, + const struct aws_rate_limiter_token_bucket_options *options) { + AWS_ZERO_STRUCT(*limiter); + + if (options->tokens_per_second == 0 || options->maximum_token_count == 0) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + limiter->config = *options; + + aws_rate_limiter_token_bucket_reset(limiter); + + return AWS_OP_SUCCESS; +} + +void aws_rate_limiter_token_bucket_reset(struct aws_rate_limiter_token_bucket *limiter) { + + limiter->current_token_count = + aws_min_u64(limiter->config.initial_token_count, limiter->config.maximum_token_count); + limiter->fractional_nanos = 0; + limiter->fractional_nano_tokens = 0; + + uint64_t now = 0; + AWS_FATAL_ASSERT(s_rate_limit_time_fn(&limiter->config, &now) == AWS_OP_SUCCESS); + + limiter->last_service_time = now; +} + +static void s_regenerate_tokens(struct aws_rate_limiter_token_bucket *limiter) { + uint64_t now = 0; + AWS_FATAL_ASSERT(s_rate_limit_time_fn(&limiter->config, &now) == AWS_OP_SUCCESS); + + if (now <= limiter->last_service_time) { + return; + } + + uint64_t nanos_elapsed = now - limiter->last_service_time; + + /* + * We break the regeneration calculation into two distinct steps: + * (1) Perform regeneration based on whole seconds elapsed (nice and easy just multiply times the regen rate) + * (2) Perform regeneration based on the remaining fraction of a second elapsed + * + * We do this to minimize the chances of multiplication saturation before the divide necessary to normalize to + * nanos. + * + * In particular, by doing this, we won't see saturation unless a regeneration rate in the multi-billions is used + * or elapsed_seconds is in the billions. This is similar reasoning to what we do in aws_timestamp_convert_u64. + * + * Additionally, we use a (sub-second) fractional counter/accumulator (fractional_nanos, fractional_nano_tokens) + * in order to prevent error accumulation due to integer division rounding. + */ + + /* break elapsed time into seconds and remainder nanos */ + uint64_t remainder_nanos = 0; + uint64_t elapsed_seconds = + aws_timestamp_convert(nanos_elapsed, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, &remainder_nanos); + + /* apply seconds-based regeneration */ + uint64_t tokens_regenerated = aws_mul_u64_saturating(elapsed_seconds, limiter->config.tokens_per_second); + + /* apply fractional remainder regeneration */ + limiter->fractional_nanos += remainder_nanos; + + /* fractional overflow check */ + if (limiter->fractional_nanos < AWS_TIMESTAMP_NANOS) { + /* + * no overflow, just do the division to figure out how many tokens are represented by the updated + * fractional nanos + */ + uint64_t new_fractional_tokens = + aws_mul_u64_saturating(limiter->fractional_nanos, limiter->config.tokens_per_second) / AWS_TIMESTAMP_NANOS; + + /* + * update token count by how much fractional tokens changed + */ + tokens_regenerated += new_fractional_tokens - limiter->fractional_nano_tokens; + limiter->fractional_nano_tokens = new_fractional_tokens; + } else { + /* + * overflow. In this case, update token count by the remaining tokens left to regenerate to make the + * original fractional nano amount equal to one second. This is the key part (a pseudo-reset) that lets us + * avoid error accumulation due to integer division rounding over time. + */ + tokens_regenerated += limiter->config.tokens_per_second - limiter->fractional_nano_tokens; + + /* + * subtract off a second from the fractional part. Guaranteed to be less than a second afterwards. + */ + limiter->fractional_nanos -= AWS_TIMESTAMP_NANOS; + + /* + * Calculate the new fractional nano token amount, and add them in. + */ + limiter->fractional_nano_tokens = + aws_mul_u64_saturating(limiter->fractional_nanos, limiter->config.tokens_per_second) / AWS_TIMESTAMP_NANOS; + tokens_regenerated += limiter->fractional_nano_tokens; + } + + limiter->current_token_count = aws_add_u64_saturating(tokens_regenerated, limiter->current_token_count); + if (limiter->current_token_count > limiter->config.maximum_token_count) { + limiter->current_token_count = limiter->config.maximum_token_count; + } + + limiter->last_service_time = now; +} + +bool aws_rate_limiter_token_bucket_can_take_tokens( + struct aws_rate_limiter_token_bucket *limiter, + uint64_t token_count) { + s_regenerate_tokens(limiter); + + return limiter->current_token_count >= token_count; +} + +int aws_rate_limiter_token_bucket_take_tokens(struct aws_rate_limiter_token_bucket *limiter, uint64_t token_count) { + s_regenerate_tokens(limiter); + + if (limiter->current_token_count < token_count) { + /* TODO: correct error once seated in aws-c-common */ + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + limiter->current_token_count -= token_count; + return AWS_OP_SUCCESS; +} + +uint64_t aws_rate_limiter_token_bucket_compute_wait_for_tokens( + struct aws_rate_limiter_token_bucket *limiter, + uint64_t token_count) { + s_regenerate_tokens(limiter); + + if (limiter->current_token_count >= token_count) { + return 0; + } + + uint64_t token_rate = limiter->config.tokens_per_second; + AWS_FATAL_ASSERT(limiter->fractional_nanos < AWS_TIMESTAMP_NANOS); + AWS_FATAL_ASSERT(limiter->fractional_nano_tokens <= token_rate); + + uint64_t expected_wait = 0; + + uint64_t deficit = token_count - limiter->current_token_count; + uint64_t remaining_fractional_tokens = token_rate - limiter->fractional_nano_tokens; + + if (deficit < remaining_fractional_tokens) { + /* + * case 1: + * The token deficit is less than what will be regenerated by waiting for the fractional nanos accumulator + * to reach one second's worth of time. + * + * In this case, base the calculation off of just a wait from fractional nanos. + */ + uint64_t target_fractional_tokens = aws_add_u64_saturating(deficit, limiter->fractional_nano_tokens); + uint64_t remainder_wait_unnormalized = aws_mul_u64_saturating(target_fractional_tokens, AWS_TIMESTAMP_NANOS); + + expected_wait = remainder_wait_unnormalized / token_rate - limiter->fractional_nanos; + + /* If the fractional wait is itself, fractional, then add one more nano second to push us over the edge */ + if (remainder_wait_unnormalized % token_rate) { + ++expected_wait; + } + } else { + /* + * case 2: + * The token deficit requires regeneration for a time interval at least as large as what is needed + * to overflow the fractional nanos accumulator. + */ + + /* First account for making the fractional nano accumulator exactly one second */ + expected_wait = AWS_TIMESTAMP_NANOS - limiter->fractional_nanos; + deficit -= remaining_fractional_tokens; + + /* + * Now, for the remaining tokens, split into tokens from whole seconds worth of regeneration as well + * as a remainder requiring a fractional regeneration + */ + uint64_t expected_wait_seconds = deficit / token_rate; + uint64_t deficit_remainder = deficit % token_rate; + + /* + * Account for seconds worth of waiting + */ + expected_wait += aws_mul_u64_saturating(expected_wait_seconds, AWS_TIMESTAMP_NANOS); + + /* + * And finally, calculate the fractional wait to give us the last few tokens + */ + uint64_t remainder_wait_unnormalized = aws_mul_u64_saturating(deficit_remainder, AWS_TIMESTAMP_NANOS); + expected_wait += remainder_wait_unnormalized / token_rate; + + /* If the fractional wait is itself, fractional, then add one more nano second to push us over the edge */ + if (remainder_wait_unnormalized % token_rate) { + ++expected_wait; + } + } + + return expected_wait; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 21e961d7..6dbf76cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,8 +3,8 @@ include(AwsTestHarness) include(AwsLibFuzzer) enable_testing() -file(GLOB TEST_HDRS "*.h") -set(TEST_SRC packet_encoding_test.c topic_tree_test.c connection_state_test.c mqtt_mock_server_handler.c) +file(GLOB TEST_HDRS "v3/*.h v5/*.h") +set(TEST_SRC v3/*.c v5/*.c) file(GLOB TESTS ${TEST_HDRS} ${TEST_SRC}) add_test_case(mqtt_packet_puback) @@ -62,11 +62,257 @@ add_test_case(mqtt_connection_publish_QoS1_timeout) add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) +# MQTT5 tests + +# topic utilities +add_test_case(mqtt5_topic_skip_rules_prefix) +add_test_case(mqtt5_topic_get_segment_count) +add_test_case(mqtt5_shared_subscription_validation) + +# topic aliasing +add_test_case(mqtt5_inbound_topic_alias_register_failure) +add_test_case(mqtt5_inbound_topic_alias_resolve_success) +add_test_case(mqtt5_inbound_topic_alias_resolve_failure) +add_test_case(mqtt5_inbound_topic_alias_reset) +add_test_case(mqtt5_outbound_topic_alias_disabled_resolve_success) +add_test_case(mqtt5_outbound_topic_alias_disabled_resolve_failure) +add_test_case(mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias) +add_test_case(mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias) +add_test_case(mqtt5_outbound_topic_alias_user_resolve_success) +add_test_case(mqtt5_outbound_topic_alias_user_reset) +add_test_case(mqtt5_outbound_topic_alias_lru_zero_size) + +# lru topic sequence tests +# cache size of 2 +# a, b, c, refer to distinct topics +# the 'r' suffice refers to expected alias reuse +add_test_case(mqtt5_outbound_topic_alias_lru_a_ar) +add_test_case(mqtt5_outbound_topic_alias_lru_b_a_br) +add_test_case(mqtt5_outbound_topic_alias_lru_a_b_ar_br) +add_test_case(mqtt5_outbound_topic_alias_lru_a_b_c_br_cr_br_cr_a) +add_test_case(mqtt5_outbound_topic_alias_lru_a_b_c_a_cr_b) +add_test_case(mqtt5_outbound_topic_alias_lru_a_b_reset_a_b) + +# mqtt operation/storage/view creation/relationship tests +add_test_case(mqtt5_publish_operation_new_set_no_optional) +add_test_case(mqtt5_publish_operation_new_set_all) +add_test_case(mqtt5_publish_operation_new_failure_packet_id) +add_test_case(mqtt5_subscribe_operation_new_set_no_optional) +add_test_case(mqtt5_subscribe_operation_new_set_all) +add_test_case(mqtt5_unsubscribe_operation_new_set_all) +add_test_case(mqtt5_connect_storage_new_set_no_optional) +add_test_case(mqtt5_connect_storage_new_set_all) +add_test_case(mqtt5_connack_storage_new_set_no_optional) +add_test_case(mqtt5_connack_storage_new_set_all) +add_test_case(mqtt5_disconnect_storage_new_set_no_optional) +add_test_case(mqtt5_disconnect_storage_new_set_all) +add_test_case(mqtt5_suback_storage_new_set_no_optional) +add_test_case(mqtt5_suback_storage_new_set_all) +add_test_case(mqtt5_unsuback_storage_new_set_no_optional) +add_test_case(mqtt5_unsuback_storage_new_set_all) +add_test_case(mqtt5_puback_storage_new_set_all) +add_test_case(mqtt5_publish_storage_new_set_all) + +# operation/view validation failure tests +add_test_case(mqtt5_operation_disconnect_validation_failure_server_reference) +add_test_case(mqtt5_operation_disconnect_validation_failure_bad_reason_code) +add_test_case(mqtt5_operation_disconnect_validation_failure_reason_string_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_too_many) +add_test_case(mqtt5_operation_connect_validation_failure_client_id_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_username_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_password_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_receive_maximum_zero) +add_test_case(mqtt5_operation_connect_validation_failure_maximum_packet_size_zero) +add_test_case(mqtt5_operation_connect_validation_failure_will_invalid) +add_test_case(mqtt5_operation_connect_validation_failure_will_payload_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_auth_method_unsupported) +add_test_case(mqtt5_operation_connect_validation_failure_auth_data_unsupported) +add_test_case(mqtt5_operation_connect_validation_failure_request_problem_information_invalid) +add_test_case(mqtt5_operation_connect_validation_failure_request_response_information_invalid) +add_test_case(mqtt5_operation_connect_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_user_properties_too_many) +add_test_case(mqtt5_operation_subscribe_validation_failure_no_subscriptions) +add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscriptions) +add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscriptions_for_iot_core) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_subscription_identifier) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter_for_iot_core) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_qos) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_retain_type) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_no_local) +add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_too_many) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_no_topic_filters) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_too_many_topic_filters) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter_for_iot_core) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_too_many) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_topic) +add_test_case(mqtt5_operation_publish_validation_failure_topic_too_long_for_iot_core) +add_test_case(mqtt5_operation_publish_validation_failure_topic_too_many_slashes_for_iot_core) +add_test_case(mqtt5_operation_publish_validation_failure_no_topic) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_payload_format) +add_test_case(mqtt5_operation_publish_validation_failure_response_topic_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_response_topic) +add_test_case(mqtt5_operation_publish_validation_failure_correlation_data_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_subscription_identifier_exists) +add_test_case(mqtt5_operation_publish_validation_failure_topic_alias_zero) +add_test_case(mqtt5_operation_publish_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_user_properties_too_many) +add_test_case(mqtt5_operation_publish_validation_failure_qos0_duplicate_true) +add_test_case(mqtt5_operation_publish_validation_failure_qos0_with_packet_id) +add_test_case(mqtt5_client_options_validation_failure_no_host) +add_test_case(mqtt5_client_options_validation_failure_no_bootstrap) +add_test_case(mqtt5_client_options_validation_failure_no_publish_received) +add_test_case(mqtt5_client_options_validation_failure_invalid_socket_options) +add_test_case(mqtt5_client_options_validation_failure_invalid_connect) +add_test_case(mqtt5_client_options_validation_failure_invalid_keep_alive) +add_test_case(mqtt5_client_options_validation_failure_client_id_too_long_for_iot_core) +add_test_case(mqtt5_operation_subscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) +add_test_case(mqtt5_operation_unsubscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) +add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_packet_size) +add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_topic_alias_maximum) +add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_qos) +add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_invalid_retain) +add_test_case(mqtt5_operation_disconnect_connection_settings_validation_failure_exceeds_maximum_packet_size) +add_test_case(mqtt5_operation_disconnect_connection_settings_validation_failure_promote_zero_session_expiry) + +add_test_case(mqtt5_client_options_defaults_set) + +add_test_case(mqtt5_operation_bind_packet_id_empty_table) +add_test_case(mqtt5_operation_bind_packet_id_multiple_with_existing) +add_test_case(mqtt5_operation_bind_packet_id_multiple_with_wrap_around) +add_test_case(mqtt5_operation_bind_packet_id_full_table) +add_test_case(mqtt5_operation_bind_packet_id_not_valid) +add_test_case(mqtt5_operation_bind_packet_id_already_bound) + +add_test_case(mqtt5_operation_processing_nothing_empty_queue) +add_test_case(mqtt5_operation_processing_nothing_mqtt_connect) +add_test_case(mqtt5_operation_processing_nothing_clean_disconnect) +add_test_case(mqtt5_operation_processing_nothing_pending_write_completion_mqtt_connect) +add_test_case(mqtt5_operation_processing_nothing_pending_write_completion_connected) +add_test_case(mqtt5_operation_processing_nothing_pending_write_completion_clean_disconnect) +add_test_case(mqtt5_operation_processing_failure_message_allocation) +add_test_case(mqtt5_operation_processing_failure_message_send) +add_test_case(mqtt5_operation_processing_something_mqtt_connect) +add_test_case(mqtt5_operation_processing_something_clean_disconnect) +add_test_case(mqtt5_operation_processing_something_connected_multi) +add_test_case(mqtt5_operation_processing_something_connected_overflow) +add_test_case(mqtt5_operation_processing_disconnect_fail_all) +add_test_case(mqtt5_operation_processing_disconnect_fail_qos0) +add_test_case(mqtt5_operation_processing_disconnect_fail_non_qos1) +add_test_case(mqtt5_operation_processing_reconnect_rejoin_session_fail_all) +add_test_case(mqtt5_operation_processing_reconnect_rejoin_session_fail_qos0) +# intentionally skip the non_qos1 rejoin session case, there's no meaningful test given the logic +add_test_case(mqtt5_operation_processing_reconnect_no_session_fail_all) +add_test_case(mqtt5_operation_processing_reconnect_no_session_fail_qos0) +add_test_case(mqtt5_operation_processing_reconnect_no_session_fail_non_qos1) + +add_test_case(mqtt5_negotiated_settings_reset_test) +add_test_case(mqtt5_negotiated_settings_apply_connack_test) +add_test_case(mqtt5_negotiated_settings_server_override_test) + +# vli encode/decode +add_test_case(mqtt5_vli_size) +add_test_case(mqtt5_vli_success_round_trip) +add_test_case(mqtt5_vli_encode_failures) +add_test_case(mqtt5_vli_decode_failures) + +# packet encode/decode cycle tests +add_test_case(mqtt5_packet_disconnect_round_trip) +add_test_case(mqtt5_packet_pingreq_round_trip) +add_test_case(mqtt5_packet_pingresp_round_trip) +add_test_case(mqtt5_packet_connect_round_trip) +add_test_case(mqtt5_packet_connack_round_trip) +add_test_case(mqtt5_packet_subscribe_round_trip) +add_test_case(mqtt5_packet_suback_round_trip) +add_test_case(mqtt5_packet_unsubscribe_round_trip) +add_test_case(mqtt5_packet_unsuback_round_trip) +add_test_case(mqtt5_packet_publish_round_trip) +add_test_case(mqtt5_packet_puback_round_trip) +add_test_case(mqtt5_packet_encode_connect_no_will) +add_test_case(mqtt5_packet_encode_connect_no_username) +add_test_case(mqtt5_packet_encode_connect_no_password) +add_test_case(mqtt5_packet_encode_connect_will_property_order) +add_test_case(mqtt5_first_byte_reserved_header_check_subscribe) +add_test_case(mqtt5_first_byte_reserved_header_check_unsubscribe) +add_test_case(mqtt5_first_byte_reserved_header_check_disconnect) + +add_test_case(mqtt5_client_direct_connect_success) +add_test_case(mqtt5_client_direct_connect_sync_channel_failure) +add_test_case(mqtt5_client_direct_connect_async_channel_failure) +add_test_case(mqtt5_client_websocket_connect_sync_channel_failure) +add_test_case(mqtt5_client_websocket_connect_async_channel_failure) +add_test_case(mqtt5_client_websocket_connect_handshake_failure) +add_test_case(mqtt5_client_direct_connect_connack_refusal) +add_test_case(mqtt5_client_direct_connect_connack_timeout) +add_test_case(mqtt5_client_direct_connect_from_server_disconnect) +add_test_case(mqtt5_client_subscribe_success) +add_test_case(mqtt5_client_unsubscribe_success) +add_test_case(mqtt5_client_sub_pub_unsub_qos0) +add_test_case(mqtt5_client_sub_pub_unsub_qos1) +add_test_case(mqtt5_client_ping_sequence) +add_test_case(mqtt5_client_ping_timeout) +add_test_case(mqtt5_client_reconnect_failure_backoff) +add_test_case(mqtt5_client_reconnect_backoff_insufficient_reset) +add_test_case(mqtt5_client_reconnect_backoff_sufficient_reset) +add_test_case(mqtt5_client_subscribe_fail_packet_too_big) +add_test_case(mqtt5_client_disconnect_fail_packet_too_big) +add_test_case(mqtt5_client_flow_control_receive_maximum) +add_test_case(mqtt5_client_publish_timeout) +add_test_case(mqtt5_client_flow_control_iot_core_throughput) +add_test_case(mqtt5_client_flow_control_iot_core_publish_tps) +add_test_case(mqtt5_client_session_resumption_clean_start) +add_test_case(mqtt5_client_session_resumption_post_success) +add_test_case(mqtt5_client_receive_qos1_return_puback_test) +add_test_case(mqtt5_client_receive_nonexisting_session_state) +add_test_case(mqtt5_client_receive_assigned_client_id) +add_test_case(mqtt5_client_no_session_after_client_stop) +add_test_case(mqtt5_client_restore_session_on_ping_timeout_reconnect) +add_test_case(mqtt5_client_discard_session_on_server_clean_start) +add_test_case(mqtt5_client_statistics_subscribe) +add_test_case(mqtt5_client_statistics_unsubscribe) +add_test_case(mqtt5_client_statistics_publish_qos0) +add_test_case(mqtt5_client_statistics_publish_qos1) +add_test_case(mqtt5_client_statistics_publish_qos1_requeue) +add_test_case(mqtt5_client_puback_ordering) +add_test_case(mqtt5_client_offline_operation_submission_fail_all) +add_test_case(mqtt5_client_offline_operation_submission_fail_qos0) +add_test_case(mqtt5_client_offline_operation_submission_fail_non_qos1) +add_test_case(mqtt5_client_offline_operation_submission_then_connect) +add_test_case(mqtt5_client_inbound_alias_success) +add_test_case(mqtt5_client_inbound_alias_failure_disabled) +add_test_case(mqtt5_client_inbound_alias_failure_zero_id) +add_test_case(mqtt5_client_inbound_alias_failure_too_large_id) +add_test_case(mqtt5_client_inbound_alias_failure_unbound_id) +add_test_case(mqtt5_client_outbound_alias_disabled_failure_alias_set) +add_test_case(mqtt5_client_outbound_alias_user_failure_empty_topic) +add_test_case(mqtt5_client_outbound_alias_lru_failure_alias_set) + +# a, b, c, r imply notation as the outbound resolver unit tests above +add_test_case(mqtt5_client_outbound_alias_user_success_a_b_ar_br) +add_test_case(mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a) + +add_test_case(rate_limiter_token_bucket_init_invalid) +add_test_case(rate_limiter_token_bucket_regeneration_integral) +add_test_case(rate_limiter_token_bucket_regeneration_fractional) +add_test_case(rate_limiter_token_bucket_fractional_iteration) +add_test_case(rate_limiter_token_bucket_large_fractional_iteration) +add_test_case(rate_limiter_token_bucket_real_iteration) +add_test_case(rate_limiter_token_bucket_reset) + generate_test_driver(${PROJECT_NAME}-tests) set(TEST_PAHO_CLIENT_BINARY_NAME ${PROJECT_NAME}-paho-client) -add_executable(${TEST_PAHO_CLIENT_BINARY_NAME} "paho_client_test.c") +add_executable(${TEST_PAHO_CLIENT_BINARY_NAME} "v3-client/paho_client_test.c") target_link_libraries(${TEST_PAHO_CLIENT_BINARY_NAME} PRIVATE ${PROJECT_NAME}) aws_set_common_properties(${TEST_PAHO_CLIENT_BINARY_NAME}) aws_add_sanitizers(${TEST_PAHO_CLIENT_BINARY_NAME} ${${PROJECT_NAME}_SANITIZERS}) @@ -75,7 +321,7 @@ target_include_directories(${TEST_PAHO_CLIENT_BINARY_NAME} PRIVATE ${CMAKE_CURRE set(TEST_IOT_CLIENT_BINARY_NAME ${PROJECT_NAME}-iot-client) -add_executable(${TEST_IOT_CLIENT_BINARY_NAME} "aws_iot_client_test.c") +add_executable(${TEST_IOT_CLIENT_BINARY_NAME} "v3-client/aws_iot_client_test.c") target_link_libraries(${TEST_IOT_CLIENT_BINARY_NAME} PRIVATE ${PROJECT_NAME}) aws_set_common_properties(${TEST_IOT_CLIENT_BINARY_NAME}) aws_add_sanitizers(${TEST_IOT_CLIENT_BINARY_NAME} ${${PROJECT_NAME}_SANITIZERS}) diff --git a/tests/aws_iot_client_test.c b/tests/v3-client/aws_iot_client_test.c similarity index 97% rename from tests/aws_iot_client_test.c rename to tests/v3-client/aws_iot_client_test.c index 4db07480..5b3fea36 100644 --- a/tests/aws_iot_client_test.c +++ b/tests/v3-client/aws_iot_client_test.c @@ -23,8 +23,6 @@ #include #include -#include - #include #include #ifdef WIN32 @@ -351,7 +349,7 @@ int s_initialize_test( return AWS_OP_SUCCESS; } -int s_cleanup_test(struct test_context *tester) { +static void s_cleanup_test(struct test_context *tester) { aws_tls_connection_options_clean_up(&tester->tls_connection_options); aws_mqtt_client_connection_release(tester->connection); @@ -372,8 +370,6 @@ int s_cleanup_test(struct test_context *tester) { aws_condition_variable_clean_up(&tester->signal); aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; } int main(int argc, char **argv) { @@ -437,15 +433,15 @@ int main(int argc, char **argv) { s_wait_on_tester_predicate(&tester, s_all_packets_received_cond); - ASSERT_UINT_EQUALS(PUBLISHES, tester.packets_gotten); + AWS_FATAL_ASSERT(PUBLISHES == tester.packets_gotten); aws_mqtt_client_connection_disconnect(tester.connection, s_mqtt_on_disconnect, &tester); s_wait_on_tester_predicate(&tester, s_received_on_disconnect_pred); - ASSERT_SUCCESS(s_cleanup_test(&tester)); + s_cleanup_test(&tester); - ASSERT_UINT_EQUALS(0, aws_mem_tracer_count(allocator)); + AWS_FATAL_ASSERT(0 == aws_mem_tracer_count(allocator)); allocator = aws_mem_tracer_destroy(allocator); return AWS_OP_SUCCESS; diff --git a/tests/paho_client_test.c b/tests/v3-client/paho_client_test.c similarity index 97% rename from tests/paho_client_test.c rename to tests/v3-client/paho_client_test.c index e0966172..60d87ba1 100644 --- a/tests/paho_client_test.c +++ b/tests/v3-client/paho_client_test.c @@ -20,8 +20,6 @@ #include #include -#include - #include #include #ifdef WIN32 @@ -291,12 +289,12 @@ static int s_initialize_test( aws_mqtt_client_connection_set_on_any_publish_handler(tester->connection, s_on_any_packet_received, tester); - ASSERT_SUCCESS(aws_mqtt_client_connection_connect(tester->connection, conn_options)); + AWS_FATAL_ASSERT(aws_mqtt_client_connection_connect(tester->connection, conn_options) == AWS_OP_SUCCESS); return AWS_OP_SUCCESS; } -static int s_cleanup_test(struct test_context *tester) { +static void s_cleanup_test(struct test_context *tester) { aws_mqtt_client_connection_release(tester->connection); aws_mqtt_client_release(tester->client); @@ -311,8 +309,6 @@ static int s_cleanup_test(struct test_context *tester) { aws_condition_variable_clean_up(&tester->signal); aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; } int main(int argc, char **argv) { @@ -421,7 +417,7 @@ int main(int argc, char **argv) { s_wait_on_tester_predicate(&tester, s_on_disconnect_received); - ASSERT_SUCCESS(s_cleanup_test(&tester)); + s_cleanup_test(&tester); AWS_FATAL_ASSERT(0 == aws_mem_tracer_count(allocator)); allocator = aws_mem_tracer_destroy(allocator); diff --git a/tests/connection_state_test.c b/tests/v3/connection_state_test.c similarity index 100% rename from tests/connection_state_test.c rename to tests/v3/connection_state_test.c diff --git a/tests/mqtt_mock_server_handler.c b/tests/v3/mqtt_mock_server_handler.c similarity index 100% rename from tests/mqtt_mock_server_handler.c rename to tests/v3/mqtt_mock_server_handler.c diff --git a/tests/mqtt_mock_server_handler.h b/tests/v3/mqtt_mock_server_handler.h similarity index 100% rename from tests/mqtt_mock_server_handler.h rename to tests/v3/mqtt_mock_server_handler.h diff --git a/tests/packet_encoding_test.c b/tests/v3/packet_encoding_test.c similarity index 100% rename from tests/packet_encoding_test.c rename to tests/v3/packet_encoding_test.c diff --git a/tests/topic_tree_test.c b/tests/v3/topic_tree_test.c similarity index 100% rename from tests/topic_tree_test.c rename to tests/v3/topic_tree_test.c diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c new file mode 100644 index 00000000..662d6116 --- /dev/null +++ b/tests/v5/mqtt5_client_tests.c @@ -0,0 +1,5825 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt5_testing_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#define TEST_IO_MESSAGE_LENGTH 4096 + +static bool s_is_within_percentage_of(uint64_t expected_time, uint64_t actual_time, double percentage) { + double actual_percent = 1.0 - (double)actual_time / (double)expected_time; + return fabs(actual_percent) <= percentage; +} + +static int s_aws_mqtt5_mock_server_send_packet( + struct aws_mqtt5_server_mock_connection_context *connection, + enum aws_mqtt5_packet_type packet_type, + void *packet) { + aws_mqtt5_encoder_append_packet_encoding(&connection->encoder, packet_type, packet); + + struct aws_io_message *message = aws_channel_acquire_message_from_pool( + connection->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, TEST_IO_MESSAGE_LENGTH); + if (message == NULL) { + return AWS_OP_ERR; + } + + enum aws_mqtt5_encoding_result result = + aws_mqtt5_encoder_encode_to_buffer(&connection->encoder, &message->message_data); + AWS_FATAL_ASSERT(result == AWS_MQTT5_ER_FINISHED); + + if (aws_channel_slot_send_message(connection->slot, message, AWS_CHANNEL_DIR_WRITE)) { + aws_mem_release(message->allocator, message); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_mock_server_handle_connect_always_succeed( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +static int s_aws_mqtt5_mock_server_handle_pingreq_always_respond( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PINGRESP, NULL); +} + +static int s_aws_mqtt5_mock_server_handle_disconnect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)connection; + (void)user_data; + + return AWS_OP_SUCCESS; +} + +void s_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_client_id, "HelloWorld"); + +struct mqtt5_client_test_options { + struct aws_mqtt5_client_topic_alias_options topic_aliasing_options; + struct aws_mqtt5_packet_connect_view connect_options; + struct aws_mqtt5_client_options client_options; + struct aws_mqtt5_mock_server_vtable server_function_table; +}; + +static void s_mqtt5_client_test_init_default_options(struct mqtt5_client_test_options *test_options) { + + struct aws_mqtt5_client_topic_alias_options local_topic_aliasing_options = { + .outbound_topic_alias_behavior = AWS_MQTT5_COTABT_DISABLED, + }; + + test_options->topic_aliasing_options = local_topic_aliasing_options; + + struct aws_mqtt5_packet_connect_view local_connect_options = { + .keep_alive_interval_seconds = 30, + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = true, + }; + + test_options->connect_options = local_connect_options; + + struct aws_mqtt5_client_options local_client_options = { + .connect_options = &test_options->connect_options, + .session_behavior = AWS_MQTT5_CSBT_CLEAN, + .lifecycle_event_handler = s_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .max_reconnect_delay_ms = 120000, + .min_connected_time_to_reset_reconnect_delay_ms = 30000, + .min_reconnect_delay_ms = 1000, + .ping_timeout_ms = 10000, + .publish_received_handler = s_publish_received_callback, + .ack_timeout_seconds = 0, + .topic_aliasing_options = &test_options->topic_aliasing_options, + }; + + test_options->client_options = local_client_options; + + struct aws_mqtt5_mock_server_vtable local_server_function_table = { + .packet_handlers = { + NULL, /* RESERVED = 0 */ + &s_aws_mqtt5_mock_server_handle_connect_always_succeed, /* CONNECT */ + NULL, /* CONNACK */ + NULL, /* PUBLISH */ + NULL, /* PUBACK */ + NULL, /* PUBREC */ + NULL, /* PUBREL */ + NULL, /* PUBCOMP */ + NULL, /* SUBSCRIBE */ + NULL, /* SUBACK */ + NULL, /* UNSUBSCRIBE */ + NULL, /* UNSUBACK */ + &s_aws_mqtt5_mock_server_handle_pingreq_always_respond, /* PINGREQ */ + NULL, /* PINGRESP */ + &s_aws_mqtt5_mock_server_handle_disconnect, /* DISCONNECT */ + NULL /* AUTH */ + }}; + + test_options->server_function_table = local_server_function_table; +} + +static int s_aws_mqtt5_client_test_init_default_connect_storage( + struct aws_mqtt5_packet_connect_storage *storage, + struct aws_allocator *allocator) { + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 30, + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = true, + }; + + return aws_mqtt5_packet_connect_storage_init(storage, allocator, &connect_view); +} + +static int s_aws_mqtt5_client_test_init_default_disconnect_storage( + struct aws_mqtt5_packet_disconnect_storage *storage, + struct aws_allocator *allocator) { + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_code = AWS_MQTT5_DRC_NORMAL_DISCONNECTION, + }; + + return aws_mqtt5_packet_disconnect_storage_init(storage, allocator, &disconnect_view); +} + +static bool s_last_life_cycle_event_is( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + enum aws_mqtt5_client_lifecycle_event_type event_type) { + size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); + if (event_count == 0) { + return false; + } + + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, event_count - 1); + + return record->event.event_type == event_type; +} + +static bool s_last_mock_server_packet_received_is( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + enum aws_mqtt5_packet_type packet_type) { + size_t packet_count = aws_array_list_length(&test_fixture->server_received_packets); + if (packet_count == 0) { + return false; + } + + struct aws_mqtt5_mock_server_packet_record *packet = NULL; + aws_array_list_get_at_ptr(&test_fixture->server_received_packets, (void **)&packet, packet_count - 1); + + return packet_type == packet->packet_type; +} + +static bool s_last_mock_server_packet_received_is_disconnect(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return s_last_mock_server_packet_received_is(test_fixture, AWS_MQTT5_PT_DISCONNECT); +} + +static void s_wait_for_mock_server_to_receive_disconnect_packet( + struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_last_mock_server_packet_received_is_disconnect, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_last_lifecycle_event_is_connected(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return s_last_life_cycle_event_is(test_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS); +} + +static void s_wait_for_connected_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_last_lifecycle_event_is_connected, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_last_lifecycle_event_is_stopped(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return s_last_life_cycle_event_is(test_fixture, AWS_MQTT5_CLET_STOPPED); +} + +static void s_wait_for_stopped_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_last_lifecycle_event_is_stopped, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_has_lifecycle_event( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + enum aws_mqtt5_client_lifecycle_event_type event_type) { + + size_t record_count = aws_array_list_length(&test_fixture->lifecycle_events); + for (size_t i = 0; i < record_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + if (record->event.event_type == event_type) { + return true; + } + } + + return false; +} + +static bool s_has_connection_failure_event(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return s_has_lifecycle_event(test_fixture, AWS_MQTT5_CLET_CONNECTION_FAILURE); +} + +static void s_wait_for_connection_failure_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_has_connection_failure_event, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_has_disconnect_event(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return s_has_lifecycle_event(test_fixture, AWS_MQTT5_CLET_DISCONNECTION); +} + +static void s_wait_for_disconnection_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred(&test_context->signal, &test_context->lock, s_has_disconnect_event, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_disconnect_completion_invoked(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + return test_fixture->disconnect_completion_callback_invoked; +} + +static void s_on_disconnect_completion(int error_code, void *user_data) { + (void)error_code; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = user_data; + + aws_mutex_lock(&test_fixture->lock); + test_fixture->disconnect_completion_callback_invoked = true; + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static void s_wait_for_disconnect_completion(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_disconnect_completion_invoked, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static int s_verify_client_state_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_context, + enum aws_mqtt5_client_state *expected_states, + size_t expected_states_count) { + aws_mutex_lock(&test_context->lock); + + size_t actual_states_count = aws_array_list_length(&test_context->client_states); + ASSERT_TRUE(actual_states_count >= expected_states_count); + + for (size_t i = 0; i < expected_states_count; ++i) { + enum aws_mqtt5_client_state state = AWS_MCS_STOPPED; + aws_array_list_get_at(&test_context->client_states, &state, i); + + ASSERT_INT_EQUALS(expected_states[i], state); + } + + aws_mutex_unlock(&test_context->lock); + + return AWS_OP_SUCCESS; +} + +static int s_verify_simple_lifecycle_event_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_context, + struct aws_mqtt5_client_lifecycle_event *expected_events, + size_t expected_events_count) { + aws_mutex_lock(&test_context->lock); + + size_t actual_events_count = aws_array_list_length(&test_context->lifecycle_events); + ASSERT_TRUE(actual_events_count >= expected_events_count); + + for (size_t i = 0; i < expected_events_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *lifecycle_event = NULL; + aws_array_list_get_at(&test_context->lifecycle_events, &lifecycle_event, i); + + struct aws_mqtt5_client_lifecycle_event *expected_event = &expected_events[i]; + ASSERT_INT_EQUALS(expected_event->event_type, lifecycle_event->event.event_type); + ASSERT_INT_EQUALS(expected_event->error_code, lifecycle_event->event.error_code); + } + + aws_mutex_unlock(&test_context->lock); + + return AWS_OP_SUCCESS; +} + +static int s_verify_received_packet_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_context, + struct aws_mqtt5_mock_server_packet_record *expected_packets, + size_t expected_packets_count) { + aws_mutex_lock(&test_context->lock); + + size_t actual_packets_count = aws_array_list_length(&test_context->server_received_packets); + ASSERT_TRUE(actual_packets_count >= expected_packets_count); + + for (size_t i = 0; i < expected_packets_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *actual_packet = NULL; + aws_array_list_get_at_ptr(&test_context->server_received_packets, (void **)&actual_packet, i); + + struct aws_mqtt5_mock_server_packet_record *expected_packet = &expected_packets[i]; + + ASSERT_INT_EQUALS(expected_packet->packet_type, actual_packet->packet_type); + + /* a NULL storage means we don't care about verifying it on a field-by-field basis */ + if (expected_packet->packet_storage != NULL) { + ASSERT_TRUE(aws_mqtt5_client_test_are_packets_equal( + expected_packet->packet_type, expected_packet->packet_storage, actual_packet->packet_storage)); + } + } + + aws_mutex_unlock(&test_context->lock); + + return AWS_OP_SUCCESS; +} + +/* + * Basic successful connect/disconnect test. We check expected lifecycle events, internal client state changes, + * and server received packets. + */ +static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_disconnect_view disconnect_options = { + .reason_code = AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE, + }; + + struct aws_mqtt5_disconnect_completion_options completion_options = { + .completion_callback = s_on_disconnect_completion, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_options, &completion_options)); + + s_wait_for_stopped_lifecycle_event(&test_context); + s_wait_for_disconnect_completion(&test_context); + s_wait_for_mock_server_to_receive_disconnect_packet(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + }, + { + .event_type = AWS_MQTT5_CLET_STOPPED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CLEAN_DISCONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + AWS_MCS_STOPPED, + }; + + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + struct aws_mqtt5_packet_connect_storage expected_connect_storage; + ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_default_connect_storage(&expected_connect_storage, allocator)); + + struct aws_mqtt5_packet_disconnect_storage expected_disconnect_storage; + ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_default_disconnect_storage(&expected_disconnect_storage, allocator)); + expected_disconnect_storage.storage_view.reason_code = AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE; + + struct aws_mqtt5_mock_server_packet_record expected_packets[] = { + { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = &expected_connect_storage, + }, + { + .packet_type = AWS_MQTT5_PT_DISCONNECT, + .packet_storage = &expected_disconnect_storage, + }, + }; + ASSERT_SUCCESS( + s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_direct_connect_success, s_mqtt5_client_direct_connect_success_fn) + +/* + * Connection failure test infrastructure. Supplied callbacks are used to modify the way in which the connection + * establishment fails. + */ +static int s_mqtt5_client_simple_failure_test_fn( + struct aws_allocator *allocator, + void (*change_client_test_config_fn)(struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *config), + void (*change_client_vtable_fn)(struct aws_mqtt5_client_vtable *)) { + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + if (change_client_test_config_fn != NULL) { + (*change_client_test_config_fn)(&test_fixture_options); + } + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + struct aws_mqtt5_client_vtable vtable = *client->vtable; + if (change_client_vtable_fn != NULL) { + (*change_client_vtable_fn)(&vtable); + aws_mqtt5_client_set_vtable(client, &vtable); + } + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connection_failure_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_INVALID_STATE, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_PENDING_RECONNECT, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_test_synchronous_socket_channel_failure_fn( + struct aws_socket_channel_bootstrap_options *options) { + (void)options; + + return aws_raise_error(AWS_ERROR_INVALID_STATE); +} + +static void s_change_client_vtable_synchronous_direct_failure(struct aws_mqtt5_client_vtable *vtable) { + vtable->client_bootstrap_new_socket_channel_fn = s_mqtt5_client_test_synchronous_socket_channel_failure_fn; +} + +/* Connection failure test where direct MQTT channel establishment fails synchronously */ +static int s_mqtt5_client_direct_connect_sync_channel_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS( + s_mqtt5_client_simple_failure_test_fn(allocator, NULL, s_change_client_vtable_synchronous_direct_failure)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_direct_connect_sync_channel_failure, s_mqtt5_client_direct_connect_sync_channel_failure_fn) + +struct socket_channel_failure_wrapper { + struct aws_socket_channel_bootstrap_options bootstrap_options; + struct aws_task task; +}; + +static struct socket_channel_failure_wrapper s_socket_channel_failure_wrapper; + +void s_socket_channel_async_failure_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + if (status != AWS_TASK_STATUS_RUN_READY) { + return; + } + + struct socket_channel_failure_wrapper *wrapper = arg; + struct aws_socket_channel_bootstrap_options *options = &wrapper->bootstrap_options; + + (*wrapper->bootstrap_options.setup_callback)(options->bootstrap, AWS_ERROR_INVALID_STATE, NULL, options->user_data); +} + +static int s_mqtt5_client_test_asynchronous_socket_channel_failure_fn( + struct aws_socket_channel_bootstrap_options *options) { + aws_task_init( + &s_socket_channel_failure_wrapper.task, + s_socket_channel_async_failure_task_fn, + &s_socket_channel_failure_wrapper, + "asynchronous_socket_channel_failure"); + s_socket_channel_failure_wrapper.bootstrap_options = *options; + + struct aws_mqtt5_client *client = options->user_data; + aws_event_loop_schedule_task_now(client->loop, &s_socket_channel_failure_wrapper.task); + + return AWS_OP_SUCCESS; +} + +static void s_change_client_vtable_asynchronous_direct_failure(struct aws_mqtt5_client_vtable *vtable) { + vtable->client_bootstrap_new_socket_channel_fn = s_mqtt5_client_test_asynchronous_socket_channel_failure_fn; +} + +/* Connection failure test where direct MQTT channel establishment fails asynchronously */ +static int s_mqtt5_client_direct_connect_async_channel_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS( + s_mqtt5_client_simple_failure_test_fn(allocator, NULL, s_change_client_vtable_asynchronous_direct_failure)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_direct_connect_async_channel_failure, s_mqtt5_client_direct_connect_async_channel_failure_fn) + +static int s_mqtt5_client_test_synchronous_websocket_failure_fn( + const struct aws_websocket_client_connection_options *options) { + (void)options; + + return aws_raise_error(AWS_ERROR_INVALID_STATE); +} + +static void s_change_client_vtable_synchronous_websocket_failure(struct aws_mqtt5_client_vtable *vtable) { + vtable->websocket_connect_fn = s_mqtt5_client_test_synchronous_websocket_failure_fn; +} + +static void s_mqtt5_client_test_websocket_successful_transform( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + (void)user_data; + + (*complete_fn)(request, AWS_ERROR_SUCCESS, complete_ctx); +} + +static void s_change_client_options_to_websockets(struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *config) { + config->client_options->websocket_handshake_transform = s_mqtt5_client_test_websocket_successful_transform; +} + +/* Connection failure test where websocket MQTT channel establishment fails synchronously */ +static int s_mqtt5_client_websocket_connect_sync_channel_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_mqtt5_client_simple_failure_test_fn( + allocator, s_change_client_options_to_websockets, s_change_client_vtable_synchronous_websocket_failure)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_websocket_connect_sync_channel_failure, + s_mqtt5_client_websocket_connect_sync_channel_failure_fn) + +struct websocket_channel_failure_wrapper { + struct aws_websocket_client_connection_options websocket_options; + struct aws_task task; +}; + +static struct websocket_channel_failure_wrapper s_websocket_channel_failure_wrapper; + +void s_websocket_channel_async_failure_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + if (status != AWS_TASK_STATUS_RUN_READY) { + return; + } + + struct websocket_channel_failure_wrapper *wrapper = arg; + struct aws_websocket_client_connection_options *options = &wrapper->websocket_options; + + (*wrapper->websocket_options.on_connection_setup)(NULL, AWS_ERROR_INVALID_STATE, 0, NULL, 0, options->user_data); +} + +static int s_mqtt5_client_test_asynchronous_websocket_failure_fn( + const struct aws_websocket_client_connection_options *options) { + aws_task_init( + &s_websocket_channel_failure_wrapper.task, + s_websocket_channel_async_failure_task_fn, + &s_websocket_channel_failure_wrapper, + "asynchronous_websocket_channel_failure"); + s_websocket_channel_failure_wrapper.websocket_options = *options; + + struct aws_mqtt5_client *client = options->user_data; + aws_event_loop_schedule_task_now(client->loop, &s_websocket_channel_failure_wrapper.task); + + return AWS_OP_SUCCESS; +} + +static void s_change_client_vtable_asynchronous_websocket_failure(struct aws_mqtt5_client_vtable *vtable) { + vtable->websocket_connect_fn = s_mqtt5_client_test_asynchronous_websocket_failure_fn; +} + +/* Connection failure test where websocket MQTT channel establishment fails asynchronously */ +static int s_mqtt5_client_websocket_connect_async_channel_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_mqtt5_client_simple_failure_test_fn( + allocator, s_change_client_options_to_websockets, s_change_client_vtable_asynchronous_websocket_failure)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_websocket_connect_async_channel_failure, + s_mqtt5_client_websocket_connect_async_channel_failure_fn) + +static void s_mqtt5_client_test_websocket_failed_transform( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + + (void)user_data; + + (*complete_fn)(request, AWS_ERROR_INVALID_STATE, complete_ctx); +} + +static void s_change_client_options_to_failed_websocket_transform( + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *config) { + config->client_options->websocket_handshake_transform = s_mqtt5_client_test_websocket_failed_transform; +} + +/* Connection failure test where websocket MQTT channel establishment fails due to handshake transform failure */ +static int s_mqtt5_client_websocket_connect_handshake_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS( + s_mqtt5_client_simple_failure_test_fn(allocator, s_change_client_options_to_failed_websocket_transform, NULL)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_websocket_connect_handshake_failure, s_mqtt5_client_websocket_connect_handshake_failure_fn) + +static int s_aws_mqtt5_mock_server_handle_connect_always_fail( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_BANNED; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* Connection failure test where overall connection fails due to a CONNACK error code */ +static int s_mqtt5_client_direct_connect_connack_refusal_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_always_fail; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connection_failure_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_direct_connect_connack_refusal, s_mqtt5_client_direct_connect_connack_refusal_fn) + +/* Connection failure test where overall connection fails because there's no response to the CONNECT packet */ +static int s_mqtt5_client_direct_connect_connack_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* fast CONNACK timeout and don't response to the CONNECT packet */ + test_options.client_options.connack_timeout_ms = 2000; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = NULL; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connection_failure_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_TIMEOUT, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_direct_connect_connack_timeout, s_mqtt5_client_direct_connect_connack_timeout_fn) + +struct aws_mqtt5_server_disconnect_test_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + bool disconnect_sent; + bool connack_sent; +}; + +static void s_server_disconnect_service_fn( + struct aws_mqtt5_server_mock_connection_context *mock_server, + void *user_data) { + + struct aws_mqtt5_server_disconnect_test_context *test_context = user_data; + if (test_context->disconnect_sent || !test_context->connack_sent) { + return; + } + + test_context->disconnect_sent = true; + + struct aws_mqtt5_packet_disconnect_view disconnect = { + .reason_code = AWS_MQTT5_DRC_PACKET_TOO_LARGE, + }; + + s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); +} + +static int s_aws_mqtt5_server_disconnect_on_connect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + /* + * We intercept the CONNECT in order to correctly set the connack_sent test state. Otherwise we risk sometimes + * sending the DISCONNECT before the CONNACK + */ + int result = s_aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + + struct aws_mqtt5_server_disconnect_test_context *test_context = user_data; + test_context->connack_sent = true; + + return result; +} + +/* Connection test where we succeed and then the server sends a DISCONNECT */ +static int s_mqtt5_client_direct_connect_from_server_disconnect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* mock server sends a DISCONNECT packet back to the client after a successful CONNECTION establishment */ + test_options.server_function_table.service_task_fn = s_server_disconnect_service_fn; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_server_disconnect_on_connect; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_disconnection_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_direct_connect_from_server_disconnect, + s_mqtt5_client_direct_connect_from_server_disconnect_fn) + +struct aws_mqtt5_client_test_wait_for_n_context { + size_t required_event_count; + struct aws_mqtt5_client_mock_test_fixture *test_fixture; +}; + +static bool s_received_at_least_n_pingreqs(void *arg) { + struct aws_mqtt5_client_test_wait_for_n_context *ping_context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = ping_context->test_fixture; + + size_t ping_count = 0; + size_t packet_count = aws_array_list_length(&test_fixture->server_received_packets); + for (size_t i = 0; i < packet_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *record = NULL; + aws_array_list_get_at_ptr(&test_fixture->server_received_packets, (void **)&record, i); + + if (record->packet_type == AWS_MQTT5_PT_PINGREQ) { + ping_count++; + } + } + + return ping_count >= ping_context->required_event_count; +} + +static void s_wait_for_n_pingreqs(struct aws_mqtt5_client_test_wait_for_n_context *ping_context) { + + struct aws_mqtt5_client_mock_test_fixture *test_context = ping_context->test_fixture; + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_received_at_least_n_pingreqs, ping_context); + aws_mutex_unlock(&test_context->lock); +} + +#define TEST_PING_INTERVAL_MS 2000 + +static int s_verify_ping_sequence_timing(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + + uint64_t last_packet_time = 0; + + size_t packet_count = aws_array_list_length(&test_context->server_received_packets); + for (size_t i = 0; i < packet_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *record = NULL; + aws_array_list_get_at_ptr(&test_context->server_received_packets, (void **)&record, i); + + if (i == 0) { + ASSERT_INT_EQUALS(record->packet_type, AWS_MQTT5_PT_CONNECT); + last_packet_time = record->timestamp; + } else { + if (record->packet_type == AWS_MQTT5_PT_PINGREQ) { + uint64_t time_delta_ns = record->timestamp - last_packet_time; + uint64_t time_delta_millis = + aws_timestamp_convert(time_delta_ns, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); + + ASSERT_TRUE(s_is_within_percentage_of(TEST_PING_INTERVAL_MS, time_delta_millis, .1)); + + last_packet_time = record->timestamp; + } + } + } + aws_mutex_unlock(&test_context->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_client_test_init_ping_test_connect_storage( + struct aws_mqtt5_packet_connect_storage *storage, + struct aws_allocator *allocator) { + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = + (uint16_t)aws_timestamp_convert(TEST_PING_INTERVAL_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL), + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = true, + }; + + return aws_mqtt5_packet_connect_storage_init(storage, allocator, &connect_view); +} + +/* + * Test that the client sends pings at regular intervals to the server + * + * This is a low-keep-alive variant of the basic success test that waits for N pingreqs to be received by the server + * and validates the approximate time intervals between them. + */ +static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* fast keep alive in order keep tests reasonably short */ + uint16_t keep_alive_seconds = + (uint16_t)aws_timestamp_convert(TEST_PING_INTERVAL_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL); + test_options.connect_options.keep_alive_interval_seconds = keep_alive_seconds; + + /* faster ping timeout */ + test_options.client_options.ping_timeout_ms = 750; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_client_test_wait_for_n_context ping_context = { + .required_event_count = 5, + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &ping_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + s_wait_for_n_pingreqs(&ping_context); + + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_code = AWS_MQTT5_DRC_NORMAL_DISCONNECTION, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_view, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + s_wait_for_mock_server_to_receive_disconnect_packet(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + }, + { + .event_type = AWS_MQTT5_CLET_STOPPED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CLEAN_DISCONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + AWS_MCS_STOPPED, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + struct aws_mqtt5_packet_connect_storage expected_connect_storage; + ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_ping_test_connect_storage(&expected_connect_storage, allocator)); + + struct aws_mqtt5_packet_disconnect_storage expected_disconnect_storage; + ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_default_disconnect_storage(&expected_disconnect_storage, allocator)); + + struct aws_mqtt5_mock_server_packet_record expected_packets[] = { + { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = &expected_connect_storage, + }, + { + .packet_type = AWS_MQTT5_PT_PINGREQ, + }, + { + .packet_type = AWS_MQTT5_PT_PINGREQ, + }, + { + .packet_type = AWS_MQTT5_PT_PINGREQ, + }, + { + .packet_type = AWS_MQTT5_PT_PINGREQ, + }, + { + .packet_type = AWS_MQTT5_PT_PINGREQ, + }, + { + .packet_type = AWS_MQTT5_PT_DISCONNECT, + .packet_storage = &expected_disconnect_storage, + }, + }; + ASSERT_SUCCESS( + s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + ASSERT_SUCCESS(s_verify_ping_sequence_timing(&test_context)); + + aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_ping_sequence, s_mqtt5_client_ping_sequence_fn) + +/* + * Test that sending other data to the server pushes out the client ping timer + * + * This is a low-keep-alive variant of the basic success test that writes UNSUBSCRIBEs to the server at fast intervals. + * Verify the server doesn't receive any PINGREQs until the right amount of time after we stop sending the CONNECTs to + * it. + * + * TODO: we can't write this test until we have proper operation handling during CONNECTED state + */ +static int s_mqtt5_client_ping_write_pushout_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_ping_write_pushout, s_mqtt5_client_ping_write_pushout_fn) + +#define TIMEOUT_TEST_PING_INTERVAL_MS ((uint64_t)10000) + +static int s_verify_ping_timeout_interval(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + + uint64_t connected_time = 0; + uint64_t disconnected_time = 0; + + size_t event_count = aws_array_list_length(&test_context->lifecycle_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_context->lifecycle_events, &record, i); + if (connected_time == 0 && record->event.event_type == AWS_MQTT5_CLET_CONNECTION_SUCCESS) { + connected_time = record->timestamp; + } + + if (disconnected_time == 0 && record->event.event_type == AWS_MQTT5_CLET_DISCONNECTION) { + disconnected_time = record->timestamp; + } + } + + aws_mutex_unlock(&test_context->lock); + + ASSERT_TRUE(connected_time > 0 && disconnected_time > 0 && disconnected_time > connected_time); + + uint64_t connected_interval_ms = + aws_timestamp_convert(disconnected_time - connected_time, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); + uint64_t expected_connected_time_ms = + TIMEOUT_TEST_PING_INTERVAL_MS + (uint64_t)test_context->client->config->ping_timeout_ms; + + ASSERT_TRUE(s_is_within_percentage_of(expected_connected_time_ms, connected_interval_ms, .1)); + + return AWS_OP_SUCCESS; +} + +/* + * Test that not receiving a PINGRESP causes a disconnection + * + * This is a low-keep-alive variant of the basic success test where the mock server does not respond to a PINGREQ. + */ +static int s_mqtt5_client_ping_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* fast keep alive in order keep tests reasonably short */ + uint16_t keep_alive_seconds = + (uint16_t)aws_timestamp_convert(TIMEOUT_TEST_PING_INTERVAL_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL); + test_options.connect_options.keep_alive_interval_seconds = keep_alive_seconds; + + /* don't respond to PINGREQs */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PINGREQ] = NULL; + + /* faster ping timeout */ + test_options.client_options.ping_timeout_ms = 5000; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + s_wait_for_disconnection_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT, + }, + { + .event_type = AWS_MQTT5_CLET_STOPPED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + ASSERT_SUCCESS(s_verify_ping_timeout_interval(&test_context)); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CLEAN_DISCONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_ping_timeout, s_mqtt5_client_ping_timeout_fn) + +struct aws_connection_failure_wait_context { + size_t number_of_failures; + struct aws_mqtt5_client_mock_test_fixture *test_fixture; +}; + +static bool s_received_at_least_n_connection_failures(void *arg) { + struct aws_connection_failure_wait_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + size_t failure_count = 0; + size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + + if (record->event.event_type == AWS_MQTT5_CLET_CONNECTION_FAILURE) { + failure_count++; + } + } + + return failure_count >= context->number_of_failures; +} + +static void s_wait_for_n_connection_failure_lifecycle_events( + struct aws_mqtt5_client_mock_test_fixture *test_context, + size_t failure_count) { + struct aws_connection_failure_wait_context context = { + .number_of_failures = failure_count, + .test_fixture = test_context, + }; + + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_received_at_least_n_connection_failures, &context); + aws_mutex_unlock(&test_context->lock); +} + +#define RECONNECT_TEST_MIN_BACKOFF 500 +#define RECONNECT_TEST_MAX_BACKOFF 5000 +#define RECONNECT_TEST_BACKOFF_RESET_DELAY 5000 + +static int s_verify_reconnection_exponential_backoff_timestamps( + struct aws_mqtt5_client_mock_test_fixture *test_fixture) { + aws_mutex_lock(&test_fixture->lock); + + size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); + uint64_t last_timestamp = 0; + uint64_t expected_backoff = RECONNECT_TEST_MIN_BACKOFF; + + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + + if (record->event.event_type == AWS_MQTT5_CLET_CONNECTION_FAILURE) { + if (last_timestamp == 0) { + last_timestamp = record->timestamp; + } else { + uint64_t time_diff = aws_timestamp_convert( + record->timestamp - last_timestamp, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); + + if (!s_is_within_percentage_of(expected_backoff, time_diff, .1)) { + return AWS_OP_ERR; + } + + expected_backoff = aws_min_u64(expected_backoff * 2, RECONNECT_TEST_MAX_BACKOFF); + last_timestamp = record->timestamp; + } + } + } + + aws_mutex_unlock(&test_fixture->lock); + + return AWS_OP_SUCCESS; +} + +/* + * Always-fail variant that waits for 6 connection failures and then checks the timestamps between them against + * what we'd expect with exponential backoff and no jitter + */ +static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = RECONNECT_TEST_MIN_BACKOFF; + test_options.client_options.max_reconnect_delay_ms = RECONNECT_TEST_MAX_BACKOFF; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_always_fail; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_n_connection_failure_lifecycle_events(&test_context, 6); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* 6 (connecting, connection failure) pairs */ + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + ASSERT_SUCCESS(s_verify_reconnection_exponential_backoff_timestamps(&test_context)); + + /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_reconnect_failure_backoff, s_mqtt5_client_reconnect_failure_backoff_fn) + +struct aws_mqtt5_mock_server_reconnect_state { + size_t required_connection_failure_count; + + size_t connection_attempts; + uint64_t connect_timestamp; + + uint64_t successful_connection_disconnect_delay_ms; +}; + +static int s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct aws_mqtt5_mock_server_reconnect_state *context = user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + if (context->connection_attempts == context->required_connection_failure_count) { + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + aws_high_res_clock_get_ticks(&context->connect_timestamp); + } else { + connack_view.reason_code = AWS_MQTT5_CRC_NOT_AUTHORIZED; + } + + ++context->connection_attempts; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +static void s_aws_mqtt5_mock_server_disconnect_after_n_ms( + struct aws_mqtt5_server_mock_connection_context *mock_server, + void *user_data) { + + struct aws_mqtt5_mock_server_reconnect_state *context = user_data; + if (context->connect_timestamp == 0) { + return; + } + + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + + if (now < context->connect_timestamp) { + return; + } + + uint64_t elapsed_ms = + aws_timestamp_convert(now - context->connect_timestamp, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); + if (elapsed_ms > context->successful_connection_disconnect_delay_ms) { + + struct aws_mqtt5_packet_disconnect_view disconnect = { + .reason_code = AWS_MQTT5_DRC_PACKET_TOO_LARGE, + }; + + s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); + context->connect_timestamp = 0; + } +} + +static int s_verify_reconnection_after_success_used_backoff( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + uint64_t expected_reconnect_delay_ms) { + + aws_mutex_lock(&test_fixture->lock); + + size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); + + uint64_t disconnect_after_success_timestamp = 0; + uint64_t reconnect_failure_after_disconnect_timestamp = 0; + + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + + if (record->event.event_type == AWS_MQTT5_CLET_DISCONNECTION) { + ASSERT_INT_EQUALS(0, disconnect_after_success_timestamp); + disconnect_after_success_timestamp = record->timestamp; + } else if (record->event.event_type == AWS_MQTT5_CLET_CONNECTION_FAILURE) { + if (reconnect_failure_after_disconnect_timestamp == 0 && disconnect_after_success_timestamp > 0) { + reconnect_failure_after_disconnect_timestamp = record->timestamp; + } + } + } + + aws_mutex_unlock(&test_fixture->lock); + + ASSERT_TRUE(disconnect_after_success_timestamp > 0 && reconnect_failure_after_disconnect_timestamp > 0); + + uint64_t post_success_reconnect_time_ms = aws_timestamp_convert( + reconnect_failure_after_disconnect_timestamp - disconnect_after_success_timestamp, + AWS_TIMESTAMP_NANOS, + AWS_TIMESTAMP_MILLIS, + NULL); + + if (!s_is_within_percentage_of(expected_reconnect_delay_ms, post_success_reconnect_time_ms, .1)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +/* + * Fail-until-max-backoff variant, followed by a success that then quickly disconnects. Verify the next reconnect + * attempt still uses the maximum backoff because we weren't connected long enough to reset it. + */ +static int s_mqtt5_client_reconnect_backoff_insufficient_reset_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { + .required_connection_failure_count = 6, + /* quick disconnect should not reset reconnect delay */ + .successful_connection_disconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY / 5, + }; + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = RECONNECT_TEST_MIN_BACKOFF; + test_options.client_options.max_reconnect_delay_ms = RECONNECT_TEST_MAX_BACKOFF; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth; + test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_disconnect_after_n_ms; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &mock_server_state, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_n_connection_failure_lifecycle_events(&test_context, 7); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* 6 (connecting, connection failure) pairs, followed by a successful connection, then a disconnect and reconnect */ + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + ASSERT_SUCCESS(s_verify_reconnection_after_success_used_backoff(&test_context, RECONNECT_TEST_MAX_BACKOFF)); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_reconnect_backoff_insufficient_reset, s_mqtt5_client_reconnect_backoff_insufficient_reset_fn) + +/* + * Fail-until-max-backoff variant, followed by a success that disconnects after enough time has passed that the backoff + * should be reset. Verify that the next reconnect is back to using the minimum backoff value. + */ +static int s_mqtt5_client_reconnect_backoff_sufficient_reset_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { + .required_connection_failure_count = 6, + /* slow disconnect should reset reconnect delay */ + .successful_connection_disconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY * 2, + }; + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = RECONNECT_TEST_MIN_BACKOFF; + test_options.client_options.max_reconnect_delay_ms = RECONNECT_TEST_MAX_BACKOFF; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth; + test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_disconnect_after_n_ms; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &mock_server_state, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_n_connection_failure_lifecycle_events(&test_context, 7); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* 6 (connecting, connection failure) pairs, followed by a successful connection, then a disconnect and reconnect */ + struct aws_mqtt5_client_lifecycle_event expected_events[] = { + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS, + }, + { + .event_type = AWS_MQTT5_CLET_DISCONNECTION, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + { + .event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT, + }, + { + .event_type = AWS_MQTT5_CLET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + }; + ASSERT_SUCCESS( + s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); + + ASSERT_SUCCESS(s_verify_reconnection_after_success_used_backoff(&test_context, RECONNECT_TEST_MIN_BACKOFF)); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_reconnect_backoff_sufficient_reset, s_mqtt5_client_reconnect_backoff_sufficient_reset_fn) + +static const char s_topic_filter1[] = "some/topic/but/letsmakeit/longer/soIcanfailpacketsizetests/+"; + +static struct aws_mqtt5_subscription_view s_subscriptions[] = { + { + .topic_filter = + { + .ptr = (uint8_t *)s_topic_filter1, + .len = AWS_ARRAY_SIZE(s_topic_filter1) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + }, +}; + +static enum aws_mqtt5_suback_reason_code s_suback_reason_codes[] = { + AWS_MQTT5_SARC_GRANTED_QOS_1, +}; + +void s_aws_mqtt5_subscribe_complete_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + (void)suback; + (void)error_code; + + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + aws_mutex_lock(&test_context->lock); + test_context->subscribe_complete = true; + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static bool s_received_suback(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_context = arg; + + return test_context->subscribe_complete; +} + +static void s_wait_for_suback_received(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred(&test_context->signal, &test_context->lock, s_received_suback, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static int s_aws_mqtt5_server_send_suback_on_subscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_suback_reason_codes), + .reason_codes = s_suback_reason_codes, + }; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +/* Connection test where we succeed, send a SUBSCRIBE, and wait for a SUBACK */ +static int s_mqtt5_client_subscribe_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_suback_on_subscribe; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_aws_mqtt5_subscribe_complete_fn, + .completion_user_data = &test_context, + }; + aws_mqtt5_client_subscribe(client, &subscribe_view, &completion_options); + + s_wait_for_suback_received(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_subscribe_success, s_mqtt5_client_subscribe_success_fn) + +static int s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + uint32_t maximum_packet_size = 50; + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.maximum_packet_size = &maximum_packet_size; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +void s_aws_mqtt5_subscribe_complete_packet_size_too_small_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + AWS_FATAL_ASSERT(suback == NULL); + AWS_FATAL_ASSERT(AWS_ERROR_MQTT5_PACKET_VALIDATION == error_code); + + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + aws_mutex_lock(&test_context->lock); + test_context->subscribe_complete = true; + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static int s_mqtt5_client_subscribe_fail_packet_too_big_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_aws_mqtt5_subscribe_complete_packet_size_too_small_fn, + .completion_user_data = &test_context, + }; + aws_mqtt5_client_subscribe(client, &subscribe_view, &completion_options); + + s_wait_for_suback_received(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_subscribe_fail_packet_too_big, s_mqtt5_client_subscribe_fail_packet_too_big_fn) + +static void s_aws_mqtt5_disconnect_failure_completion_fn(int error_code, void *complete_ctx) { + AWS_FATAL_ASSERT(error_code == AWS_ERROR_MQTT5_PACKET_VALIDATION); + + s_on_disconnect_completion(error_code, complete_ctx); +} + +static int s_mqtt5_client_disconnect_fail_packet_too_big_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_byte_cursor long_reason_string_cursor = aws_byte_cursor_from_c_str( + "Not valid because it includes the 0-terminator but we don't check utf-8 so who cares"); + + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_string = &long_reason_string_cursor, + }; + + struct aws_mqtt5_disconnect_completion_options completion_options = { + .completion_callback = s_aws_mqtt5_disconnect_failure_completion_fn, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_view, &completion_options)); + + s_wait_for_disconnect_completion(&test_context); + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_disconnect_fail_packet_too_big, s_mqtt5_client_disconnect_fail_packet_too_big_fn) + +static uint8_t s_topic[] = "Hello/world"; + +#define RECEIVE_MAXIMUM_PUBLISH_COUNT 30 +#define TEST_RECEIVE_MAXIMUM 3 + +static int s_aws_mqtt5_mock_server_handle_connect_succeed_receive_maximum( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + uint16_t receive_maximum = TEST_RECEIVE_MAXIMUM; + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.receive_maximum = &receive_maximum; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +struct send_puback_task { + struct aws_allocator *allocator; + struct aws_task task; + struct aws_mqtt5_server_mock_connection_context *connection; + uint16_t packet_id; +}; + +void send_puback_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct send_puback_task *puback_response_task = arg; + if (status == AWS_TASK_STATUS_CANCELED) { + goto done; + } + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = puback_response_task->connection->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + --test_fixture->server_current_inflight_publishes; + aws_mutex_unlock(&test_fixture->lock); + + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = puback_response_task->packet_id, + }; + + s_aws_mqtt5_mock_server_send_packet(puback_response_task->connection, AWS_MQTT5_PT_PUBACK, &puback_view); + +done: + + aws_mem_release(puback_response_task->allocator, puback_response_task); +} + +static int s_aws_mqtt5_mock_server_handle_publish_delayed_puback( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + (void)user_data; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = connection->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + ++test_fixture->server_current_inflight_publishes; + test_fixture->server_maximum_inflight_publishes = + aws_max_u32(test_fixture->server_current_inflight_publishes, test_fixture->server_maximum_inflight_publishes); + aws_mutex_unlock(&test_fixture->lock); + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + struct send_puback_task *puback_response_task = + aws_mem_calloc(connection->allocator, 1, sizeof(struct send_puback_task)); + puback_response_task->allocator = connection->allocator; + puback_response_task->connection = connection; + puback_response_task->packet_id = publish_view->packet_id; + + aws_task_init(&puback_response_task->task, send_puback_fn, puback_response_task, "delayed_puback_response"); + + struct aws_event_loop *event_loop = aws_channel_get_event_loop(connection->slot->channel); + + uint64_t now = 0; + aws_event_loop_current_clock_time(event_loop, &now); + + uint64_t min_delay = aws_timestamp_convert(250, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + uint64_t max_delay = aws_timestamp_convert(500, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + uint64_t delay_nanos = aws_mqtt5_client_random_in_range(min_delay, max_delay); + + uint64_t puback_time = aws_add_u64_saturating(now, delay_nanos); + aws_event_loop_schedule_task_future(event_loop, &puback_response_task->task, puback_time); + + return AWS_OP_SUCCESS; +} + +static void s_receive_maximum_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + if (packet_type != AWS_MQTT5_PT_PUBACK) { + return; + } + + const struct aws_mqtt5_packet_puback_view *puback = packet; + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + + aws_mutex_lock(&test_context->lock); + + ++test_context->total_pubacks_received; + if (error_code == AWS_ERROR_SUCCESS && puback->reason_code < 128) { + ++test_context->successful_pubacks_received; + } + + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static bool s_received_n_successful_publishes(void *arg) { + struct aws_mqtt5_client_test_wait_for_n_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + return test_fixture->successful_pubacks_received >= context->required_event_count; +} + +static void s_wait_for_n_successful_publishes(struct aws_mqtt5_client_test_wait_for_n_context *context) { + + struct aws_mqtt5_client_mock_test_fixture *test_context = context->test_fixture; + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_received_n_successful_publishes, context); + aws_mutex_unlock(&test_context->lock); +} + +static int s_mqtt5_client_flow_control_receive_maximum_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* send delayed pubacks */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_delayed_puback; + + /* establish a low receive maximum */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_receive_maximum; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + /* send a bunch of publishes */ + for (size_t i = 0; i < RECEIVE_MAXIMUM_PUBLISH_COUNT; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_maximum_publish_completion_fn, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* wait for all publishes to succeed */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = RECEIVE_MAXIMUM_PUBLISH_COUNT, + }; + s_wait_for_n_successful_publishes(&wait_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* + * verify that the maximum number of in-progress qos1 publishes on the server was never more than what the + * server said its maximum was + */ + aws_mutex_lock(&test_context.lock); + uint32_t max_inflight_publishes = test_context.server_maximum_inflight_publishes; + aws_mutex_unlock(&test_context.lock); + + ASSERT_TRUE(max_inflight_publishes <= TEST_RECEIVE_MAXIMUM); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_flow_control_receive_maximum, s_mqtt5_client_flow_control_receive_maximum_fn) + +static void s_publish_timeout_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + (void)packet; + (void)packet_type; + + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + aws_mutex_lock(&test_context->lock); + + if (error_code == AWS_ERROR_MQTT_TIMEOUT) { + ++test_context->timeouts_received; + } + + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static bool s_received_n_publish_timeouts(void *arg) { + struct aws_mqtt5_client_test_wait_for_n_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + return test_fixture->timeouts_received >= context->required_event_count; +} + +static void s_wait_for_n_publish_timeouts(struct aws_mqtt5_client_test_wait_for_n_context *context) { + struct aws_mqtt5_client_mock_test_fixture *test_context = context->test_fixture; + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_received_n_publish_timeouts, context); + aws_mutex_unlock(&test_context->lock); +} + +static bool s_sent_n_timeout_publish_packets(void *arg) { + struct aws_mqtt5_client_test_wait_for_n_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + return test_fixture->publishes_received >= context->required_event_count; +} + +static int s_aws_mqtt5_mock_server_handle_timeout_publish( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + (void)packet; + (void)user_data; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = connection->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + ++connection->test_fixture->publishes_received; + aws_mutex_unlock(&test_fixture->lock); + + return AWS_OP_SUCCESS; +} + +static void s_wait_for_n_successful_server_timeout_publishes(struct aws_mqtt5_client_test_wait_for_n_context *context) { + struct aws_mqtt5_client_mock_test_fixture *test_context = context->test_fixture; + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_sent_n_timeout_publish_packets, context); + aws_mutex_unlock(&test_context->lock); +} + +/* + * Test that not receiving a PUBACK causes the PUBLISH waiting for the PUBACK to timeout + */ +static int s_mqtt5_client_publish_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_timeout_publish; + + /* fast publish timeout */ + test_options.client_options.ack_timeout_seconds = 5; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = &s_publish_timeout_publish_completion_fn, + .completion_user_data = &test_context, + }; + + struct aws_mqtt5_packet_publish_view packet_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = (size_t)aws_mqtt5_client_random_in_range(3, 20), + }; + + /* Send semi-random number of publishes that will not be acked */ + for (size_t publish_count = 0; publish_count < wait_context.required_event_count; ++publish_count) { + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &packet_publish_view, &completion_options)); + } + + s_wait_for_n_successful_server_timeout_publishes(&wait_context); + + s_wait_for_n_publish_timeouts(&wait_context); + + ASSERT_INT_EQUALS(wait_context.required_event_count, test_context.timeouts_received); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_publish_timeout, s_mqtt5_client_publish_timeout_fn) + +static int s_aws_mqtt5_mock_server_handle_publish_puback( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + (void)user_data; + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + if (publish_view->qos != AWS_MQTT5_QOS_AT_LEAST_ONCE) { + return AWS_OP_SUCCESS; + } + + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + }; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); +} + +#define IOT_CORE_THROUGHPUT_PACKETS 21 + +static uint8_t s_large_packet_payload[127 * 1024]; + +static int s_do_iot_core_throughput_test(struct aws_allocator *allocator, bool use_iot_core_limits) { + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* send pubacks */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_puback; + + if (use_iot_core_limits) { + test_options.client_options.extended_validation_and_flow_control_options = + AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS; + } + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + /* send a bunch of large publishes */ + aws_secure_zero(s_large_packet_payload, AWS_ARRAY_SIZE(s_large_packet_payload)); + + for (size_t i = 0; i < IOT_CORE_THROUGHPUT_PACKETS; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + .payload = aws_byte_cursor_from_array(s_large_packet_payload, AWS_ARRAY_SIZE(s_large_packet_payload)), + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_maximum_publish_completion_fn, /* can reuse receive_maximum callback */ + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* wait for all publishes to succeed */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = IOT_CORE_THROUGHPUT_PACKETS, + }; + s_wait_for_n_successful_publishes(&wait_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_flow_control_iot_core_throughput_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + uint64_t start_time1 = 0; + aws_high_res_clock_get_ticks(&start_time1); + + ASSERT_SUCCESS(s_do_iot_core_throughput_test(allocator, false)); + + uint64_t end_time1 = 0; + aws_high_res_clock_get_ticks(&end_time1); + + uint64_t test_time1 = end_time1 - start_time1; + + uint64_t start_time2 = 0; + aws_high_res_clock_get_ticks(&start_time2); + + ASSERT_SUCCESS(s_do_iot_core_throughput_test(allocator, true)); + + uint64_t end_time2 = 0; + aws_high_res_clock_get_ticks(&end_time2); + + uint64_t test_time2 = end_time2 - start_time2; + + /* We expect the unthrottled test to complete quickly */ + ASSERT_TRUE(test_time1 < AWS_TIMESTAMP_NANOS); + + /* + * We expect the throttled version to take around 5 seconds, since we're sending 21 almost-max size (127k) packets + * against a limit of 512KB/s. Since the packets are submitted immediately on CONNACK, the rate limiter + * token bucket is starting at zero and so will give us immediate throttling. + */ + ASSERT_TRUE(test_time2 > 5 * (uint64_t)AWS_TIMESTAMP_NANOS); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_flow_control_iot_core_throughput, s_mqtt5_client_flow_control_iot_core_throughput_fn) + +#define IOT_CORE_PUBLISH_TPS_PACKETS 650 + +static int s_do_iot_core_publish_tps_test(struct aws_allocator *allocator, bool use_iot_core_limits) { + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* send pubacks */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_puback; + + if (use_iot_core_limits) { + test_options.client_options.extended_validation_and_flow_control_options = + AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS; + } + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + /* send a bunch of tiny publishes */ + for (size_t i = 0; i < IOT_CORE_PUBLISH_TPS_PACKETS; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_maximum_publish_completion_fn, /* can reuse receive_maximum callback */ + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* wait for all publishes to succeed */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = IOT_CORE_PUBLISH_TPS_PACKETS, + }; + s_wait_for_n_successful_publishes(&wait_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_flow_control_iot_core_publish_tps_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + uint64_t start_time1 = 0; + aws_high_res_clock_get_ticks(&start_time1); + + ASSERT_SUCCESS(s_do_iot_core_publish_tps_test(allocator, false)); + + uint64_t end_time1 = 0; + aws_high_res_clock_get_ticks(&end_time1); + + uint64_t test_time1 = end_time1 - start_time1; + + uint64_t start_time2 = 0; + aws_high_res_clock_get_ticks(&start_time2); + + ASSERT_SUCCESS(s_do_iot_core_publish_tps_test(allocator, true)); + + uint64_t end_time2 = 0; + aws_high_res_clock_get_ticks(&end_time2); + + uint64_t test_time2 = end_time2 - start_time2; + + /* We expect the unthrottled test to complete quickly */ + ASSERT_TRUE(test_time1 < AWS_TIMESTAMP_NANOS); + + /* + * We expect the throttled version to take over 6 seconds, since we're sending over 650 tiny publish packets + * against a limit of 100TPS. Since the packets are submitted immediately on CONNACK, the rate limiter + * token bucket is starting at zero and so will give us immediate throttling. + */ + ASSERT_TRUE(test_time2 > 6 * (uint64_t)AWS_TIMESTAMP_NANOS); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_flow_control_iot_core_publish_tps, s_mqtt5_client_flow_control_iot_core_publish_tps_fn) + +static int s_aws_mqtt5_mock_server_handle_connect_honor_session( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connect_view *connect_packet = packet; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + /* Only resume a connection if the client has already connected to the server before */ + if (connection->test_fixture->client->has_connected_successfully) { + connack_view.session_present = !connect_packet->clean_start; + } + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +struct aws_mqtt5_wait_for_n_lifecycle_events_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + enum aws_mqtt5_client_lifecycle_event_type event_type; + size_t expected_event_count; +}; + +static bool s_received_n_lifecycle_events(void *arg) { + struct aws_mqtt5_wait_for_n_lifecycle_events_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + size_t matching_events = 0; + size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + + if (record->event.event_type == context->event_type) { + ++matching_events; + } + } + + return matching_events >= context->expected_event_count; +} + +static void s_wait_for_n_lifecycle_events( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + enum aws_mqtt5_client_lifecycle_event_type event_type, + size_t expected_event_count) { + struct aws_mqtt5_wait_for_n_lifecycle_events_context wait_context = { + .test_fixture = test_fixture, + .event_type = event_type, + .expected_event_count = expected_event_count, + }; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_received_n_lifecycle_events, &wait_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static bool s_compute_expected_rejoined_session( + enum aws_mqtt5_client_session_behavior_type session_behavior, + size_t connect_index) { + switch (session_behavior) { + case AWS_MQTT5_CSBT_CLEAN: + return false; + + case AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS: + return connect_index > 0; + + default: + return true; + } +} + +static int s_aws_mqtt5_client_test_init_resume_session_connect_storage( + struct aws_mqtt5_packet_connect_storage *storage, + struct aws_allocator *allocator) { + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 30, + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = false, + }; + + return aws_mqtt5_packet_connect_storage_init(storage, allocator, &connect_view); +} + +#define SESSION_RESUMPTION_CONNECT_COUNT 5 + +static int s_do_mqtt5_client_session_resumption_test( + struct aws_allocator *allocator, + enum aws_mqtt5_client_session_behavior_type session_behavior) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.session_behavior = session_behavior; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_honor_session; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + for (size_t i = 0; i < SESSION_RESUMPTION_CONNECT_COUNT; ++i) { + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + s_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_SUCCESS, i + 1); + + /* not technically truly safe to query depending on memory model. Remove if it becomes a problem. */ + bool expected_rejoined_session = s_compute_expected_rejoined_session(session_behavior, i); + ASSERT_INT_EQUALS(expected_rejoined_session, client->negotiated_settings.rejoined_session); + + /* can't use stop as that wipes session state */ + aws_channel_shutdown(test_context.server_channel, AWS_ERROR_UNKNOWN); + + s_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_DISCONNECTION, i + 1); + } + + struct aws_mqtt5_packet_connect_storage clean_start_connect_storage; + ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_default_connect_storage(&clean_start_connect_storage, allocator)); + + struct aws_mqtt5_packet_connect_storage resume_session_connect_storage; + ASSERT_SUCCESS( + s_aws_mqtt5_client_test_init_resume_session_connect_storage(&resume_session_connect_storage, allocator)); + + struct aws_mqtt5_mock_server_packet_record expected_packets[SESSION_RESUMPTION_CONNECT_COUNT]; + for (size_t i = 0; i < SESSION_RESUMPTION_CONNECT_COUNT; ++i) { + expected_packets[i].packet_type = AWS_MQTT5_PT_CONNECT; + if (s_compute_expected_rejoined_session(session_behavior, i)) { + expected_packets[i].packet_storage = &resume_session_connect_storage; + } else { + expected_packets[i].packet_storage = &clean_start_connect_storage; + } + } + + ASSERT_SUCCESS( + s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + aws_mqtt5_packet_connect_storage_clean_up(&clean_start_connect_storage); + aws_mqtt5_packet_connect_storage_clean_up(&resume_session_connect_storage); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_session_resumption_clean_start_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_session_resumption_test(allocator, AWS_MQTT5_CSBT_CLEAN)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_session_resumption_clean_start, s_mqtt5_client_session_resumption_clean_start_fn) + +static int s_mqtt5_client_session_resumption_post_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_session_resumption_test(allocator, AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_session_resumption_post_success, s_mqtt5_client_session_resumption_post_success_fn) + +static uint8_t s_sub_pub_unsub_topic_filter[] = "hello/+"; + +static struct aws_mqtt5_subscription_view s_sub_pub_unsub_subscriptions[] = { + { + .topic_filter = + { + .ptr = (uint8_t *)s_sub_pub_unsub_topic_filter, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filter) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + }, +}; + +static struct aws_byte_cursor s_sub_pub_unsub_topic_filters[] = { + { + .ptr = s_sub_pub_unsub_topic_filter, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filter) - 1, + }, +}; + +struct aws_mqtt5_sub_pub_unsub_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + + bool subscribe_complete; + bool publish_complete; + bool publish_received; + bool unsubscribe_complete; + size_t publishes_received; + + size_t subscribe_failures; + size_t publish_failures; + size_t unsubscribe_failures; + + struct aws_mqtt5_packet_publish_storage publish_storage; +}; + +static void s_sub_pub_unsub_context_clean_up(struct aws_mqtt5_sub_pub_unsub_context *context) { + aws_mqtt5_packet_publish_storage_clean_up(&context->publish_storage); +} + +void s_sub_pub_unsub_subscribe_complete_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + AWS_FATAL_ASSERT(suback != NULL); + AWS_FATAL_ASSERT(error_code == AWS_ERROR_SUCCESS); + + struct aws_mqtt5_sub_pub_unsub_context *test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + test_context->subscribe_complete = true; + if (error_code != AWS_ERROR_SUCCESS) { + ++test_context->subscribe_failures; + } + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static bool s_sub_pub_unsub_received_suback(void *arg) { + struct aws_mqtt5_sub_pub_unsub_context *test_context = arg; + + return test_context->subscribe_complete && test_context->subscribe_failures == 0; +} + +static void s_sub_pub_unsub_wait_for_suback_received(struct aws_mqtt5_sub_pub_unsub_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_sub_pub_unsub_received_suback, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static int s_mqtt5_client_sub_pub_unsub_subscribe( + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + struct aws_mqtt5_packet_subscribe_storage *expected_subscribe_storage) { + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_sub_pub_unsub_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_sub_pub_unsub_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_subscribe_complete_fn, + .completion_user_data = full_test_context, + }; + + struct aws_mqtt5_client *client = full_test_context->test_fixture->client; + ASSERT_SUCCESS(aws_mqtt5_client_subscribe(client, &subscribe_view, &completion_options)); + + s_sub_pub_unsub_wait_for_suback_received(full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_packet_subscribe_storage_init( + expected_subscribe_storage, full_test_context->test_fixture->allocator, &subscribe_view)); + + return AWS_OP_SUCCESS; +} + +void s_sub_pub_unsub_unsubscribe_complete_fn( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + + AWS_FATAL_ASSERT(unsuback != NULL); + AWS_FATAL_ASSERT(error_code == AWS_ERROR_SUCCESS); + + struct aws_mqtt5_sub_pub_unsub_context *test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + test_context->unsubscribe_complete = true; + if (error_code != AWS_ERROR_SUCCESS) { + ++test_context->unsubscribe_failures; + } + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static bool s_sub_pub_unsub_received_unsuback(void *arg) { + struct aws_mqtt5_sub_pub_unsub_context *test_context = arg; + + return test_context->unsubscribe_complete && test_context->unsubscribe_failures == 0; +} + +static void s_sub_pub_unsub_wait_for_unsuback_received(struct aws_mqtt5_sub_pub_unsub_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_sub_pub_unsub_received_unsuback, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static int s_mqtt5_client_sub_pub_unsub_unsubscribe( + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + struct aws_mqtt5_packet_unsubscribe_storage *expected_unsubscribe_storage) { + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = s_sub_pub_unsub_topic_filters, + .topic_filter_count = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filters), + }; + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_unsubscribe_complete_fn, + .completion_user_data = full_test_context, + }; + + struct aws_mqtt5_client *client = full_test_context->test_fixture->client; + ASSERT_SUCCESS(aws_mqtt5_client_unsubscribe(client, &unsubscribe_view, &completion_options)); + + s_sub_pub_unsub_wait_for_unsuback_received(full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_packet_unsubscribe_storage_init( + expected_unsubscribe_storage, full_test_context->test_fixture->allocator, &unsubscribe_view)); + + return AWS_OP_SUCCESS; +} + +void s_sub_pub_unsub_publish_received_fn(const struct aws_mqtt5_packet_publish_view *publish, void *complete_ctx) { + + AWS_FATAL_ASSERT(publish != NULL); + + struct aws_mqtt5_sub_pub_unsub_context *test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + test_context->publish_received = true; + aws_mqtt5_packet_publish_storage_init(&test_context->publish_storage, test_fixture->allocator, publish); + + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static bool s_sub_pub_unsub_received_publish(void *arg) { + struct aws_mqtt5_sub_pub_unsub_context *test_context = arg; + + return test_context->publish_received && test_context->publish_failures == 0; +} + +static void s_sub_pub_unsub_wait_for_publish_received(struct aws_mqtt5_sub_pub_unsub_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_sub_pub_unsub_received_publish, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static enum aws_mqtt5_unsuback_reason_code s_unsuback_reason_codes[] = { + AWS_MQTT5_UARC_SUCCESS, +}; + +static int s_aws_mqtt5_server_send_unsuback_on_unsubscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = packet; + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = unsubscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_unsuback_reason_codes), + .reason_codes = s_unsuback_reason_codes, + }; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); +} + +#define FORWARDED_PUBLISH_PACKET_ID 32768 + +static int s_aws_mqtt5_server_send_puback_and_forward_on_publish( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + /* send a PUBACK? */ + if (publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + .reason_code = AWS_MQTT5_PARC_SUCCESS, + }; + + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { + return AWS_OP_ERR; + } + } + + /* assume we're subscribed, reflect the publish back to the test client */ + struct aws_mqtt5_packet_publish_view reflect_publish_view = *publish_view; + if (publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + reflect_publish_view.packet_id = FORWARDED_PUBLISH_PACKET_ID; + } + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflect_publish_view); +} + +void s_sub_pub_unsub_publish_complete_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + (void)packet; + (void)packet_type; + + AWS_FATAL_ASSERT(error_code == AWS_ERROR_SUCCESS); + + struct aws_mqtt5_sub_pub_unsub_context *test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + test_context->publish_complete = true; + if (error_code != AWS_ERROR_SUCCESS) { + ++test_context->publish_failures; + } + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static bool s_sub_pub_unsub_publish_complete(void *arg) { + struct aws_mqtt5_sub_pub_unsub_context *test_context = arg; + + return test_context->publish_complete; +} + +static void s_sub_pub_unsub_wait_for_publish_complete(struct aws_mqtt5_sub_pub_unsub_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_sub_pub_unsub_publish_complete, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static uint8_t s_sub_pub_unsub_publish_topic[] = "hello/world"; +static uint8_t s_sub_pub_unsub_publish_payload[] = "PublishPayload"; + +static int s_mqtt5_client_sub_pub_unsub_publish( + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + enum aws_mqtt5_qos qos, + struct aws_mqtt5_packet_publish_storage *expected_publish_storage, + struct aws_mqtt5_packet_puback_storage *expected_puback_storage) { + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = qos, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_publish_complete_fn, + .completion_user_data = full_test_context, + }; + + struct aws_mqtt5_client *client = full_test_context->test_fixture->client; + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &publish_view, &completion_options)); + + if (qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + s_sub_pub_unsub_wait_for_publish_complete(full_test_context); + } + + struct aws_allocator *allocator = full_test_context->test_fixture->allocator; + ASSERT_SUCCESS(aws_mqtt5_packet_publish_storage_init(expected_publish_storage, allocator, &publish_view)); + + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = FORWARDED_PUBLISH_PACKET_ID, + .reason_code = AWS_MQTT5_PARC_SUCCESS, + }; + ASSERT_SUCCESS(aws_mqtt5_packet_puback_storage_init(expected_puback_storage, allocator, &puback_view)); + + return AWS_OP_SUCCESS; +} + +static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqtt5_qos qos) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + test_options.client_options.publish_received_handler = s_sub_pub_unsub_publish_received_fn; + test_options.client_options.publish_received_handler_user_data = &full_test_context; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_suback_on_subscribe; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_send_puback_and_forward_on_publish; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_aws_mqtt5_server_send_unsuback_on_unsubscribe; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_storage expected_subscribe_storage; + AWS_ZERO_STRUCT(expected_subscribe_storage); + struct aws_mqtt5_packet_publish_storage expected_publish_storage; + AWS_ZERO_STRUCT(expected_publish_storage); + struct aws_mqtt5_packet_puback_storage expected_puback_storage; + AWS_ZERO_STRUCT(expected_puback_storage); + struct aws_mqtt5_packet_unsubscribe_storage expected_unsubscribe_storage; + AWS_ZERO_STRUCT(expected_unsubscribe_storage); + + ASSERT_SUCCESS(s_mqtt5_client_sub_pub_unsub_subscribe(&full_test_context, &expected_subscribe_storage)); + ASSERT_SUCCESS(s_mqtt5_client_sub_pub_unsub_publish( + &full_test_context, qos, &expected_publish_storage, &expected_puback_storage)); + s_sub_pub_unsub_wait_for_publish_received(&full_test_context); + ASSERT_SUCCESS(s_mqtt5_client_sub_pub_unsub_unsubscribe(&full_test_context, &expected_unsubscribe_storage)); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* verify packets that server received: connect,subscribe, publish, puback(if qos1), unsubscribe */ + struct aws_array_list expected_packets; + aws_array_list_init_dynamic(&expected_packets, allocator, 5, sizeof(struct aws_mqtt5_mock_server_packet_record)); + + struct aws_mqtt5_mock_server_packet_record connect_record = { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = NULL, + }; + aws_array_list_push_back(&expected_packets, &connect_record); + + struct aws_mqtt5_mock_server_packet_record subscribe_record = { + .packet_type = AWS_MQTT5_PT_SUBSCRIBE, + .packet_storage = &expected_subscribe_storage, + }; + aws_array_list_push_back(&expected_packets, &subscribe_record); + + struct aws_mqtt5_mock_server_packet_record publish_record = { + .packet_type = AWS_MQTT5_PT_PUBLISH, + .packet_storage = &expected_publish_storage, + }; + aws_array_list_push_back(&expected_packets, &publish_record); + + if (qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + struct aws_mqtt5_mock_server_packet_record puback_record = { + .packet_type = AWS_MQTT5_PT_PUBACK, + .packet_storage = &expected_puback_storage, + }; + aws_array_list_push_back(&expected_packets, &puback_record); + } + + struct aws_mqtt5_mock_server_packet_record unsubscribe_record = { + .packet_type = AWS_MQTT5_PT_UNSUBSCRIBE, + .packet_storage = &expected_unsubscribe_storage, + }; + aws_array_list_push_back(&expected_packets, &unsubscribe_record); + + ASSERT_SUCCESS(s_verify_received_packet_sequence( + &test_context, expected_packets.data, aws_array_list_length(&expected_packets))); + + /* verify client received the publish that we sent */ + const struct aws_mqtt5_packet_publish_view *received_publish = &full_test_context.publish_storage.storage_view; + ASSERT_TRUE((received_publish->packet_id != 0) == (qos == AWS_MQTT5_QOS_AT_LEAST_ONCE)); + ASSERT_INT_EQUALS((uint32_t)qos, (uint32_t)received_publish->qos); + + ASSERT_BIN_ARRAYS_EQUALS( + s_sub_pub_unsub_publish_topic, + AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + received_publish->topic.ptr, + received_publish->topic.len); + + ASSERT_BIN_ARRAYS_EQUALS( + s_sub_pub_unsub_publish_payload, + AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + received_publish->payload.ptr, + received_publish->payload.len); + + aws_mqtt5_packet_subscribe_storage_clean_up(&expected_subscribe_storage); + aws_mqtt5_packet_publish_storage_clean_up(&expected_publish_storage); + aws_mqtt5_packet_puback_storage_clean_up(&expected_puback_storage); + aws_mqtt5_packet_unsubscribe_storage_clean_up(&expected_unsubscribe_storage); + aws_array_list_clean_up(&expected_packets); + + s_sub_pub_unsub_context_clean_up(&full_test_context); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_sub_pub_unsub_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_sub_pub_unsub_test(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_sub_pub_unsub_qos0, s_mqtt5_client_sub_pub_unsub_qos0_fn) + +static int s_mqtt5_client_sub_pub_unsub_qos1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_sub_pub_unsub_test(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_sub_pub_unsub_qos1, s_mqtt5_client_sub_pub_unsub_qos1_fn) + +static enum aws_mqtt5_unsuback_reason_code s_unsubscribe_success_reason_codes[] = { + AWS_MQTT5_UARC_NO_SUBSCRIPTION_EXISTED, +}; + +static int s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = packet; + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = unsubscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_unsubscribe_success_reason_codes), + .reason_codes = s_unsubscribe_success_reason_codes, + }; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); +} + +static int s_mqtt5_client_unsubscribe_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = s_sub_pub_unsub_topic_filters, + .topic_filter_count = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filters), + }; + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_unsubscribe_complete_fn, + .completion_user_data = &full_test_context, + }; + aws_mqtt5_client_unsubscribe(client, &unsubscribe_view, &completion_options); + + s_sub_pub_unsub_wait_for_unsuback_received(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_unsubscribe_success, s_mqtt5_client_unsubscribe_success_fn) + +static aws_mqtt5_packet_id_t s_puback_packet_id = 183; + +struct aws_mqtt5_server_send_qos1_publish_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + bool publish_sent; + bool connack_sent; + bool connack_checked; +}; + +static int s_aws_mqtt5_mock_server_handle_puback( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_packet_puback_view *puback_view = packet; + + ASSERT_INT_EQUALS(puback_view->packet_id, s_puback_packet_id); + ASSERT_TRUE(puback_view->reason_code == AWS_MQTT5_PARC_SUCCESS); + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = connection->test_fixture; + struct aws_mqtt5_server_send_qos1_publish_context *publish_context = + connection->test_fixture->mock_server_user_data; + aws_mutex_lock(&test_fixture->lock); + publish_context->connack_checked = true; + aws_mutex_unlock(&test_fixture->lock); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_mock_server_send_qos1_publish( + struct aws_mqtt5_server_mock_connection_context *mock_server, + void *user_data) { + + struct aws_mqtt5_server_send_qos1_publish_context *test_context = user_data; + if (test_context->publish_sent || !test_context->connack_sent) { + return; + } + + test_context->publish_sent = true; + + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + .packet_id = s_puback_packet_id, + }; + + s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_PUBLISH, &qos1_publish_view); +} + +static int s_aws_mqtt5_server_send_qos1_publish_on_connect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + int result = s_aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + + struct aws_mqtt5_server_send_qos1_publish_context *test_context = user_data; + test_context->connack_sent = true; + + return result; +} + +static bool s_publish_qos1_puback(void *arg) { + struct aws_mqtt5_server_send_qos1_publish_context *test_context = arg; + return test_context->connack_checked; +} + +static void s_publish_qos1_wait_for_puback(struct aws_mqtt5_server_send_qos1_publish_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred(&test_fixture->signal, &test_fixture->lock, s_publish_qos1_puback, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +/* When client receives a QoS1 PUBLISH it must send a valid PUBACK with packet id */ +static int mqtt5_client_receive_qos1_return_puback_test_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* mock server sends a PUBLISH packet to the client */ + test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_send_qos1_publish; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBACK] = s_aws_mqtt5_mock_server_handle_puback; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_server_send_qos1_publish_on_connect; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_send_qos1_publish_context publish_context = { + .test_fixture = &test_context, + .publish_sent = false, + .connack_sent = false, + .connack_checked = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &publish_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + s_publish_qos1_wait_for_puback(&publish_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_receive_qos1_return_puback_test, mqtt5_client_receive_qos1_return_puback_test_fn) + +static int s_aws_mqtt5_mock_server_handle_connect_session_present( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.session_present = true; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* When client receives a CONNACK with existing session state when one isn't present it should disconnect */ +static int mqtt5_client_receive_nonexisting_session_state_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* mock server returns a CONNACK indicating a session is being resumed */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_session_present; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connection_failure_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_receive_nonexisting_session_state, mqtt5_client_receive_nonexisting_session_state_fn) + +static const char *s_receive_assigned_client_id_client_id = "Assigned_Client_ID"; + +struct aws_mqtt5_server_receive_assigned_client_id_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + bool assigned_client_id_checked; +}; + +static int s_aws_mqtt5_mock_server_handle_connect_assigned_client_id( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_packet_connect_view *connect_view = packet; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + struct aws_byte_cursor assigned_client_id = aws_byte_cursor_from_c_str(s_receive_assigned_client_id_client_id); + + /* Server behavior sets the Assigned Client Identifier on a CONNECT packet with an empty Client ID */ + if (connect_view->client_id.len == 0) { + connack_view.assigned_client_identifier = &assigned_client_id; + } else { + ASSERT_BIN_ARRAYS_EQUALS( + assigned_client_id.ptr, assigned_client_id.len, connect_view->client_id.ptr, connect_view->client_id.len); + struct aws_mqtt5_server_receive_assigned_client_id_context *test_context = user_data; + test_context->assigned_client_id_checked = true; + } + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* + * When client connects with a zero length Client ID, server provides one. + * The client should then use the assigned Client ID on reconnection attempts. + */ +static int mqtt5_client_receive_assigned_client_id_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* Empty the Client ID for connect */ + test_options.connect_options.client_id.len = 0; + + /* mock server checks for a client ID and if it's missing sends an assigned one */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_assigned_client_id; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_server_receive_assigned_client_id_context assinged_id_context = { + .test_fixture = &test_context, + .assigned_client_id_checked = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &assinged_id_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_byte_cursor assigned_client_id = aws_byte_cursor_from_c_str(s_receive_assigned_client_id_client_id); + struct aws_byte_cursor negotiated_settings_client_id = + aws_byte_cursor_from_buf(&client->negotiated_settings.client_id_storage); + /* Test that Assigned Client ID is stored */ + ASSERT_BIN_ARRAYS_EQUALS( + assigned_client_id.ptr, + assigned_client_id.len, + negotiated_settings_client_id.ptr, + negotiated_settings_client_id.len); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + /* Check for Assigned Client ID on reconnect */ + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + ASSERT_TRUE(assinged_id_context.assigned_client_id_checked); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_receive_assigned_client_id, mqtt5_client_receive_assigned_client_id_fn); + +#define TEST_PUBLISH_COUNT 10 + +static int s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = connection->test_fixture; + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + aws_mutex_lock(&test_fixture->lock); + ++connection->test_fixture->publishes_received; + aws_mutex_unlock(&test_fixture->lock); + + /* Only send the PUBACK on the second attempt after a reconnect and restored session */ + if (publish_view->duplicate) { + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + }; + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); + } + + return AWS_OP_SUCCESS; +} + +static void s_receive_stored_session_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + if (packet_type != AWS_MQTT5_PT_PUBACK) { + return; + } + + const struct aws_mqtt5_packet_puback_view *puback = packet; + + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + aws_mutex_lock(&test_context->lock); + + if (error_code == AWS_ERROR_SUCCESS && puback->reason_code < 128) { + ++test_context->successful_pubacks_received; + } + + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static bool s_received_n_unacked_publishes(void *arg) { + struct aws_mqtt5_client_test_wait_for_n_context *context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; + + return test_fixture->publishes_received >= context->required_event_count; +} + +static void s_wait_for_n_unacked_publishes(struct aws_mqtt5_client_test_wait_for_n_context *context) { + + struct aws_mqtt5_client_mock_test_fixture *test_context = context->test_fixture; + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_received_n_unacked_publishes, context); + aws_mutex_unlock(&test_context->lock); +} + +static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* Set to rejoin */ + test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; + + /* mock server will not send PUBACKS on initial connect */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect; + /* Simulate reconnecting to an existing connection */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_honor_session; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_stored_session_publish_completion_fn, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* Wait for publishes to have gone out from client */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = TEST_PUBLISH_COUNT, + }; + s_wait_for_n_unacked_publishes(&wait_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + aws_mutex_lock(&test_context.lock); + size_t event_count = aws_array_list_length(&test_context.lifecycle_events); + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_context.lifecycle_events, &record, event_count - 1); + aws_mutex_unlock(&test_context.lock); + + ASSERT_FALSE(record->connack_storage.storage_view.session_present); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_no_session_after_client_stop, mqtt5_client_no_session_after_client_stop_fn); + +static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* Set to rejoin */ + test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; + /* faster ping timeout */ + test_options.client_options.ping_timeout_ms = 3000; + test_options.connect_options.keep_alive_interval_seconds = 5; + + /* don't respond to PINGREQs */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PINGREQ] = NULL; + /* mock server will not send PUBACKS on initial connect */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect; + /* Simulate reconnecting to an existing connection */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_honor_session; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_stored_session_publish_completion_fn, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* Wait for publishes to have gone out from client */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = TEST_PUBLISH_COUNT, + }; + s_wait_for_n_unacked_publishes(&wait_context); + + /* disconnect due to failed ping */ + s_wait_for_disconnection_lifecycle_event(&test_context); + + /* Reconnect from a disconnect automatically */ + s_wait_for_connected_lifecycle_event(&test_context); + + s_wait_for_n_successful_publishes(&wait_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + s_wait_for_stopped_lifecycle_event(&test_context); + + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CLEAN_DISCONNECT, + AWS_MCS_CHANNEL_SHUTDOWN, + AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, + AWS_MCS_MQTT_CONNECT, + AWS_MCS_CONNECTED, + AWS_MCS_CHANNEL_SHUTDOWN, + AWS_MCS_STOPPED, + }; + + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_restore_session_on_ping_timeout_reconnect, + mqtt5_client_restore_session_on_ping_timeout_reconnect_fn); + +/* If the server returns a Clean Session, client must discard any existing Session and start a new Session */ +static int mqtt5_client_discard_session_on_server_clean_start_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* Set to rejoin */ + test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; + + /* mock server will not send PUBACKS on initial connect */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_receive_stored_session_publish_completion_fn, + .completion_user_data = &test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish_view, &completion_options)); + } + + /* Wait for QoS1 publishes to have gone out from client */ + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = TEST_PUBLISH_COUNT, + }; + s_wait_for_n_unacked_publishes(&wait_context); + + /* Disconnect with unacked publishes */ + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + s_wait_for_stopped_lifecycle_event(&test_context); + + /* Reconnect with a Client Stored Session */ + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + s_wait_for_connected_lifecycle_event(&test_context); + + /* Provide time for Client to process any queued operations */ + aws_thread_current_sleep(1000000000); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + s_wait_for_stopped_lifecycle_event(&test_context); + + /* Check that no publishes were resent after the initial batch on first connect */ + ASSERT_INT_EQUALS(test_context.publishes_received, TEST_PUBLISH_COUNT); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_discard_session_on_server_clean_start, + mqtt5_client_discard_session_on_server_clean_start_fn); + +static int s_verify_zero_statistics(struct aws_mqtt5_client_operation_statistics *stats) { + ASSERT_INT_EQUALS(stats->incomplete_operation_size, 0); + ASSERT_INT_EQUALS(stats->incomplete_operation_count, 0); + ASSERT_INT_EQUALS(stats->unacked_operation_size, 0); + ASSERT_INT_EQUALS(stats->unacked_operation_count, 0); + + return AWS_OP_SUCCESS; +} + +static int s_verify_statistics_equal( + struct aws_mqtt5_client_operation_statistics *expected_stats, + struct aws_mqtt5_client_operation_statistics *actual_stats) { + ASSERT_INT_EQUALS(expected_stats->incomplete_operation_size, actual_stats->incomplete_operation_size); + ASSERT_INT_EQUALS(expected_stats->incomplete_operation_count, actual_stats->incomplete_operation_count); + ASSERT_INT_EQUALS(expected_stats->unacked_operation_size, actual_stats->unacked_operation_size); + ASSERT_INT_EQUALS(expected_stats->unacked_operation_size, actual_stats->unacked_operation_size); + + return AWS_OP_SUCCESS; +} + +static int s_verify_client_statistics( + struct aws_mqtt5_client_mock_test_fixture *test_context, + struct aws_mqtt5_client_operation_statistics *expected_stats, + size_t expected_stats_count) { + struct aws_array_list *actual_stats = &test_context->client_statistics; + size_t actual_stats_count = aws_array_list_length(actual_stats); + + /* we expect the last stats to be zero, the expected stats represent the stats before that */ + ASSERT_INT_EQUALS(actual_stats_count, expected_stats_count + 1); + + struct aws_mqtt5_client_operation_statistics *current_stats = NULL; + aws_array_list_get_at_ptr(actual_stats, (void **)¤t_stats, actual_stats_count - 1); + ASSERT_SUCCESS(s_verify_zero_statistics(current_stats)); + + for (size_t i = 0; i < expected_stats_count; ++i) { + aws_array_list_get_at_ptr(actual_stats, (void **)¤t_stats, i); + ASSERT_SUCCESS(s_verify_statistics_equal(&expected_stats[i], current_stats)); + } + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_client_operation_statistics s_subscribe_test_statistics[] = { + { + .incomplete_operation_size = 68, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, + { + .incomplete_operation_size = 68, + .incomplete_operation_count = 1, + .unacked_operation_size = 68, + .unacked_operation_count = 1, + }, +}; + +static int s_mqtt5_client_statistics_subscribe_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_suback_on_subscribe; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_server_disconnect_test_context disconnect_context = { + .test_fixture = &test_context, + .disconnect_sent = false, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &disconnect_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_aws_mqtt5_subscribe_complete_fn, + .completion_user_data = &test_context, + }; + aws_mqtt5_client_subscribe(client, &subscribe_view, &completion_options); + + s_wait_for_suback_received(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_client_statistics( + &test_context, s_subscribe_test_statistics, AWS_ARRAY_SIZE(s_subscribe_test_statistics))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_statistics_subscribe, s_mqtt5_client_statistics_subscribe_fn) + +static struct aws_mqtt5_client_operation_statistics s_unsubscribe_test_statistics[] = { + { + .incomplete_operation_size = 14, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, + { + .incomplete_operation_size = 14, + .incomplete_operation_count = 1, + .unacked_operation_size = 14, + .unacked_operation_count = 1, + }, +}; + +static int s_mqtt5_client_statistics_unsubscribe_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = s_sub_pub_unsub_topic_filters, + .topic_filter_count = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filters), + }; + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_unsubscribe_complete_fn, + .completion_user_data = &full_test_context, + }; + aws_mqtt5_client_unsubscribe(client, &unsubscribe_view, &completion_options); + + s_sub_pub_unsub_wait_for_unsuback_received(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_client_statistics( + &test_context, s_unsubscribe_test_statistics, AWS_ARRAY_SIZE(s_unsubscribe_test_statistics))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_statistics_unsubscribe, s_mqtt5_client_statistics_unsubscribe_fn) + +static struct aws_mqtt5_client_operation_statistics s_publish_qos1_test_statistics[] = { + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 30, + .unacked_operation_count = 1, + }, +}; + +static int s_do_mqtt5_client_statistics_publish_test( + struct aws_allocator *allocator, + enum aws_mqtt5_qos qos, + struct aws_mqtt5_client_operation_statistics *expected_stats, + size_t expected_stats_count) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_send_puback_and_forward_on_publish; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = qos, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_publish_complete_fn, + .completion_user_data = &full_test_context, + }; + aws_mqtt5_client_publish(client, &publish_view, &completion_options); + + s_sub_pub_unsub_wait_for_publish_complete(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_client_statistics(&test_context, expected_stats, expected_stats_count)); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_statistics_publish_qos1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_statistics_publish_test( + allocator, + AWS_MQTT5_QOS_AT_LEAST_ONCE, + s_publish_qos1_test_statistics, + AWS_ARRAY_SIZE(s_publish_qos1_test_statistics))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_statistics_publish_qos1, s_mqtt5_client_statistics_publish_qos1_fn) + +static struct aws_mqtt5_client_operation_statistics s_publish_qos0_test_statistics[] = { + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, +}; + +static int s_mqtt5_client_statistics_publish_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_statistics_publish_test( + allocator, + AWS_MQTT5_QOS_AT_MOST_ONCE, + s_publish_qos0_test_statistics, + AWS_ARRAY_SIZE(s_publish_qos0_test_statistics))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_statistics_publish_qos0, s_mqtt5_client_statistics_publish_qos0_fn) + +static struct aws_mqtt5_client_operation_statistics s_publish_qos1_requeue_test_statistics[] = { + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 30, + .unacked_operation_count = 1, + }, + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 0, + .unacked_operation_count = 0, + }, + { + .incomplete_operation_size = 30, + .incomplete_operation_count = 1, + .unacked_operation_size = 30, + .unacked_operation_count = 1, + }, +}; + +static int s_aws_mqtt5_server_disconnect_on_first_publish_puback_after( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct aws_mqtt5_sub_pub_unsub_context *test_context = user_data; + ++test_context->publishes_received; + if (test_context->publishes_received == 1) { + return AWS_OP_ERR; + } + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + /* send a PUBACK? */ + if (publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + .reason_code = AWS_MQTT5_PARC_SUCCESS, + }; + + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_statistics_publish_qos1_requeue_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_disconnect_on_first_publish_puback_after; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_honor_session; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_sub_pub_unsub_publish_complete_fn, + .completion_user_data = &full_test_context, + }; + aws_mqtt5_client_publish(client, &publish_view, &completion_options); + + s_sub_pub_unsub_wait_for_publish_complete(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_client_statistics( + &test_context, s_publish_qos1_requeue_test_statistics, AWS_ARRAY_SIZE(s_publish_qos1_requeue_test_statistics))); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_statistics_publish_qos1_requeue, s_mqtt5_client_statistics_publish_qos1_requeue_fn) + +#define PUBACK_ORDERING_PUBLISH_COUNT 5 + +static int s_aws_mqtt5_server_send_multiple_publishes( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + /* in order: send PUBLISH packet ids 1, 2, 3, 4, 5 */ + for (size_t i = 0; i < PUBACK_ORDERING_PUBLISH_COUNT; ++i) { + struct aws_mqtt5_packet_publish_view publish_view = { + .packet_id = (uint16_t)i + 1, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +static bool s_server_received_all_pubacks(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + + size_t pubacks_received = 0; + size_t packet_count = aws_array_list_length(&test_fixture->server_received_packets); + for (size_t i = 0; i < packet_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *packet = NULL; + aws_array_list_get_at_ptr(&test_fixture->server_received_packets, (void **)&packet, i); + if (packet->packet_type == AWS_MQTT5_PT_PUBACK) { + ++pubacks_received; + } + } + + return pubacks_received == PUBACK_ORDERING_PUBLISH_COUNT; +} + +static void s_wait_for_mock_server_pubacks(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred( + &test_context->signal, &test_context->lock, s_server_received_all_pubacks, test_context); + aws_mutex_unlock(&test_context->lock); +} + +static int s_verify_mock_puback_order(struct aws_mqtt5_client_mock_test_fixture *test_context) { + + aws_mutex_lock(&test_context->lock); + + uint16_t expected_packet_id = 1; + size_t packet_count = aws_array_list_length(&test_context->server_received_packets); + + /* in order: received PUBACK packet ids 1, 2, 3, 4, 5 */ + for (size_t i = 0; i < packet_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *packet = NULL; + aws_array_list_get_at_ptr(&test_context->server_received_packets, (void **)&packet, i); + if (packet->packet_type == AWS_MQTT5_PT_PUBACK) { + struct aws_mqtt5_packet_puback_view *puback_view = + &((struct aws_mqtt5_packet_puback_storage *)(packet->packet_storage))->storage_view; + ASSERT_INT_EQUALS(expected_packet_id, puback_view->packet_id); + ++expected_packet_id; + } + } + + ASSERT_INT_EQUALS(PUBACK_ORDERING_PUBLISH_COUNT + 1, expected_packet_id); + + aws_mutex_unlock(&test_context->lock); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_puback_ordering_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_send_multiple_publishes; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + aws_mqtt5_client_publish(client, &publish_view, NULL); + + s_wait_for_mock_server_pubacks(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_mock_puback_order(&test_context)); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_puback_ordering, s_mqtt5_client_puback_ordering_fn) + +static void s_on_offline_publish_completion( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *user_data) { + (void)packet_type; + (void)packet; + + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + aws_mutex_lock(&full_test_context->test_fixture->lock); + + if (error_code == AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY) { + ++full_test_context->publish_failures; + } else if (error_code == 0) { + full_test_context->publish_complete = true; + } + + aws_mutex_unlock(&full_test_context->test_fixture->lock); + aws_condition_variable_notify_all(&full_test_context->test_fixture->signal); +} + +static bool s_has_failed_publishes(void *user_data) { + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + return full_test_context->publish_failures > 0; +} + +static void s_aws_mqtt5_wait_for_publish_failure(struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + aws_mutex_lock(&full_test_context->test_fixture->lock); + aws_condition_variable_wait_pred( + &full_test_context->test_fixture->signal, + &full_test_context->test_fixture->lock, + s_has_failed_publishes, + full_test_context); + aws_mutex_unlock(&full_test_context->test_fixture->lock); +} + +static int s_offline_publish( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + enum aws_mqtt5_qos qos) { + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = qos, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + .payload = + { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }, + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_on_offline_publish_completion, + .completion_user_data = full_test_context, + }; + + return aws_mqtt5_client_publish(client, &publish_view, &completion_options); +} + +static int s_verify_offline_publish_failure( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + enum aws_mqtt5_qos qos) { + + aws_mutex_lock(&full_test_context->test_fixture->lock); + full_test_context->publish_failures = 0; + aws_mutex_unlock(&full_test_context->test_fixture->lock); + + ASSERT_SUCCESS(s_offline_publish(client, full_test_context, qos)); + + s_aws_mqtt5_wait_for_publish_failure(full_test_context); + + return AWS_OP_SUCCESS; +} + +/* There's no signalable event for internal queue changes so we have to spin-poll this in a dumb manner */ +static void s_aws_mqtt5_wait_for_offline_queue_size( + struct aws_mqtt5_sub_pub_unsub_context *full_test_context, + size_t expected_queue_size) { + bool done = false; + struct aws_mqtt5_client *client = full_test_context->test_fixture->client; + while (!done) { + aws_thread_current_sleep(aws_timestamp_convert(100, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL)); + + struct aws_mqtt5_client_operation_statistics stats; + AWS_ZERO_STRUCT(stats); + + aws_mqtt5_client_get_stats(client, &stats); + + done = stats.incomplete_operation_count == expected_queue_size; + } +} + +static void s_on_offline_subscribe_completion( + const struct aws_mqtt5_packet_suback_view *suback_view, + int error_code, + void *user_data) { + (void)suback_view; + + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + aws_mutex_lock(&full_test_context->test_fixture->lock); + if (error_code == AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY) { + ++full_test_context->subscribe_failures; + } + aws_mutex_unlock(&full_test_context->test_fixture->lock); + aws_condition_variable_notify_all(&full_test_context->test_fixture->signal); +} + +static bool s_has_failed_subscribes(void *user_data) { + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + return full_test_context->subscribe_failures > 0; +} + +static void s_aws_mqtt5_wait_for_subscribe_failure(struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + aws_mutex_lock(&full_test_context->test_fixture->lock); + aws_condition_variable_wait_pred( + &full_test_context->test_fixture->signal, + &full_test_context->test_fixture->lock, + s_has_failed_subscribes, + full_test_context); + aws_mutex_unlock(&full_test_context->test_fixture->lock); +} + +static int s_offline_subscribe( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_on_offline_subscribe_completion, + .completion_user_data = full_test_context, + }; + + return aws_mqtt5_client_subscribe(client, &subscribe_view, &completion_options); +} + +static int s_verify_offline_subscribe_failure( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + aws_mutex_lock(&full_test_context->test_fixture->lock); + full_test_context->subscribe_failures = 0; + aws_mutex_unlock(&full_test_context->test_fixture->lock); + + ASSERT_SUCCESS(s_offline_subscribe(client, full_test_context)); + + s_aws_mqtt5_wait_for_subscribe_failure(full_test_context); + + return AWS_OP_SUCCESS; +} + +static void s_on_offline_unsubscribe_completion( + const struct aws_mqtt5_packet_unsuback_view *unsuback_view, + int error_code, + void *user_data) { + (void)unsuback_view; + + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + aws_mutex_lock(&full_test_context->test_fixture->lock); + if (error_code == AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY) { + ++full_test_context->unsubscribe_failures; + } + aws_mutex_unlock(&full_test_context->test_fixture->lock); + aws_condition_variable_notify_all(&full_test_context->test_fixture->signal); +} + +static bool s_has_failed_unsubscribes(void *user_data) { + struct aws_mqtt5_sub_pub_unsub_context *full_test_context = user_data; + + return full_test_context->unsubscribe_failures > 0; +} + +static void s_aws_mqtt5_wait_for_unsubscribe_failure(struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + aws_mutex_lock(&full_test_context->test_fixture->lock); + aws_condition_variable_wait_pred( + &full_test_context->test_fixture->signal, + &full_test_context->test_fixture->lock, + s_has_failed_unsubscribes, + full_test_context); + aws_mutex_unlock(&full_test_context->test_fixture->lock); +} + +static int s_offline_unsubscribe( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = s_sub_pub_unsub_topic_filters, + .topic_filter_count = AWS_ARRAY_SIZE(s_sub_pub_unsub_topic_filters), + }; + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .completion_callback = s_on_offline_unsubscribe_completion, + .completion_user_data = full_test_context, + }; + + return aws_mqtt5_client_unsubscribe(client, &unsubscribe_view, &completion_options); +} + +static int s_verify_offline_unsubscribe_failure( + struct aws_mqtt5_client *client, + struct aws_mqtt5_sub_pub_unsub_context *full_test_context) { + aws_mutex_lock(&full_test_context->test_fixture->lock); + full_test_context->unsubscribe_failures = 0; + aws_mutex_unlock(&full_test_context->test_fixture->lock); + + ASSERT_SUCCESS(s_offline_unsubscribe(client, full_test_context)); + + s_aws_mqtt5_wait_for_unsubscribe_failure(full_test_context); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_offline_operation_submission_fail_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + /* everything should fail on submission */ + ASSERT_SUCCESS(s_verify_offline_publish_failure(client, &full_test_context, AWS_MQTT5_QOS_AT_MOST_ONCE)); + ASSERT_SUCCESS(s_verify_offline_publish_failure(client, &full_test_context, AWS_MQTT5_QOS_AT_LEAST_ONCE)); + ASSERT_SUCCESS(s_verify_offline_subscribe_failure(client, &full_test_context)); + ASSERT_SUCCESS(s_verify_offline_unsubscribe_failure(client, &full_test_context)); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_offline_operation_submission_fail_all, + s_mqtt5_client_offline_operation_submission_fail_all_fn) + +static int s_mqtt5_client_offline_operation_submission_fail_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + /* qos 0 publish should fail on submission */ + ASSERT_SUCCESS(s_verify_offline_publish_failure(client, &full_test_context, AWS_MQTT5_QOS_AT_MOST_ONCE)); + + /* qos 1 publish, subscribe, and unsubscribe should queue on submission */ + ASSERT_SUCCESS(s_offline_publish(client, &full_test_context, AWS_MQTT5_QOS_AT_LEAST_ONCE)); + s_aws_mqtt5_wait_for_offline_queue_size(&full_test_context, 1); + ASSERT_SUCCESS(s_offline_subscribe(client, &full_test_context)); + s_aws_mqtt5_wait_for_offline_queue_size(&full_test_context, 2); + ASSERT_SUCCESS(s_offline_unsubscribe(client, &full_test_context)); + s_aws_mqtt5_wait_for_offline_queue_size(&full_test_context, 3); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_offline_operation_submission_fail_qos0, + s_mqtt5_client_offline_operation_submission_fail_qos0_fn) + +static int s_mqtt5_client_offline_operation_submission_fail_non_qos1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + /* qos0 publish, subscribe, and unsubscribe should fail on submission */ + ASSERT_SUCCESS(s_verify_offline_publish_failure(client, &full_test_context, AWS_MQTT5_QOS_AT_MOST_ONCE)); + ASSERT_SUCCESS(s_verify_offline_subscribe_failure(client, &full_test_context)); + ASSERT_SUCCESS(s_verify_offline_unsubscribe_failure(client, &full_test_context)); + + /* qos1 publish should queue on submission */ + ASSERT_SUCCESS(s_offline_publish(client, &full_test_context, AWS_MQTT5_QOS_AT_LEAST_ONCE)); + s_aws_mqtt5_wait_for_offline_queue_size(&full_test_context, 1); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_offline_operation_submission_fail_non_qos1, + s_mqtt5_client_offline_operation_submission_fail_non_qos1_fn) + +static int s_mqtt5_client_offline_operation_submission_then_connect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_send_puback_and_forward_on_publish; + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + /* qos1 publish should queue on submission */ + ASSERT_SUCCESS(s_offline_publish(client, &full_test_context, AWS_MQTT5_QOS_AT_LEAST_ONCE)); + s_aws_mqtt5_wait_for_offline_queue_size(&full_test_context, 1); + + /* start the client, it should connect and immediately send the publish */ + aws_mqtt5_client_start(client); + + s_sub_pub_unsub_wait_for_publish_complete(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_offline_operation_submission_then_connect, + s_mqtt5_client_offline_operation_submission_then_connect_fn) + +#define ALIASED_PUBLISH_SEQUENCE_COUNT 4 + +struct aws_mqtt5_aliased_publish_sequence_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + struct aws_allocator *allocator; + + struct aws_array_list publishes_received; +}; + +static int s_aws_mqtt5_aliased_publish_sequence_context_init( + struct aws_mqtt5_aliased_publish_sequence_context *context, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*context); + + context->allocator = allocator; + return aws_array_list_init_dynamic( + &context->publishes_received, allocator, 10, sizeof(struct aws_mqtt5_packet_publish_storage *)); +} + +static void s_aws_mqtt5_aliased_publish_sequence_context_clean_up( + struct aws_mqtt5_aliased_publish_sequence_context *context) { + for (size_t i = 0; i < aws_array_list_length(&context->publishes_received); ++i) { + struct aws_mqtt5_packet_publish_storage *storage = NULL; + aws_array_list_get_at(&context->publishes_received, &storage, i); + + aws_mqtt5_packet_publish_storage_clean_up(storage); + aws_mem_release(context->allocator, storage); + } + aws_array_list_clean_up(&context->publishes_received); +} + +void s_aliased_publish_received_fn(const struct aws_mqtt5_packet_publish_view *publish, void *complete_ctx) { + + AWS_FATAL_ASSERT(publish != NULL); + + struct aws_mqtt5_aliased_publish_sequence_context *full_test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = full_test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + + struct aws_mqtt5_packet_publish_storage *storage = + aws_mem_calloc(full_test_context->allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); + aws_mqtt5_packet_publish_storage_init(storage, full_test_context->allocator, publish); + + aws_array_list_push_back(&full_test_context->publishes_received, &storage); + + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +static enum aws_mqtt5_suback_reason_code s_alias_reason_codes[] = { + AWS_MQTT5_SARC_GRANTED_QOS_1, + AWS_MQTT5_SARC_GRANTED_QOS_1, +}; +static uint8_t s_alias_topic1[] = "alias/first/topic"; +static uint8_t s_alias_topic2[] = "alias/second/topic"; + +static int s_aws_mqtt5_server_send_aliased_publish_sequence( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; + + // just to be thorough, send a suback + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { + return AWS_OP_ERR; + } + + uint16_t alias_id = 1; + struct aws_mqtt5_packet_publish_view publish_view = { + .packet_id = 1, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_alias_topic1, + .len = AWS_ARRAY_SIZE(s_alias_topic1) - 1, + }, + .topic_alias = &alias_id, + }; + + // establish an alias with id 1 + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + + // alias alone + AWS_ZERO_STRUCT(publish_view.topic); + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + + // establish a new alias with id 1 + publish_view.topic.ptr = s_alias_topic2; + publish_view.topic.len = AWS_ARRAY_SIZE(s_alias_topic2) - 1; + + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + + // alias alone + AWS_ZERO_STRUCT(publish_view.topic); + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static bool s_client_received_aliased_publish_sequence(void *arg) { + struct aws_mqtt5_aliased_publish_sequence_context *full_test_context = arg; + + return aws_array_list_length(&full_test_context->publishes_received) == ALIASED_PUBLISH_SEQUENCE_COUNT; +} + +static void s_wait_for_aliased_publish_sequence(struct aws_mqtt5_aliased_publish_sequence_context *full_test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = full_test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_client_received_aliased_publish_sequence, full_test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static int s_verify_aliased_publish_sequence(struct aws_mqtt5_aliased_publish_sequence_context *full_test_context) { + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = full_test_context->test_fixture; + aws_mutex_lock(&test_fixture->lock); + + for (size_t i = 0; i < aws_array_list_length(&full_test_context->publishes_received); ++i) { + struct aws_mqtt5_packet_publish_storage *publish_storage = NULL; + aws_array_list_get_at(&full_test_context->publishes_received, &publish_storage, i); + + struct aws_byte_cursor topic_cursor = publish_storage->storage_view.topic; + + if (i < 2) { + // the first two publishes should be the first topic + ASSERT_BIN_ARRAYS_EQUALS( + s_alias_topic1, AWS_ARRAY_SIZE(s_alias_topic1) - 1, topic_cursor.ptr, topic_cursor.len); + } else { + // the last two publishes should be the second topic + ASSERT_BIN_ARRAYS_EQUALS( + s_alias_topic2, AWS_ARRAY_SIZE(s_alias_topic2) - 1, topic_cursor.ptr, topic_cursor.len); + } + } + + aws_mutex_unlock(&test_fixture->lock); + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt5_subscription_view s_alias_subscriptions[] = { + { + .topic_filter = + { + .ptr = (uint8_t *)s_alias_topic1, + .len = AWS_ARRAY_SIZE(s_alias_topic1) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + }, + { + .topic_filter = + { + .ptr = (uint8_t *)s_alias_topic2, + .len = AWS_ARRAY_SIZE(s_alias_topic2) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + }, +}; + +static int s_mqtt5_client_inbound_alias_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_aliased_publish_sequence; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_aliased_publish_sequence_context full_test_context; + ASSERT_SUCCESS(s_aws_mqtt5_aliased_publish_sequence_context_init(&full_test_context, allocator)); + full_test_context.test_fixture = &test_context; + + struct aws_mqtt5_client_topic_alias_options aliasing_config = { + .inbound_alias_cache_size = 10, + .inbound_topic_alias_behavior = AWS_MQTT5_CITABT_ENABLED, + }; + + test_options.client_options.topic_aliasing_options = &aliasing_config; + test_options.client_options.publish_received_handler = s_aliased_publish_received_fn; + test_options.client_options.publish_received_handler_user_data = &full_test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_view subscribe = { + .subscriptions = s_alias_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_alias_subscriptions), + }; + + ASSERT_SUCCESS(aws_mqtt5_client_subscribe(client, &subscribe, NULL)); + + s_wait_for_aliased_publish_sequence(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_aliased_publish_sequence(&full_test_context)); + + s_aws_mqtt5_aliased_publish_sequence_context_clean_up(&full_test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_inbound_alias_success, s_mqtt5_client_inbound_alias_success_fn) + +enum aws_mqtt5_test_inbound_alias_failure_type { + AWS_MTIAFT_DISABLED, + AWS_MTIAFT_ZERO_ID, + AWS_MTIAFT_TOO_LARGE_ID, + AWS_MTIAFT_UNBOUND_ID +}; + +struct aws_mqtt5_test_inbound_alias_failure_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + + enum aws_mqtt5_test_inbound_alias_failure_type failure_type; +}; + +static int s_aws_mqtt5_server_send_aliased_publish_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + struct aws_mqtt5_test_inbound_alias_failure_context *test_context = user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; + + // just to be thorough, send a suback + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { + return AWS_OP_ERR; + } + + uint16_t alias_id = 0; + struct aws_byte_cursor topic_cursor = { + .ptr = s_alias_topic1, + .len = AWS_ARRAY_SIZE(s_alias_topic1) - 1, + }; + + switch (test_context->failure_type) { + case AWS_MTIAFT_TOO_LARGE_ID: + alias_id = 100; + break; + + case AWS_MTIAFT_UNBOUND_ID: + AWS_ZERO_STRUCT(topic_cursor); + alias_id = 1; + break; + + default: + break; + } + + struct aws_mqtt5_packet_publish_view publish_view = { + .packet_id = 1, .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, .topic = topic_cursor, .topic_alias = &alias_id}; + + // establish an alias with id 1 + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static bool s_has_decoding_error_disconnect_event(void *arg) { + struct aws_mqtt5_test_inbound_alias_failure_context *test_context = arg; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + size_t record_count = aws_array_list_length(&test_fixture->lifecycle_events); + for (size_t i = 0; i < record_count; ++i) { + struct aws_mqtt5_lifecycle_event_record *record = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); + if (record->event.event_type == AWS_MQTT5_CLET_DISCONNECTION) { + if (record->event.error_code == AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR) { + return true; + } + } + } + + return false; +} + +static void s_wait_for_decoding_error_disconnect(struct aws_mqtt5_test_inbound_alias_failure_context *test_context) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred( + &test_fixture->signal, &test_fixture->lock, s_has_decoding_error_disconnect_event, test_context); + aws_mutex_unlock(&test_fixture->lock); +} + +static int s_do_inbound_alias_failure_test( + struct aws_allocator *allocator, + enum aws_mqtt5_test_inbound_alias_failure_type test_failure_type) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_aliased_publish_failure; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_test_inbound_alias_failure_context full_test_context = { + .test_fixture = &test_context, + .failure_type = test_failure_type, + }; + + struct aws_mqtt5_client_topic_alias_options aliasing_config = { + .inbound_alias_cache_size = 10, + .inbound_topic_alias_behavior = + (test_failure_type == AWS_MTIAFT_DISABLED) ? AWS_MQTT5_CITABT_DISABLED : AWS_MQTT5_CITABT_ENABLED, + }; + + test_options.client_options.topic_aliasing_options = &aliasing_config; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_subscribe_view subscribe = { + .subscriptions = s_alias_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_alias_subscriptions), + }; + + ASSERT_SUCCESS(aws_mqtt5_client_subscribe(client, &subscribe, NULL)); + + s_wait_for_decoding_error_disconnect(&full_test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_inbound_alias_failure_disabled_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_inbound_alias_failure_test(allocator, AWS_MTIAFT_DISABLED)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_inbound_alias_failure_disabled, s_mqtt5_client_inbound_alias_failure_disabled_fn) + +static int s_mqtt5_client_inbound_alias_failure_zero_id_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_inbound_alias_failure_test(allocator, AWS_MTIAFT_ZERO_ID)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_inbound_alias_failure_zero_id, s_mqtt5_client_inbound_alias_failure_zero_id_fn) + +static int s_mqtt5_client_inbound_alias_failure_too_large_id_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_inbound_alias_failure_test(allocator, AWS_MTIAFT_TOO_LARGE_ID)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_inbound_alias_failure_too_large_id, s_mqtt5_client_inbound_alias_failure_too_large_id_fn) + +static int s_mqtt5_client_inbound_alias_failure_unbound_id_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_inbound_alias_failure_test(allocator, AWS_MTIAFT_UNBOUND_ID)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_inbound_alias_failure_unbound_id, s_mqtt5_client_inbound_alias_failure_unbound_id_fn) + +void s_outbound_alias_failure_publish_complete_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + (void)packet_type; + (void)packet; + AWS_FATAL_ASSERT(error_code != AWS_ERROR_SUCCESS); + + struct aws_mqtt5_sub_pub_unsub_context *test_context = complete_ctx; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = test_context->test_fixture; + + aws_mutex_lock(&test_fixture->lock); + test_context->publish_failures++; + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +#define SEQUENCE_TEST_CACHE_SIZE 2 + +static int s_aws_mqtt5_mock_server_handle_connect_allow_aliasing( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + uint16_t topic_alias_maximum = SEQUENCE_TEST_CACHE_SIZE; + connack_view.topic_alias_maximum = &topic_alias_maximum; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +static int s_do_mqtt5_client_outbound_alias_failure_test( + struct aws_allocator *allocator, + enum aws_mqtt5_client_outbound_topic_alias_behavior_type behavior_type) { + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; + + test_options.client_options.topic_aliasing_options->outbound_topic_alias_behavior = behavior_type; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_sub_pub_unsub_context full_test_context = { + .test_fixture = &test_context, + }; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + uint16_t topic_alias = 1; + struct aws_mqtt5_packet_publish_view packet_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }, + .topic_alias = &topic_alias, + }; + + if (behavior_type == AWS_MQTT5_COTABT_USER) { + AWS_ZERO_STRUCT(packet_publish_view.topic); + } + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_outbound_alias_failure_publish_complete_fn, + .completion_user_data = &full_test_context, + }; + + /* should result in an immediate validation failure or a subsequent dynamic validation failure */ + if (aws_mqtt5_client_publish(client, &packet_publish_view, &completion_options) == AWS_OP_SUCCESS) { + s_aws_mqtt5_wait_for_publish_failure(&full_test_context); + } + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_outbound_alias_disabled_failure_alias_set_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_DISABLED)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_outbound_alias_disabled_failure_alias_set, + s_mqtt5_client_outbound_alias_disabled_failure_alias_set_fn) + +static int s_mqtt5_client_outbound_alias_user_failure_empty_topic_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_USER)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_outbound_alias_user_failure_empty_topic, + s_mqtt5_client_outbound_alias_user_failure_empty_topic_fn) + +static int s_mqtt5_client_outbound_alias_lru_failure_alias_set_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_LRU)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_outbound_alias_lru_failure_alias_set, s_mqtt5_client_outbound_alias_lru_failure_alias_set_fn) + +struct outbound_alias_publish { + struct aws_byte_cursor topic; + uint16_t topic_alias; + + size_t expected_alias_id; + bool expected_reuse; +}; + +#define DEFINE_OUTBOUND_ALIAS_PUBLISH(topic_suffix, desired_alias, expected_alias_index, reused) \ + { \ + .topic = aws_byte_cursor_from_string(s_topic_##topic_suffix), .topic_alias = desired_alias, \ + .expected_alias_id = expected_alias_index, .expected_reuse = reused, \ + } + +static void s_outbound_alias_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + AWS_FATAL_ASSERT(packet_type == AWS_MQTT5_PT_PUBACK); + AWS_FATAL_ASSERT(error_code == AWS_ERROR_SUCCESS); + + const struct aws_mqtt5_packet_puback_view *puback = packet; + struct aws_mqtt5_client_mock_test_fixture *test_context = complete_ctx; + + aws_mutex_lock(&test_context->lock); + + ++test_context->total_pubacks_received; + if (error_code == AWS_ERROR_SUCCESS && puback->reason_code < 128) { + ++test_context->successful_pubacks_received; + } + + aws_mutex_unlock(&test_context->lock); + aws_condition_variable_notify_all(&test_context->signal); +} + +static int s_perform_outbound_alias_publish( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + struct outbound_alias_publish *publish) { + + struct aws_mqtt5_client *client = test_fixture->client; + + uint16_t alias_id = publish->topic_alias; + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = publish->topic, + }; + + if (alias_id != 0) { + publish_view.topic_alias = &alias_id; + } + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_outbound_alias_publish_completion_fn, + .completion_user_data = test_fixture, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &publish_view, &completion_options)); + + return AWS_OP_SUCCESS; +} + +static int s_perform_outbound_alias_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + struct outbound_alias_publish *publishes, + size_t publish_count) { + + for (size_t i = 0; i < publish_count; ++i) { + struct outbound_alias_publish *publish = &publishes[i]; + ASSERT_SUCCESS(s_perform_outbound_alias_publish(test_fixture, publish)); + } + + return AWS_OP_SUCCESS; +} + +static int s_perform_outbound_alias_sequence_test( + struct aws_allocator *allocator, + enum aws_mqtt5_client_outbound_topic_alias_behavior_type behavior_type, + struct outbound_alias_publish *publishes, + size_t publish_count) { + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_publish_puback; + + test_options.client_options.topic_aliasing_options->outbound_topic_alias_behavior = behavior_type; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + test_context.maximum_inbound_topic_aliases = SEQUENCE_TEST_CACHE_SIZE; + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_perform_outbound_alias_sequence(&test_context, publishes, publish_count)); + + struct aws_mqtt5_client_test_wait_for_n_context wait_context = { + .test_fixture = &test_context, + .required_event_count = publish_count, + }; + s_wait_for_n_successful_publishes(&wait_context); + + aws_mutex_lock(&test_context.lock); + size_t packet_count = aws_array_list_length(&test_context.server_received_packets); + ASSERT_INT_EQUALS(1 + publish_count, packet_count); // N publishes, 1 connect + + /* start at 1 and skip the connect */ + for (size_t i = 1; i < packet_count; ++i) { + struct aws_mqtt5_mock_server_packet_record *packet = NULL; + aws_array_list_get_at_ptr(&test_context.server_received_packets, (void **)&packet, i); + + ASSERT_INT_EQUALS(AWS_MQTT5_PT_PUBLISH, packet->packet_type); + struct aws_mqtt5_packet_publish_storage *publish_storage = packet->packet_storage; + struct aws_mqtt5_packet_publish_view *publish_view = &publish_storage->storage_view; + + struct outbound_alias_publish *publish = &publishes[i - 1]; + ASSERT_NOT_NULL(publish_view->topic_alias); + ASSERT_INT_EQUALS(publish->expected_alias_id, *publish_view->topic_alias); + + /* + * Unfortunately, the decoder fails unless it has an inbound resolver and the inbound resolver will always + * resolve the topics first. So we can't actually check that an empty topic was sent. It would be nice to + * harden this up in the future. + */ + ASSERT_BIN_ARRAYS_EQUALS( + publish->topic.ptr, publish->topic.len, publish_view->topic.ptr, publish_view->topic.len); + } + + aws_mutex_unlock(&test_context.lock); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_topic_a, "topic/a"); +AWS_STATIC_STRING_FROM_LITERAL(s_topic_b, "b/topic"); +AWS_STATIC_STRING_FROM_LITERAL(s_topic_c, "topic/c"); + +static int s_mqtt5_client_outbound_alias_user_success_a_b_ar_br_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct outbound_alias_publish test_publishes[] = { + DEFINE_OUTBOUND_ALIAS_PUBLISH(a, 1, 1, false), + DEFINE_OUTBOUND_ALIAS_PUBLISH(b, 2, 2, false), + DEFINE_OUTBOUND_ALIAS_PUBLISH(a, 1, 1, true), + DEFINE_OUTBOUND_ALIAS_PUBLISH(b, 2, 2, true), + }; + + ASSERT_SUCCESS(s_perform_outbound_alias_sequence_test( + allocator, AWS_MQTT5_COTABT_USER, test_publishes, AWS_ARRAY_SIZE(test_publishes))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_outbound_alias_user_success_a_b_ar_br, + s_mqtt5_client_outbound_alias_user_success_a_b_ar_br_fn) + +static int s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct outbound_alias_publish test_publishes[] = { + DEFINE_OUTBOUND_ALIAS_PUBLISH(a, 0, 1, false), + DEFINE_OUTBOUND_ALIAS_PUBLISH(b, 0, 2, false), + DEFINE_OUTBOUND_ALIAS_PUBLISH(c, 0, 1, false), + DEFINE_OUTBOUND_ALIAS_PUBLISH(b, 0, 2, true), + DEFINE_OUTBOUND_ALIAS_PUBLISH(c, 0, 1, true), + DEFINE_OUTBOUND_ALIAS_PUBLISH(a, 0, 2, false), + }; + + ASSERT_SUCCESS(s_perform_outbound_alias_sequence_test( + allocator, AWS_MQTT5_COTABT_LRU, test_publishes, AWS_ARRAY_SIZE(test_publishes))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a, + s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn) diff --git a/tests/v5/mqtt5_encoding_tests.c b/tests/v5/mqtt5_encoding_tests.c new file mode 100644 index 00000000..b9e5c2db --- /dev/null +++ b/tests/v5/mqtt5_encoding_tests.c @@ -0,0 +1,1808 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt5_testing_utils.h" +#include +#include +#include +#include +#include +#include +#include + +#include + +/* + * AWS_MQTT_API enum aws_mqtt5_decode_result_type aws_mqtt5_decode_vli(struct aws_byte_cursor *cursor, uint32_t *dest); + * AWS_MQTT_API int aws_mqtt5_encode_variable_length_integer(struct aws_byte_buf *buf, uint32_t value); + * AWS_MQTT_API int aws_mqtt5_get_variable_length_encode_size(size_t value, size_t *encode_size); + */ + +static int s_mqtt5_vli_size_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + size_t encode_size = 0; + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(0, &encode_size)); + ASSERT_INT_EQUALS(1, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(1, &encode_size)); + ASSERT_INT_EQUALS(1, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(127, &encode_size)); + ASSERT_INT_EQUALS(1, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(128, &encode_size)); + ASSERT_INT_EQUALS(2, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(256, &encode_size)); + ASSERT_INT_EQUALS(2, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16383, &encode_size)); + ASSERT_INT_EQUALS(2, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16384, &encode_size)); + ASSERT_INT_EQUALS(3, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16384, &encode_size)); + ASSERT_INT_EQUALS(3, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16385, &encode_size)); + ASSERT_INT_EQUALS(3, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(2097151, &encode_size)); + ASSERT_INT_EQUALS(3, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(2097152, &encode_size)); + ASSERT_INT_EQUALS(4, encode_size); + + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER, &encode_size)); + ASSERT_INT_EQUALS(4, encode_size); + + ASSERT_FAILS( + aws_mqtt5_get_variable_length_encode_size(AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER + 1, &encode_size)); + ASSERT_FAILS(aws_mqtt5_get_variable_length_encode_size(0xFFFFFFFF, &encode_size)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_vli_size, s_mqtt5_vli_size_fn) + +static int s_do_success_round_trip_vli_test(uint32_t value, struct aws_allocator *allocator) { + struct aws_byte_buf buffer; + aws_byte_buf_init(&buffer, allocator, 4); + + ASSERT_SUCCESS(aws_mqtt5_encode_variable_length_integer(&buffer, value)); + + size_t encoded_size = 0; + ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(value, &encoded_size)); + ASSERT_INT_EQUALS(encoded_size, buffer.len); + + uint32_t decoded_value = 0; + for (size_t i = 1; i < encoded_size; ++i) { + struct aws_byte_cursor partial_cursor = aws_byte_cursor_from_buf(&buffer); + partial_cursor.len = i; + + enum aws_mqtt5_decode_result_type result = aws_mqtt5_decode_vli(&partial_cursor, &decoded_value); + ASSERT_INT_EQUALS(AWS_MQTT5_DRT_MORE_DATA, result); + } + + struct aws_byte_cursor full_cursor = aws_byte_cursor_from_buf(&buffer); + + enum aws_mqtt5_decode_result_type result = aws_mqtt5_decode_vli(&full_cursor, &decoded_value); + ASSERT_INT_EQUALS(AWS_MQTT5_DRT_SUCCESS, result); + ASSERT_INT_EQUALS(decoded_value, value); + + aws_byte_buf_clean_up(&buffer); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_vli_success_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(0, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(1, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(47, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(127, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(128, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(129, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(511, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(8000, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(16383, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(16384, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(16385, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(100000, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(4200000, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(34200000, allocator)); + ASSERT_SUCCESS(s_do_success_round_trip_vli_test(AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER, allocator)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_vli_success_round_trip, s_mqtt5_vli_success_round_trip_fn) + +static int s_mqtt5_vli_encode_failures_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_buf buffer; + aws_byte_buf_init(&buffer, allocator, 4); + + ASSERT_FAILS(aws_mqtt5_encode_variable_length_integer(&buffer, AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER + 1)); + ASSERT_FAILS(aws_mqtt5_encode_variable_length_integer(&buffer, 0x80000000)); + ASSERT_FAILS(aws_mqtt5_encode_variable_length_integer(&buffer, 0xFFFFFFFF)); + + aws_byte_buf_clean_up(&buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_vli_encode_failures, s_mqtt5_vli_encode_failures_fn) + +static uint8_t bad_buffers0[] = {0x80, 0x80, 0x80, 0x80}; +static uint8_t bad_buffers1[] = {0x81, 0x81, 0x81, 0xFF}; + +static int s_mqtt5_vli_decode_failures_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + uint32_t value = 0; + + struct aws_byte_cursor cursor = aws_byte_cursor_from_array(&bad_buffers0[0], AWS_ARRAY_SIZE(bad_buffers0)); + ASSERT_INT_EQUALS(AWS_MQTT5_DRT_ERROR, aws_mqtt5_decode_vli(&cursor, &value)); + + cursor = aws_byte_cursor_from_array(&bad_buffers1[0], AWS_ARRAY_SIZE(bad_buffers1)); + ASSERT_INT_EQUALS(AWS_MQTT5_DRT_ERROR, aws_mqtt5_decode_vli(&cursor, &value)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_vli_decode_failures, s_mqtt5_vli_decode_failures_fn) + +static char s_user_prop1_name[] = "Property1"; +static char s_user_prop1_value[] = "Value1"; +static char s_user_prop2_name[] = "Property2"; +static char s_user_prop2_value[] = "Value2"; +static const struct aws_mqtt5_user_property s_user_properties[] = { + { + .name = + { + .ptr = (uint8_t *)s_user_prop1_name, + .len = AWS_ARRAY_SIZE(s_user_prop1_name) - 1, + }, + .value = + { + .ptr = (uint8_t *)s_user_prop1_value, + .len = AWS_ARRAY_SIZE(s_user_prop1_value) - 1, + }, + }, + { + .name = + { + .ptr = (uint8_t *)s_user_prop2_name, + .len = AWS_ARRAY_SIZE(s_user_prop2_name) - 1, + }, + .value = + { + .ptr = (uint8_t *)s_user_prop2_value, + .len = AWS_ARRAY_SIZE(s_user_prop2_value) - 1, + }, + }, +}; + +static const char *s_reason_string = "This is why I'm disconnecting"; +static const char *s_server_reference = "connect-here-instead.com"; +static const char *s_will_payload = "{\"content\":\"This is the payload of a will message\"}"; +static const char *s_will_topic = "zomg/where-did/my/connection-go"; +static const char *s_will_response_topic = "TheWillResponseTopic"; +static const char *s_will_correlation_data = "ThisAndThat"; +static const char *s_will_content_type = "Json"; +static const char *s_client_id = "DeviceNumber47"; +static const char *s_username = "MyUser"; +static const char *s_password = "SuprSekritDontRead"; + +struct aws_mqtt5_encode_decode_tester { + struct aws_mqtt5_encoder_function_table encoder_function_table; + struct aws_mqtt5_decoder_function_table decoder_function_table; + + size_t view_count; + + size_t expected_view_count; + void *expected_views; +}; + +static void s_aws_mqtt5_encode_decoder_tester_init_single_view( + struct aws_mqtt5_encode_decode_tester *tester, + void *expected_view) { + tester->view_count = 0; + tester->expected_view_count = 1; + tester->expected_views = expected_view; + + aws_mqtt5_encode_init_testing_function_table(&tester->encoder_function_table); + aws_mqtt5_decode_init_testing_function_table(&tester->decoder_function_table); +} + +int s_check_packet_encoding_reserved_flags(struct aws_byte_buf *encoding, enum aws_mqtt5_packet_type packet_type) { + if (packet_type == AWS_MQTT5_PT_PUBLISH) { + return AWS_OP_SUCCESS; + } + + uint8_t expected_reserved_flags_value = 0; + switch (packet_type) { + case AWS_MQTT5_PT_PUBREL: + case AWS_MQTT5_PT_SUBSCRIBE: + case AWS_MQTT5_PT_UNSUBSCRIBE: + expected_reserved_flags_value = 0x02; + break; + + default: + break; + } + + uint8_t first_byte = encoding->buffer[0]; + + uint8_t encoded_packet_type = (first_byte >> 4) & 0x0F; + ASSERT_INT_EQUALS(packet_type, encoded_packet_type); + + uint8_t reserved_flags = first_byte & 0x0F; + ASSERT_INT_EQUALS(expected_reserved_flags_value, reserved_flags); + + return AWS_OP_SUCCESS; +} + +typedef int(aws_mqtt5_check_encoding_fn)(struct aws_byte_buf *encoding); + +struct aws_mqtt5_packet_round_trip_test_context { + struct aws_allocator *allocator; + enum aws_mqtt5_packet_type packet_type; + void *packet_view; + aws_mqtt5_on_packet_received_fn *decoder_callback; + aws_mqtt5_check_encoding_fn *encoding_checker; +}; + +static int s_aws_mqtt5_encode_decode_round_trip_test( + struct aws_mqtt5_packet_round_trip_test_context *context, + size_t encode_fragment_size, + size_t decode_fragment_size) { + struct aws_byte_buf whole_dest; + aws_byte_buf_init(&whole_dest, context->allocator, 4096); + + struct aws_mqtt5_encode_decode_tester tester; + s_aws_mqtt5_encode_decoder_tester_init_single_view(&tester, context->packet_view); + + struct aws_mqtt5_encoder_options encoder_options = { + .encoders = &tester.encoder_function_table, + }; + + struct aws_mqtt5_encoder encoder; + ASSERT_SUCCESS(aws_mqtt5_encoder_init(&encoder, context->allocator, &encoder_options)); + ASSERT_SUCCESS(aws_mqtt5_encoder_append_packet_encoding(&encoder, context->packet_type, context->packet_view)); + + enum aws_mqtt5_encoding_result result = AWS_MQTT5_ER_OUT_OF_ROOM; + while (result == AWS_MQTT5_ER_OUT_OF_ROOM) { + struct aws_byte_buf fragment_dest; + aws_byte_buf_init(&fragment_dest, context->allocator, encode_fragment_size); + + result = aws_mqtt5_encoder_encode_to_buffer(&encoder, &fragment_dest); + ASSERT_TRUE(result != AWS_MQTT5_ER_ERROR); + + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_from_buf(&fragment_dest); + ASSERT_SUCCESS(aws_byte_buf_append_dynamic(&whole_dest, &fragment_cursor)); + + aws_byte_buf_clean_up(&fragment_dest); + } + + /* + * For packet types that support encoded size calculations (outgoing operations), verify that the encoded size + * calculation matches the length we encoded + */ + size_t expected_packet_size = 0; + if (!aws_mqtt5_packet_view_get_encoded_size(context->packet_type, context->packet_view, &expected_packet_size)) { + ASSERT_INT_EQUALS(whole_dest.len, expected_packet_size); + } + + ASSERT_SUCCESS(s_check_packet_encoding_reserved_flags(&whole_dest, context->packet_type)); + + ASSERT_INT_EQUALS(AWS_MQTT5_ER_FINISHED, result); + + struct aws_mqtt5_decoder_options decoder_options = { + .on_packet_received = context->decoder_callback, + .callback_user_data = &tester, + .decoder_table = &tester.decoder_function_table, + }; + + struct aws_mqtt5_decoder decoder; + ASSERT_SUCCESS(aws_mqtt5_decoder_init(&decoder, context->allocator, &decoder_options)); + + struct aws_mqtt5_inbound_topic_alias_resolver inbound_alias_resolver; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_init(&inbound_alias_resolver, context->allocator)); + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&inbound_alias_resolver, 65535)); + aws_mqtt5_decoder_set_inbound_topic_alias_resolver(&decoder, &inbound_alias_resolver); + + struct aws_byte_cursor whole_cursor = aws_byte_cursor_from_buf(&whole_dest); + while (whole_cursor.len > 0) { + size_t advance = aws_min_size(whole_cursor.len, decode_fragment_size); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&whole_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt5_decoder_on_data_received(&decoder, fragment_cursor)); + } + + ASSERT_INT_EQUALS(1, tester.view_count); + + aws_byte_buf_clean_up(&whole_dest); + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&inbound_alias_resolver); + aws_mqtt5_encoder_clean_up(&encoder); + aws_mqtt5_decoder_clean_up(&decoder); + + return AWS_OP_SUCCESS; +} + +static size_t s_encode_fragment_sizes[] = {4, 5, 7, 65536}; +static size_t s_decode_fragment_sizes[] = {1, 2, 3, 11, 65536}; + +static int s_aws_mqtt5_encode_decode_round_trip_matrix_test(struct aws_mqtt5_packet_round_trip_test_context *context) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_encode_fragment_sizes); ++i) { + size_t encode_fragment_size = s_encode_fragment_sizes[i]; + for (size_t j = 0; j < AWS_ARRAY_SIZE(s_decode_fragment_sizes); ++j) { + size_t decode_fragment_size = s_decode_fragment_sizes[j]; + + ASSERT_SUCCESS( + s_aws_mqtt5_encode_decode_round_trip_test(context, encode_fragment_size, decode_fragment_size)); + } + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_on_disconnect_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_DISCONNECT, (uint32_t)type); + + struct aws_mqtt5_packet_disconnect_view *disconnect_view = packet_view; + struct aws_mqtt5_packet_disconnect_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS((uint32_t)expected_view->reason_code, (uint32_t)disconnect_view->reason_code); + ASSERT_INT_EQUALS( + *expected_view->session_expiry_interval_seconds, *disconnect_view->session_expiry_interval_seconds); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->reason_string->ptr, + expected_view->reason_string->len, + disconnect_view->reason_string->ptr, + disconnect_view->reason_string->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->server_reference->ptr, + expected_view->server_reference->len, + disconnect_view->server_reference->ptr, + disconnect_view->server_reference->len); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + disconnect_view->user_property_count, + disconnect_view->user_properties, + expected_view->user_property_count, + expected_view->user_properties)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_disconnect_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + uint32_t session_expiry_interval_seconds = 333; + struct aws_byte_cursor reason_string_cursor = aws_byte_cursor_from_c_str(s_reason_string); + struct aws_byte_cursor server_reference_cursor = aws_byte_cursor_from_c_str(s_server_reference); + + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_code = AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .reason_string = &reason_string_cursor, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .server_reference = &server_reference_cursor, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_DISCONNECT, + .packet_view = &disconnect_view, + .decoder_callback = s_aws_mqtt5_on_disconnect_received_fn, + }; + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_disconnect_round_trip, s_mqtt5_packet_disconnect_round_trip_fn) + +static int s_aws_mqtt5_on_pingreq_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_PINGREQ, (uint32_t)type); + ASSERT_NULL(packet_view); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_pingreq_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_PINGREQ, + .packet_view = NULL, + .decoder_callback = s_aws_mqtt5_on_pingreq_received_fn, + }; + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_pingreq_round_trip, s_mqtt5_packet_pingreq_round_trip_fn) + +static int s_aws_mqtt5_on_pingresp_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_PINGRESP, (uint32_t)type); + ASSERT_NULL(packet_view); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_pingresp_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_PINGRESP, + .packet_view = NULL, + .decoder_callback = s_aws_mqtt5_on_pingresp_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_pingresp_round_trip, s_mqtt5_packet_pingresp_round_trip_fn) + +static int s_aws_mqtt5_on_connect_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_CONNECT, (uint32_t)type); + + struct aws_mqtt5_packet_connect_view *connect_view = packet_view; + struct aws_mqtt5_packet_connect_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->keep_alive_interval_seconds, connect_view->keep_alive_interval_seconds); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->client_id.ptr, + expected_view->client_id.len, + connect_view->client_id.ptr, + connect_view->client_id.len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->username->ptr, + expected_view->username->len, + connect_view->username->ptr, + connect_view->username->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->password->ptr, + expected_view->password->len, + connect_view->password->ptr, + connect_view->password->len); + ASSERT_TRUE(expected_view->clean_start == connect_view->clean_start); + ASSERT_INT_EQUALS(*expected_view->session_expiry_interval_seconds, *connect_view->session_expiry_interval_seconds); + + ASSERT_INT_EQUALS(*expected_view->request_response_information, *connect_view->request_response_information); + ASSERT_INT_EQUALS(*expected_view->request_problem_information, *connect_view->request_problem_information); + ASSERT_INT_EQUALS(*expected_view->receive_maximum, *connect_view->receive_maximum); + ASSERT_INT_EQUALS(*expected_view->topic_alias_maximum, *connect_view->topic_alias_maximum); + ASSERT_INT_EQUALS(*expected_view->maximum_packet_size_bytes, *connect_view->maximum_packet_size_bytes); + + ASSERT_INT_EQUALS(*expected_view->will_delay_interval_seconds, *connect_view->will_delay_interval_seconds); + + const struct aws_mqtt5_packet_publish_view *expected_will_view = expected_view->will; + const struct aws_mqtt5_packet_publish_view *will_view = connect_view->will; + + ASSERT_BIN_ARRAYS_EQUALS( + expected_will_view->payload.ptr, + expected_will_view->payload.len, + will_view->payload.ptr, + will_view->payload.len); + ASSERT_INT_EQUALS(expected_will_view->qos, will_view->qos); + ASSERT_INT_EQUALS(expected_will_view->retain, will_view->retain); + ASSERT_BIN_ARRAYS_EQUALS( + expected_will_view->topic.ptr, expected_will_view->topic.len, will_view->topic.ptr, will_view->topic.len); + ASSERT_INT_EQUALS(*expected_will_view->payload_format, *will_view->payload_format); + ASSERT_INT_EQUALS( + *expected_will_view->message_expiry_interval_seconds, *will_view->message_expiry_interval_seconds); + ASSERT_BIN_ARRAYS_EQUALS( + expected_will_view->response_topic->ptr, + expected_will_view->response_topic->len, + will_view->response_topic->ptr, + will_view->response_topic->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_will_view->correlation_data->ptr, + expected_will_view->correlation_data->len, + will_view->correlation_data->ptr, + will_view->correlation_data->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_will_view->content_type->ptr, + expected_will_view->content_type->len, + will_view->content_type->ptr, + will_view->content_type->len); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_will_view->user_property_count, + expected_will_view->user_properties, + will_view->user_property_count, + will_view->user_properties)); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + connect_view->user_property_count, + connect_view->user_properties)); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_method->ptr, + expected_view->authentication_method->len, + connect_view->authentication_method->ptr, + connect_view->authentication_method->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_data->ptr, + expected_view->authentication_data->len, + connect_view->authentication_data->ptr, + connect_view->authentication_data->len); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_encode_connect_to_buffer( + struct aws_allocator *allocator, + struct aws_mqtt5_packet_connect_view *connect_view, + struct aws_byte_buf *buffer) { + AWS_ZERO_STRUCT(*buffer); + aws_byte_buf_init(buffer, allocator, 4096); + + struct aws_mqtt5_encoder_options encoder_options; + AWS_ZERO_STRUCT(encoder_options); + + struct aws_mqtt5_encoder encoder; + ASSERT_SUCCESS(aws_mqtt5_encoder_init(&encoder, allocator, &encoder_options)); + ASSERT_SUCCESS(aws_mqtt5_encoder_append_packet_encoding(&encoder, AWS_MQTT5_PT_CONNECT, connect_view)); + + enum aws_mqtt5_encoding_result result = aws_mqtt5_encoder_encode_to_buffer(&encoder, buffer); + ASSERT_INT_EQUALS(AWS_MQTT5_ER_FINISHED, result); + + aws_mqtt5_encoder_clean_up(&encoder); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_connect_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor will_payload_cursor = aws_byte_cursor_from_c_str(s_will_payload); + enum aws_mqtt5_payload_format_indicator payload_format = AWS_MQTT5_PFI_UTF8; + uint32_t message_expiry_interval_seconds = 65537; + struct aws_byte_cursor will_response_topic = aws_byte_cursor_from_c_str(s_will_response_topic); + struct aws_byte_cursor will_correlation_data = aws_byte_cursor_from_c_str(s_will_correlation_data); + struct aws_byte_cursor will_content_type = aws_byte_cursor_from_c_str(s_will_content_type); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(s_username); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(s_password); + uint32_t session_expiry_interval_seconds = 3600; + uint8_t request_response_information = 1; + uint8_t request_problem_information = 1; + uint16_t receive_maximum = 50; + uint16_t topic_alias_maximum = 16; + uint32_t maximum_packet_size_bytes = 1ULL << 24; + uint32_t will_delay_interval_seconds = 30; + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("AuthMethod"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("SuperSecret"); + + struct aws_mqtt5_packet_publish_view will_view = { + .payload = will_payload_cursor, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = aws_byte_cursor_from_c_str(s_will_topic), + .payload_format = &payload_format, + .message_expiry_interval_seconds = &message_expiry_interval_seconds, + .response_topic = &will_response_topic, + .correlation_data = &will_correlation_data, + .content_type = &will_content_type, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 1200, + .client_id = aws_byte_cursor_from_c_str(s_client_id), + .username = &username, + .password = &password, + .clean_start = true, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .request_response_information = &request_response_information, + .request_problem_information = &request_problem_information, + .receive_maximum = &receive_maximum, + .topic_alias_maximum = &topic_alias_maximum, + .maximum_packet_size_bytes = &maximum_packet_size_bytes, + .will_delay_interval_seconds = &will_delay_interval_seconds, + .will = &will_view, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_view = &connect_view, + .decoder_callback = s_aws_mqtt5_on_connect_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + struct aws_byte_buf encoding_buffer; + ASSERT_SUCCESS(s_mqtt5_packet_encode_connect_to_buffer(allocator, &connect_view, &encoding_buffer)); + + /* + * For this packet: 1 byte packet type + flags, 2 bytes vli remaining length + 7 bytes protocol prefix + * (0x00, 0x04, "MQTT", 0x05), then we're at the CONNECT flags which we want to check + */ + size_t connect_flags_byte_index = 10; + uint8_t connect_flags = encoding_buffer.buffer[connect_flags_byte_index]; + + /* + * Verify Will flag (0x04), Will QoS (0x08), Will Retain (0x20), Username (0x80), + * clean start (0x02), password (0x40) flags are set + */ + ASSERT_INT_EQUALS(connect_flags, 0xEE); + + aws_byte_buf_clean_up(&encoding_buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_connect_round_trip, s_mqtt5_packet_connect_round_trip_fn) + +static int s_aws_mqtt5_on_connack_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_CONNACK, (uint32_t)type); + + struct aws_mqtt5_packet_connack_view *connack_view = packet_view; + struct aws_mqtt5_packet_connack_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->session_present, connack_view->session_present); + ASSERT_INT_EQUALS(expected_view->reason_code, connack_view->reason_code); + ASSERT_INT_EQUALS(*expected_view->session_expiry_interval, *connack_view->session_expiry_interval); + ASSERT_INT_EQUALS(*expected_view->receive_maximum, *connack_view->receive_maximum); + ASSERT_INT_EQUALS(*expected_view->maximum_qos, *connack_view->maximum_qos); + ASSERT_INT_EQUALS(*expected_view->retain_available, *connack_view->retain_available); + ASSERT_INT_EQUALS(*expected_view->maximum_packet_size, *connack_view->maximum_packet_size); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->assigned_client_identifier->ptr, + expected_view->assigned_client_identifier->len, + connack_view->assigned_client_identifier->ptr, + connack_view->assigned_client_identifier->len); + ASSERT_INT_EQUALS(*expected_view->topic_alias_maximum, *connack_view->topic_alias_maximum); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->reason_string->ptr, + expected_view->reason_string->len, + connack_view->reason_string->ptr, + connack_view->reason_string->len); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + connack_view->user_property_count, + connack_view->user_properties)); + + ASSERT_INT_EQUALS( + *expected_view->wildcard_subscriptions_available, *connack_view->wildcard_subscriptions_available); + ASSERT_INT_EQUALS( + *expected_view->subscription_identifiers_available, *connack_view->subscription_identifiers_available); + ASSERT_INT_EQUALS(*expected_view->shared_subscriptions_available, *connack_view->shared_subscriptions_available); + ASSERT_INT_EQUALS(*expected_view->server_keep_alive, *connack_view->server_keep_alive); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->response_information->ptr, + expected_view->response_information->len, + connack_view->response_information->ptr, + connack_view->response_information->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->server_reference->ptr, + expected_view->server_reference->len, + connack_view->server_reference->ptr, + connack_view->server_reference->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_method->ptr, + expected_view->authentication_method->len, + connack_view->authentication_method->ptr, + connack_view->authentication_method->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_data->ptr, + expected_view->authentication_data->len, + connack_view->authentication_data->ptr, + connack_view->authentication_data->len); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_connack_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + uint32_t session_expiry_interval = 3600; + uint16_t receive_maximum = 20; + enum aws_mqtt5_qos maximum_qos = AWS_MQTT5_QOS_AT_LEAST_ONCE; + bool retain_available = true; + uint32_t maximum_packet_size = 1U << 18; + struct aws_byte_cursor assigned_client_identifier = aws_byte_cursor_from_c_str("server-assigned-client-id-01"); + uint16_t topic_alias_maximum = 10; + struct aws_byte_cursor reason_string = aws_byte_cursor_from_c_str("You've been banned from this server"); + bool wildcard_subscriptions_available = true; + bool subscription_identifiers_available = true; + bool shared_subscriptions_available = true; + uint16_t server_keep_alive = 1200; + struct aws_byte_cursor response_information = aws_byte_cursor_from_c_str("some/topic-prefix"); + struct aws_byte_cursor server_reference = aws_byte_cursor_from_c_str("www.somewhere-else.com:1883"); + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("GSSAPI"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("TOP-SECRET"); + + struct aws_mqtt5_packet_connack_view connack_view = { + .session_present = true, + .reason_code = AWS_MQTT5_CRC_BANNED, + .session_expiry_interval = &session_expiry_interval, + .receive_maximum = &receive_maximum, + .maximum_qos = &maximum_qos, + .retain_available = &retain_available, + .maximum_packet_size = &maximum_packet_size, + .assigned_client_identifier = &assigned_client_identifier, + .topic_alias_maximum = &topic_alias_maximum, + .reason_string = &reason_string, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .wildcard_subscriptions_available = &wildcard_subscriptions_available, + .subscription_identifiers_available = &subscription_identifiers_available, + .shared_subscriptions_available = &shared_subscriptions_available, + .server_keep_alive = &server_keep_alive, + .response_information = &response_information, + .server_reference = &server_reference, + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_CONNACK, + .packet_view = &connack_view, + .decoder_callback = s_aws_mqtt5_on_connack_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_connack_round_trip, s_mqtt5_packet_connack_round_trip_fn) + +static char s_subscription_topic_1[] = "test_topic_1"; +static char s_subscription_topic_2[] = "test_topic_2"; +static char s_subscription_topic_3[] = "test_topic_3"; + +static const struct aws_mqtt5_subscription_view s_subscriptions[] = { + {.topic_filter = + { + .ptr = (uint8_t *)s_subscription_topic_1, + .len = AWS_ARRAY_SIZE(s_subscription_topic_1) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = true, + .retain_as_published = true, + .retain_handling_type = AWS_MQTT5_RHT_DONT_SEND}, + {.topic_filter = + { + .ptr = (uint8_t *)s_subscription_topic_2, + .len = AWS_ARRAY_SIZE(s_subscription_topic_2) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = true, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE}, + {.topic_filter = + { + .ptr = (uint8_t *)s_subscription_topic_3, + .len = AWS_ARRAY_SIZE(s_subscription_topic_3) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = false, + .retain_as_published = true, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE_IF_NEW}, +}; + +int aws_mqtt5_test_verify_subscriptions_raw( + size_t subscription_count, + const struct aws_mqtt5_subscription_view *subscriptions, + size_t expected_count, + const struct aws_mqtt5_subscription_view *expected_subscriptions) { + + ASSERT_UINT_EQUALS(expected_count, subscription_count); + + for (size_t i = 0; i < expected_count; ++i) { + const struct aws_mqtt5_subscription_view *expected_subscription = &expected_subscriptions[i]; + ASSERT_BIN_ARRAYS_EQUALS( + expected_subscription->topic_filter.ptr, + expected_subscription->topic_filter.len, + subscriptions[i].topic_filter.ptr, + subscriptions[i].topic_filter.len); + ASSERT_INT_EQUALS(expected_subscription->qos, subscriptions[i].qos); + ASSERT_INT_EQUALS(expected_subscription->no_local, subscriptions[i].no_local); + ASSERT_INT_EQUALS(expected_subscription->retain_as_published, subscriptions[i].retain_as_published); + ASSERT_INT_EQUALS(expected_subscription->retain_handling_type, subscriptions[i].retain_handling_type); + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_on_subscribe_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + (void)type; + + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet_view; + struct aws_mqtt5_packet_subscribe_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->packet_id, subscribe_view->packet_id); + ASSERT_SUCCESS(aws_mqtt5_test_verify_subscriptions_raw( + expected_view->subscription_count, + expected_view->subscriptions, + subscribe_view->subscription_count, + subscribe_view->subscriptions)); + + ASSERT_INT_EQUALS(*expected_view->subscription_identifier, *subscribe_view->subscription_identifier); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + subscribe_view->user_property_count, + subscribe_view->user_properties)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_subscribe_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + uint32_t subscription_identifier = 1; + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .packet_id = packet_id, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + .subscriptions = &s_subscriptions[0], + .subscription_identifier = &subscription_identifier, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_SUBSCRIBE, + .packet_view = &subscribe_view, + .decoder_callback = s_aws_mqtt5_on_subscribe_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_subscribe_round_trip, s_mqtt5_packet_subscribe_round_trip_fn) + +static int s_aws_mqtt5_on_suback_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_SUBACK, (uint32_t)type); + + struct aws_mqtt5_packet_suback_view *suback_view = packet_view; + struct aws_mqtt5_packet_suback_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->packet_id, suback_view->packet_id); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->reason_string->ptr, + expected_view->reason_string->len, + suback_view->reason_string->ptr, + suback_view->reason_string->len); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + suback_view->user_property_count, + suback_view->user_properties)); + + ASSERT_INT_EQUALS(expected_view->reason_code_count, suback_view->reason_code_count); + for (size_t i = 0; i < suback_view->reason_code_count; ++i) { + ASSERT_INT_EQUALS(expected_view->reason_codes[i], suback_view->reason_codes[i]); + } + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_suback_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + struct aws_byte_cursor reason_string = aws_byte_cursor_from_c_str("Some random reason string"); + enum aws_mqtt5_suback_reason_code reason_code_1 = AWS_MQTT5_SARC_GRANTED_QOS_0; + enum aws_mqtt5_suback_reason_code reason_code_2 = AWS_MQTT5_SARC_GRANTED_QOS_1; + enum aws_mqtt5_suback_reason_code reason_code_3 = AWS_MQTT5_SARC_GRANTED_QOS_2; + const enum aws_mqtt5_suback_reason_code reason_codes[3] = { + reason_code_1, + reason_code_2, + reason_code_3, + }; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = packet_id, + .reason_string = &reason_string, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .reason_code_count = 3, + .reason_codes = &reason_codes[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_SUBACK, + .packet_view = &suback_view, + .decoder_callback = s_aws_mqtt5_on_suback_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_suback_round_trip, s_mqtt5_packet_suback_round_trip_fn) + +static char s_unsubscribe_topic_1[] = "test_topic_1"; +static char s_unsubscribe_topic_2[] = "test_topic_2"; +static char s_unsubscribe_topic_3[] = "test_topic_3"; + +static const struct aws_byte_cursor s_unsubscribe_topics[] = { + { + .ptr = (uint8_t *)s_unsubscribe_topic_1, + .len = AWS_ARRAY_SIZE(s_unsubscribe_topic_1) - 1, + }, + { + .ptr = (uint8_t *)s_unsubscribe_topic_2, + .len = AWS_ARRAY_SIZE(s_unsubscribe_topic_2) - 1, + }, + { + .ptr = (uint8_t *)s_unsubscribe_topic_3, + .len = AWS_ARRAY_SIZE(s_unsubscribe_topic_3) - 1, + }, +}; + +static int s_aws_mqtt5_on_unsubscribe_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + (void)type; + + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = packet_view; + struct aws_mqtt5_packet_unsubscribe_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->packet_id, unsubscribe_view->packet_id); + + ASSERT_INT_EQUALS(expected_view->topic_filter_count, unsubscribe_view->topic_filter_count); + + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + const struct aws_byte_cursor unsubscribe_topic = unsubscribe_view->topic_filters[i]; + const struct aws_byte_cursor expected_topic = expected_view->topic_filters[i]; + ASSERT_BIN_ARRAYS_EQUALS(expected_topic.ptr, expected_topic.len, unsubscribe_topic.ptr, unsubscribe_topic.len); + } + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + unsubscribe_view->user_property_count, + unsubscribe_view->user_properties)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_unsubscribe_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .packet_id = packet_id, + .topic_filter_count = AWS_ARRAY_SIZE(s_unsubscribe_topics), + .topic_filters = &s_unsubscribe_topics[0], + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_UNSUBSCRIBE, + .packet_view = &unsubscribe_view, + .decoder_callback = s_aws_mqtt5_on_unsubscribe_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_unsubscribe_round_trip, s_mqtt5_packet_unsubscribe_round_trip_fn) + +static int s_aws_mqtt5_on_unsuback_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_UNSUBACK, (uint32_t)type); + + struct aws_mqtt5_packet_unsuback_view *unsuback_view = packet_view; + struct aws_mqtt5_packet_unsuback_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->packet_id, unsuback_view->packet_id); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->reason_string->ptr, + expected_view->reason_string->len, + unsuback_view->reason_string->ptr, + unsuback_view->reason_string->len); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + unsuback_view->user_property_count, + unsuback_view->user_properties)); + + ASSERT_INT_EQUALS(expected_view->reason_code_count, unsuback_view->reason_code_count); + for (size_t i = 0; i < unsuback_view->reason_code_count; ++i) { + ASSERT_INT_EQUALS(expected_view->reason_codes[i], unsuback_view->reason_codes[i]); + } + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_unsuback_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + struct aws_byte_cursor reason_string = aws_byte_cursor_from_c_str("Some random reason string"); + enum aws_mqtt5_unsuback_reason_code reason_code_1 = AWS_MQTT5_UARC_SUCCESS; + enum aws_mqtt5_unsuback_reason_code reason_code_2 = AWS_MQTT5_UARC_NO_SUBSCRIPTION_EXISTED; + enum aws_mqtt5_unsuback_reason_code reason_code_3 = AWS_MQTT5_UARC_UNSPECIFIED_ERROR; + const enum aws_mqtt5_unsuback_reason_code reason_codes[3] = { + reason_code_1, + reason_code_2, + reason_code_3, + }; + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = packet_id, + .reason_string = &reason_string, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .reason_code_count = AWS_ARRAY_SIZE(reason_codes), + .reason_codes = &reason_codes[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_UNSUBACK, + .packet_view = &unsuback_view, + .decoder_callback = s_aws_mqtt5_on_unsuback_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_unsuback_round_trip, s_mqtt5_packet_unsuback_round_trip_fn) + +static char s_publish_topic[] = "test_topic"; +static char s_publish_payload[] = "test payload contents"; +static char s_publish_response_topic[] = "test response topic"; +static char s_publish_correlation_data[] = "test correlation data"; +static char s_publish_content_type[] = "test content type"; + +static int s_aws_mqtt5_on_publish_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + (void)type; + + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + struct aws_mqtt5_packet_publish_view *publish_view = packet_view; + struct aws_mqtt5_packet_publish_view *expected_view = tester->expected_views; + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->payload.ptr, expected_view->payload.len, publish_view->payload.ptr, publish_view->payload.len); + ASSERT_INT_EQUALS(expected_view->packet_id, publish_view->packet_id); + ASSERT_INT_EQUALS(expected_view->qos, publish_view->qos); + ASSERT_INT_EQUALS(expected_view->duplicate, publish_view->duplicate); + ASSERT_INT_EQUALS(expected_view->retain, publish_view->retain); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->topic.ptr, expected_view->topic.len, publish_view->topic.ptr, publish_view->topic.len); + ASSERT_INT_EQUALS(*expected_view->payload_format, *publish_view->payload_format); + ASSERT_INT_EQUALS(*expected_view->message_expiry_interval_seconds, *publish_view->message_expiry_interval_seconds); + ASSERT_INT_EQUALS(*expected_view->topic_alias, *publish_view->topic_alias); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->response_topic->ptr, + expected_view->response_topic->len, + publish_view->response_topic->ptr, + publish_view->response_topic->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->correlation_data->ptr, + expected_view->correlation_data->len, + publish_view->correlation_data->ptr, + publish_view->correlation_data->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->content_type->ptr, + expected_view->content_type->len, + publish_view->content_type->ptr, + publish_view->content_type->len); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + publish_view->user_property_count, + publish_view->user_properties)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_publish_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + enum aws_mqtt5_payload_format_indicator payload_format = AWS_MQTT5_PFI_UTF8; + uint32_t message_expiry_interval_seconds = 100; + uint16_t topic_alias = 1; + struct aws_byte_cursor response_topic = { + .ptr = (uint8_t *)s_publish_response_topic, + .len = AWS_ARRAY_SIZE(s_publish_response_topic) - 1, + }; + struct aws_byte_cursor correlation_data = { + .ptr = (uint8_t *)s_publish_correlation_data, + .len = AWS_ARRAY_SIZE(s_publish_correlation_data) - 1, + }; + struct aws_byte_cursor content_type = { + .ptr = (uint8_t *)s_publish_content_type, + .len = AWS_ARRAY_SIZE(s_publish_content_type) - 1, + }; + + uint32_t subscription_identifiers[2] = {2, 3}; + + struct aws_mqtt5_packet_publish_view publish_view = { + .payload = + { + .ptr = (uint8_t *)s_publish_payload, + .len = AWS_ARRAY_SIZE(s_publish_payload) - 1, + }, + .packet_id = packet_id, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .duplicate = false, + .retain = false, + .topic = + { + .ptr = (uint8_t *)s_publish_topic, + .len = AWS_ARRAY_SIZE(s_publish_topic) - 1, + }, + .payload_format = &payload_format, + .message_expiry_interval_seconds = &message_expiry_interval_seconds, + .topic_alias = &topic_alias, + .response_topic = &response_topic, + .correlation_data = &correlation_data, + .subscription_identifier_count = AWS_ARRAY_SIZE(subscription_identifiers), + .subscription_identifiers = subscription_identifiers, + .content_type = &content_type, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_PUBLISH, + .packet_view = &publish_view, + .decoder_callback = s_aws_mqtt5_on_publish_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_publish_round_trip, s_mqtt5_packet_publish_round_trip_fn) + +static char s_puback_reason_string[] = "test reason string"; + +static int s_aws_mqtt5_on_puback_received_fn(enum aws_mqtt5_packet_type type, void *packet_view, void *user_data) { + (void)type; + + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + struct aws_mqtt5_packet_puback_view *puback_view = packet_view; + struct aws_mqtt5_packet_puback_view *expected_view = tester->expected_views; + ASSERT_INT_EQUALS(expected_view->packet_id, puback_view->packet_id); + ASSERT_INT_EQUALS(expected_view->reason_code, puback_view->reason_code); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->reason_string->ptr, + expected_view->reason_string->len, + puback_view->reason_string->ptr, + puback_view->reason_string->len); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + puback_view->user_property_count, + puback_view->user_properties)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_puback_round_trip_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + enum aws_mqtt5_puback_reason_code reason_code = AWS_MQTT5_PARC_NO_MATCHING_SUBSCRIBERS; + struct aws_byte_cursor reason_string = { + .ptr = (uint8_t *)s_puback_reason_string, + .len = AWS_ARRAY_SIZE(s_puback_reason_string) - 1, + }; + + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = packet_id, + .reason_code = reason_code, + .reason_string = &reason_string, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_PUBACK, + .packet_view = &puback_view, + .decoder_callback = s_aws_mqtt5_on_puback_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_puback_round_trip, s_mqtt5_packet_puback_round_trip_fn) + +static int s_aws_mqtt5_on_no_will_connect_received_fn( + enum aws_mqtt5_packet_type type, + void *packet_view, + void *user_data) { + struct aws_mqtt5_encode_decode_tester *tester = user_data; + + ++tester->view_count; + + ASSERT_INT_EQUALS((uint32_t)AWS_MQTT5_PT_CONNECT, (uint32_t)type); + + struct aws_mqtt5_packet_connect_view *connect_view = packet_view; + struct aws_mqtt5_packet_connect_view *expected_view = tester->expected_views; + + ASSERT_INT_EQUALS(expected_view->keep_alive_interval_seconds, connect_view->keep_alive_interval_seconds); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->client_id.ptr, + expected_view->client_id.len, + connect_view->client_id.ptr, + connect_view->client_id.len); + + if (expected_view->username != NULL) { + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->username->ptr, + expected_view->username->len, + connect_view->username->ptr, + connect_view->username->len); + } else { + ASSERT_NULL(connect_view->username); + } + + if (expected_view->password != NULL) { + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->password->ptr, + expected_view->password->len, + connect_view->password->ptr, + connect_view->password->len); + } else { + ASSERT_NULL(connect_view->password); + } + + ASSERT_TRUE(expected_view->clean_start == connect_view->clean_start); + ASSERT_INT_EQUALS(*expected_view->session_expiry_interval_seconds, *connect_view->session_expiry_interval_seconds); + + ASSERT_INT_EQUALS(*expected_view->request_response_information, *connect_view->request_response_information); + ASSERT_INT_EQUALS(*expected_view->request_problem_information, *connect_view->request_problem_information); + ASSERT_INT_EQUALS(*expected_view->receive_maximum, *connect_view->receive_maximum); + ASSERT_INT_EQUALS(*expected_view->topic_alias_maximum, *connect_view->topic_alias_maximum); + ASSERT_INT_EQUALS(*expected_view->maximum_packet_size_bytes, *connect_view->maximum_packet_size_bytes); + + ASSERT_NULL(connect_view->will_delay_interval_seconds); + + ASSERT_NULL(connect_view->will); + + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + expected_view->user_property_count, + expected_view->user_properties, + connect_view->user_property_count, + connect_view->user_properties)); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_method->ptr, + expected_view->authentication_method->len, + connect_view->authentication_method->ptr, + connect_view->authentication_method->len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->authentication_data->ptr, + expected_view->authentication_data->len, + connect_view->authentication_data->ptr, + connect_view->authentication_data->len); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_packet_encode_connect_no_will_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(s_username); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(s_password); + uint32_t session_expiry_interval_seconds = 3600; + uint8_t request_response_information = 1; + uint8_t request_problem_information = 1; + uint16_t receive_maximum = 50; + uint16_t topic_alias_maximum = 16; + uint32_t maximum_packet_size_bytes = 1ULL << 24; + uint32_t will_delay_interval_seconds = 30; + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("AuthMethod"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("SuperSecret"); + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 1200, + .client_id = aws_byte_cursor_from_c_str(s_client_id), + .username = &username, + .password = &password, + .clean_start = true, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .request_response_information = &request_response_information, + .request_problem_information = &request_problem_information, + .receive_maximum = &receive_maximum, + .topic_alias_maximum = &topic_alias_maximum, + .maximum_packet_size_bytes = &maximum_packet_size_bytes, + .will_delay_interval_seconds = &will_delay_interval_seconds, + .will = NULL, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_view = &connect_view, + .decoder_callback = s_aws_mqtt5_on_no_will_connect_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + struct aws_byte_buf encoding_buffer; + ASSERT_SUCCESS(s_mqtt5_packet_encode_connect_to_buffer(allocator, &connect_view, &encoding_buffer)); + + /* + * For this packet: 1 byte packet type + flags, 2 bytes vli remaining length + 7 bytes protocol prefix + * (0x00, 0x04, "MQTT", 0x05), then we're at the CONNECT flags which we want to check + */ + size_t connect_flags_byte_index = 10; + uint8_t connect_flags = encoding_buffer.buffer[connect_flags_byte_index]; + + /* + * Verify Will flag, Will QoS, Will Retain are all zero, + * while clean start (0x02), password (0x40) and username (0x80) flags are set + */ + ASSERT_INT_EQUALS(connect_flags, 0xC2); + + aws_byte_buf_clean_up(&encoding_buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_encode_connect_no_will, s_mqtt5_packet_encode_connect_no_will_fn) + +static int s_mqtt5_packet_encode_connect_no_username_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(s_password); + uint32_t session_expiry_interval_seconds = 3600; + uint8_t request_response_information = 1; + uint8_t request_problem_information = 1; + uint16_t receive_maximum = 50; + uint16_t topic_alias_maximum = 16; + uint32_t maximum_packet_size_bytes = 1ULL << 24; + uint32_t will_delay_interval_seconds = 30; + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("AuthMethod"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("SuperSecret"); + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 1200, + .client_id = aws_byte_cursor_from_c_str(s_client_id), + .username = NULL, + .password = &password, + .clean_start = true, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .request_response_information = &request_response_information, + .request_problem_information = &request_problem_information, + .receive_maximum = &receive_maximum, + .topic_alias_maximum = &topic_alias_maximum, + .maximum_packet_size_bytes = &maximum_packet_size_bytes, + .will_delay_interval_seconds = &will_delay_interval_seconds, + .will = NULL, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_view = &connect_view, + .decoder_callback = s_aws_mqtt5_on_no_will_connect_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + struct aws_byte_buf encoding_buffer; + ASSERT_SUCCESS(s_mqtt5_packet_encode_connect_to_buffer(allocator, &connect_view, &encoding_buffer)); + + /* + * For this packet: 1 byte packet type + flags, 2 bytes vli remaining length + 7 bytes protocol prefix + * (0x00, 0x04, "MQTT", 0x05), then we're at the CONNECT flags which we want to check + */ + size_t connect_flags_byte_index = 10; + uint8_t connect_flags = encoding_buffer.buffer[connect_flags_byte_index]; + + /* + * Verify Will flag, Will QoS, Will Retain, Username are all zero, + * while clean start (0x02), password (0x40) flags are set + */ + ASSERT_INT_EQUALS(connect_flags, 0x42); + + aws_byte_buf_clean_up(&encoding_buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_encode_connect_no_username, s_mqtt5_packet_encode_connect_no_username_fn) + +static int s_mqtt5_packet_encode_connect_no_password_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(s_username); + uint32_t session_expiry_interval_seconds = 3600; + uint8_t request_response_information = 1; + uint8_t request_problem_information = 1; + uint16_t receive_maximum = 50; + uint16_t topic_alias_maximum = 16; + uint32_t maximum_packet_size_bytes = 1ULL << 24; + uint32_t will_delay_interval_seconds = 30; + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("AuthMethod"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("SuperSecret"); + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 1200, + .client_id = aws_byte_cursor_from_c_str(s_client_id), + .username = &username, + .password = NULL, + .clean_start = true, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .request_response_information = &request_response_information, + .request_problem_information = &request_problem_information, + .receive_maximum = &receive_maximum, + .topic_alias_maximum = &topic_alias_maximum, + .maximum_packet_size_bytes = &maximum_packet_size_bytes, + .will_delay_interval_seconds = &will_delay_interval_seconds, + .will = NULL, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_mqtt5_packet_round_trip_test_context context = { + .allocator = allocator, + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_view = &connect_view, + .decoder_callback = s_aws_mqtt5_on_no_will_connect_received_fn, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_encode_decode_round_trip_matrix_test(&context)); + + struct aws_byte_buf encoding_buffer; + ASSERT_SUCCESS(s_mqtt5_packet_encode_connect_to_buffer(allocator, &connect_view, &encoding_buffer)); + + /* + * For this packet: 1 byte packet type + flags, 1 byte vli remaining length + 7 bytes protocol prefix + * (0x00, 0x04, "MQTT", 0x05), then we're at the CONNECT flags which we want to check + */ + size_t connect_flags_byte_index = 9; + uint8_t connect_flags = encoding_buffer.buffer[connect_flags_byte_index]; + + /* + * Verify Will flag, Will QoS, Will Retain, Password are all zero, + * while clean start (0x02), username (0x80) flags are set + */ + ASSERT_INT_EQUALS(connect_flags, 0x82); + + aws_byte_buf_clean_up(&encoding_buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_encode_connect_no_password, s_mqtt5_packet_encode_connect_no_password_fn) + +static int s_mqtt5_packet_encode_connect_will_property_order_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor will_payload_cursor = aws_byte_cursor_from_c_str(s_will_payload); + enum aws_mqtt5_payload_format_indicator payload_format = AWS_MQTT5_PFI_UTF8; + uint32_t message_expiry_interval_seconds = 65537; + struct aws_byte_cursor will_response_topic = aws_byte_cursor_from_c_str(s_will_response_topic); + struct aws_byte_cursor will_correlation_data = aws_byte_cursor_from_c_str(s_will_correlation_data); + struct aws_byte_cursor will_content_type = aws_byte_cursor_from_c_str(s_will_content_type); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(s_username); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(s_password); + uint32_t session_expiry_interval_seconds = 3600; + uint8_t request_response_information = 1; + uint8_t request_problem_information = 1; + uint16_t receive_maximum = 50; + uint16_t topic_alias_maximum = 16; + uint32_t maximum_packet_size_bytes = 1ULL << 24; + uint32_t will_delay_interval_seconds = 30; + struct aws_byte_cursor authentication_method = aws_byte_cursor_from_c_str("AuthMethod"); + struct aws_byte_cursor authentication_data = aws_byte_cursor_from_c_str("SuperSecret"); + + struct aws_mqtt5_packet_publish_view will_view = { + .payload = will_payload_cursor, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = aws_byte_cursor_from_c_str(s_will_topic), + .payload_format = &payload_format, + .message_expiry_interval_seconds = &message_expiry_interval_seconds, + .response_topic = &will_response_topic, + .correlation_data = &will_correlation_data, + .content_type = &will_content_type, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 1200, + .client_id = aws_byte_cursor_from_c_str(s_client_id), + .username = &username, + .password = &password, + .clean_start = true, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .request_response_information = &request_response_information, + .request_problem_information = &request_problem_information, + .receive_maximum = &receive_maximum, + .topic_alias_maximum = &topic_alias_maximum, + .maximum_packet_size_bytes = &maximum_packet_size_bytes, + .will_delay_interval_seconds = &will_delay_interval_seconds, + .will = &will_view, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .authentication_method = &authentication_method, + .authentication_data = &authentication_data, + }; + + struct aws_byte_buf encoding_buffer; + ASSERT_SUCCESS(s_mqtt5_packet_encode_connect_to_buffer(allocator, &connect_view, &encoding_buffer)); + + struct aws_byte_cursor encoding_cursor = aws_byte_cursor_from_buf(&encoding_buffer); + + struct aws_byte_cursor client_id_cursor; + AWS_ZERO_STRUCT(client_id_cursor); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&encoding_cursor, &connect_view.client_id, &client_id_cursor)); + + struct aws_byte_cursor will_topic_cursor; + AWS_ZERO_STRUCT(will_topic_cursor); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&encoding_cursor, &will_view.topic, &will_topic_cursor)); + + struct aws_byte_cursor will_message_payload_cursor; + AWS_ZERO_STRUCT(will_message_payload_cursor); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&encoding_cursor, &will_view.payload, &will_message_payload_cursor)); + + struct aws_byte_cursor username_cursor; + AWS_ZERO_STRUCT(username_cursor); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&encoding_cursor, connect_view.username, &username_cursor)); + + struct aws_byte_cursor password_cursor; + AWS_ZERO_STRUCT(password_cursor); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&encoding_cursor, connect_view.password, &password_cursor)); + + ASSERT_NOT_NULL(client_id_cursor.ptr); + ASSERT_NOT_NULL(will_topic_cursor.ptr); + ASSERT_NOT_NULL(will_message_payload_cursor.ptr); + ASSERT_NOT_NULL(username_cursor.ptr); + ASSERT_NOT_NULL(password_cursor.ptr); + + ASSERT_TRUE(client_id_cursor.ptr < will_topic_cursor.ptr); + ASSERT_TRUE(will_topic_cursor.ptr < will_message_payload_cursor.ptr); + ASSERT_TRUE(will_message_payload_cursor.ptr < username_cursor.ptr); + ASSERT_TRUE(username_cursor.ptr < password_cursor.ptr); + + aws_byte_buf_clean_up(&encoding_buffer); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_packet_encode_connect_will_property_order, s_mqtt5_packet_encode_connect_will_property_order_fn) + +static int s_aws_mqtt5_decoder_decode_subscribe_first_byte_check(struct aws_mqtt5_decoder *decoder) { + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & 0x0F) != 2) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} +static int s_aws_mqtt5_decoder_decode_unsubscribe_first_byte_check(struct aws_mqtt5_decoder *decoder) { + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & 0x0F) != 2) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} +static int s_aws_mqtt5_decoder_decode_disconnect_first_byte_check(struct aws_mqtt5_decoder *decoder) { + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & 0x0F) != 0) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_first_byte_check( + struct aws_allocator *allocator, + enum aws_mqtt5_packet_type packet_type, + void *packet_view) { + struct aws_byte_buf whole_dest; + aws_byte_buf_init(&whole_dest, allocator, 4096); + + struct aws_mqtt5_encode_decode_tester tester; + aws_mqtt5_encode_init_testing_function_table(&tester.encoder_function_table); + aws_mqtt5_decode_init_testing_function_table(&tester.decoder_function_table); + + tester.decoder_function_table.decoders_by_packet_type[AWS_MQTT5_PT_SUBSCRIBE] = + &s_aws_mqtt5_decoder_decode_subscribe_first_byte_check; + tester.decoder_function_table.decoders_by_packet_type[AWS_MQTT5_PT_UNSUBSCRIBE] = + &s_aws_mqtt5_decoder_decode_unsubscribe_first_byte_check; + tester.decoder_function_table.decoders_by_packet_type[AWS_MQTT5_PT_DISCONNECT] = + &s_aws_mqtt5_decoder_decode_disconnect_first_byte_check; + + struct aws_mqtt5_encoder_options encoder_options = { + .encoders = &tester.encoder_function_table, + }; + + struct aws_mqtt5_encoder encoder; + + ASSERT_SUCCESS(aws_mqtt5_encoder_init(&encoder, allocator, &encoder_options)); + ASSERT_SUCCESS(aws_mqtt5_encoder_append_packet_encoding(&encoder, packet_type, packet_view)); + + enum aws_mqtt5_encoding_result result = AWS_MQTT5_ER_ERROR; + result = aws_mqtt5_encoder_encode_to_buffer(&encoder, &whole_dest); + ASSERT_INT_EQUALS(AWS_MQTT5_ER_FINISHED, result); + + struct aws_mqtt5_decoder_options decoder_options = { + .callback_user_data = &tester, + .decoder_table = &tester.decoder_function_table, + }; + + struct aws_mqtt5_decoder decoder; + ASSERT_SUCCESS(aws_mqtt5_decoder_init(&decoder, allocator, &decoder_options)); + + struct aws_byte_cursor whole_cursor = aws_byte_cursor_from_buf(&whole_dest); + ASSERT_SUCCESS(aws_mqtt5_decoder_on_data_received(&decoder, whole_cursor)); + + aws_byte_buf_clean_up(&whole_dest); + aws_mqtt5_encoder_clean_up(&encoder); + aws_mqtt5_decoder_clean_up(&decoder); + + return AWS_OP_SUCCESS; +} + +static int mqtt5_first_byte_reserved_header_check_subscribe_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + uint32_t subscription_identifier = 1; + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .packet_id = packet_id, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + .subscriptions = &s_subscriptions[0], + .subscription_identifier = &subscription_identifier, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + ASSERT_SUCCESS(s_aws_mqtt5_first_byte_check(allocator, AWS_MQTT5_PT_SUBSCRIBE, &subscribe_view)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_first_byte_reserved_header_check_subscribe, mqtt5_first_byte_reserved_header_check_subscribe_fn) + +static int mqtt5_first_byte_reserved_header_check_unsubscribe_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt5_packet_id_t packet_id = 47; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .packet_id = packet_id, + .topic_filter_count = AWS_ARRAY_SIZE(s_unsubscribe_topics), + .topic_filters = &s_unsubscribe_topics[0], + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + }; + + ASSERT_SUCCESS(s_aws_mqtt5_first_byte_check(allocator, AWS_MQTT5_PT_UNSUBSCRIBE, &unsubscribe_view)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_first_byte_reserved_header_check_unsubscribe, mqtt5_first_byte_reserved_header_check_unsubscribe_fn) + +static int mqtt5_first_byte_reserved_header_check_disconnect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + uint32_t session_expiry_interval_seconds = 333; + struct aws_byte_cursor reason_string_cursor = aws_byte_cursor_from_c_str(s_reason_string); + struct aws_byte_cursor server_reference_cursor = aws_byte_cursor_from_c_str(s_server_reference); + + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_code = AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE, + .session_expiry_interval_seconds = &session_expiry_interval_seconds, + .reason_string = &reason_string_cursor, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = &s_user_properties[0], + .server_reference = &server_reference_cursor, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_first_byte_check(allocator, AWS_MQTT5_PT_DISCONNECT, &disconnect_view)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_first_byte_reserved_header_check_disconnect, mqtt5_first_byte_reserved_header_check_disconnect_fn) diff --git a/tests/v5/mqtt5_operation_and_storage_tests.c b/tests/v5/mqtt5_operation_and_storage_tests.c new file mode 100644 index 00000000..fb038779 --- /dev/null +++ b/tests/v5/mqtt5_operation_and_storage_tests.c @@ -0,0 +1,3303 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include "mqtt5_testing_utils.h" +#include +#include +#include +#include +#include +#include +#include + +#include + +static int s_verify_user_properties( + struct aws_mqtt5_user_property_set *property_set, + size_t expected_count, + const struct aws_mqtt5_user_property *expected_properties) { + + return aws_mqtt5_test_verify_user_properties_raw( + aws_mqtt5_user_property_set_size(property_set), + property_set->properties.data, + expected_count, + expected_properties); +} + +AWS_STATIC_STRING_FROM_LITERAL(s_client_id, "MyClientId"); + +static bool s_is_cursor_in_buffer(const struct aws_byte_buf *buffer, struct aws_byte_cursor cursor) { + if (cursor.ptr < buffer->buffer) { + return false; + } + + if (cursor.ptr + cursor.len > buffer->buffer + buffer->len) { + return false; + } + + return true; +} + +/* + * a bunch of macros to simplify the verification of required and optional properties being properly propagated + * and referenced within packet storage and packet views + */ + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(storage_ptr, field_name) \ + ASSERT_NULL((storage_ptr)->storage_view.field_name); + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(storage_ptr, view_ptr, field_name) \ + ASSERT_BIN_ARRAYS_EQUALS( \ + (view_ptr)->field_name->ptr, \ + (view_ptr)->field_name->len, \ + (storage_ptr)->field_name.ptr, \ + (storage_ptr)->field_name.len); \ + ASSERT_BIN_ARRAYS_EQUALS( \ + (view_ptr)->field_name->ptr, \ + (view_ptr)->field_name->len, \ + (storage_ptr)->storage_view.field_name->ptr, \ + (storage_ptr)->storage_view.field_name->len); \ + ASSERT_TRUE(s_is_cursor_in_buffer(&(storage_ptr)->storage, ((storage_ptr)->field_name))); \ + ASSERT_TRUE(s_is_cursor_in_buffer(&(storage_ptr)->storage, *((storage_ptr)->storage_view.field_name))); \ + ASSERT_TRUE((view_ptr)->field_name->ptr != (storage_ptr)->field_name.ptr); + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(storage_ptr, view_ptr, field_name) \ + ASSERT_BIN_ARRAYS_EQUALS( \ + (view_ptr)->field_name.ptr, \ + (view_ptr)->field_name.len, \ + (storage_ptr)->storage_view.field_name.ptr, \ + (storage_ptr)->storage_view.field_name.len); \ + ASSERT_TRUE(s_is_cursor_in_buffer(&(storage_ptr)->storage, (storage_ptr)->storage_view.field_name)); \ + ASSERT_TRUE((view_ptr)->field_name.ptr != (storage_ptr)->storage_view.field_name.ptr); + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_EMPTY_CURSOR(storage_ptr, view_ptr, field_name) \ + ASSERT_UINT_EQUALS(0, (storage_ptr)->storage_view.field_name.len); + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(storage_ptr, view_ptr, field_name) \ + ASSERT_UINT_EQUALS((view_ptr)->field_name, (storage_ptr)->storage_view.field_name); + +#define AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(storage_ptr, view_ptr, field_name) \ + ASSERT_UINT_EQUALS(*(view_ptr)->field_name, (storage_ptr)->field_name); \ + ASSERT_PTR_EQUALS((storage_ptr)->storage_view.field_name, &(storage_ptr)->field_name); + +static const char *PUBLISH_PAYLOAD = "hello-world"; +static const char *PUBLISH_TOPIC = "greetings/friendly"; + +static int s_verify_publish_operation_required_fields( + struct aws_mqtt5_packet_publish_storage *publish_storage, + struct aws_mqtt5_packet_publish_view *original_view) { + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_EMPTY_CURSOR(publish_storage, original_view, payload); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(publish_storage, original_view, qos); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(publish_storage, original_view, retain); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(publish_storage, original_view, topic); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_publish_operation_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_publish_view publish_options = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_publish_view_validate(&publish_options)); + + struct aws_mqtt5_operation_publish *publish_op = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, NULL); + + ASSERT_NOT_NULL(publish_op); + + /* This test will check both the values in storage as well as the embedded view. They should be in sync. */ + struct aws_mqtt5_packet_publish_storage *publish_storage = &publish_op->options_storage; + struct aws_mqtt5_packet_publish_view *stored_view = &publish_storage->storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_publish_view_validate(stored_view)); + + /* required fields */ + ASSERT_SUCCESS(s_verify_publish_operation_required_fields(publish_storage, &publish_options)); + + /* optional fields */ + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, payload_format); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, message_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, topic_alias); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, response_topic); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, correlation_data); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(publish_storage, content_type); + + ASSERT_SUCCESS(s_verify_user_properties(&publish_storage->user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + ASSERT_NULL(publish_op->completion_options.completion_callback); + ASSERT_NULL(publish_op->completion_options.completion_user_data); + + aws_mqtt5_packet_publish_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_operation_release(&publish_op->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_publish_operation_new_set_no_optional, s_mqtt5_publish_operation_new_set_no_optional_fn) + +static const uint32_t s_message_expiry_interval_seconds = 60; +static const uint16_t s_topic_alias = 2; +static const char *s_response_topic = "A-response-topic"; +static const char *s_correlation_data = "CorrelationData"; +static const char *s_content_type = "JSON"; + +static char s_user_prop1_name[] = "Property1"; +static char s_user_prop1_value[] = "Value1"; +static char s_user_prop2_name[] = "Property2"; +static char s_user_prop2_value[] = "Value2"; +static const struct aws_mqtt5_user_property s_user_properties[] = { + { + .name = + { + .ptr = (uint8_t *)s_user_prop1_name, + .len = AWS_ARRAY_SIZE(s_user_prop1_name), + }, + .value = + { + .ptr = (uint8_t *)s_user_prop1_value, + .len = AWS_ARRAY_SIZE(s_user_prop1_value), + }, + }, + { + .name = + { + .ptr = (uint8_t *)s_user_prop2_name, + .len = AWS_ARRAY_SIZE(s_user_prop2_name), + }, + .value = + { + .ptr = (uint8_t *)s_user_prop2_value, + .len = AWS_ARRAY_SIZE(s_user_prop2_value), + }, + }, +}; + +static void s_aws_mqtt5_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + (void)packet_type; + (void)packet; + (void)error_code; + (void)complete_ctx; +} + +static int s_mqtt5_publish_operation_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor response_topic = aws_byte_cursor_from_c_str(s_response_topic); + struct aws_byte_cursor correlation_data = aws_byte_cursor_from_c_str(s_correlation_data); + struct aws_byte_cursor content_type = aws_byte_cursor_from_c_str(s_content_type); + enum aws_mqtt5_payload_format_indicator payload_format = AWS_MQTT5_PFI_UTF8; + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_c_str(PUBLISH_PAYLOAD); + + struct aws_mqtt5_packet_publish_view publish_options = { + .payload = payload_cursor, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .retain = false, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + .payload_format = &payload_format, + .message_expiry_interval_seconds = &s_message_expiry_interval_seconds, + .topic_alias = &s_topic_alias, + .response_topic = &response_topic, + .correlation_data = &correlation_data, + .subscription_identifier_count = 0, + .subscription_identifiers = NULL, + .content_type = &content_type, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_publish_view_validate(&publish_options)); + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = &s_aws_mqtt5_publish_completion_fn, + .completion_user_data = (void *)0xFFFF, + }; + + struct aws_mqtt5_operation_publish *publish_op = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, &completion_options); + + ASSERT_NOT_NULL(publish_op); + + struct aws_mqtt5_packet_publish_storage *publish_storage = &publish_op->options_storage; + struct aws_mqtt5_packet_publish_view *stored_view = &publish_storage->storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_publish_view_validate(stored_view)); + + /* required fields */ + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(publish_storage, &publish_options, payload); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(publish_storage, &publish_options, qos); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(publish_storage, &publish_options, retain); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(publish_storage, &publish_options, topic); + + /* optional fields */ + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(publish_storage, &publish_options, payload_format); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + publish_storage, &publish_options, message_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(publish_storage, &publish_options, topic_alias); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(publish_storage, &publish_options, response_topic); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(publish_storage, &publish_options, correlation_data); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(publish_storage, &publish_options, content_type); + + ASSERT_SUCCESS(s_verify_user_properties( + &publish_storage->user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + ASSERT_PTR_EQUALS(completion_options.completion_callback, publish_op->completion_options.completion_callback); + ASSERT_PTR_EQUALS(completion_options.completion_user_data, publish_op->completion_options.completion_user_data); + + aws_mqtt5_packet_publish_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_operation_release(&publish_op->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_publish_operation_new_set_all, s_mqtt5_publish_operation_new_set_all_fn) + +static int s_mqtt5_publish_operation_new_failure_packet_id_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_publish_view publish_options = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .retain = true, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = &s_aws_mqtt5_publish_completion_fn, + .completion_user_data = (void *)0xFFFF, + }; + + struct aws_mqtt5_operation_publish *publish_op = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, &completion_options); + ASSERT_NOT_NULL(publish_op); + aws_mqtt5_operation_release(&publish_op->base); + + publish_options.packet_id = 1, + publish_op = aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, &completion_options); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION, aws_last_error()); + ASSERT_NULL(publish_op); + + aws_raise_error(AWS_ERROR_SUCCESS); + + publish_options.qos = AWS_MQTT5_QOS_AT_LEAST_ONCE; + publish_op = aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, &completion_options); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION, aws_last_error()); + ASSERT_NULL(publish_op); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_publish_operation_new_failure_packet_id, s_mqtt5_publish_operation_new_failure_packet_id_fn) + +static const char s_topic_filter1[] = "some/topic/+"; +static const char s_topic_filter2[] = "another/topic/*"; + +static struct aws_mqtt5_subscription_view s_subscriptions[] = { + { + .topic_filter = + { + .ptr = (uint8_t *)s_topic_filter1, + .len = AWS_ARRAY_SIZE(s_topic_filter1) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = true, + .retain_as_published = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + }, + { + .topic_filter = + { + .ptr = (uint8_t *)s_topic_filter2, + .len = AWS_ARRAY_SIZE(s_topic_filter2) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .no_local = false, + .retain_as_published = true, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE_IF_NEW, + }, +}; + +static int s_verify_subscriptions_raw( + size_t expected_subscription_count, + const struct aws_mqtt5_subscription_view *expected_subscriptions, + size_t actual_subscription_count, + const struct aws_mqtt5_subscription_view *actual_subscriptions) { + ASSERT_INT_EQUALS(expected_subscription_count, actual_subscription_count); + + for (size_t i = 0; i < expected_subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *expected_view = &expected_subscriptions[i]; + const struct aws_mqtt5_subscription_view *actual_view = &actual_subscriptions[i]; + + ASSERT_BIN_ARRAYS_EQUALS( + expected_view->topic_filter.ptr, + expected_view->topic_filter.len, + actual_view->topic_filter.ptr, + actual_view->topic_filter.len); + ASSERT_INT_EQUALS(expected_view->qos, actual_view->qos); + ASSERT_INT_EQUALS(expected_view->no_local, actual_view->no_local); + ASSERT_INT_EQUALS(expected_view->retain_as_published, actual_view->retain_as_published); + ASSERT_INT_EQUALS(expected_view->retain_handling_type, actual_view->retain_handling_type); + } + + return AWS_OP_SUCCESS; +} + +static int s_verify_subscriptions( + size_t expected_subscription_count, + const struct aws_mqtt5_subscription_view *expected_subscriptions, + struct aws_array_list *storage_subscriptions) { + return s_verify_subscriptions_raw( + expected_subscription_count, + expected_subscriptions, + aws_array_list_length(storage_subscriptions), + storage_subscriptions->data); +} + +static int s_aws_mqtt5_subcribe_operation_verify_required_properties( + struct aws_mqtt5_operation_subscribe *subscribe_op, + struct aws_mqtt5_packet_subscribe_view *original_view, + struct aws_mqtt5_subscribe_completion_options *original_completion_options) { + (void)original_view; + + ASSERT_NOT_NULL(subscribe_op); + + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage = &subscribe_op->options_storage; + struct aws_mqtt5_packet_subscribe_view *stored_view = &subscribe_storage->storage_view; + + ASSERT_SUCCESS( + s_verify_subscriptions(AWS_ARRAY_SIZE(s_subscriptions), s_subscriptions, &subscribe_storage->subscriptions)); + ASSERT_SUCCESS(s_verify_subscriptions_raw( + AWS_ARRAY_SIZE(s_subscriptions), s_subscriptions, stored_view->subscription_count, stored_view->subscriptions)); + + ASSERT_PTR_EQUALS( + original_completion_options->completion_callback, subscribe_op->completion_options.completion_callback); + ASSERT_PTR_EQUALS( + original_completion_options->completion_user_data, subscribe_op->completion_options.completion_user_data); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_subscribe_completion_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + (void)suback; + (void)error_code; + (void)complete_ctx; +} + +static int s_mqtt5_subscribe_operation_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_subscribe_view subscribe_options = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + .user_property_count = 0, + .user_properties = NULL, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_subscribe_view_validate(&subscribe_options)); + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = &s_aws_mqtt5_subscribe_completion_fn, + .completion_user_data = (void *)0xFFFF, + }; + + struct aws_mqtt5_operation_subscribe *subscribe_op = + aws_mqtt5_operation_subscribe_new(allocator, NULL, &subscribe_options, &completion_options); + + ASSERT_SUCCESS(s_aws_mqtt5_subcribe_operation_verify_required_properties( + subscribe_op, &subscribe_options, &completion_options)); + + struct aws_mqtt5_packet_subscribe_view *stored_view = &subscribe_op->options_storage.storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_subscribe_view_validate(stored_view)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&subscribe_op->options_storage, subscription_identifier); + + aws_mqtt5_packet_subscribe_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_operation_release(&subscribe_op->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_subscribe_operation_new_set_no_optional, s_mqtt5_subscribe_operation_new_set_no_optional_fn) + +static int s_mqtt5_subscribe_operation_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_subscribe_view subscribe_options = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + }; + + uint32_t sub_id = 5; + subscribe_options.subscription_identifier = &sub_id; + + ASSERT_SUCCESS(aws_mqtt5_packet_subscribe_view_validate(&subscribe_options)); + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = &s_aws_mqtt5_subscribe_completion_fn, + .completion_user_data = (void *)0xFFFF, + }; + + struct aws_mqtt5_operation_subscribe *subscribe_op = + aws_mqtt5_operation_subscribe_new(allocator, NULL, &subscribe_options, &completion_options); + + ASSERT_SUCCESS(s_aws_mqtt5_subcribe_operation_verify_required_properties( + subscribe_op, &subscribe_options, &completion_options)); + + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage = &subscribe_op->options_storage; + struct aws_mqtt5_packet_subscribe_view *stored_view = &subscribe_storage->storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_subscribe_view_validate(stored_view)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(subscribe_storage, &subscribe_options, subscription_identifier); + + ASSERT_SUCCESS(s_verify_user_properties( + &subscribe_storage->user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_subscribe_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_operation_release(&subscribe_op->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_subscribe_operation_new_set_all, s_mqtt5_subscribe_operation_new_set_all_fn) + +static void s_aws_mqtt5_unsubscribe_completion_fn( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + (void)unsuback; + (void)error_code; + (void)complete_ctx; +} + +static const char s_unsub_topic_filter1[] = "a/topic"; +static const char s_unsub_topic_filter2[] = "another/*"; +static const char s_unsub_topic_filter3[] = "hello/+/world"; + +static const struct aws_byte_cursor s_topics[] = { + { + .ptr = (uint8_t *)s_unsub_topic_filter1, + .len = AWS_ARRAY_SIZE(s_unsub_topic_filter1) - 1, + }, + { + .ptr = (uint8_t *)s_unsub_topic_filter2, + .len = AWS_ARRAY_SIZE(s_unsub_topic_filter2) - 1, + }, + { + .ptr = (uint8_t *)s_unsub_topic_filter3, + .len = AWS_ARRAY_SIZE(s_unsub_topic_filter3) - 1, + }, +}; + +static int s_mqtt5_unsubscribe_operation_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_options = { + .topic_filters = s_topics, + .topic_filter_count = AWS_ARRAY_SIZE(s_topics), + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_unsubscribe_view_validate(&unsubscribe_options)); + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .completion_callback = &s_aws_mqtt5_unsubscribe_completion_fn, + .completion_user_data = (void *)0xFFFF, + }; + + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = + aws_mqtt5_operation_unsubscribe_new(allocator, NULL, &unsubscribe_options, &completion_options); + + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage = &unsubscribe_op->options_storage; + struct aws_mqtt5_packet_unsubscribe_view *stored_view = &unsubscribe_storage->storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_unsubscribe_view_validate(stored_view)); + + ASSERT_UINT_EQUALS(stored_view->topic_filter_count, unsubscribe_options.topic_filter_count); + for (size_t i = 0; i < stored_view->topic_filter_count; ++i) { + const struct aws_byte_cursor *expected_topic = &unsubscribe_options.topic_filters[i]; + const struct aws_byte_cursor *actual_topic = &stored_view->topic_filters[i]; + + ASSERT_UINT_EQUALS(expected_topic->len, actual_topic->len); + ASSERT_TRUE(expected_topic->ptr != actual_topic->ptr); + + ASSERT_BIN_ARRAYS_EQUALS(expected_topic->ptr, expected_topic->len, actual_topic->ptr, actual_topic->len); + } + + ASSERT_SUCCESS(s_verify_user_properties( + &unsubscribe_storage->user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_unsubscribe_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_operation_release(&unsubscribe_op->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_unsubscribe_operation_new_set_all, s_mqtt5_unsubscribe_operation_new_set_all_fn) + +static int s_aws_mqtt5_connect_storage_verify_required_properties( + struct aws_mqtt5_packet_connect_storage *connect_storage, + struct aws_mqtt5_packet_connect_view *connect_options) { + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(connect_storage, connect_options, keep_alive_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(connect_storage, connect_options, client_id); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(connect_storage, connect_options, clean_start); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_connect_storage_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_connect_view connect_options = { + .keep_alive_interval_seconds = 50, + .client_id = aws_byte_cursor_from_string(s_client_id), + .clean_start = true, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_view_validate(&connect_options)); + + struct aws_mqtt5_packet_connect_storage connect_storage; + AWS_ZERO_STRUCT(connect_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(&connect_storage, allocator, &connect_options)); + + ASSERT_SUCCESS(s_aws_mqtt5_connect_storage_verify_required_properties(&connect_storage, &connect_options)); + + struct aws_mqtt5_packet_connect_view *stored_view = &connect_storage.storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_connect_view_validate(stored_view)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, username); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, password); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, session_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, request_response_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, request_problem_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, receive_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, topic_alias_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, maximum_packet_size_bytes); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, will_delay_interval_seconds); + + ASSERT_NULL(connect_storage.will); + ASSERT_NULL(stored_view->will); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, authentication_method); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connect_storage, authentication_data); + + ASSERT_SUCCESS(s_verify_user_properties(&connect_storage.user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + aws_mqtt5_packet_connect_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_connect_storage_clean_up(&connect_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_connect_storage_new_set_no_optional, s_mqtt5_connect_storage_new_set_no_optional_fn) + +static const char s_username[] = "SomeUser"; +static const struct aws_byte_cursor s_username_cursor = { + .ptr = (uint8_t *)s_username, + .len = AWS_ARRAY_SIZE(s_username) - 1, +}; + +static const char s_password[] = "CantBeGuessed"; +static const struct aws_byte_cursor s_password_cursor = { + .ptr = (uint8_t *)s_password, + .len = AWS_ARRAY_SIZE(s_password) - 1, +}; + +static const uint32_t s_session_expiry_interval_seconds = 60; +static const uint8_t s_request_response_information = true; +static const uint8_t s_request_problem_information = true; +static const uint16_t s_connect_receive_maximum = 10; +static const uint16_t s_connect_topic_alias_maximum = 15; +static const uint32_t s_connect_maximum_packet_size_bytes = 128 * 1024 * 1024; +static const uint32_t s_will_delay_interval_seconds = 30; + +static const char s_authentication_method[] = "ECDSA-DH-RSA-EVP-SEKRTI"; +static const struct aws_byte_cursor s_authentication_method_cursor = { + .ptr = (uint8_t *)s_authentication_method, + .len = AWS_ARRAY_SIZE(s_authentication_method) - 1, +}; + +static const char s_authentication_data[] = "SomeSignature"; +static const struct aws_byte_cursor s_authentication_data_cursor = { + .ptr = (uint8_t *)s_authentication_data, + .len = AWS_ARRAY_SIZE(s_authentication_data) - 1, +}; + +static int s_mqtt5_connect_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_publish_view publish_options = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + }; + + struct aws_mqtt5_packet_connect_view connect_options = { + .keep_alive_interval_seconds = 50, + .client_id = aws_byte_cursor_from_string(s_client_id), + .username = &s_username_cursor, + .password = &s_password_cursor, + .clean_start = true, + .session_expiry_interval_seconds = &s_session_expiry_interval_seconds, + .request_response_information = &s_request_response_information, + .request_problem_information = &s_request_problem_information, + .receive_maximum = &s_connect_receive_maximum, + .topic_alias_maximum = &s_connect_topic_alias_maximum, + .maximum_packet_size_bytes = &s_connect_maximum_packet_size_bytes, + .will_delay_interval_seconds = &s_will_delay_interval_seconds, + .will = &publish_options, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + .authentication_method = &s_authentication_method_cursor, + .authentication_data = &s_authentication_data_cursor, + }; + + struct aws_mqtt5_packet_connect_storage connect_storage; + AWS_ZERO_STRUCT(connect_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(&connect_storage, allocator, &connect_options)); + + ASSERT_SUCCESS(s_aws_mqtt5_connect_storage_verify_required_properties(&connect_storage, &connect_options)); + + struct aws_mqtt5_packet_connect_view *stored_view = &connect_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connect_storage, &connect_options, username); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connect_storage, &connect_options, password); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &connect_storage, &connect_options, session_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &connect_storage, &connect_options, request_response_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connect_storage, &connect_options, request_problem_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connect_storage, &connect_options, receive_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connect_storage, &connect_options, topic_alias_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connect_storage, &connect_options, maximum_packet_size_bytes); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connect_storage, &connect_options, will_delay_interval_seconds); + + ASSERT_NOT_NULL(connect_storage.will); + ASSERT_NOT_NULL(stored_view->will); + ASSERT_SUCCESS(s_verify_publish_operation_required_fields(connect_storage.will, &publish_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connect_storage, &connect_options, authentication_method); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connect_storage, &connect_options, authentication_data); + + ASSERT_SUCCESS(s_verify_user_properties( + &connect_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_connect_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_connect_storage_clean_up(&connect_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_connect_storage_new_set_all, s_mqtt5_connect_storage_new_set_all_fn) + +static int s_aws_mqtt5_connack_storage_verify_required_properties( + struct aws_mqtt5_packet_connack_storage *connack_storage, + struct aws_mqtt5_packet_connack_view *connack_view) { + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(connack_storage, connack_view, session_present); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(connack_storage, connack_view, reason_code); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_connack_storage_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + struct aws_mqtt5_packet_connack_view connack_options = { + .session_present = true, + .reason_code = AWS_MQTT5_CRC_BANNED, + }; + + struct aws_mqtt5_packet_connack_storage connack_storage; + AWS_ZERO_STRUCT(connack_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_connack_storage_init(&connack_storage, allocator, &connack_options)); + + ASSERT_SUCCESS(s_aws_mqtt5_connack_storage_verify_required_properties(&connack_storage, &connack_options)); + + struct aws_mqtt5_packet_connack_view *stored_view = &connack_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, session_expiry_interval); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, receive_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, maximum_qos); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, retain_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, maximum_packet_size); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, assigned_client_identifier); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, topic_alias_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, reason_string); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, wildcard_subscriptions_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, subscription_identifiers_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, shared_subscriptions_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, server_keep_alive); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, response_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, server_reference); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, authentication_method); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&connack_storage, authentication_data); + + ASSERT_SUCCESS(s_verify_user_properties(&connack_storage.user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + aws_mqtt5_packet_connack_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_connack_storage_clean_up(&connack_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_connack_storage_new_set_no_optional, s_mqtt5_connack_storage_new_set_no_optional_fn) + +static const uint32_t s_connack_session_expiry_interval = 300; +static const uint16_t s_connack_receive_maximum = 15; +static const enum aws_mqtt5_qos s_connack_maximum_qos = AWS_MQTT5_QOS_EXACTLY_ONCE; +static const bool s_connack_retain_available = true; +static const uint32_t s_connack_maximum_packet_size = 256 * 1024 * 1024; + +static const char s_assigned_client_identifier[] = "ThisIsYourClientId"; +static const struct aws_byte_cursor s_assigned_client_identifier_cursor = { + .ptr = (uint8_t *)s_assigned_client_identifier, + .len = AWS_ARRAY_SIZE(s_assigned_client_identifier) - 1, +}; +static const uint16_t s_connack_topic_alias_maximum = 32; + +static const char s_reason_string[] = "Very Bad Behavior"; +static const struct aws_byte_cursor s_reason_string_cursor = { + .ptr = (uint8_t *)s_reason_string, + .len = AWS_ARRAY_SIZE(s_reason_string) - 1, +}; + +static const bool s_connack_wildcard_subscriptions_available = true; +static const bool s_connack_subscription_identifiers_available = true; +static const bool s_connack_shared_subscriptions_available = true; +static const uint16_t s_connack_server_keep_alive = 3600; + +static const char s_response_information[] = "Everything worked great!"; +static const struct aws_byte_cursor s_response_information_cursor = { + .ptr = (uint8_t *)s_response_information, + .len = AWS_ARRAY_SIZE(s_response_information) - 1, +}; + +static const char s_server_reference[] = "no-dont-leave.com"; +static const struct aws_byte_cursor s_server_reference_cursor = { + .ptr = (uint8_t *)s_server_reference, + .len = AWS_ARRAY_SIZE(s_server_reference) - 1, +}; + +static int s_mqtt5_connack_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + struct aws_mqtt5_packet_connack_view connack_options = { + .session_present = true, + .reason_code = AWS_MQTT5_CRC_BANNED, + .session_expiry_interval = &s_connack_session_expiry_interval, + .receive_maximum = &s_connack_receive_maximum, + .maximum_qos = &s_connack_maximum_qos, + .retain_available = &s_connack_retain_available, + .maximum_packet_size = &s_connack_maximum_packet_size, + .assigned_client_identifier = &s_assigned_client_identifier_cursor, + .topic_alias_maximum = &s_connack_topic_alias_maximum, + .reason_string = &s_reason_string_cursor, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + .wildcard_subscriptions_available = &s_connack_wildcard_subscriptions_available, + .subscription_identifiers_available = &s_connack_subscription_identifiers_available, + .shared_subscriptions_available = &s_connack_shared_subscriptions_available, + .server_keep_alive = &s_connack_server_keep_alive, + .response_information = &s_response_information_cursor, + .server_reference = &s_server_reference_cursor, + .authentication_method = &s_authentication_method_cursor, + .authentication_data = &s_authentication_data_cursor, + }; + + struct aws_mqtt5_packet_connack_storage connack_storage; + AWS_ZERO_STRUCT(connack_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_connack_storage_init(&connack_storage, allocator, &connack_options)); + + ASSERT_SUCCESS(s_aws_mqtt5_connack_storage_verify_required_properties(&connack_storage, &connack_options)); + + struct aws_mqtt5_packet_connack_view *stored_view = &connack_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, session_expiry_interval); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, receive_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, maximum_qos); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, retain_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, maximum_packet_size); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR( + &connack_storage, &connack_options, assigned_client_identifier); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, topic_alias_maximum); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connack_storage, &connack_options, reason_string); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &connack_storage, &connack_options, wildcard_subscriptions_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &connack_storage, &connack_options, subscription_identifiers_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &connack_storage, &connack_options, shared_subscriptions_available); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&connack_storage, &connack_options, server_keep_alive); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connack_storage, &connack_options, response_information); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connack_storage, &connack_options, server_reference); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connack_storage, &connack_options, authentication_method); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&connack_storage, &connack_options, authentication_data); + + ASSERT_SUCCESS(s_verify_user_properties( + &connack_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_connack_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_connack_storage_clean_up(&connack_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_connack_storage_new_set_all, s_mqtt5_connack_storage_new_set_all_fn) + +static int s_mqtt5_disconnect_storage_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + struct aws_mqtt5_packet_disconnect_view disconnect_options = { + .reason_code = AWS_MQTT5_DRC_ADMINISTRATIVE_ACTION, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_disconnect_view_validate(&disconnect_options)); + + struct aws_mqtt5_packet_disconnect_storage disconnect_storage; + AWS_ZERO_STRUCT(disconnect_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_disconnect_storage_init(&disconnect_storage, allocator, &disconnect_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&disconnect_storage, &disconnect_options, reason_code); + + struct aws_mqtt5_packet_disconnect_view *stored_view = &disconnect_storage.storage_view; + ASSERT_SUCCESS(aws_mqtt5_packet_disconnect_view_validate(stored_view)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&disconnect_storage, session_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&disconnect_storage, reason_string); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&disconnect_storage, server_reference); + + ASSERT_SUCCESS(s_verify_user_properties(&disconnect_storage.user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + aws_mqtt5_packet_disconnect_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_disconnect_storage_clean_up(&disconnect_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_disconnect_storage_new_set_no_optional, s_mqtt5_disconnect_storage_new_set_no_optional_fn) + +static int s_mqtt5_disconnect_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_disconnect_view disconnect_options = { + .reason_code = AWS_MQTT5_DRC_ADMINISTRATIVE_ACTION, + .session_expiry_interval_seconds = &s_session_expiry_interval_seconds, + .reason_string = &s_reason_string_cursor, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + .server_reference = &s_server_reference_cursor, + }; + + struct aws_mqtt5_packet_disconnect_storage disconnect_storage; + AWS_ZERO_STRUCT(disconnect_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_disconnect_storage_init(&disconnect_storage, allocator, &disconnect_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&disconnect_storage, &disconnect_options, reason_code); + + struct aws_mqtt5_packet_disconnect_view *stored_view = &disconnect_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &disconnect_storage, &disconnect_options, session_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&disconnect_storage, &disconnect_options, reason_string); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&disconnect_storage, &disconnect_options, server_reference); + + ASSERT_SUCCESS(s_verify_user_properties( + &disconnect_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_disconnect_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_disconnect_storage_clean_up(&disconnect_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_disconnect_storage_new_set_all, s_mqtt5_disconnect_storage_new_set_all_fn) + +static const enum aws_mqtt5_suback_reason_code s_suback_reason_codes[] = { + AWS_MQTT5_SARC_GRANTED_QOS_0, + AWS_MQTT5_SARC_GRANTED_QOS_2, + AWS_MQTT5_SARC_NOT_AUTHORIZED, +}; + +static int s_verify_suback_reason_codes_raw( + const enum aws_mqtt5_suback_reason_code *reason_codes, + size_t reason_code_count, + const struct aws_mqtt5_packet_suback_view *original_view) { + ASSERT_UINT_EQUALS(reason_code_count, original_view->reason_code_count); + + for (size_t i = 0; i < reason_code_count; ++i) { + ASSERT_UINT_EQUALS(reason_codes[i], original_view->reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +static int s_verify_suback_reason_codes( + const struct aws_mqtt5_packet_suback_storage *suback_storage, + const struct aws_mqtt5_packet_suback_view *original_view) { + + ASSERT_SUCCESS(s_verify_suback_reason_codes_raw( + suback_storage->reason_codes.data, aws_array_list_length(&suback_storage->reason_codes), original_view)); + + const struct aws_mqtt5_packet_suback_view *storage_view = &suback_storage->storage_view; + ASSERT_SUCCESS( + s_verify_suback_reason_codes_raw(storage_view->reason_codes, storage_view->reason_code_count, original_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_suback_storage_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_suback_view suback_options = { + .reason_codes = s_suback_reason_codes, + .reason_code_count = AWS_ARRAY_SIZE(s_suback_reason_codes), + }; + + struct aws_mqtt5_packet_suback_storage suback_storage; + AWS_ZERO_STRUCT(suback_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_suback_storage_init(&suback_storage, allocator, &suback_options)); + + struct aws_mqtt5_packet_suback_view *stored_view = &suback_storage.storage_view; + + ASSERT_SUCCESS(s_verify_suback_reason_codes(&suback_storage, &suback_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&suback_storage, reason_string); + + ASSERT_SUCCESS(s_verify_user_properties(&suback_storage.user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + aws_mqtt5_packet_suback_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_suback_storage_clean_up(&suback_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_suback_storage_new_set_no_optional, s_mqtt5_suback_storage_new_set_no_optional_fn) + +static int s_mqtt5_suback_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_suback_view suback_options = { + .reason_string = &s_reason_string_cursor, + .reason_codes = s_suback_reason_codes, + .reason_code_count = AWS_ARRAY_SIZE(s_suback_reason_codes), + .user_properties = s_user_properties, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + }; + + struct aws_mqtt5_packet_suback_storage suback_storage; + AWS_ZERO_STRUCT(suback_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_suback_storage_init(&suback_storage, allocator, &suback_options)); + + struct aws_mqtt5_packet_suback_view *stored_view = &suback_storage.storage_view; + + ASSERT_SUCCESS(s_verify_suback_reason_codes(&suback_storage, &suback_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&suback_storage, &suback_options, reason_string); + + ASSERT_SUCCESS(s_verify_user_properties( + &suback_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_suback_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_suback_storage_clean_up(&suback_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_suback_storage_new_set_all, s_mqtt5_suback_storage_new_set_all_fn) + +static const enum aws_mqtt5_unsuback_reason_code s_unsuback_reason_codes[] = { + AWS_MQTT5_UARC_NOT_AUTHORIZED, + AWS_MQTT5_UARC_SUCCESS, + AWS_MQTT5_UARC_NO_SUBSCRIPTION_EXISTED, +}; + +static int s_verify_unsuback_reason_codes_raw( + const enum aws_mqtt5_unsuback_reason_code *reason_codes, + size_t reason_code_count, + const struct aws_mqtt5_packet_unsuback_view *original_view) { + ASSERT_UINT_EQUALS(reason_code_count, original_view->reason_code_count); + + for (size_t i = 0; i < reason_code_count; ++i) { + ASSERT_UINT_EQUALS(reason_codes[i], original_view->reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +static int s_verify_unsuback_reason_codes( + const struct aws_mqtt5_packet_unsuback_storage *unsuback_storage, + const struct aws_mqtt5_packet_unsuback_view *original_view) { + + ASSERT_SUCCESS(s_verify_unsuback_reason_codes_raw( + unsuback_storage->reason_codes.data, aws_array_list_length(&unsuback_storage->reason_codes), original_view)); + + const struct aws_mqtt5_packet_unsuback_view *storage_view = &unsuback_storage->storage_view; + ASSERT_SUCCESS( + s_verify_unsuback_reason_codes_raw(storage_view->reason_codes, storage_view->reason_code_count, original_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_unsuback_storage_new_set_no_optional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_unsuback_view unsuback_options = { + .reason_codes = s_unsuback_reason_codes, + .reason_code_count = AWS_ARRAY_SIZE(s_unsuback_reason_codes), + }; + + struct aws_mqtt5_packet_unsuback_storage unsuback_storage; + AWS_ZERO_STRUCT(unsuback_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_unsuback_storage_init(&unsuback_storage, allocator, &unsuback_options)); + + struct aws_mqtt5_packet_unsuback_view *stored_view = &unsuback_storage.storage_view; + + ASSERT_SUCCESS(s_verify_unsuback_reason_codes(&unsuback_storage, &unsuback_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULL(&unsuback_storage, reason_string); + + ASSERT_SUCCESS(s_verify_user_properties(&unsuback_storage.user_properties, 0, NULL)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, stored_view->user_properties, 0, NULL)); + + aws_mqtt5_packet_unsuback_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_unsuback_storage_clean_up(&unsuback_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_unsuback_storage_new_set_no_optional, s_mqtt5_unsuback_storage_new_set_no_optional_fn) + +static int s_mqtt5_unsuback_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_unsuback_view unsuback_options = { + .reason_string = &s_reason_string_cursor, + .reason_codes = s_unsuback_reason_codes, + .reason_code_count = AWS_ARRAY_SIZE(s_unsuback_reason_codes), + .user_properties = s_user_properties, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + }; + + struct aws_mqtt5_packet_unsuback_storage unsuback_storage; + AWS_ZERO_STRUCT(unsuback_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_unsuback_storage_init(&unsuback_storage, allocator, &unsuback_options)); + + struct aws_mqtt5_packet_unsuback_view *stored_view = &unsuback_storage.storage_view; + + ASSERT_SUCCESS(s_verify_unsuback_reason_codes(&unsuback_storage, &unsuback_options)); + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&unsuback_storage, &unsuback_options, reason_string); + + ASSERT_SUCCESS(s_verify_user_properties( + &unsuback_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_unsuback_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_unsuback_storage_clean_up(&unsuback_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_unsuback_storage_new_set_all, s_mqtt5_unsuback_storage_new_set_all_fn) + +static int s_mqtt5_puback_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_puback_view puback_options = { + .packet_id = 333, + .reason_code = AWS_MQTT5_PARC_NO_MATCHING_SUBSCRIBERS, + .reason_string = &s_reason_string_cursor, + .user_properties = s_user_properties, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + }; + + struct aws_mqtt5_packet_puback_storage puback_storage; + AWS_ZERO_STRUCT(puback_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_puback_storage_init(&puback_storage, allocator, &puback_options)); + + struct aws_mqtt5_packet_puback_view *stored_view = &puback_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&puback_storage, &puback_options, packet_id); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&puback_storage, &puback_options, reason_code); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&puback_storage, &puback_options, reason_string); + + ASSERT_SUCCESS(s_verify_user_properties( + &puback_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_puback_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_puback_storage_clean_up(&puback_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_puback_storage_new_set_all, s_mqtt5_puback_storage_new_set_all_fn) + +static int s_verify_publish_subscription_identifiers_raw( + const uint32_t *subscription_identifiers, + size_t subscription_identifier_count, + const struct aws_mqtt5_packet_publish_view *original_view) { + ASSERT_UINT_EQUALS(subscription_identifier_count, original_view->subscription_identifier_count); + + for (size_t i = 0; i < subscription_identifier_count; ++i) { + ASSERT_UINT_EQUALS(subscription_identifiers[i], original_view->subscription_identifiers[i]); + } + + return AWS_OP_SUCCESS; +} + +static int s_verify_publish_subscription_identifiers( + const struct aws_mqtt5_packet_publish_storage *publish_storage, + const struct aws_mqtt5_packet_publish_view *original_view) { + + ASSERT_SUCCESS(s_verify_publish_subscription_identifiers_raw( + publish_storage->subscription_identifiers.data, + aws_array_list_length(&publish_storage->subscription_identifiers), + original_view)); + + const struct aws_mqtt5_packet_publish_view *storage_view = &publish_storage->storage_view; + ASSERT_SUCCESS(s_verify_publish_subscription_identifiers_raw( + storage_view->subscription_identifiers, storage_view->subscription_identifier_count, original_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_publish_storage_new_set_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_cursor response_topic = aws_byte_cursor_from_c_str(s_response_topic); + struct aws_byte_cursor correlation_data = aws_byte_cursor_from_c_str(s_correlation_data); + struct aws_byte_cursor content_type = aws_byte_cursor_from_c_str(s_content_type); + enum aws_mqtt5_payload_format_indicator payload_format = AWS_MQTT5_PFI_UTF8; + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_c_str(PUBLISH_PAYLOAD); + uint32_t subscription_identifiers[] = {2, 128000}; + + struct aws_mqtt5_packet_publish_view publish_options = { + .packet_id = 333, + .payload = payload_cursor, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .retain = false, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + .payload_format = &payload_format, + .message_expiry_interval_seconds = &s_message_expiry_interval_seconds, + .topic_alias = &s_topic_alias, + .response_topic = &response_topic, + .correlation_data = &correlation_data, + .subscription_identifier_count = AWS_ARRAY_SIZE(subscription_identifiers), + .subscription_identifiers = subscription_identifiers, + .content_type = &content_type, + .user_property_count = AWS_ARRAY_SIZE(s_user_properties), + .user_properties = s_user_properties, + }; + + struct aws_mqtt5_packet_publish_storage publish_storage; + AWS_ZERO_STRUCT(publish_storage); + + ASSERT_SUCCESS(aws_mqtt5_packet_publish_storage_init(&publish_storage, allocator, &publish_options)); + + struct aws_mqtt5_packet_publish_view *stored_view = &publish_storage.storage_view; + + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&publish_storage, &publish_options, packet_id); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(&publish_storage, &publish_options, payload); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&publish_storage, &publish_options, qos); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_UINT(&publish_storage, &publish_options, retain); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_CURSOR(&publish_storage, &publish_options, topic); + + /* optional fields */ + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&publish_storage, &publish_options, payload_format); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT( + &publish_storage, &publish_options, message_expiry_interval_seconds); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_UINT(&publish_storage, &publish_options, topic_alias); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&publish_storage, &publish_options, response_topic); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&publish_storage, &publish_options, correlation_data); + AWS_VERIFY_VIEW_STORAGE_RELATIONSHIP_NULLABLE_CURSOR(&publish_storage, &publish_options, content_type); + + ASSERT_SUCCESS(s_verify_publish_subscription_identifiers(&publish_storage, &publish_options)); + + ASSERT_SUCCESS(s_verify_user_properties( + &publish_storage.user_properties, AWS_ARRAY_SIZE(s_user_properties), s_user_properties)); + ASSERT_SUCCESS(aws_mqtt5_test_verify_user_properties_raw( + stored_view->user_property_count, + stored_view->user_properties, + AWS_ARRAY_SIZE(s_user_properties), + s_user_properties)); + + aws_mqtt5_packet_publish_view_log(stored_view, AWS_LL_DEBUG); + + aws_mqtt5_packet_publish_storage_clean_up(&publish_storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_publish_storage_new_set_all, s_mqtt5_publish_storage_new_set_all_fn) + +static const enum aws_mqtt5_qos s_maximum_qos_at_least_once = AWS_MQTT5_QOS_AT_LEAST_ONCE; +static const enum aws_mqtt5_qos s_maximum_qos_at_most_once = AWS_MQTT5_QOS_AT_MOST_ONCE; +static const uint16_t s_keep_alive_interval_seconds = 999; +static const uint32_t s_session_expiry_interval = 999; +static const uint16_t s_receive_maximum = 999; +static const uint32_t s_maximum_packet_size = 999; +static const uint16_t s_topic_alias_maximum_to_server = 999; +static const uint16_t s_topic_alias_maximum = 999; +static const uint16_t s_server_keep_alive = 999; +static const bool s_retain_available = false; +static const bool s_wildcard_subscriptions_available = false; +static const bool s_subscription_identifiers_available = false; +static const bool s_shared_subscriptions_available = false; + +static int mqtt5_negotiated_settings_reset_test_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + /* aws_mqtt5_negotiated_settings used for testing */ + struct aws_mqtt5_negotiated_settings negotiated_settings; + AWS_ZERO_STRUCT(negotiated_settings); + + /* Simulate an aws_mqtt5_packet_connect_view with no user set settings */ + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 0, + }; + + /* Apply no client settings to a reset of negotiated_settings */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Check that all settings are the expected default values */ + ASSERT_TRUE(negotiated_settings.maximum_qos == AWS_MQTT5_QOS_AT_LEAST_ONCE); + + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, 0); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, AWS_MQTT5_RECEIVE_MAXIMUM); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, AWS_MQTT5_MAXIMUM_PACKET_SIZE); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, 0); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_client, 0); + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, 0); + + ASSERT_TRUE(negotiated_settings.retain_available); + ASSERT_TRUE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_TRUE(negotiated_settings.subscription_identifiers_available); + ASSERT_TRUE(negotiated_settings.shared_subscriptions_available); + ASSERT_FALSE(negotiated_settings.rejoined_session); + + /* Set client modifiable CONNECT settings */ + connect_view.keep_alive_interval_seconds = s_keep_alive_interval_seconds; + connect_view.session_expiry_interval_seconds = &s_session_expiry_interval; + connect_view.receive_maximum = &s_receive_maximum; + connect_view.maximum_packet_size_bytes = &s_maximum_packet_size; + connect_view.topic_alias_maximum = &s_topic_alias_maximum; + + /* Apply client settings to a reset of negotiated settings */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Check that all settings are the expected values with client settings */ + ASSERT_TRUE(negotiated_settings.maximum_qos == AWS_MQTT5_QOS_AT_LEAST_ONCE); + + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, connect_view.keep_alive_interval_seconds); + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, *connect_view.session_expiry_interval_seconds); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, AWS_MQTT5_RECEIVE_MAXIMUM); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, 0); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_client, *connect_view.topic_alias_maximum); + + ASSERT_TRUE(negotiated_settings.retain_available); + ASSERT_TRUE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_TRUE(negotiated_settings.subscription_identifiers_available); + ASSERT_TRUE(negotiated_settings.shared_subscriptions_available); + ASSERT_FALSE(negotiated_settings.rejoined_session); + + /* Reset connect view to clean defaults */ + + connect_view.keep_alive_interval_seconds = 0; + connect_view.session_expiry_interval_seconds = NULL; + connect_view.receive_maximum = NULL; + connect_view.maximum_packet_size_bytes = NULL; + connect_view.topic_alias_maximum = NULL; + + /* Change remaining default properties on negotiated_settings to non-default values */ + negotiated_settings.maximum_qos = AWS_MQTT5_QOS_EXACTLY_ONCE; + + negotiated_settings.topic_alias_maximum_to_server = s_topic_alias_maximum_to_server; + negotiated_settings.topic_alias_maximum_to_client = s_topic_alias_maximum_to_server; + + negotiated_settings.retain_available = s_retain_available; + negotiated_settings.wildcard_subscriptions_available = s_wildcard_subscriptions_available; + negotiated_settings.subscription_identifiers_available = s_subscription_identifiers_available; + negotiated_settings.shared_subscriptions_available = s_shared_subscriptions_available; + + /* Apply no client settings to a reset of negotiated_settings */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Check that all settings are the expected default values */ + ASSERT_TRUE(negotiated_settings.maximum_qos == AWS_MQTT5_QOS_AT_LEAST_ONCE); + + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, 0); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, AWS_MQTT5_RECEIVE_MAXIMUM); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, AWS_MQTT5_MAXIMUM_PACKET_SIZE); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, 0); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_client, 0); + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, 0); + + ASSERT_TRUE(negotiated_settings.retain_available); + ASSERT_TRUE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_TRUE(negotiated_settings.subscription_identifiers_available); + ASSERT_TRUE(negotiated_settings.shared_subscriptions_available); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_negotiated_settings_reset_test, mqtt5_negotiated_settings_reset_test_fn) + +static int mqtt5_negotiated_settings_apply_connack_test_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + + /* aws_mqtt5_negotiated_settings used for testing */ + struct aws_mqtt5_negotiated_settings negotiated_settings; + AWS_ZERO_STRUCT(negotiated_settings); + + /* An aws_mqtt5_packet_connect_view with no user set settings to reset negotiated_settings */ + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 0, + }; + + /* reset negotiated_settings to default values */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Simulate an aws_mqtt5_packet_connack_view with no user set settings */ + struct aws_mqtt5_packet_connack_view connack_view = { + .session_present = false, + }; + + /* Check if everything defaults appropriately if no properties are set in either direction */ + aws_mqtt5_negotiated_settings_apply_connack(&negotiated_settings, &connack_view); + + ASSERT_TRUE(negotiated_settings.maximum_qos == AWS_MQTT5_QOS_AT_LEAST_ONCE); + + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, 0); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, AWS_MQTT5_RECEIVE_MAXIMUM); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, AWS_MQTT5_MAXIMUM_PACKET_SIZE); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, 0); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_client, 0); + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, 0); + + ASSERT_TRUE(negotiated_settings.retain_available); + ASSERT_TRUE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_TRUE(negotiated_settings.subscription_identifiers_available); + ASSERT_TRUE(negotiated_settings.shared_subscriptions_available); + ASSERT_FALSE(negotiated_settings.rejoined_session); + + /* Apply server settings to properties in connack_view */ + connack_view.session_present = true; + connack_view.maximum_qos = &s_maximum_qos_at_least_once; + connack_view.session_expiry_interval = &s_session_expiry_interval; + connack_view.receive_maximum = &s_receive_maximum; + connack_view.retain_available = &s_retain_available; + connack_view.maximum_packet_size = &s_maximum_packet_size; + connack_view.topic_alias_maximum = &s_topic_alias_maximum_to_server; + connack_view.wildcard_subscriptions_available = &s_wildcard_subscriptions_available; + connack_view.subscription_identifiers_available = &s_subscription_identifiers_available; + connack_view.shared_subscriptions_available = &s_shared_subscriptions_available; + connack_view.server_keep_alive = &s_server_keep_alive; + + aws_mqtt5_negotiated_settings_apply_connack(&negotiated_settings, &connack_view); + + ASSERT_TRUE(negotiated_settings.rejoined_session); + ASSERT_TRUE(negotiated_settings.maximum_qos == s_maximum_qos_at_least_once); + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, *connack_view.session_expiry_interval); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, *connack_view.receive_maximum); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, *connack_view.maximum_packet_size); + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, *connack_view.server_keep_alive); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, *connack_view.topic_alias_maximum); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_client, 0); + + ASSERT_FALSE(negotiated_settings.retain_available); + ASSERT_FALSE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_FALSE(negotiated_settings.subscription_identifiers_available); + ASSERT_FALSE(negotiated_settings.shared_subscriptions_available); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_negotiated_settings_apply_connack_test, mqtt5_negotiated_settings_apply_connack_test_fn) + +static int mqtt5_negotiated_settings_server_override_test_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + + /* aws_mqtt5_negotiated_settings used for client */ + struct aws_mqtt5_negotiated_settings negotiated_settings; + AWS_ZERO_STRUCT(negotiated_settings); + + /* An aws_mqtt5_packet_connect_view with no user set settings to reset negotiated_settings */ + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 0, + }; + + /* reset negotiated_settings to default values */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Simulate negotiated settings that the client may have set to values different from incoming CONNACK settings */ + negotiated_settings.session_expiry_interval = 123; + negotiated_settings.maximum_qos = s_maximum_qos_at_least_once; + negotiated_settings.receive_maximum_from_server = 123; + negotiated_settings.maximum_packet_size_to_server = 123; + negotiated_settings.topic_alias_maximum_to_server = 123; + negotiated_settings.topic_alias_maximum_to_client = 123; + negotiated_settings.server_keep_alive = 123; + + /* CONNACK settings from a server that should overwrite client settings */ + struct aws_mqtt5_packet_connack_view connack_view = { + .session_present = false, + .server_keep_alive = &s_keep_alive_interval_seconds, + .maximum_qos = &s_maximum_qos_at_most_once, + .session_expiry_interval = &s_session_expiry_interval, + .receive_maximum = &s_receive_maximum, + .maximum_packet_size = &s_maximum_packet_size, + .topic_alias_maximum = &s_topic_alias_maximum, + .retain_available = &s_retain_available, + .wildcard_subscriptions_available = &s_wildcard_subscriptions_available, + .subscription_identifiers_available = &s_subscription_identifiers_available, + .shared_subscriptions_available = &s_shared_subscriptions_available, + }; + + /* Apply CONNACK settings to client values in negotiated_settings */ + aws_mqtt5_negotiated_settings_apply_connack(&negotiated_settings, &connack_view); + + /* Assert values that should have been overwritten have been overwritten */ + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, s_keep_alive_interval_seconds); + ASSERT_TRUE(negotiated_settings.maximum_qos == s_maximum_qos_at_most_once); + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, s_session_expiry_interval); + ASSERT_UINT_EQUALS(negotiated_settings.receive_maximum_from_server, s_receive_maximum); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, s_maximum_packet_size); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, s_topic_alias_maximum); + ASSERT_FALSE(negotiated_settings.retain_available); + ASSERT_FALSE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_FALSE(negotiated_settings.subscription_identifiers_available); + ASSERT_FALSE(negotiated_settings.shared_subscriptions_available); + + /* reset negotiated_settings to default values */ + aws_mqtt5_negotiated_settings_reset(&negotiated_settings, &connect_view); + + /* Simulate negotiated settings that would change based on default/missing settings from a CONNACK */ + negotiated_settings.session_expiry_interval = s_session_expiry_interval_seconds; + + /* NULL CONNACK values that result in an override in negotiated settings */ + connack_view.server_keep_alive = NULL; + connack_view.topic_alias_maximum = NULL; + connack_view.maximum_qos = NULL; + connack_view.session_expiry_interval = NULL; + connack_view.receive_maximum = NULL; + connack_view.retain_available = NULL; + connack_view.maximum_packet_size = NULL; + connack_view.wildcard_subscriptions_available = NULL; + connack_view.subscription_identifiers_available = NULL; + connack_view.shared_subscriptions_available = NULL; + + /* Apply CONNACK settings to client values in negotiated_settings */ + aws_mqtt5_negotiated_settings_apply_connack(&negotiated_settings, &connack_view); + + /* Assert values that should have been overwritten have been overwritten */ + ASSERT_UINT_EQUALS(negotiated_settings.server_keep_alive, 0); + ASSERT_UINT_EQUALS(negotiated_settings.topic_alias_maximum_to_server, 0); + ASSERT_TRUE(negotiated_settings.maximum_qos == s_maximum_qos_at_least_once); + ASSERT_UINT_EQUALS(negotiated_settings.session_expiry_interval, s_session_expiry_interval_seconds); + ASSERT_TRUE(negotiated_settings.retain_available); + ASSERT_UINT_EQUALS(negotiated_settings.maximum_packet_size_to_server, AWS_MQTT5_MAXIMUM_PACKET_SIZE); + ASSERT_TRUE(negotiated_settings.wildcard_subscriptions_available); + ASSERT_TRUE(negotiated_settings.subscription_identifiers_available); + ASSERT_TRUE(negotiated_settings.shared_subscriptions_available); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_negotiated_settings_server_override_test, mqtt5_negotiated_settings_server_override_test_fn) + +static const struct aws_byte_cursor s_topic = { + .ptr = (uint8_t *)s_unsub_topic_filter1, + .len = AWS_ARRAY_SIZE(s_unsub_topic_filter1) - 1, +}; + +static const char s_payload[] = "ThePayload"; + +static const struct aws_byte_cursor s_payload_cursor = { + .ptr = (uint8_t *)s_payload, + .len = AWS_ARRAY_SIZE(s_payload) - 1, +}; + +/* test that allocates packet ids from an empty table. */ +static int s_mqtt5_operation_bind_packet_id_empty_table_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = s_topic, + .payload = s_payload_cursor, + }; + + struct aws_mqtt5_operation_publish *publish_operation = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + operational_state.next_mqtt_packet_id = 1; + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&publish_operation->base, &operational_state)); + ASSERT_UINT_EQUALS(1, aws_mqtt5_operation_get_packet_id(&publish_operation->base)); + ASSERT_UINT_EQUALS(2, operational_state.next_mqtt_packet_id); + + aws_mqtt5_operation_set_packet_id(&publish_operation->base, 0); + operational_state.next_mqtt_packet_id = 5; + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&publish_operation->base, &operational_state)); + ASSERT_UINT_EQUALS(5, aws_mqtt5_operation_get_packet_id(&publish_operation->base)); + ASSERT_UINT_EQUALS(6, operational_state.next_mqtt_packet_id); + + aws_mqtt5_operation_set_packet_id(&publish_operation->base, 0); + operational_state.next_mqtt_packet_id = 65535; + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&publish_operation->base, &operational_state)); + ASSERT_UINT_EQUALS(65535, aws_mqtt5_operation_get_packet_id(&publish_operation->base)); + ASSERT_UINT_EQUALS(1, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + + aws_mqtt5_operation_release(&publish_operation->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_bind_packet_id_empty_table, s_mqtt5_operation_bind_packet_id_empty_table_fn) + +static void s_create_operations( + struct aws_allocator *allocator, + struct aws_mqtt5_operation_publish **publish_op, + struct aws_mqtt5_operation_subscribe **subscribe_op, + struct aws_mqtt5_operation_unsubscribe **unsubscribe_op) { + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = s_topic, + .payload = s_payload_cursor, + }; + + *publish_op = aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + *subscribe_op = aws_mqtt5_operation_subscribe_new(allocator, NULL, &subscribe_view, NULL); + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = s_topics, + .topic_filter_count = AWS_ARRAY_SIZE(s_topics), + }; + + *unsubscribe_op = aws_mqtt5_operation_unsubscribe_new(allocator, NULL, &unsubscribe_view, NULL); +} + +static void s_seed_unacked_operations( + struct aws_mqtt5_client_operational_state *operational_state, + struct aws_mqtt5_operation_publish *pending_publish, + struct aws_mqtt5_operation_subscribe *pending_subscribe, + struct aws_mqtt5_operation_unsubscribe *pending_unsubscribe) { + aws_hash_table_put( + &operational_state->unacked_operations_table, + &pending_publish->options_storage.storage_view.packet_id, + &pending_publish->base, + NULL); + aws_linked_list_push_back(&operational_state->unacked_operations, &pending_publish->base.node); + aws_hash_table_put( + &operational_state->unacked_operations_table, + &pending_subscribe->options_storage.storage_view.packet_id, + &pending_subscribe->base, + NULL); + aws_linked_list_push_back(&operational_state->unacked_operations, &pending_subscribe->base.node); + aws_hash_table_put( + &operational_state->unacked_operations_table, + &pending_unsubscribe->options_storage.storage_view.packet_id, + &pending_unsubscribe->base, + NULL); + aws_linked_list_push_back(&operational_state->unacked_operations, &pending_unsubscribe->base.node); +} + +/* test that allocates packet ids from a table with entries that overlap the next id space */ +static int s_mqtt5_operation_bind_packet_id_multiple_with_existing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_publish *pending_publish = NULL; + struct aws_mqtt5_operation_subscribe *pending_subscribe = NULL; + struct aws_mqtt5_operation_unsubscribe *pending_unsubscribe = NULL; + s_create_operations(allocator, &pending_publish, &pending_subscribe, &pending_unsubscribe); + + aws_mqtt5_operation_set_packet_id(&pending_publish->base, 1); + aws_mqtt5_operation_set_packet_id(&pending_subscribe->base, 3); + aws_mqtt5_operation_set_packet_id(&pending_unsubscribe->base, 5); + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + + s_seed_unacked_operations(&operational_state, pending_publish, pending_subscribe, pending_unsubscribe); + + struct aws_mqtt5_operation_publish *new_publish = NULL; + struct aws_mqtt5_operation_subscribe *new_subscribe = NULL; + struct aws_mqtt5_operation_unsubscribe *new_unsubscribe = NULL; + s_create_operations(allocator, &new_publish, &new_subscribe, &new_unsubscribe); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_publish->base, &operational_state)); + ASSERT_UINT_EQUALS(2, aws_mqtt5_operation_get_packet_id(&new_publish->base)); + ASSERT_UINT_EQUALS(3, operational_state.next_mqtt_packet_id); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_subscribe->base, &operational_state)); + ASSERT_UINT_EQUALS(4, aws_mqtt5_operation_get_packet_id(&new_subscribe->base)); + ASSERT_UINT_EQUALS(5, operational_state.next_mqtt_packet_id); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_unsubscribe->base, &operational_state)); + ASSERT_UINT_EQUALS(6, aws_mqtt5_operation_get_packet_id(&new_unsubscribe->base)); + ASSERT_UINT_EQUALS(7, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + aws_mqtt5_operation_release(&new_publish->base); + aws_mqtt5_operation_release(&new_subscribe->base); + aws_mqtt5_operation_release(&new_unsubscribe->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_bind_packet_id_multiple_with_existing, + s_mqtt5_operation_bind_packet_id_multiple_with_existing_fn) + +/* test that allocates packet ids from a table where the next id forces an id wraparound */ +static int s_mqtt5_operation_bind_packet_id_multiple_with_wrap_around_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_publish *pending_publish = NULL; + struct aws_mqtt5_operation_subscribe *pending_subscribe = NULL; + struct aws_mqtt5_operation_unsubscribe *pending_unsubscribe = NULL; + s_create_operations(allocator, &pending_publish, &pending_subscribe, &pending_unsubscribe); + + aws_mqtt5_operation_set_packet_id(&pending_publish->base, 65533); + aws_mqtt5_operation_set_packet_id(&pending_subscribe->base, 65535); + aws_mqtt5_operation_set_packet_id(&pending_unsubscribe->base, 1); + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + operational_state.next_mqtt_packet_id = 65532; + + s_seed_unacked_operations(&operational_state, pending_publish, pending_subscribe, pending_unsubscribe); + + struct aws_mqtt5_operation_publish *new_publish = NULL; + struct aws_mqtt5_operation_subscribe *new_subscribe = NULL; + struct aws_mqtt5_operation_unsubscribe *new_unsubscribe = NULL; + s_create_operations(allocator, &new_publish, &new_subscribe, &new_unsubscribe); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_publish->base, &operational_state)); + ASSERT_UINT_EQUALS(65532, aws_mqtt5_operation_get_packet_id(&new_publish->base)); + ASSERT_UINT_EQUALS(65533, operational_state.next_mqtt_packet_id); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_subscribe->base, &operational_state)); + ASSERT_UINT_EQUALS(65534, aws_mqtt5_operation_get_packet_id(&new_subscribe->base)); + ASSERT_UINT_EQUALS(65535, operational_state.next_mqtt_packet_id); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_unsubscribe->base, &operational_state)); + ASSERT_UINT_EQUALS(2, aws_mqtt5_operation_get_packet_id(&new_unsubscribe->base)); + ASSERT_UINT_EQUALS(3, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + aws_mqtt5_operation_release(&new_publish->base); + aws_mqtt5_operation_release(&new_subscribe->base); + aws_mqtt5_operation_release(&new_unsubscribe->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_bind_packet_id_multiple_with_wrap_around, + s_mqtt5_operation_bind_packet_id_multiple_with_wrap_around_fn) + +/* test that fails to allocate packet ids from a full table */ +static int s_mqtt5_operation_bind_packet_id_full_table_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = s_topic, + .payload = s_payload_cursor, + }; + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + + for (uint16_t i = 0; i < UINT16_MAX; ++i) { + struct aws_mqtt5_operation_publish *publish_op = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + aws_mqtt5_operation_set_packet_id(&publish_op->base, i + 1); + + aws_hash_table_put( + &operational_state.unacked_operations_table, + &publish_op->options_storage.storage_view.packet_id, + &publish_op->base, + NULL); + aws_linked_list_push_back(&operational_state.unacked_operations, &publish_op->base.node); + } + + struct aws_mqtt5_operation_publish *new_publish = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + + ASSERT_FAILS(aws_mqtt5_operation_bind_packet_id(&new_publish->base, &operational_state)); + ASSERT_UINT_EQUALS(1, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + aws_mqtt5_operation_release(&new_publish->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_bind_packet_id_full_table, s_mqtt5_operation_bind_packet_id_full_table_fn) + +/* test that skips allocation because the packet is not a QOS1+PUBLISH */ +static int s_mqtt5_operation_bind_packet_id_not_valid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .topic = s_topic, + .payload = s_payload_cursor, + }; + + struct aws_mqtt5_operation_publish *new_publish = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_publish->base, &operational_state)); + ASSERT_UINT_EQUALS(0, aws_mqtt5_operation_get_packet_id(&new_publish->base)); + ASSERT_UINT_EQUALS(1, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + aws_mqtt5_operation_release(&new_publish->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_bind_packet_id_not_valid, s_mqtt5_operation_bind_packet_id_not_valid_fn) + +/* test that skips allocation because the packet already has an id bound */ +static int s_mqtt5_operation_bind_packet_id_already_bound_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + struct aws_mqtt5_packet_publish_view publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = s_topic, + .payload = s_payload_cursor, + }; + + struct aws_mqtt5_operation_publish *new_publish = + aws_mqtt5_operation_publish_new(allocator, NULL, &publish_view, NULL); + aws_mqtt5_operation_set_packet_id(&new_publish->base, 2); + + struct aws_mqtt5_client_operational_state operational_state; + aws_mqtt5_client_operational_state_init(&operational_state, allocator, NULL); + + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(&new_publish->base, &operational_state)); + ASSERT_UINT_EQUALS(2, aws_mqtt5_operation_get_packet_id(&new_publish->base)); + ASSERT_UINT_EQUALS(1, operational_state.next_mqtt_packet_id); + + aws_mqtt5_client_operational_state_clean_up(&operational_state); + aws_mqtt5_operation_release(&new_publish->base); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_bind_packet_id_already_bound, s_mqtt5_operation_bind_packet_id_already_bound_fn) + +/* + * A large suit of test infrastructure oriented towards exercising and checking the result of servicing an mqtt5 + * client's operational state + * + * We mock io message acquisition/sending, but no other mocks are needed. We use a dummy + * hand-initialized client to hold additional state (current_state) needed by the operational processing logic. + */ +struct aws_mqtt5_operation_processing_test_context { + struct aws_allocator *allocator; + struct aws_mqtt5_client dummy_client; + struct aws_mqtt5_client_options_storage dummy_client_options; + struct aws_mqtt5_client_vtable vtable; + struct aws_channel_slot dummy_slot; + + struct aws_mqtt5_encoder verification_encoder; + struct aws_array_list output_io_messages; + void *failed_io_message_buffer; + + struct aws_array_list completed_operation_error_codes; +}; + +/* io message mocks */ + +static struct aws_io_message *s_aws_channel_acquire_message_from_pool_success_fn( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data) { + + (void)channel; + (void)message_type; + (void)size_hint; + + struct aws_mqtt5_operation_processing_test_context *test_context = user_data; + struct aws_allocator *allocator = test_context->allocator; + + struct aws_io_message *new_message = aws_mem_calloc(allocator, 1, sizeof(struct aws_io_message)); + new_message->allocator = allocator; + aws_byte_buf_init(&new_message->message_data, allocator, size_hint); + + return new_message; +} + +static struct aws_io_message *s_aws_channel_acquire_message_from_pool_success_small_fn( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data) { + + (void)channel; + (void)message_type; + (void)size_hint; + + struct aws_mqtt5_operation_processing_test_context *test_context = user_data; + struct aws_allocator *allocator = test_context->allocator; + + struct aws_io_message *new_message = aws_mem_calloc(allocator, 1, sizeof(struct aws_io_message)); + new_message->allocator = allocator; + aws_byte_buf_init(&new_message->message_data, allocator, 35); + + return new_message; +} + +static struct aws_io_message *s_aws_channel_acquire_message_from_pool_success_send_failure_fn( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data) { + + (void)channel; + (void)message_type; + + struct aws_mqtt5_operation_processing_test_context *test_context = user_data; + struct aws_allocator *allocator = test_context->allocator; + + struct aws_io_message *new_message = aws_mem_calloc(allocator, 1, sizeof(struct aws_io_message)); + new_message->allocator = allocator; + aws_byte_buf_init(&new_message->message_data, allocator, size_hint); + + test_context->failed_io_message_buffer = new_message->message_data.buffer; + + return new_message; +} + +static struct aws_io_message *s_aws_channel_acquire_message_from_pool_failure_fn( + struct aws_channel *channel, + enum aws_io_message_type message_type, + size_t size_hint, + void *user_data) { + (void)channel; + (void)message_type; + (void)size_hint; + (void)user_data; + + aws_raise_error(AWS_ERROR_INVALID_STATE); + return NULL; +} + +static int s_aws_channel_slot_send_message_success_fn( + struct aws_channel_slot *slot, + struct aws_io_message *message, + enum aws_channel_direction dir, + void *user_data) { + + (void)slot; + (void)dir; + + struct aws_mqtt5_operation_processing_test_context *test_context = user_data; + + aws_array_list_push_back(&test_context->output_io_messages, &message); + + return AWS_OP_SUCCESS; +} + +static int s_aws_channel_slot_send_message_failure_fn( + struct aws_channel_slot *slot, + struct aws_io_message *message, + enum aws_channel_direction dir, + void *user_data) { + + (void)slot; + (void)message; + (void)dir; + (void)user_data; + + return aws_raise_error(AWS_ERROR_INVALID_STATE); +} + +static void s_aws_mqtt5_operation_processing_test_context_init( + struct aws_mqtt5_operation_processing_test_context *test_context, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*test_context); + + test_context->allocator = allocator; + aws_mqtt5_client_operational_state_init( + &test_context->dummy_client.operational_state, allocator, &test_context->dummy_client); + + struct aws_mqtt5_client_options_storage test_storage = { + .ack_timeout_seconds = 0, + }; + + test_context->dummy_client.config = &test_storage; + + struct aws_mqtt5_encoder_options encoder_options = { + .client = &test_context->dummy_client, + }; + + aws_mqtt5_encoder_init(&test_context->dummy_client.encoder, allocator, &encoder_options); + + aws_mqtt5_inbound_topic_alias_resolver_init(&test_context->dummy_client.inbound_topic_alias_resolver, allocator); + test_context->dummy_client.outbound_topic_alias_resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_DISABLED); + + test_context->vtable = *aws_mqtt5_client_get_default_vtable(); + test_context->vtable.aws_channel_acquire_message_from_pool_fn = s_aws_channel_acquire_message_from_pool_success_fn; + test_context->vtable.aws_channel_slot_send_message_fn = s_aws_channel_slot_send_message_success_fn; + test_context->vtable.vtable_user_data = test_context; + + test_context->dummy_client.vtable = &test_context->vtable; + + /* this keeps the operation processing logic from crashing when dereferencing client->slot->channel */ + test_context->dummy_client.slot = &test_context->dummy_slot; + + /* this keeps operation processing tests from failing operations due to a 0 maximum packet size */ + test_context->dummy_client.negotiated_settings.maximum_packet_size_to_server = AWS_MQTT5_MAXIMUM_PACKET_SIZE; + + test_context->dummy_client.negotiated_settings.maximum_qos = AWS_MQTT5_QOS_AT_LEAST_ONCE; + + /* this keeps operation processing tests from crashing when dereferencing config options */ + test_context->dummy_client.config = &test_context->dummy_client_options; + + aws_mutex_init(&test_context->dummy_client.operation_statistics_lock); + + aws_array_list_init_dynamic(&test_context->output_io_messages, allocator, 0, sizeof(struct aws_io_message *)); + + struct aws_mqtt5_encoder_options verification_encoder_options = { + .client = NULL, + }; + + aws_mqtt5_encoder_init(&test_context->verification_encoder, allocator, &verification_encoder_options); + + aws_array_list_init_dynamic(&test_context->completed_operation_error_codes, allocator, 0, sizeof(int)); +} + +static void s_aws_mqtt5_operation_processing_test_context_clean_up( + struct aws_mqtt5_operation_processing_test_context *test_context) { + for (size_t i = 0; i < aws_array_list_length(&test_context->output_io_messages); ++i) { + struct aws_io_message *message = NULL; + aws_array_list_get_at(&test_context->output_io_messages, &message, i); + + aws_byte_buf_clean_up(&message->message_data); + aws_mem_release(message->allocator, message); + } + + aws_array_list_clean_up(&test_context->output_io_messages); + aws_mqtt5_encoder_clean_up(&test_context->verification_encoder); + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&test_context->dummy_client.inbound_topic_alias_resolver); + aws_mqtt5_outbound_topic_alias_resolver_destroy(test_context->dummy_client.outbound_topic_alias_resolver); + + aws_mqtt5_encoder_clean_up(&test_context->dummy_client.encoder); + aws_mqtt5_client_operational_state_clean_up(&test_context->dummy_client.operational_state); + + aws_mutex_clean_up(&test_context->dummy_client.operation_statistics_lock); + + aws_array_list_clean_up(&test_context->completed_operation_error_codes); +} + +static void s_aws_mqtt5_operation_processing_test_context_enqueue_op( + struct aws_mqtt5_operation_processing_test_context *test_context, + struct aws_mqtt5_operation *operation) { + aws_linked_list_push_back(&test_context->dummy_client.operational_state.queued_operations, &operation->node); +} + +/* Test that just runs the operational processing logic with an empty operation queue */ +static int s_mqtt5_operation_processing_nothing_empty_queue_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + for (int32_t i = 0; i <= AWS_MCS_TERMINATED; ++i) { + enum aws_mqtt5_client_state state = i; + + test_context.dummy_client.current_state = state; + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + } + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_nothing_empty_queue, s_mqtt5_operation_processing_nothing_empty_queue_fn) + +static struct aws_mqtt5_operation_subscribe *s_make_simple_subscribe_operation(struct aws_allocator *allocator) { + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + return aws_mqtt5_operation_subscribe_new(allocator, NULL, &subscribe_view, NULL); +} + +/* + * Test that runs the operational processing logic for the MQTT_CONNECT state where the pending operation + * is not valid to be sent by this state (a SUBSCRIBE) + */ +static int s_mqtt5_operation_processing_nothing_mqtt_connect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_MQTT_CONNECT; + + struct aws_mqtt5_operation_subscribe *subscribe_op = s_make_simple_subscribe_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &subscribe_op->base); + + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_nothing_mqtt_connect, s_mqtt5_operation_processing_nothing_mqtt_connect_fn) + +/* + * Test that runs the operational processing logic for the CLEAN_DISCONNECT state where the pending operation + * is not valid to be sent by this state (a SUBSCRIBE) + */ +static int s_mqtt5_operation_processing_nothing_clean_disconnect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CLEAN_DISCONNECT; + + struct aws_mqtt5_operation_subscribe *subscribe_op = s_make_simple_subscribe_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &subscribe_op->base); + + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_nothing_clean_disconnect, + s_mqtt5_operation_processing_nothing_clean_disconnect_fn) + +static struct aws_mqtt5_operation_connect *s_make_simple_connect_operation(struct aws_allocator *allocator) { + struct aws_mqtt5_packet_connect_view connect_view = { + .keep_alive_interval_seconds = 0, + }; + + return aws_mqtt5_operation_connect_new(allocator, &connect_view); +} + +/* + * Test that runs the operational processing logic for the MQTT_CONNECT state with a valid CONNECT operation, but + * the pending_write_completion flag is set and should block any further processing + */ +static int s_mqtt5_operation_processing_nothing_pending_write_completion_mqtt_connect_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_MQTT_CONNECT; + + struct aws_mqtt5_operation_connect *connect_op = s_make_simple_connect_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &connect_op->base); + test_context.dummy_client.operational_state.pending_write_completion = true; + + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_nothing_pending_write_completion_mqtt_connect, + s_mqtt5_operation_processing_nothing_pending_write_completion_mqtt_connect_fn) + +/* + * Test that runs the operational processing logic for the CONNECTED state with a valid SUSBCRIBE operation, but + * the pending_write_completion flag is set and should block any further processing + */ +static int s_mqtt5_operation_processing_nothing_pending_write_completion_connected_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation_subscribe *subscribe_op = s_make_simple_subscribe_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &subscribe_op->base); + test_context.dummy_client.operational_state.pending_write_completion = true; + + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_nothing_pending_write_completion_connected, + s_mqtt5_operation_processing_nothing_pending_write_completion_connected_fn) + +static struct aws_mqtt5_operation_disconnect *s_make_simple_disconnect_operation(struct aws_allocator *allocator) { + struct aws_mqtt5_packet_disconnect_view disconnect_view = { + .reason_code = AWS_MQTT5_DRC_ADMINISTRATIVE_ACTION, + }; + + return aws_mqtt5_operation_disconnect_new(allocator, &disconnect_view, NULL, NULL); +} + +/* + * Test that runs the operational processing logic for the CLEAN_DISCONNECT state with a valid DISCONNECT operation, + * but the pending_write_completion flag is set and should block any further processing + */ +static int s_mqtt5_operation_processing_nothing_pending_write_completion_clean_disconnect_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CLEAN_DISCONNECT; + + struct aws_mqtt5_operation_disconnect *disconnect_op = s_make_simple_disconnect_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &disconnect_op->base); + test_context.dummy_client.operational_state.pending_write_completion = true; + + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_nothing_pending_write_completion_clean_disconnect, + s_mqtt5_operation_processing_nothing_pending_write_completion_clean_disconnect_fn) + +/* + * Test that runs the operational processing logic for the CONNECTED state with a valid SUBSCRIBE operation, + * but the io message acquisition call fails + */ +static int s_mqtt5_operation_processing_failure_message_allocation_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + test_context.vtable.aws_channel_acquire_message_from_pool_fn = s_aws_channel_acquire_message_from_pool_failure_fn; + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation_subscribe *subscribe_op = s_make_simple_subscribe_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &subscribe_op->base); + + ASSERT_FAILS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_failure_message_allocation, + s_mqtt5_operation_processing_failure_message_allocation_fn) + +/* + * Test that runs the operational processing logic for the CONNECTED state with a valid SUBSCRIBE operation, + * but the io message send call fails + */ +static int s_mqtt5_operation_processing_failure_message_send_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + test_context.vtable.aws_channel_slot_send_message_fn = s_aws_channel_slot_send_message_failure_fn; + test_context.vtable.aws_channel_acquire_message_from_pool_fn = + s_aws_channel_acquire_message_from_pool_success_send_failure_fn; + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation_subscribe *subscribe_op = s_make_simple_subscribe_operation(allocator); + s_aws_mqtt5_operation_processing_test_context_enqueue_op(&test_context, &subscribe_op->base); + + ASSERT_FAILS(aws_mqtt5_client_service_operational_state(&test_context.dummy_client.operational_state)); + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.output_io_messages)); + + aws_mem_release(test_context.allocator, test_context.failed_io_message_buffer); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_failure_message_send, s_mqtt5_operation_processing_failure_message_send_fn) + +static int s_verify_operation_list_versus_expected( + struct aws_linked_list *operation_list, + struct aws_mqtt5_operation **expected_operations, + size_t expected_operations_size) { + struct aws_linked_list_node *node = aws_linked_list_begin(operation_list); + for (size_t i = 0; i < expected_operations_size; ++i) { + ASSERT_TRUE(node != aws_linked_list_end(operation_list)); + struct aws_mqtt5_operation *operation = expected_operations[i]; + struct aws_mqtt5_operation *queued_operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); + + ASSERT_PTR_EQUALS(operation, queued_operation); + node = aws_linked_list_next(node); + } + ASSERT_TRUE(node == aws_linked_list_end(operation_list)); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt5_simple_operation_processing_write_test_context { + size_t requested_service_count; + + struct aws_mqtt5_operation **initial_operations; + size_t initial_operations_size; + + struct aws_mqtt5_operation **expected_written; + size_t expected_written_size; + + struct aws_mqtt5_operation **expected_write_completions; + size_t expected_write_completions_size; + + struct aws_mqtt5_operation **expected_pending_acks; + size_t expected_pending_acks_size; + + struct aws_mqtt5_operation **expected_queued; + size_t expected_queued_size; +}; + +/* + * Basic success test with actual output via 1 or more io messages + */ +static int s_do_simple_operation_processing_io_message_write_test( + struct aws_allocator *allocator, + struct aws_mqtt5_operation_processing_test_context *test_context, + struct aws_mqtt5_simple_operation_processing_write_test_context *write_context) { + + /* add operations to the pending queue */ + for (size_t i = 0; i < write_context->initial_operations_size; ++i) { + s_aws_mqtt5_operation_processing_test_context_enqueue_op(test_context, write_context->initial_operations[i]); + } + + /* service the operational state the requested number of times. reset pending write completion between calls */ + for (size_t i = 0; i < write_context->requested_service_count; ++i) { + ASSERT_SUCCESS(aws_mqtt5_client_service_operational_state(&test_context->dummy_client.operational_state)); + test_context->dummy_client.operational_state.pending_write_completion = false; + } + + /* # of outputted io messages should match the request number of service calls */ + ASSERT_UINT_EQUALS( + write_context->requested_service_count, aws_array_list_length(&test_context->output_io_messages)); + + /* encode all of the messages that we expect to have sent as a result of processing into one large buffer */ + struct aws_byte_buf verification_buffer; + aws_byte_buf_init(&verification_buffer, allocator, 4096); + + for (size_t i = 0; i < write_context->expected_written_size; ++i) { + struct aws_mqtt5_operation *operation = write_context->expected_written[i]; + + aws_mqtt5_encoder_append_packet_encoding( + &test_context->verification_encoder, operation->packet_type, operation->packet_view); + aws_mqtt5_encoder_encode_to_buffer(&test_context->verification_encoder, &verification_buffer); + } + + /* concatenate all of the sent io message buffers into a single buffer */ + struct aws_byte_buf concatenated_message_buffers; + aws_byte_buf_init(&concatenated_message_buffers, allocator, 4096); + + for (size_t i = 0; i < write_context->requested_service_count; ++i) { + struct aws_io_message *message = NULL; + aws_array_list_get_at(&test_context->output_io_messages, &message, i); + + struct aws_byte_cursor message_cursor = aws_byte_cursor_from_buf(&message->message_data); + aws_byte_buf_append_dynamic(&concatenated_message_buffers, &message_cursor); + } + + /* + * verify that what we sent out (in 1 or more io messages) matches the sequential encoding of the operations + * that we expected to go out + */ + ASSERT_BIN_ARRAYS_EQUALS( + verification_buffer.buffer, + verification_buffer.len, + concatenated_message_buffers.buffer, + concatenated_message_buffers.len); + + aws_byte_buf_clean_up(&verification_buffer); + aws_byte_buf_clean_up(&concatenated_message_buffers); + + /* verify that operations we expected to *NOT* be processed are still in the queue in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context->dummy_client.operational_state.queued_operations, + write_context->expected_queued, + write_context->expected_queued_size)); + + /* verify that the operations we expected to be placed into the write completion list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context->dummy_client.operational_state.write_completion_operations, + write_context->expected_write_completions, + write_context->expected_write_completions_size)); + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context->dummy_client.operational_state.unacked_operations, + write_context->expected_pending_acks, + write_context->expected_pending_acks_size)); + + ASSERT_UINT_EQUALS( + write_context->expected_pending_acks_size, + aws_hash_table_get_entry_count(&test_context->dummy_client.operational_state.unacked_operations_table)); + + /* verify that every operation that should be in pending ack has a packet id and is in the pending ack table */ + for (size_t i = 0; i < write_context->expected_pending_acks_size; ++i) { + struct aws_mqtt5_operation *operation = write_context->expected_pending_acks[i]; + + uint16_t packet_id = aws_mqtt5_operation_get_packet_id(operation); + ASSERT_TRUE(packet_id != 0); + + struct aws_hash_element *elem = NULL; + aws_hash_table_find(&test_context->dummy_client.operational_state.unacked_operations_table, &packet_id, &elem); + + ASSERT_NOT_NULL(elem); + ASSERT_NOT_NULL(elem->value); + + struct aws_mqtt5_operation *table_operation = elem->value; + ASSERT_PTR_EQUALS(operation, table_operation); + } + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_operation_processing_something_mqtt_connect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_MQTT_CONNECT; + + struct aws_mqtt5_operation *connect_op = &s_make_simple_connect_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe_op = &s_make_simple_subscribe_operation(allocator)->base; + + struct aws_mqtt5_operation *initial_operations[] = {connect_op, subscribe_op}; + struct aws_mqtt5_operation *expected_written[] = {connect_op}; + struct aws_mqtt5_operation *expected_write_completions[] = {connect_op}; + struct aws_mqtt5_operation *expected_queued[] = {subscribe_op}; + + struct aws_mqtt5_simple_operation_processing_write_test_context write_context = { + .requested_service_count = 1, + .initial_operations = initial_operations, + .initial_operations_size = AWS_ARRAY_SIZE(initial_operations), + .expected_written = expected_written, + .expected_written_size = AWS_ARRAY_SIZE(expected_written), + .expected_write_completions = expected_write_completions, + .expected_write_completions_size = AWS_ARRAY_SIZE(expected_write_completions), + .expected_queued = expected_queued, + .expected_queued_size = AWS_ARRAY_SIZE(expected_queued), + }; + + ASSERT_SUCCESS(s_do_simple_operation_processing_io_message_write_test(allocator, &test_context, &write_context)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_something_mqtt_connect, s_mqtt5_operation_processing_something_mqtt_connect_fn) + +static int s_mqtt5_operation_processing_something_clean_disconnect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CLEAN_DISCONNECT; + + struct aws_mqtt5_operation *disconnect_op = &s_make_simple_disconnect_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe_op = &s_make_simple_subscribe_operation(allocator)->base; + + struct aws_mqtt5_operation *initial_operations[] = {disconnect_op, subscribe_op}; + struct aws_mqtt5_operation *expected_written[] = {disconnect_op}; + struct aws_mqtt5_operation *expected_write_completions[] = {disconnect_op}; + struct aws_mqtt5_operation *expected_queued[] = {subscribe_op}; + + struct aws_mqtt5_simple_operation_processing_write_test_context write_context = { + .requested_service_count = 1, + .initial_operations = initial_operations, + .initial_operations_size = AWS_ARRAY_SIZE(initial_operations), + .expected_written = expected_written, + .expected_written_size = AWS_ARRAY_SIZE(expected_written), + .expected_write_completions = expected_write_completions, + .expected_write_completions_size = AWS_ARRAY_SIZE(expected_write_completions), + .expected_queued = expected_queued, + .expected_queued_size = AWS_ARRAY_SIZE(expected_queued), + }; + + ASSERT_SUCCESS(s_do_simple_operation_processing_io_message_write_test(allocator, &test_context, &write_context)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_something_clean_disconnect, + s_mqtt5_operation_processing_something_clean_disconnect_fn) + +static int s_mqtt5_operation_processing_something_connected_multi_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation *disconnect_op = &s_make_simple_disconnect_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_simple_subscribe_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_simple_subscribe_operation(allocator)->base; + + struct aws_mqtt5_operation *initial_operations[] = {subscribe1_op, subscribe2_op, disconnect_op}; + struct aws_mqtt5_operation *expected_written[] = {subscribe1_op, subscribe2_op, disconnect_op}; + struct aws_mqtt5_operation *expected_write_completions[] = {disconnect_op}; + struct aws_mqtt5_operation *expected_pending_acks[] = {subscribe1_op, subscribe2_op}; + + struct aws_mqtt5_simple_operation_processing_write_test_context write_context = { + .requested_service_count = 1, + .initial_operations = initial_operations, + .initial_operations_size = AWS_ARRAY_SIZE(initial_operations), + .expected_written = expected_written, + .expected_written_size = AWS_ARRAY_SIZE(expected_written), + .expected_write_completions = expected_write_completions, + .expected_write_completions_size = AWS_ARRAY_SIZE(expected_write_completions), + .expected_pending_acks = expected_pending_acks, + .expected_pending_acks_size = AWS_ARRAY_SIZE(expected_pending_acks), + }; + + ASSERT_SUCCESS(s_do_simple_operation_processing_io_message_write_test(allocator, &test_context, &write_context)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_something_connected_multi, + s_mqtt5_operation_processing_something_connected_multi_fn) + +static int s_mqtt5_operation_processing_something_connected_overflow_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + test_context.vtable.aws_channel_acquire_message_from_pool_fn = + s_aws_channel_acquire_message_from_pool_success_small_fn; + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation *disconnect_op = &s_make_simple_disconnect_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_simple_subscribe_operation(allocator)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_simple_subscribe_operation(allocator)->base; + + struct aws_mqtt5_operation *initial_operations[] = {subscribe1_op, subscribe2_op, disconnect_op}; + struct aws_mqtt5_operation *expected_written[] = {subscribe1_op, subscribe2_op, disconnect_op}; + struct aws_mqtt5_operation *expected_write_completions[] = {disconnect_op}; + struct aws_mqtt5_operation *expected_pending_acks[] = {subscribe1_op, subscribe2_op}; + + struct aws_mqtt5_simple_operation_processing_write_test_context write_context = { + .requested_service_count = 3, + .initial_operations = initial_operations, + .initial_operations_size = AWS_ARRAY_SIZE(initial_operations), + .expected_written = expected_written, + .expected_written_size = AWS_ARRAY_SIZE(expected_written), + .expected_write_completions = expected_write_completions, + .expected_write_completions_size = AWS_ARRAY_SIZE(expected_write_completions), + .expected_pending_acks = expected_pending_acks, + .expected_pending_acks_size = AWS_ARRAY_SIZE(expected_pending_acks), + }; + + ASSERT_SUCCESS(s_do_simple_operation_processing_io_message_write_test(allocator, &test_context, &write_context)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_something_connected_overflow, + s_mqtt5_operation_processing_something_connected_overflow_fn) + +void s_on_subscribe_operation_complete( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + (void)suback; + + struct aws_mqtt5_operation_processing_test_context *test_context = complete_ctx; + + aws_array_list_push_back(&test_context->completed_operation_error_codes, &error_code); +} + +static struct aws_mqtt5_operation_subscribe *s_make_completable_subscribe_operation( + struct aws_allocator *allocator, + struct aws_mqtt5_operation_processing_test_context *test_context) { + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = s_subscriptions, + .subscription_count = AWS_ARRAY_SIZE(s_subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .completion_callback = s_on_subscribe_operation_complete, + .completion_user_data = test_context, + }; + + return aws_mqtt5_operation_subscribe_new(allocator, NULL, &subscribe_view, &completion_options); +} + +void s_on_publish_operation_complete( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + (void)packet_type; + (void)packet; + + struct aws_mqtt5_operation_processing_test_context *test_context = complete_ctx; + + aws_array_list_push_back(&test_context->completed_operation_error_codes, &error_code); +} + +static struct aws_mqtt5_operation_publish *s_make_completable_publish_operation( + struct aws_allocator *allocator, + enum aws_mqtt5_qos qos, + struct aws_mqtt5_operation_processing_test_context *test_context) { + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_c_str(PUBLISH_PAYLOAD); + + struct aws_mqtt5_packet_publish_view publish_options = { + .payload = payload_cursor, + .qos = qos, + .topic = aws_byte_cursor_from_c_str(PUBLISH_TOPIC), + }; + + struct aws_mqtt5_publish_completion_options completion_options = { + .completion_callback = s_on_publish_operation_complete, + .completion_user_data = test_context, + }; + + return aws_mqtt5_operation_publish_new(allocator, NULL, &publish_options, &completion_options); +} + +static int s_setup_unacked_operation( + struct aws_mqtt5_client_operational_state *operational_state, + struct aws_mqtt5_operation *operation) { + ASSERT_SUCCESS(aws_mqtt5_operation_bind_packet_id(operation, operational_state)); + aws_linked_list_push_back(&operational_state->unacked_operations, &operation->node); + + aws_mqtt5_packet_id_t *packet_id_ptr = aws_mqtt5_operation_get_packet_id_address(operation); + aws_hash_table_put(&operational_state->unacked_operations_table, packet_id_ptr, operation, NULL); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_operation_processing_disconnect_fail_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe3_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE, &test_context)->base; + + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &subscribe3_op->node); + aws_linked_list_push_back( + &test_context.dummy_client.operational_state.write_completion_operations, &publish2_op->node); + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + aws_mqtt5_client_on_disconnection_update_operational_state(&test_context.dummy_client); + + /* Should have been failed: publish2_op, subscribe1_op, subscribe2_op, subscribe3_op */ + /* Should still be in unacked list: publish1_op */ + ASSERT_UINT_EQUALS(4, aws_array_list_length(&test_context.completed_operation_error_codes)); + for (size_t i = 0; i < aws_array_list_length(&test_context.completed_operation_error_codes); ++i) { + int error_code = 0; + aws_array_list_get_at(&test_context.completed_operation_error_codes, &error_code, i); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, error_code); + } + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.queued_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + struct aws_mqtt5_operation *expected_post_disconnect_pending_acks[] = {publish1_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.unacked_operations, + expected_post_disconnect_pending_acks, + AWS_ARRAY_SIZE(expected_post_disconnect_pending_acks))); + + ASSERT_UINT_EQUALS( + AWS_ARRAY_SIZE(expected_post_disconnect_pending_acks), + aws_mqtt5_linked_list_length(&test_context.dummy_client.operational_state.unacked_operations)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_disconnect_fail_all, s_mqtt5_operation_processing_disconnect_fail_all_fn) + +static int s_mqtt5_operation_processing_disconnect_fail_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe3_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish3_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish4_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &subscribe3_op->node); + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &publish3_op->node); + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &publish4_op->node); + + aws_linked_list_push_back( + &test_context.dummy_client.operational_state.write_completion_operations, &publish2_op->node); + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + aws_mqtt5_client_on_disconnection_update_operational_state(&test_context.dummy_client); + + /* Should have been failed: publish2_op, publish3_op */ + ASSERT_UINT_EQUALS(2, aws_array_list_length(&test_context.completed_operation_error_codes)); + for (size_t i = 0; i < aws_array_list_length(&test_context.completed_operation_error_codes); ++i) { + int error_code = 0; + aws_array_list_get_at(&test_context.completed_operation_error_codes, &error_code, i); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, error_code); + } + + /* Should still be in pending queue: subscribe3_op, publish4_op */ + struct aws_mqtt5_operation *expected_post_disconnect_queued_operations[] = {subscribe3_op, publish4_op}; + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_post_disconnect_queued_operations, + AWS_ARRAY_SIZE(expected_post_disconnect_queued_operations))); + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + struct aws_mqtt5_operation *expected_post_disconnect_pending_acks[] = {subscribe1_op, publish1_op, subscribe2_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + /* Should still be in unacked list: subscribe1_op, publish1_op, subscribe2_op */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.unacked_operations, + expected_post_disconnect_pending_acks, + AWS_ARRAY_SIZE(expected_post_disconnect_pending_acks))); + + /* verify pending-requeue subscribes have had their packet ids erased */ + ASSERT_INT_EQUALS(0, aws_mqtt5_operation_get_packet_id(subscribe1_op)); + ASSERT_INT_EQUALS(0, aws_mqtt5_operation_get_packet_id(subscribe2_op)); + + /* interrupted qos1 publish should be marked as duplicate and still have a packet id */ + const struct aws_mqtt5_packet_publish_view *publish_view = publish1_op->packet_view; + ASSERT_TRUE(publish_view->duplicate); + ASSERT_TRUE(publish_view->packet_id != 0); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_operation_processing_disconnect_fail_qos0, s_mqtt5_operation_processing_disconnect_fail_qos0_fn) + +static int s_mqtt5_operation_processing_disconnect_fail_non_qos1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe3_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish3_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_MOST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish4_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &subscribe3_op->node); + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &publish3_op->node); + aws_linked_list_push_back(&test_context.dummy_client.operational_state.queued_operations, &publish4_op->node); + + aws_linked_list_push_back( + &test_context.dummy_client.operational_state.write_completion_operations, &publish2_op->node); + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + aws_mqtt5_client_on_disconnection_update_operational_state(&test_context.dummy_client); + + /* Should have been failed: publish2_op, publish3_op, subscribe1_op, subscribe2_op, subscribe3_op */ + ASSERT_UINT_EQUALS(5, aws_array_list_length(&test_context.completed_operation_error_codes)); + for (size_t i = 0; i < aws_array_list_length(&test_context.completed_operation_error_codes); ++i) { + int error_code = 0; + aws_array_list_get_at(&test_context.completed_operation_error_codes, &error_code, i); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, error_code); + } + + /* Should still be in pending queue: publish4_op */ + struct aws_mqtt5_operation *expected_post_disconnect_queued_operations[] = {publish4_op}; + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_post_disconnect_queued_operations, + AWS_ARRAY_SIZE(expected_post_disconnect_queued_operations))); + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + struct aws_mqtt5_operation *expected_post_disconnect_pending_acks[] = {publish1_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + /* Should still be in unacked list: publish1_op */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.unacked_operations, + expected_post_disconnect_pending_acks, + AWS_ARRAY_SIZE(expected_post_disconnect_pending_acks))); + + /* interrupted qos1 publish should be marked as duplicate and still have a packet id */ + const struct aws_mqtt5_packet_publish_view *publish_view = publish1_op->packet_view; + ASSERT_TRUE(publish_view->duplicate); + ASSERT_TRUE(publish_view->packet_id != 0); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_disconnect_fail_non_qos1, + s_mqtt5_operation_processing_disconnect_fail_non_qos1_fn) + +static int s_mqtt5_operation_processing_reconnect_rejoin_session_fail_all_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish2_op)); + + /* we now clear this on disconnect, which we aren't simulating here, so do it manually */ + aws_hash_table_clear(&test_context.dummy_client.operational_state.unacked_operations_table); + + test_context.dummy_client.negotiated_settings.rejoined_session = true; + + aws_mqtt5_client_on_connection_update_operational_state(&test_context.dummy_client); + + /* Nothing should have failed */ + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.completed_operation_error_codes)); + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.unacked_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + struct aws_mqtt5_operation *expected_queued_operations[] = {publish1_op, publish2_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_queued_operations, + AWS_ARRAY_SIZE(expected_queued_operations))); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_reconnect_rejoin_session_fail_all, + s_mqtt5_operation_processing_reconnect_rejoin_session_fail_all_fn) + +static int s_mqtt5_operation_processing_reconnect_rejoin_session_fail_qos0_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish2_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + /* we now clear this on disconnect, which we aren't simulating here, so do it manually */ + aws_hash_table_clear(&test_context.dummy_client.operational_state.unacked_operations_table); + + test_context.dummy_client.negotiated_settings.rejoined_session = true; + + aws_mqtt5_client_on_connection_update_operational_state(&test_context.dummy_client); + + /* Nothing should have failed */ + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.completed_operation_error_codes)); + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.unacked_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + /* The only place where order gets modified: the resubmitted publishes should be strictly ahead of everything else + */ + struct aws_mqtt5_operation *expected_queued_operations[] = {publish1_op, publish2_op, subscribe1_op, subscribe2_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_queued_operations, + AWS_ARRAY_SIZE(expected_queued_operations))); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_reconnect_rejoin_session_fail_qos0, + s_mqtt5_operation_processing_reconnect_rejoin_session_fail_qos0_fn) + +static int s_mqtt5_operation_processing_reconnect_no_session_fail_all_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish2_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + /* we now clear this on disconnect, which we aren't simulating here, so do it manually */ + aws_hash_table_clear(&test_context.dummy_client.operational_state.unacked_operations_table); + + test_context.dummy_client.negotiated_settings.rejoined_session = false; + + aws_mqtt5_client_on_connection_update_operational_state(&test_context.dummy_client); + + /* Everything should have failed */ + ASSERT_UINT_EQUALS(4, aws_array_list_length(&test_context.completed_operation_error_codes)); + for (size_t i = 0; i < aws_array_list_length(&test_context.completed_operation_error_codes); ++i) { + int error_code = 0; + aws_array_list_get_at(&test_context.completed_operation_error_codes, &error_code, i); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, error_code); + } + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.unacked_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.queued_operations)); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_reconnect_no_session_fail_all, + s_mqtt5_operation_processing_reconnect_no_session_fail_all_fn) + +static int s_mqtt5_operation_processing_reconnect_no_session_fail_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish2_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + /* we now clear this on disconnect, which we aren't simulating here, so do it manually */ + aws_hash_table_clear(&test_context.dummy_client.operational_state.unacked_operations_table); + + test_context.dummy_client.negotiated_settings.rejoined_session = false; + + aws_mqtt5_client_on_connection_update_operational_state(&test_context.dummy_client); + + /* Nothing should have failed */ + ASSERT_UINT_EQUALS(0, aws_array_list_length(&test_context.completed_operation_error_codes)); + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.unacked_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + /* Unlike the session case, operation order should be unchanged */ + struct aws_mqtt5_operation *expected_queued_operations[] = {subscribe1_op, publish1_op, publish2_op, subscribe2_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_queued_operations, + AWS_ARRAY_SIZE(expected_queued_operations))); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_reconnect_no_session_fail_qos0, + s_mqtt5_operation_processing_reconnect_no_session_fail_qos0_fn) + +static int s_mqtt5_operation_processing_reconnect_no_session_fail_non_qos1_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_operation_processing_test_context test_context; + s_aws_mqtt5_operation_processing_test_context_init(&test_context, allocator); + + test_context.dummy_client.current_state = AWS_MCS_CONNECTED; + + struct aws_mqtt5_client_options_storage *config = + (struct aws_mqtt5_client_options_storage *)test_context.dummy_client.config; + config->offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_operation *publish1_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *publish2_op = + &s_make_completable_publish_operation(allocator, AWS_MQTT5_QOS_AT_LEAST_ONCE, &test_context)->base; + struct aws_mqtt5_operation *subscribe1_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + struct aws_mqtt5_operation *subscribe2_op = &s_make_completable_subscribe_operation(allocator, &test_context)->base; + + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish1_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, publish2_op)); + ASSERT_SUCCESS(s_setup_unacked_operation(&test_context.dummy_client.operational_state, subscribe2_op)); + + /* we now clear this on disconnect, which we aren't simulating here, so do it manually */ + aws_hash_table_clear(&test_context.dummy_client.operational_state.unacked_operations_table); + + test_context.dummy_client.negotiated_settings.rejoined_session = false; + + aws_mqtt5_client_on_connection_update_operational_state(&test_context.dummy_client); + + /* The subscribes should have failed */ + ASSERT_UINT_EQUALS(2, aws_array_list_length(&test_context.completed_operation_error_codes)); + for (size_t i = 0; i < aws_array_list_length(&test_context.completed_operation_error_codes); ++i) { + int error_code = 0; + aws_array_list_get_at(&test_context.completed_operation_error_codes, &error_code, i); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, error_code); + } + + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.unacked_operations)); + ASSERT_TRUE(aws_linked_list_empty(&test_context.dummy_client.operational_state.write_completion_operations)); + + /* Only the qos1 publishes should remain */ + struct aws_mqtt5_operation *expected_queued_operations[] = {publish1_op, publish2_op}; + + /* verify that the operations we expected to be in the unacked operation list are there, in order */ + ASSERT_SUCCESS(s_verify_operation_list_versus_expected( + &test_context.dummy_client.operational_state.queued_operations, + expected_queued_operations, + AWS_ARRAY_SIZE(expected_queued_operations))); + + s_aws_mqtt5_operation_processing_test_context_clean_up(&test_context); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_processing_reconnect_no_session_fail_non_qos1, + s_mqtt5_operation_processing_reconnect_no_session_fail_non_qos1_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_host_name, "derp.com"); + +static void s_dummy_lifecycle_handler(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +static void s_dummy_publish_received_(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static int s_mqtt5_client_options_defaults_set_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_event_loop_group *elg = aws_event_loop_group_new_default(allocator, 1, NULL); + + struct aws_host_resolver_default_options hr_options = { + .el_group = elg, + .max_entries = 1, + }; + struct aws_host_resolver *hr = aws_host_resolver_new_default(allocator, &hr_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = elg, + .host_resolver = hr, + }; + struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + struct aws_mqtt5_packet_connect_view connect_options; + AWS_ZERO_STRUCT(connect_options); + + struct aws_mqtt5_client_options client_options = { + .host_name = aws_byte_cursor_from_string(s_host_name), + .port = 1883, + .bootstrap = bootstrap, + .lifecycle_event_handler = s_dummy_lifecycle_handler, + .publish_received_handler = s_dummy_publish_received_, + .connect_options = &connect_options, + }; + + struct aws_mqtt5_client_options_storage *client_options_storage = + aws_mqtt5_client_options_storage_new(allocator, &client_options); + + ASSERT_INT_EQUALS( + AWS_MQTT5_DEFAULT_SOCKET_CONNECT_TIMEOUT_MS, client_options_storage->socket_options.connect_timeout_ms); + ASSERT_INT_EQUALS(AWS_MQTT5_CLIENT_DEFAULT_MIN_RECONNECT_DELAY_MS, client_options_storage->min_reconnect_delay_ms); + ASSERT_INT_EQUALS(AWS_MQTT5_CLIENT_DEFAULT_MAX_RECONNECT_DELAY_MS, client_options_storage->max_reconnect_delay_ms); + ASSERT_INT_EQUALS( + AWS_MQTT5_CLIENT_DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS, + client_options_storage->min_connected_time_to_reset_reconnect_delay_ms); + ASSERT_INT_EQUALS(AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS, client_options_storage->ping_timeout_ms); + ASSERT_INT_EQUALS(AWS_MQTT5_CLIENT_DEFAULT_CONNACK_TIMEOUT_MS, client_options_storage->connack_timeout_ms); + ASSERT_INT_EQUALS(AWS_MQTT5_CLIENT_DEFAULT_OPERATION_TIMEOUNT_SECONDS, client_options_storage->ack_timeout_seconds); + + aws_mqtt5_client_options_storage_destroy(client_options_storage); + aws_client_bootstrap_release(bootstrap); + aws_host_resolver_release(hr); + aws_event_loop_group_release(elg); + + aws_thread_join_all_managed(); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_options_defaults_set, s_mqtt5_client_options_defaults_set_fn) diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c new file mode 100644 index 00000000..dd4db628 --- /dev/null +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -0,0 +1,1327 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static uint8_t s_server_reference[] = "derp.com"; +static struct aws_byte_cursor s_server_reference_cursor = { + .ptr = s_server_reference, + .len = AWS_ARRAY_SIZE(s_server_reference) - 1, +}; + +static uint8_t s_binary_data[] = "binary data"; +static struct aws_byte_cursor s_binary_data_cursor = { + .ptr = s_binary_data, + .len = AWS_ARRAY_SIZE(s_binary_data) - 1, +}; + +static uint8_t s_too_long_for_uint16[UINT16_MAX + 1]; +static struct aws_byte_cursor s_too_long_for_uint16_cursor = { + .ptr = s_too_long_for_uint16, + .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), +}; + +static uint8_t s_user_prop_name[] = "name"; +static uint8_t s_user_prop_value[] = "value"; + +static const struct aws_mqtt5_user_property s_bad_user_properties_name[] = { + { + .name = + { + .ptr = s_too_long_for_uint16, + .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), + }, + .value = + { + .ptr = (uint8_t *)s_user_prop_value, + .len = AWS_ARRAY_SIZE(s_user_prop_value) - 1, + }, + }, +}; + +static const struct aws_mqtt5_user_property s_bad_user_properties_value[] = { + { + .name = + { + .ptr = s_user_prop_name, + .len = AWS_ARRAY_SIZE(s_user_prop_name) - 1, + }, + .value = + { + .ptr = s_too_long_for_uint16, + .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), + }, + }, +}; + +static struct aws_mqtt5_user_property s_bad_user_properties_too_many[AWS_MQTT5_CLIENT_MAXIMUM_USER_PROPERTIES + 1]; + +/* + * rather than just checking the bad view, we do a side-by-side before-after view validation as well to give more + * confidence that the failure is coming from what we're actually trying to check + */ +#define AWS_VALIDATION_FAILURE_PREFIX(packet_type, failure_reason, view_name, mutate_function) \ + static int s_mqtt5_operation_##packet_type##_validation_failure_##failure_reason##_fn( \ + struct aws_allocator *allocator, void *ctx) { \ + (void)ctx; \ + \ + struct aws_mqtt5_packet_##packet_type##_view good_view = view_name; \ + struct aws_mqtt5_packet_##packet_type##_view bad_view = view_name; \ + (*mutate_function)(&bad_view); \ + ASSERT_SUCCESS(aws_mqtt5_packet_##packet_type##_view_validate(&good_view)); \ + ASSERT_FAILS(aws_mqtt5_packet_##packet_type##_view_validate(&bad_view)); + +#define AWS_VALIDATION_FAILURE_SUFFIX(packet_type, failure_reason) \ + return AWS_OP_SUCCESS; \ + } \ + \ + AWS_TEST_CASE( \ + mqtt5_operation_##packet_type##_validation_failure_##failure_reason, \ + s_mqtt5_operation_##packet_type##_validation_failure_##failure_reason##_fn) + +#define AWS_VALIDATION_FAILURE_TEST4(packet_type, failure_reason, view_name, mutate_function) \ + AWS_VALIDATION_FAILURE_PREFIX(packet_type, failure_reason, view_name, mutate_function) \ + struct aws_mqtt5_operation_##packet_type *operation = \ + aws_mqtt5_operation_##packet_type##_new(allocator, &good_view, NULL, NULL); \ + ASSERT_NOT_NULL(operation); \ + aws_mqtt5_operation_release(&operation->base); \ + ASSERT_NULL(aws_mqtt5_operation_##packet_type##_new(allocator, &bad_view, NULL, NULL)); \ + AWS_VALIDATION_FAILURE_SUFFIX(packet_type, failure_reason) + +#define AWS_VALIDATION_FAILURE_TEST3(packet_type, failure_reason, view_name, mutate_function) \ + AWS_VALIDATION_FAILURE_PREFIX(packet_type, failure_reason, view_name, mutate_function) \ + ASSERT_NULL(aws_mqtt5_operation_##packet_type##_new(allocator, NULL, &bad_view, NULL)); \ + AWS_VALIDATION_FAILURE_SUFFIX(packet_type, failure_reason) + +#define AWS_VALIDATION_FAILURE_TEST2(packet_type, failure_reason, view_name, mutate_function) \ + AWS_VALIDATION_FAILURE_PREFIX(packet_type, failure_reason, view_name, mutate_function) \ + ASSERT_NULL(aws_mqtt5_operation_##packet_type##_new(allocator, &bad_view)); \ + AWS_VALIDATION_FAILURE_SUFFIX(packet_type, failure_reason) + +#define AWS_IOT_CORE_VALIDATION_FAILURE(packet_type, failure_reason, view_name, mutate_function) \ + static int s_mqtt5_operation_##packet_type##_validation_failure_##failure_reason##_fn( \ + struct aws_allocator *allocator, void *ctx) { \ + (void)ctx; \ + \ + struct aws_mqtt5_packet_##packet_type##_view good_view = view_name; \ + struct aws_mqtt5_packet_##packet_type##_view bad_view = view_name; \ + (*mutate_function)(&bad_view); \ + ASSERT_SUCCESS(aws_mqtt5_packet_##packet_type##_view_validate(&good_view)); \ + ASSERT_SUCCESS(aws_mqtt5_packet_##packet_type##_view_validate(&bad_view)); \ + \ + struct aws_mqtt5_client dummy_client; \ + AWS_ZERO_STRUCT(dummy_client); \ + struct aws_mqtt5_client_options_storage client_options_storage; \ + AWS_ZERO_STRUCT(client_options_storage); \ + client_options_storage.extended_validation_and_flow_control_options = AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS; \ + dummy_client.config = &client_options_storage; \ + \ + ASSERT_NULL(aws_mqtt5_operation_##packet_type##_new(allocator, &dummy_client, &bad_view, NULL)); \ + return AWS_OP_SUCCESS; \ + } \ + \ + AWS_TEST_CASE( \ + mqtt5_operation_##packet_type##_validation_failure_##failure_reason, \ + s_mqtt5_operation_##packet_type##_validation_failure_##failure_reason##_fn) + +static struct aws_mqtt5_packet_disconnect_view s_good_disconnect_view; + +static void s_make_server_reference_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->server_reference = &s_server_reference_cursor; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + server_reference, + s_good_disconnect_view, + s_make_server_reference_disconnect_view) + +static void s_make_bad_reason_code_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->reason_code = -1; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + bad_reason_code, + s_good_disconnect_view, + s_make_bad_reason_code_disconnect_view) + +static void s_make_reason_string_too_long_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->reason_string = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + reason_string_too_long, + s_good_disconnect_view, + s_make_reason_string_too_long_disconnect_view) + +static void s_make_user_properties_name_too_long_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); + view->user_properties = s_bad_user_properties_name; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + user_properties_name_too_long, + s_good_disconnect_view, + s_make_user_properties_name_too_long_disconnect_view) + +static void s_make_user_properties_value_too_long_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); + view->user_properties = s_bad_user_properties_value; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + user_properties_value_too_long, + s_good_disconnect_view, + s_make_user_properties_value_too_long_disconnect_view) + +static void s_make_user_properties_too_many_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); + view->user_properties = s_bad_user_properties_too_many; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + user_properties_too_many, + s_good_disconnect_view, + s_make_user_properties_too_many_disconnect_view) + +static struct aws_mqtt5_packet_connect_view s_good_connect_view; + +static void s_make_client_id_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->client_id.ptr = s_too_long_for_uint16; + view->client_id.len = AWS_ARRAY_SIZE(s_too_long_for_uint16); +} + +AWS_VALIDATION_FAILURE_TEST2(connect, client_id_too_long, s_good_connect_view, s_make_client_id_too_long_connect_view) + +static void s_make_username_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->username = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST2(connect, username_too_long, s_good_connect_view, s_make_username_too_long_connect_view) + +static void s_make_password_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->password = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST2(connect, password_too_long, s_good_connect_view, s_make_password_too_long_connect_view) + +static const uint16_t s_zero_receive_maximum = 0; +static void s_make_receive_maximum_zero_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->receive_maximum = &s_zero_receive_maximum; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + receive_maximum_zero, + s_good_connect_view, + s_make_receive_maximum_zero_connect_view) + +static const uint32_t s_maximum_packet_size_zero = 0; +static void s_make_maximum_packet_size_zero_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->maximum_packet_size_bytes = &s_maximum_packet_size_zero; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + maximum_packet_size_zero, + s_good_connect_view, + s_make_maximum_packet_size_zero_connect_view) + +static void s_make_auth_method_unsupported_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->authentication_method = &s_binary_data_cursor; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + auth_method_unsupported, + s_good_connect_view, + s_make_auth_method_unsupported_connect_view) + +static void s_make_auth_data_unsupported_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->authentication_data = &s_binary_data_cursor; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + auth_data_unsupported, + s_good_connect_view, + s_make_auth_data_unsupported_connect_view) + +static uint8_t s_bad_boolean = 2; +static void s_make_request_problem_information_invalid_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->request_problem_information = &s_bad_boolean; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + request_problem_information_invalid, + s_good_connect_view, + s_make_request_problem_information_invalid_connect_view) + +static void s_make_request_response_information_invalid_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->request_response_information = &s_bad_boolean; +}; + +AWS_VALIDATION_FAILURE_TEST2( + connect, + request_response_information_invalid, + s_good_connect_view, + s_make_request_response_information_invalid_connect_view) + +static void s_make_user_properties_name_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); + view->user_properties = s_bad_user_properties_name; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + user_properties_name_too_long, + s_good_connect_view, + s_make_user_properties_name_too_long_connect_view) + +static void s_make_user_properties_value_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); + view->user_properties = s_bad_user_properties_value; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + user_properties_value_too_long, + s_good_connect_view, + s_make_user_properties_value_too_long_connect_view) + +static void s_make_user_properties_too_many_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); + view->user_properties = s_bad_user_properties_too_many; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + user_properties_too_many, + s_good_connect_view, + s_make_user_properties_too_many_connect_view) + +static uint8_t s_good_topic[] = "hello/world"; + +/* no-topic and no-topic-alias is invalid */ +static struct aws_mqtt5_packet_publish_view s_good_will_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, +}; + +static struct aws_mqtt5_packet_connect_view s_good_will_connect_view = { + .will = &s_good_will_publish_view, +}; + +static struct aws_mqtt5_packet_publish_view s_will_invalid_publish_view; + +static void s_make_will_invalid_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->will = &s_will_invalid_publish_view; +} + +AWS_VALIDATION_FAILURE_TEST2(connect, will_invalid, s_good_will_connect_view, s_make_will_invalid_connect_view) + +static struct aws_mqtt5_packet_publish_view s_will_payload_too_long_publish_view = { + .topic = + { + .ptr = s_user_prop_name, + .len = AWS_ARRAY_SIZE(s_user_prop_name) - 1, + }, + .payload = { + .ptr = s_too_long_for_uint16, + .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), + }}; + +static void s_make_will_payload_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->will = &s_will_payload_too_long_publish_view; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + will_payload_too_long, + s_good_will_connect_view, + s_make_will_payload_too_long_connect_view) + +static struct aws_mqtt5_subscription_view s_good_subscription[] = { + { + .topic_filter = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + .retain_as_published = false, + }, +}; + +static struct aws_mqtt5_packet_subscribe_view s_good_subscribe_view = { + .subscriptions = s_good_subscription, + .subscription_count = AWS_ARRAY_SIZE(s_good_subscription), +}; + +static void s_make_no_subscriptions_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = NULL; + view->subscription_count = 0; +} + +AWS_VALIDATION_FAILURE_TEST3(subscribe, no_subscriptions, s_good_subscribe_view, s_make_no_subscriptions_subscribe_view) + +static struct aws_mqtt5_subscription_view + s_too_many_subscriptions[AWS_MQTT5_CLIENT_MAXIMUM_SUBSCRIPTIONS_PER_SUBSCRIBE + 1]; + +static void s_make_too_many_subscriptions_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_many_subscriptions); ++i) { + struct aws_mqtt5_subscription_view *subscription_view = &s_too_many_subscriptions[i]; + + subscription_view->topic_filter = aws_byte_cursor_from_array(s_good_topic, AWS_ARRAY_SIZE(s_good_topic) - 1); + subscription_view->qos = AWS_MQTT5_QOS_AT_MOST_ONCE; + subscription_view->no_local = false; + subscription_view->retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE; + subscription_view->retain_as_published = false; + } + + view->subscriptions = s_too_many_subscriptions; + view->subscription_count = AWS_ARRAY_SIZE(s_too_many_subscriptions); +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + too_many_subscriptions, + s_good_subscribe_view, + s_make_too_many_subscriptions_subscribe_view) + +static const uint32_t s_invalid_subscription_identifier = AWS_MQTT5_MAXIMUM_VARIABLE_LENGTH_INTEGER + 1; + +static void s_make_invalid_subscription_identifier_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscription_identifier = &s_invalid_subscription_identifier; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + invalid_subscription_identifier, + s_good_subscribe_view, + s_make_invalid_subscription_identifier_subscribe_view) + +static uint8_t s_bad_topic_filter[] = "hello/#/world"; + +static struct aws_mqtt5_subscription_view s_invalid_topic_filter_subscription[] = { + { + .topic_filter = + { + .ptr = s_bad_topic_filter, + .len = AWS_ARRAY_SIZE(s_bad_topic_filter) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + .retain_as_published = false, + }, +}; + +static void s_make_invalid_topic_filter_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_topic_filter_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_subscription); +}; + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + invalid_topic_filter, + s_good_subscribe_view, + s_make_invalid_topic_filter_subscribe_view) + +static struct aws_mqtt5_subscription_view s_invalid_qos_subscription[] = { + { + .topic_filter = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .qos = 3, + .no_local = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + .retain_as_published = false, + }, +}; + +static void s_make_invalid_qos_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_qos_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_qos_subscription); +} + +AWS_VALIDATION_FAILURE_TEST3(subscribe, invalid_qos, s_good_subscribe_view, s_make_invalid_qos_subscribe_view) + +static struct aws_mqtt5_subscription_view s_invalid_retain_type_subscription[] = { + { + .topic_filter = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = false, + .retain_handling_type = 5, + .retain_as_published = false, + }, +}; + +static void s_make_invalid_retain_type_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_retain_type_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_retain_type_subscription); +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + invalid_retain_type, + s_good_subscribe_view, + s_make_invalid_retain_type_subscribe_view) + +static uint8_t s_shared_topic[] = "$share/sharename/topic/filter"; + +static struct aws_mqtt5_subscription_view s_invalid_no_local_subscription[] = { + { + .topic_filter = + { + .ptr = s_shared_topic, + .len = AWS_ARRAY_SIZE(s_shared_topic) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = true, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + .retain_as_published = false, + }, +}; + +static void s_make_invalid_no_local_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_no_local_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_no_local_subscription); +} + +AWS_VALIDATION_FAILURE_TEST3(subscribe, invalid_no_local, s_good_subscribe_view, s_make_invalid_no_local_subscribe_view) + +static uint8_t s_too_many_slashes_topic_filter[] = "///a///b///c/d/#"; + +static struct aws_mqtt5_subscription_view s_invalid_topic_filter_for_iot_core_subscription[] = { + { + .topic_filter = + { + .ptr = s_too_many_slashes_topic_filter, + .len = AWS_ARRAY_SIZE(s_too_many_slashes_topic_filter) - 1, + }, + }, +}; + +static void s_make_invalid_topic_filter_for_iot_core_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_topic_filter_for_iot_core_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_for_iot_core_subscription); +} + +AWS_IOT_CORE_VALIDATION_FAILURE( + subscribe, + invalid_topic_filter_for_iot_core, + s_good_subscribe_view, + s_make_invalid_topic_filter_for_iot_core_subscribe_view) + +static struct aws_mqtt5_subscription_view s_too_many_subscriptions_for_iot_core_subscription[9]; + +static void s_make_too_many_subscriptions_for_iot_core_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_many_subscriptions_for_iot_core_subscription); ++i) { + struct aws_mqtt5_subscription_view *subscription_view = &s_too_many_subscriptions_for_iot_core_subscription[i]; + + subscription_view->topic_filter = aws_byte_cursor_from_array(s_good_topic, AWS_ARRAY_SIZE(s_good_topic) - 1); + subscription_view->qos = AWS_MQTT5_QOS_AT_MOST_ONCE; + subscription_view->no_local = false; + subscription_view->retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE; + subscription_view->retain_as_published = false; + } + + view->subscriptions = s_too_many_subscriptions_for_iot_core_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_too_many_subscriptions_for_iot_core_subscription); +} + +AWS_IOT_CORE_VALIDATION_FAILURE( + subscribe, + too_many_subscriptions_for_iot_core, + s_good_subscribe_view, + s_make_too_many_subscriptions_for_iot_core_subscribe_view) + +static void s_make_user_properties_name_too_long_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); + view->user_properties = s_bad_user_properties_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + user_properties_name_too_long, + s_good_subscribe_view, + s_make_user_properties_name_too_long_subscribe_view) + +static void s_make_user_properties_value_too_long_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); + view->user_properties = s_bad_user_properties_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + user_properties_value_too_long, + s_good_subscribe_view, + s_make_user_properties_value_too_long_subscribe_view) + +static void s_make_user_properties_too_many_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); + view->user_properties = s_bad_user_properties_too_many; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + user_properties_too_many, + s_good_subscribe_view, + s_make_user_properties_too_many_subscribe_view) + +static struct aws_byte_cursor s_good_topic_filter[] = { + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, +}; + +static struct aws_mqtt5_packet_unsubscribe_view s_good_unsubscribe_view = { + .topic_filters = s_good_topic_filter, + .topic_filter_count = AWS_ARRAY_SIZE(s_good_topic_filter), +}; + +static void s_make_no_topic_filters_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->topic_filters = NULL; + view->topic_filter_count = 0; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + no_topic_filters, + s_good_unsubscribe_view, + s_make_no_topic_filters_unsubscribe_view) + +static struct aws_byte_cursor s_too_many_topic_filters[AWS_MQTT5_CLIENT_MAXIMUM_TOPIC_FILTERS_PER_UNSUBSCRIBE + 1]; + +static void s_make_too_many_topic_filters_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_many_topic_filters); ++i) { + struct aws_byte_cursor *cursor = &s_too_many_topic_filters[i]; + + cursor->ptr = s_good_topic; + cursor->len = AWS_ARRAY_SIZE(s_good_topic) - 1; + } + + view->topic_filters = s_too_many_topic_filters; + view->topic_filter_count = AWS_ARRAY_SIZE(s_too_many_topic_filters); +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + too_many_topic_filters, + s_good_unsubscribe_view, + s_make_too_many_topic_filters_unsubscribe_view) + +static struct aws_byte_cursor s_invalid_topic_filter[] = { + { + .ptr = s_bad_topic_filter, + .len = AWS_ARRAY_SIZE(s_bad_topic_filter) - 1, + }, +}; + +static void s_make_invalid_topic_filter_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->topic_filters = s_invalid_topic_filter; + view->topic_filter_count = AWS_ARRAY_SIZE(s_invalid_topic_filter); +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + invalid_topic_filter, + s_good_unsubscribe_view, + s_make_invalid_topic_filter_unsubscribe_view) + +static struct aws_byte_cursor s_invalid_topic_filter_for_iot_core[] = { + { + .ptr = s_too_many_slashes_topic_filter, + .len = AWS_ARRAY_SIZE(s_too_many_slashes_topic_filter) - 1, + }, +}; + +static void s_make_invalid_topic_filter_for_iot_core_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->topic_filters = s_invalid_topic_filter_for_iot_core; + view->topic_filter_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_for_iot_core); +} + +AWS_IOT_CORE_VALIDATION_FAILURE( + unsubscribe, + invalid_topic_filter_for_iot_core, + s_good_unsubscribe_view, + s_make_invalid_topic_filter_for_iot_core_unsubscribe_view) + +static void s_make_user_properties_name_too_long_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); + view->user_properties = s_bad_user_properties_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + user_properties_name_too_long, + s_good_unsubscribe_view, + s_make_user_properties_name_too_long_unsubscribe_view) + +static void s_make_user_properties_value_too_long_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); + view->user_properties = s_bad_user_properties_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + user_properties_value_too_long, + s_good_unsubscribe_view, + s_make_user_properties_value_too_long_unsubscribe_view) + +static void s_make_user_properties_too_many_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); + view->user_properties = s_bad_user_properties_too_many; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + user_properties_too_many, + s_good_unsubscribe_view, + s_make_user_properties_too_many_unsubscribe_view) + +static struct aws_mqtt5_packet_publish_view s_good_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, +}; + +static uint8_t s_invalid_topic[] = "hello/#"; +static void s_make_invalid_topic_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->topic.ptr = s_invalid_topic; + view->topic.len = AWS_ARRAY_SIZE(s_invalid_topic) - 1; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, invalid_topic, s_good_publish_view, s_make_invalid_topic_publish_view) + +static uint8_t s_topic_too_long_for_iot_core[258]; + +static void s_make_topic_too_long_for_iot_core_publish_view(struct aws_mqtt5_packet_publish_view *view) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_topic_too_long_for_iot_core); ++i) { + s_topic_too_long_for_iot_core[i] = 'b'; + } + + view->topic.ptr = s_topic_too_long_for_iot_core; + view->topic.len = AWS_ARRAY_SIZE(s_topic_too_long_for_iot_core) - 1; +} + +AWS_IOT_CORE_VALIDATION_FAILURE( + publish, + topic_too_long_for_iot_core, + s_good_publish_view, + s_make_topic_too_long_for_iot_core_publish_view) + +static uint8_t s_topic_too_many_slashes_for_iot_core[] = "a/b/c/d/efg/h/i/j/k/l/m"; + +static void s_make_topic_too_many_slashes_for_iot_core_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->topic.ptr = s_topic_too_many_slashes_for_iot_core; + view->topic.len = AWS_ARRAY_SIZE(s_topic_too_many_slashes_for_iot_core) - 1; +} + +AWS_IOT_CORE_VALIDATION_FAILURE( + publish, + topic_too_many_slashes_for_iot_core, + s_good_publish_view, + s_make_topic_too_many_slashes_for_iot_core_publish_view) + +static void s_make_no_topic_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->topic.ptr = NULL; + view->topic.len = 0; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, no_topic, s_good_publish_view, s_make_no_topic_publish_view) + +static enum aws_mqtt5_payload_format_indicator s_invalid_payload_format = 3; + +static void s_make_invalid_payload_format_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->payload_format = &s_invalid_payload_format; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + invalid_payload_format, + s_good_publish_view, + s_make_invalid_payload_format_publish_view) + +static void s_make_response_topic_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->response_topic = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + response_topic_too_long, + s_good_publish_view, + s_make_response_topic_too_long_publish_view) + +static struct aws_byte_cursor s_invalid_topic_cursor = { + .ptr = s_invalid_topic, + .len = AWS_ARRAY_SIZE(s_invalid_topic) - 1, +}; + +static void s_make_response_topic_invalid_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->response_topic = &s_invalid_topic_cursor; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + invalid_response_topic, + s_good_publish_view, + s_make_response_topic_invalid_publish_view) + +static void s_make_correlation_data_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->correlation_data = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + correlation_data_too_long, + s_good_publish_view, + s_make_correlation_data_too_long_publish_view) + +static const uint32_t s_subscription_identifiers[] = {1, 2}; + +static void s_make_subscription_identifier_exists_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->subscription_identifiers = s_subscription_identifiers; + view->subscription_identifier_count = AWS_ARRAY_SIZE(s_subscription_identifiers); +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + subscription_identifier_exists, + s_good_publish_view, + s_make_subscription_identifier_exists_publish_view) + +static void s_make_content_type_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->content_type = &s_too_long_for_uint16_cursor; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + content_type_too_long, + s_good_publish_view, + s_make_content_type_too_long_publish_view) + +static const uint16_t s_topic_alias_zero = 0; +static void s_make_topic_alias_zero_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->topic_alias = &s_topic_alias_zero; + view->topic.ptr = NULL; + view->topic.len = 0; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, topic_alias_zero, s_good_publish_view, s_make_topic_alias_zero_publish_view) + +static void s_make_user_properties_name_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); + view->user_properties = s_bad_user_properties_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + user_properties_name_too_long, + s_good_publish_view, + s_make_user_properties_name_too_long_publish_view) + +static void s_make_user_properties_value_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); + view->user_properties = s_bad_user_properties_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + user_properties_value_too_long, + s_good_publish_view, + s_make_user_properties_value_too_long_publish_view) + +static void s_make_user_properties_too_many_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); + view->user_properties = s_bad_user_properties_too_many; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + user_properties_too_many, + s_good_publish_view, + s_make_user_properties_too_many_publish_view) + +static void s_make_qos0_duplicate_true_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->qos = AWS_MQTT5_QOS_AT_MOST_ONCE; + view->duplicate = true; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, qos0_duplicate_true, s_good_publish_view, s_make_qos0_duplicate_true_publish_view) + +static void s_make_qos0_with_packet_id_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->qos = AWS_MQTT5_QOS_AT_MOST_ONCE; + view->packet_id = 1; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, qos0_with_packet_id, s_good_publish_view, s_make_qos0_with_packet_id_publish_view) + +#define AWS_CLIENT_CREATION_VALIDATION_FAILURE(failure_reason, base_options, mutate_function) \ + static int s_mqtt5_client_options_validation_failure_##failure_reason##_fn( \ + struct aws_allocator *allocator, void *ctx) { \ + (void)ctx; \ + aws_mqtt_library_init(allocator); \ + struct aws_event_loop_group *elg = NULL; \ + struct aws_host_resolver *hr = NULL; \ + struct aws_client_bootstrap *bootstrap = NULL; \ + elg = aws_event_loop_group_new_default(allocator, 1, NULL); \ + \ + struct aws_host_resolver_default_options hr_options = { \ + .el_group = elg, \ + .max_entries = 1, \ + }; \ + hr = aws_host_resolver_new_default(allocator, &hr_options); \ + \ + struct aws_client_bootstrap_options bootstrap_options = { \ + .event_loop_group = elg, \ + .host_resolver = hr, \ + }; \ + bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); \ + \ + struct aws_mqtt5_client_options good_options = base_options; \ + good_options.bootstrap = bootstrap; \ + \ + ASSERT_SUCCESS(aws_mqtt5_client_options_validate(&good_options)); \ + struct aws_mqtt5_client *client = aws_mqtt5_client_new(allocator, &good_options); \ + ASSERT_NOT_NULL(client); \ + aws_mqtt5_client_release(client); \ + \ + struct aws_mqtt5_client_options bad_options = good_options; \ + (*mutate_function)(&bad_options); \ + ASSERT_FAILS(aws_mqtt5_client_options_validate(&bad_options)); \ + ASSERT_NULL(aws_mqtt5_client_new(allocator, &bad_options)); \ + \ + aws_client_bootstrap_release(bootstrap); \ + aws_host_resolver_release(hr); \ + aws_event_loop_group_release(elg); \ + \ + aws_mqtt_library_clean_up(); \ + return AWS_OP_SUCCESS; \ + } \ + \ + AWS_TEST_CASE( \ + mqtt5_client_options_validation_failure_##failure_reason, \ + s_mqtt5_client_options_validation_failure_##failure_reason##_fn) + +static struct aws_socket_options s_good_socket_options = { + .type = AWS_SOCKET_STREAM, + .domain = AWS_SOCKET_IPV4, + .connect_timeout_ms = 10000, +}; + +static struct aws_mqtt5_packet_connect_view s_good_connect = { + .keep_alive_interval_seconds = 30, +}; + +void s_lifecycle_event_handler(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_publish_received(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static struct aws_mqtt5_client_options s_good_client_options = { + .host_name = + { + .ptr = s_server_reference, + .len = AWS_ARRAY_SIZE(s_server_reference) - 1, + }, + .socket_options = &s_good_socket_options, + .connect_options = &s_good_connect, + .ping_timeout_ms = 5000, + .lifecycle_event_handler = &s_lifecycle_event_handler, + .publish_received_handler = &s_publish_received, +}; + +static void s_make_no_host_client_options(struct aws_mqtt5_client_options *options) { + options->host_name.ptr = NULL; + options->host_name.len = 0; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE(no_host, s_good_client_options, s_make_no_host_client_options) + +static void s_make_no_bootstrap_client_options(struct aws_mqtt5_client_options *options) { + options->bootstrap = NULL; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE(no_bootstrap, s_good_client_options, s_make_no_bootstrap_client_options) + +static void s_make_no_publish_received_client_options(struct aws_mqtt5_client_options *options) { + options->publish_received_handler = NULL; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE( + no_publish_received, + s_good_client_options, + s_make_no_publish_received_client_options) + +static struct aws_socket_options s_bad_socket_options = { + .type = AWS_SOCKET_DGRAM, +}; + +static void s_make_invalid_socket_options_client_options(struct aws_mqtt5_client_options *options) { + options->socket_options = &s_bad_socket_options; +}; + +AWS_CLIENT_CREATION_VALIDATION_FAILURE( + invalid_socket_options, + s_good_client_options, + s_make_invalid_socket_options_client_options) + +static struct aws_mqtt5_packet_connect_view s_client_id_too_long_connect_view = { + .client_id = + { + .ptr = s_too_long_for_uint16, + .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), + }, +}; + +static void s_make_invalid_connect_client_options(struct aws_mqtt5_client_options *options) { + options->connect_options = &s_client_id_too_long_connect_view; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE(invalid_connect, s_good_client_options, s_make_invalid_connect_client_options) + +static struct aws_mqtt5_packet_connect_view s_short_keep_alive_connect_view = { + .keep_alive_interval_seconds = 20, +}; + +static void s_make_invalid_keep_alive_client_options(struct aws_mqtt5_client_options *options) { + options->connect_options = &s_short_keep_alive_connect_view; + options->ping_timeout_ms = 30000; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE( + invalid_keep_alive, + s_good_client_options, + s_make_invalid_keep_alive_client_options) + +static uint8_t s_too_long_client_id[129]; + +static struct aws_mqtt5_packet_connect_view s_client_id_too_long_for_iot_core_connect_view = { + .client_id = + { + .ptr = s_too_long_client_id, + .len = AWS_ARRAY_SIZE(s_too_long_client_id), + }, +}; + +static void s_make_client_id_too_long_for_iot_core_client_options(struct aws_mqtt5_client_options *options) { + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_long_client_id); ++i) { + s_too_long_client_id[i] = 'a'; + } + + options->connect_options = &s_client_id_too_long_for_iot_core_connect_view; + options->extended_validation_and_flow_control_options = AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE( + client_id_too_long_for_iot_core, + s_good_client_options, + s_make_client_id_too_long_for_iot_core_client_options) + +#define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_PREFIX(packet_type, failure_reason, init_success_settings_fn) \ + static int s_mqtt5_operation_##packet_type##_connection_settings_validation_failure_##failure_reason##_fn( \ + struct aws_allocator *allocator, void *ctx) { \ + (void)ctx; \ + \ + struct aws_mqtt5_client dummy_client; \ + AWS_ZERO_STRUCT(dummy_client); \ + \ + dummy_client.current_state = AWS_MCS_CONNECTED; \ + (*init_success_settings_fn)(&dummy_client); + +#define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_SUFFIX(packet_type, failure_reason, init_failure_settings_fn) \ + ASSERT_NOT_NULL(operation); \ + \ + ASSERT_SUCCESS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); \ + \ + (*init_failure_settings_fn)(&dummy_client); \ + \ + ASSERT_FAILS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); \ + \ + aws_mqtt5_operation_release(&operation->base); \ + \ + return AWS_OP_SUCCESS; \ + } \ + \ + AWS_TEST_CASE( \ + mqtt5_operation_##packet_type##_connection_settings_validation_failure_##failure_reason, \ + s_mqtt5_operation_##packet_type##_connection_settings_validation_failure_##failure_reason##_fn) + +#define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( \ + packet_type, failure_reason, view_name, init_success_settings_fn, init_failure_settings_fn) \ + AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_PREFIX(packet_type, failure_reason, init_success_settings_fn) \ + struct aws_mqtt5_operation_##packet_type *operation = \ + aws_mqtt5_operation_##packet_type##_new(allocator, NULL, &view_name, NULL); \ + AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_SUFFIX(packet_type, failure_reason, init_failure_settings_fn) + +static struct aws_mqtt5_packet_subscribe_view s_exceeds_maximum_packet_size_subscribe_view = { + .subscriptions = s_good_subscription, + .subscription_count = AWS_ARRAY_SIZE(s_good_subscription), +}; + +static void s_packet_size_init_settings_success_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.maximum_packet_size_to_server = 100; +} + +static void s_packet_size_init_settings_failure_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.maximum_packet_size_to_server = 10; +} + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + subscribe, + exceeds_maximum_packet_size, + s_exceeds_maximum_packet_size_subscribe_view, + s_packet_size_init_settings_success_fn, + s_packet_size_init_settings_failure_fn) + +static struct aws_mqtt5_packet_unsubscribe_view s_exceeds_maximum_packet_size_unsubscribe_view = { + .topic_filters = s_good_topic_filter, + .topic_filter_count = AWS_ARRAY_SIZE(s_good_topic_filter), +}; + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + unsubscribe, + exceeds_maximum_packet_size, + s_exceeds_maximum_packet_size_unsubscribe_view, + s_packet_size_init_settings_success_fn, + s_packet_size_init_settings_failure_fn) + +static struct aws_mqtt5_packet_publish_view s_exceeds_maximum_packet_size_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .payload = + { + .ptr = s_binary_data, + .len = AWS_ARRAY_SIZE(s_binary_data), + }, +}; + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + publish, + exceeds_maximum_packet_size, + s_exceeds_maximum_packet_size_publish_view, + s_packet_size_init_settings_success_fn, + s_packet_size_init_settings_failure_fn) + +static const uint16_t s_topic_alias = 5; + +static struct aws_mqtt5_packet_publish_view s_exceeds_topic_alias_maximum_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .topic_alias = &s_topic_alias, +}; + +static struct aws_mqtt5_client_options_storage s_dummy_client_options; + +static void s_topic_alias_init_settings_success_fn(struct aws_mqtt5_client *dummy_client) { + AWS_ZERO_STRUCT(s_dummy_client_options); + s_dummy_client_options.topic_aliasing_options.outbound_topic_alias_behavior = AWS_MQTT5_COTABT_USER; + + dummy_client->config = &s_dummy_client_options; + dummy_client->negotiated_settings.maximum_packet_size_to_server = 100; + dummy_client->negotiated_settings.topic_alias_maximum_to_server = 10; +} + +static void s_topic_alias_init_settings_failure_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.topic_alias_maximum_to_server = 2; +} + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + publish, + exceeds_topic_alias_maximum, + s_exceeds_topic_alias_maximum_publish_view, + s_topic_alias_init_settings_success_fn, + s_topic_alias_init_settings_failure_fn) + +static struct aws_mqtt5_packet_publish_view s_exceeds_maximum_qos_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .qos = AWS_MQTT5_QOS_EXACTLY_ONCE, +}; + +static void s_maximum_qos_init_settings_success_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.maximum_packet_size_to_server = 100; + dummy_client->negotiated_settings.maximum_qos = AWS_MQTT5_QOS_EXACTLY_ONCE; +} + +static void s_maximum_qos_init_settings_failure_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.maximum_qos = AWS_MQTT5_QOS_AT_LEAST_ONCE; +} + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + publish, + exceeds_maximum_qos, + s_exceeds_maximum_qos_publish_view, + s_maximum_qos_init_settings_success_fn, + s_maximum_qos_init_settings_failure_fn) + +static struct aws_mqtt5_packet_publish_view s_invalid_retain_publish_view = { + .topic = + { + .ptr = s_good_topic, + .len = AWS_ARRAY_SIZE(s_good_topic) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .retain = true, +}; + +static void s_invalid_retain_init_settings_success_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.maximum_packet_size_to_server = 100; + dummy_client->negotiated_settings.retain_available = true; +} + +static void s_invalid_retain_init_settings_failure_fn(struct aws_mqtt5_client *dummy_client) { + dummy_client->negotiated_settings.retain_available = false; +} + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( + publish, + invalid_retain, + s_invalid_retain_publish_view, + s_invalid_retain_init_settings_success_fn, + s_invalid_retain_init_settings_failure_fn) + +#define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST4( \ + packet_type, failure_reason, view_name, init_success_settings_fn, init_failure_settings_fn) \ + AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_PREFIX(packet_type, failure_reason, init_success_settings_fn) \ + struct aws_mqtt5_operation_##packet_type *operation = \ + aws_mqtt5_operation_##packet_type##_new(allocator, &view_name, NULL, NULL); \ + AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_SUFFIX(packet_type, failure_reason, init_failure_settings_fn) + +static uint8_t s_disconnect_reason_string[] = "We're leaving this planet for somewhere else"; +static struct aws_byte_cursor s_disconnect_reason_string_cursor = { + .ptr = s_disconnect_reason_string, + .len = AWS_ARRAY_SIZE(s_disconnect_reason_string) - 1, +}; + +static struct aws_mqtt5_packet_disconnect_view s_exceeds_maximum_packet_size_disconnect_view = { + .reason_string = &s_disconnect_reason_string_cursor, +}; + +AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST4( + disconnect, + exceeds_maximum_packet_size, + s_exceeds_maximum_packet_size_disconnect_view, + s_packet_size_init_settings_success_fn, + s_packet_size_init_settings_failure_fn) + +static const uint32_t s_positive_session_expiry = 1; +static const uint32_t s_session_expiry = 5; + +static struct aws_mqtt5_packet_disconnect_view s_promote_zero_session_expiry_disconnect_view = { + .session_expiry_interval_seconds = &s_session_expiry, +}; + +static int mqtt5_operation_disconnect_connection_settings_validation_failure_promote_zero_session_expiry_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_event_loop_group *elg = aws_event_loop_group_new_default(allocator, 1, NULL); + + struct aws_host_resolver_default_options hr_options = { + .el_group = elg, + .max_entries = 1, + }; + struct aws_host_resolver *hr = aws_host_resolver_new_default(allocator, &hr_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = elg, + .host_resolver = hr, + }; + struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + struct aws_mqtt5_client_options client_options = s_good_client_options; + client_options.host_name = s_server_reference_cursor; + client_options.bootstrap = bootstrap; + + struct aws_mqtt5_packet_connect_view connect_options = s_good_connect; + connect_options.session_expiry_interval_seconds = &s_positive_session_expiry; + client_options.connect_options = &connect_options; + + struct aws_mqtt5_client_options_storage *client_options_storage = + aws_mqtt5_client_options_storage_new(allocator, &client_options); + ASSERT_NOT_NULL(client_options_storage); + + struct aws_mqtt5_client dummy_client; + AWS_ZERO_STRUCT(dummy_client); + + dummy_client.current_state = AWS_MCS_CONNECTED; + dummy_client.negotiated_settings.maximum_packet_size_to_server = 100; + dummy_client.config = client_options_storage; + + struct aws_mqtt5_operation_disconnect *operation = + aws_mqtt5_operation_disconnect_new(allocator, &s_promote_zero_session_expiry_disconnect_view, NULL, NULL); + + ASSERT_SUCCESS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); + + ((struct aws_mqtt5_client_options_storage *)dummy_client.config) + ->connect.storage_view.session_expiry_interval_seconds = NULL; + + ASSERT_FAILS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); + + aws_mqtt5_operation_release(&operation->base); + aws_mqtt5_client_options_storage_destroy(client_options_storage); + + aws_client_bootstrap_release(bootstrap); + aws_host_resolver_release(hr); + aws_event_loop_group_release(elg); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_operation_disconnect_connection_settings_validation_failure_promote_zero_session_expiry, + mqtt5_operation_disconnect_connection_settings_validation_failure_promote_zero_session_expiry_fn) diff --git a/tests/v5/mqtt5_testing_utils.c b/tests/v5/mqtt5_testing_utils.c new file mode 100644 index 00000000..42e5fd27 --- /dev/null +++ b/tests/v5/mqtt5_testing_utils.c @@ -0,0 +1,1747 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt5_testing_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +int aws_mqtt5_test_verify_user_properties_raw( + size_t property_count, + const struct aws_mqtt5_user_property *properties, + size_t expected_count, + const struct aws_mqtt5_user_property *expected_properties) { + + ASSERT_UINT_EQUALS(expected_count, property_count); + + for (size_t i = 0; i < expected_count; ++i) { + const struct aws_mqtt5_user_property *expected_property = &expected_properties[i]; + struct aws_byte_cursor expected_name = expected_property->name; + struct aws_byte_cursor expected_value = expected_property->value; + + bool found = false; + for (size_t j = 0; j < property_count; ++j) { + const struct aws_mqtt5_user_property *nv_pair = &properties[j]; + + if (aws_byte_cursor_compare_lexical(&expected_name, &nv_pair->name) == 0 && + aws_byte_cursor_compare_lexical(&expected_value, &nv_pair->value) == 0) { + found = true; + break; + } + } + + if (!found) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +static int s_compute_connack_variable_length_fields( + const struct aws_mqtt5_packet_connack_view *connack_view, + uint32_t *total_remaining_length, + uint32_t *property_length) { + + size_t local_property_length = + aws_mqtt5_compute_user_property_encode_length(connack_view->user_properties, connack_view->user_property_count); + + ADD_OPTIONAL_U32_PROPERTY_LENGTH(connack_view->session_expiry_interval, local_property_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(connack_view->receive_maximum, local_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connack_view->maximum_qos, local_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connack_view->retain_available, local_property_length); + ADD_OPTIONAL_U32_PROPERTY_LENGTH(connack_view->maximum_packet_size, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->assigned_client_identifier, local_property_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(connack_view->topic_alias_maximum, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->reason_string, local_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connack_view->wildcard_subscriptions_available, local_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connack_view->subscription_identifiers_available, local_property_length); + ADD_OPTIONAL_U8_PROPERTY_LENGTH(connack_view->shared_subscriptions_available, local_property_length); + ADD_OPTIONAL_U16_PROPERTY_LENGTH(connack_view->server_keep_alive, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->response_information, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->server_reference, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->authentication_method, local_property_length); + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(connack_view->authentication_data, local_property_length); + + *property_length = (uint32_t)local_property_length; + + size_t property_length_encoding_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &property_length_encoding_length)) { + return AWS_OP_ERR; + } + + /* reason code (1 byte) + flags (1 byte) */ + *total_remaining_length = *property_length + (uint32_t)property_length_encoding_length + 2; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_encoder_begin_connack(struct aws_mqtt5_encoder *encoder, const void *packet_view) { + + const struct aws_mqtt5_packet_connack_view *connack_view = packet_view; + + uint32_t total_remaining_length = 0; + uint32_t property_length = 0; + if (s_compute_connack_variable_length_fields(connack_view, &total_remaining_length, &property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for CONNACK packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a CONNACK packet with remaining length %" PRIu32, + (void *)encoder->config.client, + total_remaining_length); + + uint8_t flags = connack_view->session_present ? 1 : 0; + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_CONNACK, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length); + ADD_ENCODE_STEP_U8(encoder, flags); + ADD_ENCODE_STEP_U8(encoder, (uint8_t)connack_view->reason_code); + ADD_ENCODE_STEP_VLI(encoder, property_length); + + if (property_length > 0) { + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL, connack_view->session_expiry_interval); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_RECEIVE_MAXIMUM, connack_view->receive_maximum); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY(encoder, AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_QOS, connack_view->maximum_qos); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_RETAIN_AVAILABLE, connack_view->retain_available); + ADD_ENCODE_STEP_OPTIONAL_U32_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_PACKET_SIZE, connack_view->maximum_packet_size); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_ASSIGNED_CLIENT_IDENTIFIER, connack_view->assigned_client_identifier); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS_MAXIMUM, connack_view->topic_alias_maximum); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, connack_view->reason_string); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, + AWS_MQTT5_PROPERTY_TYPE_WILDCARD_SUBSCRIPTIONS_AVAILABLE, + connack_view->wildcard_subscriptions_available); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, + AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIERS_AVAILABLE, + connack_view->subscription_identifiers_available); + ADD_ENCODE_STEP_OPTIONAL_U8_PROPERTY( + encoder, + AWS_MQTT5_PROPERTY_TYPE_SHARED_SUBSCRIPTIONS_AVAILABLE, + connack_view->shared_subscriptions_available); + ADD_ENCODE_STEP_OPTIONAL_U16_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SERVER_KEEP_ALIVE, connack_view->server_keep_alive); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_RESPONSE_INFORMATION, connack_view->response_information); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_SERVER_REFERENCE, connack_view->server_reference); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_METHOD, connack_view->authentication_method); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_DATA, connack_view->authentication_data); + + aws_mqtt5_add_user_property_encoding_steps( + encoder, connack_view->user_properties, connack_view->user_property_count); + } + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_encoder_begin_pingresp(struct aws_mqtt5_encoder *encoder, const void *packet_view) { + (void)packet_view; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a PINGRESP packet", + (void *)encoder->config.client); + + /* A ping response is just a fixed header with a 0-valued remaining length which we encode as a 0 u8 */ + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PINGRESP, 0)); + ADD_ENCODE_STEP_U8(encoder, 0); + + return AWS_OP_SUCCESS; +} + +static int s_compute_suback_variable_length_fields( + const struct aws_mqtt5_packet_suback_view *suback_view, + uint32_t *total_remaining_length, + uint32_t *property_length) { + /* User Properties length */ + size_t local_property_length = + aws_mqtt5_compute_user_property_encode_length(suback_view->user_properties, suback_view->user_property_count); + /* Optional Reason String */ + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(suback_view->reason_string, local_property_length); + + *property_length = (uint32_t)local_property_length; + + size_t local_total_remaining_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &local_total_remaining_length)) { + return AWS_OP_ERR; + } + + /* Packet Identifier (2 bytes) */ + local_total_remaining_length += 2; + + /* Reason Codes (1 byte each) */ + local_total_remaining_length += suback_view->reason_code_count; + + /* Add property length */ + *total_remaining_length = *property_length + (uint32_t)local_total_remaining_length; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_encoder_begin_suback(struct aws_mqtt5_encoder *encoder, const void *packet_view) { + + const struct aws_mqtt5_packet_suback_view *suback_view = packet_view; + + uint32_t total_remaining_length = 0; + uint32_t property_length = 0; + if (s_compute_suback_variable_length_fields(suback_view, &total_remaining_length, &property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for SUBACK packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for a SUBACK packet with remaining length %" PRIu32, + (void *)encoder->config.client, + total_remaining_length); + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_SUBACK, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length); + ADD_ENCODE_STEP_U16(encoder, suback_view->packet_id); + ADD_ENCODE_STEP_VLI(encoder, property_length); + + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, suback_view->reason_string); + + aws_mqtt5_add_user_property_encoding_steps(encoder, suback_view->user_properties, suback_view->user_property_count); + + for (size_t i = 0; i < suback_view->reason_code_count; ++i) { + ADD_ENCODE_STEP_U8(encoder, suback_view->reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +static int s_compute_unsuback_variable_length_fields( + const struct aws_mqtt5_packet_unsuback_view *unsuback_view, + uint32_t *total_remaining_length, + uint32_t *property_length) { + /* User Properties length */ + size_t local_property_length = aws_mqtt5_compute_user_property_encode_length( + unsuback_view->user_properties, unsuback_view->user_property_count); + /* Optional Reason String */ + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(unsuback_view->reason_string, local_property_length); + + *property_length = (uint32_t)local_property_length; + + size_t local_total_remaining_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &local_total_remaining_length)) { + return AWS_OP_ERR; + } + + /* Packet Identifier (2 bytes) */ + local_total_remaining_length += 2; + + /* Reason Codes (1 byte each) */ + local_total_remaining_length += unsuback_view->reason_code_count; + + /* Add property length */ + *total_remaining_length = *property_length + (uint32_t)local_total_remaining_length; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_encoder_begin_unsuback(struct aws_mqtt5_encoder *encoder, const void *packet_view) { + + const struct aws_mqtt5_packet_unsuback_view *unsuback_view = packet_view; + + uint32_t total_remaining_length = 0; + uint32_t property_length = 0; + if (s_compute_unsuback_variable_length_fields(unsuback_view, &total_remaining_length, &property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for UNSUBACK packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for an UNSUBACK packet with remaining length %" PRIu32, + (void *)encoder->config.client, + total_remaining_length); + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_UNSUBACK, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length); + ADD_ENCODE_STEP_U16(encoder, unsuback_view->packet_id); + ADD_ENCODE_STEP_VLI(encoder, property_length); + + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, unsuback_view->reason_string); + + aws_mqtt5_add_user_property_encoding_steps( + encoder, unsuback_view->user_properties, unsuback_view->user_property_count); + + for (size_t i = 0; i < unsuback_view->reason_code_count; ++i) { + ADD_ENCODE_STEP_U8(encoder, unsuback_view->reason_codes[i]); + } + + return AWS_OP_SUCCESS; +} + +static int s_compute_puback_variable_length_fields( + const struct aws_mqtt5_packet_puback_view *puback_view, + uint32_t *total_remaining_length, + uint32_t *property_length) { + /* User Properties length */ + size_t local_property_length = + aws_mqtt5_compute_user_property_encode_length(puback_view->user_properties, puback_view->user_property_count); + /* Optional Reason String */ + ADD_OPTIONAL_CURSOR_PROPERTY_LENGTH(puback_view->reason_string, local_property_length); + + *property_length = (uint32_t)local_property_length; + + size_t local_total_remaining_length = 0; + if (aws_mqtt5_get_variable_length_encode_size(local_property_length, &local_total_remaining_length)) { + return AWS_OP_ERR; + } + + /* Packet Identifier (2 bytes) */ + local_total_remaining_length += 2; + + /* Reason Code */ + local_total_remaining_length += 1; + + /* Add property length */ + *total_remaining_length = *property_length + (uint32_t)local_total_remaining_length; + + return AWS_OP_SUCCESS; +} + +int aws_mqtt5_encoder_begin_puback(struct aws_mqtt5_encoder *encoder, const void *packet_view) { + + const struct aws_mqtt5_packet_puback_view *puback_view = packet_view; + + uint32_t total_remaining_length = 0; + uint32_t property_length = 0; + if (s_compute_puback_variable_length_fields(puback_view, &total_remaining_length, &property_length)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - failed to compute variable length values for PUBACK packet with error " + "%d(%s)", + (void *)encoder->config.client, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - setting up encode for an PUBACK packet with remaining length %" PRIu32, + (void *)encoder->config.client, + total_remaining_length); + + ADD_ENCODE_STEP_U8(encoder, aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PUBACK, 0)); + ADD_ENCODE_STEP_VLI(encoder, total_remaining_length); + ADD_ENCODE_STEP_U16(encoder, puback_view->packet_id); + ADD_ENCODE_STEP_U8(encoder, puback_view->reason_code); + ADD_ENCODE_STEP_VLI(encoder, property_length); + ADD_ENCODE_STEP_OPTIONAL_CURSOR_PROPERTY( + encoder, AWS_MQTT5_PROPERTY_TYPE_REASON_STRING, puback_view->reason_string); + aws_mqtt5_add_user_property_encoding_steps(encoder, puback_view->user_properties, puback_view->user_property_count); + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_encode_init_testing_function_table(struct aws_mqtt5_encoder_function_table *function_table) { + *function_table = *g_aws_mqtt5_encoder_default_function_table; + function_table->encoders_by_packet_type[AWS_MQTT5_PT_PINGRESP] = &aws_mqtt5_encoder_begin_pingresp; + function_table->encoders_by_packet_type[AWS_MQTT5_PT_CONNACK] = &aws_mqtt5_encoder_begin_connack; + function_table->encoders_by_packet_type[AWS_MQTT5_PT_SUBACK] = &aws_mqtt5_encoder_begin_suback; + function_table->encoders_by_packet_type[AWS_MQTT5_PT_UNSUBACK] = &aws_mqtt5_encoder_begin_unsuback; + function_table->encoders_by_packet_type[AWS_MQTT5_PT_PUBACK] = &aws_mqtt5_encoder_begin_puback; +} + +static int s_aws_mqtt5_decoder_decode_pingreq(struct aws_mqtt5_decoder *decoder) { + if (decoder->packet_cursor.len != 0) { + goto error; + } + + uint8_t expected_first_byte = aws_mqtt5_compute_fixed_header_byte1(AWS_MQTT5_PT_PINGREQ, 0); + if (decoder->packet_first_byte != expected_first_byte || decoder->remaining_length != 0) { + goto error; + } + + if (decoder->options.on_packet_received != NULL) { + (*decoder->options.on_packet_received)(AWS_MQTT5_PT_PINGREQ, NULL, decoder->options.callback_user_data); + } + + return AWS_OP_SUCCESS; + +error: + + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "(%p) aws_mqtt5_decoder - PINGREQ decode failure", decoder->options.callback_user_data); + return aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); +} + +/* decode function for all CONNECT properties. Movable to test-only code if we switched to a decoding function table */ +static int s_read_connect_property( + struct aws_mqtt5_packet_connect_storage *storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_connect_view *storage_view = &storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_SESSION_EXPIRY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, + &storage->session_expiry_interval_seconds, + &storage_view->session_expiry_interval_seconds, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RECEIVE_MAXIMUM: + AWS_MQTT5_DECODE_U16_OPTIONAL( + packet_cursor, &storage->receive_maximum, &storage_view->receive_maximum, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_MAXIMUM_PACKET_SIZE: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, &storage->maximum_packet_size_bytes, &storage_view->maximum_packet_size_bytes, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_TOPIC_ALIAS_MAXIMUM: + AWS_MQTT5_DECODE_U16_OPTIONAL( + packet_cursor, &storage->topic_alias_maximum, &storage_view->topic_alias_maximum, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_REQUEST_RESPONSE_INFORMATION: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, + &storage->request_response_information, + &storage_view->request_response_information, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_REQUEST_PROBLEM_INFORMATION: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, &storage->request_problem_information, &storage_view->request_problem_information, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_METHOD: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->authentication_method, &storage_view->authentication_method, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_AUTHENTICATION_DATA: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &storage->authentication_data, &storage_view->authentication_data, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* decode function for all will properties. Movable to test-only code if we switched to a decoding function table */ +static int s_read_will_property( + struct aws_mqtt5_packet_connect_storage *connect_storage, + struct aws_mqtt5_packet_publish_storage *will_storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_connect_view *connect_storage_view = &connect_storage->storage_view; + struct aws_mqtt5_packet_publish_view *will_storage_view = &will_storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_WILL_DELAY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, + &connect_storage->will_delay_interval_seconds, + &connect_storage_view->will_delay_interval_seconds, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_PAYLOAD_FORMAT_INDICATOR: + AWS_MQTT5_DECODE_U8_OPTIONAL( + packet_cursor, &will_storage->payload_format, &will_storage_view->payload_format, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_MESSAGE_EXPIRY_INTERVAL: + AWS_MQTT5_DECODE_U32_OPTIONAL( + packet_cursor, + &will_storage->message_expiry_interval_seconds, + &will_storage_view->message_expiry_interval_seconds, + done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_CONTENT_TYPE: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &will_storage->content_type, &will_storage_view->content_type, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_RESPONSE_TOPIC: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &will_storage->response_topic, &will_storage_view->response_topic, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_CORRELATION_DATA: + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + packet_cursor, &will_storage->correlation_data, &will_storage_view->correlation_data, done); + break; + + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &will_storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* + * Decodes a CONNECT packet whose data must be in the scratch buffer. + * Could be moved to test-only if we used a function table for per-packet-type decoding. + */ +static int s_aws_mqtt5_decoder_decode_connect(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_connect_storage connect_storage; + struct aws_mqtt5_packet_publish_storage publish_storage; + int result = AWS_OP_ERR; + bool has_will = false; + + if (aws_mqtt5_packet_connect_storage_init_from_external_storage(&connect_storage, decoder->allocator)) { + return AWS_OP_ERR; + } + + if (aws_mqtt5_packet_publish_storage_init_from_external_storage(&publish_storage, decoder->allocator)) { + goto done; + } + + uint8_t first_byte = decoder->packet_first_byte; + /* CONNECT flags must be zero by protocol */ + if ((first_byte & 0x0F) != 0) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + if (decoder->remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + struct aws_byte_cursor protocol_cursor = aws_byte_cursor_advance(&packet_cursor, 7); + if (!aws_byte_cursor_eq(&protocol_cursor, &g_aws_mqtt5_connect_protocol_cursor)) { + goto done; + } + + uint8_t connect_flags = 0; + AWS_MQTT5_DECODE_U8(&packet_cursor, &connect_flags, done); + + struct aws_mqtt5_packet_connect_view *connect_storage_view = &connect_storage.storage_view; + struct aws_mqtt5_packet_publish_view *will_storage_view = &publish_storage.storage_view; + + connect_storage_view->clean_start = (connect_flags & AWS_MQTT5_CONNECT_FLAGS_CLEAN_START_BIT) != 0; + + AWS_MQTT5_DECODE_U16(&packet_cursor, &connect_storage_view->keep_alive_interval_seconds, done); + + uint32_t connect_property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &connect_property_length, done); + if (connect_property_length > packet_cursor.len) { + goto done; + } + + struct aws_byte_cursor connect_property_cursor = aws_byte_cursor_advance(&packet_cursor, connect_property_length); + while (connect_property_cursor.len > 0) { + if (s_read_connect_property(&connect_storage, &connect_property_cursor)) { + goto done; + } + } + + connect_storage_view->user_property_count = aws_mqtt5_user_property_set_size(&connect_storage.user_properties); + connect_storage_view->user_properties = connect_storage.user_properties.properties.data; + + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &connect_storage_view->client_id, done); + + has_will = (connect_flags & AWS_MQTT5_CONNECT_FLAGS_WILL_BIT) != 0; + if (has_will) { + uint32_t will_property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &will_property_length, done); + if (will_property_length > packet_cursor.len) { + goto done; + } + + struct aws_byte_cursor will_property_cursor = aws_byte_cursor_advance(&packet_cursor, will_property_length); + while (will_property_cursor.len > 0) { + if (s_read_will_property(&connect_storage, &publish_storage, &will_property_cursor)) { + goto done; + } + } + + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &will_storage_view->topic, done); + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &will_storage_view->payload, done); + + /* apply will flags from the connect flags to the will's storage */ + will_storage_view->qos = (enum aws_mqtt5_qos)( + (connect_flags >> AWS_MQTT5_CONNECT_FLAGS_WILL_QOS_BIT_POSITION) & + AWS_MQTT5_CONNECT_FLAGS_WILL_QOS_BIT_MASK); + will_storage_view->retain = (connect_flags & AWS_MQTT5_CONNECT_FLAGS_WILL_RETAIN_BIT) != 0; + + will_storage_view->user_property_count = aws_mqtt5_user_property_set_size(&publish_storage.user_properties); + will_storage_view->user_properties = publish_storage.user_properties.properties.data; + } + + if ((connect_flags & AWS_MQTT5_CONNECT_FLAGS_USER_NAME_BIT) != 0) { + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + &packet_cursor, &connect_storage.username, &connect_storage_view->username, done); + } + + if ((connect_flags & AWS_MQTT5_CONNECT_FLAGS_PASSWORD_BIT) != 0) { + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR_OPTIONAL( + &packet_cursor, &connect_storage.password, &connect_storage_view->password, done); + } + + if (packet_cursor.len == 0) { + result = AWS_OP_SUCCESS; + } + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + if (has_will) { + connect_storage.storage_view.will = &publish_storage.storage_view; + } + + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_CONNECT, &connect_storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - CONNECT decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_publish_storage_clean_up(&publish_storage); + aws_mqtt5_packet_connect_storage_clean_up(&connect_storage); + + return result; +} + +/* decode function for subscribe properties. Movable to test-only code if we switched to a decoding function table */ +static int s_read_subscribe_property( + struct aws_mqtt5_packet_subscribe_storage *subscribe_storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + struct aws_mqtt5_packet_subscribe_view *storage_view = &subscribe_storage->storage_view; + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_SUBSCRIPTION_IDENTIFIER: + AWS_MQTT5_DECODE_VLI(packet_cursor, &subscribe_storage->subscription_identifier, done); + storage_view->subscription_identifier = &subscribe_storage->subscription_identifier; + + break; + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &subscribe_storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* + * Decodes a SUBSCRIBE packet whose data must be in the scratch buffer. + */ +static int s_aws_mqtt5_decoder_decode_subscribe(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_subscribe_storage subscribe_storage; + int result = AWS_OP_ERR; + + if (aws_mqtt5_packet_subscribe_storage_init_from_external_storage(&subscribe_storage, decoder->allocator)) { + goto done; + } + + struct aws_mqtt5_packet_subscribe_view *storage_view = &subscribe_storage.storage_view; + + /* SUBSCRIBE flags must be 2 by protocol*/ + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & 0x0F) != 2) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + if (decoder->remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + uint16_t packet_id = 0; + AWS_MQTT5_DECODE_U16(&packet_cursor, &packet_id, done); + subscribe_storage.storage_view.packet_id = packet_id; + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length > packet_cursor.len) { + goto done; + } + + struct aws_byte_cursor property_cursor = aws_byte_cursor_advance(&packet_cursor, property_length); + while (property_cursor.len > 0) { + if (s_read_subscribe_property(&subscribe_storage, &property_cursor)) { + goto done; + } + } + + while (packet_cursor.len > 0) { + struct aws_mqtt5_subscription_view subscription_view; + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &subscription_view.topic_filter, done); + + uint8_t subscription_options = 0; + AWS_MQTT5_DECODE_U8(&packet_cursor, &subscription_options, done); + + subscription_view.no_local = (subscription_options & AWS_MQTT5_SUBSCRIBE_FLAGS_NO_LOCAL) != 0; + subscription_view.retain_as_published = + (subscription_options & AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_AS_PUBLISHED) != 0; + subscription_view.qos = (enum aws_mqtt5_qos)( + (subscription_options >> AWS_MQTT5_SUBSCRIBE_FLAGS_QOS_BIT_POSITION) & + AWS_MQTT5_SUBSCRIBE_FLAGS_QOS_BIT_MASK); + subscription_view.retain_handling_type = (enum aws_mqtt5_retain_handling_type)( + (subscription_options >> AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_HANDLING_TYPE_BIT_POSITION) & + AWS_MQTT5_SUBSCRIBE_FLAGS_RETAIN_HANDLING_TYPE_BIT_MASK); + + if (aws_array_list_push_back(&subscribe_storage.subscriptions, &subscription_view)) { + goto done; + } + } + + storage_view->subscription_count = aws_array_list_length(&subscribe_storage.subscriptions); + storage_view->subscriptions = subscribe_storage.subscriptions.data; + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&subscribe_storage.user_properties); + storage_view->user_properties = subscribe_storage.user_properties.properties.data; + + if (packet_cursor.len == 0) { + result = AWS_OP_SUCCESS; + } + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_SUBSCRIBE, &subscribe_storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - SUBSCRIBE decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_subscribe_storage_clean_up(&subscribe_storage); + + return result; +} + +/* decode function for unsubscribe properties. Movable to test-only code if we switched to a decoding function table */ +static int s_read_unsubscribe_property( + struct aws_mqtt5_packet_unsubscribe_storage *unsubscribe_storage, + struct aws_byte_cursor *packet_cursor) { + int result = AWS_OP_ERR; + + uint8_t property_type = 0; + AWS_MQTT5_DECODE_U8(packet_cursor, &property_type, done); + + switch (property_type) { + case AWS_MQTT5_PROPERTY_TYPE_USER_PROPERTY: + if (aws_mqtt5_decode_user_property(packet_cursor, &unsubscribe_storage->user_properties)) { + goto done; + } + break; + + default: + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + return result; +} + +/* + * Decodes an UNSUBSCRIBE packet whose data must be in the scratch buffer. + */ +static int s_aws_mqtt5_decoder_decode_unsubscribe(struct aws_mqtt5_decoder *decoder) { + struct aws_mqtt5_packet_unsubscribe_storage unsubscribe_storage; + int result = AWS_OP_ERR; + + if (aws_mqtt5_packet_unsubscribe_storage_init_from_external_storage(&unsubscribe_storage, decoder->allocator)) { + goto done; + } + + struct aws_mqtt5_packet_unsubscribe_view *storage_view = &unsubscribe_storage.storage_view; + + /* UNSUBSCRIBE flags must be 2 by protocol*/ + uint8_t first_byte = decoder->packet_first_byte; + if ((first_byte & 0x0F) != 2) { + goto done; + } + + struct aws_byte_cursor packet_cursor = decoder->packet_cursor; + if (decoder->remaining_length != (uint32_t)packet_cursor.len) { + goto done; + } + + uint16_t packet_id = 0; + AWS_MQTT5_DECODE_U16(&packet_cursor, &packet_id, done); + unsubscribe_storage.storage_view.packet_id = packet_id; + + uint32_t property_length = 0; + AWS_MQTT5_DECODE_VLI(&packet_cursor, &property_length, done); + if (property_length > packet_cursor.len) { + goto done; + } + + struct aws_byte_cursor property_cursor = aws_byte_cursor_advance(&packet_cursor, property_length); + while (property_cursor.len > 0) { + if (s_read_unsubscribe_property(&unsubscribe_storage, &property_cursor)) { + goto done; + } + } + + while (packet_cursor.len > 0) { + struct aws_byte_cursor topic; + AWS_MQTT5_DECODE_LENGTH_PREFIXED_CURSOR(&packet_cursor, &topic, done); + if (aws_array_list_push_back(&unsubscribe_storage.topic_filters, &topic)) { + goto done; + } + } + + storage_view->topic_filter_count = aws_array_list_length(&unsubscribe_storage.topic_filters); + storage_view->topic_filters = unsubscribe_storage.topic_filters.data; + storage_view->user_property_count = aws_mqtt5_user_property_set_size(&unsubscribe_storage.user_properties); + storage_view->user_properties = unsubscribe_storage.user_properties.properties.data; + + result = AWS_OP_SUCCESS; + +done: + + if (result == AWS_OP_SUCCESS) { + if (decoder->options.on_packet_received != NULL) { + result = (*decoder->options.on_packet_received)( + AWS_MQTT5_PT_UNSUBSCRIBE, &unsubscribe_storage.storage_view, decoder->options.callback_user_data); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "(%p) aws_mqtt5_decoder - UNSUBSCRIBE decode failure", + decoder->options.callback_user_data); + aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); + } + + aws_mqtt5_packet_unsubscribe_storage_clean_up(&unsubscribe_storage); + + return result; +} + +void aws_mqtt5_decode_init_testing_function_table(struct aws_mqtt5_decoder_function_table *function_table) { + *function_table = *g_aws_mqtt5_default_decoder_table; + function_table->decoders_by_packet_type[AWS_MQTT5_PT_PINGREQ] = &s_aws_mqtt5_decoder_decode_pingreq; + function_table->decoders_by_packet_type[AWS_MQTT5_PT_CONNECT] = &s_aws_mqtt5_decoder_decode_connect; + function_table->decoders_by_packet_type[AWS_MQTT5_PT_SUBSCRIBE] = &s_aws_mqtt5_decoder_decode_subscribe; + function_table->decoders_by_packet_type[AWS_MQTT5_PT_UNSUBSCRIBE] = &s_aws_mqtt5_decoder_decode_unsubscribe; +} + +static int s_aws_mqtt5_mock_test_fixture_on_packet_received_fn( + enum aws_mqtt5_packet_type type, + void *packet_view, + void *decoder_callback_user_data) { + + struct aws_mqtt5_mock_server_packet_record packet_record; + AWS_ZERO_STRUCT(packet_record); + + struct aws_mqtt5_server_mock_connection_context *server_connection = decoder_callback_user_data; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = server_connection->test_fixture; + + switch (type) { + case AWS_MQTT5_PT_CONNECT: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); + aws_mqtt5_packet_connect_storage_init(packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + case AWS_MQTT5_PT_DISCONNECT: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_disconnect_storage)); + aws_mqtt5_packet_disconnect_storage_init( + packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + case AWS_MQTT5_PT_SUBSCRIBE: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_subscribe_storage)); + aws_mqtt5_packet_subscribe_storage_init(packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + case AWS_MQTT5_PT_UNSUBSCRIBE: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_unsubscribe_storage)); + aws_mqtt5_packet_unsubscribe_storage_init( + packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + case AWS_MQTT5_PT_PUBLISH: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); + aws_mqtt5_packet_publish_storage_init(packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + case AWS_MQTT5_PT_PUBACK: + packet_record.packet_storage = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_packet_puback_storage)); + aws_mqtt5_packet_puback_storage_init(packet_record.packet_storage, test_fixture->allocator, packet_view); + break; + + default: + break; + } + + packet_record.storage_allocator = test_fixture->allocator; + packet_record.timestamp = (*test_fixture->client_vtable.get_current_time_fn)(); + packet_record.packet_type = type; + + aws_mutex_lock(&test_fixture->lock); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, "mqtt5 mock server received packet of type %s", aws_mqtt5_packet_type_to_c_string(type)); + aws_array_list_push_back(&test_fixture->server_received_packets, &packet_record); + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); + + int result = AWS_OP_SUCCESS; + aws_mqtt5_on_mock_server_packet_received_fn *packet_handler = + test_fixture->server_function_table->packet_handlers[type]; + if (packet_handler != NULL) { + result = + (*packet_handler)(packet_view, server_connection, server_connection->test_fixture->mock_server_user_data); + } + + return result; +} + +#ifdef _WIN32 +# define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" +#else +# define LOCAL_SOCK_TEST_PATTERN "testsock%llu.sock" +#endif + +static int s_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + + struct aws_mqtt5_server_mock_connection_context *server_connection = handler->impl; + + if (message->message_type != AWS_IO_MESSAGE_APPLICATION_DATA) { + return AWS_OP_ERR; + } + + struct aws_byte_cursor message_cursor = aws_byte_cursor_from_buf(&message->message_data); + + int result = aws_mqtt5_decoder_on_data_received(&server_connection->decoder, message_cursor); + if (result != AWS_OP_SUCCESS) { + aws_channel_shutdown(server_connection->channel, aws_last_error()); + goto done; + } + + aws_channel_slot_increment_read_window(slot, message->message_data.len); + +done: + + aws_mem_release(message->allocator, message); + + return AWS_OP_SUCCESS; +} + +static int s_shutdown( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + enum aws_channel_direction dir, + int error_code, + bool free_scarce_resources_immediately) { + + (void)handler; + + return aws_channel_slot_on_handler_shutdown_complete(slot, dir, error_code, free_scarce_resources_immediately); +} + +static size_t s_initial_window_size(struct aws_channel_handler *handler) { + (void)handler; + + return SIZE_MAX; +} + +static void s_destroy(struct aws_channel_handler *handler) { + struct aws_mqtt5_server_mock_connection_context *server_connection = handler->impl; + + aws_event_loop_cancel_task( + aws_channel_get_event_loop(server_connection->channel), &server_connection->service_task); + + aws_mqtt5_decoder_clean_up(&server_connection->decoder); + aws_mqtt5_encoder_clean_up(&server_connection->encoder); + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&server_connection->inbound_alias_resolver); + + aws_mem_release(server_connection->allocator, server_connection); +} + +static size_t s_message_overhead(struct aws_channel_handler *handler) { + (void)handler; + + return 0; +} + +static struct aws_channel_handler_vtable s_mqtt5_mock_server_channel_handler_vtable = { + .process_read_message = &s_process_read_message, + .process_write_message = NULL, + .increment_read_window = NULL, + .shutdown = &s_shutdown, + .initial_window_size = &s_initial_window_size, + .message_overhead = &s_message_overhead, + .destroy = &s_destroy, +}; + +static void s_mock_server_service_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + if (status != AWS_TASK_STATUS_RUN_READY) { + return; + } + + struct aws_mqtt5_server_mock_connection_context *server_connection = arg; + + aws_mqtt5_mock_server_service_fn *service_fn = + server_connection->test_fixture->server_function_table->service_task_fn; + if (service_fn != NULL) { + (*service_fn)(server_connection, server_connection->test_fixture->mock_server_user_data); + } + + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + uint64_t next_service_time = now + aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + + aws_event_loop_schedule_task_future( + aws_channel_get_event_loop(server_connection->channel), task, next_service_time); +} + +static void s_on_incoming_channel_setup_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = user_data; + + if (!error_code) { + struct aws_channel_slot *test_handler_slot = aws_channel_slot_new(channel); + aws_channel_slot_insert_end(channel, test_handler_slot); + + struct aws_mqtt5_server_mock_connection_context *server_connection = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_server_mock_connection_context)); + server_connection->allocator = test_fixture->allocator; + server_connection->channel = channel; + server_connection->test_fixture = test_fixture; + server_connection->slot = test_handler_slot; + server_connection->handler.alloc = server_connection->allocator; + server_connection->handler.vtable = &s_mqtt5_mock_server_channel_handler_vtable; + server_connection->handler.impl = server_connection; + + aws_task_init( + &server_connection->service_task, + s_mock_server_service_task_fn, + server_connection, + "mock_server_service_task_fn"); + aws_event_loop_schedule_task_now(aws_channel_get_event_loop(channel), &server_connection->service_task); + + aws_channel_slot_set_handler(server_connection->slot, &server_connection->handler); + + aws_mqtt5_encode_init_testing_function_table(&server_connection->encoding_table); + + struct aws_mqtt5_encoder_options encoder_options = { + .client = NULL, + .encoders = &server_connection->encoding_table, + }; + + aws_mqtt5_encoder_init(&server_connection->encoder, server_connection->allocator, &encoder_options); + + aws_mqtt5_decode_init_testing_function_table(&server_connection->decoding_table); + + struct aws_mqtt5_decoder_options decoder_options = { + .callback_user_data = server_connection, + .on_packet_received = s_aws_mqtt5_mock_test_fixture_on_packet_received_fn, + .decoder_table = &server_connection->decoding_table, + }; + + aws_mqtt5_decoder_init(&server_connection->decoder, server_connection->allocator, &decoder_options); + aws_mqtt5_inbound_topic_alias_resolver_init( + &server_connection->inbound_alias_resolver, server_connection->allocator); + aws_mqtt5_inbound_topic_alias_resolver_reset( + &server_connection->inbound_alias_resolver, test_fixture->maximum_inbound_topic_aliases); + aws_mqtt5_decoder_set_inbound_topic_alias_resolver( + &server_connection->decoder, &server_connection->inbound_alias_resolver); + + aws_mutex_lock(&test_fixture->lock); + test_fixture->server_channel = channel; + aws_mutex_unlock(&test_fixture->lock); + + /* + * Just like the tls tests in aws-c-io, it's possible for the server channel setup to execute after the client + * channel setup has already posted data to the socket. In this case, the read notification gets lost because + * the server hasn't subscribed to it yet and then we hang and time out. So do the same thing we do for + * tls server channel setup and force a read of the socket after we're fully initialized. + */ + aws_channel_trigger_read(channel); + } +} + +static void s_on_incoming_channel_shutdown_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + (void)error_code; + (void)channel; + (void)user_data; +} + +static void s_on_listener_destroy(struct aws_server_bootstrap *bootstrap, void *user_data) { + (void)bootstrap; + struct aws_mqtt5_client_mock_test_fixture *test_fixture = user_data; + aws_mutex_lock(&test_fixture->lock); + test_fixture->listener_destroyed = true; + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_one(&test_fixture->signal); +} + +static bool s_is_listener_destroyed(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + return test_fixture->listener_destroyed; +} + +static void s_wait_on_listener_cleanup(struct aws_mqtt5_client_mock_test_fixture *test_fixture) { + aws_mutex_lock(&test_fixture->lock); + aws_condition_variable_wait_pred(&test_fixture->signal, &test_fixture->lock, s_is_listener_destroyed, test_fixture); + aws_mutex_unlock(&test_fixture->lock); +} + +static bool s_has_client_terminated(void *arg) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = arg; + return test_fixture->client_terminated; +} + +static void s_wait_for_client_terminated(struct aws_mqtt5_client_mock_test_fixture *test_context) { + aws_mutex_lock(&test_context->lock); + aws_condition_variable_wait_pred(&test_context->signal, &test_context->lock, s_has_client_terminated, test_context); + aws_mutex_unlock(&test_context->lock); +} + +void s_aws_mqtt5_test_fixture_lifecycle_event_handler(const struct aws_mqtt5_client_lifecycle_event *event) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = event->user_data; + + struct aws_mqtt5_lifecycle_event_record *record = + aws_mem_calloc(test_fixture->allocator, 1, sizeof(struct aws_mqtt5_lifecycle_event_record)); + record->allocator = test_fixture->allocator; + aws_high_res_clock_get_ticks(&record->timestamp); + record->event = *event; + + if (event->settings != NULL) { + record->settings_storage = *event->settings; + record->event.settings = &record->settings_storage; + } + + if (event->disconnect_data != NULL) { + aws_mqtt5_packet_disconnect_storage_init( + &record->disconnect_storage, record->allocator, event->disconnect_data); + record->event.disconnect_data = &record->disconnect_storage.storage_view; + } + + if (event->connack_data != NULL) { + aws_mqtt5_packet_connack_storage_init(&record->connack_storage, record->allocator, event->connack_data); + record->event.connack_data = &record->connack_storage.storage_view; + } + + aws_mutex_lock(&test_fixture->lock); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "mqtt5 mock server received lifecycle event of type %s", + aws_mqtt5_client_lifecycle_event_type_to_c_string(event->event_type)); + aws_array_list_push_back(&test_fixture->lifecycle_events, &record); + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); + + aws_mqtt5_client_connection_event_callback_fn *event_callback = test_fixture->original_lifecycle_event_handler; + if (event_callback != NULL) { + struct aws_mqtt5_client_lifecycle_event event_copy = *event; + event_copy.user_data = test_fixture->original_lifecycle_event_handler_user_data; + + (*event_callback)(&event_copy); + } +} + +void s_aws_mqtt5_test_fixture_state_changed_callback( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state old_state, + enum aws_mqtt5_client_state new_state, + void *vtable_user_data) { + (void)old_state; + (void)client; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = vtable_user_data; + + aws_mutex_lock(&test_fixture->lock); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "mqtt5 mock server received client state change to %s", + aws_mqtt5_client_state_to_c_string(new_state)); + aws_array_list_push_back(&test_fixture->client_states, &new_state); + aws_mutex_unlock(&test_fixture->lock); +} + +static void s_aws_mqtt5_test_fixture_statistics_changed_callback( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + void *vtable_user_data) { + (void)operation; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture = vtable_user_data; + + struct aws_mqtt5_client_operation_statistics stats; + AWS_ZERO_STRUCT(stats); + + aws_mutex_lock(&test_fixture->lock); + aws_mqtt5_client_get_stats(client, &stats); + aws_array_list_push_back(&test_fixture->client_statistics, &stats); + aws_mutex_unlock(&test_fixture->lock); +} + +static void s_on_test_client_termination(void *user_data) { + struct aws_mqtt5_client_mock_test_fixture *test_fixture = user_data; + + aws_mutex_lock(&test_fixture->lock); + test_fixture->client_terminated = true; + aws_mutex_unlock(&test_fixture->lock); + aws_condition_variable_notify_all(&test_fixture->signal); +} + +int aws_mqtt5_client_mock_test_fixture_init( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *options) { + + AWS_ZERO_STRUCT(*test_fixture); + + test_fixture->allocator = allocator; + + aws_mutex_init(&test_fixture->lock); + aws_condition_variable_init(&test_fixture->signal); + + test_fixture->server_function_table = options->server_function_table; + test_fixture->mock_server_user_data = options->mock_server_user_data; + + struct aws_socket_options socket_options = { + .connect_timeout_ms = 1000, + .domain = AWS_SOCKET_LOCAL, + }; + + test_fixture->socket_options = socket_options; + test_fixture->server_elg = aws_event_loop_group_new_default(allocator, 1, NULL); + test_fixture->server_bootstrap = aws_server_bootstrap_new(allocator, test_fixture->server_elg); + + test_fixture->client_elg = aws_event_loop_group_new_default(allocator, 4, NULL); + struct aws_host_resolver_default_options resolver_options = { + .el_group = test_fixture->client_elg, + .max_entries = 1, + }; + test_fixture->host_resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = test_fixture->client_elg, + .user_data = test_fixture, + .host_resolver = test_fixture->host_resolver, + }; + + test_fixture->client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + uint64_t timestamp = 0; + ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); + + snprintf( + test_fixture->endpoint.address, + sizeof(test_fixture->endpoint.address), + LOCAL_SOCK_TEST_PATTERN, + (long long unsigned)timestamp); + + struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { + .bootstrap = test_fixture->server_bootstrap, + .host_name = test_fixture->endpoint.address, + .port = test_fixture->endpoint.port, + .socket_options = &test_fixture->socket_options, + .incoming_callback = s_on_incoming_channel_setup_fn, + .shutdown_callback = s_on_incoming_channel_shutdown_fn, + .destroy_callback = s_on_listener_destroy, + .user_data = test_fixture, + }; + test_fixture->listener = aws_server_bootstrap_new_socket_listener(&server_bootstrap_options); + + test_fixture->original_lifecycle_event_handler = options->client_options->lifecycle_event_handler; + test_fixture->original_lifecycle_event_handler_user_data = + options->client_options->lifecycle_event_handler_user_data; + options->client_options->lifecycle_event_handler = &s_aws_mqtt5_test_fixture_lifecycle_event_handler; + options->client_options->lifecycle_event_handler_user_data = test_fixture; + + options->client_options->host_name = aws_byte_cursor_from_c_str(test_fixture->endpoint.address); + options->client_options->port = test_fixture->endpoint.port; + options->client_options->socket_options = &test_fixture->socket_options; + options->client_options->bootstrap = test_fixture->client_bootstrap; + + options->client_options->client_termination_handler = s_on_test_client_termination; + options->client_options->client_termination_handler_user_data = test_fixture; + + test_fixture->client = aws_mqtt5_client_new(allocator, options->client_options); + + test_fixture->client_vtable = *aws_mqtt5_client_get_default_vtable(); + test_fixture->client_vtable.on_client_state_change_callback_fn = s_aws_mqtt5_test_fixture_state_changed_callback; + test_fixture->client_vtable.on_client_statistics_changed_callback_fn = + s_aws_mqtt5_test_fixture_statistics_changed_callback; + test_fixture->client_vtable.vtable_user_data = test_fixture; + + aws_mqtt5_client_set_vtable(test_fixture->client, &test_fixture->client_vtable); + + aws_array_list_init_dynamic( + &test_fixture->server_received_packets, allocator, 10, sizeof(struct aws_mqtt5_mock_server_packet_record)); + + aws_array_list_init_dynamic( + &test_fixture->lifecycle_events, allocator, 10, sizeof(struct aws_mqtt5_lifecycle_event_record *)); + + aws_array_list_init_dynamic(&test_fixture->client_states, allocator, 10, sizeof(enum aws_mqtt5_client_state)); + aws_array_list_init_dynamic( + &test_fixture->client_statistics, allocator, 10, sizeof(struct aws_mqtt5_client_operation_statistics)); + + return AWS_OP_SUCCESS; +} + +static void s_destroy_packet_storage( + void *packet_storage, + struct aws_allocator *allocator, + enum aws_mqtt5_packet_type packet_type) { + switch (packet_type) { + case AWS_MQTT5_PT_CONNECT: + aws_mqtt5_packet_connect_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_DISCONNECT: + aws_mqtt5_packet_disconnect_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_SUBSCRIBE: + aws_mqtt5_packet_subscribe_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_SUBACK: + aws_mqtt5_packet_suback_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_UNSUBSCRIBE: + aws_mqtt5_packet_unsubscribe_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_UNSUBACK: + aws_mqtt5_packet_unsuback_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_PUBLISH: + aws_mqtt5_packet_publish_storage_clean_up(packet_storage); + break; + + case AWS_MQTT5_PT_PUBACK: + /* TODO */ + break; + + case AWS_MQTT5_PT_PINGREQ: + AWS_FATAL_ASSERT(packet_storage == NULL); + break; + + default: + AWS_FATAL_ASSERT(false); + } + + if (packet_storage != NULL) { + aws_mem_release(allocator, packet_storage); + } +} + +static void s_destroy_lifecycle_event_storage(struct aws_mqtt5_lifecycle_event_record *event) { + aws_mqtt5_packet_connack_storage_clean_up(&event->connack_storage); + aws_mqtt5_packet_disconnect_storage_clean_up(&event->disconnect_storage); + + aws_mem_release(event->allocator, event); +} + +void aws_mqtt5_client_mock_test_fixture_clean_up(struct aws_mqtt5_client_mock_test_fixture *test_fixture) { + aws_mqtt5_client_release(test_fixture->client); + s_wait_for_client_terminated(test_fixture); + aws_client_bootstrap_release(test_fixture->client_bootstrap); + aws_host_resolver_release(test_fixture->host_resolver); + aws_server_bootstrap_destroy_socket_listener(test_fixture->server_bootstrap, test_fixture->listener); + + s_wait_on_listener_cleanup(test_fixture); + + aws_server_bootstrap_release(test_fixture->server_bootstrap); + aws_event_loop_group_release(test_fixture->server_elg); + aws_event_loop_group_release(test_fixture->client_elg); + + aws_thread_join_all_managed(); + + for (size_t i = 0; i < aws_array_list_length(&test_fixture->server_received_packets); ++i) { + struct aws_mqtt5_mock_server_packet_record *packet = NULL; + aws_array_list_get_at_ptr(&test_fixture->server_received_packets, (void **)&packet, i); + + s_destroy_packet_storage(packet->packet_storage, packet->storage_allocator, packet->packet_type); + } + + aws_array_list_clean_up(&test_fixture->server_received_packets); + + for (size_t i = 0; i < aws_array_list_length(&test_fixture->lifecycle_events); ++i) { + struct aws_mqtt5_lifecycle_event_record *event = NULL; + aws_array_list_get_at(&test_fixture->lifecycle_events, &event, i); + + s_destroy_lifecycle_event_storage(event); + } + + aws_array_list_clean_up(&test_fixture->lifecycle_events); + aws_array_list_clean_up(&test_fixture->client_states); + aws_array_list_clean_up(&test_fixture->client_statistics); + + aws_mutex_clean_up(&test_fixture->lock); + aws_condition_variable_clean_up(&test_fixture->signal); +} + +#define AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs, rhs) \ + if ((lhs) != (rhs)) { \ + return false; \ + } + +#define AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs, rhs) \ + if (((lhs) != NULL) != ((rhs) != NULL)) { \ + return false; \ + } \ + if (((lhs) != NULL) && ((rhs) != NULL) && (*(lhs) != *(rhs))) { \ + return false; \ + } + +#define AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs, rhs) \ + if (!aws_byte_cursor_eq(&(lhs), &(rhs))) { \ + return false; \ + } + +#define AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs, rhs) \ + if (((lhs) != NULL) != ((rhs) != NULL)) { \ + return false; \ + } \ + if (((lhs) != NULL) && ((rhs) != NULL) && (!aws_byte_cursor_eq(lhs, rhs))) { \ + return false; \ + } + +#define AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES(lhs_props, lhs_prop_count, rhs_props, rhs_prop_count) \ + if (aws_mqtt5_test_verify_user_properties_raw((lhs_prop_count), (lhs_props), (rhs_prop_count), (rhs_props)) != \ + AWS_OP_SUCCESS) { \ + return false; \ + } + +static bool s_aws_connect_packets_equal( + const struct aws_mqtt5_packet_connect_view *lhs, + const struct aws_mqtt5_packet_connect_view *rhs) { + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->keep_alive_interval_seconds, rhs->keep_alive_interval_seconds); + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->client_id, rhs->client_id); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->username, rhs->username); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->password, rhs->password); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->clean_start, rhs->clean_start); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( + lhs->session_expiry_interval_seconds, rhs->session_expiry_interval_seconds); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( + lhs->request_response_information, rhs->request_response_information); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->request_problem_information, rhs->request_problem_information); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->receive_maximum, rhs->receive_maximum); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->topic_alias_maximum, rhs->topic_alias_maximum); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->maximum_packet_size_bytes, rhs->maximum_packet_size_bytes); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->will_delay_interval_seconds, rhs->will_delay_interval_seconds); + + /* TODO: Check Will */ + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->authentication_method, rhs->authentication_method); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->authentication_data, rhs->authentication_data); + + return true; +} + +static bool s_aws_disconnect_packets_equal( + const struct aws_mqtt5_packet_disconnect_view *lhs, + const struct aws_mqtt5_packet_disconnect_view *rhs) { + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->reason_code, rhs->reason_code); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->reason_string, rhs->reason_string); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( + lhs->session_expiry_interval_seconds, rhs->session_expiry_interval_seconds); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->server_reference, rhs->server_reference); + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + +static bool s_are_subscription_views_equal( + const struct aws_mqtt5_subscription_view *lhs, + const struct aws_mqtt5_subscription_view *rhs) { + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->topic_filter, rhs->topic_filter); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->qos, rhs->qos); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->no_local, rhs->no_local); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->retain_as_published, rhs->retain_as_published); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->retain_handling_type, rhs->retain_handling_type); + + return true; +} + +static bool s_aws_subscribe_packets_equal( + const struct aws_mqtt5_packet_subscribe_view *lhs, + const struct aws_mqtt5_packet_subscribe_view *rhs) { + + // Don't check packet id intentionally + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_count, rhs->subscription_count); + for (size_t i = 0; i < lhs->subscription_count; ++i) { + const struct aws_mqtt5_subscription_view *lhs_sub_view = &lhs->subscriptions[i]; + const struct aws_mqtt5_subscription_view *rhs_sub_view = &rhs->subscriptions[i]; + + if (!s_are_subscription_views_equal(lhs_sub_view, rhs_sub_view)) { + return false; + } + } + + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->subscription_identifier, rhs->subscription_identifier); + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + +static bool s_aws_unsubscribe_packets_equal( + const struct aws_mqtt5_packet_unsubscribe_view *lhs, + const struct aws_mqtt5_packet_unsubscribe_view *rhs) { + + // Don't check packet id intentionally + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->topic_filter_count, rhs->topic_filter_count); + for (size_t i = 0; i < lhs->topic_filter_count; ++i) { + struct aws_byte_cursor lhs_topic_filter = lhs->topic_filters[i]; + struct aws_byte_cursor rhs_topic_filter = rhs->topic_filters[i]; + + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs_topic_filter, rhs_topic_filter); + } + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + +static bool s_aws_publish_packets_equal( + const struct aws_mqtt5_packet_publish_view *lhs, + const struct aws_mqtt5_packet_publish_view *rhs) { + + // Don't check packet id intentionally + + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->payload, rhs->payload); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->qos, rhs->qos); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->duplicate, rhs->duplicate); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->retain, rhs->retain); + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->topic, rhs->topic); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->payload_format, rhs->payload_format); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( + lhs->message_expiry_interval_seconds, rhs->message_expiry_interval_seconds); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->topic_alias, rhs->topic_alias); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->response_topic, rhs->response_topic); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->correlation_data, rhs->correlation_data); + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifier_count, rhs->subscription_identifier_count); + for (size_t i = 0; i < lhs->subscription_identifier_count; ++i) { + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifiers[i], rhs->subscription_identifiers[i]); + } + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + +static bool s_aws_puback_packets_equal( + const struct aws_mqtt5_packet_puback_view *lhs, + const struct aws_mqtt5_packet_puback_view *rhs) { + + // Don't check packet id intentionally + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->reason_code, rhs->reason_code); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->reason_string, rhs->reason_string); + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + +bool aws_mqtt5_client_test_are_packets_equal( + enum aws_mqtt5_packet_type packet_type, + void *lhs_packet_storage, + void *rhs_packet_storage) { + switch (packet_type) { + case AWS_MQTT5_PT_CONNECT: + return s_aws_connect_packets_equal( + &((struct aws_mqtt5_packet_connect_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_connect_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_DISCONNECT: + return s_aws_disconnect_packets_equal( + &((struct aws_mqtt5_packet_disconnect_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_disconnect_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_SUBSCRIBE: + return s_aws_subscribe_packets_equal( + &((struct aws_mqtt5_packet_subscribe_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_subscribe_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_UNSUBSCRIBE: + return s_aws_unsubscribe_packets_equal( + &((struct aws_mqtt5_packet_unsubscribe_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_unsubscribe_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_PUBLISH: + return s_aws_publish_packets_equal( + &((struct aws_mqtt5_packet_publish_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_publish_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_PUBACK: + return s_aws_puback_packets_equal( + &((struct aws_mqtt5_packet_puback_storage *)lhs_packet_storage)->storage_view, + &((struct aws_mqtt5_packet_puback_storage *)rhs_packet_storage)->storage_view); + + case AWS_MQTT5_PT_PINGREQ: + case AWS_MQTT5_PT_PINGRESP: + return true; + + default: + return false; + } +} + +size_t aws_mqtt5_linked_list_length(struct aws_linked_list *list) { + size_t length = 0; + struct aws_linked_list_node *node = aws_linked_list_begin(list); + while (node != aws_linked_list_end(list)) { + ++length; + node = aws_linked_list_next(node); + } + + return length; +} diff --git a/tests/v5/mqtt5_testing_utils.h b/tests/v5/mqtt5_testing_utils.h new file mode 100644 index 00000000..4ccb1b43 --- /dev/null +++ b/tests/v5/mqtt5_testing_utils.h @@ -0,0 +1,148 @@ +#ifndef MQTT_MQTT5_TESTING_UTILS_H +#define MQTT_MQTT5_TESTING_UTILS_H +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +struct aws_event_loop_group; + +struct aws_mqtt5_mock_server_packet_record { + struct aws_allocator *storage_allocator; + + uint64_t timestamp; + + void *packet_storage; + enum aws_mqtt5_packet_type packet_type; +}; + +struct aws_mqtt5_lifecycle_event_record { + struct aws_allocator *allocator; + + uint64_t timestamp; + + struct aws_mqtt5_client_lifecycle_event event; + + struct aws_mqtt5_negotiated_settings settings_storage; + struct aws_mqtt5_packet_disconnect_storage disconnect_storage; + struct aws_mqtt5_packet_connack_storage connack_storage; +}; + +struct aws_mqtt5_server_mock_connection_context { + struct aws_allocator *allocator; + + struct aws_channel *channel; + struct aws_channel_handler handler; + struct aws_channel_slot *slot; + + struct aws_mqtt5_encoder_function_table encoding_table; + struct aws_mqtt5_encoder encoder; + + struct aws_mqtt5_decoder_function_table decoding_table; + struct aws_mqtt5_decoder decoder; + struct aws_mqtt5_inbound_topic_alias_resolver inbound_alias_resolver; + + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + + struct aws_task service_task; +}; + +typedef int(aws_mqtt5_on_mock_server_packet_received_fn)( + void *packet_view, + struct aws_mqtt5_server_mock_connection_context *connection, + void *packet_received_user_data); + +typedef void( + aws_mqtt5_mock_server_service_fn)(struct aws_mqtt5_server_mock_connection_context *mock_server, void *user_data); + +struct aws_mqtt5_mock_server_vtable { + aws_mqtt5_on_mock_server_packet_received_fn *packet_handlers[16]; + aws_mqtt5_mock_server_service_fn *service_task_fn; +}; + +struct aws_mqtt5_client_mqtt5_mock_test_fixture_options { + struct aws_mqtt5_client_options *client_options; + const struct aws_mqtt5_mock_server_vtable *server_function_table; + + void *mock_server_user_data; +}; + +struct aws_mqtt5_client_mock_test_fixture { + struct aws_allocator *allocator; + + struct aws_event_loop_group *client_elg; + struct aws_event_loop_group *server_elg; + struct aws_host_resolver *host_resolver; + struct aws_client_bootstrap *client_bootstrap; + struct aws_server_bootstrap *server_bootstrap; + struct aws_socket_endpoint endpoint; + struct aws_socket_options socket_options; + struct aws_socket *listener; + struct aws_channel *server_channel; + + const struct aws_mqtt5_mock_server_vtable *server_function_table; + void *mock_server_user_data; + + struct aws_mqtt5_client_vtable client_vtable; + struct aws_mqtt5_client *client; + + aws_mqtt5_client_connection_event_callback_fn *original_lifecycle_event_handler; + void *original_lifecycle_event_handler_user_data; + + uint16_t maximum_inbound_topic_aliases; + + struct aws_mutex lock; + struct aws_condition_variable signal; + struct aws_array_list server_received_packets; + struct aws_array_list lifecycle_events; + struct aws_array_list client_states; + struct aws_array_list client_statistics; + bool listener_destroyed; + bool subscribe_complete; + bool disconnect_completion_callback_invoked; + bool client_terminated; + uint32_t total_pubacks_received; + uint32_t publishes_received; + uint32_t successful_pubacks_received; + uint32_t timeouts_received; + + uint32_t server_maximum_inflight_publishes; + uint32_t server_current_inflight_publishes; +}; + +int aws_mqtt5_test_verify_user_properties_raw( + size_t property_count, + const struct aws_mqtt5_user_property *properties, + size_t expected_count, + const struct aws_mqtt5_user_property *expected_properties); + +void aws_mqtt5_encode_init_testing_function_table(struct aws_mqtt5_encoder_function_table *function_table); + +void aws_mqtt5_decode_init_testing_function_table(struct aws_mqtt5_decoder_function_table *function_table); + +int aws_mqtt5_client_mock_test_fixture_init( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *options); + +void aws_mqtt5_client_mock_test_fixture_clean_up(struct aws_mqtt5_client_mock_test_fixture *test_fixture); + +bool aws_mqtt5_client_test_are_packets_equal( + enum aws_mqtt5_packet_type packet_type, + void *lhs_packet_storage, + void *rhs_packet_storage); + +size_t aws_mqtt5_linked_list_length(struct aws_linked_list *list); + +#endif /* MQTT_MQTT5_TESTING_UTILS_H */ diff --git a/tests/v5/mqtt5_topic_alias_tests.c b/tests/v5/mqtt5_topic_alias_tests.c new file mode 100644 index 00000000..803fa708 --- /dev/null +++ b/tests/v5/mqtt5_topic_alias_tests.c @@ -0,0 +1,589 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +#include +#include + +#include + +AWS_STATIC_STRING_FROM_LITERAL(s_topic1, "hello/world"); +AWS_STATIC_STRING_FROM_LITERAL(s_topic2, "this/is/a/longer/topic"); + +static int s_mqtt5_inbound_topic_alias_register_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_inbound_topic_alias_resolver resolver; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_init(&resolver, allocator)); + + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&resolver, 10)); + + ASSERT_FAILS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 0, aws_byte_cursor_from_string(s_topic1))); + ASSERT_FAILS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 11, aws_byte_cursor_from_string(s_topic1))); + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_inbound_topic_alias_register_failure, s_mqtt5_inbound_topic_alias_register_failure_fn) + +static int s_mqtt5_inbound_topic_alias_resolve_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_inbound_topic_alias_resolver resolver; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_init(&resolver, allocator)); + + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&resolver, 10)); + + ASSERT_SUCCESS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 1, aws_byte_cursor_from_string(s_topic1))); + ASSERT_SUCCESS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 10, aws_byte_cursor_from_string(s_topic2))); + + struct aws_byte_cursor topic1; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 1, &topic1)); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, topic1.ptr, topic1.len); + + struct aws_byte_cursor topic2; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 10, &topic2)); + ASSERT_BIN_ARRAYS_EQUALS(s_topic2->bytes, s_topic2->len, topic2.ptr, topic2.len); + + /* overwrite an existing alias to verify memory is cleaned up */ + ASSERT_SUCCESS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 10, aws_byte_cursor_from_string(s_topic1))); + + struct aws_byte_cursor topic3; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 10, &topic3)); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, topic3.ptr, topic3.len); + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_inbound_topic_alias_resolve_success, s_mqtt5_inbound_topic_alias_resolve_success_fn) + +static int s_mqtt5_inbound_topic_alias_resolve_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_inbound_topic_alias_resolver resolver; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_init(&resolver, allocator)); + + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&resolver, 10)); + + struct aws_byte_cursor topic; + ASSERT_FAILS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 0, &topic)); + ASSERT_FAILS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 11, &topic)); + ASSERT_FAILS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 10, &topic)); + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_inbound_topic_alias_resolve_failure, s_mqtt5_inbound_topic_alias_resolve_failure_fn) + +static int s_mqtt5_inbound_topic_alias_reset_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_inbound_topic_alias_resolver resolver; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_init(&resolver, allocator)); + + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&resolver, 10)); + + ASSERT_SUCCESS( + aws_mqtt5_inbound_topic_alias_resolver_register_alias(&resolver, 1, aws_byte_cursor_from_string(s_topic1))); + + struct aws_byte_cursor topic; + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 1, &topic)); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, topic.ptr, topic.len); + + ASSERT_SUCCESS(aws_mqtt5_inbound_topic_alias_resolver_reset(&resolver, 10)); + + ASSERT_FAILS(aws_mqtt5_inbound_topic_alias_resolver_resolve_alias(&resolver, 1, &topic)); + + aws_mqtt5_inbound_topic_alias_resolver_clean_up(&resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_inbound_topic_alias_reset, s_mqtt5_inbound_topic_alias_reset_fn) + +static int s_mqtt5_outbound_topic_alias_disabled_resolve_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_DISABLED); + ASSERT_NOT_NULL(resolver); + + struct aws_mqtt5_packet_publish_view publish_view = {.topic = aws_byte_cursor_from_string(s_topic1)}; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(0, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, outbound_topic.ptr, outbound_topic.len); + + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 0)); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_outbound_topic_alias_disabled_resolve_success, + s_mqtt5_outbound_topic_alias_disabled_resolve_success_fn) + +static int s_mqtt5_outbound_topic_alias_disabled_resolve_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_DISABLED); + ASSERT_NOT_NULL(resolver); + + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = + { + .ptr = NULL, + .len = 0, + }, + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_FAILS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_outbound_topic_alias_disabled_resolve_failure, + s_mqtt5_outbound_topic_alias_disabled_resolve_failure_fn) + +static int s_mqtt5_outbound_topic_alias_user_resolve_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); + + struct aws_mqtt5_packet_publish_view publish_view1 = { + .topic = aws_byte_cursor_from_string(s_topic1), + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + /* no alias case */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view1, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(0, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, outbound_topic.ptr, outbound_topic.len); + + uint16_t alias = 1; + struct aws_mqtt5_packet_publish_view publish_view2 = { + .topic = aws_byte_cursor_from_string(s_topic1), + .topic_alias = &alias, + }; + + outbound_alias_id = 0; + AWS_ZERO_STRUCT(outbound_topic); + + /* new valid alias assignment case */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view2, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(1, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, outbound_topic.ptr, outbound_topic.len); + + struct aws_mqtt5_packet_publish_view publish_view3 = { + .topic = aws_byte_cursor_from_string(s_topic1), + .topic_alias = &alias, + }; + + outbound_alias_id = 0; + AWS_ZERO_STRUCT(outbound_topic); + + /* reuse valid alias assignment case */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view3, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(1, outbound_alias_id); + ASSERT_INT_EQUALS(0, outbound_topic.len); + + /* switch topics but keep the alias, we should resolve to a full binding with the new topic */ + struct aws_mqtt5_packet_publish_view publish_view4 = { + .topic = aws_byte_cursor_from_string(s_topic2), + .topic_alias = &alias, + }; + + outbound_alias_id = 0; + AWS_ZERO_STRUCT(outbound_topic); + + /* reuse valid alias assignment case */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view4, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(1, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic2->bytes, s_topic2->len, outbound_topic.ptr, outbound_topic.len); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_user_resolve_success, s_mqtt5_outbound_topic_alias_user_resolve_success_fn) + +static int s_mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); + + uint16_t alias = 0; + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = aws_byte_cursor_from_string(s_topic1), + .topic_alias = &alias, + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_FAILS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias, + s_mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias_fn) + +static int s_mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); + + uint16_t alias = 6; + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = aws_byte_cursor_from_string(s_topic1), + .topic_alias = &alias, + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_FAILS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias, + s_mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias_fn) + +static int s_mqtt5_outbound_topic_alias_user_reset_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); + + uint16_t alias = 2; + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = aws_byte_cursor_from_string(s_topic1), + .topic_alias = &alias, + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + /* First, successfully bind an alias */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(2, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, outbound_topic.ptr, outbound_topic.len); + + /* Successfully use the alias */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(2, outbound_alias_id); + ASSERT_INT_EQUALS(0, outbound_topic.len); + + /* Reset */ + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); + + /* Fail to reuse the alias */ + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(2, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(s_topic1->bytes, s_topic1->len, outbound_topic.ptr, outbound_topic.len); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_user_reset, s_mqtt5_outbound_topic_alias_user_reset_fn) + +static int s_mqtt5_outbound_topic_alias_lru_zero_size_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_LRU); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 0); + + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = aws_byte_cursor_from_string(s_topic1), + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(0, outbound_alias_id); + ASSERT_BIN_ARRAYS_EQUALS(publish_view.topic.ptr, publish_view.topic.len, outbound_topic.ptr, outbound_topic.len); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_zero_size, s_mqtt5_outbound_topic_alias_lru_zero_size_fn) + +#define LRU_SEQUENCE_TEST_CACHE_SIZE 2 + +struct lru_test_operation { + struct aws_byte_cursor topic; + size_t expected_alias_id; + bool expected_reuse; +}; + +#define DEFINE_LRU_TEST_OPERATION(topic_type, alias_index, reused) \ + { \ + .topic = aws_byte_cursor_from_string(s_topic_##topic_type), .expected_alias_id = alias_index, \ + .expected_reuse = reused, \ + } + +static int s_perform_lru_test_operation( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + struct lru_test_operation *operation) { + + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = operation->topic, + }; + + uint16_t outbound_alias_id = 0; + struct aws_byte_cursor outbound_topic; + AWS_ZERO_STRUCT(outbound_topic); + + ASSERT_SUCCESS(aws_mqtt5_outbound_topic_alias_resolver_resolve_outbound_publish( + resolver, &publish_view, &outbound_alias_id, &outbound_topic)); + + ASSERT_INT_EQUALS(operation->expected_alias_id, outbound_alias_id); + if (operation->expected_reuse) { + ASSERT_INT_EQUALS(0, outbound_topic.len); + } else { + ASSERT_BIN_ARRAYS_EQUALS(operation->topic.ptr, operation->topic.len, outbound_topic.ptr, outbound_topic.len); + } + + return AWS_OP_SUCCESS; +} + +static int s_check_lru_sequence( + struct aws_mqtt5_outbound_topic_alias_resolver *resolver, + struct lru_test_operation *operations, + size_t operation_count) { + for (size_t i = 0; i < operation_count; ++i) { + struct lru_test_operation *operation = &operations[i]; + ASSERT_SUCCESS(s_perform_lru_test_operation(resolver, operation)); + } + + return AWS_OP_SUCCESS; +} + +static int s_perform_lru_sequence_test( + struct aws_allocator *allocator, + struct lru_test_operation *operations, + size_t operation_count) { + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_LRU); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, LRU_SEQUENCE_TEST_CACHE_SIZE); + + ASSERT_SUCCESS(s_check_lru_sequence(resolver, operations, operation_count)); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_topic_a, "topic/a"); +AWS_STATIC_STRING_FROM_LITERAL(s_topic_b, "b/topic"); +AWS_STATIC_STRING_FROM_LITERAL(s_topic_c, "topic/c"); + +static int s_mqtt5_outbound_topic_alias_lru_a_ar_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(a, 1, false), + DEFINE_LRU_TEST_OPERATION(a, 1, true), + }; + + ASSERT_SUCCESS(s_perform_lru_sequence_test(allocator, test_operations, AWS_ARRAY_SIZE(test_operations))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_a_ar, s_mqtt5_outbound_topic_alias_lru_a_ar_fn) + +static int s_mqtt5_outbound_topic_alias_lru_b_a_br_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(b, 1, false), + DEFINE_LRU_TEST_OPERATION(a, 2, false), + DEFINE_LRU_TEST_OPERATION(b, 1, true), + }; + + ASSERT_SUCCESS(s_perform_lru_sequence_test(allocator, test_operations, AWS_ARRAY_SIZE(test_operations))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_b_a_br, s_mqtt5_outbound_topic_alias_lru_b_a_br_fn) + +static int s_mqtt5_outbound_topic_alias_lru_a_b_ar_br_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(a, 1, false), + DEFINE_LRU_TEST_OPERATION(b, 2, false), + DEFINE_LRU_TEST_OPERATION(a, 1, true), + DEFINE_LRU_TEST_OPERATION(b, 2, true), + }; + + ASSERT_SUCCESS(s_perform_lru_sequence_test(allocator, test_operations, AWS_ARRAY_SIZE(test_operations))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_a_b_ar_br, s_mqtt5_outbound_topic_alias_lru_a_b_ar_br_fn) + +static int s_mqtt5_outbound_topic_alias_lru_a_b_c_br_cr_br_cr_a_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(a, 1, false), + DEFINE_LRU_TEST_OPERATION(b, 2, false), + DEFINE_LRU_TEST_OPERATION(c, 1, false), + DEFINE_LRU_TEST_OPERATION(b, 2, true), + DEFINE_LRU_TEST_OPERATION(c, 1, true), + DEFINE_LRU_TEST_OPERATION(b, 2, true), + DEFINE_LRU_TEST_OPERATION(c, 1, true), + DEFINE_LRU_TEST_OPERATION(a, 2, false), + }; + + ASSERT_SUCCESS(s_perform_lru_sequence_test(allocator, test_operations, AWS_ARRAY_SIZE(test_operations))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5_outbound_topic_alias_lru_a_b_c_br_cr_br_cr_a, + s_mqtt5_outbound_topic_alias_lru_a_b_c_br_cr_br_cr_a_fn) + +static int s_mqtt5_outbound_topic_alias_lru_a_b_c_a_cr_b_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(a, 1, false), + DEFINE_LRU_TEST_OPERATION(b, 2, false), + DEFINE_LRU_TEST_OPERATION(c, 1, false), + DEFINE_LRU_TEST_OPERATION(a, 2, false), + DEFINE_LRU_TEST_OPERATION(c, 1, true), + DEFINE_LRU_TEST_OPERATION(b, 2, false), + }; + + ASSERT_SUCCESS(s_perform_lru_sequence_test(allocator, test_operations, AWS_ARRAY_SIZE(test_operations))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_a_b_c_a_cr_b, s_mqtt5_outbound_topic_alias_lru_a_b_c_a_cr_b_fn) + +static int s_mqtt5_outbound_topic_alias_lru_a_b_reset_a_b_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt5_outbound_topic_alias_resolver *resolver = + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_LRU); + ASSERT_NOT_NULL(resolver); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, LRU_SEQUENCE_TEST_CACHE_SIZE); + + struct lru_test_operation test_operations[] = { + DEFINE_LRU_TEST_OPERATION(a, 1, false), + DEFINE_LRU_TEST_OPERATION(b, 2, false), + }; + + ASSERT_SUCCESS(s_check_lru_sequence(resolver, test_operations, AWS_ARRAY_SIZE(test_operations))); + + aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, LRU_SEQUENCE_TEST_CACHE_SIZE); + + ASSERT_SUCCESS(s_check_lru_sequence(resolver, test_operations, AWS_ARRAY_SIZE(test_operations))); + + aws_mqtt5_outbound_topic_alias_resolver_destroy(resolver); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_outbound_topic_alias_lru_a_b_reset_a_b, s_mqtt5_outbound_topic_alias_lru_a_b_reset_a_b_fn) diff --git a/tests/v5/mqtt5_utils_tests.c b/tests/v5/mqtt5_utils_tests.c new file mode 100644 index 00000000..16a67517 --- /dev/null +++ b/tests/v5/mqtt5_utils_tests.c @@ -0,0 +1,115 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +#include + +static int s_mqtt5_topic_skip_rules_prefix_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_byte_cursor skip_cursor; + struct aws_byte_cursor expected_cursor; + + /* nothing should be skipped */ + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("dont/skip/anything")); + expected_cursor = aws_byte_cursor_from_c_str("dont/skip/anything"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("")); + expected_cursor = aws_byte_cursor_from_c_str(""); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("/")); + expected_cursor = aws_byte_cursor_from_c_str("/"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws")); + expected_cursor = aws_byte_cursor_from_c_str("$aws"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules")); + expected_cursor = aws_byte_cursor_from_c_str("$aws/rules"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/")); + expected_cursor = aws_byte_cursor_from_c_str("$aws/rules/"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename")); + expected_cursor = aws_byte_cursor_from_c_str("$aws/rules/rulename"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + /* prefix should be skipped */ + skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename/")); + expected_cursor = aws_byte_cursor_from_c_str(""); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = + aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1/segment2")); + expected_cursor = aws_byte_cursor_from_c_str("segment1/segment2"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_topic_skip_rules_prefix, s_mqtt5_topic_skip_rules_prefix_fn) + +static int s_mqtt5_topic_get_segment_count_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + ASSERT_INT_EQUALS(1, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str(""))); + ASSERT_INT_EQUALS(1, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("hello"))); + ASSERT_INT_EQUALS(2, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("hello/"))); + ASSERT_INT_EQUALS(2, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("hello/world"))); + ASSERT_INT_EQUALS(3, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_INT_EQUALS(3, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("//"))); + ASSERT_INT_EQUALS(4, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("$SYS/bad/no/"))); + ASSERT_INT_EQUALS(1, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("$aws"))); + ASSERT_INT_EQUALS(8, aws_mqtt5_topic_get_segment_count(aws_byte_cursor_from_c_str("//a//b/c//"))); + + ASSERT_INT_EQUALS( + 2, + aws_mqtt5_topic_get_segment_count(aws_mqtt5_topic_skip_aws_iot_rules_prefix( + aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1/segment2")))); + ASSERT_INT_EQUALS( + 1, + aws_mqtt5_topic_get_segment_count( + aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1")))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_topic_get_segment_count, s_mqtt5_topic_get_segment_count_fn) + +static int s_mqtt5_shared_subscription_validation_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str(""))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("oof"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$sha"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share//"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share//test"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m+/"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m#/"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m"))); + ASSERT_FALSE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/"))); + + ASSERT_TRUE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/#"))); + ASSERT_TRUE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/great"))); + ASSERT_TRUE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/test/+"))); + ASSERT_TRUE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/+/test"))); + ASSERT_TRUE(aws_mqtt_is_topic_filter_shared_subscription(aws_byte_cursor_from_c_str("$share/m/test/#"))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_shared_subscription_validation, s_mqtt5_shared_subscription_validation_fn) diff --git a/tests/v5/rate_limiter_tests.c b/tests/v5/rate_limiter_tests.c new file mode 100644 index 00000000..dc58cdf6 --- /dev/null +++ b/tests/v5/rate_limiter_tests.c @@ -0,0 +1,343 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +#include +#include + +static uint64_t s_test_time = 0; +static int s_get_test_time(uint64_t *time) { + *time = s_test_time; + + return AWS_OP_SUCCESS; +} + +static int s_rate_limiter_token_bucket_init_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket token_bucket; + + struct aws_rate_limiter_token_bucket_options invalid_options1 = { + .clock_fn = s_get_test_time, + .tokens_per_second = 0, + .maximum_token_count = 1, + }; + + ASSERT_FAILS(aws_rate_limiter_token_bucket_init(&token_bucket, &invalid_options1)); + + struct aws_rate_limiter_token_bucket_options invalid_options2 = { + .clock_fn = s_get_test_time, + .tokens_per_second = 1000, + .maximum_token_count = 0, + }; + + ASSERT_FAILS(aws_rate_limiter_token_bucket_init(&token_bucket, &invalid_options2)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_init_invalid, s_rate_limiter_token_bucket_init_invalid_fn) + +static int s_rate_limiter_token_bucket_regeneration_integral_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .clock_fn = s_get_test_time, + .tokens_per_second = 5, + .maximum_token_count = 10, + }; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 0)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 1)); + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 0)); + ASSERT_FAILS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 1)); + ASSERT_INT_EQUALS(0, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 0)); + ASSERT_INT_EQUALS( + 2 * AWS_TIMESTAMP_NANOS, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + + /* one second elapsed, should be able to take 5 tokens now */ + s_test_time = AWS_TIMESTAMP_NANOS; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 5)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS(AWS_TIMESTAMP_NANOS, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + ASSERT_INT_EQUALS(0, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 1)); + ASSERT_INT_EQUALS(0, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 5)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 1)); + ASSERT_FAILS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 1)); + ASSERT_INT_EQUALS( + 2 * AWS_TIMESTAMP_NANOS, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + + /* three more elapsed seconds, regen should be maxed but clamped */ + s_test_time += 3 * (uint64_t)AWS_TIMESTAMP_NANOS; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 10)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 11)); + ASSERT_INT_EQUALS(0, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 10)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 1)); + ASSERT_FAILS(aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 1)); + ASSERT_INT_EQUALS( + 2 * (uint64_t)AWS_TIMESTAMP_NANOS, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_regeneration_integral, s_rate_limiter_token_bucket_regeneration_integral_fn) + +static int s_rate_limiter_token_bucket_regeneration_fractional_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .clock_fn = s_get_test_time, + .initial_token_count = 2, + .tokens_per_second = 3, + .maximum_token_count = 20, + }; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + uint64_t initial_wait_for_3 = AWS_TIMESTAMP_NANOS / 3 + 1; + uint64_t initial_wait_for_5 = AWS_TIMESTAMP_NANOS; + uint64_t initial_wait_for_6 = AWS_TIMESTAMP_NANOS + AWS_TIMESTAMP_NANOS / 3 + 1; + uint64_t initial_wait_for_7 = AWS_TIMESTAMP_NANOS + AWS_TIMESTAMP_NANOS / 3 * 2 + 1; + uint64_t initial_wait_for_8 = 2 * AWS_TIMESTAMP_NANOS; + uint64_t initial_wait_for_11 = 3 * (uint64_t)AWS_TIMESTAMP_NANOS; + + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 2)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_3, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_5, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_INT_EQUALS(initial_wait_for_6, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS(initial_wait_for_7, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7)); + ASSERT_INT_EQUALS(initial_wait_for_8, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 8)); + + /* one nanosecond elapsed, wait should shrink by 1 */ + s_test_time = 1; + ASSERT_INT_EQUALS( + initial_wait_for_3 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + /* idempotent check */ + ASSERT_INT_EQUALS( + initial_wait_for_3 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS( + initial_wait_for_5 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_INT_EQUALS( + initial_wait_for_6 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS( + initial_wait_for_7 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7)); + ASSERT_INT_EQUALS( + initial_wait_for_8 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 8)); + + s_test_time = 2; + ASSERT_INT_EQUALS( + initial_wait_for_3 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS( + initial_wait_for_5 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_INT_EQUALS( + initial_wait_for_6 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS( + initial_wait_for_7 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7)); + ASSERT_INT_EQUALS( + initial_wait_for_8 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 8)); + + /* one nanosecond short of a token's worth of time, nothing should change */ + s_test_time = AWS_TIMESTAMP_NANOS / 3; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 2)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS( + initial_wait_for_3 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS( + initial_wait_for_5 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_INT_EQUALS( + initial_wait_for_6 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS( + initial_wait_for_7 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7)); + ASSERT_INT_EQUALS( + initial_wait_for_8 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 8)); + + /* one more nanosecond, should give us a token */ + s_test_time += 1; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 3)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 4)); + ASSERT_INT_EQUALS( + initial_wait_for_3 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS( + initial_wait_for_5 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + ASSERT_INT_EQUALS( + initial_wait_for_6 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 6)); + ASSERT_INT_EQUALS( + initial_wait_for_7 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7)); + ASSERT_INT_EQUALS( + initial_wait_for_8 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 8)); + + /* now let's do multi-second plus fractional */ + s_test_time += (uint64_t)AWS_TIMESTAMP_NANOS * 2 + AWS_TIMESTAMP_NANOS / 2; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 10)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 11)); + ASSERT_INT_EQUALS(0, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 10)); + ASSERT_INT_EQUALS( + initial_wait_for_11 - s_test_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 11)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_regeneration_fractional, s_rate_limiter_token_bucket_regeneration_fractional_fn) + +#define REGENERATION_INTERVAL 9973 + +static int s_rate_limiter_token_bucket_fractional_iteration_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .clock_fn = s_get_test_time, + .initial_token_count = 0, + .tokens_per_second = 7, + .maximum_token_count = 100, + }; + + s_test_time = 47; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + size_t iterations = 2 * AWS_TIMESTAMP_NANOS / REGENERATION_INTERVAL + 3; + + uint64_t expected_wait_time = (uint64_t)3 * AWS_TIMESTAMP_NANOS; + + for (size_t i = 0; i < iterations; ++i) { + ASSERT_INT_EQUALS(expected_wait_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 21)); + + s_test_time += REGENERATION_INTERVAL; + expected_wait_time -= REGENERATION_INTERVAL; + } + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_fractional_iteration, s_rate_limiter_token_bucket_fractional_iteration_fn) + +#define LARGE_REGENERATION_INTERVAL (43 * (uint64_t)AWS_TIMESTAMP_NANOS + AWS_TIMESTAMP_NANOS / 13) + +static int s_rate_limiter_token_bucket_large_fractional_iteration_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .clock_fn = s_get_test_time, + .initial_token_count = 0, + .tokens_per_second = 7, + .maximum_token_count = 100000, + }; + + s_test_time = 47; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + uint64_t expected_wait_time = aws_timestamp_convert(1001, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + + while (expected_wait_time >= LARGE_REGENERATION_INTERVAL) { + ASSERT_INT_EQUALS( + expected_wait_time, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 7007)); + + s_test_time += LARGE_REGENERATION_INTERVAL; + expected_wait_time -= LARGE_REGENERATION_INTERVAL; + } + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rate_limiter_token_bucket_large_fractional_iteration, + s_rate_limiter_token_bucket_large_fractional_iteration_fn) + +#define TOKEN_REGENERATION_RATE_REAL 111111 + +static int s_rate_limiter_token_bucket_real_iteration_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .initial_token_count = 0, + .tokens_per_second = TOKEN_REGENERATION_RATE_REAL, + .maximum_token_count = 100, + }; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + uint64_t start_time = 0; + aws_high_res_clock_get_ticks(&start_time); + + uint64_t tokens_taken = 0; + while (tokens_taken < TOKEN_REGENERATION_RATE_REAL * 3) { + uint64_t wait = aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 1); + if (wait > 0) { + aws_thread_current_sleep(wait); + } + + if (!aws_rate_limiter_token_bucket_take_tokens(&token_bucket, 1)) { + ++tokens_taken; + } + } + + uint64_t end_time = 0; + aws_high_res_clock_get_ticks(&end_time); + + uint64_t elapsed_time = end_time - start_time; + uint64_t expected_elapsed = aws_timestamp_convert(3, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + + ASSERT_TRUE(elapsed_time > (uint64_t)(expected_elapsed * .99)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_real_iteration, s_rate_limiter_token_bucket_real_iteration_fn) + +static int s_rate_limiter_token_bucket_reset_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + (void)allocator; + + struct aws_rate_limiter_token_bucket_options options = { + .clock_fn = s_get_test_time, + .initial_token_count = 2, + .tokens_per_second = 3, + .maximum_token_count = 20, + }; + + struct aws_rate_limiter_token_bucket token_bucket; + ASSERT_SUCCESS(aws_rate_limiter_token_bucket_init(&token_bucket, &options)); + + uint64_t initial_wait_for_3 = AWS_TIMESTAMP_NANOS / 3 + 1; + uint64_t initial_wait_for_5 = AWS_TIMESTAMP_NANOS; + + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 2)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_3, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_5, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + + s_test_time = AWS_TIMESTAMP_NANOS * 2 + 1; + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 8)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 9)); + + aws_rate_limiter_token_bucket_reset(&token_bucket); + ASSERT_TRUE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 2)); + ASSERT_FALSE(aws_rate_limiter_token_bucket_can_take_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_3, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 3)); + ASSERT_INT_EQUALS(initial_wait_for_5, aws_rate_limiter_token_bucket_compute_wait_for_tokens(&token_bucket, 5)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rate_limiter_token_bucket_reset, s_rate_limiter_token_bucket_reset_fn) From 51f1ec928618d10305111d648643a7a01848e5ed Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Wed, 23 Nov 2022 14:17:25 -0500 Subject: [PATCH 19/98] Fix zip and update canary time (#228) Fix the zip command and update the canary time back to 7 hours. --- codebuild/mqtt-canary-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codebuild/mqtt-canary-test.yml b/codebuild/mqtt-canary-test.yml index 4f4bcbcc..d27f5b44 100644 --- a/codebuild/mqtt-canary-test.yml +++ b/codebuild/mqtt-canary-test.yml @@ -2,7 +2,7 @@ version: 0.2 env: shell: bash variables: - CANARY_DURATION: 7200 + CANARY_DURATION: 25200 CANARY_THREADS: 3 CANARY_TPS: 50 CANARY_CLIENT_COUNT: 10 @@ -49,7 +49,7 @@ phases: - zip -r latestBuild.zip build/install - aws s3 cp ./latestBuild.zip ${S3_DST}build/latest # upload latest source to S3 bucket - - zip -r latestSnapshot.zip **/*(.r) + - find * -type f ! -perm +r -exec zip latestSnapshot.zip {} + - aws s3 cp ./latestSnapshot.zip ${S3_DST}source/latest # ========== From 502cbfcf8eeefa65c8eaf73507c26c0fb0fbee5c Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Wed, 23 Nov 2022 16:45:59 -0500 Subject: [PATCH 20/98] Update canary comment to trigger CI (#229) --- codebuild/mqtt-canary-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codebuild/mqtt-canary-test.yml b/codebuild/mqtt-canary-test.yml index d27f5b44..2fbd3a9e 100644 --- a/codebuild/mqtt-canary-test.yml +++ b/codebuild/mqtt-canary-test.yml @@ -42,13 +42,13 @@ phases: # Canary related: # ========== - echo run canary test through wrapper - # start canary + # Start canary - python3 codebuild/CanaryWrapper.py --canary_executable $CANNARY_TEST_EXE --canary_arguments "-s ${CANARY_DURATION} -t ${CANARY_THREADS} -T ${CANARY_TPS} -C ${CANARY_CLIENT_COUNT} -l ${CANARY_LOG_FILE} -v ${CANARY_LOG_LEVEL} endpoint ${ENDPOINT}" --git_hash ${GIT_HASH} --git_repo_name $PACKAGE_NAME --codebuild_log_path $CODEBUILD_LOG_PATH - aws s3 cp ./${CANARY_LOG_FILE} ${S3_DST}log/${GIT_HASH}/ - # upload built canary test build result to s3 bucket + # Upload built canary test build result to s3 bucket - zip -r latestBuild.zip build/install - aws s3 cp ./latestBuild.zip ${S3_DST}build/latest - # upload latest source to S3 bucket + # Upload latest source to S3 bucket - find * -type f ! -perm +r -exec zip latestSnapshot.zip {} + - aws s3 cp ./latestSnapshot.zip ${S3_DST}source/latest # ========== From 11ae38e9b0325b38e1d6e1349d098225879d6ded Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 24 Nov 2022 09:02:47 -0800 Subject: [PATCH 21/98] Add developer preview notice at top of all public header files --- include/aws/mqtt/v5/mqtt5_client.h | 8 ++++++++ include/aws/mqtt/v5/mqtt5_packet_storage.h | 8 ++++++++ include/aws/mqtt/v5/mqtt5_types.h | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 247d9d2d..3641c7c5 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include diff --git a/include/aws/mqtt/v5/mqtt5_packet_storage.h b/include/aws/mqtt/v5/mqtt5_packet_storage.h index 44e7f633..9a7028f4 100644 --- a/include/aws/mqtt/v5/mqtt5_packet_storage.h +++ b/include/aws/mqtt/v5/mqtt5_packet_storage.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include diff --git a/include/aws/mqtt/v5/mqtt5_types.h b/include/aws/mqtt/v5/mqtt5_types.h index 5f13e867..f8db3951 100644 --- a/include/aws/mqtt/v5/mqtt5_types.h +++ b/include/aws/mqtt/v5/mqtt5_types.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include From 2f97156722bce8847120afce9b33fdeabc3c2285 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 25 Nov 2022 13:11:48 -0800 Subject: [PATCH 22/98] Add developer preview notice for MQTT5 at top of all public header files (#230) * Add developer preview notice at top of all public header files * Fix another overly-sensitive-to-timing test --- include/aws/mqtt/v5/mqtt5_client.h | 8 +++++++ include/aws/mqtt/v5/mqtt5_packet_storage.h | 8 +++++++ include/aws/mqtt/v5/mqtt5_types.h | 8 +++++++ tests/v5/mqtt5_client_tests.c | 25 ++++++---------------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 247d9d2d..3641c7c5 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include diff --git a/include/aws/mqtt/v5/mqtt5_packet_storage.h b/include/aws/mqtt/v5/mqtt5_packet_storage.h index 44e7f633..9a7028f4 100644 --- a/include/aws/mqtt/v5/mqtt5_packet_storage.h +++ b/include/aws/mqtt/v5/mqtt5_packet_storage.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include diff --git a/include/aws/mqtt/v5/mqtt5_types.h b/include/aws/mqtt/v5/mqtt5_types.h index 5f13e867..f8db3951 100644 --- a/include/aws/mqtt/v5/mqtt5_types.h +++ b/include/aws/mqtt/v5/mqtt5_types.h @@ -6,6 +6,14 @@ * SPDX-License-Identifier: Apache-2.0. */ +/** + * DEVELOPER PREVIEW DISCLAIMER + * + * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the + * preview window is especially valuable in shaping the final product. During the preview period we may make + * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. + */ + #include #include diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 662d6116..b318828b 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -2582,35 +2582,22 @@ static int s_mqtt5_client_flow_control_iot_core_throughput_fn(struct aws_allocat aws_mqtt_library_init(allocator); - uint64_t start_time1 = 0; - aws_high_res_clock_get_ticks(&start_time1); - - ASSERT_SUCCESS(s_do_iot_core_throughput_test(allocator, false)); - - uint64_t end_time1 = 0; - aws_high_res_clock_get_ticks(&end_time1); - - uint64_t test_time1 = end_time1 - start_time1; - - uint64_t start_time2 = 0; - aws_high_res_clock_get_ticks(&start_time2); + uint64_t start_time = 0; + aws_high_res_clock_get_ticks(&start_time); ASSERT_SUCCESS(s_do_iot_core_throughput_test(allocator, true)); - uint64_t end_time2 = 0; - aws_high_res_clock_get_ticks(&end_time2); - - uint64_t test_time2 = end_time2 - start_time2; + uint64_t end_time = 0; + aws_high_res_clock_get_ticks(&end_time); - /* We expect the unthrottled test to complete quickly */ - ASSERT_TRUE(test_time1 < AWS_TIMESTAMP_NANOS); + uint64_t test_time = end_time - start_time; /* * We expect the throttled version to take around 5 seconds, since we're sending 21 almost-max size (127k) packets * against a limit of 512KB/s. Since the packets are submitted immediately on CONNACK, the rate limiter * token bucket is starting at zero and so will give us immediate throttling. */ - ASSERT_TRUE(test_time2 > 5 * (uint64_t)AWS_TIMESTAMP_NANOS); + ASSERT_TRUE(test_time > 5 * (uint64_t)AWS_TIMESTAMP_NANOS); aws_mqtt_library_clean_up(); From cde06258edb8462c1d89f79f7eb124d782370387 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 25 Nov 2022 14:51:37 -0800 Subject: [PATCH 23/98] Sizet stats (#231) * Convert statistics to atomics, remove lock * Another flaky test --- .../aws/mqtt/private/v5/mqtt5_client_impl.h | 41 ++++++++++++---- source/v5/mqtt5_client.c | 47 ++++++++++--------- tests/v5/mqtt5_client_tests.c | 13 +++++ tests/v5/mqtt5_operation_and_storage_tests.c | 4 -- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index 38651647..05fe0a7b 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -302,6 +302,35 @@ struct aws_mqtt5_client_flow_control_state { struct aws_rate_limiter_token_bucket publish_throttle; }; +/** + * Contains some simple statistics about the current state of the client's queue of operations + */ +struct aws_mqtt5_client_operation_statistics_impl { + /* + * total number of operations submitted to the client that have not yet been completed. Unacked operations + * are a subset of this. + */ + struct aws_atomic_var incomplete_operation_count_atomic; + + /* + * total packet size of operations submitted to the client that have not yet been completed. Unacked operations + * are a subset of this. + */ + struct aws_atomic_var incomplete_operation_size_atomic; + + /* + * total number of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + struct aws_atomic_var unacked_operation_count_atomic; + + /* + * total packet size of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + struct aws_atomic_var unacked_operation_size_atomic; +}; + struct aws_mqtt5_client { struct aws_allocator *allocator; @@ -389,7 +418,7 @@ struct aws_mqtt5_client { * * clean_disconnect_error_code - the CLEAN_DISCONNECT state takes time to complete and we want to be able * to pass an error code from a prior event to the channel shutdown. This holds the "override" error code - * that we'd like to shutdown the channel with while CLEAN_DISCONNECT is processed. + * that we'd like to shut down the channel with while CLEAN_DISCONNECT is processed. * * handshake exists on websocket-configured clients between the transform completion timepoint and the * websocket setup callback. @@ -402,16 +431,8 @@ struct aws_mqtt5_client { */ struct aws_mqtt5_client_operational_state operational_state; - /* - * TODO: topic alias mappings, from-server and to-server have independent mappings - * - * From-server requires a single table - * To-server requires both a table and a list (for LRU) - */ - /* Statistics tracking operational state */ - struct aws_mutex operation_statistics_lock; - struct aws_mqtt5_client_operation_statistics operation_statistics; + struct aws_mqtt5_client_operation_statistics_impl operation_statistics_impl; /* * Wraps all state related to outbound flow control. diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index bd55b4bf..edb7ee3c 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -70,6 +70,13 @@ static bool s_aws_mqtt5_operation_is_retainable(struct aws_mqtt5_operation *oper } } +static void s_init_statistics(struct aws_mqtt5_client_operation_statistics_impl *stats) { + aws_atomic_store_int(&stats->incomplete_operation_count_atomic, 0); + aws_atomic_store_int(&stats->incomplete_operation_size_atomic, 0); + aws_atomic_store_int(&stats->unacked_operation_count_atomic, 0); + aws_atomic_store_int(&stats->unacked_operation_size_atomic, 0); +} + static bool s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy( struct aws_mqtt5_operation *operation, enum aws_mqtt5_client_operation_queue_behavior_type queue_behavior) { @@ -291,8 +298,6 @@ static void s_mqtt5_client_final_destroy(struct aws_mqtt5_client *client) { aws_mqtt5_inbound_topic_alias_resolver_clean_up(&client->inbound_topic_alias_resolver); aws_mqtt5_outbound_topic_alias_resolver_destroy(client->outbound_topic_alias_resolver); - aws_mutex_clean_up(&client->operation_statistics_lock); - aws_mem_release(client->allocator, client); if (client_termination_handler != NULL) { @@ -2071,7 +2076,7 @@ struct aws_mqtt5_client *aws_mqtt5_client_new( aws_mqtt5_client_options_storage_log(client->config, AWS_LL_DEBUG); - aws_mutex_init(&client->operation_statistics_lock); + s_init_statistics(&client->operation_statistics_impl); return client; @@ -3262,35 +3267,28 @@ void aws_mqtt5_client_statistics_change_operation_statistic_state( return; } - aws_mutex_lock(&client->operation_statistics_lock); - struct aws_mqtt5_client_operation_statistics *stats = &client->operation_statistics; + struct aws_mqtt5_client_operation_statistics_impl *stats = &client->operation_statistics_impl; if ((old_state_flags & AWS_MQTT5_OSS_INCOMPLETE) != (new_state_flags & AWS_MQTT5_OSS_INCOMPLETE)) { if ((new_state_flags & AWS_MQTT5_OSS_INCOMPLETE) != 0) { - ++stats->incomplete_operation_count; - stats->incomplete_operation_size += packet_size; + aws_atomic_fetch_add(&stats->incomplete_operation_count_atomic, 1); + aws_atomic_fetch_add(&stats->incomplete_operation_size_atomic, (size_t)packet_size); } else { - AWS_FATAL_ASSERT(stats->incomplete_operation_count > 0 && stats->incomplete_operation_size >= packet_size); - - --stats->incomplete_operation_count; - stats->incomplete_operation_size -= packet_size; + aws_atomic_fetch_sub(&stats->incomplete_operation_count_atomic, 1); + aws_atomic_fetch_sub(&stats->incomplete_operation_size_atomic, (size_t)packet_size); } } if ((old_state_flags & AWS_MQTT5_OSS_UNACKED) != (new_state_flags & AWS_MQTT5_OSS_UNACKED)) { if ((new_state_flags & AWS_MQTT5_OSS_UNACKED) != 0) { - ++stats->unacked_operation_count; - stats->unacked_operation_size += packet_size; + aws_atomic_fetch_add(&stats->unacked_operation_count_atomic, 1); + aws_atomic_fetch_add(&stats->unacked_operation_size_atomic, (size_t)packet_size); } else { - AWS_FATAL_ASSERT(stats->unacked_operation_count > 0 && stats->unacked_operation_size >= packet_size); - - --stats->unacked_operation_count; - stats->unacked_operation_size -= packet_size; + aws_atomic_fetch_sub(&stats->unacked_operation_count_atomic, 1); + aws_atomic_fetch_sub(&stats->unacked_operation_size_atomic, (size_t)packet_size); } } - aws_mutex_unlock(&client->operation_statistics_lock); - operation->statistic_state_flags = new_state_flags; if (client->vtable != NULL && client->vtable->on_client_statistics_changed_callback_fn != NULL) { @@ -3300,7 +3298,12 @@ void aws_mqtt5_client_statistics_change_operation_statistic_state( } void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt5_client_operation_statistics *stats) { - aws_mutex_lock(&client->operation_statistics_lock); - *stats = client->operation_statistics; - aws_mutex_unlock(&client->operation_statistics_lock); + stats->incomplete_operation_count = + (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.incomplete_operation_count_atomic); + stats->incomplete_operation_size = + (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.incomplete_operation_size_atomic); + stats->unacked_operation_count = + (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.unacked_operation_count_atomic); + stats->unacked_operation_size = + (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.unacked_operation_size_atomic); } diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index b318828b..27832291 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -1105,6 +1105,19 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void s_wait_for_connected_lifecycle_event(&test_context); s_wait_for_n_pingreqs(&ping_context); + /* + * There's a really unpleasant race condition where we can stop the client so fast (based on the mock + * server receiving PINGREQs that the mock server's socket gets closed underneath it as it is trying to + * write the PINGRESP back to the client, which in turn triggers channel shutdown where no further data + * is read from the socket, so we never see the DISCONNECT that the client actually sent. + * + * We're not able to wait on the PINGRESP because we have no insight into when it's received. So for now, + * we'll insert an artificial sleep before stopping the client. We should try and come up with a more + * elegant solution. + */ + + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + struct aws_mqtt5_packet_disconnect_view disconnect_view = { .reason_code = AWS_MQTT5_DRC_NORMAL_DISCONNECTION, }; diff --git a/tests/v5/mqtt5_operation_and_storage_tests.c b/tests/v5/mqtt5_operation_and_storage_tests.c index fb038779..2a8f5cd6 100644 --- a/tests/v5/mqtt5_operation_and_storage_tests.c +++ b/tests/v5/mqtt5_operation_and_storage_tests.c @@ -2076,8 +2076,6 @@ static void s_aws_mqtt5_operation_processing_test_context_init( /* this keeps operation processing tests from crashing when dereferencing config options */ test_context->dummy_client.config = &test_context->dummy_client_options; - aws_mutex_init(&test_context->dummy_client.operation_statistics_lock); - aws_array_list_init_dynamic(&test_context->output_io_messages, allocator, 0, sizeof(struct aws_io_message *)); struct aws_mqtt5_encoder_options verification_encoder_options = { @@ -2107,8 +2105,6 @@ static void s_aws_mqtt5_operation_processing_test_context_clean_up( aws_mqtt5_encoder_clean_up(&test_context->dummy_client.encoder); aws_mqtt5_client_operational_state_clean_up(&test_context->dummy_client.operational_state); - aws_mutex_clean_up(&test_context->dummy_client.operation_statistics_lock); - aws_array_list_clean_up(&test_context->completed_operation_error_codes); } From 3bead1e602a67ac91d4050be8a2edcd4d2cd9cbb Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Sat, 26 Nov 2022 19:22:54 -0500 Subject: [PATCH 24/98] Decoder logging and several canary improvements (#232) * Add additional logging to mqtt5 decoder, adjust canary to monitor OS metrics, fix canary bugs --- codebuild/CanaryWrapper.py | 133 ++++++++------- codebuild/CanaryWrapper_24_7.py | 188 +++++++++++---------- codebuild/CanaryWrapper_Classes.py | 90 ++++++---- codebuild/CanaryWrapper_MetricFunctions.py | 13 +- source/v5/mqtt5_decoder.c | 7 + 5 files changed, 235 insertions(+), 196 deletions(-) diff --git a/codebuild/CanaryWrapper.py b/codebuild/CanaryWrapper.py index fe894d8b..c089ffd5 100644 --- a/codebuild/CanaryWrapper.py +++ b/codebuild/CanaryWrapper.py @@ -123,7 +123,8 @@ new_metric_unit="Percent", new_metric_alarm_threshold=70, new_metric_reports_to_skip=1, - new_metric_alarm_severity=5) + new_metric_alarm_severity=5, + is_percent=True) data_snapshot.register_metric( new_metric_name="total_memory_usage_value", new_metric_function=get_metric_total_memory_usage_value, @@ -134,7 +135,8 @@ new_metric_unit="Percent", new_metric_alarm_threshold=70, new_metric_reports_to_skip=0, - new_metric_alarm_severity=5) + new_metric_alarm_severity=5, + is_percent=True) # Print diagnosis information data_snapshot.output_diagnosis_information(command_parser_arguments.dependencies) @@ -217,51 +219,25 @@ def application_thread(): finished_email_body = "MQTT5 Short Running Canary Wrapper has stopped." finished_email_body += "\n\n" - # Find out why we stopped - if (snapshot_monitor.had_internal_error == True): - if (snapshot_monitor.has_cut_ticket == True): - # We do not need to cut a ticket here - it's cut by the snapshot monitor! - print ("ERROR - Snapshot monitor stopped due to metric in alarm!", flush=True) - finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" - finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) - wrapper_error_occurred = True - else: - print ("ERROR - Snapshot monitor stopped due to internal error!", flush=True) - cut_ticket_using_cloudwatch( - git_repo_name=command_parser_arguments.git_repo_name, - git_hash=command_parser_arguments.git_hash, - git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, - git_fixed_namespace_text="mqtt5_canary", - cloudwatch_region="us-east-1", - ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, - ticket_reason="Snapshot monitor stopped due to internal error", - ticket_allow_duplicates=True, - ticket_category=command_parser_arguments.ticket_category, - ticket_item=command_parser_arguments.ticket_item, - ticket_group=command_parser_arguments.ticket_group, - ticket_type=command_parser_arguments.ticket_type, - ticket_severity=4) - wrapper_error_occurred = True - finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." - finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason - - elif (application_monitor.error_has_occurred == True): - if (application_monitor.error_due_to_credentials == True): - print ("INFO - Stopping application due to error caused by credentials") - print ("Please fix your credentials and then restart this application again", flush=True) - wrapper_error_occurred = True - send_finished_email = False - else: - # Is the error something in the canary failed? - if (application_monitor.error_code != 0): + try: + # Find out why we stopped + if (snapshot_monitor.had_internal_error == True): + if (snapshot_monitor.has_cut_ticket == True): + # We do not need to cut a ticket here - it's cut by the snapshot monitor! + print ("ERROR - Snapshot monitor stopped due to metric in alarm!", flush=True) + finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" + finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) + wrapper_error_occurred = True + else: + print ("ERROR - Snapshot monitor stopped due to internal error!", flush=True) cut_ticket_using_cloudwatch( git_repo_name=command_parser_arguments.git_repo_name, git_hash=command_parser_arguments.git_hash, git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, git_fixed_namespace_text="mqtt5_canary", cloudwatch_region="us-east-1", - ticket_description="The Short Running Canary exited with a non-zero exit code! This likely means something in the canary failed.", - ticket_reason="The Short Running Canary exited with a non-zero exit code", + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, + ticket_reason="Snapshot monitor stopped due to internal error", ticket_allow_duplicates=True, ticket_category=command_parser_arguments.ticket_category, ticket_item=command_parser_arguments.ticket_item, @@ -269,29 +245,60 @@ def application_thread(): ticket_type=command_parser_arguments.ticket_type, ticket_severity=4) wrapper_error_occurred = True - finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code! This means something in the Canary application itself failed" + finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason + + elif (application_monitor.error_has_occurred == True): + if (application_monitor.error_due_to_credentials == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again", flush=True) + wrapper_error_occurred = True + send_finished_email = False else: - print ("INFO - Stopping application. No error has occurred, application has stopped normally", flush=True) - finished_email_body += "Short Running Canary finished successfully and run without errors!" - wrapper_error_occurred = False - else: - print ("ERROR - Short Running Canary stopped due to unknown reason!", flush=True) - cut_ticket_using_cloudwatch( - git_repo_name=command_parser_arguments.git_repo_name, - git_hash=command_parser_arguments.git_hash, - git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, - git_fixed_namespace_text="mqtt5_canary", - cloudwatch_region="us-east-1", - ticket_description="The Short Running Canary stopped for an unknown reason!", - ticket_reason="The Short Running Canary stopped for unknown reason", - ticket_allow_duplicates=True, - ticket_category=command_parser_arguments.ticket_category, - ticket_item=command_parser_arguments.ticket_item, - ticket_group=command_parser_arguments.ticket_group, - ticket_type=command_parser_arguments.ticket_type, - ticket_severity=4) - wrapper_error_occurred = True - finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + # Is the error something in the canary failed? + if (application_monitor.error_code != 0): + cut_ticket_using_cloudwatch( + git_repo_name=command_parser_arguments.git_repo_name, + git_hash=command_parser_arguments.git_hash, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1", + ticket_description="The Short Running Canary exited with a non-zero exit code! This likely means something in the canary failed.", + ticket_reason="The Short Running Canary exited with a non-zero exit code", + ticket_allow_duplicates=True, + ticket_category=command_parser_arguments.ticket_category, + ticket_item=command_parser_arguments.ticket_item, + ticket_group=command_parser_arguments.ticket_group, + ticket_type=command_parser_arguments.ticket_type, + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code! This means something in the Canary application itself failed" + else: + print ("INFO - Stopping application. No error has occurred, application has stopped normally", flush=True) + application_monitor.print_stdout() + finished_email_body += "Short Running Canary finished successfully and run without errors!" + wrapper_error_occurred = False + else: + print ("ERROR - Short Running Canary stopped due to unknown reason!", flush=True) + cut_ticket_using_cloudwatch( + git_repo_name=command_parser_arguments.git_repo_name, + git_hash=command_parser_arguments.git_hash, + git_hash_as_namespace=command_parser_arguments.git_hash_as_namespace, + git_fixed_namespace_text="mqtt5_canary", + cloudwatch_region="us-east-1", + ticket_description="The Short Running Canary stopped for an unknown reason!", + ticket_reason="The Short Running Canary stopped for unknown reason", + ticket_allow_duplicates=True, + ticket_category=command_parser_arguments.ticket_category, + ticket_item=command_parser_arguments.ticket_item, + ticket_group=command_parser_arguments.ticket_group, + ticket_type=command_parser_arguments.ticket_type, + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + except Exception as e: + print ("ERROR: Could not (possibly) cut ticket due to exception!") + print ("Exception: " + str(e), flush=True) # Clean everything up and stop snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) diff --git a/codebuild/CanaryWrapper_24_7.py b/codebuild/CanaryWrapper_24_7.py index d4fa3a0c..877b8259 100644 --- a/codebuild/CanaryWrapper_24_7.py +++ b/codebuild/CanaryWrapper_24_7.py @@ -123,7 +123,8 @@ new_metric_unit="Percent", new_metric_alarm_threshold=70, new_metric_reports_to_skip=1, - new_metric_alarm_severity=5) + new_metric_alarm_severity=5, + is_percent=True) data_snapshot.register_metric( new_metric_name="total_memory_usage_value", new_metric_function=get_metric_total_memory_usage_value, @@ -134,7 +135,8 @@ new_metric_unit="Percent", new_metric_alarm_threshold=70, new_metric_reports_to_skip=0, - new_metric_alarm_severity=5) + new_metric_alarm_severity=5, + is_percent=True) data_snapshot.register_dashboard_widget("Process CPU Usage - Percentage", ["total_cpu_usage"], 60) data_snapshot.register_dashboard_widget("Process Memory Usage - Percentage", ["total_memory_usage_percent"], 60) @@ -250,120 +252,124 @@ def application_thread(): finished_email_body = "MQTT5 24/7 Canary Wrapper has stopped." finished_email_body += "\n\n" - # Find out why we stopped - # S3 Monitor - if (s3_monitor.had_internal_error == True): - if (s3_monitor.error_due_to_credentials == False): - print ("ERROR - S3 monitor stopped due to internal error!") - cut_ticket_using_cloudwatch( - git_repo_name=canary_local_git_repo_stub, - git_hash=canary_local_git_hash_stub, - git_hash_as_namespace=False, - git_fixed_namespace_text=canary_local_git_fixed_namespace, - cloudwatch_region=canary_region_stub, - ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + s3_monitor.internal_error_reason, - ticket_reason="S3 monitor stopped due to internal error", - ticket_allow_duplicates=True, - ticket_category="AWS", - ticket_type="SDKs and Tools", - ticket_item="IoT SDK for CPP", - ticket_group="AWS IoT Device SDK", - ticket_severity=4) - finished_email_body += "Failure due to S3 monitor stopping due to an internal error." - finished_email_body += " Reason given for error: " + s3_monitor.internal_error_reason - wrapper_error_occurred = True - # Snapshot Monitor - elif (snapshot_monitor.had_internal_error == True): - if (snapshot_monitor.has_cut_ticket == True): - # We do not need to cut a ticket here - it's cut by the snapshot monitor! - print ("ERROR - Snapshot monitor stopped due to metric in alarm!") - finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" - finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) - finished_email_body += "\nNOTE - this shouldn't occur in the 24/7 Canary! If it does, then the wrapper needs adjusting." - wrapper_error_occurred = True - else: - print ("ERROR - Snapshot monitor stopped due to internal error!") - cut_ticket_using_cloudwatch( - git_repo_name=canary_local_git_repo_stub, - git_hash=canary_local_git_hash_stub, - git_hash_as_namespace=False, - git_fixed_namespace_text=canary_local_git_fixed_namespace, - cloudwatch_region=canary_region_stub, - ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, - ticket_reason="Snapshot monitor stopped due to internal error", - ticket_allow_duplicates=True, - ticket_category="AWS", - ticket_type="SDKs and Tools", - ticket_item="IoT SDK for CPP", - ticket_group="AWS IoT Device SDK", - ticket_severity=4) - wrapper_error_occurred = True - finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." - finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason - # Application Monitor - elif (application_monitor.error_has_occurred == True): - if (application_monitor.error_due_to_credentials == True): - print ("INFO - Stopping application due to error caused by credentials") - print ("Please fix your credentials and then restart this application again") - wrapper_error_occurred = True - send_finished_email = False - else: - # Is the error something in the canary failed? - if (application_monitor.error_code != 0): + try: + # Find out why we stopped + # S3 Monitor + if (s3_monitor.had_internal_error == True): + if (s3_monitor.error_due_to_credentials == False): + print ("ERROR - S3 monitor stopped due to internal error!") cut_ticket_using_cloudwatch( git_repo_name=canary_local_git_repo_stub, git_hash=canary_local_git_hash_stub, git_hash_as_namespace=False, git_fixed_namespace_text=canary_local_git_fixed_namespace, cloudwatch_region=canary_region_stub, - ticket_description="The 24/7 Canary exited with a non-zero exit code! This likely means something in the canary failed.", - ticket_reason="The 24/7 Canary exited with a non-zero exit code", + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + s3_monitor.internal_error_reason, + ticket_reason="S3 monitor stopped due to internal error", ticket_allow_duplicates=True, ticket_category="AWS", ticket_type="SDKs and Tools", ticket_item="IoT SDK for CPP", ticket_group="AWS IoT Device SDK", - ticket_severity=3) + ticket_severity=4) + finished_email_body += "Failure due to S3 monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + s3_monitor.internal_error_reason + wrapper_error_occurred = True + # Snapshot Monitor + elif (snapshot_monitor.had_internal_error == True): + if (snapshot_monitor.has_cut_ticket == True): + # We do not need to cut a ticket here - it's cut by the snapshot monitor! + print ("ERROR - Snapshot monitor stopped due to metric in alarm!") + finished_email_body += "Failure due to required metrics being in alarm! A new ticket should have been cut!" + finished_email_body += "\nMetrics in Alarm: " + str(snapshot_monitor.cloudwatch_current_alarms_triggered) + finished_email_body += "\nNOTE - this shouldn't occur in the 24/7 Canary! If it does, then the wrapper needs adjusting." wrapper_error_occurred = True - finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code!" - finished_email_body += " This means something in the Canary application itself failed" else: + print ("ERROR - Snapshot monitor stopped due to internal error!") cut_ticket_using_cloudwatch( git_repo_name=canary_local_git_repo_stub, git_hash=canary_local_git_hash_stub, git_hash_as_namespace=False, git_fixed_namespace_text=canary_local_git_fixed_namespace, cloudwatch_region=canary_region_stub, - ticket_description="The 24/7 Canary exited with a zero exit code but did not restart!", - ticket_reason="The 24/7 Canary exited with a zero exit code but did not restart", + ticket_description="Snapshot monitor stopped due to internal error! Reason info: " + snapshot_monitor.internal_error_reason, + ticket_reason="Snapshot monitor stopped due to internal error", ticket_allow_duplicates=True, ticket_category="AWS", ticket_type="SDKs and Tools", ticket_item="IoT SDK for CPP", ticket_group="AWS IoT Device SDK", - ticket_severity=3) + ticket_severity=4) + wrapper_error_occurred = True + finished_email_body += "Failure due to Snapshot monitor stopping due to an internal error." + finished_email_body += " Reason given for error: " + snapshot_monitor.internal_error_reason + # Application Monitor + elif (application_monitor.error_has_occurred == True): + if (application_monitor.error_due_to_credentials == True): + print ("INFO - Stopping application due to error caused by credentials") + print ("Please fix your credentials and then restart this application again") wrapper_error_occurred = True - finished_email_body += "Failure due to MQTT5 application stopping and not automatically restarting!" - finished_email_body += " This shouldn't occur and means something is wrong with the Canary wrapper!" - # Other - else: - print ("ERROR - 24/7 Canary stopped due to unknown reason!") - cut_ticket_using_cloudwatch( - git_repo_name=canary_local_git_repo_stub, - git_hash=canary_local_git_hash_stub, - git_hash_as_namespace=False, - git_fixed_namespace_text=canary_local_git_fixed_namespace, - cloudwatch_region=canary_region_stub, - ticket_description="The 24/7 Canary stopped for an unknown reason!", - ticket_reason="The 24/7 Canary stopped for unknown reason", - ticket_allow_duplicates=True, - ticket_category="AWS", - ticket_type="SDKs and Tools", - ticket_item="IoT SDK for CPP", - ticket_group="AWS IoT Device SDK", - ticket_severity=3) - wrapper_error_occurred = True - finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + send_finished_email = False + else: + # Is the error something in the canary failed? + if (application_monitor.error_code != 0): + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary exited with a non-zero exit code! This likely means something in the canary failed.", + ticket_reason="The 24/7 Canary exited with a non-zero exit code", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application exiting with a non-zero exit code!" + finished_email_body += " This means something in the Canary application itself failed" + else: + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary exited with a zero exit code but did not restart!", + ticket_reason="The 24/7 Canary exited with a zero exit code but did not restart", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to MQTT5 application stopping and not automatically restarting!" + finished_email_body += " This shouldn't occur and means something is wrong with the Canary wrapper!" + # Other + else: + print ("ERROR - 24/7 Canary stopped due to unknown reason!") + cut_ticket_using_cloudwatch( + git_repo_name=canary_local_git_repo_stub, + git_hash=canary_local_git_hash_stub, + git_hash_as_namespace=False, + git_fixed_namespace_text=canary_local_git_fixed_namespace, + cloudwatch_region=canary_region_stub, + ticket_description="The 24/7 Canary stopped for an unknown reason!", + ticket_reason="The 24/7 Canary stopped for unknown reason", + ticket_allow_duplicates=True, + ticket_category="AWS", + ticket_type="SDKs and Tools", + ticket_item="IoT SDK for CPP", + ticket_group="AWS IoT Device SDK", + ticket_severity=3) + wrapper_error_occurred = True + finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" + except Exception as e: + print ("ERROR: Could not (possibly) cut ticket due to exception!") + print ("Exception: " + str(e), flush=True) # Clean everything up and stop snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) diff --git a/codebuild/CanaryWrapper_Classes.py b/codebuild/CanaryWrapper_Classes.py index c31c0d5d..202675df 100644 --- a/codebuild/CanaryWrapper_Classes.py +++ b/codebuild/CanaryWrapper_Classes.py @@ -18,7 +18,7 @@ class DataSnapshot_Metric(): def __init__(self, metric_name, metric_function, metric_dimensions=[], metric_unit="None", metric_alarm_threshold=None, metric_alarm_severity=6, - git_hash="", git_repo_name="", reports_to_skip=0): + git_hash="", git_repo_name="", reports_to_skip=0, is_percent=False): self.metric_name = metric_name self.metric_function = metric_function self.metric_dimensions = metric_dimensions @@ -29,6 +29,7 @@ def __init__(self, metric_name, metric_function, metric_dimensions=[], self.metric_value = None self.reports_to_skip = reports_to_skip self.metric_alarm_severity = metric_alarm_severity + self.is_percent = is_percent # Gets the latest metric value from the metric_function callback def get_metric_value(self, psutil_process : psutil.Process): @@ -486,8 +487,9 @@ def lambda_send_email(self, message, subject): # * (OPTIONAL) new_reports_to_skip is the number of reports this metric will return nothing, but will get it's value. # * Useful for CPU calculations that require deltas # * (OPTIONAL) new_metric_alarm_severity is the severity of the ticket if this alarm is triggered. A severity of 6+ means no ticket. + # * (OPTIONAL) is_percent whether or not to display the metric as a percent when printing it (default=false) def register_metric(self, new_metric_name, new_metric_function, new_metric_unit="None", - new_metric_alarm_threshold=None, new_metric_reports_to_skip=0, new_metric_alarm_severity=6): + new_metric_alarm_threshold=None, new_metric_reports_to_skip=0, new_metric_alarm_severity=6, is_percent=False): new_metric_dimensions = [] @@ -508,7 +510,8 @@ def register_metric(self, new_metric_name, new_metric_function, new_metric_unit= metric_alarm_severity=new_metric_alarm_severity, git_hash=self.git_hash, git_repo_name=self.git_repo_name, - reports_to_skip=new_metric_reports_to_skip + reports_to_skip=new_metric_reports_to_skip, + is_percent=is_percent ) self.metrics.append(new_metric) # append an empty list so we can track it's metrics over time @@ -567,12 +570,16 @@ def _find_cloudwatch_widget(self, name): # Prints the metrics to the console def export_metrics_console(self): datetime_now = datetime.datetime.now() - datetime_string = datetime_now.strftime("%d-%m-%Y/%H-%M-%S") + datetime_string = datetime_now.strftime("%d-%m-%Y/%H:%M:%S") self.print_message("\n[DataSnapshot] Metric report: " + str(self.metric_report_number) + " (" + datetime_string + ")") for metric in self.metrics: - self.print_message(" " + metric.metric_name + - " - value: " + str(metric.metric_value)) + if (metric.is_percent == True): + self.print_message(" " + metric.metric_name + + " - value: " + str(metric.metric_value) + "%") + else: + self.print_message(" " + metric.metric_name + + " - value: " + str(metric.metric_value)) self.print_message("") # Sends all registered metrics to Cloudwatch. @@ -886,13 +893,15 @@ def __init__(self, wrapper_application_path, wrapper_application_arguments, wrap self.wrapper_application_restart_on_finish = wrapper_application_restart_on_finish self.data_snapshot=data_snapshot + self.stdout_file_path = "Canary_Stdout_File.txt" + def start_monitoring(self): self.print_message("[ApplicationMonitor] Starting to monitor application...") if (self.application_process == None): try: canary_command = self.wrapper_application_path + " " + self.wrapper_application_arguments - self.application_process = subprocess.Popen(canary_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8") + self.application_process = subprocess.Popen(canary_command + " | tee " + self.stdout_file_path, shell=True) self.application_process_psutil = psutil.Process(self.application_process.pid) self.print_message ("[ApplicationMonitor] Application started...") except Exception as e: @@ -912,7 +921,8 @@ def restart_monitoring(self): try: self.stop_monitoring() self.start_monitoring() - self.print_message("[ApplicationMonitor] Restarted monitor application!") + self.print_message("\n[ApplicationMonitor] Restarted monitor application!") + self.print_message("================================================================================") except Exception as e: self.print_message("[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception!") self.print_message("[ApplicationMonitor] Exception: " + str(e)) @@ -934,18 +944,18 @@ def stop_monitoring(self): self.application_process.terminate() self.application_process.wait() self.print_message ("[ApplicationMonitor] Stopped monitor application!") - - if self.application_process.stdout != None: - self.print_message("\nApplication STDOUT:\n") - self.print_message("=========================================\n") - for line in self.application_process.stdout: - self.print_message(line) - self.application_process.stdout.close() - self.print_message("\n=========================================\n") self.application_process = None + self.print_stdout() else: self.print_message ("[ApplicationMonitor] ERROR - cannot stop monitor application because no process is found!") + def print_stdout(self): + # Print the STDOUT file + if (os.path.isfile(self.stdout_file_path)): + self.print_message("Just finished Application STDOUT: ") + with open(self.stdout_file_path, "r") as stdout_file: + self.print_message(stdout_file.read()) + os.remove(self.stdout_file_path) def monitor_loop_function(self, time_passed=30): if (self.application_process != None): @@ -1182,8 +1192,13 @@ def cut_ticket_using_cloudwatch( git_namespace_prepend_text = git_repo_name + "-" + git_hash git_metric_namespace = git_namespace_prepend_text - cloudwatch_client = boto3.client('cloudwatch', cloudwatch_region) - ticket_alarm_name = git_repo_name + "-" + git_hash + "-AUTO-TICKET" + try: + cloudwatch_client = boto3.client('cloudwatch', cloudwatch_region) + ticket_alarm_name = git_repo_name + "-" + git_hash + "-AUTO-TICKET" + except Exception as e: + print ("ERROR - could not create Cloudwatch client to make ticket metric alarm due to exception!") + print ("Exception: " + str(e), flush=True) + return new_metric_dimensions = [] if (git_hash_as_namespace == False): @@ -1203,23 +1218,28 @@ def cut_ticket_using_cloudwatch( ticket_alarm_description = f"AUTO CUT CANARY WRAPPER TICKET\n\nREASON: {ticket_reason}\n\nDESCRIPTION: {ticket_description}\n\n" - # Regsiter a metric alarm so it can auto-cut a ticket for us - cloudwatch_client.put_metric_alarm( - AlarmName=ticket_alarm_name, - AlarmDescription=ticket_alarm_description, - MetricName=ticket_alarm_name, - Namespace=git_metric_namespace, - Statistic="Maximum", - Dimensions=new_metric_dimensions, - Period=60, # How long (in seconds) is an evaluation period? - EvaluationPeriods=1, # How many periods does it need to be invalid for? - DatapointsToAlarm=1, # How many data points need to be invalid? - Threshold=1, - ComparisonOperator="GreaterThanOrEqualToThreshold", - # The data above does not really matter - it just needs to be valid input data. - # This is the part that tells Cloudwatch to cut the ticket - AlarmActions=[ticket_arn] - ) + # Register a metric alarm so it can auto-cut a ticket for us + try: + cloudwatch_client.put_metric_alarm( + AlarmName=ticket_alarm_name, + AlarmDescription=ticket_alarm_description, + MetricName=ticket_alarm_name, + Namespace=git_metric_namespace, + Statistic="Maximum", + Dimensions=new_metric_dimensions, + Period=60, # How long (in seconds) is an evaluation period? + EvaluationPeriods=1, # How many periods does it need to be invalid for? + DatapointsToAlarm=1, # How many data points need to be invalid? + Threshold=1, + ComparisonOperator="GreaterThanOrEqualToThreshold", + # The data above does not really matter - it just needs to be valid input data. + # This is the part that tells Cloudwatch to cut the ticket + AlarmActions=[ticket_arn] + ) + except Exception as e: + print ("ERROR - could not create ticket metric alarm due to exception!") + print ("Exception: " + str(e), flush=True) + return # Trigger the alarm so it cuts the ticket try: diff --git a/codebuild/CanaryWrapper_MetricFunctions.py b/codebuild/CanaryWrapper_MetricFunctions.py index a53d896d..05b1934e 100644 --- a/codebuild/CanaryWrapper_MetricFunctions.py +++ b/codebuild/CanaryWrapper_MetricFunctions.py @@ -12,24 +12,24 @@ def get_metric_total_cpu_usage(psutil_process : psutil.Process): if (psutil_process == None): print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) return None - # We always need to skip the first CPU poll on a new process + # We always need to skip the first CPU poll if (cache_cpu_psutil_process != psutil_process): - psutil_process.cpu_percent(interval=None) + psutil.cpu_percent(interval=None) cache_cpu_psutil_process = psutil_process return None - return psutil_process.cpu_percent(interval=None) + return psutil.cpu_percent(interval=None) except Exception as e: print ("ERROR - exception occurred gathering metrics!") print ("Exception: " + str(e), flush=True) return None - +# Note: This value is in BYTES. def get_metric_total_memory_usage_value(psutil_process : psutil.Process): try: if (psutil_process == None): print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) return None - return psutil_process.memory_info().rss + return psutil.virtual_memory()[3] except Exception as e: print ("ERROR - exception occurred gathering metrics!") print ("Exception: " + str(e), flush=True) @@ -41,9 +41,8 @@ def get_metric_total_memory_usage_percent(psutil_process : psutil.Process): if (psutil_process == None): print ("ERROR - No psutil.process passed! Cannot gather metric!", flush=True) return None - return psutil_process.memory_percent() + return psutil.virtual_memory()[2] except Exception as e: print ("ERROR - exception occurred gathering metrics!") print ("Exception: " + str(e), flush=True) return None - diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index c1840025..2ead2311 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -284,6 +284,7 @@ static int s_read_connack_property( done: if (result != AWS_OP_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read CONNACK property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -426,6 +427,7 @@ static int s_read_publish_property( done: if (result != AWS_OP_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read PUBLISH property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -589,6 +591,7 @@ static int s_read_puback_property( done: if (result != AWS_OP_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read PUBACK property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -697,6 +700,7 @@ static int s_read_suback_property( done: if (result != AWS_OP_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read SUBACK property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -795,6 +799,7 @@ static int s_read_unsuback_property( done: if (result != AWS_OP_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read UNSUBACK property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -924,6 +929,7 @@ static int s_read_disconnect_property( done: if (result == AWS_OP_ERR) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Read DISCONNECT property decode failure"); aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -1024,6 +1030,7 @@ static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) enum aws_mqtt5_packet_type packet_type = (enum aws_mqtt5_packet_type)(decoder->packet_first_byte >> 4); aws_mqtt5_decoding_fn *decoder_fn = decoder->options.decoder_table->decoders_by_packet_type[packet_type]; if (decoder_fn == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Decoder decode packet function missing"); return aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } From 14f2c95ee5d2522fc816ba82f4e0a0538beb9f0f Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Sun, 27 Nov 2022 14:08:31 -0800 Subject: [PATCH 25/98] Private API to dump full packet byte sequences to logging (#233) * Private API to dump full packet byte sequences to logging * Full packet logging in the canary, assuming appropriate log level --- bin/mqtt5canary/main.c | 3 + .../aws/mqtt/private/v5/mqtt5_client_impl.h | 5 ++ include/aws/mqtt/private/v5/mqtt5_decoder.h | 8 ++ source/v5/mqtt5_client.c | 4 + source/v5/mqtt5_decoder.c | 80 +++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/bin/mqtt5canary/main.c b/bin/mqtt5canary/main.c index f20850ce..d3b621f8 100644 --- a/bin/mqtt5canary/main.c +++ b/bin/mqtt5canary/main.c @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -881,6 +882,8 @@ int main(int argc, char **argv) { clients[i].shared_topic = shared_topic; clients[i].client = aws_mqtt5_client_new(allocator, &client_options); + aws_mqtt5_client_enable_full_packet_logging(clients[i].client); + aws_mqtt5_canary_operation_fn *operation_fn = s_aws_mqtt5_canary_operation_table.operation_by_operation_type[AWS_MQTT5_CANARY_OPERATION_START]; (*operation_fn)(&clients[i]); diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index 05fe0a7b..ec11b650 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -632,6 +632,11 @@ AWS_MQTT_API void aws_mqtt5_client_statistics_change_operation_statistic_state( */ AWS_MQTT_API const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_client_state state); +/* + * Temporary, private API to turn on total incoming packet logging at the byte level. + */ +AWS_MQTT_API void aws_mqtt5_client_enable_full_packet_logging(struct aws_mqtt5_client *client); + AWS_EXTERN_C_END #endif /* AWS_MQTT_MQTT5_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_decoder.h b/include/aws/mqtt/private/v5/mqtt5_decoder.h index e2294b24..a5d642bc 100644 --- a/include/aws/mqtt/private/v5/mqtt5_decoder.h +++ b/include/aws/mqtt/private/v5/mqtt5_decoder.h @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -100,6 +101,8 @@ struct aws_mqtt5_decoder { struct aws_byte_cursor packet_cursor; struct aws_mqtt5_inbound_topic_alias_resolver *topic_alias_resolver; + + struct aws_atomic_var is_full_packet_logging_enabled; }; AWS_EXTERN_C_BEGIN @@ -156,6 +159,11 @@ AWS_MQTT_API void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( */ AWS_MQTT_API extern const struct aws_mqtt5_decoder_function_table *g_aws_mqtt5_default_decoder_table; +/* + * Temporary, private API to turn on total incoming packet logging at the byte level. + */ +AWS_MQTT_API void aws_mqtt5_decoder_enable_full_packet_logging(struct aws_mqtt5_decoder *decoder); + AWS_EXTERN_C_END /* Decode helpers */ diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index edb7ee3c..0f638549 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -3307,3 +3307,7 @@ void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt stats->unacked_operation_size = (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.unacked_operation_size_atomic); } + +void aws_mqtt5_client_enable_full_packet_logging(struct aws_mqtt5_client *client) { + aws_mqtt5_decoder_enable_full_packet_logging(&client->decoder); +} diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index 2ead2311..34c2cccc 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -13,6 +13,10 @@ #define PUBLISH_PACKET_FIXED_HEADER_RETAIN_FLAG 1 #define PUBLISH_PACKET_FIXED_HEADER_QOS_FLAG 3 +static bool s_is_full_packet_logging_enabled(struct aws_mqtt5_decoder *decoder) { + return aws_atomic_load_int(&decoder->is_full_packet_logging_enabled) == 1; +} + static void s_reset_decoder_for_new_packet(struct aws_mqtt5_decoder *decoder) { aws_byte_buf_reset(&decoder->scratch_space, false); @@ -51,6 +55,14 @@ static int s_aws_mqtt5_decoder_read_packet_type_on_data( aws_byte_cursor_advance(data, 1); aws_byte_buf_append_byte_dynamic(&decoder->scratch_space, byte); + if (s_is_full_packet_logging_enabled(decoder)) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: Decoder FPL first byte: %2X", + decoder->options.callback_user_data, + (unsigned int)byte); + } + enum aws_mqtt5_packet_type packet_type = (byte >> 4); if (!s_is_decodable_packet_type(decoder, packet_type)) { @@ -121,6 +133,14 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_vli_on_data( struct aws_byte_cursor byte_cursor = aws_byte_cursor_advance(data, 1); aws_byte_buf_append_dynamic(&decoder->scratch_space, &byte_cursor); + if (s_is_full_packet_logging_enabled(decoder)) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: Decoder FPL remaining length VLI byte: %2X", + decoder->options.callback_user_data, + (unsigned int)(*byte_cursor.ptr)); + } + /* now try and decode a vli integer based on the range implied by the offset into the buffer */ struct aws_byte_cursor vli_cursor = { .ptr = decoder->scratch_space.buffer, @@ -1037,6 +1057,56 @@ static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) return (*decoder_fn)(decoder); } +#define FULL_PACKET_LOGGING_BYTES_PER_LINE 20 + +static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder) { + + struct aws_mqtt5_client *client = decoder->options.callback_user_data; + if (decoder->packet_cursor.len == 0) { + AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor empty", (void *)client); + return; + } + + char line_buffer[4096]; + size_t line_buffer_index = 0; + + size_t i = 0; + size_t packet_len = decoder->packet_cursor.len; + for (i = 0; i < packet_len; ++i) { + uint8_t packet_byte = *(decoder->packet_cursor.ptr + i); + if (i % FULL_PACKET_LOGGING_BYTES_PER_LINE) { + line_buffer_index += (size_t)snprintf( + line_buffer + line_buffer_index, + AWS_ARRAY_SIZE(line_buffer) - line_buffer_index, + ", 0x%02X", + (unsigned int)packet_byte); + } else { + line_buffer_index += (size_t)snprintf( + line_buffer + line_buffer_index, + AWS_ARRAY_SIZE(line_buffer) - line_buffer_index, + "0x%02X", + (unsigned int)packet_byte); + } + + if ((i + 1) % FULL_PACKET_LOGGING_BYTES_PER_LINE == 0) { + int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, + "id=%p: Decoder FPL packet cursor %12d: %s", + (void *)client, + byte_count, + line_buffer); + line_buffer_index = 0; + } + } + + if (line_buffer_index > 0) { + int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor %12d: %s", (void *)client, byte_count, line_buffer); + } +} + /* * (Streaming) Given a packet type and a variable length integer specifying the packet length, this state either * (1) decodes directly from the cursor if possible @@ -1068,6 +1138,10 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_packet_on_data decoder->packet_cursor = aws_byte_cursor_from_buf(&decoder->scratch_space); } + if (s_is_full_packet_logging_enabled(decoder)) { + s_log_packet_cursor(decoder); + } + if (s_aws_mqtt5_decoder_decode_packet(decoder)) { return AWS_MQTT5_DRT_ERROR; } @@ -1154,6 +1228,8 @@ int aws_mqtt5_decoder_init( return AWS_OP_ERR; } + aws_atomic_init_int(&decoder->is_full_packet_logging_enabled, 1); + return AWS_OP_SUCCESS; } @@ -1170,3 +1246,7 @@ void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( struct aws_mqtt5_inbound_topic_alias_resolver *resolver) { decoder->topic_alias_resolver = resolver; } + +void aws_mqtt5_decoder_enable_full_packet_logging(struct aws_mqtt5_decoder *decoder) { + aws_atomic_store_int(&decoder->is_full_packet_logging_enabled, 1); +} From bba6df7841701f6b5b3d8a8e5964c254003d3cca Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 30 Nov 2022 15:29:58 -0800 Subject: [PATCH 26/98] Do not turn on full packet logging by default (#235) --- source/v5/mqtt5_decoder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index 34c2cccc..61203efe 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -1228,7 +1228,7 @@ int aws_mqtt5_decoder_init( return AWS_OP_ERR; } - aws_atomic_init_int(&decoder->is_full_packet_logging_enabled, 1); + aws_atomic_init_int(&decoder->is_full_packet_logging_enabled, 0); return AWS_OP_SUCCESS; } From 4b5758677af810537ef0ecd6d4c9af4e5c423024 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Thu, 1 Dec 2022 16:19:01 -0500 Subject: [PATCH 27/98] Make the Canary more resistant (#236) --- codebuild/CanaryWrapper_Classes.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/codebuild/CanaryWrapper_Classes.py b/codebuild/CanaryWrapper_Classes.py index 202675df..b4eceb5e 100644 --- a/codebuild/CanaryWrapper_Classes.py +++ b/codebuild/CanaryWrapper_Classes.py @@ -612,8 +612,7 @@ def export_metrics_cloudwatch(self): except Exception as e: self.print_message("[DataSnapshot] Error - something when wrong posting cloudwatch metrics!") self.print_message("[DataSnapshot] Exception: " + str(e)) - self.abort_due_to_internal_error = True - self.abort_due_to_internal_error_reason = "Could not export Cloudwatch metrics due to exception in Cloudwatch client!" + self.print_message("[DataSnapshot] Not going to crash - just going to try again later") return # Call this at a set interval to post the metrics to Cloudwatch, etc. @@ -833,11 +832,7 @@ def monitor_loop_function(self, psutil_process : psutil.Process, time_passed=30) except Exception as e: self.print_message("[SnaptshotMonitor] ERROR - exception occurred posting metrics!") self.print_message("[SnaptshotMonitor] (Likely session credentials expired)") - - print (e, flush=True) - - self.had_internal_error = True - self.internal_error_reason = "Exception occurred posting metrics! Likely session credentials expired" + self.print_message("[SnaptshotMonitor] Not going to crash - just going to try again later") return # reset the timer @@ -1078,8 +1073,7 @@ def check_for_file_change(self): except Exception as e: self.print_message("[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception!") self.print_message("[S3Monitor] Exception: " + str(e)) - self.had_internal_error = True - self.internal_error_reason = "Could not check for S3 file due to exception in S3 client" + self.print_message("[S3Monitor] Going to try again later - will not crash Canary") def replace_current_file_for_new_file(self): From 22bc1d893757b5d65d47c923ded9fa21c2dd63f3 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Mon, 5 Dec 2022 16:43:20 -0500 Subject: [PATCH 28/98] More canary adjustments (#237) Handle exceptions better and do not crash on out of memory errors. --- codebuild/CanaryWrapper_Classes.py | 104 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/codebuild/CanaryWrapper_Classes.py b/codebuild/CanaryWrapper_Classes.py index b4eceb5e..01f39062 100644 --- a/codebuild/CanaryWrapper_Classes.py +++ b/codebuild/CanaryWrapper_Classes.py @@ -268,7 +268,19 @@ def cleanup(self, error_occurred=False): # Utility function for printing messages def print_message(self, message): if self.output_to_file == True: - self.output_file.write(message + "\n") + try: + if (self.output_file is None): + self.output_file = open(self.output_to_file_filepath, "w") + self.output_file.write(message + "\n") + + except Exception as ex: + print (f"[DataSnapshot] Exception trying to print to file: {ex}") + if (self.output_file is not None): + self.output_file.close() + self.output_file = None + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = "Could not print data to output file!" + if self.output_to_console == True: print(message, flush=True) @@ -303,10 +315,9 @@ def _init_cloudwatch_pre_first_run_dashboard(self): DashboardBody= new_dashboard_body_json) self.print_message("[DataSnapshot] Added Cloudwatch dashboard successfully") except Exception as e: - self.print_message("[DataSnapshot] ERROR - Cloudwatch client could not make dashboard due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - Cloudwatch client could not make dashboard due to exception: {e}") self.abort_due_to_internal_error = True - self.abort_due_to_internal_error_reason = "Cloudwatch client could not make dashboard due to exception" + self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make dashboard due to exception {e}" return # Utility function - The function that adds each individual metric alarm. @@ -330,8 +341,9 @@ def _add_cloudwatch_metric_alarm(self, metric): ComparisonOperator="GreaterThanOrEqualToThreshold", ) except Exception as e: - self.print_message("[DataSnapshot] ERROR - could not register alarm for metric due to exception: " + metric.metric_name) - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - could not register alarm for metric {metric.metric_name} due to exception: {e}") + self.abort_due_to_internal_error = True + self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make alarm due to exception: {e}" # Utility function - removes all the Cloudwatch alarms for the metrics def _cleanup_cloudwatch_alarms(self): @@ -341,8 +353,7 @@ def _cleanup_cloudwatch_alarms(self): if (not metric.metric_alarm_threshold is None): self.cloudwatch_client.delete_alarms(AlarmNames=[metric.metric_alarm_name]) except Exception as e: - self.print_message("[DataSnapshot] ERROR - could not delete alarms due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - could not delete alarms due to exception: {e}") # Utility function - removes all Cloudwatch dashboards created def _cleanup_cloudwatch_dashboard(self): @@ -351,8 +362,7 @@ def _cleanup_cloudwatch_dashboard(self): self.cloudwatch_client.delete_dashboards(DashboardNames=[self.cloudwatch_dashboard_name]) self.print_message("[DataSnapshot] Cloudwatch Dashboards deleted successfully!") except Exception as e: - self.print_message("[DataSnapshot] ERROR - dashboard cleaning function failed due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - dashboard cleaning function failed due to exception: {e}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Cloudwatch dashboard cleaning function failed due to exception" return @@ -382,20 +392,24 @@ def _check_cloudwatch_alarm_states(self): # Utility function - checks each individual alarm and returns a tuple with the following format: # [Boolean (False if the alarm is in the ALARM state, otherwise it is true), String (name of the alarm), Int (severity of alarm)] def _check_cloudwatch_alarm_state_metric(self, metric): - alarms_response = self.cloudwatch_client.describe_alarms_for_metric( - MetricName=metric.metric_name, - Namespace=self.git_metric_namespace, - Dimensions=metric.metric_dimensions) + try: + alarms_response = self.cloudwatch_client.describe_alarms_for_metric( + MetricName=metric.metric_name, + Namespace=self.git_metric_namespace, + Dimensions=metric.metric_dimensions) - return_result = [True, None, metric.metric_alarm_severity] + return_result = [True, None, metric.metric_alarm_severity] - for metric_alarm_dict in alarms_response["MetricAlarms"]: - if metric_alarm_dict["StateValue"] == "ALARM": - return_result[0] = False - return_result[1] = metric_alarm_dict["AlarmName"] - break + for metric_alarm_dict in alarms_response["MetricAlarms"]: + if metric_alarm_dict["StateValue"] == "ALARM": + return_result[0] = False + return_result[1] = metric_alarm_dict["AlarmName"] + break - return return_result + return return_result + except Exception as e: + self.print_message(f"[DataSnapshot] ERROR - checking cloudwatch alarm failed due to exception: {e}") + return None # Exports a file with the same name as the commit Git hash to an S3 bucket in a folder with the Git repo name. # By default, this file will only contain the Git hash. @@ -447,8 +461,7 @@ def export_result_to_s3_bucket(self, copy_output_log=False, log_is_error=False): self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/Failed_Logs/" + self.datetime_string + "/" + self.git_hash + ".log") self.print_message("[DataSnapshot] Uploaded to S3!") except Exception as e: - self.print_message("[DataSnapshot] ERROR - could not upload to S3 due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - could not upload to S3 due to exception: {e}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "S3 client had exception and therefore could not upload log!" os.remove(self.git_hash + ".log") @@ -461,7 +474,6 @@ def export_result_to_s3_bucket(self, copy_output_log=False, log_is_error=False): # * (REQUIRED) message is the message you want to send in the body of the email # * (REQUIRED) subject is the subject that the email will be sent with def lambda_send_email(self, message, subject): - payload = {"Message":message, "Subject":subject} payload_string = json.dumps(payload) @@ -473,8 +485,7 @@ def lambda_send_email(self, message, subject): Payload=payload_string ) except Exception as e: - self.print_message("[DataSnapshot] ERROR - could not send email via Lambda due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] ERROR - could not send email via Lambda due to exception: {e}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Lambda email function had an exception!" return @@ -587,7 +598,7 @@ def export_metrics_console(self): # This is just the Cloudwatch part of that loop. def export_metrics_cloudwatch(self): if (self.cloudwatch_client == None): - self.print_message("[DataSnapshot] Error - cannot export Cloudwatch metrics! Cloudwatch was not initiallized.") + self.print_message("[DataSnapshot] Error - cannot export Cloudwatch metrics! Cloudwatch was not initialized.") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Could not export Cloudwatch metrics due to no Cloudwatch client initialized!" return @@ -610,8 +621,7 @@ def export_metrics_cloudwatch(self): MetricData=metrics_data) self.print_message("[DataSnapshot] Metrics sent to Cloudwatch.") except Exception as e: - self.print_message("[DataSnapshot] Error - something when wrong posting cloudwatch metrics!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] Error - something when wrong posting cloudwatch metrics. Exception: {e}") self.print_message("[DataSnapshot] Not going to crash - just going to try again later") return @@ -731,8 +741,7 @@ def register_metric(self, new_metric_name, new_metric_function, new_metric_unit= new_metric_reports_to_skip=new_metric_reports_to_skip, new_metric_alarm_severity=new_metric_alarm_severity) except Exception as e: - self.print_message("[SnaptshotMonitor] ERROR - could not register metric in data snapshot due to exception!") - self.print_message("[SnaptshotMonitor] Exception: " + str(e)) + self.print_message(f"[SnaptshotMonitor] ERROR - could not register metric in data snapshot due to exception: {e}") self.had_internal_error = True self.internal_error_reason = "Could not register metric in data snapshot due to exception" return @@ -887,7 +896,7 @@ def __init__(self, wrapper_application_path, wrapper_application_arguments, wrap self.wrapper_application_arguments = wrapper_application_arguments self.wrapper_application_restart_on_finish = wrapper_application_restart_on_finish self.data_snapshot=data_snapshot - + self.still_running_wait_number = 0 self.stdout_file_path = "Canary_Stdout_File.txt" def start_monitoring(self): @@ -919,8 +928,7 @@ def restart_monitoring(self): self.print_message("\n[ApplicationMonitor] Restarted monitor application!") self.print_message("================================================================================") except Exception as e: - self.print_message("[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception!") - self.print_message("[ApplicationMonitor] Exception: " + str(e)) + self.print_message(f"[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception: {e}") self.error_has_occurred = True self.error_reason = "Could not restart Canary/Application due to exception" self.error_code = 1 @@ -948,9 +956,12 @@ def print_stdout(self): # Print the STDOUT file if (os.path.isfile(self.stdout_file_path)): self.print_message("Just finished Application STDOUT: ") - with open(self.stdout_file_path, "r") as stdout_file: - self.print_message(stdout_file.read()) - os.remove(self.stdout_file_path) + try: + with open(self.stdout_file_path, "r") as stdout_file: + self.print_message(stdout_file.read()) + os.remove(self.stdout_file_path) + except Exception as e: + self.print_message(f"[ApplicationMonitor] ERROR - Could not print Canary/Application stdout to exception: {e}") def monitor_loop_function(self, time_passed=30): if (self.application_process != None): @@ -968,7 +979,6 @@ def monitor_loop_function(self, time_passed=30): # If it is not none, then the application finished if (application_process_return_code != None): - self.print_message("[ApplicationMonitor] Monitor application has stopped! Processing result...") if (application_process_return_code != 0): @@ -989,7 +999,11 @@ def monitor_loop_function(self, time_passed=30): self.error_reason = "Canary Application Finished" self.error_code = 0 else: - self.print_message("[ApplicationMonitor] Monitor application is still running...") + # Only print that we are still running the monitor application every 4 times to reduce log spam. + self.still_running_wait_number =+ 1 + if self.still_running_wait_number >= 4: + self.print_message("[ApplicationMonitor] Monitor application is still running...") + self.still_running_wait_number = 0 def cleanup_monitor(self, error_occurred=False): pass @@ -1071,8 +1085,7 @@ def check_for_file_change(self): return except Exception as e: - self.print_message("[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception!") - self.print_message("[S3Monitor] Exception: " + str(e)) + self.print_message(f"[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception: {e}") self.print_message("[S3Monitor] Going to try again later - will not crash Canary") @@ -1190,8 +1203,7 @@ def cut_ticket_using_cloudwatch( cloudwatch_client = boto3.client('cloudwatch', cloudwatch_region) ticket_alarm_name = git_repo_name + "-" + git_hash + "-AUTO-TICKET" except Exception as e: - print ("ERROR - could not create Cloudwatch client to make ticket metric alarm due to exception!") - print ("Exception: " + str(e), flush=True) + print (f"ERROR - could not create Cloudwatch client to make ticket metric alarm due to exception: {e}", flush=True) return new_metric_dimensions = [] @@ -1231,8 +1243,7 @@ def cut_ticket_using_cloudwatch( AlarmActions=[ticket_arn] ) except Exception as e: - print ("ERROR - could not create ticket metric alarm due to exception!") - print ("Exception: " + str(e), flush=True) + print (f"ERROR - could not create ticket metric alarm due to exception: {e}", flush=True) return # Trigger the alarm so it cuts the ticket @@ -1242,8 +1253,7 @@ def cut_ticket_using_cloudwatch( StateValue="ALARM", StateReason="AUTO TICKET CUT") except Exception as e: - print ("ERROR - could not cut ticket due to exception!") - print ("Exception: " + str(e), flush=True) + print (f"ERROR - could not cut ticket due to exception: {e}", flush=True) return print("Waiting for ticket metric to trigger...", flush=True) From dc3425edb4f45ca77c48a70ddee75df6a500c371 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Thu, 8 Dec 2022 17:18:59 -0500 Subject: [PATCH 29/98] TMP: Only print packet on error (#238) Print the packet data only when a decoder has an error. --- source/v5/mqtt5_decoder.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index 61203efe..b365b108 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -56,7 +56,7 @@ static int s_aws_mqtt5_decoder_read_packet_type_on_data( aws_byte_buf_append_byte_dynamic(&decoder->scratch_space, byte); if (s_is_full_packet_logging_enabled(decoder)) { - AWS_LOGF_DEBUG( + AWS_LOGF_ERROR( AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL first byte: %2X", decoder->options.callback_user_data, @@ -134,7 +134,7 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_vli_on_data( aws_byte_buf_append_dynamic(&decoder->scratch_space, &byte_cursor); if (s_is_full_packet_logging_enabled(decoder)) { - AWS_LOGF_DEBUG( + AWS_LOGF_ERROR( AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL remaining length VLI byte: %2X", decoder->options.callback_user_data, @@ -1059,11 +1059,11 @@ static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) #define FULL_PACKET_LOGGING_BYTES_PER_LINE 20 -static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder) { +static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder, struct aws_byte_cursor *packet_cursor) { struct aws_mqtt5_client *client = decoder->options.callback_user_data; - if (decoder->packet_cursor.len == 0) { - AWS_LOGF_DEBUG(AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor empty", (void *)client); + if (packet_cursor->len == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor empty", (void *)client); return; } @@ -1071,9 +1071,9 @@ static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder) { size_t line_buffer_index = 0; size_t i = 0; - size_t packet_len = decoder->packet_cursor.len; + size_t packet_len = packet_cursor->len; for (i = 0; i < packet_len; ++i) { - uint8_t packet_byte = *(decoder->packet_cursor.ptr + i); + uint8_t packet_byte = *(packet_cursor->ptr + i); if (i % FULL_PACKET_LOGGING_BYTES_PER_LINE) { line_buffer_index += (size_t)snprintf( line_buffer + line_buffer_index, @@ -1090,7 +1090,7 @@ static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder) { if ((i + 1) % FULL_PACKET_LOGGING_BYTES_PER_LINE == 0) { int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); - AWS_LOGF_DEBUG( + AWS_LOGF_ERROR( AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor %12d: %s", (void *)client, @@ -1102,7 +1102,7 @@ static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder) { if (line_buffer_index > 0) { int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); - AWS_LOGF_DEBUG( + AWS_LOGF_ERROR( AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor %12d: %s", (void *)client, byte_count, line_buffer); } } @@ -1138,11 +1138,15 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_packet_on_data decoder->packet_cursor = aws_byte_cursor_from_buf(&decoder->scratch_space); } + struct aws_byte_cursor copy_cursor; if (s_is_full_packet_logging_enabled(decoder)) { - s_log_packet_cursor(decoder); + copy_cursor = aws_byte_cursor_from_array(decoder->packet_cursor.ptr, decoder->packet_cursor.len); } if (s_aws_mqtt5_decoder_decode_packet(decoder)) { + if (s_is_full_packet_logging_enabled(decoder)) { + s_log_packet_cursor(decoder, ©_cursor); + } return AWS_MQTT5_DRT_ERROR; } From 709047b9fd1b1137fdfaaefe5116710191186539 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Dec 2022 17:57:36 -0800 Subject: [PATCH 30/98] Disable outbound topic aliasing by default (#239) * Disable outbound topic aliasing by default Co-authored-by: Bret Ambrose --- include/aws/mqtt/v5/mqtt5_client.h | 3 ++- source/v5/mqtt5_utils.c | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 3641c7c5..53c0aeb9 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -63,7 +63,8 @@ enum aws_mqtt5_client_session_behavior_type { */ enum aws_mqtt5_client_outbound_topic_alias_behavior_type { /** - * Maps to AWS_MQTT5_COTABT_LRU at the moment + * Maps to AWS_MQTT5_COTABT_DISABLED This keeps the client from being broken (by default) if the broker + * topic aliasing implementation has a problem. */ AWS_MQTT5_COTABT_DEFAULT, diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index 5c2ac2de..40115eb2 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -291,6 +291,9 @@ const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( return "User-controlled outbound topic aliasing behavior"; case AWS_MQTT5_COTABT_LRU: return "LRU caching outbound topic aliasing behavior"; + case AWS_MQTT5_COTABT_DISABLED: + return "Outbound topic aliasing disabled"; + default: return "Unknown outbound topic aliasing behavior"; } @@ -299,7 +302,7 @@ const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( enum aws_mqtt5_client_outbound_topic_alias_behavior_type aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default( enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { if (outbound_aliasing_behavior == AWS_MQTT5_COTABT_DEFAULT) { - return AWS_MQTT5_COTABT_LRU; + return AWS_MQTT5_COTABT_DISABLED; } return outbound_aliasing_behavior; From 6b2cc6ae2f74bfcf92bc37ea81e5482fd90967d6 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Tue, 13 Dec 2022 10:07:15 -0500 Subject: [PATCH 31/98] More debugging for MQTT5 decoder (#241) * Log the type of packet, and which decoder function caused the error --- source/v5/mqtt5_decoder.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index b365b108..c9ea1ff7 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -1050,7 +1050,7 @@ static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) enum aws_mqtt5_packet_type packet_type = (enum aws_mqtt5_packet_type)(decoder->packet_first_byte >> 4); aws_mqtt5_decoding_fn *decoder_fn = decoder->options.decoder_table->decoders_by_packet_type[packet_type]; if (decoder_fn == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Decoder decode packet function missing"); + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Decoder decode packet function missing for enum: %d", packet_type); return aws_raise_error(AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR); } @@ -1162,14 +1162,29 @@ int aws_mqtt5_decoder_on_data_received(struct aws_mqtt5_decoder *decoder, struct switch (decoder->state) { case AWS_MQTT5_DS_READ_PACKET_TYPE: result = s_aws_mqtt5_decoder_read_packet_type_on_data(decoder, &data); + if (s_is_full_packet_logging_enabled(decoder)) { + if (result != AWS_MQTT5_DRT_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading packet type"); + } + } break; case AWS_MQTT5_DS_READ_REMAINING_LENGTH: result = s_aws_mqtt5_decoder_read_remaining_length_on_data(decoder, &data); + if (s_is_full_packet_logging_enabled(decoder)) { + if (result != AWS_MQTT5_DRT_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading packet remaining length"); + } + } break; case AWS_MQTT5_DS_READ_PACKET: result = s_aws_mqtt5_decoder_read_packet_on_data(decoder, &data); + if (s_is_full_packet_logging_enabled(decoder)) { + if (result != AWS_MQTT5_DRT_SUCCESS) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading data"); + } + } break; default: From 0e55e376d7e327615b7a61ce4378fa9a8a00a78b Mon Sep 17 00:00:00 2001 From: Dengke Tang Date: Tue, 13 Dec 2022 13:58:40 -0800 Subject: [PATCH 32/98] add codecov action (#240) * add codecov action * remove things not needed --- .github/workflows/codecov.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/codecov.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 00000000..09e4c184 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,25 @@ +name: Code coverage check + +on: + push: + +env: + BUILDER_VERSION: v0.9.29 + BUILDER_SOURCE: releases + BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net + PACKAGE_NAME: aws-c-mqtt + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-east-1 + +jobs: + codecov-linux: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Sources + uses: actions/checkout@v3 + - name: Build ${{ env.PACKAGE_NAME }} + consumers + run: | + python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" + chmod a+x builder + ./builder build -p ${{ env.PACKAGE_NAME }} --compiler=gcc-9 --coverage From c2bc31060984dc3eb89b016c9ea0a525d259fc7d Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 15 Dec 2022 09:16:25 -0800 Subject: [PATCH 33/98] Always rejoin session option (#242) * Always rejoin session option * Log a warning on non-compliant session resumption --- include/aws/mqtt/v5/mqtt5_client.h | 8 ++++++ source/v5/mqtt5_client.c | 16 +++++++++--- source/v5/mqtt5_utils.c | 2 ++ tests/CMakeLists.txt | 1 + tests/v5/mqtt5_client_tests.c | 39 ++++++++++++++++++++++++++---- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 53c0aeb9..49bd7ba9 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -48,6 +48,14 @@ enum aws_mqtt5_client_session_behavior_type { * Always attempt to rejoin an existing session after an initial connection success. */ AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS, + + /** + * Always attempt to rejoin an existing session. Since the client does not support durable session persistence, + * this option is not guaranteed to be spec compliant because any unacknowledged qos1 publishes (which are + * part of the client session state) will not be present on the initial connection. Until we support + * durable session resumption, this option is technically spec-breaking, but useful. + */ + AWS_MQTT5_CSBT_REJOIN_ALWAYS, }; /** diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 0f638549..f141475d 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -1164,7 +1164,8 @@ static bool s_should_resume_session(const struct aws_mqtt5_client *client) { enum aws_mqtt5_client_session_behavior_type session_behavior = aws_mqtt5_client_session_behavior_type_to_non_default(client->config->session_behavior); - return session_behavior == AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS && client->has_connected_successfully; + return (session_behavior == AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS && client->has_connected_successfully) || + (session_behavior == AWS_MQTT5_CSBT_REJOIN_ALWAYS); } static void s_change_current_state_to_mqtt_connect(struct aws_mqtt5_client *client) { @@ -1716,13 +1717,20 @@ static void s_aws_mqtt5_client_on_connack( /* Check if a session is being rejoined and perform associated rejoin connect logic here */ if (client->negotiated_settings.rejoined_session) { /* Disconnect if the server is attempting to connect the client to an unexpected session */ - if (aws_mqtt5_client_session_behavior_type_to_non_default(client->config->session_behavior) == - AWS_MQTT5_CSBT_CLEAN || - client->has_connected_successfully == false) { + if (!s_should_resume_session(client)) { s_aws_mqtt5_client_emit_final_lifecycle_event( client, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION, connack_view, NULL); s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); return; + } else if (!client->has_connected_successfully) { + /* + * We were configured with REJOIN_ALWAYS and this is the first connection. This is technically not safe + * and so let's log a warning for future diagnostics should it cause the user problems. + */ + AWS_LOGF_WARN( + AWS_LS_MQTT5_CLIENT, + "id=%p: initial connection rejoined existing session. This may cause packet id collisions.", + (void *)client); } } diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index 40115eb2..088bea24 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -270,6 +270,8 @@ const char *aws_mqtt5_client_session_behavior_type_to_c_string( return "Clean session always"; case AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS: return "Attempt to resume a session after initial connection success"; + case AWS_MQTT5_CSBT_REJOIN_ALWAYS: + return "Always attempt to resume a session"; default: return "Unknown session behavior"; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6dbf76cc..f95a7ae6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -271,6 +271,7 @@ add_test_case(mqtt5_client_flow_control_iot_core_throughput) add_test_case(mqtt5_client_flow_control_iot_core_publish_tps) add_test_case(mqtt5_client_session_resumption_clean_start) add_test_case(mqtt5_client_session_resumption_post_success) +add_test_case(mqtt5_client_session_resumption_always) add_test_case(mqtt5_client_receive_qos1_return_puback_test) add_test_case(mqtt5_client_receive_nonexisting_session_state) add_test_case(mqtt5_client_receive_assigned_client_id) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 27832291..8dc859d5 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -2732,7 +2732,7 @@ static int s_mqtt5_client_flow_control_iot_core_publish_tps_fn(struct aws_alloca AWS_TEST_CASE(mqtt5_client_flow_control_iot_core_publish_tps, s_mqtt5_client_flow_control_iot_core_publish_tps_fn) -static int s_aws_mqtt5_mock_server_handle_connect_honor_session( +static int s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -2753,6 +2753,24 @@ static int s_aws_mqtt5_mock_server_handle_connect_honor_session( return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } +static int s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connect_view *connect_packet = packet; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.session_present = !connect_packet->clean_start; + + return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + struct aws_mqtt5_wait_for_n_lifecycle_events_context { struct aws_mqtt5_client_mock_test_fixture *test_fixture; enum aws_mqtt5_client_lifecycle_event_type event_type; @@ -2803,6 +2821,7 @@ static bool s_compute_expected_rejoined_session( case AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS: return connect_index > 0; + case AWS_MQTT5_CSBT_REJOIN_ALWAYS: default: return true; } @@ -2833,7 +2852,7 @@ static int s_do_mqtt5_client_session_resumption_test( test_options.client_options.session_behavior = session_behavior; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_honor_session; + s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -2908,6 +2927,16 @@ static int s_mqtt5_client_session_resumption_post_success_fn(struct aws_allocato AWS_TEST_CASE(mqtt5_client_session_resumption_post_success, s_mqtt5_client_session_resumption_post_success_fn) +static int s_mqtt5_client_session_resumption_always_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5_client_session_resumption_test(allocator, AWS_MQTT5_CSBT_REJOIN_ALWAYS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_session_resumption_always, s_mqtt5_client_session_resumption_always_fn) + static uint8_t s_sub_pub_unsub_topic_filter[] = "hello/+"; static struct aws_mqtt5_subscription_view s_sub_pub_unsub_subscriptions[] = { @@ -3832,7 +3861,7 @@ static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *al s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect; /* Simulate reconnecting to an existing connection */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_honor_session; + s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success; struct aws_mqtt5_client_mock_test_fixture test_context; @@ -3922,7 +3951,7 @@ static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_ s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect; /* Simulate reconnecting to an existing connection */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_honor_session; + s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success; struct aws_mqtt5_client_mock_test_fixture test_context; @@ -4458,7 +4487,7 @@ static int s_mqtt5_client_statistics_publish_qos1_requeue_fn(struct aws_allocato test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_server_disconnect_on_first_publish_puback_after; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_honor_session; + s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success; struct aws_mqtt5_client_mock_test_fixture test_context; struct aws_mqtt5_sub_pub_unsub_context full_test_context = { From ac51334df4afde48338671d17f387a152ef406b5 Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Fri, 23 Dec 2022 10:48:35 -0800 Subject: [PATCH 34/98] Adapt to minor breaking change in websocket API (#243) Adapt to changes from https://github.com/awslabs/aws-c-http/pull/409 --- source/client.c | 29 +++++++++++------------------ source/v5/mqtt5_client.c | 28 +++++++++------------------- tests/v5/mqtt5_client_tests.c | 3 ++- 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/source/client.c b/source/client.c index ba8a8ae9..f6c1b59d 100644 --- a/source/client.c +++ b/source/client.c @@ -1158,18 +1158,10 @@ static void s_on_websocket_shutdown(struct aws_websocket *websocket, int error_c } } -static void s_on_websocket_setup( - struct aws_websocket *websocket, - int error_code, - int handshake_response_status, - const struct aws_http_header *handshake_response_header_array, - size_t num_handshake_response_headers, - void *user_data) { - - (void)handshake_response_status; +static void s_on_websocket_setup(const struct aws_websocket_on_connection_setup_data *setup, void *user_data) { /* Setup callback contract is: if error_code is non-zero then websocket is NULL. */ - AWS_FATAL_ASSERT((error_code != 0) == (websocket == NULL)); + AWS_FATAL_ASSERT((setup->error_code != 0) == (setup->websocket == NULL)); struct aws_mqtt_client_connection *connection = user_data; struct aws_channel *channel = NULL; @@ -1179,13 +1171,13 @@ static void s_on_websocket_setup( connection->websocket.handshake_request = NULL; } - if (websocket) { - channel = aws_websocket_get_channel(websocket); + if (setup->websocket) { + channel = aws_websocket_get_channel(setup->websocket); AWS_FATAL_ASSERT(channel); AWS_FATAL_ASSERT(aws_channel_get_event_loop(channel) == connection->loop); /* Websocket must be "converted" before the MQTT handler can be installed next to it. */ - if (aws_websocket_convert_to_midchannel_handler(websocket)) { + if (aws_websocket_convert_to_midchannel_handler(setup->websocket)) { AWS_LOGF_ERROR( AWS_LS_MQTT_CLIENT, "id=%p: Failed converting websocket, error %d (%s)", @@ -1203,8 +1195,8 @@ static void s_on_websocket_setup( if (connection->websocket.handshake_validator( connection, - handshake_response_header_array, - num_handshake_response_headers, + setup->handshake_response_header_array, + setup->num_handshake_response_headers, connection->websocket.handshake_validator_ud)) { AWS_LOGF_ERROR( @@ -1224,7 +1216,7 @@ static void s_on_websocket_setup( } /* Call into the channel-setup callback, the rest of the logic is the same. */ - s_mqtt_client_init(connection->client->bootstrap, error_code, channel, connection); + s_mqtt_client_init(connection->client->bootstrap, setup->error_code, channel, connection); } static aws_mqtt_transform_websocket_handshake_complete_fn s_websocket_handshake_transform_complete; /* fwd declare */ @@ -1323,9 +1315,10 @@ static void s_websocket_handshake_transform_complete( /* Success */ return; -error: +error:; /* Proceed to next step, telling it that we failed. */ - s_on_websocket_setup(NULL, error_code, -1, NULL, 0, connection); + struct aws_websocket_on_connection_setup_data websocket_setup = {.error_code = error_code}; + s_on_websocket_setup(&websocket_setup, connection); } #else /* AWS_MQTT_WITH_WEBSOCKETS */ diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index f141475d..8e728493 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -836,32 +836,22 @@ static void s_on_websocket_shutdown(struct aws_websocket *websocket, int error_c } } -static void s_on_websocket_setup( - struct aws_websocket *websocket, - int error_code, - int handshake_response_status, - const struct aws_http_header *handshake_response_header_array, - size_t num_handshake_response_headers, - void *user_data) { - - (void)handshake_response_status; - (void)handshake_response_header_array; - (void)num_handshake_response_headers; +static void s_on_websocket_setup(const struct aws_websocket_on_connection_setup_data *setup, void *user_data) { struct aws_mqtt5_client *client = user_data; client->handshake = aws_http_message_release(client->handshake); /* Setup callback contract is: if error_code is non-zero then websocket is NULL. */ - AWS_FATAL_ASSERT((error_code != 0) == (websocket == NULL)); + AWS_FATAL_ASSERT((setup->error_code != 0) == (setup->websocket == NULL)); struct aws_channel *channel = NULL; - if (websocket) { - channel = aws_websocket_get_channel(websocket); + if (setup->websocket) { + channel = aws_websocket_get_channel(setup->websocket); AWS_ASSERT(channel); /* Websocket must be "converted" before the MQTT handler can be installed next to it. */ - if (aws_websocket_convert_to_midchannel_handler(websocket)) { + if (aws_websocket_convert_to_midchannel_handler(setup->websocket)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_CLIENT, "id=%p: Failed converting websocket, error %d (%s)", @@ -875,7 +865,7 @@ static void s_on_websocket_setup( } /* Call into the channel-setup callback, the rest of the logic is the same. */ - s_mqtt5_client_setup(client->config->bootstrap, error_code, channel, client); + s_mqtt5_client_setup(client->config->bootstrap, setup->error_code, channel, client); } struct aws_mqtt5_websocket_transform_complete_task { @@ -936,9 +926,9 @@ void s_websocket_transform_complete_task_fn(struct aws_task *task, void *arg, en } } -error: - - s_on_websocket_setup(NULL, error_code, -1, NULL, 0, client); +error:; + struct aws_websocket_on_connection_setup_data websocket_setup = {.error_code = error_code}; + s_on_websocket_setup(&websocket_setup, client); done: diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 8dc859d5..dd0717de 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -680,7 +680,8 @@ void s_websocket_channel_async_failure_task_fn(struct aws_task *task, void *arg, struct websocket_channel_failure_wrapper *wrapper = arg; struct aws_websocket_client_connection_options *options = &wrapper->websocket_options; - (*wrapper->websocket_options.on_connection_setup)(NULL, AWS_ERROR_INVALID_STATE, 0, NULL, 0, options->user_data); + struct aws_websocket_on_connection_setup_data websocket_setup = {.error_code = AWS_ERROR_INVALID_STATE}; + (*wrapper->websocket_options.on_connection_setup)(&websocket_setup, options->user_data); } static int s_mqtt5_client_test_asynchronous_websocket_failure_fn( From b96bf4c23a1404539ddb0047eb348ab2d21f4e4a Mon Sep 17 00:00:00 2001 From: Dmitriy Musatkin <63878209+DmitriyMusatkin@users.noreply.github.com> Date: Tue, 27 Dec 2022 21:01:59 -0500 Subject: [PATCH 35/98] consistent macro calls (#244) --- source/client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/client.c b/source/client.c index f6c1b59d..23232a4e 100644 --- a/source/client.c +++ b/source/client.c @@ -549,7 +549,7 @@ static void s_mqtt_client_init( AWS_LS_MQTT_CLIENT, "id=%p: Adding username " PRInSTR " to connection", (void *)connection, - AWS_BYTE_CURSOR_PRI(username_cur)) + AWS_BYTE_CURSOR_PRI(username_cur)); struct aws_byte_cursor password_cur = { .ptr = NULL, From 38621124255a5d531956037a38fe0df3a092c92a Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Wed, 28 Dec 2022 15:56:15 -0800 Subject: [PATCH 36/98] Tweak comment about websockets (#245) Comment warned that user needed to validate some headers. But now it's handled by the websocket: https://github.com/awslabs/aws-c-http/pull/415 --- include/aws/mqtt/client.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 9263f263..8f4427b5 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -157,7 +157,6 @@ typedef void(aws_mqtt_transform_websocket_handshake_fn)( * Called each time a valid websocket connection is established. * * All required headers have been checked already (ex: "Sec-Websocket-Accept"), - * but optional headers have not (Ex: "Sec-Websocket-Protocol"). * * Return AWS_OP_SUCCESS to accept the connection or AWS_OP_ERR to stop the connection attempt. */ From 5cbde90916a1f9945e2a1ef36f3db58e1c976167 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 4 Jan 2023 10:11:16 -0800 Subject: [PATCH 37/98] Force channel setup failures to always execute on the client's event loop (#246) * Force channel setup failures to always execute on the client's event loop --- source/v5/mqtt5_client.c | 68 +++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 8e728493..1defc86b 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -725,21 +725,34 @@ static void s_aws_mqtt5_client_shutdown_channel_clean( aws_mqtt5_operation_disconnect_release(disconnect_op); } -static void s_mqtt5_client_shutdown( - struct aws_client_bootstrap *bootstrap, - int error_code, - struct aws_channel *channel, - void *user_data) { +struct aws_mqtt5_shutdown_task { + struct aws_task task; + struct aws_allocator *allocator; + int error_code; + struct aws_mqtt5_client *client; +}; - (void)bootstrap; - (void)channel; +static void s_mqtt5_client_shutdown_final(int error_code, struct aws_mqtt5_client *client); - struct aws_mqtt5_client *client = user_data; +static void s_shutdown_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; - if (error_code == AWS_ERROR_SUCCESS) { - error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP; + struct aws_mqtt5_shutdown_task *shutdown_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; } + s_mqtt5_client_shutdown_final(shutdown_task->error_code, shutdown_task->client); + +done: + + aws_mem_release(shutdown_task->allocator, shutdown_task); +} + +static void s_mqtt5_client_shutdown_final(int error_code, struct aws_mqtt5_client *client) { + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); + s_aws_mqtt5_client_emit_final_lifecycle_event(client, error_code, NULL, NULL); AWS_LOGF_INFO( @@ -764,6 +777,36 @@ static void s_mqtt5_client_shutdown( } } +static void s_mqtt5_client_shutdown( + struct aws_client_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + + (void)bootstrap; + (void)channel; + + struct aws_mqtt5_client *client = user_data; + + if (error_code == AWS_ERROR_SUCCESS) { + error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP; + } + + if (aws_event_loop_thread_is_callers_thread(client->loop)) { + s_mqtt5_client_shutdown_final(error_code, client); + return; + } + + struct aws_mqtt5_shutdown_task *shutdown_task = + aws_mem_calloc(client->allocator, 1, sizeof(struct aws_mqtt5_shutdown_task)); + + aws_task_init(&shutdown_task->task, s_shutdown_task_fn, (void *)shutdown_task, "ShutdownTask"); + shutdown_task->allocator = client->allocator; + shutdown_task->client = client; + shutdown_task->error_code = error_code; + aws_event_loop_schedule_task_now(client->loop, &shutdown_task->task); +} + static void s_mqtt5_client_setup( struct aws_client_bootstrap *bootstrap, int error_code, @@ -776,14 +819,15 @@ static void s_mqtt5_client_setup( AWS_FATAL_ASSERT((error_code != 0) == (channel == NULL)); struct aws_mqtt5_client *client = user_data; - AWS_FATAL_ASSERT(client->current_state == AWS_MCS_CONNECTING); - if (error_code != AWS_OP_SUCCESS) { /* client shutdown already handles this case, so just call that. */ s_mqtt5_client_shutdown(bootstrap, error_code, channel, user_data); return; } + AWS_FATAL_ASSERT(client->current_state == AWS_MCS_CONNECTING); + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); + if (client->desired_state != AWS_MCS_CONNECTED) { aws_raise_error(AWS_ERROR_MQTT5_USER_REQUESTED_STOP); goto error; From 893e58632794ce2f9f44cefa90c377c354c10953 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Tue, 10 Jan 2023 16:05:58 -0500 Subject: [PATCH 38/98] Remove temporary logging around MQTT5 decoder (#249) --- bin/mqtt5canary/main.c | 2 - include/aws/mqtt/private/v5/mqtt5_decoder.h | 7 -- source/v5/mqtt5_client.c | 4 - source/v5/mqtt5_decoder.c | 99 --------------------- 4 files changed, 112 deletions(-) diff --git a/bin/mqtt5canary/main.c b/bin/mqtt5canary/main.c index d3b621f8..2169873f 100644 --- a/bin/mqtt5canary/main.c +++ b/bin/mqtt5canary/main.c @@ -882,8 +882,6 @@ int main(int argc, char **argv) { clients[i].shared_topic = shared_topic; clients[i].client = aws_mqtt5_client_new(allocator, &client_options); - aws_mqtt5_client_enable_full_packet_logging(clients[i].client); - aws_mqtt5_canary_operation_fn *operation_fn = s_aws_mqtt5_canary_operation_table.operation_by_operation_type[AWS_MQTT5_CANARY_OPERATION_START]; (*operation_fn)(&clients[i]); diff --git a/include/aws/mqtt/private/v5/mqtt5_decoder.h b/include/aws/mqtt/private/v5/mqtt5_decoder.h index a5d642bc..8d6aea92 100644 --- a/include/aws/mqtt/private/v5/mqtt5_decoder.h +++ b/include/aws/mqtt/private/v5/mqtt5_decoder.h @@ -101,8 +101,6 @@ struct aws_mqtt5_decoder { struct aws_byte_cursor packet_cursor; struct aws_mqtt5_inbound_topic_alias_resolver *topic_alias_resolver; - - struct aws_atomic_var is_full_packet_logging_enabled; }; AWS_EXTERN_C_BEGIN @@ -159,11 +157,6 @@ AWS_MQTT_API void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( */ AWS_MQTT_API extern const struct aws_mqtt5_decoder_function_table *g_aws_mqtt5_default_decoder_table; -/* - * Temporary, private API to turn on total incoming packet logging at the byte level. - */ -AWS_MQTT_API void aws_mqtt5_decoder_enable_full_packet_logging(struct aws_mqtt5_decoder *decoder); - AWS_EXTERN_C_END /* Decode helpers */ diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 1defc86b..b7de8e06 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -3349,7 +3349,3 @@ void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt stats->unacked_operation_size = (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.unacked_operation_size_atomic); } - -void aws_mqtt5_client_enable_full_packet_logging(struct aws_mqtt5_client *client) { - aws_mqtt5_decoder_enable_full_packet_logging(&client->decoder); -} diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index c9ea1ff7..184f3daf 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -13,10 +13,6 @@ #define PUBLISH_PACKET_FIXED_HEADER_RETAIN_FLAG 1 #define PUBLISH_PACKET_FIXED_HEADER_QOS_FLAG 3 -static bool s_is_full_packet_logging_enabled(struct aws_mqtt5_decoder *decoder) { - return aws_atomic_load_int(&decoder->is_full_packet_logging_enabled) == 1; -} - static void s_reset_decoder_for_new_packet(struct aws_mqtt5_decoder *decoder) { aws_byte_buf_reset(&decoder->scratch_space, false); @@ -55,14 +51,6 @@ static int s_aws_mqtt5_decoder_read_packet_type_on_data( aws_byte_cursor_advance(data, 1); aws_byte_buf_append_byte_dynamic(&decoder->scratch_space, byte); - if (s_is_full_packet_logging_enabled(decoder)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CLIENT, - "id=%p: Decoder FPL first byte: %2X", - decoder->options.callback_user_data, - (unsigned int)byte); - } - enum aws_mqtt5_packet_type packet_type = (byte >> 4); if (!s_is_decodable_packet_type(decoder, packet_type)) { @@ -133,14 +121,6 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_vli_on_data( struct aws_byte_cursor byte_cursor = aws_byte_cursor_advance(data, 1); aws_byte_buf_append_dynamic(&decoder->scratch_space, &byte_cursor); - if (s_is_full_packet_logging_enabled(decoder)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CLIENT, - "id=%p: Decoder FPL remaining length VLI byte: %2X", - decoder->options.callback_user_data, - (unsigned int)(*byte_cursor.ptr)); - } - /* now try and decode a vli integer based on the range implied by the offset into the buffer */ struct aws_byte_cursor vli_cursor = { .ptr = decoder->scratch_space.buffer, @@ -1057,56 +1037,6 @@ static int s_aws_mqtt5_decoder_decode_packet(struct aws_mqtt5_decoder *decoder) return (*decoder_fn)(decoder); } -#define FULL_PACKET_LOGGING_BYTES_PER_LINE 20 - -static void s_log_packet_cursor(struct aws_mqtt5_decoder *decoder, struct aws_byte_cursor *packet_cursor) { - - struct aws_mqtt5_client *client = decoder->options.callback_user_data; - if (packet_cursor->len == 0) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor empty", (void *)client); - return; - } - - char line_buffer[4096]; - size_t line_buffer_index = 0; - - size_t i = 0; - size_t packet_len = packet_cursor->len; - for (i = 0; i < packet_len; ++i) { - uint8_t packet_byte = *(packet_cursor->ptr + i); - if (i % FULL_PACKET_LOGGING_BYTES_PER_LINE) { - line_buffer_index += (size_t)snprintf( - line_buffer + line_buffer_index, - AWS_ARRAY_SIZE(line_buffer) - line_buffer_index, - ", 0x%02X", - (unsigned int)packet_byte); - } else { - line_buffer_index += (size_t)snprintf( - line_buffer + line_buffer_index, - AWS_ARRAY_SIZE(line_buffer) - line_buffer_index, - "0x%02X", - (unsigned int)packet_byte); - } - - if ((i + 1) % FULL_PACKET_LOGGING_BYTES_PER_LINE == 0) { - int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CLIENT, - "id=%p: Decoder FPL packet cursor %12d: %s", - (void *)client, - byte_count, - line_buffer); - line_buffer_index = 0; - } - } - - if (line_buffer_index > 0) { - int byte_count = (int)((i / FULL_PACKET_LOGGING_BYTES_PER_LINE) * FULL_PACKET_LOGGING_BYTES_PER_LINE); - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CLIENT, "id=%p: Decoder FPL packet cursor %12d: %s", (void *)client, byte_count, line_buffer); - } -} - /* * (Streaming) Given a packet type and a variable length integer specifying the packet length, this state either * (1) decodes directly from the cursor if possible @@ -1138,15 +1068,7 @@ static enum aws_mqtt5_decode_result_type s_aws_mqtt5_decoder_read_packet_on_data decoder->packet_cursor = aws_byte_cursor_from_buf(&decoder->scratch_space); } - struct aws_byte_cursor copy_cursor; - if (s_is_full_packet_logging_enabled(decoder)) { - copy_cursor = aws_byte_cursor_from_array(decoder->packet_cursor.ptr, decoder->packet_cursor.len); - } - if (s_aws_mqtt5_decoder_decode_packet(decoder)) { - if (s_is_full_packet_logging_enabled(decoder)) { - s_log_packet_cursor(decoder, ©_cursor); - } return AWS_MQTT5_DRT_ERROR; } @@ -1162,29 +1084,14 @@ int aws_mqtt5_decoder_on_data_received(struct aws_mqtt5_decoder *decoder, struct switch (decoder->state) { case AWS_MQTT5_DS_READ_PACKET_TYPE: result = s_aws_mqtt5_decoder_read_packet_type_on_data(decoder, &data); - if (s_is_full_packet_logging_enabled(decoder)) { - if (result != AWS_MQTT5_DRT_SUCCESS) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading packet type"); - } - } break; case AWS_MQTT5_DS_READ_REMAINING_LENGTH: result = s_aws_mqtt5_decoder_read_remaining_length_on_data(decoder, &data); - if (s_is_full_packet_logging_enabled(decoder)) { - if (result != AWS_MQTT5_DRT_SUCCESS) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading packet remaining length"); - } - } break; case AWS_MQTT5_DS_READ_PACKET: result = s_aws_mqtt5_decoder_read_packet_on_data(decoder, &data); - if (s_is_full_packet_logging_enabled(decoder)) { - if (result != AWS_MQTT5_DRT_SUCCESS) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_CLIENT, "Error detected reading data"); - } - } break; default: @@ -1247,8 +1154,6 @@ int aws_mqtt5_decoder_init( return AWS_OP_ERR; } - aws_atomic_init_int(&decoder->is_full_packet_logging_enabled, 0); - return AWS_OP_SUCCESS; } @@ -1265,7 +1170,3 @@ void aws_mqtt5_decoder_set_inbound_topic_alias_resolver( struct aws_mqtt5_inbound_topic_alias_resolver *resolver) { decoder->topic_alias_resolver = resolver; } - -void aws_mqtt5_decoder_enable_full_packet_logging(struct aws_mqtt5_decoder *decoder) { - aws_atomic_store_int(&decoder->is_full_packet_logging_enabled, 1); -} From 1d5a74fe7f8b18ef3237c684e8e4f05263bcd1a2 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 20 Jan 2023 14:36:32 -0500 Subject: [PATCH 39/98] Mqtt311 operation statistics support (#247) Add operation statistics support to MQTT311 like the operation statistics support in MQTT5. --- include/aws/mqtt/client.h | 40 + include/aws/mqtt/private/client_impl.h | 90 +- source/client.c | 207 +++- source/client_channel_handler.c | 42 +- tests/CMakeLists.txt | 9 + tests/v3/operation_statistics_test.c | 1331 ++++++++++++++++++++++++ 6 files changed, 1710 insertions(+), 9 deletions(-) create mode 100644 tests/v3/operation_statistics_test.c diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 8f4427b5..143ab73d 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -221,6 +221,35 @@ struct aws_mqtt_connection_options { bool clean_session; }; +/** + * Contains some simple statistics about the current state of the connection's queue of operations + */ +struct aws_mqtt_connection_operation_statistics { + /** + * total number of operations submitted to the connection that have not yet been completed. Unacked operations + * are a subset of this. + */ + uint64_t incomplete_operation_count; + + /** + * total packet size of operations submitted to the connection that have not yet been completed. Unacked operations + * are a subset of this. + */ + uint64_t incomplete_operation_size; + + /** + * total number of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + uint64_t unacked_operation_count; + + /** + * total packet size of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + uint64_t unacked_operation_size; +}; + AWS_EXTERN_C_BEGIN /** @@ -571,6 +600,17 @@ uint16_t aws_mqtt_client_connection_publish( aws_mqtt_op_complete_fn *on_complete, void *userdata); +/** + * Queries the connection's internal statistics for incomplete/unacked operations. + * \param connection connection to get statistics for + * \param stats set of incomplete/unacked operation statistics + * \returns AWS_OP_SUCCESS if getting the operation statistics were successful, AWS_OP_ERR otherwise + */ +AWS_MQTT_API +int aws_mqtt_client_connection_get_stats( + struct aws_mqtt_client_connection *connection, + struct aws_mqtt_connection_operation_statistics *stats); + AWS_EXTERN_C_END #endif /* AWS_MQTT_CLIENT_H */ diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 833e147e..bbf7e44b 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -59,6 +59,35 @@ enum aws_mqtt_client_request_state { AWS_MQTT_CLIENT_REQUEST_ERROR, }; +/** + * Contains some simple statistics about the current state of the connection's queue of operations + */ +struct aws_mqtt_connection_operation_statistics_impl { + /** + * total number of operations submitted to the connection that have not yet been completed. Unacked operations + * are a subset of this. + */ + struct aws_atomic_var incomplete_operation_count_atomic; + + /** + * total packet size of operations submitted to the connection that have not yet been completed. Unacked operations + * are a subset of this. + */ + struct aws_atomic_var incomplete_operation_size_atomic; + + /** + * total number of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + struct aws_atomic_var unacked_operation_count_atomic; + + /** + * total packet size of operations that have been sent to the server and are waiting for a corresponding ACK before + * they can be completed. + */ + struct aws_atomic_var unacked_operation_size_atomic; +}; + /** * Called after the timeout if a matching ack packet hasn't arrived, with is_first_attempt set as false. * Or called when the request packet attempt to send firstly, with is_first_attempt set as true. @@ -69,6 +98,23 @@ enum aws_mqtt_client_request_state { typedef enum aws_mqtt_client_request_state( aws_mqtt_send_request_fn)(uint16_t packet_id, bool is_first_attempt, void *userdata); +/** + * Called when the operation statistics change. + */ +typedef void(aws_mqtt_on_operation_statistics_fn)(struct aws_mqtt_client_connection *connection, void *userdata); + +/* Flags that indicate the way in which way an operation is currently affecting the statistics of the connection */ +enum aws_mqtt_operation_statistic_state_flags { + /* The operation is not affecting the connection's statistics at all */ + AWS_MQTT_OSS_NONE = 0, + + /* The operation is affecting the connection's "incomplete operation" statistics */ + AWS_MQTT_OSS_INCOMPLETE = 1 << 0, + + /* The operation is affecting the connection's "unacked operation" statistics */ + AWS_MQTT_OSS_UNACKED = 1 << 1, +}; + struct aws_mqtt_request { struct aws_linked_list_node list_node; @@ -77,6 +123,11 @@ struct aws_mqtt_request { struct aws_channel_task outgoing_task; + /* How this operation is currently affecting the statistics of the connection */ + enum aws_mqtt_operation_statistic_state_flags statistic_state_flags; + /* The encoded size of the packet - used for operation statistics tracking */ + uint64_t packet_size; + uint16_t packet_id; bool retryable; bool initiated; @@ -175,6 +226,8 @@ struct aws_mqtt_client_connection { void *on_any_publish_ud; aws_mqtt_client_on_disconnect_fn *on_disconnect; void *on_disconnect_ud; + aws_mqtt_on_operation_statistics_fn *on_any_operation_statistics; + void *on_any_operation_statistics_ud; /* Connection tasks. */ struct aws_mqtt_reconnect_task *reconnect_task; @@ -236,6 +289,7 @@ struct aws_mqtt_client_connection { * Helps us find the next free ID faster. */ uint16_t packet_id; + } synced_data; struct { @@ -247,6 +301,11 @@ struct aws_mqtt_client_connection { struct aws_http_message *handshake_request; } websocket; + + /** + * Statistics tracking operational state + */ + struct aws_mqtt_connection_operation_statistics_impl operation_statistics_impl; }; struct aws_channel_handler_vtable *aws_mqtt_get_client_channel_vtable(void); @@ -276,7 +335,8 @@ AWS_MQTT_API uint16_t mqtt_create_request( void *send_request_ud, aws_mqtt_op_complete_fn *on_complete, void *on_complete_ud, - bool noRetry); + bool noRetry, + uint64_t packet_size); /* Call when an ack packet comes back from the server. */ AWS_MQTT_API void mqtt_request_complete( @@ -290,6 +350,18 @@ AWS_MQTT_API void mqtt_disconnect_impl(struct aws_mqtt_client_connection *connec /* Creates the task used to reestablish a broken connection */ AWS_MQTT_API void aws_create_reconnect_task(struct aws_mqtt_client_connection *connection); +/** + * Sets the callback to call whenever the operation statistics change. + * + * \param[in] connection The connection object + * \param[in] on_operation_statistics The function to call when the operation statistics change (pass NULL to unset) + * \param[in] on_operation_statistics_ud Userdata for on_operation_statistics + */ +AWS_MQTT_API int aws_mqtt_client_connection_set_on_operation_statistics_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_on_operation_statistics_fn *on_operation_statistics, + void *on_operation_statistics_ud); + /* * Sends a PINGREQ packet to the server to keep the connection alive. This is not exported and should not ever * be called directly. This function is driven by the timeout values passed to aws_mqtt_client_connect(). @@ -302,4 +374,20 @@ AWS_MQTT_API void aws_create_reconnect_task(struct aws_mqtt_client_connection *c */ int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connection); +/** + * Changes the operation statistics for the passed-in aws_mqtt_request. Used for tracking + * whether operations have been completed or not. + * + * NOTE: This function will get lock the synced data! Do NOT call with the synced data already + * held or the function will deadlock trying to get the lock + * + * @param connection The connection whose operations are being tracked + * @param request The request to change the state of + * @param new_state_flags The new state to use + */ +void aws_mqtt_connection_statistics_change_operation_statistic_state( + struct aws_mqtt_client_connection *connection, + struct aws_mqtt_request *request, + enum aws_mqtt_operation_statistic_state_flags new_state_flags); + #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_H */ diff --git a/source/client.c b/source/client.c index 23232a4e..4465fc4a 100644 --- a/source/client.c +++ b/source/client.c @@ -152,6 +152,13 @@ static struct request_timeout_task_arg *s_schedule_timeout_task( return timeout_task_arg; } +static void s_init_statistics(struct aws_mqtt_connection_operation_statistics_impl *stats) { + aws_atomic_store_int(&stats->incomplete_operation_count_atomic, 0); + aws_atomic_store_int(&stats->incomplete_operation_size_atomic, 0); + aws_atomic_store_int(&stats->unacked_operation_count_atomic, 0); + aws_atomic_store_int(&stats->unacked_operation_size_atomic, 0); +} + /******************************************************************************* * Client Init ******************************************************************************/ @@ -803,6 +810,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt connection->reconnect_timeouts.max_sec = 128; aws_linked_list_init(&connection->synced_data.pending_requests_list); aws_linked_list_init(&connection->thread_data.ongoing_requests_list); + s_init_statistics(&connection->operation_statistics_impl); if (aws_mutex_init(&connection->synced_data.lock)) { AWS_LOGF_ERROR( @@ -1879,6 +1887,11 @@ uint16_t aws_mqtt_client_connection_subscribe_multiple( AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Starting multi-topic subscribe", (void *)connection); + /* Calculate the size of the subscribe packet + * The fixed header is 2 bytes and the packet ID is 2 bytes. + * Note: The size of the topic filter(s) are calculated in the loop below */ + uint64_t subscribe_packet_size = 4; + for (size_t i = 0; i < num_topics; ++i) { struct aws_mqtt_topic_subscription *request = NULL; @@ -1917,10 +1930,19 @@ uint16_t aws_mqtt_client_connection_subscribe_multiple( /* Push into the list */ aws_array_list_push_back(&task_arg->topics, &task_topic); + + /* Subscribe topic filter is: always 3 bytes (1 for QoS, 2 for Length MSB/LSB) + the size of the topic filter */ + subscribe_packet_size += 3 + task_topic->request.topic.len; } uint16_t packet_id = mqtt_create_request( - task_arg->connection, &s_subscribe_send, task_arg, &s_subscribe_complete, task_arg, false /* noRetry */); + task_arg->connection, + &s_subscribe_send, + task_arg, + &s_subscribe_complete, + task_arg, + false, /* noRetry */ + subscribe_packet_size); if (packet_id == 0) { AWS_LOGF_ERROR( @@ -2054,8 +2076,21 @@ uint16_t aws_mqtt_client_connection_subscribe( task_topic->request.on_cleanup = on_ud_cleanup; task_topic->request.on_publish_ud = on_publish_ud; + /* Calculate the size of the (single) subscribe packet + * The fixed header is 2 bytes, + * the topic filter is always at least 3 bytes (1 for QoS, 2 for Length MSB/LSB) + * - plus the size of the topic filter + * and finally the packet ID is 2 bytes */ + uint64_t subscribe_packet_size = 7 + topic_filter->len; + uint16_t packet_id = mqtt_create_request( - task_arg->connection, &s_subscribe_send, task_arg, &s_subscribe_single_complete, task_arg, false /* noRetry */); + task_arg->connection, + &s_subscribe_send, + task_arg, + &s_subscribe_single_complete, + task_arg, + false, /* noRetry */ + subscribe_packet_size); if (packet_id == 0) { AWS_LOGF_ERROR( @@ -2212,13 +2247,20 @@ uint16_t aws_mqtt_client_connection_subscribe_local( task_topic->request.on_cleanup = on_ud_cleanup; task_topic->request.on_publish_ud = on_publish_ud; + /* Calculate the size of the (local) subscribe packet + * The fixed header is 2 bytes, the packet ID is 2 bytes + * the topic filter is always 3 bytes (1 for QoS, 2 for Length MSB/LSB) + * - plus the size of the topic filter */ + uint64_t subscribe_packet_size = 7 + topic_filter->len; + uint16_t packet_id = mqtt_create_request( task_arg->connection, s_subscribe_local_send, task_arg, &s_subscribe_local_complete, task_arg, - false /* noRetry */); + false, /* noRetry */ + subscribe_packet_size); if (packet_id == 0) { AWS_LOGF_ERROR( @@ -2275,6 +2317,19 @@ static bool s_reconnect_resub_iterator(const struct aws_byte_cursor *topic, enum return true; } +static bool s_reconnect_resub_operation_statistics_iterator( + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + void *user_data) { + (void)qos; + uint64_t *packet_size = user_data; + /* Always 3 bytes (1 for QoS, 2 for length MSB and LSB respectively) */ + *packet_size += 3; + /* The size of the topic filter */ + *packet_size += topic->len; + return true; +} + static enum aws_mqtt_client_request_state s_resubscribe_send( uint16_t packet_id, bool is_first_attempt, @@ -2429,8 +2484,24 @@ uint16_t aws_mqtt_resubscribe_existing_topics( task_arg->on_suback.multi = on_suback; task_arg->on_suback_ud = on_suback_ud; + /* Calculate the size of the packet. + * The fixed header is 2 bytes and the packet ID is 2 bytes + * plus the size of each topic in the topic tree */ + uint64_t resubscribe_packet_size = 4; + /* Get the length of each subscription we are going to resubscribe with */ + aws_mqtt_topic_tree_iterate( + &connection->thread_data.subscriptions, + s_reconnect_resub_operation_statistics_iterator, + &resubscribe_packet_size); + uint16_t packet_id = mqtt_create_request( - task_arg->connection, &s_resubscribe_send, task_arg, &s_resubscribe_complete, task_arg, false /* noRetry */); + task_arg->connection, + &s_resubscribe_send, + task_arg, + &s_resubscribe_complete, + task_arg, + false, /* noRetry */ + resubscribe_packet_size); if (packet_id == 0) { AWS_LOGF_ERROR( @@ -2625,8 +2696,19 @@ uint16_t aws_mqtt_client_connection_unsubscribe( task_arg->on_unsuback = on_unsuback; task_arg->on_unsuback_ud = on_unsuback_ud; + /* Calculate the size of the unsubscribe packet. + * The fixed header is always 2 bytes, the packet ID is always 2 bytes + * plus the size of the topic filter */ + uint64_t unsubscribe_packet_size = 4 + task_arg->filter.len; + uint16_t packet_id = mqtt_create_request( - connection, &s_unsubscribe_send, task_arg, s_unsubscribe_complete, task_arg, false /* noRetry */); + connection, + &s_unsubscribe_send, + task_arg, + s_unsubscribe_complete, + task_arg, + false, /* noRetry */ + unsubscribe_packet_size); if (packet_id == 0) { AWS_LOGF_DEBUG( AWS_LS_MQTT_CLIENT, @@ -2882,8 +2964,14 @@ uint16_t aws_mqtt_client_connection_publish( arg->on_complete = on_complete; arg->userdata = userdata; + /* Calculate the size of the publish packet. + * The fixed header size is 2 bytes, the packet ID is 2 bytes, + * plus the size of both the topic name and payload */ + uint64_t publish_packet_size = 4 + arg->topic.len + arg->payload.len; + bool retry = qos == AWS_MQTT_QOS_AT_MOST_ONCE; - uint16_t packet_id = mqtt_create_request(connection, &s_publish_send, arg, &s_publish_complete, arg, retry); + uint16_t packet_id = + mqtt_create_request(connection, &s_publish_send, arg, &s_publish_complete, arg, retry, publish_packet_size); if (packet_id == 0) { /* bummer, we failed to make a new request */ @@ -2991,9 +3079,114 @@ int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connectio AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Starting ping", (void *)connection); - uint16_t packet_id = mqtt_create_request(connection, &s_pingreq_send, connection, NULL, NULL, true /* noRetry */); + uint16_t packet_id = + mqtt_create_request(connection, &s_pingreq_send, connection, NULL, NULL, true, /* noRetry */ 0); AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Starting ping with packet id %" PRIu16, (void *)connection, packet_id); return (packet_id > 0) ? AWS_OP_SUCCESS : AWS_OP_ERR; } + +/******************************************************************************* + * Operation Statistics + ******************************************************************************/ + +void aws_mqtt_connection_statistics_change_operation_statistic_state( + struct aws_mqtt_client_connection *connection, + struct aws_mqtt_request *request, + enum aws_mqtt_operation_statistic_state_flags new_state_flags) { + + // Error checking + if (!connection) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, "Invalid MQTT311 connection used when trying to change operation statistic state"); + return; + } + if (!request) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, "Invalid MQTT311 request used when trying to change operation statistic state"); + return; + } + + uint64_t packet_size = request->packet_size; + /** + * If the packet size is zero, then just skip it as we only want to track packets we have intentially + * calculated the size of and therefore it will be non-zero (zero packets will be ACKs, Pings, etc) + */ + if (packet_size <= 0) { + return; + } + + enum aws_mqtt_operation_statistic_state_flags old_state_flags = request->statistic_state_flags; + if (new_state_flags == old_state_flags) { + return; + } + + struct aws_mqtt_connection_operation_statistics_impl *stats = &connection->operation_statistics_impl; + if ((old_state_flags & AWS_MQTT_OSS_INCOMPLETE) != (new_state_flags & AWS_MQTT_OSS_INCOMPLETE)) { + if ((new_state_flags & AWS_MQTT_OSS_INCOMPLETE) != 0) { + aws_atomic_fetch_add(&stats->incomplete_operation_count_atomic, 1); + aws_atomic_fetch_add(&stats->incomplete_operation_size_atomic, (size_t)packet_size); + } else { + aws_atomic_fetch_sub(&stats->incomplete_operation_count_atomic, 1); + aws_atomic_fetch_sub(&stats->incomplete_operation_size_atomic, (size_t)packet_size); + } + } + + if ((old_state_flags & AWS_MQTT_OSS_UNACKED) != (new_state_flags & AWS_MQTT_OSS_UNACKED)) { + if ((new_state_flags & AWS_MQTT_OSS_UNACKED) != 0) { + aws_atomic_fetch_add(&stats->unacked_operation_count_atomic, 1); + aws_atomic_fetch_add(&stats->unacked_operation_size_atomic, (size_t)packet_size); + } else { + aws_atomic_fetch_sub(&stats->unacked_operation_count_atomic, 1); + aws_atomic_fetch_sub(&stats->unacked_operation_size_atomic, (size_t)packet_size); + } + } + request->statistic_state_flags = new_state_flags; + + // If the callback is defined, then call it + if (connection && connection->on_any_operation_statistics && connection->on_any_operation_statistics_ud) { + (*connection->on_any_operation_statistics)(connection, connection->on_any_operation_statistics_ud); + } +} + +int aws_mqtt_client_connection_get_stats( + struct aws_mqtt_client_connection *connection, + struct aws_mqtt_connection_operation_statistics *stats) { + // Error checking + if (!connection) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "Invalid MQTT311 connection used when trying to get operation statistics"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + if (!stats) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Invalid MQTT311 connection statistics struct used when trying to get operation statistics", + (void *)connection); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + stats->incomplete_operation_count = + (uint64_t)aws_atomic_load_int(&connection->operation_statistics_impl.incomplete_operation_count_atomic); + stats->incomplete_operation_size = + (uint64_t)aws_atomic_load_int(&connection->operation_statistics_impl.incomplete_operation_size_atomic); + stats->unacked_operation_count = + (uint64_t)aws_atomic_load_int(&connection->operation_statistics_impl.unacked_operation_count_atomic); + stats->unacked_operation_size = + (uint64_t)aws_atomic_load_int(&connection->operation_statistics_impl.unacked_operation_size_atomic); + + return AWS_OP_SUCCESS; +} + +int aws_mqtt_client_connection_set_on_operation_statistics_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_on_operation_statistics_fn *on_operation_statistics, + void *on_operation_statistics_ud) { + + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Setting on_operation_statistics handler", (void *)connection); + + connection->on_any_operation_statistics = on_operation_statistics; + connection->on_any_operation_statistics_ud = on_operation_statistics_ud; + + return AWS_OP_SUCCESS; +} diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index ef0cbb22..2ad5808d 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -721,10 +721,17 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en "%" PRIu16 ". will be retried", (void *)task, request->packet_id); + /* put it into the offline queue. */ { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); + + /* Set the status as incomplete */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + connection, request, AWS_MQTT_OSS_INCOMPLETE); + aws_linked_list_push_back(&connection->synced_data.pending_requests_list, &request->list_node); + mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ } else { @@ -734,13 +741,19 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en "%" PRIu16 ". will NOT be retried, will be cancelled", (void *)task, request->packet_id); + /* Fire the callback and clean up the memory, as the connection get destroyed. */ if (request->on_complete) { request->on_complete( connection, request->packet_id, AWS_ERROR_MQTT_NOT_CONNECTED, request->on_complete_ud); } + { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); + + /* Cancel the request in the operation statistics */ + aws_mqtt_connection_statistics_change_operation_statistic_state(connection, request, AWS_MQTT_OSS_NONE); + aws_hash_table_remove( &connection->synced_data.outstanding_requests_table, &request->packet_id, NULL, NULL); aws_memory_pool_release(&connection->synced_data.requests_pool, request); @@ -772,13 +785,20 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en "id=%p: sending request %" PRIu16 " complete, invoking on_complete callback.", (void *)request->connection, request->packet_id); + /* If the send_request function reports the request is complete, * remove from the hash table and call the callback. */ if (request->on_complete) { request->on_complete(connection, request->packet_id, error_code, request->on_complete_ud); } + { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); + + /* Set the request as complete in the operation statistics */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + request->connection, request, AWS_MQTT_OSS_NONE); + aws_hash_table_remove( &connection->synced_data.outstanding_requests_table, &request->packet_id, NULL, NULL); aws_memory_pool_release(&connection->synced_data.requests_pool, request); @@ -792,6 +812,11 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en "id=%p: request %" PRIu16 " sent, but waiting on an acknowledgement from peer.", (void *)request->connection, request->packet_id); + + /* Set the request as incomplete and un-acked in the operation statistics */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + request->connection, request, AWS_MQTT_OSS_INCOMPLETE | AWS_MQTT_OSS_UNACKED); + /* Put the request into the ongoing list */ aws_linked_list_push_back(&connection->thread_data.ongoing_requests_list, &request->list_node); break; @@ -804,7 +829,8 @@ uint16_t mqtt_create_request( void *send_request_ud, aws_mqtt_op_complete_fn *on_complete, void *on_complete_ud, - bool noRetry) { + bool noRetry, + uint64_t packet_size) { AWS_ASSERT(connection); AWS_ASSERT(send_request); @@ -899,6 +925,7 @@ uint16_t mqtt_create_request( next_request->send_request_ud = send_request_ud; next_request->on_complete = on_complete; next_request->on_complete_ud = on_complete_ud; + next_request->packet_size = packet_size; aws_channel_task_init( &next_request->outgoing_task, s_request_outgoing_task, next_request, "mqtt_outgoing_request_task"); if (connection->synced_data.state != AWS_MQTT_CLIENT_STATE_CONNECTED) { @@ -911,8 +938,17 @@ uint16_t mqtt_create_request( /* keep the channel alive until the task is scheduled */ aws_channel_acquire_hold(channel); } + mqtt_connection_unlock_synced_data(connection); + } /* END CRITICAL SECTION */ + + if (next_request && next_request->packet_size > 0) { + /* Set the status as incomplete */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + next_request->connection, next_request, AWS_MQTT_OSS_INCOMPLETE); + } + if (should_schedule_task) { AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, @@ -951,6 +987,10 @@ void mqtt_request_complete(struct aws_mqtt_client_connection *connection, int er on_complete = request->on_complete; on_complete_ud = request->on_complete_ud; + /* Set the status as complete */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + request->connection, request, AWS_MQTT_OSS_NONE); + /* clean up request resources */ aws_hash_table_remove_element(&connection->synced_data.outstanding_requests_table, elem); /* remove the request from the list, which is thread_data.ongoing_requests_list */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f95a7ae6..8e4539e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,6 +61,15 @@ add_test_case(mqtt_clean_session_keep_next_session) add_test_case(mqtt_connection_publish_QoS1_timeout) add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) +# Operation statistics tests +add_test_case(mqtt_operation_statistics_simple_publish) +add_test_case(mqtt_operation_statistics_offline_publish) +add_test_case(mqtt_operation_statistics_disconnect_publish) +add_test_case(mqtt_operation_statistics_reconnect_publish) +add_test_case(mqtt_operation_statistics_simple_subscribe) +add_test_case(mqtt_operation_statistics_simple_unsubscribe) +add_test_case(mqtt_operation_statistics_simple_resubscribe) +add_test_case(mqtt_operation_statistics_simple_callback) # MQTT5 tests diff --git a/tests/v3/operation_statistics_test.c b/tests/v3/operation_statistics_test.c new file mode 100644 index 00000000..849277d7 --- /dev/null +++ b/tests/v3/operation_statistics_test.c @@ -0,0 +1,1331 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt_mock_server_handler.h" + +#include + +#include +#include +#include + +#include +#include + +#include + +#ifdef _WIN32 +# define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" +#else +# define LOCAL_SOCK_TEST_PATTERN "testsock%llu.sock" +#endif + +static const int TEST_LOG_SUBJECT = 60000; +static const int ONE_SEC = 1000000000; + +struct received_publish_packet { + struct aws_byte_buf topic; + struct aws_byte_buf payload; + bool dup; + enum aws_mqtt_qos qos; + bool retain; +}; + +struct mqtt_connection_state_test { + struct aws_allocator *allocator; + struct aws_channel *server_channel; + struct aws_channel_handler *mock_server; + struct aws_client_bootstrap *client_bootstrap; + struct aws_server_bootstrap *server_bootstrap; + struct aws_event_loop_group *el_group; + struct aws_host_resolver *host_resolver; + struct aws_socket_endpoint endpoint; + struct aws_socket *listener; + struct aws_mqtt_client *mqtt_client; + struct aws_mqtt_client_connection *mqtt_connection; + struct aws_socket_options socket_options; + bool session_present; + bool connection_completed; + bool client_disconnect_completed; + bool server_disconnect_completed; + bool connection_interrupted; + bool connection_resumed; + bool subscribe_completed; + bool listener_destroyed; + int interruption_error; + int subscribe_complete_error; + int op_complete_error; + enum aws_mqtt_connect_return_code mqtt_return_code; + int error; + struct aws_condition_variable cvar; + struct aws_mutex lock; + /* any published messages from mock server, that you may not subscribe to. (Which should not happen in real life) */ + struct aws_array_list any_published_messages; /* list of struct received_publish_packet */ + size_t any_publishes_received; + size_t expected_any_publishes; + /* the published messages from mock server, that you did subscribe to. */ + struct aws_array_list published_messages; /* list of struct received_publish_packet */ + size_t publishes_received; + size_t expected_publishes; + /* The returned QoS from mock server */ + struct aws_array_list qos_returned; /* list of uint_8 */ + size_t ops_completed; + size_t expected_ops_completed; +}; + +static struct mqtt_connection_state_test test_data = {0}; + +/* ========== HELPER FUNCTIONS FOR THE TEST ========== */ + +static void s_operation_statistics_on_any_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata); + +static void s_operation_statistics_on_incoming_channel_setup_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + struct mqtt_connection_state_test *state_test_data = user_data; + + state_test_data->error = error_code; + + if (!error_code) { + aws_mutex_lock(&state_test_data->lock); + state_test_data->server_disconnect_completed = false; + aws_mutex_unlock(&state_test_data->lock); + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel setup completed"); + + state_test_data->server_channel = channel; + struct aws_channel_slot *test_handler_slot = aws_channel_slot_new(channel); + aws_channel_slot_insert_end(channel, test_handler_slot); + mqtt_mock_server_handler_update_slot(state_test_data->mock_server, test_handler_slot); + aws_channel_slot_set_handler(test_handler_slot, state_test_data->mock_server); + } +} + +static void s_operation_statistics_on_incoming_channel_shutdown_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + (void)error_code; + (void)channel; + struct mqtt_connection_state_test *state_test_data = user_data; + aws_mutex_lock(&state_test_data->lock); + state_test_data->server_disconnect_completed = true; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel shutdown completed"); + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_operation_statistics_on_listener_destroy(struct aws_server_bootstrap *bootstrap, void *user_data) { + (void)bootstrap; + struct mqtt_connection_state_test *state_test_data = user_data; + aws_mutex_lock(&state_test_data->lock); + state_test_data->listener_destroyed = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_operation_statistics_is_listener_destroyed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->listener_destroyed; +} + +static void s_operation_statistics_wait_on_listener_cleanup(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_operation_statistics_is_listener_destroyed, state_test_data); + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_operation_statistics_on_connection_interrupted( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + (void)connection; + (void)error_code; + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_interrupted = true; + state_test_data->interruption_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "connection interrupted"); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_operation_statistics_on_connection_resumed( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + (void)return_code; + (void)session_present; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "reconnect completed"); + + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_resumed = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +/** sets up a unix domain socket server and socket options. Creates an mqtt connection configured to use + * the domain socket. + */ +static int s_operation_statistics_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { + aws_mqtt_library_init(allocator); + + struct mqtt_connection_state_test *state_test_data = ctx; + + AWS_ZERO_STRUCT(*state_test_data); + + state_test_data->allocator = allocator; + state_test_data->el_group = aws_event_loop_group_new_default(allocator, 1, NULL); + + state_test_data->mock_server = new_mqtt_mock_server(allocator); + ASSERT_NOT_NULL(state_test_data->mock_server); + + state_test_data->server_bootstrap = aws_server_bootstrap_new(allocator, state_test_data->el_group); + ASSERT_NOT_NULL(state_test_data->server_bootstrap); + + struct aws_socket_options socket_options = { + .connect_timeout_ms = 100, + .domain = AWS_SOCKET_LOCAL, + }; + + state_test_data->socket_options = socket_options; + ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); + ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); + + uint64_t timestamp = 0; + ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); + + snprintf( + state_test_data->endpoint.address, + sizeof(state_test_data->endpoint.address), + LOCAL_SOCK_TEST_PATTERN, + (long long unsigned)timestamp); + + struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { + .bootstrap = state_test_data->server_bootstrap, + .host_name = state_test_data->endpoint.address, + .port = state_test_data->endpoint.port, + .socket_options = &state_test_data->socket_options, + .incoming_callback = s_operation_statistics_on_incoming_channel_setup_fn, + .shutdown_callback = s_operation_statistics_on_incoming_channel_shutdown_fn, + .destroy_callback = s_operation_statistics_on_listener_destroy, + .user_data = state_test_data, + }; + state_test_data->listener = aws_server_bootstrap_new_socket_listener(&server_bootstrap_options); + + ASSERT_NOT_NULL(state_test_data->listener); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = state_test_data->el_group, + .max_entries = 1, + }; + state_test_data->host_resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = state_test_data->el_group, + .user_data = state_test_data, + .host_resolver = state_test_data->host_resolver, + }; + + state_test_data->client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + state_test_data->mqtt_client = aws_mqtt_client_new(allocator, state_test_data->client_bootstrap); + state_test_data->mqtt_connection = aws_mqtt_client_connection_new(state_test_data->mqtt_client); + ASSERT_NOT_NULL(state_test_data->mqtt_connection); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_interruption_handlers( + state_test_data->mqtt_connection, + s_operation_statistics_on_connection_interrupted, + state_test_data, + s_operation_statistics_on_connection_resumed, + state_test_data)); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_on_any_publish_handler( + state_test_data->mqtt_connection, s_operation_statistics_on_any_publish_received, state_test_data)); + + ASSERT_SUCCESS(aws_array_list_init_dynamic( + &state_test_data->published_messages, allocator, 4, sizeof(struct received_publish_packet))); + ASSERT_SUCCESS(aws_array_list_init_dynamic( + &state_test_data->any_published_messages, allocator, 4, sizeof(struct received_publish_packet))); + ASSERT_SUCCESS(aws_array_list_init_dynamic(&state_test_data->qos_returned, allocator, 2, sizeof(uint8_t))); + return AWS_OP_SUCCESS; +} + +static void s_received_publish_packet_list_clean_up(struct aws_array_list *list) { + for (size_t i = 0; i < aws_array_list_length(list); ++i) { + struct received_publish_packet *val_ptr = NULL; + aws_array_list_get_at_ptr(list, (void **)&val_ptr, i); + aws_byte_buf_clean_up(&val_ptr->payload); + aws_byte_buf_clean_up(&val_ptr->topic); + } + aws_array_list_clean_up(list); +} + +static int s_operation_statistics_clean_up_mqtt_server_fn( + struct aws_allocator *allocator, + int setup_result, + void *ctx) { + (void)allocator; + + if (!setup_result) { + struct mqtt_connection_state_test *state_test_data = ctx; + + s_received_publish_packet_list_clean_up(&state_test_data->published_messages); + s_received_publish_packet_list_clean_up(&state_test_data->any_published_messages); + aws_array_list_clean_up(&state_test_data->qos_returned); + aws_mqtt_client_connection_release(state_test_data->mqtt_connection); + aws_mqtt_client_release(state_test_data->mqtt_client); + aws_client_bootstrap_release(state_test_data->client_bootstrap); + aws_host_resolver_release(state_test_data->host_resolver); + aws_server_bootstrap_destroy_socket_listener(state_test_data->server_bootstrap, state_test_data->listener); + s_operation_statistics_wait_on_listener_cleanup(state_test_data); + aws_server_bootstrap_release(state_test_data->server_bootstrap); + aws_event_loop_group_release(state_test_data->el_group); + destroy_mqtt_mock_server(state_test_data->mock_server); + } + + aws_mqtt_library_clean_up(); + return AWS_OP_SUCCESS; +} + +static void s_operation_statistics_on_connection_complete_fn( + struct aws_mqtt_client_connection *connection, + int error_code, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->session_present = session_present; + state_test_data->mqtt_return_code = return_code; + state_test_data->error = error_code; + state_test_data->connection_completed = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_operation_statistics_is_connection_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_completed; +} + +static void s_operation_statistics_wait_for_connection_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, + &state_test_data->lock, + s_operation_statistics_is_connection_completed, + state_test_data); + state_test_data->connection_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +void s_operation_statistics_on_disconnect_fn(struct aws_mqtt_client_connection *connection, void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "disconnect completed"); + aws_mutex_lock(&state_test_data->lock); + state_test_data->client_disconnect_completed = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_operation_statistics_is_disconnect_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->client_disconnect_completed && state_test_data->server_disconnect_completed; +} + +static void s_operation_statistics_wait_for_disconnect_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, + &state_test_data->lock, + s_operation_statistics_is_disconnect_completed, + state_test_data); + state_test_data->client_disconnect_completed = false; + state_test_data->server_disconnect_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_operation_statistics_on_any_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + + struct aws_byte_buf payload_cp; + aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); + struct aws_byte_buf topic_cp; + aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); + struct received_publish_packet received_packet = { + .payload = payload_cp, + .topic = topic_cp, + .dup = dup, + .qos = qos, + .retain = retain, + }; + + aws_mutex_lock(&state_test_data->lock); + aws_array_list_push_back(&state_test_data->any_published_messages, &received_packet); + state_test_data->any_publishes_received++; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_operation_statistics_on_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + + (void)connection; + (void)topic; + struct mqtt_connection_state_test *state_test_data = userdata; + + struct aws_byte_buf payload_cp; + aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); + struct aws_byte_buf topic_cp; + aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); + struct received_publish_packet received_packet = { + .payload = payload_cp, + .topic = topic_cp, + .dup = dup, + .qos = qos, + .retain = retain, + }; + + aws_mutex_lock(&state_test_data->lock); + aws_array_list_push_back(&state_test_data->published_messages, &received_packet); + state_test_data->publishes_received++; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_operation_statistics_on_suback( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + (void)topic; + + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + if (!error_code) { + aws_array_list_push_back(&state_test_data->qos_returned, &qos); + } + state_test_data->subscribe_completed = true; + state_test_data->subscribe_complete_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_on_op_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + + struct mqtt_connection_state_test *state_test_data = userdata; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "pub op completed"); + aws_mutex_lock(&state_test_data->lock); + state_test_data->ops_completed++; + state_test_data->op_complete_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_ops_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->ops_completed == state_test_data->expected_ops_completed; +} + +static void s_wait_for_ops_completed(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_for_pred( + &state_test_data->cvar, &state_test_data->lock, 10000000000, s_is_ops_completed, state_test_data); + aws_mutex_unlock(&state_test_data->lock); +} + +static bool s_operation_statistics_is_subscribe_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->subscribe_completed; +} + +static void s_operation_statistics_wait_for_subscribe_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_operation_statistics_is_subscribe_completed, state_test_data); + state_test_data->subscribe_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +/* ========== PUBLISH TESTS ========== */ + +/** + * Make a connection, tell the server NOT to send Acks, publish and immediately check the statistics to make sure + * it is incomplete, then wait a little bit and check that it was properly marked as UnAcked, then send the PubAck + * confirm statistics are zero, and then disconnect + */ +static int s_test_mqtt_operation_statistics_simple_publish(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message"); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* We want to wait for 1 operation to complete */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Publish a packet */ + uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is only one operation waiting + * (The size of the topic, the size of the payload, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = pub_topic.len + payload_1.len + 4; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Send the PubAck and wait for the client to receive it */ + mqtt_mock_server_send_puback(state_test_data->mock_server, packet_id_1); + s_wait_for_ops_completed(state_test_data); + + /* Make sure the operation values are back to zero now that the publish went out */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_simple_publish, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_simple_publish, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/** + * Make five publishes offline, confirm they are in the incomplete statistics, make a connection to a server + * that does not send ACKs, send ConnAck, confirm five publishes are in unacked statistics, send PubAcks, + * confirm operation statistics are zero, and then disconnect + */ +static int s_test_mqtt_operation_statistics_offline_publish(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic_1"); + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("Test Message"); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + uint16_t pub_packet_id_1 = 0; + uint16_t pub_packet_id_2 = 0; + uint16_t pub_packet_id_3 = 0; + uint16_t pub_packet_id_4 = 0; + uint16_t pub_packet_id_5 = 0; + + /* Publish the five packets */ + for (int i = 0; i < 5; i++) { + uint16_t packet = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet > 0); + + if (i == 0) { + pub_packet_id_1 = packet; + } else if (i == 1) { + pub_packet_id_2 = packet; + } else if (i == 2) { + pub_packet_id_3 = packet; + } else if (i == 3) { + pub_packet_id_4 = packet; + } else { + pub_packet_id_5 = packet; + } + } + + /* Wait a little bit to make sure the client has processed them (and NOT put them in the un-acked statistics) */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is five operations waiting + * Each packet size = (The size of the topic, the size of the payload, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = (pub_topic.len + payload.len + 4) * 5; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(5, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + // The UnAcked operations should be zero, because we are NOT connected + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* We want to wait for 5 operations to complete */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 5; + aws_mutex_unlock(&state_test_data->lock); + + /* Send the PubAck for each packet and wait for the client to receive it */ + mqtt_mock_server_send_puback(state_test_data->mock_server, pub_packet_id_1); + mqtt_mock_server_send_puback(state_test_data->mock_server, pub_packet_id_2); + mqtt_mock_server_send_puback(state_test_data->mock_server, pub_packet_id_3); + mqtt_mock_server_send_puback(state_test_data->mock_server, pub_packet_id_4); + mqtt_mock_server_send_puback(state_test_data->mock_server, pub_packet_id_5); + s_wait_for_ops_completed(state_test_data); + + /* Make sure the operation values are back to zero now that the publish went out */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_offline_publish, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_offline_publish, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/** + * Make five publishes offline and confirm the operation statistics properly tracks them only in the incomplete + * operations, before connecting and confirming post-connect they are also in unacked in the operation statistics. + * Then disconnect and confirm the operation statistics are still correct post-disconnect + */ +static int s_test_mqtt_operation_statistics_disconnect_publish(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic_1"); + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("Test Message"); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* Publish the five packets */ + for (int i = 0; i < 5; i++) { + uint16_t packet = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet > 0); + } + + /* Wait a little bit to make sure the client has processed them (and NOT put them in the un-acked statistics) */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is five operations waiting + * Each packet size = (The size of the topic, the size of the payload, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = (pub_topic.len + payload.len + 4) * 5; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(5, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + // The UnAcked operations should be zero, because we are NOT connected + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Wait a little bit to make sure the client has had a chance to put the publishes out */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Confirm the UnAcked operations are now correct as well */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(5, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(5, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + /* Wait a little bit just to make sure the client has fully processed the shutdown */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Confirm the operation statistics are still correctly tracking post-disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(5, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(5, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_disconnect_publish, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_disconnect_publish, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/** + * Makes a publish offline, checks operation statistics, connects to non-ACK sending server, checks operation + * statistics, makes another publish while online, checks operation statistics, disconnects, makes another publish, and + * finally checks operation statistics one last time. + */ +static int s_test_mqtt_operation_statistics_reconnect_publish(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic_1"); + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("Test Message"); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* First publish! */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + uint64_t pub_packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(pub_packet_id_1 > 0); + s_wait_for_ops_completed(state_test_data); + + /* Make sure the sizes are correct and there is five operations waiting + * Each packet size = (The size of the topic, the size of the payload, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = (pub_topic.len + payload.len + 4); + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + // The UnAcked operations should be zero, because we are NOT connected + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Wait a second to give the MQTT311 client time to move the offline publish to unacked */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Confirm the UnAcked operations are now correct as well */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Second publish! */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + uint64_t pub_packet_id_2 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(pub_packet_id_2 > 0); + s_wait_for_ops_completed(state_test_data); + + /* Confirm both publishes are correct across all statistics */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(2, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size * 2, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(2, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size * 2, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + /* Third publish! */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + uint64_t pub_packet_id_3 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(pub_packet_id_3 > 0); + s_wait_for_ops_completed(state_test_data); + + /* Confirm all three publishes are in the incomplete statistics, but only two are in unacked */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(3, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size * 3, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(2, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size * 2, operation_statistics.unacked_operation_size); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_reconnect_publish, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_reconnect_publish, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/* ========== SUBSCRIBE TESTS ========== */ + +/** + * Make a connection, tell the server NOT to send Acks, subscribe and check the statistics to make sure + * it is incomplete, then wait a little bit and check that it was properly marked as UnAcked, then send the SubAck + * confirm statistics are zero, and then disconnect + */ +static int s_test_mqtt_operation_statistics_simple_subscribe(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor sub_topic = aws_byte_cursor_from_c_str("/test/topic"); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* We want to wait for 1 operation */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + // Send a subscribe packet + uint16_t packet_id_1 = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &sub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_operation_statistics_on_publish_received, + state_test_data, + NULL, + s_operation_statistics_on_suback, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is only one operation waiting + * (The size of the topic + 3 (QoS, MSB, LSB), 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = sub_topic.len + 7; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Send the SubAck and wait for the client to get the ACK */ + mqtt_mock_server_send_single_suback(state_test_data->mock_server, packet_id_1, AWS_MQTT_QOS_AT_LEAST_ONCE); + s_wait_for_ops_completed(state_test_data); + + /* Make sure the operation statistics are empty */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_simple_subscribe, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_simple_subscribe, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/* ========== UNSUBSCRIBE TESTS ========== */ + +/** + * Make a connection, tell the server NOT to send Acks, publish and immediately check the statistics to make sure + * it is incomplete, then wait a little bit and check that it was properly marked as UnAcked, then send the UnsubAck + * confirm statistics are zero, and then disconnect + */ +static int s_test_mqtt_operation_statistics_simple_unsubscribe(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor unsub_topic = aws_byte_cursor_from_c_str("/test/topic"); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* We want to wait for 1 operation */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* Send a subscribe packet */ + uint16_t packet_id_1 = aws_mqtt_client_connection_unsubscribe( + state_test_data->mqtt_connection, &unsub_topic, s_on_op_complete, state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is only one operation waiting + * (The size of the topic, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = unsub_topic.len + 4; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Send the UnsubAck and wait for the client to get the ACK */ + mqtt_mock_server_send_unsuback(state_test_data->mock_server, packet_id_1); + s_wait_for_ops_completed(state_test_data); + + /* Make sure the operation statistics are empty */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_simple_unsubscribe, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_simple_unsubscribe, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/* ========== RESUBSCRIBE TESTS ========== */ + +/** + * Subscribe to multiple topics prior to connection, make a CONNECT, unsubscribe to a topic, disconnect with the broker, + * make a connection with clean_session set to true, then call resubscribe (without the server being able to send ACKs) + * and confirm the operation statistics size is correct, then resubscribe and finally disconnect. + */ +static int s_test_mqtt_operation_statistics_simple_resubscribe(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor sub_topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); + struct aws_byte_cursor sub_topic_2 = aws_byte_cursor_from_c_str("/test/topic2"); + struct aws_byte_cursor sub_topic_3 = aws_byte_cursor_from_c_str("/test/topic3"); + + /* We want to wait for 3 operations */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 3; + aws_mutex_unlock(&state_test_data->lock); + + /* Subscribe to the three topics */ + uint16_t sub_packet_id_1 = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &sub_topic_1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_operation_statistics_on_publish_received, + state_test_data, + NULL, + s_operation_statistics_on_suback, + state_test_data); + ASSERT_TRUE(sub_packet_id_1 > 0); + uint16_t sub_packet_id_2 = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &sub_topic_2, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_operation_statistics_on_publish_received, + state_test_data, + NULL, + s_operation_statistics_on_suback, + state_test_data); + ASSERT_TRUE(sub_packet_id_2 > 0); + uint16_t sub_packet_id_3 = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &sub_topic_3, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_operation_statistics_on_publish_received, + state_test_data, + NULL, + s_operation_statistics_on_suback, + state_test_data); + ASSERT_TRUE(sub_packet_id_3 > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Confirm the 3 subscribes are both pending and unacked, and confirm their byte size + * The size = each subscribe: 4 (fixed header + packet ID) + topic filter + 3 (QoS, MSB and LSB length) */ + uint64_t expected_packet_size = 12; // fixed packet headers and IDs + expected_packet_size += sub_topic_1.len + 3; + expected_packet_size += sub_topic_2.len + 3; + expected_packet_size += sub_topic_3.len + 3; + /* Check the size (Note: UnAcked will be ZERO because we are not connected) */ + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(3, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Wait for the subscribes to complete */ + s_wait_for_ops_completed(state_test_data); + + /* We want to wait for 1 operation */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + /* unsubscribe to the first topic */ + uint16_t unsub_packet_id = aws_mqtt_client_connection_unsubscribe( + state_test_data->mqtt_connection, &sub_topic_1, s_on_op_complete, state_test_data); + ASSERT_TRUE(unsub_packet_id > 0); + s_wait_for_ops_completed(state_test_data); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + /* Note: The client is still subscribed to both topic_2 and topic_3 */ + + /* Reconnect to the same server */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Get all the packets out of the way */ + ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); + + // Stop ACKs - we want to determine the size + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* Resubscribes to topic_2 & topic_3 (Note: we do not need a callback for the purpose of this test) */ + uint16_t resub_packet_id = + aws_mqtt_resubscribe_existing_topics(state_test_data->mqtt_connection, NULL, state_test_data); + ASSERT_TRUE(resub_packet_id > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + // Make sure the resubscribe packet size is correct and there is only one resubscribe waiting + // The size = 4 (fixed header + packet ID) + [for each topic](topic filter size + 3 (QoS, MSB and LSB length)) + expected_packet_size = 4; + expected_packet_size += sub_topic_2.len + 3; + expected_packet_size += sub_topic_3.len + 3; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Send the resubscribe */ + mqtt_mock_server_send_single_suback(state_test_data->mock_server, resub_packet_id, AWS_MQTT_QOS_AT_LEAST_ONCE); + s_operation_statistics_wait_for_subscribe_to_complete(state_test_data); + + /* Enable ACKs again, and then disconnect */ + mqtt_mock_server_enable_auto_ack(state_test_data->mock_server); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_simple_resubscribe, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_simple_resubscribe, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) + +/* ========== OTHER TESTS ========== */ + +static void s_test_operation_statistics_simple_callback(struct aws_mqtt_client_connection *connection, void *userdata) { + struct aws_atomic_var *statistics_count = (struct aws_atomic_var *)userdata; + aws_atomic_fetch_add(statistics_count, 1); + + // Confirm we can get the operation statistics from the callback + struct aws_mqtt_connection_operation_statistics operation_statistics; + aws_mqtt_client_connection_get_stats(connection, &operation_statistics); + (void)operation_statistics; +} + +/** + * Tests the operation statistics callback to make sure it is being called as expected. This is a very simple + * test that just ensures the callback is being called multiple times AND that you can access the operation + * statistics from within the callback. + */ +static int s_test_mqtt_operation_statistics_simple_callback(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_operation_statistics_on_connection_complete_fn, + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message"); + + /* Connect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_operation_statistics_wait_for_connection_to_complete(state_test_data); + + /* Set the operation statistics callback */ + struct aws_atomic_var statistics_count; + aws_atomic_store_int(&statistics_count, 0); + aws_mqtt_client_connection_set_on_operation_statistics_handler( + state_test_data->mqtt_connection, s_test_operation_statistics_simple_callback, &statistics_count); + + // /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + // /* We want to wait for 1 operation to complete */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Publish a packet */ + uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + + /* Wait a little bit to allow the code to put the packet into the socket from the queue, allowing it + * to be unacked. If we check right away, we may or may not see it in the un-acked statistics */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + + /* Make sure the sizes are correct and there is only one operation waiting + * (The size of the topic, the size of the payload, 2 for the header, 2 for the packet ID) */ + uint64_t expected_packet_size = pub_topic.len + payload_1.len + 4; + struct aws_mqtt_connection_operation_statistics operation_statistics; + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(1, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(1, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(expected_packet_size, operation_statistics.unacked_operation_size); + + /* Assert the callback was called twice (first for putting in incomplete, second for putting in unacked) */ + ASSERT_INT_EQUALS(2, aws_atomic_load_int(&statistics_count)); + + /* Send the PubAck and wait for the client to receive it */ + mqtt_mock_server_send_puback(state_test_data->mock_server, packet_id_1); + s_wait_for_ops_completed(state_test_data); + + // /* Assert the callback was called */ + aws_thread_current_sleep((uint64_t)ONE_SEC); + ASSERT_INT_EQUALS(3, aws_atomic_load_int(&statistics_count)); + + /* Make sure the operation values are back to zero now that the publish went out */ + ASSERT_SUCCESS(aws_mqtt_client_connection_get_stats(state_test_data->mqtt_connection, &operation_statistics)); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.incomplete_operation_size); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_count); + ASSERT_INT_EQUALS(0, operation_statistics.unacked_operation_size); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_operation_statistics_on_disconnect_fn, state_test_data)); + s_operation_statistics_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_operation_statistics_simple_callback, + s_operation_statistics_setup_mqtt_server_fn, + s_test_mqtt_operation_statistics_simple_callback, + s_operation_statistics_clean_up_mqtt_server_fn, + &test_data) From 6668ffd60607bf070c078d46b8c8f4b2cecfc1db Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 20 Jan 2023 14:51:31 -0800 Subject: [PATCH 40/98] Fix and test decoder reset bug (#251) --- source/v5/mqtt5_decoder.c | 2 ++ tests/v5/mqtt5_encoding_tests.c | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/source/v5/mqtt5_decoder.c b/source/v5/mqtt5_decoder.c index 184f3daf..1df6be0b 100644 --- a/source/v5/mqtt5_decoder.c +++ b/source/v5/mqtt5_decoder.c @@ -1159,6 +1159,8 @@ int aws_mqtt5_decoder_init( void aws_mqtt5_decoder_reset(struct aws_mqtt5_decoder *decoder) { s_reset_decoder_for_new_packet(decoder); + + decoder->state = AWS_MQTT5_DS_READ_PACKET_TYPE; } void aws_mqtt5_decoder_clean_up(struct aws_mqtt5_decoder *decoder) { diff --git a/tests/v5/mqtt5_encoding_tests.c b/tests/v5/mqtt5_encoding_tests.c index b9e5c2db..daa25141 100644 --- a/tests/v5/mqtt5_encoding_tests.c +++ b/tests/v5/mqtt5_encoding_tests.c @@ -332,6 +332,39 @@ static int s_aws_mqtt5_encode_decode_round_trip_test( ASSERT_INT_EQUALS(1, tester.view_count); + /* Now do a check where we partially decode the buffer, reset, and then fully decode. */ + aws_mqtt5_decoder_reset(&decoder); + tester.view_count = 0; + + whole_cursor = aws_byte_cursor_from_buf(&whole_dest); + if (decode_fragment_size >= whole_cursor.len) { + whole_cursor.len--; + } else { + whole_cursor.len -= decode_fragment_size; + } + + while (whole_cursor.len > 0) { + size_t advance = aws_min_size(whole_cursor.len, decode_fragment_size); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&whole_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt5_decoder_on_data_received(&decoder, fragment_cursor)); + } + + /* Nothing should have been received */ + ASSERT_INT_EQUALS(0, tester.view_count); + + /* Reset and decode the whole packet, everything should be fine */ + aws_mqtt5_decoder_reset(&decoder); + whole_cursor = aws_byte_cursor_from_buf(&whole_dest); + while (whole_cursor.len > 0) { + size_t advance = aws_min_size(whole_cursor.len, decode_fragment_size); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&whole_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt5_decoder_on_data_received(&decoder, fragment_cursor)); + } + + ASSERT_INT_EQUALS(1, tester.view_count); + aws_byte_buf_clean_up(&whole_dest); aws_mqtt5_inbound_topic_alias_resolver_clean_up(&inbound_alias_resolver); aws_mqtt5_encoder_clean_up(&encoder); From 33c3455cec82b16feb940e12006cefd7b3ef4194 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Wed, 25 Jan 2023 11:39:26 -0500 Subject: [PATCH 41/98] make sure any operation staistics changes happen in locks (#255) --- source/client_channel_handler.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 2ad5808d..2a1222e6 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -813,9 +813,15 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en (void *)request->connection, request->packet_id); - /* Set the request as incomplete and un-acked in the operation statistics */ - aws_mqtt_connection_statistics_change_operation_statistic_state( - request->connection, request, AWS_MQTT_OSS_INCOMPLETE | AWS_MQTT_OSS_UNACKED); + { /* BEGIN CRITICAL SECTION */ + mqtt_connection_lock_synced_data(connection); + + /* Set the request as incomplete and un-acked in the operation statistics */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + request->connection, request, AWS_MQTT_OSS_INCOMPLETE | AWS_MQTT_OSS_UNACKED); + + mqtt_connection_unlock_synced_data(connection); + } /* END CRITICAL SECTION */ /* Put the request into the ongoing list */ aws_linked_list_push_back(&connection->thread_data.ongoing_requests_list, &request->list_node); @@ -939,16 +945,15 @@ uint16_t mqtt_create_request( aws_channel_acquire_hold(channel); } - mqtt_connection_unlock_synced_data(connection); + if (next_request && next_request->packet_size > 0) { + /* Set the status as incomplete */ + aws_mqtt_connection_statistics_change_operation_statistic_state( + next_request->connection, next_request, AWS_MQTT_OSS_INCOMPLETE); + } + mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ - if (next_request && next_request->packet_size > 0) { - /* Set the status as incomplete */ - aws_mqtt_connection_statistics_change_operation_statistic_state( - next_request->connection, next_request, AWS_MQTT_OSS_INCOMPLETE); - } - if (should_schedule_task) { AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, From b66bb1a9bb3e232c83387133e78c806215c8751c Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 1 Feb 2023 13:37:38 -0800 Subject: [PATCH 42/98] Port mqtt5 listener PR from staging to public (#234) * Port mqtt5 listener PR from staging to public * Unreference param * add logs * include header inttypes * make sure the print statement print before destory... --------- Co-authored-by: Zhihui Xia --- include/aws/mqtt/private/v5/mqtt5_callbacks.h | 90 +++++++ .../aws/mqtt/private/v5/mqtt5_client_impl.h | 6 + include/aws/mqtt/v5/mqtt5_client.h | 7 + include/aws/mqtt/v5/mqtt5_listener.h | 85 +++++++ source/v5/mqtt5_callbacks.c | 159 ++++++++++++ source/v5/mqtt5_client.c | 54 ++-- source/v5/mqtt5_listener.c | 121 +++++++++ tests/CMakeLists.txt | 1 + tests/v5/mqtt5_client_tests.c | 237 ++++++++++++++++++ 9 files changed, 729 insertions(+), 31 deletions(-) create mode 100644 include/aws/mqtt/private/v5/mqtt5_callbacks.h create mode 100644 include/aws/mqtt/v5/mqtt5_listener.h create mode 100644 source/v5/mqtt5_callbacks.c create mode 100644 source/v5/mqtt5_listener.c diff --git a/include/aws/mqtt/private/v5/mqtt5_callbacks.h b/include/aws/mqtt/private/v5/mqtt5_callbacks.h new file mode 100644 index 00000000..2ff4fb67 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_callbacks.h @@ -0,0 +1,90 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_CALLBACKS_H +#define AWS_MQTT_MQTT5_CALLBACKS_H + +#include + +#include +#include + +struct aws_mqtt5_callback_set; + +/* + * An internal type for managing chains of callbacks attached to an mqtt5 client. Supports chains for + * lifecycle event handling and incoming publish packet handling. + * + * Assumed to be owned and used only by an MQTT5 client. + */ +struct aws_mqtt5_callback_set_manager { + struct aws_mqtt5_client *client; + + struct aws_linked_list callback_set_entries; + + uint64_t next_callback_set_entry_id; +}; + +AWS_EXTERN_C_BEGIN + +/* + * Initializes a callback set manager + */ +AWS_MQTT_API +void aws_mqtt5_callback_set_manager_init( + struct aws_mqtt5_callback_set_manager *manager, + struct aws_mqtt5_client *client); + +/* + * Cleans up a callback set manager. + * + * aws_mqtt5_callback_set_manager_init must have been previously called or this will crash. + */ +AWS_MQTT_API +void aws_mqtt5_callback_set_manager_clean_up(struct aws_mqtt5_callback_set_manager *manager); + +/* + * Adds a callback set to the front of the handler chain. Returns an integer id that can be used to selectively + * remove the callback set from the manager. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +uint64_t aws_mqtt5_callback_set_manager_push_front( + struct aws_mqtt5_callback_set_manager *manager, + struct aws_mqtt5_callback_set *callback_set); + +/* + * Removes a callback set from the handler chain. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt5_callback_set_manager_remove(struct aws_mqtt5_callback_set_manager *manager, uint64_t callback_set_id); + +/* + * Walks the handler chain for an MQTT5 client's incoming publish messages. The chain's callbacks will be invoked + * until either the end is reached or one of the callbacks returns true. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt5_callback_set_manager_on_publish_received( + struct aws_mqtt5_callback_set_manager *manager, + const struct aws_mqtt5_packet_publish_view *publish_view); + +/* + * Walks the handler chain for an MQTT5 client's lifecycle events. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt5_callback_set_manager_on_lifecycle_event( + struct aws_mqtt5_callback_set_manager *manager, + const struct aws_mqtt5_client_lifecycle_event *lifecycle_event); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_CALLBACKS_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index ec11b650..7c083549 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -439,6 +440,11 @@ struct aws_mqtt5_client { */ struct aws_mqtt5_client_flow_control_state flow_control_state; + /* + * Manages notification listener chains for lifecycle events and incoming publishes + */ + struct aws_mqtt5_callback_set_manager callback_manager; + /* * When should the next PINGREQ be sent? */ diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 49bd7ba9..e99338ce 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -328,6 +328,13 @@ typedef void(aws_mqtt5_unsubscribe_completion_fn)( */ typedef void(aws_mqtt5_publish_received_fn)(const struct aws_mqtt5_packet_publish_view *publish, void *user_data); +/** + * Signature of a listener publish received callback that returns an indicator whether or not the publish + * was handled by the listener. + */ +typedef bool( + aws_mqtt5_listener_publish_received_fn)(const struct aws_mqtt5_packet_publish_view *publish, void *user_data); + /** * Signature of callback to invoke when a DISCONNECT is fully written to the socket (or fails to be) */ diff --git a/include/aws/mqtt/v5/mqtt5_listener.h b/include/aws/mqtt/v5/mqtt5_listener.h new file mode 100644 index 00000000..8d0498ce --- /dev/null +++ b/include/aws/mqtt/v5/mqtt5_listener.h @@ -0,0 +1,85 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_LISTENER_H +#define AWS_MQTT_MQTT5_LISTENER_H + +#include + +#include + +/* + * Callback signature for when an mqtt5 listener has completely destroyed itself. + */ +typedef void(aws_mqtt5_listener_termination_completion_fn)(void *complete_ctx); + +/** + * A record that tracks MQTT5 client callbacks which can be dynamically injected via a listener. + */ +struct aws_mqtt5_callback_set { + aws_mqtt5_listener_publish_received_fn *listener_publish_received_handler; + void *listener_publish_received_handler_user_data; + + aws_mqtt5_client_connection_event_callback_fn *lifecycle_event_handler; + void *lifecycle_event_handler_user_data; +}; + +/** + * Configuration options for MQTT5 listener objects. + */ +struct aws_mqtt5_listener_config { + + /** + * MQTT5 client to listen to events on + */ + struct aws_mqtt5_client *client; + + /** + * Callbacks to invoke when events occur on the MQTT5 client + */ + struct aws_mqtt5_callback_set listener_callbacks; + + /** + * Listener destruction is asynchronous and thus requires a termination callback and associated user data + * to notify the user that the listener has been fully destroyed and no further events will be received. + */ + aws_mqtt5_listener_termination_completion_fn *termination_callback; + void *termination_callback_user_data; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Creates a new MQTT5 listener object. For as long as the listener lives, incoming publishes and lifecycle events + * will be forwarded to the callbacks configured on the listener. + * + * @param allocator allocator to use + * @param config listener configuration + * @return a new aws_mqtt5_listener object + */ +AWS_MQTT_API struct aws_mqtt5_listener *aws_mqtt5_listener_new( + struct aws_allocator *allocator, + struct aws_mqtt5_listener_config *config); + +/** + * Adds a reference to an mqtt5 listener. + * + * @param listener listener to add a reference to + * @return the listener object + */ +AWS_MQTT_API struct aws_mqtt5_listener *aws_mqtt5_listener_acquire(struct aws_mqtt5_listener *listener); + +/** + * Removes a reference to an mqtt5 listener. When the reference count drops to zero, the listener's asynchronous + * destruction will be started. + * + * @param listener listener to remove a reference from + * @return NULL + */ +AWS_MQTT_API struct aws_mqtt5_listener *aws_mqtt5_listener_release(struct aws_mqtt5_listener *listener); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_LISTENER_H */ diff --git a/source/v5/mqtt5_callbacks.c b/source/v5/mqtt5_callbacks.c new file mode 100644 index 00000000..bb8943cd --- /dev/null +++ b/source/v5/mqtt5_callbacks.c @@ -0,0 +1,159 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include + +#include + +struct aws_mqtt5_callback_set_entry { + struct aws_allocator *allocator; + + struct aws_linked_list_node node; + + uint64_t id; + + struct aws_mqtt5_callback_set callbacks; +}; + +void aws_mqtt5_callback_set_manager_init( + struct aws_mqtt5_callback_set_manager *manager, + struct aws_mqtt5_client *client) { + + manager->client = client; /* no need to ref count, it's assumed to be owned by the client */ + manager->next_callback_set_entry_id = 1; + + aws_linked_list_init(&manager->callback_set_entries); +} + +void aws_mqtt5_callback_set_manager_clean_up(struct aws_mqtt5_callback_set_manager *manager) { + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt5_callback_set_entry *entry = AWS_CONTAINER_OF(node, struct aws_mqtt5_callback_set_entry, node); + node = aws_linked_list_next(node); + + aws_linked_list_remove(&entry->node); + aws_mem_release(entry->allocator, entry); + } +} + +static struct aws_mqtt5_callback_set_entry *s_new_callback_set_entry( + struct aws_mqtt5_callback_set_manager *manager, + struct aws_mqtt5_callback_set *callback_set) { + struct aws_mqtt5_callback_set_entry *entry = + aws_mem_calloc(manager->client->allocator, 1, sizeof(struct aws_mqtt5_callback_set_entry)); + + entry->allocator = manager->client->allocator; + entry->id = manager->next_callback_set_entry_id++; + entry->callbacks = *callback_set; + + AWS_LOGF_INFO( + AWS_LS_MQTT5_GENERAL, + "id=%p: callback manager created new entry :%" PRIu64, + (void *)manager->client, + entry->id); + + return entry; +} + +uint64_t aws_mqtt5_callback_set_manager_push_front( + struct aws_mqtt5_callback_set_manager *manager, + struct aws_mqtt5_callback_set *callback_set) { + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(manager->client->loop)); + + struct aws_mqtt5_callback_set_entry *entry = s_new_callback_set_entry(manager, callback_set); + + aws_linked_list_push_front(&manager->callback_set_entries, &entry->node); + + return entry->id; +} + +void aws_mqtt5_callback_set_manager_remove(struct aws_mqtt5_callback_set_manager *manager, uint64_t callback_set_id) { + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(manager->client->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt5_callback_set_entry *entry = AWS_CONTAINER_OF(node, struct aws_mqtt5_callback_set_entry, node); + node = aws_linked_list_next(node); + + if (entry->id == callback_set_id) { + aws_linked_list_remove(&entry->node); + + AWS_LOGF_INFO( + AWS_LS_MQTT5_GENERAL, + "id=%p: callback manager removed entry id=%" PRIu64, + (void *)manager->client, + entry->id); + aws_mem_release(entry->allocator, entry); + return; + } + } + AWS_LOGF_INFO( + AWS_LS_MQTT5_GENERAL, + "id=%p: callback manager failed to remove entry id=%" PRIu64 ", callback set id not found.", + (void *)manager->client, + callback_set_id); +} + +void aws_mqtt5_callback_set_manager_on_publish_received( + struct aws_mqtt5_callback_set_manager *manager, + const struct aws_mqtt5_packet_publish_view *publish_view) { + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(manager->client->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt5_callback_set_entry *entry = AWS_CONTAINER_OF(node, struct aws_mqtt5_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt5_callback_set *callback_set = &entry->callbacks; + if (callback_set->listener_publish_received_handler != NULL) { + bool handled = (*callback_set->listener_publish_received_handler)( + publish_view, callback_set->listener_publish_received_handler_user_data); + if (handled) { + return; + } + } + } + + if (manager->client->config->publish_received_handler != NULL) { + (*manager->client->config->publish_received_handler)( + publish_view, manager->client->config->publish_received_handler_user_data); + } +} + +void aws_mqtt5_callback_set_manager_on_lifecycle_event( + struct aws_mqtt5_callback_set_manager *manager, + const struct aws_mqtt5_client_lifecycle_event *lifecycle_event) { + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(manager->client->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt5_callback_set_entry *entry = AWS_CONTAINER_OF(node, struct aws_mqtt5_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt5_callback_set *callback_set = &entry->callbacks; + + if (callback_set->lifecycle_event_handler != NULL) { + struct aws_mqtt5_client_lifecycle_event listener_copy = *lifecycle_event; + listener_copy.user_data = callback_set->lifecycle_event_handler_user_data; + + (*callback_set->lifecycle_event_handler)(&listener_copy); + } + } + + struct aws_mqtt5_client_lifecycle_event client_copy = *lifecycle_event; + client_copy.user_data = manager->client->config->lifecycle_event_handler_user_data; + + if (manager->client->config->lifecycle_event_handler != NULL) { + (*manager->client->config->lifecycle_event_handler)(&client_copy); + } +} diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index b7de8e06..5374e41d 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -284,6 +284,8 @@ static void s_mqtt5_client_final_destroy(struct aws_mqtt5_client *client) { client_termination_handler_user_data = client->config->client_termination_handler_user_data; } + aws_mqtt5_callback_set_manager_clean_up(&client->callback_manager); + aws_mqtt5_client_operational_state_clean_up(&client->operational_state); aws_mqtt5_client_options_storage_destroy((struct aws_mqtt5_client_options_storage *)client->config); @@ -314,16 +316,13 @@ static void s_on_mqtt5_client_zero_ref_count(void *user_data) { static void s_aws_mqtt5_client_emit_stopped_lifecycle_event(struct aws_mqtt5_client *client) { AWS_LOGF_INFO(AWS_LS_MQTT5_CLIENT, "id=%p: emitting stopped lifecycle event", (void *)client); - if (client->config->lifecycle_event_handler != NULL) { - struct aws_mqtt5_client_lifecycle_event event; - AWS_ZERO_STRUCT(event); + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); - event.event_type = AWS_MQTT5_CLET_STOPPED; - event.client = client; - event.user_data = client->config->lifecycle_event_handler_user_data; + event.event_type = AWS_MQTT5_CLET_STOPPED; + event.client = client; - (*client->config->lifecycle_event_handler)(&event); - } + aws_mqtt5_callback_set_manager_on_lifecycle_event(&client->callback_manager, &event); } static void s_aws_mqtt5_client_emit_connecting_lifecycle_event(struct aws_mqtt5_client *client) { @@ -331,16 +330,13 @@ static void s_aws_mqtt5_client_emit_connecting_lifecycle_event(struct aws_mqtt5_ client->lifecycle_state = AWS_MQTT5_LS_CONNECTING; - if (client->config->lifecycle_event_handler != NULL) { - struct aws_mqtt5_client_lifecycle_event event; - AWS_ZERO_STRUCT(event); + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); - event.event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT; - event.client = client; - event.user_data = client->config->lifecycle_event_handler_user_data; + event.event_type = AWS_MQTT5_CLET_ATTEMPTING_CONNECT; + event.client = client; - (*client->config->lifecycle_event_handler)(&event); - } + aws_mqtt5_callback_set_manager_on_lifecycle_event(&client->callback_manager, &event); } static void s_aws_mqtt5_client_emit_connection_success_lifecycle_event( @@ -351,18 +347,15 @@ static void s_aws_mqtt5_client_emit_connection_success_lifecycle_event( client->lifecycle_state = AWS_MQTT5_LS_CONNECTED; - if (client->config->lifecycle_event_handler != NULL) { - struct aws_mqtt5_client_lifecycle_event event; - AWS_ZERO_STRUCT(event); + struct aws_mqtt5_client_lifecycle_event event; + AWS_ZERO_STRUCT(event); - event.event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS; - event.client = client; - event.user_data = client->config->lifecycle_event_handler_user_data; - event.settings = &client->negotiated_settings; - event.connack_data = connack_view; + event.event_type = AWS_MQTT5_CLET_CONNECTION_SUCCESS; + event.client = client; + event.settings = &client->negotiated_settings; + event.connack_data = connack_view; - (*client->config->lifecycle_event_handler)(&event); - } + aws_mqtt5_callback_set_manager_on_lifecycle_event(&client->callback_manager, &event); } /* @@ -409,15 +402,12 @@ static void s_aws_mqtt5_client_emit_final_lifecycle_event( } event.error_code = error_code; - event.user_data = client->config->lifecycle_event_handler_user_data; event.connack_data = connack_view; event.disconnect_data = disconnect_view; client->lifecycle_state = AWS_MQTT5_LS_NONE; - if (client->config->lifecycle_event_handler != NULL) { - (*client->config->lifecycle_event_handler)(&event); - } + aws_mqtt5_callback_set_manager_on_lifecycle_event(&client->callback_manager, &event); } /* @@ -1930,7 +1920,7 @@ static void s_aws_mqtt5_client_connected_on_packet_received( case AWS_MQTT5_PT_PUBLISH: { const struct aws_mqtt5_packet_publish_view *publish_view = packet_view; - client->config->publish_received_handler(publish_view, client->config->publish_received_handler_user_data); + aws_mqtt5_callback_set_manager_on_publish_received(&client->callback_manager, publish_view); /* Send a puback if QoS 1+ */ if (publish_view->qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { @@ -2057,6 +2047,8 @@ struct aws_mqtt5_client *aws_mqtt5_client_new( aws_ref_count_init(&client->ref_count, client, s_on_mqtt5_client_zero_ref_count); + aws_mqtt5_callback_set_manager_init(&client->callback_manager, client); + if (aws_mqtt5_client_operational_state_init(&client->operational_state, allocator, client)) { goto on_error; } diff --git a/source/v5/mqtt5_listener.c b/source/v5/mqtt5_listener.c new file mode 100644 index 00000000..04170394 --- /dev/null +++ b/source/v5/mqtt5_listener.c @@ -0,0 +1,121 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include + +struct aws_mqtt5_listener { + struct aws_allocator *allocator; + + struct aws_ref_count ref_count; + + struct aws_mqtt5_listener_config config; + + uint64_t callback_set_id; + + struct aws_task initialize_task; + struct aws_task terminate_task; +}; + +static void s_mqtt5_listener_destroy(struct aws_mqtt5_listener *listener) { + + aws_mqtt5_client_release(listener->config.client); + + aws_mqtt5_listener_termination_completion_fn *termination_callback = listener->config.termination_callback; + void *temination_callback_user_data = listener->config.termination_callback_user_data; + + aws_mem_release(listener->allocator, listener); + + if (termination_callback != NULL) { + (*termination_callback)(temination_callback_user_data); + } +} + +static void s_mqtt5_listener_initialize_task_fn(struct aws_task *task, void *arg, enum aws_task_status task_status) { + (void)task; + + struct aws_mqtt5_listener *listener = arg; + + if (task_status == AWS_TASK_STATUS_RUN_READY) { + listener->callback_set_id = aws_mqtt5_callback_set_manager_push_front( + &listener->config.client->callback_manager, &listener->config.listener_callbacks); + AWS_LOGF_INFO( + AWS_LS_MQTT5_GENERAL, + "id=%p: Mqtt5 Listener initialized, listener id=%p", + (void *)listener->config.client, + (void *)listener); + aws_mqtt5_listener_release(listener); + } else { + s_mqtt5_listener_destroy(listener); + } +} + +static void s_mqtt5_listener_terminate_task_fn(struct aws_task *task, void *arg, enum aws_task_status task_status) { + (void)task; + + struct aws_mqtt5_listener *listener = arg; + + if (task_status == AWS_TASK_STATUS_RUN_READY) { + aws_mqtt5_callback_set_manager_remove(&listener->config.client->callback_manager, listener->callback_set_id); + } + + AWS_LOGF_INFO( + AWS_LS_MQTT5_GENERAL, + "id=%p: Mqtt5 Listener terminated, listener id=%p", + (void *)listener->config.client, + (void *)listener); + + s_mqtt5_listener_destroy(listener); +} + +static void s_aws_mqtt5_listener_on_zero_ref_count(void *context) { + struct aws_mqtt5_listener *listener = context; + + aws_event_loop_schedule_task_now(listener->config.client->loop, &listener->terminate_task); +} + +struct aws_mqtt5_listener *aws_mqtt5_listener_new( + struct aws_allocator *allocator, + struct aws_mqtt5_listener_config *config) { + if (config->client == NULL) { + return NULL; + } + + struct aws_mqtt5_listener *listener = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_listener)); + + listener->allocator = allocator; + listener->config = *config; + + aws_mqtt5_client_acquire(config->client); + aws_ref_count_init(&listener->ref_count, listener, s_aws_mqtt5_listener_on_zero_ref_count); + + aws_task_init(&listener->initialize_task, s_mqtt5_listener_initialize_task_fn, listener, "Mqtt5ListenerInitialize"); + aws_task_init(&listener->terminate_task, s_mqtt5_listener_terminate_task_fn, listener, "Mqtt5ListenerTerminate"); + + aws_mqtt5_listener_acquire(listener); + aws_event_loop_schedule_task_now(config->client->loop, &listener->initialize_task); + + return listener; +} + +struct aws_mqtt5_listener *aws_mqtt5_listener_acquire(struct aws_mqtt5_listener *listener) { + if (listener != NULL) { + aws_ref_count_acquire(&listener->ref_count); + } + + return listener; +} + +struct aws_mqtt5_listener *aws_mqtt5_listener_release(struct aws_mqtt5_listener *listener) { + if (listener != NULL) { + aws_ref_count_release(&listener->ref_count); + } + + return NULL; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e4539e8..177c072b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -293,6 +293,7 @@ add_test_case(mqtt5_client_statistics_publish_qos0) add_test_case(mqtt5_client_statistics_publish_qos1) add_test_case(mqtt5_client_statistics_publish_qos1_requeue) add_test_case(mqtt5_client_puback_ordering) +add_test_case(mqtt5_client_listeners) add_test_case(mqtt5_client_offline_operation_submission_fail_all) add_test_case(mqtt5_client_offline_operation_submission_fail_qos0) add_test_case(mqtt5_client_offline_operation_submission_fail_non_qos1) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index dd0717de..89135166 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -4689,6 +4690,242 @@ static int s_mqtt5_client_puback_ordering_fn(struct aws_allocator *allocator, vo AWS_TEST_CASE(mqtt5_client_puback_ordering, s_mqtt5_client_puback_ordering_fn) +enum aws_mqtt5_listener_test_publish_received_callback_type { + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, +}; + +struct aws_mqtt5_listeners_test_context { + struct aws_mqtt5_client_mock_test_fixture *test_fixture; + struct aws_array_list publish_received_callback_types; + struct aws_mutex lock; + struct aws_condition_variable signal; +}; + +static void s_aws_mqtt5_listeners_test_context_init( + struct aws_mqtt5_listeners_test_context *listener_test_context, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*listener_test_context); + + aws_array_list_init_dynamic( + &listener_test_context->publish_received_callback_types, + allocator, + 0, + sizeof(enum aws_mqtt5_listener_test_publish_received_callback_type)); + aws_mutex_init(&listener_test_context->lock); + aws_condition_variable_init(&listener_test_context->signal); +} + +static void s_aws_mqtt5_listeners_test_context_clean_up( + struct aws_mqtt5_listeners_test_context *listener_test_context) { + aws_condition_variable_clean_up(&listener_test_context->signal); + aws_mutex_clean_up(&listener_test_context->lock); + aws_array_list_clean_up(&listener_test_context->publish_received_callback_types); +} + +static int s_aws_mqtt5_mock_server_reflect_publish( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + struct aws_mqtt5_packet_publish_view reflected_view = *publish_view; + + if (reflected_view.qos != AWS_MQTT5_QOS_AT_MOST_ONCE) { + reflected_view.packet_id = 1; + } + + if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflected_view)) { + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_listeners_test_publish_received_default_handler( + const struct aws_mqtt5_packet_publish_view *publish_view, + void *user_data) { + (void)publish_view; + + struct aws_mqtt5_listeners_test_context *context = user_data; + + aws_mutex_lock(&context->lock); + enum aws_mqtt5_listener_test_publish_received_callback_type callback_type = AWS_MQTT5_LTPRCT_DEFAULT; + aws_array_list_push_back(&context->publish_received_callback_types, &callback_type); + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static bool s_aws_mqtt5_listeners_test_publish_received_listener_handler( + const struct aws_mqtt5_packet_publish_view *publish_view, + void *user_data) { + struct aws_mqtt5_listeners_test_context *context = user_data; + + aws_mutex_lock(&context->lock); + enum aws_mqtt5_listener_test_publish_received_callback_type callback_type = AWS_MQTT5_LTPRCT_LISTENER; + aws_array_list_push_back(&context->publish_received_callback_types, &callback_type); + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); + + return publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE; +} + +struct aws_mqtt5_listeners_test_wait_context { + size_t callback_count; + struct aws_mqtt5_listeners_test_context *test_fixture; +}; + +static bool s_aws_mqtt5_listeners_test_wait_on_callback_count(void *context) { + struct aws_mqtt5_listeners_test_wait_context *wait_context = context; + return wait_context->callback_count == + aws_array_list_length(&wait_context->test_fixture->publish_received_callback_types); +} + +static int s_aws_mqtt5_listeners_test_wait_on_and_verify_callbacks( + struct aws_mqtt5_listeners_test_context *context, + size_t callback_count, + enum aws_mqtt5_listener_test_publish_received_callback_type *callback_types) { + struct aws_mqtt5_listeners_test_wait_context wait_context = { + .callback_count = callback_count, + .test_fixture = context, + }; + + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred( + &context->signal, &context->lock, s_aws_mqtt5_listeners_test_wait_on_callback_count, &wait_context); + for (size_t i = 0; i < callback_count; ++i) { + enum aws_mqtt5_listener_test_publish_received_callback_type callback_type; + aws_array_list_get_at(&context->publish_received_callback_types, &callback_type, i); + + ASSERT_INT_EQUALS(callback_types[i], callback_type); + } + aws_mutex_unlock(&context->lock); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5_client_listeners_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_mock_server_reflect_publish; + + struct aws_mqtt5_client_mock_test_fixture test_context; + struct aws_mqtt5_listeners_test_context full_test_context = { + .test_fixture = &test_context, + }; + s_aws_mqtt5_listeners_test_context_init(&full_test_context, allocator); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &full_test_context, + }; + + test_fixture_options.client_options->publish_received_handler = + s_aws_mqtt5_listeners_test_publish_received_default_handler; + test_fixture_options.client_options->publish_received_handler_user_data = &full_test_context; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + struct aws_mqtt5_packet_publish_view qos0_publish_view = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + }; + + struct aws_mqtt5_packet_publish_view qos1_publish_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = + { + .ptr = s_sub_pub_unsub_publish_topic, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_topic) - 1, + }, + }; + + // send a qos 0 publish, wait for it to reflect, verify it's the default handler + aws_mqtt5_client_publish(client, &qos0_publish_view, NULL); + + enum aws_mqtt5_listener_test_publish_received_callback_type first_callback_type_array[] = { + AWS_MQTT5_LTPRCT_DEFAULT, + }; + ASSERT_SUCCESS(s_aws_mqtt5_listeners_test_wait_on_and_verify_callbacks( + &full_test_context, AWS_ARRAY_SIZE(first_callback_type_array), first_callback_type_array)); + + // attach a listener at the beginning of the handler chain + struct aws_mqtt5_listener_config listener_config = { + .client = client, + .listener_callbacks = { + .listener_publish_received_handler_user_data = &full_test_context, + .listener_publish_received_handler = s_aws_mqtt5_listeners_test_publish_received_listener_handler, + }}; + struct aws_mqtt5_listener *listener = aws_mqtt5_listener_new(allocator, &listener_config); + + // send a qos 0 publish, wait for it to reflect, verify both handlers were invoked in the proper order + aws_mqtt5_client_publish(client, &qos0_publish_view, NULL); + + enum aws_mqtt5_listener_test_publish_received_callback_type second_callback_type_array[] = { + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, + AWS_MQTT5_LTPRCT_DEFAULT, + }; + ASSERT_SUCCESS(s_aws_mqtt5_listeners_test_wait_on_and_verify_callbacks( + &full_test_context, AWS_ARRAY_SIZE(second_callback_type_array), second_callback_type_array)); + + // send a qos1 publish (which is short-circuited by the listener), verify just the listener was notified + aws_mqtt5_client_publish(client, &qos1_publish_view, NULL); + + enum aws_mqtt5_listener_test_publish_received_callback_type third_callback_type_array[] = { + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, + }; + ASSERT_SUCCESS(s_aws_mqtt5_listeners_test_wait_on_and_verify_callbacks( + &full_test_context, AWS_ARRAY_SIZE(third_callback_type_array), third_callback_type_array)); + + // remove the listener + aws_mqtt5_listener_release(listener); + + // send a qos1 publish, wait for it to reflect, verify it's the default handler + aws_mqtt5_client_publish(client, &qos1_publish_view, NULL); + + enum aws_mqtt5_listener_test_publish_received_callback_type fourth_callback_type_array[] = { + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, + AWS_MQTT5_LTPRCT_DEFAULT, + AWS_MQTT5_LTPRCT_LISTENER, + AWS_MQTT5_LTPRCT_DEFAULT, + }; + ASSERT_SUCCESS(s_aws_mqtt5_listeners_test_wait_on_and_verify_callbacks( + &full_test_context, AWS_ARRAY_SIZE(fourth_callback_type_array), fourth_callback_type_array)); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + s_aws_mqtt5_listeners_test_context_clean_up(&full_test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_listeners, s_mqtt5_client_listeners_fn) + static void s_on_offline_publish_completion( enum aws_mqtt5_packet_type packet_type, const void *packet, From 13cae5ae2c57e454939d9c4628d3dd1382a69ff9 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 3 Feb 2023 15:48:42 -0500 Subject: [PATCH 43/98] Add on_closed callback to MQTT311 (#258) Add on_closed callback that is called whenever a mqtt311 connection is fully closed --- include/aws/mqtt/client.h | 31 ++++++ include/aws/mqtt/private/client_impl.h | 2 + source/client.c | 19 ++++ tests/CMakeLists.txt | 3 + tests/v3/connection_state_test.c | 146 +++++++++++++++++++++++++ 5 files changed, 201 insertions(+) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 143ab73d..f12a5c19 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -25,6 +25,12 @@ struct aws_http_proxy_options; struct aws_socket_options; struct aws_tls_connection_options; +/** + * Empty struct that is passed when on_connection_closed is called. + * Currently holds nothing but will allow expanding in the future should it be needed. + */ +struct on_connection_closed_data; + struct aws_mqtt_client { struct aws_allocator *allocator; struct aws_client_bootstrap *bootstrap; @@ -63,6 +69,16 @@ typedef void(aws_mqtt_client_on_connection_interrupted_fn)( int error_code, void *userdata); +/** + * Called if the connection to the server is closed by user request + * Note: Currently the "data" argument is always NULL, but this may change in the future if additional data is needed to + * be sent. + */ +typedef void(aws_mqtt_client_on_connection_closed_fn)( + struct aws_mqtt_client_connection *connection, + struct on_connection_closed_data *data, + void *userdata); + /** * Called when a connection to the server is resumed * (if clean_session is true, calling aws_mqtt_resubscribe_existing_topics is suggested) @@ -403,6 +419,21 @@ int aws_mqtt_client_connection_set_connection_interruption_handlers( aws_mqtt_client_on_connection_resumed_fn *on_resumed, void *on_resumed_ud); +/** + * Sets the callback to call when the connection is closed normally by user request. + * This is different than the connection interrupted or lost, this only covers successful + * closure. + * + * \param[in] connection The connection object + * \param[in] on_closed The function to call when a connection is closed + * \param[in] on_closed_ud Userdata for on_closed + */ +AWS_MQTT_API +int aws_mqtt_client_connection_set_connection_closed_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_ud); + /** * Sets the callback to call whenever ANY publish packet is received. Only safe to set when connection is not connected. * diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index bbf7e44b..185ebc17 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -222,6 +222,8 @@ struct aws_mqtt_client_connection { void *on_interrupted_ud; aws_mqtt_client_on_connection_resumed_fn *on_resumed; void *on_resumed_ud; + aws_mqtt_client_on_connection_closed_fn *on_closed; + void *on_closed_ud; aws_mqtt_client_publish_received_fn *on_any_publish; void *on_any_publish_ud; aws_mqtt_client_on_disconnect_fn *on_disconnect; diff --git a/source/client.c b/source/client.c index 4465fc4a..2e9fbae8 100644 --- a/source/client.c +++ b/source/client.c @@ -363,6 +363,7 @@ static void s_mqtt_client_shutdown( "id=%p: Caller requested disconnect from on_interrupted callback, aborting reconnect", (void *)connection); MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); + MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); break; case AWS_MQTT_CLIENT_STATE_DISCONNECTING: AWS_LOGF_DEBUG( @@ -370,6 +371,7 @@ static void s_mqtt_client_shutdown( "id=%p: Disconnect completed, clearing request queue and calling callback", (void *)connection); MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); + MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); break; case AWS_MQTT_CLIENT_STATE_CONNECTING: AWS_LOGF_TRACE( @@ -1086,6 +1088,23 @@ int aws_mqtt_client_connection_set_connection_interruption_handlers( return AWS_OP_SUCCESS; } +int aws_mqtt_client_connection_set_connection_closed_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_ud) { + + AWS_PRECONDITION(connection); + if (s_check_connection_state_for_configuration(connection)) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Setting connection closed handler", (void *)connection); + + connection->on_closed = on_closed; + connection->on_closed_ud = on_closed_ud; + + return AWS_OP_SUCCESS; +} + int aws_mqtt_client_connection_set_on_any_publish_handler( struct aws_mqtt_client_connection *connection, aws_mqtt_client_publish_received_fn *on_any_publish, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 177c072b..0473d50e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,6 +61,9 @@ add_test_case(mqtt_clean_session_keep_next_session) add_test_case(mqtt_connection_publish_QoS1_timeout) add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) +add_test_case(mqtt_connection_close_callback_simple) +add_test_case(mqtt_connection_close_callback_interrupted) +add_test_case(mqtt_connection_close_callback_multi) # Operation statistics tests add_test_case(mqtt_operation_statistics_simple_publish) add_test_case(mqtt_operation_statistics_offline_publish) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 0422aafb..58a141ed 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -73,6 +73,7 @@ struct mqtt_connection_state_test { struct aws_array_list qos_returned; /* list of uint_8 */ size_t ops_completed; size_t expected_ops_completed; + size_t connection_close_calls; /* All of the times on_connection_closed has been called */ }; static struct mqtt_connection_state_test test_data = {0}; @@ -2759,3 +2760,148 @@ AWS_TEST_CASE_FIXTURE( s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time_fn, s_clean_up_mqtt_server_fn, &test_data) + +/* Function called for testing the on_connection_closed callback */ +static void s_on_connection_closed_fn( + struct aws_mqtt_client_connection *connection, + struct on_connection_closed_data *data, + void *userdata) { + (void)connection; + (void)data; + + struct mqtt_connection_state_test *state_test_data = (struct mqtt_connection_state_test *)userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_close_calls += 1; + aws_mutex_unlock(&state_test_data->lock); +} + +/** + * Test that the connection close callback is fired only once and when the connection was closed + */ +static int s_test_mqtt_connection_close_callback_simple_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + aws_mqtt_client_connection_set_connection_closed_handler( + state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + /* sleep for 2 sec, just to make sure the connection is stable */ + aws_thread_current_sleep((uint64_t)ONE_SEC * 2); + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + /* Make sure the callback was called and the value is what we expect */ + ASSERT_UINT_EQUALS(1, state_test_data->connection_close_calls); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_close_callback_simple, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_close_callback_simple_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that the connection close callback is NOT fired during an interrupt + */ +static int s_test_mqtt_connection_close_callback_interrupted_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + aws_mqtt_client_connection_set_connection_closed_handler( + state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + /* Kill the connection */ + aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); + s_wait_for_reconnect_to_complete(state_test_data); + + /* sleep for 2 sec, just to make sure the connection is stable */ + aws_thread_current_sleep((uint64_t)ONE_SEC * 2); + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + /* Make sure the callback was called only ONCE and the value is what we expect */ + ASSERT_UINT_EQUALS(1, state_test_data->connection_close_calls); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_close_callback_interrupted, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_close_callback_interrupted_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that the connection close callback is called every time a disconnect happens, if it happens multiple times + */ +static int s_test_mqtt_connection_close_callback_multi_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + aws_mqtt_client_connection_set_connection_closed_handler( + state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); + + int disconnect_amount = 10; + for (int i = 0; i < disconnect_amount; i++) { + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + /* Disconnect */ + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + } + + /* Make sure the callback was called disconnect_amount times */ + ASSERT_UINT_EQUALS(disconnect_amount, state_test_data->connection_close_calls); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_close_callback_multi, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_close_callback_multi_fn, + s_clean_up_mqtt_server_fn, + &test_data) From 09db26d949a8ecf64fe68ada90da28bc03b0ad1d Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 3 Feb 2023 18:10:21 -0500 Subject: [PATCH 44/98] Remove upper bound checks in MQTT5 tests for now (#259) * just increase the threshold from 10 percent to 30 percent --- tests/v5/mqtt5_client_tests.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 89135166..909df6f2 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -1040,7 +1040,7 @@ static int s_verify_ping_sequence_timing(struct aws_mqtt5_client_mock_test_fixtu uint64_t time_delta_millis = aws_timestamp_convert(time_delta_ns, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); - ASSERT_TRUE(s_is_within_percentage_of(TEST_PING_INTERVAL_MS, time_delta_millis, .1)); + ASSERT_TRUE(s_is_within_percentage_of(TEST_PING_INTERVAL_MS, time_delta_millis, .3)); last_packet_time = record->timestamp; } @@ -1251,7 +1251,7 @@ static int s_verify_ping_timeout_interval(struct aws_mqtt5_client_mock_test_fixt uint64_t expected_connected_time_ms = TIMEOUT_TEST_PING_INTERVAL_MS + (uint64_t)test_context->client->config->ping_timeout_ms; - ASSERT_TRUE(s_is_within_percentage_of(expected_connected_time_ms, connected_interval_ms, .1)); + ASSERT_TRUE(s_is_within_percentage_of(expected_connected_time_ms, connected_interval_ms, .3)); return AWS_OP_SUCCESS; } @@ -1395,7 +1395,7 @@ static int s_verify_reconnection_exponential_backoff_timestamps( uint64_t time_diff = aws_timestamp_convert( record->timestamp - last_timestamp, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL); - if (!s_is_within_percentage_of(expected_backoff, time_diff, .1)) { + if (!s_is_within_percentage_of(expected_backoff, time_diff, .3)) { return AWS_OP_ERR; } @@ -1613,7 +1613,7 @@ static int s_verify_reconnection_after_success_used_backoff( AWS_TIMESTAMP_MILLIS, NULL); - if (!s_is_within_percentage_of(expected_reconnect_delay_ms, post_success_reconnect_time_ms, .1)) { + if (!s_is_within_percentage_of(expected_reconnect_delay_ms, post_success_reconnect_time_ms, .3)) { return AWS_OP_ERR; } From 4de63451eb3fff89166c39b10fee6925b600ba7f Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Mon, 6 Feb 2023 14:27:53 -0800 Subject: [PATCH 45/98] remove next_attempt_ms (#260) * remove next_attempt_ms * calculate next connection attempt time at task creation --- include/aws/mqtt/private/client_impl.h | 1 - source/client.c | 36 ++++++++++++-------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 185ebc17..970bdf01 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -211,7 +211,6 @@ struct aws_mqtt_client_connection { uint64_t current_sec; /* seconds */ uint64_t min_sec; /* seconds */ uint64_t max_sec; /* seconds */ - uint64_t next_attempt_ms; /* milliseconds */ uint64_t next_attempt_reset_timer_ns; /* nanoseconds */ } reconnect_timeouts; diff --git a/source/client.c b/source/client.c index 2e9fbae8..bbcb2321 100644 --- a/source/client.c +++ b/source/client.c @@ -60,6 +60,20 @@ void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection *conne (void)err; } +static void s_aws_mqtt_schedule_reconnect_task(struct aws_mqtt_client_connection *connection) { + uint64_t next_attempt_ns = 0; + aws_high_res_clock_get_ticks(&next_attempt_ns); + next_attempt_ns += aws_timestamp_convert( + connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + aws_event_loop_schedule_task_future(connection->loop, &connection->reconnect_task->task, next_attempt_ns); + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Scheduling reconnect, for %" PRIu64 " on event-loop %p", + (void *)connection, + next_attempt_ns, + (void *)connection->loop); +} + static void s_aws_mqtt_client_destroy(struct aws_mqtt_client *client) { AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "client=%p: Cleaning up MQTT client", (void *)client); @@ -312,9 +326,7 @@ static void s_mqtt_client_shutdown( case AWS_MQTT_CLIENT_STATE_RECONNECTING: { /* If reconnect attempt failed, schedule the next attempt */ AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Reconnect failed, retrying", (void *)connection); - - aws_event_loop_schedule_task_future( - connection->loop, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); + s_aws_mqtt_schedule_reconnect_task(connection); break; } case AWS_MQTT_CLIENT_STATE_CONNECTED: { @@ -340,10 +352,7 @@ static void s_mqtt_client_shutdown( } /* END CRITICAL SECTION */ if (!stop_reconnect) { - aws_event_loop_schedule_task_future( - connection->loop, - &connection->reconnect_task->task, - connection->reconnect_timeouts.next_attempt_ms); + s_aws_mqtt_schedule_reconnect_task(connection); } break; } @@ -614,10 +623,6 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ mqtt_connection_lock_synced_data(connection); - aws_high_res_clock_get_ticks(&connection->reconnect_timeouts.next_attempt_ms); - connection->reconnect_timeouts.next_attempt_ms += aws_timestamp_convert( - connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); - AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Attempting reconnect, if it fails next attempt will be in %" PRIu64 " seconds", @@ -645,14 +650,7 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (s_mqtt_client_connect( connection, connection->on_connection_complete, connection->on_connection_complete_ud)) { /* If reconnect attempt failed, schedule the next attempt */ - aws_event_loop_schedule_task_future( - connection->loop, &connection->reconnect_task->task, connection->reconnect_timeouts.next_attempt_ms); - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, - "id=%p: Scheduling reconnect, for %" PRIu64 " on event-loop %p", - (void *)connection, - connection->reconnect_timeouts.next_attempt_ms, - (void *)connection->loop); + s_aws_mqtt_schedule_reconnect_task(connection); } else { /* Ideally, it would be nice to move this inside the lock, but I'm unsure of the correctness */ connection->reconnect_task->task.timestamp = 0; From 173bbb2155702b8cbb11abee2b767fdafd31fa6c Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:12:16 -0800 Subject: [PATCH 46/98] add utf8 validation check on user properties (#256) * add utf8 validation check on user properties * clang format * add utf8 validation for outbound packets * add utf-8 validation based on mqtt specs * optimize validator code * minor: fix comment style * update aws-c-common api&update utf8 util tests * added packet view valiadtion tests for utf8 strings * add tests in validation failure tests * added tests for user properties * updated aws-c-common encoding api * clang format * fix tests * update mqtt test with new encoding api * fix illegal utf8 string * fix disconnection validation * fix encoding test case * add util function for validation utf8 --------- Co-authored-by: Zhihui Xia --- include/aws/mqtt/mqtt.h | 1 + include/aws/mqtt/private/v5/mqtt5_utils.h | 8 + source/v5/mqtt5_options_storage.c | 91 +++++++ source/v5/mqtt5_utils.c | 35 +++ tests/CMakeLists.txt | 21 ++ tests/v5/mqtt5_operation_and_storage_tests.c | 8 +- ...mqtt5_operation_validation_failure_tests.c | 244 ++++++++++++++++++ tests/v5/mqtt5_utils_tests.c | 150 +++++++++++ 8 files changed, 554 insertions(+), 4 deletions(-) diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 8a86c2ed..22a63ce2 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -75,6 +75,7 @@ enum aws_mqtt_error { AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE, AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS, AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, + AWS_ERROR_MQTT5_INVALID_UTF8_STRING, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; diff --git a/include/aws/mqtt/private/v5/mqtt5_utils.h b/include/aws/mqtt/private/v5/mqtt5_utils.h index 0aacfba7..be4c8ba2 100644 --- a/include/aws/mqtt/private/v5/mqtt5_utils.h +++ b/include/aws/mqtt/private/v5/mqtt5_utils.h @@ -96,6 +96,14 @@ AWS_EXTERN_C_BEGIN */ AWS_MQTT_API extern struct aws_byte_cursor g_aws_mqtt5_connect_protocol_cursor; +/** + * Validate utf-8 string under mqtt5 specs + * + * @param text + * @return AWS_OP_SUCCESS if the text is validate, otherwise AWS_OP_ERR + */ +AWS_MQTT_API int aws_mqtt5_validate_utf8_text(struct aws_byte_cursor text); + /** * Simple helper function to compute the first byte of an MQTT packet encoding as a function of 4 bit flags * and the packet type. diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 493649c5..b8b566e9 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -183,6 +184,12 @@ static int s_aws_mqtt5_user_property_set_validate( property->name.len); return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); } + + if (aws_mqtt5_validate_utf8_text(property->name)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "id=%p: %s - user property #%zu name not valid UTF8", log_context, log_prefix, i); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } if (property->value.len > UINT16_MAX) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, @@ -193,6 +200,15 @@ static int s_aws_mqtt5_user_property_set_validate( property->value.len); return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); } + if (aws_mqtt5_validate_utf8_text(property->value)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: %s - user property #%zu value not valid UTF8", + log_context, + log_prefix, + i); + return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); + } } return AWS_OP_SUCCESS; @@ -329,6 +345,14 @@ int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); } + if (aws_mqtt5_validate_utf8_text(connect_options->client_id)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - client id not valid UTF-8", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } + if (connect_options->username != NULL) { if (connect_options->username->len > UINT16_MAX) { AWS_LOGF_ERROR( @@ -337,6 +361,14 @@ int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect (void *)connect_options); return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); } + + if (aws_mqtt5_validate_utf8_text(*connect_options->username)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_connect_view - username not valid UTF-8", + (void *)connect_options); + return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + } } if (connect_options->password != NULL) { @@ -424,6 +456,8 @@ int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect "id=%p: aws_mqtt5_packet_connect_view - CONNECT packet has unsupported authentication fields set.", (void *)connect_options); return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); + + // TODO: UTF-8 validation for authentication_method once supported. } return AWS_OP_SUCCESS; @@ -1237,6 +1271,14 @@ int aws_mqtt5_packet_disconnect_view_validate(const struct aws_mqtt5_packet_disc (void *)disconnect_view); return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); } + + if (aws_mqtt5_validate_utf8_text(*disconnect_view->reason_string)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_disconnect_view - reason string not valid UTF-8", + (void *)disconnect_view); + return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); + } } if (disconnect_view->server_reference != NULL) { @@ -1562,6 +1604,10 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - missing topic", (void *)publish_view); return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } else if (aws_mqtt5_validate_utf8_text(publish_view->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - topic not valid UTF-8", (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } else if (!aws_mqtt_is_valid_topic(&publish_view->topic)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, @@ -1590,6 +1636,18 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish (int)*publish_view->payload_format); return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } + + // Make sure the payload data is UTF-8 if the payload_format set to UTF8 + if (*publish_view->payload_format == AWS_MQTT5_PFI_UTF8) { + if (aws_mqtt5_validate_utf8_text(publish_view->payload)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - payload value is not valid UTF-8 while payload format " + "set to UTF-8", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + } } if (publish_view->response_topic != NULL) { @@ -1601,6 +1659,14 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } + if (aws_mqtt5_validate_utf8_text(*publish_view->response_topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - response topic not valid UTF-8", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } + if (!aws_mqtt_is_valid_topic(publish_view->response_topic)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, @@ -1638,6 +1704,14 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish (void *)publish_view); return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } + + if (aws_mqtt5_validate_utf8_text(*publish_view->content_type)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_publish_view - content type not valid UTF-8", + (void *)publish_view); + return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); + } } if (s_aws_mqtt5_user_property_set_validate( @@ -2289,6 +2363,14 @@ int aws_mqtt5_packet_unsubscribe_view_validate(const struct aws_mqtt5_packet_uns for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { const struct aws_byte_cursor *topic_filter = &unsubscribe_view->topic_filters[i]; + if (aws_mqtt5_validate_utf8_text(*topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_unsubscribe_view - topic filter not valid UTF-8: \"" PRInSTR "\"", + (void *)unsubscribe_view, + AWS_BYTE_CURSOR_PRI(*topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); + } if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, @@ -2576,6 +2658,15 @@ static int s_aws_mqtt5_validate_subscription( const struct aws_mqtt5_subscription_view *subscription, void *log_context) { + if (aws_mqtt5_validate_utf8_text(subscription->topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_GENERAL, + "id=%p: aws_mqtt5_packet_subscribe_view - topic filter \"" PRInSTR "\" not valid UTF-8 in subscription", + log_context, + AWS_BYTE_CURSOR_PRI(subscription->topic_filter)); + return aws_raise_error(AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION); + } + if (!aws_mqtt_is_valid_topic_filter(&subscription->topic_filter)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index 088bea24..88de757c 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -7,6 +7,7 @@ #include #include +#include #include uint8_t aws_mqtt5_compute_fixed_header_byte1(enum aws_mqtt5_packet_type packet_type, uint8_t flags) { @@ -537,3 +538,37 @@ bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_c return true; } + +/* UTF-8 encoded string validation respect to [MQTT-1.5.3-2]. */ +static int aws_mqtt5_utf8_decoder(uint32_t codepoint, void *user_data) { + (void)user_data; + /* U+0000 - A UTF-8 Encoded String MUST NOT include an encoding of the null character U+0000. [MQTT-1.5.4-2] + * U+0001..U+001F control characters are not valid + */ + if (AWS_UNLIKELY(codepoint <= 0x001F)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + /* U+007F..U+009F control characters are not valid */ + if (AWS_UNLIKELY((codepoint >= 0x007F) && (codepoint <= 0x009F))) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + /* Unicode non-characters are not valid: https://www.unicode.org/faq/private_use.html#nonchar1 */ + if (AWS_UNLIKELY((codepoint & 0x00FFFF) >= 0x00FFFE)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + if (AWS_UNLIKELY(codepoint >= 0xFDD0 && codepoint <= 0xFDEF)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + return AWS_ERROR_SUCCESS; +} + +struct aws_utf8_decoder_options g_aws_mqtt5_utf8_decoder_options = { + .on_codepoint = aws_mqtt5_utf8_decoder, +}; + +int aws_mqtt5_validate_utf8_text(struct aws_byte_cursor text) { + return aws_decode_utf8(text, &g_aws_mqtt5_utf8_decoder_options); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0473d50e..56695abd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -81,6 +81,9 @@ add_test_case(mqtt5_topic_skip_rules_prefix) add_test_case(mqtt5_topic_get_segment_count) add_test_case(mqtt5_shared_subscription_validation) +# utf8 utility +add_test_case(mqtt5_utf8_encoded_string_test) + # topic aliasing add_test_case(mqtt5_inbound_topic_alias_register_failure) add_test_case(mqtt5_inbound_topic_alias_resolve_success) @@ -129,11 +132,16 @@ add_test_case(mqtt5_publish_storage_new_set_all) add_test_case(mqtt5_operation_disconnect_validation_failure_server_reference) add_test_case(mqtt5_operation_disconnect_validation_failure_bad_reason_code) add_test_case(mqtt5_operation_disconnect_validation_failure_reason_string_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_reason_string_invalid_utf8) add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_value_invalid_utf8) add_test_case(mqtt5_operation_disconnect_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_connect_validation_failure_client_id_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_client_id_invalid_utf8) add_test_case(mqtt5_operation_connect_validation_failure_username_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_username_invalid_utf8) add_test_case(mqtt5_operation_connect_validation_failure_password_too_long) add_test_case(mqtt5_operation_connect_validation_failure_receive_maximum_zero) add_test_case(mqtt5_operation_connect_validation_failure_maximum_packet_size_zero) @@ -144,7 +152,9 @@ add_test_case(mqtt5_operation_connect_validation_failure_auth_data_unsupported) add_test_case(mqtt5_operation_connect_validation_failure_request_problem_information_invalid) add_test_case(mqtt5_operation_connect_validation_failure_request_response_information_invalid) add_test_case(mqtt5_operation_connect_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_connect_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_connect_validation_failure_user_properties_value_invalid_utf8) add_test_case(mqtt5_operation_connect_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_subscribe_validation_failure_no_subscriptions) add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscriptions) @@ -152,31 +162,42 @@ add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscription add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_subscription_identifier) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter_for_iot_core) +add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_utf8_topic_filter) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_qos) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_retain_type) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_no_local) add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_value_invalid_utf8) add_test_case(mqtt5_operation_subscribe_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_unsubscribe_validation_failure_no_topic_filters) add_test_case(mqtt5_operation_unsubscribe_validation_failure_too_many_topic_filters) add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_utf8_topic_filter) add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter_for_iot_core) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_value_invalid_utf8) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_publish_validation_failure_invalid_topic) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_topic) add_test_case(mqtt5_operation_publish_validation_failure_topic_too_long_for_iot_core) add_test_case(mqtt5_operation_publish_validation_failure_topic_too_many_slashes_for_iot_core) add_test_case(mqtt5_operation_publish_validation_failure_no_topic) add_test_case(mqtt5_operation_publish_validation_failure_invalid_payload_format) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_payload) add_test_case(mqtt5_operation_publish_validation_failure_response_topic_too_long) add_test_case(mqtt5_operation_publish_validation_failure_invalid_response_topic) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_response_topic) add_test_case(mqtt5_operation_publish_validation_failure_correlation_data_too_long) add_test_case(mqtt5_operation_publish_validation_failure_subscription_identifier_exists) add_test_case(mqtt5_operation_publish_validation_failure_topic_alias_zero) add_test_case(mqtt5_operation_publish_validation_failure_user_properties_name_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_publish_validation_failure_user_properties_value_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_user_properties_value_invalid_utf8) add_test_case(mqtt5_operation_publish_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_publish_validation_failure_qos0_duplicate_true) add_test_case(mqtt5_operation_publish_validation_failure_qos0_with_packet_id) diff --git a/tests/v5/mqtt5_operation_and_storage_tests.c b/tests/v5/mqtt5_operation_and_storage_tests.c index 2a8f5cd6..a593c779 100644 --- a/tests/v5/mqtt5_operation_and_storage_tests.c +++ b/tests/v5/mqtt5_operation_and_storage_tests.c @@ -161,24 +161,24 @@ static const struct aws_mqtt5_user_property s_user_properties[] = { .name = { .ptr = (uint8_t *)s_user_prop1_name, - .len = AWS_ARRAY_SIZE(s_user_prop1_name), + .len = AWS_ARRAY_SIZE(s_user_prop1_name) - 1, }, .value = { .ptr = (uint8_t *)s_user_prop1_value, - .len = AWS_ARRAY_SIZE(s_user_prop1_value), + .len = AWS_ARRAY_SIZE(s_user_prop1_value) - 1, }, }, { .name = { .ptr = (uint8_t *)s_user_prop2_name, - .len = AWS_ARRAY_SIZE(s_user_prop2_name), + .len = AWS_ARRAY_SIZE(s_user_prop2_name) - 1, }, .value = { .ptr = (uint8_t *)s_user_prop2_value, - .len = AWS_ARRAY_SIZE(s_user_prop2_value), + .len = AWS_ARRAY_SIZE(s_user_prop2_value) - 1, }, }, }; diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c index dd4db628..1de9ab8a 100644 --- a/tests/v5/mqtt5_operation_validation_failure_tests.c +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -34,6 +34,9 @@ static struct aws_byte_cursor s_too_long_for_uint16_cursor = { .len = AWS_ARRAY_SIZE(s_too_long_for_uint16), }; +// Mqtt5 Specific invalid codepoint in prohibited range U+007F - U+009F (value = U+008F)", +static uint8_t s_invalid_utf8[] = "\xC2\x8F"; +static struct aws_byte_cursor s_invalid_utf8_string = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(s_invalid_utf8); static uint8_t s_user_prop_name[] = "name"; static uint8_t s_user_prop_value[] = "value"; @@ -67,6 +70,28 @@ static const struct aws_mqtt5_user_property s_bad_user_properties_value[] = { }, }; +static const struct aws_mqtt5_user_property s_user_properties_with_invalid_name[] = { + {.name = + { + .ptr = (uint8_t *)s_invalid_utf8, + .len = AWS_ARRAY_SIZE(s_invalid_utf8) - 1, + }, + .value = { + .ptr = (uint8_t *)s_user_prop_value, + .len = AWS_ARRAY_SIZE(s_user_prop_value) - 1, + }}}; + +static const struct aws_mqtt5_user_property s_user_properties_with_invalid_value[] = { + {.name = + { + .ptr = s_user_prop_name, + .len = AWS_ARRAY_SIZE(s_user_prop_name) - 1, + }, + .value = { + .ptr = (uint8_t *)s_invalid_utf8, + .len = AWS_ARRAY_SIZE(s_invalid_utf8) - 1, + }}}; + static struct aws_mqtt5_user_property s_bad_user_properties_too_many[AWS_MQTT5_CLIENT_MAXIMUM_USER_PROPERTIES + 1]; /* @@ -169,6 +194,16 @@ AWS_VALIDATION_FAILURE_TEST4( s_good_disconnect_view, s_make_reason_string_too_long_disconnect_view) +static void s_make_reason_string_invalid_utf8_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->reason_string = &s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + reason_string_invalid_utf8, + s_good_disconnect_view, + s_make_reason_string_invalid_utf8_disconnect_view) + static void s_make_user_properties_name_too_long_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); view->user_properties = s_bad_user_properties_name; @@ -180,6 +215,17 @@ AWS_VALIDATION_FAILURE_TEST4( s_good_disconnect_view, s_make_user_properties_name_too_long_disconnect_view) +static void s_make_user_properties_name_invalid_utf8_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_name); + view->user_properties = s_user_properties_with_invalid_name; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + user_properties_name_invalid_utf8, + s_good_disconnect_view, + s_make_user_properties_name_invalid_utf8_disconnect_view) + static void s_make_user_properties_value_too_long_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); view->user_properties = s_bad_user_properties_value; @@ -191,6 +237,17 @@ AWS_VALIDATION_FAILURE_TEST4( s_good_disconnect_view, s_make_user_properties_value_too_long_disconnect_view) +static void s_make_user_properties_value_invalid_utf8_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_value); + view->user_properties = s_user_properties_with_invalid_value; +} + +AWS_VALIDATION_FAILURE_TEST4( + disconnect, + user_properties_value_invalid_utf8, + s_good_disconnect_view, + s_make_user_properties_value_invalid_utf8_disconnect_view) + static void s_make_user_properties_too_many_disconnect_view(struct aws_mqtt5_packet_disconnect_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); view->user_properties = s_bad_user_properties_too_many; @@ -211,12 +268,32 @@ static void s_make_client_id_too_long_connect_view(struct aws_mqtt5_packet_conne AWS_VALIDATION_FAILURE_TEST2(connect, client_id_too_long, s_good_connect_view, s_make_client_id_too_long_connect_view) +static void s_make_client_id_invalid_utf8_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->client_id = s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + client_id_invalid_utf8, + s_good_connect_view, + s_make_client_id_invalid_utf8_connect_view) + static void s_make_username_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { view->username = &s_too_long_for_uint16_cursor; } AWS_VALIDATION_FAILURE_TEST2(connect, username_too_long, s_good_connect_view, s_make_username_too_long_connect_view) +static void s_make_username_invalid_utf8_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->username = &s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + username_invalid_utf8, + s_good_connect_view, + s_make_username_invalid_utf8_connect_view) + static void s_make_password_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { view->password = &s_too_long_for_uint16_cursor; } @@ -297,6 +374,17 @@ AWS_VALIDATION_FAILURE_TEST2( s_good_connect_view, s_make_user_properties_name_too_long_connect_view) +static void s_make_user_properties_name_invalid_utf8_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_name); + view->user_properties = s_user_properties_with_invalid_name; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + user_properties_name_invalid_utf8, + s_good_connect_view, + s_make_user_properties_name_invalid_utf8_connect_view) + static void s_make_user_properties_value_too_long_connect_view(struct aws_mqtt5_packet_connect_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); view->user_properties = s_bad_user_properties_value; @@ -308,6 +396,17 @@ AWS_VALIDATION_FAILURE_TEST2( s_good_connect_view, s_make_user_properties_value_too_long_connect_view) +static void s_make_user_properties_value_invalid_utf8_connect_view(struct aws_mqtt5_packet_connect_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_value); + view->user_properties = s_user_properties_with_invalid_value; +} + +AWS_VALIDATION_FAILURE_TEST2( + connect, + user_properties_value_invalid_utf8, + s_good_connect_view, + s_make_user_properties_value_invalid_utf8_connect_view) + static void s_make_user_properties_too_many_connect_view(struct aws_mqtt5_packet_connect_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); view->user_properties = s_bad_user_properties_too_many; @@ -441,6 +540,20 @@ static struct aws_mqtt5_subscription_view s_invalid_topic_filter_subscription[] }, }; +static struct aws_mqtt5_subscription_view s_invalid_utf8_topic_filter_subscription[] = { + { + .topic_filter = + { + .ptr = (uint8_t *)s_invalid_utf8, + .len = AWS_ARRAY_SIZE(s_invalid_utf8) - 1, + }, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .no_local = false, + .retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE, + .retain_as_published = false, + }, +}; + static void s_make_invalid_topic_filter_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { view->subscriptions = s_invalid_topic_filter_subscription; view->subscription_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_subscription); @@ -452,6 +565,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_subscribe_view, s_make_invalid_topic_filter_subscribe_view) +static void s_make_invalid_utf8_topic_filter_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->subscriptions = s_invalid_utf8_topic_filter_subscription; + view->subscription_count = AWS_ARRAY_SIZE(s_invalid_utf8_topic_filter_subscription); +}; + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + invalid_utf8_topic_filter, + s_good_subscribe_view, + s_make_invalid_utf8_topic_filter_subscribe_view) + static struct aws_mqtt5_subscription_view s_invalid_qos_subscription[] = { { .topic_filter = @@ -578,6 +702,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_subscribe_view, s_make_user_properties_name_too_long_subscribe_view) +static void s_make_user_properties_name_invalid_utf8_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_name); + view->user_properties = s_user_properties_with_invalid_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + user_properties_name_invalid_utf8, + s_good_subscribe_view, + s_make_user_properties_name_invalid_utf8_subscribe_view) + static void s_make_user_properties_value_too_long_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); view->user_properties = s_bad_user_properties_value; @@ -589,6 +724,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_subscribe_view, s_make_user_properties_value_too_long_subscribe_view) +static void s_make_user_properties_value_invalid_utf8_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_value); + view->user_properties = s_user_properties_with_invalid_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + subscribe, + user_properties_value_invalid_utf8, + s_good_subscribe_view, + s_make_user_properties_value_invalid_utf8_subscribe_view) + static void s_make_user_properties_too_many_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); view->user_properties = s_bad_user_properties_too_many; @@ -650,6 +796,11 @@ static struct aws_byte_cursor s_invalid_topic_filter[] = { }, }; +static struct aws_byte_cursor s_invalid_utf8_topic_filter[] = {{ + .ptr = (uint8_t *)s_invalid_utf8, + .len = AWS_ARRAY_SIZE(s_invalid_utf8) - 1, +}}; + static void s_make_invalid_topic_filter_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { view->topic_filters = s_invalid_topic_filter; view->topic_filter_count = AWS_ARRAY_SIZE(s_invalid_topic_filter); @@ -661,6 +812,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_unsubscribe_view, s_make_invalid_topic_filter_unsubscribe_view) +static void s_make_invalid_utf8_topic_filter_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->topic_filters = s_invalid_utf8_topic_filter; + view->topic_filter_count = AWS_ARRAY_SIZE(s_invalid_utf8_topic_filter); +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + invalid_utf8_topic_filter, + s_good_unsubscribe_view, + s_make_invalid_utf8_topic_filter_unsubscribe_view) + static struct aws_byte_cursor s_invalid_topic_filter_for_iot_core[] = { { .ptr = s_too_many_slashes_topic_filter, @@ -690,6 +852,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_unsubscribe_view, s_make_user_properties_name_too_long_unsubscribe_view) +static void s_make_user_properties_name_invalid_utf8_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_name); + view->user_properties = s_user_properties_with_invalid_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + user_properties_name_invalid_utf8, + s_good_unsubscribe_view, + s_make_user_properties_name_invalid_utf8_unsubscribe_view) + static void s_make_user_properties_value_too_long_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); view->user_properties = s_bad_user_properties_value; @@ -701,6 +874,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_unsubscribe_view, s_make_user_properties_value_too_long_unsubscribe_view) +static void s_make_user_properties_value_invalid_utf8_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_value); + view->user_properties = s_user_properties_with_invalid_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + unsubscribe, + user_properties_value_invalid_utf8, + s_good_unsubscribe_view, + s_make_user_properties_value_invalid_utf8_unsubscribe_view) + static void s_make_user_properties_too_many_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); view->user_properties = s_bad_user_properties_too_many; @@ -728,6 +912,12 @@ static void s_make_invalid_topic_publish_view(struct aws_mqtt5_packet_publish_vi AWS_VALIDATION_FAILURE_TEST3(publish, invalid_topic, s_good_publish_view, s_make_invalid_topic_publish_view) +static void s_make_invalid_utf8_topic_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->topic = s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST3(publish, invalid_utf8_topic, s_good_publish_view, s_make_invalid_utf8_topic_publish_view) + static uint8_t s_topic_too_long_for_iot_core[258]; static void s_make_topic_too_long_for_iot_core_publish_view(struct aws_mqtt5_packet_publish_view *view) { @@ -766,6 +956,7 @@ static void s_make_no_topic_publish_view(struct aws_mqtt5_packet_publish_view *v AWS_VALIDATION_FAILURE_TEST3(publish, no_topic, s_good_publish_view, s_make_no_topic_publish_view) static enum aws_mqtt5_payload_format_indicator s_invalid_payload_format = 3; +static enum aws_mqtt5_payload_format_indicator s_valid_payload_format = AWS_MQTT5_PFI_UTF8; static void s_make_invalid_payload_format_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->payload_format = &s_invalid_payload_format; @@ -777,6 +968,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_publish_view, s_make_invalid_payload_format_publish_view) +static void s_make_invalid_utf8_payload_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->payload_format = &s_valid_payload_format; + view->payload = s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + invalid_utf8_payload, + s_good_publish_view, + s_make_invalid_utf8_payload_publish_view) + static void s_make_response_topic_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->response_topic = &s_too_long_for_uint16_cursor; } @@ -802,6 +1004,16 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_publish_view, s_make_response_topic_invalid_publish_view) +static void s_make_response_topic_invalid_utf8_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->response_topic = &s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + invalid_utf8_response_topic, + s_good_publish_view, + s_make_response_topic_invalid_utf8_publish_view) + static void s_make_correlation_data_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->correlation_data = &s_too_long_for_uint16_cursor; } @@ -812,6 +1024,16 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_publish_view, s_make_correlation_data_too_long_publish_view) +static void s_make_content_type_invalid_utf8_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->content_type = &s_invalid_utf8_string; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + invalid_utf8_content_type, + s_good_publish_view, + s_make_content_type_invalid_utf8_publish_view) + static const uint32_t s_subscription_identifiers[] = {1, 2}; static void s_make_subscription_identifier_exists_publish_view(struct aws_mqtt5_packet_publish_view *view) { @@ -855,6 +1077,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_publish_view, s_make_user_properties_name_too_long_publish_view) +static void s_make_user_properties_name_invalid_utf8_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_name); + view->user_properties = s_user_properties_with_invalid_name; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + user_properties_name_invalid_utf8, + s_good_publish_view, + s_make_user_properties_name_invalid_utf8_publish_view) + static void s_make_user_properties_value_too_long_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_value); view->user_properties = s_bad_user_properties_value; @@ -866,6 +1099,17 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_publish_view, s_make_user_properties_value_too_long_publish_view) +static void s_make_user_properties_value_invalid_utf8_publish_view(struct aws_mqtt5_packet_publish_view *view) { + view->user_property_count = AWS_ARRAY_SIZE(s_user_properties_with_invalid_value); + view->user_properties = s_user_properties_with_invalid_value; +} + +AWS_VALIDATION_FAILURE_TEST3( + publish, + user_properties_value_invalid_utf8, + s_good_publish_view, + s_make_user_properties_value_invalid_utf8_publish_view) + static void s_make_user_properties_too_many_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_too_many); view->user_properties = s_bad_user_properties_too_many; diff --git a/tests/v5/mqtt5_utils_tests.c b/tests/v5/mqtt5_utils_tests.c index 16a67517..c761b45c 100644 --- a/tests/v5/mqtt5_utils_tests.c +++ b/tests/v5/mqtt5_utils_tests.c @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ +#include #include #include @@ -113,3 +114,152 @@ static int s_mqtt5_shared_subscription_validation_fn(struct aws_allocator *alloc } AWS_TEST_CASE(mqtt5_shared_subscription_validation, s_mqtt5_shared_subscription_validation_fn) + +struct utf8_example { + const char *name; + struct aws_byte_cursor text; +}; + +static struct utf8_example s_valid_mqtt5_utf8_examples[] = { + { + .name = "1 letter", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("a"), + }, + { + .name = "Several ascii letters", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("ascii word"), + }, + { + .name = "empty string", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(""), + }, + { + .name = "2 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\xA3"), + }, + { + .name = "3 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE2\x82\xAC"), + }, + { + .name = "4 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x8D\x88"), + }, + { + .name = "A variety of different length codepoints", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "\xF0\x90\x8D\x88\xE2\x82\xAC\xC2\xA3\x24\xC2\xA3\xE2\x82\xAC\xF0\x90\x8D\x88"), + }, + { + .name = "UTF8 BOM", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF"), + }, + { + .name = "UTF8 BOM plus extra", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF\x24\xC2\xA3"), + }, + { + .name = "First possible 3 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE0\xA0\x80"), + }, + { + .name = "First possible 4 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x80\x80"), + }, + { + .name = "Last possible 2 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xDF\xBF"), + }, + { + .name = "Last valid codepoint before prohibited range U+D800 - U+DFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xED\x9F\xBF"), + }, + { + .name = "Next valid codepoint after prohibited range U+D800 - U+DFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEE\x80\x80"), + }, + { + .name = "Boundary condition", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBD"), + }, + { + .name = "Boundary condition", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF4\x90\x80\x80"), + }, +}; + +static struct utf8_example s_illegal_mqtt5_utf8_examples[] = { + { + .name = "non character U+0000", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x00"), + }, + { + .name = "Codepoint in prohibited range U+0001 - U+001F (in the middle)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x04"), + }, + { + .name = "Codepoint in prohibited range U+0001 - U+001F (boundary)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x1F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (min: U+7F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x7F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (in the middle u+8F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x8F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (boundary U+9F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x9F"), + }, + { + .name = "non character end with U+FFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBF"), + }, + { + .name = "non character end with U+FFFE", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF7\xBF\xBF\xBE"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (lower bound)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\x90"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (in middle)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xA1"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (upper bound)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xAF"), + }}; + +static int s_mqtt5_utf8_encoded_string_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + /* Check the valid test cases */ + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt5_utf8_examples); ++i) { + struct utf8_example example = s_valid_mqtt5_utf8_examples[i]; + printf("valid example [%zu]: %s\n", i, example.name); + ASSERT_SUCCESS(aws_mqtt5_validate_utf8_text(example.text)); + } + + /* Glue all the valid test cases together, they ought to pass */ + struct aws_byte_buf all_good_text; + aws_byte_buf_init(&all_good_text, allocator, 1024); + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt5_utf8_examples); ++i) { + aws_byte_buf_append_dynamic(&all_good_text, &s_valid_mqtt5_utf8_examples[i].text); + } + ASSERT_SUCCESS(aws_mqtt5_validate_utf8_text(aws_byte_cursor_from_buf(&all_good_text))); + aws_byte_buf_clean_up(&all_good_text); + + /* Check the illegal test cases */ + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_illegal_mqtt5_utf8_examples); ++i) { + struct utf8_example example = s_illegal_mqtt5_utf8_examples[i]; + printf("illegal example [%zu]: %s\n", i, example.name); + ASSERT_FAILS(aws_mqtt5_validate_utf8_text(example.text)); + } + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_utf8_encoded_string_test, s_mqtt5_utf8_encoded_string_test) From ebc66e8cb979c281e94cfa095a7fbcf205bc2c04 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 9 Feb 2023 11:32:47 -0800 Subject: [PATCH 47/98] Update timeouts to be more container/emu-friendly (#264) --- tests/v3/connection_state_test.c | 49 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 58a141ed..34a5a925 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -25,6 +25,9 @@ static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; +#define DEFAULT_TEST_PING_TIMEOUT_MS 1000 +#define DEFAULT_TEST_KEEP_ALIVE_S 2 + struct received_publish_packet { struct aws_byte_buf topic; struct aws_byte_buf payload; @@ -808,8 +811,8 @@ static int s_test_mqtt_connection_timeout_fn(struct aws_allocator *allocator, vo .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .keep_alive_time_secs = 1, - .ping_timeout_ms = 100, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, }; mqtt_mock_server_set_max_ping_resp(state_test_data->mock_server, 0); @@ -934,8 +937,8 @@ static int s_test_mqtt_connection_connack_timeout_fn(struct aws_allocator *alloc .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .keep_alive_time_secs = 1, - .ping_timeout_ms = 100, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, }; mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); @@ -1747,8 +1750,8 @@ static int s_test_mqtt_connection_offline_publish_fn(struct aws_allocator *alloc .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); @@ -1865,8 +1868,8 @@ static int s_test_mqtt_connection_disconnect_while_reconnecting(struct aws_alloc .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); @@ -1941,8 +1944,8 @@ static int s_test_mqtt_connection_closes_while_making_requests_fn(struct aws_all .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -2023,8 +2026,8 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -2088,7 +2091,7 @@ static int s_test_mqtt_connection_not_retry_publish_QoS_0_fn(struct aws_allocato .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2155,7 +2158,7 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2255,8 +2258,8 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -2377,8 +2380,8 @@ static int s_test_mqtt_clean_session_not_retry_fn(struct aws_allocator *allocato .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, - .keep_alive_time_secs = 1, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); @@ -2446,7 +2449,7 @@ static int s_test_mqtt_clean_session_discard_previous_fn(struct aws_allocator *a .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2516,7 +2519,7 @@ static int s_test_mqtt_clean_session_keep_next_session_fn(struct aws_allocator * .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2588,7 +2591,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_fn(struct aws_allocator * .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2648,7 +2651,7 @@ static int s_test_mqtt_connection_unsub_timeout_fn(struct aws_allocator *allocat .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2704,7 +2707,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, .on_connection_complete = s_on_connection_complete_fn, - .ping_timeout_ms = 10, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; From 0e9dc9b3144ac2f660d1b3ccfdfe860daf66bcff Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 15 Feb 2023 08:55:15 -0800 Subject: [PATCH 48/98] Adds a basic check for reconnect delay to the 311 reconnect test (#262) --- tests/v3/connection_state_test.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 34a5a925..6e0ec474 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -742,8 +742,12 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) +#define MIN_RECONNECT_DELAY_SECONDS 5 +#define MAX_RECONNECT_DELAY_SECONDS 120 + /* * Makes a CONNECT, then the server hangs up, tests that the client reconnects on its own, then sends a DISCONNECT. + * Also checks that the minimum reconnect time delay is honored. */ static int s_test_mqtt_connection_interrupted_fn(struct aws_allocator *allocator, void *ctx) { (void)allocator; @@ -758,13 +762,28 @@ static int s_test_mqtt_connection_interrupted_fn(struct aws_allocator *allocator .on_connection_complete = s_on_connection_complete_fn, }; + aws_mqtt_client_connection_set_reconnect_timeout( + state_test_data->mqtt_connection, MIN_RECONNECT_DELAY_SECONDS, MAX_RECONNECT_DELAY_SECONDS); + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); s_wait_for_connection_to_complete(state_test_data); /* shut it down and make sure the client automatically reconnects.*/ + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + uint64_t start_shutdown = now; + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); s_wait_for_reconnect_to_complete(state_test_data); + aws_high_res_clock_get_ticks(&now); + uint64_t reconnect_complete = now; + + uint64_t elapsed_time = reconnect_complete - start_shutdown; + ASSERT_TRUE( + aws_timestamp_convert(elapsed_time, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, NULL) >= + MIN_RECONNECT_DELAY_SECONDS); + ASSERT_SUCCESS( aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); s_wait_for_disconnect_to_complete(state_test_data); From 137c31c4df06d4e82114d2c8f82e1afb134c3e3d Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Mon, 6 Mar 2023 14:07:29 -0800 Subject: [PATCH 49/98] add a validation for stop operation (#265) --- source/v5/mqtt5_client.c | 1 + 1 file changed, 1 insertion(+) diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 5374e41d..27af7603 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -2257,6 +2257,7 @@ int aws_mqtt5_client_stop( struct aws_mqtt5_client *client, const struct aws_mqtt5_packet_disconnect_view *options, const struct aws_mqtt5_disconnect_completion_options *completion_options) { + AWS_FATAL_ASSERT(client != NULL); struct aws_mqtt5_operation_disconnect *disconnect_op = NULL; if (options != NULL) { struct aws_mqtt5_disconnect_completion_options internal_completion_options = { From f3efee5b850c1991802db407d251a983ae0d4e60 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Mon, 13 Mar 2023 12:24:32 -0400 Subject: [PATCH 50/98] General improvements to MQTT5 canary (#252) --- bin/mqtt5canary/main.c | 100 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/bin/mqtt5canary/main.c b/bin/mqtt5canary/main.c index 2169873f..cf7c50bc 100644 --- a/bin/mqtt5canary/main.c +++ b/bin/mqtt5canary/main.c @@ -97,11 +97,12 @@ static void s_usage(int exit_code) { fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); fprintf(stderr, " -w, --websockets: use mqtt-over-websockets rather than direct mqtt\n"); + fprintf(stderr, " -p, --port: Port to use when making MQTT connections\n"); fprintf(stderr, " -t, --threads: number of eventloop group threads to use\n"); fprintf(stderr, " -C, --clients: number of mqtt5 clients to use\n"); fprintf(stderr, " -T, --tps: operations to run per second\n"); - fprintf(stderr, " -s, --seconds: seconds to run canary test\n"); + fprintf(stderr, " -s, --seconds: seconds to run canary test (set as 0 to run forever)\n"); fprintf(stderr, " -h, --help\n"); fprintf(stderr, " Display this message and quit.\n"); exit(exit_code); @@ -115,6 +116,7 @@ static struct aws_cli_option s_long_options[] = { {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, {"websockets", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'w'}, + {"port", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'p'}, {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, {"threads", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 't'}, @@ -134,7 +136,7 @@ static void s_parse_options( while (true) { int option_index = 0; - int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wht:C:T:s:", s_long_options, &option_index); + int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wht:p:C:T:s:", s_long_options, &option_index); if (c == -1) { break; } @@ -178,6 +180,9 @@ static void s_parse_options( case 'w': ctx->use_websockets = true; break; + case 'p': + ctx->port = (uint16_t)atoi(aws_cli_optarg); + break; case 't': tester_options->elg_max_threads = (uint16_t)atoi(aws_cli_optarg); break; @@ -381,6 +386,66 @@ static void s_aws_mqtt5_transform_websocket_handshake_fn( (*complete_fn)(request, AWS_ERROR_SUCCESS, complete_ctx); } +/********************************************************** + * OPERATION CALLBACKS + **********************************************************/ + +static void s_aws_mqtt5_canary_operation_subscribe_completion( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + (void)suback; + + if (error_code != AWS_MQTT5_UARC_SUCCESS) { + struct aws_mqtt5_canary_test_client *test_client = (struct aws_mqtt5_canary_test_client *)(complete_ctx); + if (test_client != NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Subscribe completed with error code: %i", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + error_code); + } + } +} + +static void s_aws_mqtt5_canary_operation_unsubscribe_completion( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + (void)unsuback; + + if (error_code != AWS_MQTT5_UARC_SUCCESS) { + struct aws_mqtt5_canary_test_client *test_client = (struct aws_mqtt5_canary_test_client *)(complete_ctx); + if (test_client != NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Unsubscribe completed with error code: %i", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + error_code); + } + } +} + +static void s_aws_mqtt5_canary_operation_publish_completion( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + (void)packet; + (void)packet_type; + + if (error_code != AWS_MQTT5_PARC_SUCCESS) { + struct aws_mqtt5_canary_test_client *test_client = (struct aws_mqtt5_canary_test_client *)(complete_ctx); + if (test_client != NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, + "ID:" PRInSTR " Publish completed with error code: %i", + AWS_BYTE_CURSOR_PRI(test_client->client_id), + error_code); + } + } +} + /********************************************************** * OPERATION FUNCTIONS **********************************************************/ @@ -449,6 +514,11 @@ static int s_aws_mqtt5_canary_operation_subscribe(struct aws_mqtt5_canary_test_c .subscription_count = AWS_ARRAY_SIZE(subscriptions), }; + struct aws_mqtt5_subscribe_completion_options subscribe_view_options = { + .completion_callback = &s_aws_mqtt5_canary_operation_subscribe_completion, + .completion_user_data = test_client, + }; + test_client->subscription_count++; AWS_LOGF_INFO( @@ -456,7 +526,7 @@ static int s_aws_mqtt5_canary_operation_subscribe(struct aws_mqtt5_canary_test_c "ID:" PRInSTR " Subscribe to topic: " PRInSTR, AWS_BYTE_CURSOR_PRI(test_client->client_id), AWS_BYTE_CURSOR_PRI(subscriptions->topic_filter)); - return aws_mqtt5_client_subscribe(test_client->client, &subscribe_view, NULL); + return aws_mqtt5_client_subscribe(test_client->client, &subscribe_view, &subscribe_view_options); } static int s_aws_mqtt5_canary_operation_unsubscribe_bad(struct aws_mqtt5_canary_test_client *test_client) { @@ -513,12 +583,16 @@ static int s_aws_mqtt5_canary_operation_unsubscribe(struct aws_mqtt5_canary_test .topic_filter_count = AWS_ARRAY_SIZE(unsubscribes), }; + struct aws_mqtt5_unsubscribe_completion_options unsubscribe_view_options = { + .completion_callback = &s_aws_mqtt5_canary_operation_unsubscribe_completion, + .completion_user_data = test_client}; + AWS_LOGF_INFO( AWS_LS_MQTT5_CANARY, "ID:" PRInSTR " Unsubscribe from topic: " PRInSTR, AWS_BYTE_CURSOR_PRI(test_client->client_id), AWS_BYTE_CURSOR_PRI(topic)); - return aws_mqtt5_client_unsubscribe(test_client->client, &unsubscribe_view, NULL); + return aws_mqtt5_client_unsubscribe(test_client->client, &unsubscribe_view, &unsubscribe_view_options); } static int s_aws_mqtt5_canary_operation_publish( @@ -571,7 +645,10 @@ static int s_aws_mqtt5_canary_operation_publish( .user_property_count = AWS_ARRAY_SIZE(user_properties), }; - return aws_mqtt5_client_publish(test_client->client, &packet_publish_view, NULL); + struct aws_mqtt5_publish_completion_options packet_publish_view_options = { + .completion_callback = &s_aws_mqtt5_canary_operation_publish_completion, .completion_user_data = test_client}; + + return aws_mqtt5_client_publish(test_client->client, &packet_publish_view, &packet_publish_view_options); } static int s_aws_mqtt5_canary_operation_publish_qos0(struct aws_mqtt5_canary_test_client *test_client) { @@ -706,7 +783,9 @@ int main(int argc, char **argv) { app_ctx.signal = (struct aws_condition_variable)AWS_CONDITION_VARIABLE_INIT; app_ctx.connect_timeout = 3000; aws_mutex_init(&app_ctx.lock); - app_ctx.port = 1883; + if (app_ctx.port == 0) { + app_ctx.port = 1883; + } struct aws_mqtt5_canary_tester_options tester_options; AWS_ZERO_STRUCT(tester_options); @@ -863,6 +942,7 @@ int main(int argc, char **argv) { .websocket_handshake_transform = websocket_handshake_transform, .websocket_handshake_transform_user_data = websocket_handshake_transform_user_data, .publish_received_handler = s_on_publish_received, + .ack_timeout_seconds = 300, /* 5 minute timeout */ }; struct aws_mqtt5_canary_test_client clients[AWS_MQTT5_CANARY_CLIENT_MAX]; @@ -901,7 +981,11 @@ int main(int argc, char **argv) { time_test_finish += aws_timestamp_convert(tester_options.test_run_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); - printf("Running test for %zu seconds\n", tester_options.test_run_seconds); + if (tester_options.test_run_seconds > 0) { + printf("Running test for %zu seconds\n", tester_options.test_run_seconds); + } else { + printf("Running test forever\n"); + } while (!done) { uint64_t now = 0; @@ -914,7 +998,7 @@ int main(int argc, char **argv) { (*operation_fn)(&clients[rand() % tester_options.client_count]); - if (now > time_test_finish) { + if (now > time_test_finish && tester_options.test_run_seconds > 0) { done = true; } From 7e384c9d989f7936c7d403115b7983fa2b0727a0 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 4 Apr 2023 10:43:34 -0700 Subject: [PATCH 51/98] Reconnection Backoff Fix (#267) * Solution proposal * Explanatory comments + logic fix * More comments * Move reset logic into shared state lock so that we can check for a user-initiated disconnection too * Reconnect fix unit tests (#268) * reconnection backoff time unit tests * add uc4 * fix reconnection resumed flag * remove unncessary flag reset * fix log position * add error margin for the connection time --------- Co-authored-by: xiazhvera --- include/aws/mqtt/private/client_impl.h | 13 +- source/client.c | 53 +++-- source/client_channel_handler.c | 20 +- tests/CMakeLists.txt | 5 + tests/v3/connection_state_test.c | 271 +++++++++++++++++++++++++ 5 files changed, 330 insertions(+), 32 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 970bdf01..6bdfe749 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -208,10 +208,15 @@ struct aws_mqtt_client_connection { struct aws_byte_buf payload; } will; struct { - uint64_t current_sec; /* seconds */ - uint64_t min_sec; /* seconds */ - uint64_t max_sec; /* seconds */ - uint64_t next_attempt_reset_timer_ns; /* nanoseconds */ + uint64_t current_sec; /* seconds */ + uint64_t min_sec; /* seconds */ + uint64_t max_sec; /* seconds */ + + /* + * Invariant: this is always zero except when the current MQTT channel has received a successful connack + * and is not yet shutdown. During that interval, it is the timestamp the connack was received. + */ + uint64_t channel_successful_connack_timestamp_ns; } reconnect_timeouts; /* User connection callbacks */ diff --git a/source/client.c b/source/client.c index bbcb2321..7aea13b7 100644 --- a/source/client.c +++ b/source/client.c @@ -208,6 +208,8 @@ void aws_mqtt_client_release(struct aws_mqtt_client *client) { } } +#define AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS 10 + /* At this point, the channel for the MQTT connection has completed its shutdown */ static void s_mqtt_client_shutdown( struct aws_client_bootstrap *bootstrap, @@ -222,12 +224,41 @@ static void s_mqtt_client_shutdown( AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Channel has been shutdown with error code %d", (void *)connection, error_code); + enum aws_mqtt_client_connection_state prev_state; struct aws_linked_list cancelling_requests; aws_linked_list_init(&cancelling_requests); bool disconnected_state = false; { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); + + /* + * On a channel that represents a valid connection (successful connack received), + * channel_successful_connack_timestamp_ns will be the time the connack was received. Otherwise it will be + * zero. + * + * Use that fact to determine whether or not we should reset the current reconnect backoff delay. + * + * We reset the reconnect backoff if either of: + * 1) the user called disconnect() + * 2) a successful connection had lasted longer than our minimum reset time (10s at the moment) + */ + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + uint64_t time_diff = now - connection->reconnect_timeouts.channel_successful_connack_timestamp_ns; + + bool was_user_disconnect = connection->synced_data.state == AWS_MQTT_CLIENT_STATE_DISCONNECTING; + bool was_sufficiently_long_connection = + (connection->reconnect_timeouts.channel_successful_connack_timestamp_ns != 0) && + (time_diff >= + aws_timestamp_convert( + AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + if (was_user_disconnect || was_sufficiently_long_connection) { + connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.min_sec; + } + connection->reconnect_timeouts.channel_successful_connack_timestamp_ns = 0; + /* Move all the ongoing requests to the pending requests list, because the response they are waiting for will * never arrives. Sad. But, we will retry. */ if (connection->clean_session) { @@ -623,12 +654,6 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ mqtt_connection_lock_synced_data(connection); - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, - "id=%p: Attempting reconnect, if it fails next attempt will be in %" PRIu64 " seconds", - (void *)connection, - connection->reconnect_timeouts.current_sec); - /* Check before multiplying to avoid potential overflow */ if (connection->reconnect_timeouts.current_sec > connection->reconnect_timeouts.max_sec / 2) { connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.max_sec; @@ -636,14 +661,11 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ connection->reconnect_timeouts.current_sec *= 2; } - /* Apply updated reconnect_timeout to next_attempt_reset_timer_ns to prevent premature reset to min - * of min reconnect on a successful connect after a prolonged period of failed connections */ - uint64_t now = 0; - aws_high_res_clock_get_ticks(&now); - connection->reconnect_timeouts.next_attempt_reset_timer_ns = - now + 10000000000 + - aws_timestamp_convert( - connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Attempting reconnect, if it fails next attempt will be in %" PRIu64 " seconds", + (void *)connection, + connection->reconnect_timeouts.current_sec); mqtt_connection_unlock_synced_data(connection); @@ -807,6 +829,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt AWS_ZERO_STRUCT(connection->synced_data); connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; connection->reconnect_timeouts.min_sec = 1; + connection->reconnect_timeouts.current_sec = 1; connection->reconnect_timeouts.max_sec = 128; aws_linked_list_init(&connection->synced_data.pending_requests_list); aws_linked_list_init(&connection->thread_data.ongoing_requests_list); @@ -1060,6 +1083,7 @@ int aws_mqtt_client_connection_set_reconnect_timeout( max_timeout); connection->reconnect_timeouts.min_sec = min_timeout; connection->reconnect_timeouts.max_sec = max_timeout; + connection->reconnect_timeouts.current_sec = min_timeout; return AWS_OP_SUCCESS; } @@ -1683,7 +1707,6 @@ int aws_mqtt_client_connection_disconnect( (void *)connection); connection->on_disconnect = on_disconnect; connection->on_disconnect_ud = userdata; - connection->reconnect_timeouts.next_attempt_reset_timer_ns = 0; mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 2a1222e6..f9c01cbd 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -110,20 +110,14 @@ static int s_packet_handler_connack( uint64_t now = 0; aws_high_res_clock_get_ticks(&now); - /* - * Only reset the duration of the reconnect timer to min if this connect is happening past - * the previously set next_attempt_reset_timer value. The next reset value will be 10 seconds after the next - * connection attempt - */ - if (connection->reconnect_timeouts.next_attempt_reset_timer_ns < now) { - connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.min_sec; - } - connection->reconnect_timeouts.next_attempt_reset_timer_ns = - now + 10000000000 + - aws_timestamp_convert( - connection->reconnect_timeouts.current_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); - if (connack.connect_return_code == AWS_MQTT_CONNECT_ACCEPTED) { + + /* + * This was a successful MQTT connection establishment, record the time so that channel shutdown + * can make a good decision about reconnect backoff reset. + */ + connection->reconnect_timeouts.channel_successful_connack_timestamp_ns = now; + /* If successfully connected, schedule all pending tasks */ AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: connection was accepted processing offline requests.", (void *)connection); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 56695abd..8becd4d4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,6 +64,11 @@ add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) add_test_case(mqtt_connection_close_callback_simple) add_test_case(mqtt_connection_close_callback_interrupted) add_test_case(mqtt_connection_close_callback_multi) +add_test_case(mqtt_connection_reconnection_backoff_stable) +add_test_case(mqtt_connection_reconnection_backoff_unstable) +add_test_case(mqtt_connection_reconnection_backoff_reset) +add_test_case(mqtt_connection_reconnection_backoff_reset_after_disconnection) + # Operation statistics tests add_test_case(mqtt_operation_statistics_simple_publish) add_test_case(mqtt_operation_statistics_offline_publish) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 6e0ec474..86d680c3 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -16,6 +16,8 @@ #include +#include + #ifdef _WIN32 # define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" #else @@ -24,6 +26,10 @@ static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; +// The value is extract from aws-c-mqtt/source/client.c +static const int AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS = 10; +static const uint64_t RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS = 500000000; +#define DEFAULT_MIN_RECONNECT_DELAY_SECONDS 1 #define DEFAULT_TEST_PING_TIMEOUT_MS 1000 #define DEFAULT_TEST_KEEP_ALIVE_S 2 @@ -49,6 +55,7 @@ struct mqtt_connection_state_test { struct aws_mqtt_client *mqtt_client; struct aws_mqtt_client_connection *mqtt_connection; struct aws_socket_options socket_options; + bool session_present; bool connection_completed; bool client_disconnect_completed; @@ -204,6 +211,7 @@ static void s_wait_for_reconnect_to_complete(struct mqtt_connection_state_test * aws_mutex_lock(&state_test_data->lock); aws_condition_variable_wait_pred( &state_test_data->cvar, &state_test_data->lock, s_is_connection_resumed, state_test_data); + state_test_data->connection_resumed = false; aws_mutex_unlock(&state_test_data->lock); } @@ -2927,3 +2935,266 @@ AWS_TEST_CASE_FIXTURE( s_test_mqtt_connection_close_callback_multi_fn, s_clean_up_mqtt_server_fn, &test_data) + +static int s_test_mqtt_connection_reconnection_backoff_stable(struct aws_allocator *allocator, void *ctx) { + + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + uint64_t time_before = 0; + uint64_t time_after = 0; + for (int i = 0; i < 3; i++) { + /* sleep for AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS to make sure our connection is successful */ + aws_thread_current_sleep( + (uint64_t)ONE_SEC * AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS + + RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + + aws_high_res_clock_get_ticks(&time_before); + + /* shut it down and make sure the client automatically reconnects.*/ + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + + uint64_t reconnection_backoff_time = time_after - time_before; + uint64_t remainder = 0; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff_time, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, &remainder) == + DEFAULT_MIN_RECONNECT_DELAY_SECONDS); + ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + } + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_reconnection_backoff_stable, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_reconnection_backoff_stable, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_test_mqtt_connection_reconnection_backoff_unstable(struct aws_allocator *allocator, void *ctx) { + + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + uint64_t time_before = 0; + uint64_t time_after = 0; + uint64_t expected_reconnect_backoff = 1; + for (int i = 0; i < 3; i++) { + + aws_high_res_clock_get_ticks(&time_before); + + /* shut it down and make sure the client automatically reconnects.*/ + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + + uint64_t reconnection_backoff = time_after - time_before; + uint64_t remainder = 0; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, &remainder) == + expected_reconnect_backoff); + ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + + // Increase the exponential backoff + expected_reconnect_backoff = aws_min_u64(expected_reconnect_backoff * 2, 10); + } + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_reconnection_backoff_unstable, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_reconnection_backoff_unstable, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_test_mqtt_connection_reconnection_backoff_reset(struct aws_allocator *allocator, void *ctx) { + + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + uint64_t time_before = 0; + uint64_t time_after = 0; + uint64_t expected_reconnect_backoff = 1; + uint64_t reconnection_backoff = 0; + for (int i = 0; i < 3; i++) { + + aws_high_res_clock_get_ticks(&time_before); + + /* shut it down and make sure the client automatically reconnects.*/ + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + reconnection_backoff = time_after - time_before; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, NULL) >= + expected_reconnect_backoff); + + expected_reconnect_backoff = aws_min_u64(expected_reconnect_backoff * 2, 10); + } + + /* sleep for AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS to make sure our connection is successful */ + aws_thread_current_sleep( + (uint64_t)ONE_SEC * AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS + + RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + + aws_high_res_clock_get_ticks(&time_before); + + /* shut it down and make sure the client automatically reconnects.*/ + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + reconnection_backoff = time_after - time_before; + uint64_t remainder = 0; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, &remainder) == + DEFAULT_MIN_RECONNECT_DELAY_SECONDS); + ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_reconnection_backoff_reset, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_reconnection_backoff_reset, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection( + struct aws_allocator *allocator, + void *ctx) { + + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + uint64_t time_before = 0; + uint64_t time_after = 0; + uint64_t expected_reconnect_backoff = 1; + uint64_t reconnection_backoff = 0; + for (int i = 0; i < 3; i++) { + aws_high_res_clock_get_ticks(&time_before); + + /* shut it down and make sure the client automatically reconnects.*/ + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + reconnection_backoff = time_after - time_before; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, NULL) >= + expected_reconnect_backoff); + + expected_reconnect_backoff = aws_min_u64(expected_reconnect_backoff * 2, 10); + } + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + /* connect again */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_before); + + aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); + s_wait_for_reconnect_to_complete(state_test_data); + + aws_high_res_clock_get_ticks(&time_after); + reconnection_backoff = time_after - time_before; + uint64_t remainder = 0; + ASSERT_TRUE( + aws_timestamp_convert(reconnection_backoff, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, &remainder) == + DEFAULT_MIN_RECONNECT_DELAY_SECONDS); + ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); + + /* Disconnect */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_reconnection_backoff_reset_after_disconnection, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection, + s_clean_up_mqtt_server_fn, + &test_data) From 09c06c1475a6ea9974fd11cceab14390d0ecff11 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 11 Apr 2023 11:58:51 -0700 Subject: [PATCH 52/98] Resolution override (#269) * MQTT5 client by default no longer pings DNS after connection * Websocket and proxy support for DNS resolution overrides --- include/aws/mqtt/client.h | 8 ++++++++ include/aws/mqtt/private/client_impl.h | 1 + include/aws/mqtt/private/v5/mqtt5_options_storage.h | 3 +++ include/aws/mqtt/v5/mqtt5_client.h | 8 +++++++- source/client.c | 13 +++++++++++++ source/v5/mqtt5_client.c | 3 ++- source/v5/mqtt5_options_storage.c | 9 +++++++++ 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index f12a5c19..374d853b 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -386,6 +386,14 @@ int aws_mqtt_client_connection_set_http_proxy_options( struct aws_mqtt_client_connection *connection, struct aws_http_proxy_options *proxy_options); +/** + * Set host resolution ooptions for the connection. + */ +AWS_MQTT_API +int aws_mqtt_client_connection_set_host_resolution_options( + struct aws_mqtt_client_connection *connection, + struct aws_host_resolution_config *host_resolution_config); + /** * Sets the minimum and maximum reconnect timeouts. * diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 6bdfe749..6145c23a 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -192,6 +192,7 @@ struct aws_mqtt_client_connection { struct aws_socket_options socket_options; struct aws_http_proxy_config *http_proxy_config; struct aws_event_loop *loop; + struct aws_host_resolution_config host_resolution_config; /* Connect parameters */ struct aws_byte_buf client_id; diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index dcc07d39..91c2b087 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -176,6 +177,8 @@ struct aws_mqtt5_client_options_storage { aws_mqtt5_client_termination_completion_fn *client_termination_handler; void *client_termination_handler_user_data; + + struct aws_host_resolution_config host_resolution_override; }; AWS_EXTERN_C_BEGIN diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index e99338ce..8dbbc915 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -21,8 +21,8 @@ struct aws_allocator; struct aws_client_bootstrap; +struct aws_host_resolution_config; struct aws_http_message; -struct aws_input_stream; struct aws_mqtt5_client; struct aws_mqtt5_client_lifecycle_event; struct aws_tls_connection_options; @@ -655,6 +655,12 @@ struct aws_mqtt5_client_options { */ aws_mqtt5_client_termination_completion_fn *client_termination_handler; void *client_termination_handler_user_data; + + /** + * Options to override aspects of DNS resolution. If unspecified, use a default that matches the regular + * configuration but changes the refresh frequency to a value that prevents DNS pinging. + */ + struct aws_host_resolution_config *host_resolution_override; }; AWS_EXTERN_C_BEGIN diff --git a/source/client.c b/source/client.c index 7aea13b7..fefcdd10 100644 --- a/source/client.c +++ b/source/client.c @@ -888,6 +888,10 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt connection->loop = aws_event_loop_group_get_next_loop(client->bootstrap->event_loop_group); + connection->host_resolution_config = aws_host_resolver_init_default_resolution_config(); + connection->host_resolution_config.resolve_frequency_ns = + aws_timestamp_convert(connection->reconnect_timeouts.max_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + /* Initialize the handler */ connection->handler.alloc = connection->allocator; connection->handler.vtable = aws_mqtt_get_client_channel_vtable(); @@ -1195,6 +1199,15 @@ int aws_mqtt_client_connection_set_http_proxy_options( return connection->http_proxy_config != NULL ? AWS_OP_SUCCESS : AWS_OP_ERR; } +int aws_mqtt_client_connection_set_host_resolution_options( + struct aws_mqtt_client_connection *connection, + struct aws_host_resolution_config *host_resolution_config) { + + connection->host_resolution_config = *host_resolution_config; + + return AWS_OP_SUCCESS; +} + static void s_on_websocket_shutdown(struct aws_websocket *websocket, int error_code, void *user_data) { struct aws_mqtt_client_connection *connection = user_data; diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 27af7603..9138a6c4 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -939,7 +939,7 @@ void s_websocket_transform_complete_task_fn(struct aws_task *task, void *arg, en .on_connection_setup = s_on_websocket_setup, .on_connection_shutdown = s_on_websocket_shutdown, .requested_event_loop = client->loop, - }; + .host_resolution_config = &client->config->host_resolution_override}; if (client->config->http_proxy_config != NULL) { websocket_options.proxy_options = &client->config->http_proxy_options; @@ -1055,6 +1055,7 @@ static void s_change_current_state_to_connecting(struct aws_mqtt5_client *client channel_options.shutdown_callback = &s_mqtt5_client_shutdown; channel_options.user_data = client; channel_options.requested_event_loop = client->loop; + channel_options.host_resolution_override_config = &client->config->host_resolution_override; if (client->config->http_proxy_config == NULL) { result = (*client->vtable->client_bootstrap_new_socket_channel_fn)(&channel_options); diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index b8b566e9..e81da80a 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -3956,6 +3956,15 @@ struct aws_mqtt5_client_options_storage *aws_mqtt5_client_options_storage_new( s_apply_zero_valued_defaults_to_client_options_storage(options_storage); + /* must do this after zero-valued defaults are applied so that max reconnect is accurate */ + if (options->host_resolution_override) { + options_storage->host_resolution_override = *options->host_resolution_override; + } else { + options_storage->host_resolution_override = aws_host_resolver_init_default_resolution_config(); + options_storage->host_resolution_override.resolve_frequency_ns = aws_timestamp_convert( + options_storage->max_reconnect_delay_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + } + return options_storage; error: From 815fc9731b77c7a21308a1be9ac11378c053b515 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 11 Apr 2023 13:36:08 -0700 Subject: [PATCH 53/98] Missed actually setting the override on mqtt311 (#270) --- source/client.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/client.c b/source/client.c index fefcdd10..a3632cd6 100644 --- a/source/client.c +++ b/source/client.c @@ -1359,6 +1359,7 @@ static void s_websocket_handshake_transform_complete( .on_connection_setup = s_on_websocket_setup, .on_connection_shutdown = s_on_websocket_shutdown, .requested_event_loop = connection->loop, + .host_resolution_config = &connection->host_resolution_config, }; struct aws_http_proxy_options proxy_options; @@ -1648,6 +1649,7 @@ static int s_mqtt_client_connect( channel_options.shutdown_callback = &s_mqtt_client_shutdown; channel_options.user_data = connection; channel_options.requested_event_loop = connection->loop; + channel_options.host_resolution_override_config = &connection->host_resolution_config; if (connection->http_proxy_config == NULL) { result = aws_client_bootstrap_new_socket_channel(&channel_options); From 95c0afd7a7ce190e41614cdf1c0e047702be9118 Mon Sep 17 00:00:00 2001 From: Joseph Klix Date: Fri, 14 Apr 2023 14:17:54 -0700 Subject: [PATCH 54/98] add github templates and bots (#272) * add github templates and bots * Update config.yml --- .github/ISSUE_TEMPLATE/bug-report.yml | 82 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/documentation.yml | 23 ++++++ .github/ISSUE_TEMPLATE/feature-request.yml | 47 +++++++++++++ .github/workflows/closed-issue-message.yml | 17 +++++ .github/workflows/stale_issue.yml | 46 ++++++++++++ 6 files changed, 220 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/workflows/closed-issue-message.yml create mode 100644 .github/workflows/stale_issue.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..6c99277f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,82 @@ +--- +name: "🐛 Bug Report" +description: Report a bug +title: "(short issue description)" +labels: [bug, needs-triage] +assignees: [] +body: + - type: textarea + id: description + attributes: + label: Describe the bug + description: What is the problem? A clear and concise description of the bug. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: | + What did you expect to happen? + validations: + required: true + - type: textarea + id: current + attributes: + label: Current Behavior + description: | + What actually happened? + + Please include full errors, uncaught exceptions, stack traces, and relevant logs. + If service responses are relevant, please include wire logs. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction Steps + description: | + Provide a self-contained, concise snippet of code that can be used to reproduce the issue. + For more complex issues provide a repo with the smallest sample that reproduces the bug. + + Avoid including business logic or unrelated code, it makes diagnosis more difficult. + The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Possible Solution + description: | + Suggest a fix/reason for the bug + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional Information/Context + description: | + Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. + validations: + required: false + + - type: input + id: aws-c-mqtt-version + attributes: + label: aws-c-mqtt version used + validations: + required: true + + - type: input + id: compiler-version + attributes: + label: Compiler and version used + validations: + required: true + + - type: input + id: operating-system + attributes: + label: Operating System and version + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..6acb1b2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 General Question + url: https://github.com/awslabs/aws-c-mqtt/discussions/categories/q-a + about: Please ask and answer questions as a discussion thread diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 00000000..c068514d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,23 @@ +--- +name: "📕 Documentation Issue" +description: Report an issue in the API Reference documentation or Developer Guide +title: "(short issue description)" +labels: [documentation, needs-triage] +assignees: [] +body: + - type: textarea + id: description + attributes: + label: Describe the issue + description: A clear and concise description of the issue. + validations: + required: true + + - type: textarea + id: links + attributes: + label: Links + description: | + Include links to affected documentation page(s). + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..11d667d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,47 @@ +--- +name: 🚀 Feature Request +description: Suggest an idea for this project +title: "(short issue description)" +labels: [feature-request, needs-triage] +assignees: [] +body: + - type: textarea + id: description + attributes: + label: Describe the feature + description: A clear and concise description of the feature you are proposing. + validations: + required: true + - type: textarea + id: use-case + attributes: + label: Use Case + description: | + Why do you need this feature? For example: "I'm always frustrated when..." + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: | + Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. + validations: + required: false + - type: textarea + id: other + attributes: + label: Other Information + description: | + Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. + validations: + required: false + - type: checkboxes + id: ack + attributes: + label: Acknowledgements + options: + - label: I may be able to implement this feature request + required: false + - label: This feature might incur a breaking change + required: false diff --git a/.github/workflows/closed-issue-message.yml b/.github/workflows/closed-issue-message.yml new file mode 100644 index 00000000..3340afb1 --- /dev/null +++ b/.github/workflows/closed-issue-message.yml @@ -0,0 +1,17 @@ +name: Closed Issue Message +on: + issues: + types: [closed] +jobs: + auto_comment: + runs-on: ubuntu-latest + steps: + - uses: aws-actions/closed-issue-message@v1 + with: + # These inputs are both required + repo-token: "${{ secrets.GITHUB_TOKEN }}" + message: | + ### ⚠️COMMENT VISIBILITY WARNING⚠️ + Comments on closed issues are hard for our team to see. + If you need more assistance, please either tag a team member or open a new issue that references this one. + If you wish to keep having a conversation with other community members under this issue feel free to do so. diff --git a/.github/workflows/stale_issue.yml b/.github/workflows/stale_issue.yml new file mode 100644 index 00000000..857a4b0f --- /dev/null +++ b/.github/workflows/stale_issue.yml @@ -0,0 +1,46 @@ +name: "Close stale issues" + +# Controls when the action will run. +on: + schedule: + - cron: "*/60 * * * *" + +jobs: + cleanup: + runs-on: ubuntu-latest + name: Stale issue job + steps: + - uses: aws-actions/stale-issue-cleanup@v3 + with: + # Setting messages to an empty string will cause the automation to skip + # that category + ancient-issue-message: Greetings! Sorry to say but this is a very old issue that is probably not getting as much attention as it deservers. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to open a new one. + stale-issue-message: Greetings! It looks like this issue hasn’t been active in longer than a week. We encourage you to check if this is still an issue in the latest release. Because it has been longer than a week since the last update on this, and in the absence of more information, we will be closing this issue soon. If you find that this is still a problem, please feel free to provide a comment or add an upvote to prevent automatic closure, or if the issue is already closed, please feel free to open a new one. + stale-pr-message: Greetings! It looks like this PR hasn’t been active in longer than a week, add a comment or an upvote to prevent automatic closure, or if the issue is already closed, please feel free to open a new one. + + # These labels are required + stale-issue-label: closing-soon + exempt-issue-label: automation-exempt + stale-pr-label: closing-soon + exempt-pr-label: pr/needs-review + response-requested-label: response-requested + + # Don't set closed-for-staleness label to skip closing very old issues + # regardless of label + closed-for-staleness-label: closed-for-staleness + + # Issue timing + days-before-stale: 2 + days-before-close: 5 + days-before-ancient: 365 + + # If you don't want to mark a issue as being ancient based on a + # threshold of "upvotes", you can set this here. An "upvote" is + # the total number of +1, heart, hooray, and rocket reactions + # on an issue. + minimum-upvotes-to-exempt: 1 + + repo-token: ${{ secrets.GITHUB_TOKEN }} + loglevel: DEBUG + # Set dry-run to true to not perform label or close actions. + dry-run: false From 78d56c0cfd81776defd8a0da66749f14e7aae147 Mon Sep 17 00:00:00 2001 From: Dengke Tang Date: Wed, 19 Apr 2023 13:40:06 -0700 Subject: [PATCH 55/98] update builder version to fix aws-lc build on manylinux1 (#273) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44e8a9c9..564f6ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - 'main' env: - BUILDER_VERSION: v0.9.26 + BUILDER_VERSION: v0.9.40 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt From 0b6c56c42bd48d697d7d12d86e4e8457fd75bce0 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Thu, 20 Apr 2023 13:09:26 -0400 Subject: [PATCH 56/98] Canary amazon linux (#275) Change MQTT5 canary from Ubuntu to Amazon Linux 2. --- codebuild/mqtt-canary-test.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/codebuild/mqtt-canary-test.yml b/codebuild/mqtt-canary-test.yml index 2fbd3a9e..25e4cc6e 100644 --- a/codebuild/mqtt-canary-test.yml +++ b/codebuild/mqtt-canary-test.yml @@ -8,7 +8,7 @@ env: CANARY_CLIENT_COUNT: 10 CANARY_LOG_FILE: 'canary_log.txt' CANARY_LOG_LEVEL: 'ERROR' - BUILDER_VERSION: v0.9.15 + BUILDER_VERSION: v0.9.40 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: 'aws-c-mqtt' @@ -19,16 +19,22 @@ env: phases: install: commands: - # install cmake for codebuild environment. - - apt-get install build-essential libssl-dev -y -f - - apt-get update -y - - apt-get install cmake -y -f + # install c++ dev libraries for codebuild environment. + - sudo yum update -y + - sudo yum groupinstall "Development Tools" -y # Install necessary lib for canary wrapper - - sudo apt-get install gcc python3-dev -y -f - - sudo apt-get install pip -y -f + - sudo yum install gcc python3-dev -y + - sudo yum install pip -y - python3 -m pip install psutil - python3 -m pip install boto3 - + # Install Cmake3 + - wget https://cmake.org/files/v3.18/cmake-3.18.0.tar.gz + - tar -xvzf cmake-3.18.0.tar.gz + - cd cmake-3.18.0 + - ./bootstrap + - make + - sudo make install + - cd .. build: commands: - export CANNARY_TEST_EXE=$CODEBUILD_SRC_DIR/$CANARY_TEST_EXE_PATH From ef84313daba8ba85c3aa3cbae0520de76d85832a Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:10:19 -0700 Subject: [PATCH 57/98] add a millisecond sleep to insure tests dont share socket endpoints (#276) * add a millisecond sleep to insure tests don't share socket endpoints --- tests/v3/connection_state_test.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 86d680c3..a768ddbd 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -26,6 +26,7 @@ static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; +static const int ONE_MILLISECOND = 1000000; // The value is extract from aws-c-mqtt/source/client.c static const int AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS = 10; static const uint64_t RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS = 500000000; @@ -243,6 +244,8 @@ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); + /* Sleep for one millisecond to insure timestamp doesn't repeat over tests */ + aws_thread_current_sleep(ONE_MILLISECOND); uint64_t timestamp = 0; ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); From aea5b9e1e7fe9c663c762e79acaccd03ae6583bd Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Wed, 26 Apr 2023 14:31:21 -0700 Subject: [PATCH 58/98] Test socket endpoint using UUID (#277) * swap timestamp for uuid for unique socket endpoints in tests --- tests/v3/connection_state_test.c | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index a768ddbd..b406df2a 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -17,16 +17,10 @@ #include #include - -#ifdef _WIN32 -# define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" -#else -# define LOCAL_SOCK_TEST_PATTERN "testsock%llu.sock" -#endif +#include static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; -static const int ONE_MILLISECOND = 1000000; // The value is extract from aws-c-mqtt/source/client.c static const int AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS = 10; static const uint64_t RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS = 500000000; @@ -244,16 +238,22 @@ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); - /* Sleep for one millisecond to insure timestamp doesn't repeat over tests */ - aws_thread_current_sleep(ONE_MILLISECOND); - uint64_t timestamp = 0; - ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); + struct aws_byte_buf endpoint_buf = + aws_byte_buf_from_empty_array(state_test_data->endpoint.address, sizeof(state_test_data->endpoint.address)); +#ifdef _WIN32 + AWS_FATAL_ASSERT( + aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str("\\\\.\\pipe\\testsock"))); +#else + AWS_FATAL_ASSERT(aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str("testsock"))); +#endif + /* Use UUID to generate a random endpoint for the socket */ + struct aws_uuid uuid; + ASSERT_SUCCESS(aws_uuid_init(&uuid)); + ASSERT_SUCCESS(aws_uuid_to_str(&uuid, &endpoint_buf)); - snprintf( - state_test_data->endpoint.address, - sizeof(state_test_data->endpoint.address), - LOCAL_SOCK_TEST_PATTERN, - (long long unsigned)timestamp); +#ifndef _WIN32 + AWS_FATAL_ASSERT(aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str(".sock"))); +#endif struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { .bootstrap = state_test_data->server_bootstrap, From 46ff2b127b143e5fac0b535a1e17606db1c6822a Mon Sep 17 00:00:00 2001 From: Steve Kim <86316075+sbSteveK@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:39:19 -0700 Subject: [PATCH 59/98] add const qualifier to topic alias options in client options for mqtt5 (#278) --- include/aws/mqtt/v5/mqtt5_client.h | 2 +- tests/v5/mqtt5_client_tests.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 8dbbc915..3b4cc83e 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -629,7 +629,7 @@ struct aws_mqtt5_client_options { /** * Controls how the client uses mqtt5 topic aliasing. If NULL, zero-based defaults will be used. */ - struct aws_mqtt5_client_topic_alias_options *topic_aliasing_options; + const struct aws_mqtt5_client_topic_alias_options *topic_aliasing_options; /** * Callback for received publish packets diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 909df6f2..5a62ec41 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -5802,7 +5802,7 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; - test_options.client_options.topic_aliasing_options->outbound_topic_alias_behavior = behavior_type; + test_options.topic_aliasing_options.outbound_topic_alias_behavior = behavior_type; struct aws_mqtt5_client_mock_test_fixture test_context; @@ -5982,7 +5982,7 @@ static int s_perform_outbound_alias_sequence_test( test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_mock_server_handle_publish_puback; - test_options.client_options.topic_aliasing_options->outbound_topic_alias_behavior = behavior_type; + test_options.topic_aliasing_options.outbound_topic_alias_behavior = behavior_type; struct aws_mqtt5_client_mock_test_fixture test_context; From 51e35a96ad0f159ff1f8542a7218ac0693c04291 Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 28 Apr 2023 12:30:26 -0400 Subject: [PATCH 60/98] Mqtt311 shared subscription support (#274) * Convert MQTT311 shared subscription topics to normal topics in the topic tree --- source/client.c | 159 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 139 insertions(+), 20 deletions(-) diff --git a/source/client.c b/source/client.c index a3632cd6..c18d7257 100644 --- a/source/client.c +++ b/source/client.c @@ -30,6 +30,7 @@ #ifdef _MSC_VER # pragma warning(disable : 4204) +# pragma warning(disable : 4996) /* allow strncpy() */ #endif /* 3 seconds */ @@ -173,6 +174,44 @@ static void s_init_statistics(struct aws_mqtt_connection_operation_statistics_im aws_atomic_store_int(&stats->unacked_operation_size_atomic, 0); } +static bool s_is_topic_shared_topic(struct aws_byte_cursor *input) { + char *input_str = (char *)input->ptr; + if (strncmp("$share/", input_str, strlen("$share/")) == 0) { + return true; + } + return false; +} + +static struct aws_string *s_get_normal_topic_from_shared_topic(struct aws_string *input) { + const char *input_char_str = aws_string_c_str(input); + size_t input_char_length = strlen(input_char_str); + size_t split_position = 7; // Start at '$share/' since we know it has to exist + while (split_position < input_char_length) { + split_position += 1; + if (input_char_str[split_position] == '/') { + break; + } + } + // If we got all the way to the end, OR there is not at least a single character + // after the second /, then it's invalid input. + if (split_position + 1 >= input_char_length) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "Cannot parse shared subscription topic: Topic is not formatted correctly"); + return NULL; + } + const size_t split_delta = input_char_length - split_position; + if (split_delta > 0) { + // Annoyingly, we cannot just use 'char result_char[split_delta];' because + // MSVC doesn't support it. + char *result_char = aws_mem_calloc(input->allocator, split_delta, sizeof(char)); + strncpy(result_char, input_char_str + split_position + 1, split_delta); + struct aws_string *result_string = aws_string_new_from_c_str(input->allocator, (const char *)result_char); + aws_mem_release(input->allocator, result_char); + return result_string; + } + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "Cannot parse shared subscription topic: Topic is not formatted correctly"); + return NULL; +} + /******************************************************************************* * Client Init ******************************************************************************/ @@ -1816,16 +1855,41 @@ static enum aws_mqtt_client_request_state s_subscribe_send(uint16_t packet_id, b } if (!task_arg->tree_updated) { - if (aws_mqtt_topic_tree_transaction_insert( - &task_arg->connection->thread_data.subscriptions, - &transaction, - topic->filter, - topic->request.qos, - s_on_publish_client_wrapper, - s_task_topic_release, - topic)) { - goto handle_error; + struct aws_byte_cursor filter_cursor = aws_byte_cursor_from_string(topic->filter); + if (s_is_topic_shared_topic(&filter_cursor)) { + struct aws_string *normal_topic = s_get_normal_topic_from_shared_topic(topic->filter); + if (normal_topic == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Topic is shared subscription topic but topic could not be parsed from " + "shared subscription topic.", + (void *)task_arg->connection); + goto handle_error; + } + if (aws_mqtt_topic_tree_transaction_insert( + &task_arg->connection->thread_data.subscriptions, + &transaction, + normal_topic, + topic->request.qos, + s_on_publish_client_wrapper, + s_task_topic_release, + topic)) { + aws_string_destroy(normal_topic); + goto handle_error; + } + aws_string_destroy(normal_topic); + } else { + if (aws_mqtt_topic_tree_transaction_insert( + &task_arg->connection->thread_data.subscriptions, + &transaction, + topic->filter, + topic->request.qos, + s_on_publish_client_wrapper, + s_task_topic_release, + topic)) { + goto handle_error; + } } /* If insert succeed, acquire the refcount */ aws_ref_count_acquire(&topic->ref_count); @@ -2214,15 +2278,39 @@ static enum aws_mqtt_client_request_state s_subscribe_local_send( is_first_attempt ? "first attempt" : "redo"); struct subscribe_task_topic *topic = task_arg->task_topic; - if (aws_mqtt_topic_tree_insert( - &task_arg->connection->thread_data.subscriptions, - topic->filter, - topic->request.qos, - s_on_publish_client_wrapper, - s_task_topic_release, - topic)) { - return AWS_MQTT_CLIENT_REQUEST_ERROR; + struct aws_byte_cursor filter_cursor = aws_byte_cursor_from_string(topic->filter); + if (s_is_topic_shared_topic(&filter_cursor)) { + struct aws_string *normal_topic = s_get_normal_topic_from_shared_topic(topic->filter); + if (normal_topic == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Topic is shared subscription topic but topic could not be parsed from " + "shared subscription topic.", + (void *)task_arg->connection); + return AWS_MQTT_CLIENT_REQUEST_ERROR; + } + if (aws_mqtt_topic_tree_insert( + &task_arg->connection->thread_data.subscriptions, + normal_topic, + topic->request.qos, + s_on_publish_client_wrapper, + s_task_topic_release, + topic)) { + aws_string_destroy(normal_topic); + return AWS_MQTT_CLIENT_REQUEST_ERROR; + } + aws_string_destroy(normal_topic); + } else { + if (aws_mqtt_topic_tree_insert( + &task_arg->connection->thread_data.subscriptions, + topic->filter, + topic->request.qos, + s_on_publish_client_wrapper, + s_task_topic_release, + topic)) { + return AWS_MQTT_CLIENT_REQUEST_ERROR; + } } aws_ref_count_acquire(&topic->ref_count); @@ -2626,9 +2714,40 @@ static enum aws_mqtt_client_request_state s_unsubscribe_send( if (!task_arg->tree_updated) { struct subscribe_task_topic *topic; - if (aws_mqtt_topic_tree_transaction_remove( - &task_arg->connection->thread_data.subscriptions, &transaction, &task_arg->filter, (void **)&topic)) { - goto handle_error; + + if (s_is_topic_shared_topic(&task_arg->filter)) { + struct aws_string *shared_topic = + aws_string_new_from_cursor(task_arg->connection->allocator, &task_arg->filter); + struct aws_string *normal_topic = s_get_normal_topic_from_shared_topic(shared_topic); + if (normal_topic == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Topic is shared subscription topic but topic could not be parsed from " + "shared subscription topic.", + (void *)task_arg->connection); + aws_string_destroy(shared_topic); + goto handle_error; + } + struct aws_byte_cursor normal_topic_cursor = aws_byte_cursor_from_string(normal_topic); + if (aws_mqtt_topic_tree_transaction_remove( + &task_arg->connection->thread_data.subscriptions, + &transaction, + &normal_topic_cursor, + (void **)&topic)) { + aws_string_destroy(shared_topic); + aws_string_destroy(normal_topic); + goto handle_error; + } + aws_string_destroy(shared_topic); + aws_string_destroy(normal_topic); + } else { + if (aws_mqtt_topic_tree_transaction_remove( + &task_arg->connection->thread_data.subscriptions, + &transaction, + &task_arg->filter, + (void **)&topic)) { + goto handle_error; + } } task_arg->is_local = topic ? topic->is_local : false; From 9b9eb47cd80179bd82ca511d17e586275a36d6eb Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 28 Apr 2023 14:21:49 -0400 Subject: [PATCH 61/98] Backed up socket fixes (#254) Adjust reconnect to abort if in the middle of a disconnect. Track the last outbound socket write and adjust ping time accordingly. Both changes only apply to MQTT311. --- include/aws/mqtt/private/client_impl.h | 9 + source/client.c | 60 +++++- source/client_channel_handler.c | 54 ++++- tests/CMakeLists.txt | 4 + tests/v3/connection_state_test.c | 279 +++++++++++++++++++++++++ tests/v3/mqtt_mock_server_handler.c | 10 + tests/v3/mqtt_mock_server_handler.h | 5 + 7 files changed, 405 insertions(+), 16 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 6145c23a..d1321360 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -198,6 +198,7 @@ struct aws_mqtt_client_connection { struct aws_byte_buf client_id; bool clean_session; uint16_t keep_alive_time_secs; + uint64_t keep_alive_time_ns; uint64_t ping_timeout_ns; uint64_t operation_timeout_ns; struct aws_string *username; @@ -309,6 +310,14 @@ struct aws_mqtt_client_connection { struct aws_http_message *handshake_request; } websocket; + /** + * The time that the next ping task should execute at. Note that this does not mean that + * this IS when the ping task will execute, but rather that this is when the next ping + * SHOULD execute. There may be an already scheduled PING task that will elapse sooner + * than this time that has to be rescheduled. + */ + uint64_t next_ping_time; + /** * Statistics tracking operational state */ diff --git a/source/client.c b/source/client.c index c18d7257..c028effc 100644 --- a/source/client.c +++ b/source/client.c @@ -542,7 +542,7 @@ static void s_mqtt_client_init( mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ - /* intall the slot and handler */ + /* install the slot and handler */ if (failed_create_slot) { AWS_LOGF_ERROR( @@ -688,11 +688,55 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ struct aws_mqtt_reconnect_task *reconnect = userdata; struct aws_mqtt_client_connection *connection = aws_atomic_load_ptr(&reconnect->connection_ptr); + /* If the task is not cancelled and a connection has not succeeded, attempt reconnect */ if (status == AWS_TASK_STATUS_RUN_READY && connection) { - /* If the task is not cancelled and a connection has not succeeded, attempt reconnect */ - mqtt_connection_lock_synced_data(connection); + /** + * Check the state and if we are disconnecting (AWS_MQTT_CLIENT_STATE_DISCONNECTING) then we want to skip it + * and abort the reconnect task (or rather, just do not try to reconnect) + */ + if (connection->synced_data.state == AWS_MQTT_CLIENT_STATE_DISCONNECTING) { + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, "id=%p: Skipping reconnect: Client is trying to disconnect", (void *)connection); + + /** + * There is the nasty world where the disconnect task/function is called right when we are "reconnecting" as + * our state but we have not reconnected. When this happens, the disconnect function doesn't do anything + * beyond setting the state to AWS_MQTT_CLIENT_STATE_DISCONNECTING (aws_mqtt_client_connection_disconnect), + * meaning the disconnect callback will NOT be called nor will we release memory. + * For this reason, we have to do the callback and release of the connection here otherwise the code + * will DEADLOCK forever and that is bad. + */ + bool perform_full_destroy = false; + if (!connection->slot) { + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Reconnect task called but client is disconnecting and has no slot. Finishing disconnect", + (void *)connection); + mqtt_connection_set_state(connection, AWS_MQTT_CLIENT_STATE_DISCONNECTED); + perform_full_destroy = true; + } + + aws_mem_release(reconnect->allocator, reconnect); + connection->reconnect_task = NULL; + + /* Unlock the synced data, then potentially call the disconnect callback and release the connection */ + mqtt_connection_unlock_synced_data(connection); + if (perform_full_destroy) { + MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); + MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); + aws_mqtt_client_connection_release(connection); + } + return; + } + + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Attempting reconnect, if it fails next attempt will be in %" PRIu64 " seconds", + (void *)connection, + connection->reconnect_timeouts.current_sec); + /* Check before multiplying to avoid potential overflow */ if (connection->reconnect_timeouts.current_sec > connection->reconnect_timeouts.max_sec / 2) { connection->reconnect_timeouts.current_sec = connection->reconnect_timeouts.max_sec; @@ -1508,6 +1552,9 @@ int aws_mqtt_client_connection_connect( if (!connection->keep_alive_time_secs) { connection->keep_alive_time_secs = s_default_keep_alive_sec; } + connection->keep_alive_time_ns = + aws_timestamp_convert(connection->keep_alive_time_secs, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + if (!connection_options->protocol_operation_timeout_ms) { connection->operation_timeout_ns = UINT64_MAX; } else { @@ -1526,16 +1573,15 @@ int aws_mqtt_client_connection_connect( } /* Keep alive time should always be greater than the timeouts. */ - if (AWS_UNLIKELY(connection->keep_alive_time_secs * (uint64_t)AWS_TIMESTAMP_NANOS <= connection->ping_timeout_ns)) { + if (AWS_UNLIKELY(connection->keep_alive_time_ns <= connection->ping_timeout_ns)) { AWS_LOGF_FATAL( AWS_LS_MQTT_CLIENT, "id=%p: Illegal configuration, Connection keep alive %" PRIu64 "ns must be greater than the request timeouts %" PRIu64 "ns.", (void *)connection, - (uint64_t)connection->keep_alive_time_secs * (uint64_t)AWS_TIMESTAMP_NANOS, + connection->keep_alive_time_ns, connection->ping_timeout_ns); - AWS_FATAL_ASSERT( - connection->keep_alive_time_secs * (uint64_t)AWS_TIMESTAMP_NANOS > connection->ping_timeout_ns); + AWS_FATAL_ASSERT(connection->keep_alive_time_ns > connection->ping_timeout_ns); } AWS_LOGF_INFO( diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index f9c01cbd..62b5b063 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -20,6 +20,18 @@ # pragma warning(disable : 4204) #endif +/******************************************************************************* + * Static Helper functions + ******************************************************************************/ + +/* Caches the socket write time for ping scheduling purposes */ +static void s_update_next_ping_time(struct aws_mqtt_client_connection *connection) { + if (connection->slot != NULL && connection->slot->channel != NULL) { + aws_channel_current_clock_time(connection->slot->channel, &connection->next_ping_time); + aws_add_u64_checked(connection->next_ping_time, connection->keep_alive_time_ns, &connection->next_ping_time); + } +} + /******************************************************************************* * Packet State Machine ******************************************************************************/ @@ -42,18 +54,17 @@ static void s_schedule_ping(struct aws_mqtt_client_connection *connection) { uint64_t now = 0; aws_channel_current_clock_time(connection->slot->channel, &now); - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, "id=%p: Scheduling PING. current timestamp is %" PRIu64, (void *)connection, now); - uint64_t schedule_time = - now + aws_timestamp_convert(connection->keep_alive_time_secs, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, "id=%p: Scheduling PING task. current timestamp is %" PRIu64, (void *)connection, now); AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, - "id=%p: The next ping will be run at timestamp %" PRIu64, + "id=%p: The next PING task will be run at timestamp %" PRIu64, (void *)connection, - schedule_time); - aws_channel_schedule_task_future(connection->slot->channel, &connection->ping_task, schedule_time); + connection->next_ping_time); + + aws_channel_schedule_task_future(connection->slot->channel, &connection->ping_task, connection->next_ping_time); } static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status) { @@ -61,8 +72,24 @@ static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, if (status == AWS_TASK_STATUS_RUN_READY) { struct aws_mqtt_client_connection *connection = arg; - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Sending PING", (void *)connection); - aws_mqtt_client_connection_ping(connection); + + uint64_t now = 0; + aws_channel_current_clock_time(connection->slot->channel, &now); + if (now >= connection->next_ping_time) { + s_update_next_ping_time(connection); + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Sending PING", (void *)connection); + aws_mqtt_client_connection_ping(connection); + } else { + + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: Skipped sending PING because scheduled ping time %" PRIu64 + " has not elapsed yet. Current time is %" PRIu64 + ". Rescheduling ping to run at the scheduled ping time...", + (void *)connection, + connection->next_ping_time, + now); + } s_schedule_ping(connection); } } @@ -175,6 +202,7 @@ static int s_packet_handler_connack( AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: connection callback completed", (void *)connection); + s_update_next_ping_time(connection); s_schedule_ping(connection); return AWS_OP_SUCCESS; } @@ -793,6 +821,9 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en aws_mqtt_connection_statistics_change_operation_statistic_state( request->connection, request, AWS_MQTT_OSS_NONE); + /* Since a request has complete, update the next ping time */ + s_update_next_ping_time(connection); + aws_hash_table_remove( &connection->synced_data.outstanding_requests_table, &request->packet_id, NULL, NULL); aws_memory_pool_release(&connection->synced_data.requests_pool, request); @@ -814,6 +845,9 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en aws_mqtt_connection_statistics_change_operation_statistic_state( request->connection, request, AWS_MQTT_OSS_INCOMPLETE | AWS_MQTT_OSS_UNACKED); + /* Since a request has complete, update the next ping time */ + s_update_next_ping_time(connection); + mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ @@ -1057,5 +1091,7 @@ void mqtt_disconnect_impl(struct aws_mqtt_client_connection *connection, int err shutdown_task->error_code = error_code; aws_channel_task_init(&shutdown_task->task, s_mqtt_disconnect_task, connection, "mqtt_disconnect"); aws_channel_schedule_task_now(connection->slot->channel, &shutdown_task->task); + } else { + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Client currently has no slot to disconnect", (void *)connection); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8becd4d4..63568959 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,6 +61,10 @@ add_test_case(mqtt_clean_session_keep_next_session) add_test_case(mqtt_connection_publish_QoS1_timeout) add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) +add_test_case(mqtt_connection_ping_norm) +add_test_case(mqtt_connection_ping_no) +add_test_case(mqtt_connection_ping_basic_scenario) +add_test_case(mqtt_connection_ping_double_scenario) add_test_case(mqtt_connection_close_callback_simple) add_test_case(mqtt_connection_close_callback_interrupted) add_test_case(mqtt_connection_close_callback_multi) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index b406df2a..2355fb02 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -3201,3 +3201,282 @@ AWS_TEST_CASE_FIXTURE( s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection, s_clean_up_mqtt_server_fn, &test_data) + +/** + * Makes a CONNECT, with 1 second keep alive ping interval, does nothing for roughly 4 seconds, ensures 4 pings are sent + */ +static int s_test_mqtt_connection_ping_norm_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = 1, + .ping_timeout_ms = 100, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + /* Wait for 4.5 seconds (to account for slight drift/jitter) */ + aws_thread_current_sleep(4500000000); + + /* Ensure the server got 4 PING packets */ + ASSERT_INT_EQUALS(4, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_ping_norm, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_ping_norm_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Makes a CONNECT, with 1 second keep alive ping interval, send a publish roughly every second, and then ensure NO + * pings were sent + */ +static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = 1, + .ping_timeout_ms = 100, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + for (int i = 0; i < 4; i++) { + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + + /* Wait 0.8 seconds */ + aws_thread_current_sleep(800000000); + } + + /* Ensure the server got 0 PING packets */ + ASSERT_INT_EQUALS(0, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_ping_no, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_ping_no_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test to make sure the PING timing is correct if a publish/packet is sent near the end of the keep alive time. + * Note: Because of socket write jitter and scheduling jitter, the times have a 0.25 (quarter of a second) delta range. + * + * To test this, this test has a keep alive at 4 seconds and makes a publish after 3 seconds. This resets the ping + * task and will reschedule it for 4 seconds from the publish (the PING will be scheduled for 3 seconds after the 4 + * second task is invoked). This test then waits a second, makes sure a PING has NOT been sent (with no ping reschedule, + * it would have) and then waits 3 seconds to ensure and checks that a PING has been sent. Finally, it waits 4 seconds + * to ensure a second PING was sent at the correct time. + */ +static int s_test_mqtt_connection_ping_basic_scenario_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = 4, + .ping_timeout_ms = 100, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + /* PING will be in 4 seconds */ + + aws_thread_current_sleep(3000000000); /* Wait 3 seconds */ + + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Make a publish */ + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + s_wait_for_ops_completed(state_test_data); + /* Publish packet written at 3 seconds */ + + aws_thread_current_sleep(1250000000); /* Wait 1.25 second (the extra 0.25 is to account for jitter) */ + /* PING task has executed and been rescheduled at 3 seconds (1 second passed) */ + + /* Ensure the server has gotten 0 PING packets so far */ + ASSERT_INT_EQUALS(0, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + aws_thread_current_sleep( + 3000000000); /* Wait 3 seconds more (no jitter needed because we already added 0.25 in the prior sleep) */ + /* PING task (from publish) has been executed */ + + /* Ensure the server has gotten only 1 PING packet */ + ASSERT_INT_EQUALS(1, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + aws_thread_current_sleep(4000000000); /* Wait 4 seconds (since we didn't publish or anything, it should go back to + normal keep alive time) */ + + /* Ensure the server has gotten 2 PING packets */ + ASSERT_INT_EQUALS(2, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + /* Disconnect and finish! */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_ping_basic_scenario, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_ping_basic_scenario_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * The test is the same as above (s_test_mqtt_connection_ping_basic_scenario_fn) but after the first publish, it waits + * 1 second and makes another publish, before waiting 4 seconds from that point and ensures only a single PING was sent. + */ +static int s_test_mqtt_connection_ping_double_scenario_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = 4, + .ping_timeout_ms = 100, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + /* PING will be in 4 seconds */ + + aws_thread_current_sleep(3000000000); /* Wait 3 seconds */ + + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* Make a publish */ + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_1 > 0); + s_wait_for_ops_completed(state_test_data); + /* Publish packet written at 3 seconds */ + + aws_thread_current_sleep(1250000000); /* Wait 1.25 second (the extra 0.25 is to account for jitter) */ + /* PING task has executed and been rescheduled at 3 seconds (1 second passed) */ + + /* Ensure the server has gotten 0 PING packets so far */ + ASSERT_INT_EQUALS(0, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + aws_thread_current_sleep(750000000); /* wait 0.75 seconds */ + + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 2; + aws_mutex_unlock(&state_test_data->lock); + + /* Make as second publish */ + uint16_t packet_id_2 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_2 > 0); + s_wait_for_ops_completed(state_test_data); + /* Publish packet written at 2 seconds (relative to PING that was scheduled above) */ + + aws_thread_current_sleep(4250000000); /* Wait 4.25 (the extra 0.25 is to account for jitter) seconds */ + /** + * Note: The extra 2 seconds are to account for the time it takes to publish on the socket. Despite best efforts, + * I cannot get it to trigger right away in the test suite... + * If you read the logs though, the scheduled PINGs should be 4 seconds, 3 seconds, 2 seconds, 4 seconds + */ + + /* Ensure the server has gotten only 1 PING packet */ + ASSERT_INT_EQUALS(1, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + /** + * At this point a new PING task is scheduled for 4 seconds, but we do not care anymore for the + * purposes of this test. + */ + + /* Disconnect and finish! */ + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_ping_double_scenario, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_ping_double_scenario_fn, + s_clean_up_mqtt_server_fn, + &test_data) diff --git a/tests/v3/mqtt_mock_server_handler.c b/tests/v3/mqtt_mock_server_handler.c index 313dafe3..13f7171f 100644 --- a/tests/v3/mqtt_mock_server_handler.c +++ b/tests/v3/mqtt_mock_server_handler.c @@ -28,6 +28,7 @@ struct mqtt_mock_server_handler { size_t ping_resp_avail; size_t pubacks_received; + size_t ping_received; size_t connacks_avail; bool auto_ack; @@ -111,6 +112,7 @@ static int s_mqtt_mock_server_handler_process_packet( size_t ping_resp_available = 0; aws_mutex_lock(&server->synced.lock); ping_resp_available = server->synced.ping_resp_avail > 0 ? server->synced.ping_resp_avail-- : 0; + server->synced.ping_received += 1; aws_mutex_unlock(&server->synced.lock); if (ping_resp_available) { @@ -725,3 +727,11 @@ int mqtt_mock_server_decode_packets(struct aws_channel_handler *handler) { aws_mutex_unlock(&server->synced.lock); return AWS_OP_SUCCESS; } + +size_t mqtt_mock_server_get_ping_count(struct aws_channel_handler *handler) { + struct mqtt_mock_server_handler *server = handler->impl; + aws_mutex_lock(&server->synced.lock); + size_t count = server->synced.ping_received; + aws_mutex_unlock(&server->synced.lock); + return count; +} diff --git a/tests/v3/mqtt_mock_server_handler.h b/tests/v3/mqtt_mock_server_handler.h index c668c088..6693e422 100644 --- a/tests/v3/mqtt_mock_server_handler.h +++ b/tests/v3/mqtt_mock_server_handler.h @@ -126,4 +126,9 @@ struct mqtt_decoded_packet *mqtt_mock_server_find_decoded_packet_by_type( */ int mqtt_mock_server_decode_packets(struct aws_channel_handler *handler); +/** + * Returns the number of PINGs the server has gotten + */ +size_t mqtt_mock_server_get_ping_count(struct aws_channel_handler *handler); + #endif /* MQTT_MOCK_SERVER_HANDLER_H */ From e6ea3cc88d1654515519701b060a4c21098fcc8c Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Mon, 1 May 2023 13:55:16 -0400 Subject: [PATCH 62/98] Adjust the canary not to crash if it cannot poll the metric alarms (#279) Adjust the canary not to crash if it cannot poll the metric alarms. Improve exception printing. --- codebuild/CanaryWrapper.py | 2 +- codebuild/CanaryWrapper_24_7.py | 2 +- codebuild/CanaryWrapper_Classes.py | 82 ++++++++++++++-------- codebuild/CanaryWrapper_MetricFunctions.py | 6 +- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/codebuild/CanaryWrapper.py b/codebuild/CanaryWrapper.py index c089ffd5..f3819137 100644 --- a/codebuild/CanaryWrapper.py +++ b/codebuild/CanaryWrapper.py @@ -298,7 +298,7 @@ def application_thread(): finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" except Exception as e: print ("ERROR: Could not (possibly) cut ticket due to exception!") - print ("Exception: " + str(e), flush=True) + print (f"Exception: {repr(e)}", flush=True) # Clean everything up and stop snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) diff --git a/codebuild/CanaryWrapper_24_7.py b/codebuild/CanaryWrapper_24_7.py index 877b8259..034f85f4 100644 --- a/codebuild/CanaryWrapper_24_7.py +++ b/codebuild/CanaryWrapper_24_7.py @@ -369,7 +369,7 @@ def application_thread(): finished_email_body += "Failure due to unknown reason! This shouldn't happen and means something has gone wrong!" except Exception as e: print ("ERROR: Could not (possibly) cut ticket due to exception!") - print ("Exception: " + str(e), flush=True) + print (f"Exception: {repr(e)}", flush=True) # Clean everything up and stop snapshot_monitor.cleanup_monitor(error_occurred=wrapper_error_occurred) diff --git a/codebuild/CanaryWrapper_Classes.py b/codebuild/CanaryWrapper_Classes.py index 01f39062..75346a74 100644 --- a/codebuild/CanaryWrapper_Classes.py +++ b/codebuild/CanaryWrapper_Classes.py @@ -67,14 +67,14 @@ def add_metric_to_widget(self, new_metric_name): self.metric_list.append(new_metric_name) except Exception as e: print ("[DataSnapshot_Dashboard] ERROR - could not add metric to dashboard widget due to exception!") - print ("[DataSnapshot_Dashboard] Exception: " + str(e)) + print (f"[DataSnapshot_Dashboard] Exception: {repr(e)}") def remove_metric_from_widget(self, existing_metric_name): try: self.metric_list.remove(existing_metric_name) except Exception as e: print ("[DataSnapshot_Dashboard] ERROR - could not remove metric from dashboard widget due to exception!") - print ("[DataSnapshot_Dashboard] Exception: " + str(e)) + print (f"[DataSnapshot_Dashboard] Exception: {repr(e)}") def get_widget_dictionary(self): metric_list_json = [] @@ -170,6 +170,7 @@ def __init__(self, tmp_sts_client.get_caller_identity() except Exception as e: print ("[DataSnapshot] ERROR - AWS credentials are NOT valid!") + print (f"[DataSnapshot] ERROR - Exception: {repr(e)}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "AWS credentials are NOT valid!" self.abort_due_to_internal_error_due_to_credentials = True @@ -204,7 +205,7 @@ def __init__(self, self.cloudwatch_dashboard_name = self.git_metric_namespace except Exception as e: self.print_message("[DataSnapshot] ERROR - could not make Cloudwatch client due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] Exception: {repr(e)}") self.cloudwatch_client = None self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Could not make Cloudwatch client!" @@ -217,7 +218,7 @@ def __init__(self, self.s3_client = boto3.client("s3") except Exception as e: self.print_message("[DataSnapshot] ERROR - could not make S3 client due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] Exception: {repr(e)}") self.s3_client = None self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Could not make S3 client!" @@ -230,7 +231,7 @@ def __init__(self, self.lambda_client = boto3.client("lambda", self.cloudwatch_region) except Exception as e: self.print_message("[DataSnapshot] ERROR - could not make Lambda client due to exception!") - self.print_message("[DataSnapshot] Exception: " + str(e)) + self.print_message(f"[DataSnapshot] Exception: {repr(e)}") self.lambda_client = None self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Could not make Lambda client!" @@ -274,7 +275,8 @@ def print_message(self, message): self.output_file.write(message + "\n") except Exception as ex: - print (f"[DataSnapshot] Exception trying to print to file: {ex}") + print (f"[DataSnapshot] ERROR - Exception trying to print to file") + print (f"[DataSnapshot] ERROR - Exception: {repr(ex)}") if (self.output_file is not None): self.output_file.close() self.output_file = None @@ -315,9 +317,10 @@ def _init_cloudwatch_pre_first_run_dashboard(self): DashboardBody= new_dashboard_body_json) self.print_message("[DataSnapshot] Added Cloudwatch dashboard successfully") except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - Cloudwatch client could not make dashboard due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - Cloudwatch client could not make dashboard due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception: {repr(e)}") self.abort_due_to_internal_error = True - self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make dashboard due to exception {e}" + self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make dashboard due to exception" return # Utility function - The function that adds each individual metric alarm. @@ -341,9 +344,10 @@ def _add_cloudwatch_metric_alarm(self, metric): ComparisonOperator="GreaterThanOrEqualToThreshold", ) except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - could not register alarm for metric {metric.metric_name} due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - could not register alarm for metric {metric.metric_name} due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") self.abort_due_to_internal_error = True - self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make alarm due to exception: {e}" + self.abort_due_to_internal_error_reason = f"Cloudwatch client could not make alarm due to exception" # Utility function - removes all the Cloudwatch alarms for the metrics def _cleanup_cloudwatch_alarms(self): @@ -353,7 +357,8 @@ def _cleanup_cloudwatch_alarms(self): if (not metric.metric_alarm_threshold is None): self.cloudwatch_client.delete_alarms(AlarmNames=[metric.metric_alarm_name]) except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - could not delete alarms due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - could not delete alarms due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") # Utility function - removes all Cloudwatch dashboards created def _cleanup_cloudwatch_dashboard(self): @@ -362,7 +367,8 @@ def _cleanup_cloudwatch_dashboard(self): self.cloudwatch_client.delete_dashboards(DashboardNames=[self.cloudwatch_dashboard_name]) self.print_message("[DataSnapshot] Cloudwatch Dashboards deleted successfully!") except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - dashboard cleaning function failed due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - dashboard cleaning function failed due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Cloudwatch dashboard cleaning function failed due to exception" return @@ -408,7 +414,8 @@ def _check_cloudwatch_alarm_state_metric(self, metric): return return_result except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - checking cloudwatch alarm failed due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - checking cloudwatch alarm failed due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") return None # Exports a file with the same name as the commit Git hash to an S3 bucket in a folder with the Git repo name. @@ -461,7 +468,8 @@ def export_result_to_s3_bucket(self, copy_output_log=False, log_is_error=False): self.s3_client.upload_file(self.git_hash + ".log", self.s3_bucket_name, self.git_repo_name + "/Failed_Logs/" + self.datetime_string + "/" + self.git_hash + ".log") self.print_message("[DataSnapshot] Uploaded to S3!") except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - could not upload to S3 due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - could not upload to S3 due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "S3 client had exception and therefore could not upload log!" os.remove(self.git_hash + ".log") @@ -485,7 +493,8 @@ def lambda_send_email(self, message, subject): Payload=payload_string ) except Exception as e: - self.print_message(f"[DataSnapshot] ERROR - could not send email via Lambda due to exception: {e}") + self.print_message(f"[DataSnapshot] ERROR - could not send email via Lambda due to exception") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") self.abort_due_to_internal_error = True self.abort_due_to_internal_error_reason = "Lambda email function had an exception!" return @@ -621,7 +630,8 @@ def export_metrics_cloudwatch(self): MetricData=metrics_data) self.print_message("[DataSnapshot] Metrics sent to Cloudwatch.") except Exception as e: - self.print_message(f"[DataSnapshot] Error - something when wrong posting cloudwatch metrics. Exception: {e}") + self.print_message(f"[DataSnapshot] Error - something when wrong posting cloudwatch metrics") + self.print_message(f"[DataSnapshot] ERROR - Exception {repr(e)}") self.print_message("[DataSnapshot] Not going to crash - just going to try again later") return @@ -741,7 +751,8 @@ def register_metric(self, new_metric_name, new_metric_function, new_metric_unit= new_metric_reports_to_skip=new_metric_reports_to_skip, new_metric_alarm_severity=new_metric_alarm_severity) except Exception as e: - self.print_message(f"[SnaptshotMonitor] ERROR - could not register metric in data snapshot due to exception: {e}") + self.print_message(f"[SnaptshotMonitor] ERROR - could not register metric in data snapshot due to exception") + self.print_message(f"[SnaptshotMonitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.internal_error_reason = "Could not register metric in data snapshot due to exception" return @@ -829,9 +840,8 @@ def monitor_loop_function(self, psutil_process : psutil.Process, time_passed=30) self.check_alarms_for_new_alarms(triggered_alarms) except Exception as e: self.print_message("[SnaptshotMonitor] ERROR - exception occurred checking metric alarms!") - self.print_message("[SnaptshotMonitor] (Likely session credentials expired)") - self.had_internal_error = True - self.internal_error_reason = "Exception occurred checking metric alarms! Likely session credentials expired" + self.print_message(f"[SnaptshotMonitor] ERROR - Exception {repr(e)}") + self.print_message("[SnaptshotMonitor] Not going to crash - just going to try again later") return if (self.metric_post_timer <= 0): @@ -840,8 +850,10 @@ def monitor_loop_function(self, psutil_process : psutil.Process, time_passed=30) self.data_snapshot.post_metrics(psutil_process) except Exception as e: self.print_message("[SnaptshotMonitor] ERROR - exception occurred posting metrics!") - self.print_message("[SnaptshotMonitor] (Likely session credentials expired)") + self.print_message(f"[SnaptshotMonitor] ERROR - Exception {repr(e)}") self.print_message("[SnaptshotMonitor] Not going to crash - just going to try again later") + # reset the timer + self.metric_post_timer += self.metric_post_timer_time return # reset the timer @@ -910,7 +922,7 @@ def start_monitoring(self): self.print_message ("[ApplicationMonitor] Application started...") except Exception as e: self.print_message ("[ApplicationMonitor] ERROR - Could not launch Canary/Application due to exception!") - self.print_message ("[ApplicationMonitor] Exception: " + str(e)) + self.print_message(f"[ApplicationMonitor] ERROR - Exception {repr(e)}") self.error_has_occurred = True self.error_reason = "Could not launch Canary/Application due to exception" self.error_code = 1 @@ -928,7 +940,8 @@ def restart_monitoring(self): self.print_message("\n[ApplicationMonitor] Restarted monitor application!") self.print_message("================================================================================") except Exception as e: - self.print_message(f"[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception: {e}") + self.print_message(f"[ApplicationMonitor] ERROR - Could not restart Canary/Application due to exception") + self.print_message(f"[ApplicationMonitor] ERROR - Exception {repr(e)}") self.error_has_occurred = True self.error_reason = "Could not restart Canary/Application due to exception" self.error_code = 1 @@ -961,7 +974,8 @@ def print_stdout(self): self.print_message(stdout_file.read()) os.remove(self.stdout_file_path) except Exception as e: - self.print_message(f"[ApplicationMonitor] ERROR - Could not print Canary/Application stdout to exception: {e}") + self.print_message(f"[ApplicationMonitor] ERROR - Could not print Canary/Application stdout to exception") + self.print_message(f"[ApplicationMonitor] ERROR - Exception {repr(e)}") def monitor_loop_function(self, time_passed=30): if (self.application_process != None): @@ -971,7 +985,7 @@ def monitor_loop_function(self, time_passed=30): application_process_return_code = self.application_process.poll() except Exception as e: self.print_message("[ApplicationMonitor] ERROR - exception occurred while trying to poll application status!") - self.print_message("[ApplicationMonitor] Exception: " + str(e)) + self.print_message(f"[ApplicationMonitor] ERROR - Exception {repr(e)}") self.error_has_occurred = True self.error_reason = "Exception when polling application status" self.error_code = 1 @@ -1048,6 +1062,7 @@ def __init__(self, s3_bucket_name, s3_file_name, s3_file_name_in_zip, canary_loc tmp_sts_client.get_caller_identity() except Exception as e: self.print_message("[S3Monitor] ERROR - (S3 Check) AWS credentials are NOT valid!") + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.error_due_to_credentials = True self.internal_error_reason = "AWS credentials are NOT valid!" @@ -1058,6 +1073,7 @@ def __init__(self, s3_bucket_name, s3_file_name, s3_file_name_in_zip, canary_loc self.s3_client = boto3.client("s3") except Exception as e: self.print_message("[S3Monitor] ERROR - (S3 Check) Could not make S3 client") + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.internal_error_reason = "Could not make S3 client for S3 Monitor" return @@ -1085,7 +1101,8 @@ def check_for_file_change(self): return except Exception as e: - self.print_message(f"[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception: {e}") + self.print_message(f"[S3Monitor] ERROR - Could not check for new version of file in S3 due to exception") + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.print_message("[S3Monitor] Going to try again later - will not crash Canary") @@ -1096,6 +1113,7 @@ def replace_current_file_for_new_file(self): os.makedirs("tmp") except Exception as e: self.print_message ("[S3Monitor] ERROR - could not make tmp directory to place S3 file into!") + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.internal_error_reason = "Could not make TMP folder for S3 file download" return @@ -1108,6 +1126,7 @@ def replace_current_file_for_new_file(self): s3_resource.meta.client.download_file(self.s3_bucket_name, self.s3_file_name, new_file_path) except Exception as e: self.print_message("[S3Monitor] ERROR - could not download latest S3 file into TMP folder!") + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.internal_error_reason = "Could not download latest S3 file into TMP folder" return @@ -1132,7 +1151,7 @@ def replace_current_file_for_new_file(self): except Exception as e: self.print_message("[S3Monitor] ERROR - could not move file into local application path due to exception!") - self.print_message("[S3Monitor] Exception: " + str(e)) + self.print_message(f"[S3Monitor] ERROR - Exception {repr(e)}") self.had_internal_error = True self.internal_error_reason = "Could not move file into local application path" return @@ -1203,7 +1222,8 @@ def cut_ticket_using_cloudwatch( cloudwatch_client = boto3.client('cloudwatch', cloudwatch_region) ticket_alarm_name = git_repo_name + "-" + git_hash + "-AUTO-TICKET" except Exception as e: - print (f"ERROR - could not create Cloudwatch client to make ticket metric alarm due to exception: {e}", flush=True) + print (f"ERROR - could not create Cloudwatch client to make ticket metric alarm due to exception", flush=True) + print(f"ERROR - Exception {repr(e)}", flush=True) return new_metric_dimensions = [] @@ -1243,7 +1263,8 @@ def cut_ticket_using_cloudwatch( AlarmActions=[ticket_arn] ) except Exception as e: - print (f"ERROR - could not create ticket metric alarm due to exception: {e}", flush=True) + print (f"ERROR - could not create ticket metric alarm due to exception", flush=True) + print(f"ERROR - Exception {repr(e)}", flush=True) return # Trigger the alarm so it cuts the ticket @@ -1253,7 +1274,8 @@ def cut_ticket_using_cloudwatch( StateValue="ALARM", StateReason="AUTO TICKET CUT") except Exception as e: - print (f"ERROR - could not cut ticket due to exception: {e}", flush=True) + print (f"ERROR - could not cut ticket due to exception", flush=True) + print(f"ERROR - Exception {repr(e)}", flush=True) return print("Waiting for ticket metric to trigger...", flush=True) diff --git a/codebuild/CanaryWrapper_MetricFunctions.py b/codebuild/CanaryWrapper_MetricFunctions.py index 05b1934e..d352181a 100644 --- a/codebuild/CanaryWrapper_MetricFunctions.py +++ b/codebuild/CanaryWrapper_MetricFunctions.py @@ -20,7 +20,7 @@ def get_metric_total_cpu_usage(psutil_process : psutil.Process): return psutil.cpu_percent(interval=None) except Exception as e: print ("ERROR - exception occurred gathering metrics!") - print ("Exception: " + str(e), flush=True) + print (f"Exception: {repr(e)}", flush=True) return None # Note: This value is in BYTES. @@ -32,7 +32,7 @@ def get_metric_total_memory_usage_value(psutil_process : psutil.Process): return psutil.virtual_memory()[3] except Exception as e: print ("ERROR - exception occurred gathering metrics!") - print ("Exception: " + str(e), flush=True) + print (f"Exception: {repr(e)}", flush=True) return None @@ -44,5 +44,5 @@ def get_metric_total_memory_usage_percent(psutil_process : psutil.Process): return psutil.virtual_memory()[2] except Exception as e: print ("ERROR - exception occurred gathering metrics!") - print ("Exception: " + str(e), flush=True) + print (f"Exception: {repr(e)}", flush=True) return None From cdff1b700e29d12a64a71577f56d2fa6d515c003 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 8 May 2023 13:46:26 -0700 Subject: [PATCH 63/98] Vtable refactor311 (#280) Refactors the mqtt311 implementation into two pieces: a generic wrapper with a vtable and the actual implementation. --- include/aws/mqtt/private/client_impl.h | 45 +- include/aws/mqtt/private/client_impl_shared.h | 111 ++++ .../mqtt/private/mqtt_client_test_helper.h | 2 +- source/client.c | 523 ++++++++++-------- source/client_channel_handler.c | 53 +- source/client_impl_shared.c | 194 +++++++ tests/v3/operation_statistics_test.c | 8 +- 7 files changed, 669 insertions(+), 267 deletions(-) create mode 100644 include/aws/mqtt/private/client_impl_shared.h create mode 100644 source/client_impl_shared.c diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index d1321360..09f92d38 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -8,6 +8,7 @@ #include +#include #include #include @@ -21,16 +22,18 @@ #include #include +struct aws_mqtt_client_connection_311_impl; + #define MQTT_CLIENT_CALL_CALLBACK(client_ptr, callback) \ do { \ if ((client_ptr)->callback) { \ - (client_ptr)->callback((client_ptr), (client_ptr)->callback##_ud); \ + (client_ptr)->callback((&client_ptr->base), (client_ptr)->callback##_ud); \ } \ } while (false) #define MQTT_CLIENT_CALL_CALLBACK_ARGS(client_ptr, callback, ...) \ do { \ if ((client_ptr)->callback) { \ - (client_ptr)->callback((client_ptr), __VA_ARGS__, (client_ptr)->callback##_ud); \ + (client_ptr)->callback((&client_ptr->base), __VA_ARGS__, (client_ptr)->callback##_ud); \ } \ } while (false) @@ -101,7 +104,8 @@ typedef enum aws_mqtt_client_request_state( /** * Called when the operation statistics change. */ -typedef void(aws_mqtt_on_operation_statistics_fn)(struct aws_mqtt_client_connection *connection, void *userdata); +typedef void( + aws_mqtt_on_operation_statistics_fn)(struct aws_mqtt_client_connection_311_impl *connection, void *userdata); /* Flags that indicate the way in which way an operation is currently affecting the statistics of the connection */ enum aws_mqtt_operation_statistic_state_flags { @@ -119,7 +123,7 @@ struct aws_mqtt_request { struct aws_linked_list_node list_node; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct aws_channel_task outgoing_task; @@ -146,7 +150,7 @@ struct aws_mqtt_reconnect_task { /* The lifetime of this struct is from subscribe -> suback */ struct subscribe_task_arg { - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; /* list of pointer of subscribe_task_topics */ struct aws_array_list topics; @@ -166,7 +170,7 @@ struct subscribe_task_arg { /* The lifetime of this struct is the same as the lifetime of the subscription */ struct subscribe_task_topic { - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct aws_mqtt_topic_subscription request; struct aws_string *filter; @@ -175,10 +179,11 @@ struct subscribe_task_topic { struct aws_ref_count ref_count; }; -struct aws_mqtt_client_connection { - +struct aws_mqtt_client_connection_311_impl { struct aws_allocator *allocator; - struct aws_ref_count ref_count; + + struct aws_mqtt_client_connection base; + struct aws_mqtt_client *client; /* Channel handler information */ @@ -328,15 +333,15 @@ struct aws_channel_handler_vtable *aws_mqtt_get_client_channel_vtable(void); /* Helper for getting a message object for a packet */ struct aws_io_message *mqtt_get_message_for_packet( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_mqtt_fixed_header *header); -void mqtt_connection_lock_synced_data(struct aws_mqtt_client_connection *connection); -void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection *connection); +void mqtt_connection_lock_synced_data(struct aws_mqtt_client_connection_311_impl *connection); +void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection_311_impl *connection); /* Note: needs to be called with lock held. */ void mqtt_connection_set_state( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, enum aws_mqtt_client_connection_state state); /** @@ -346,7 +351,7 @@ void mqtt_connection_set_state( * noRetry is true for the packets will never be retried or offline queued. */ AWS_MQTT_API uint16_t mqtt_create_request( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_send_request_fn *send_request, void *send_request_ud, aws_mqtt_op_complete_fn *on_complete, @@ -356,15 +361,15 @@ AWS_MQTT_API uint16_t mqtt_create_request( /* Call when an ack packet comes back from the server. */ AWS_MQTT_API void mqtt_request_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, int error_code, uint16_t packet_id); /* Call to close the connection with an error code */ -AWS_MQTT_API void mqtt_disconnect_impl(struct aws_mqtt_client_connection *connection, int error_code); +AWS_MQTT_API void mqtt_disconnect_impl(struct aws_mqtt_client_connection_311_impl *connection, int error_code); /* Creates the task used to reestablish a broken connection */ -AWS_MQTT_API void aws_create_reconnect_task(struct aws_mqtt_client_connection *connection); +AWS_MQTT_API void aws_create_reconnect_task(struct aws_mqtt_client_connection_311_impl *connection); /** * Sets the callback to call whenever the operation statistics change. @@ -374,7 +379,7 @@ AWS_MQTT_API void aws_create_reconnect_task(struct aws_mqtt_client_connection *c * \param[in] on_operation_statistics_ud Userdata for on_operation_statistics */ AWS_MQTT_API int aws_mqtt_client_connection_set_on_operation_statistics_handler( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_on_operation_statistics_fn *on_operation_statistics, void *on_operation_statistics_ud); @@ -388,7 +393,7 @@ AWS_MQTT_API int aws_mqtt_client_connection_set_on_operation_statistics_handler( * \returns AWS_OP_SUCCESS if the connection is open and the PINGREQ is sent or queued to send, * otherwise AWS_OP_ERR and aws_last_error() is set. */ -int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connection); +int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection_311_impl *connection); /** * Changes the operation statistics for the passed-in aws_mqtt_request. Used for tracking @@ -402,7 +407,7 @@ int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connectio * @param new_state_flags The new state to use */ void aws_mqtt_connection_statistics_change_operation_statistic_state( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_mqtt_request *request, enum aws_mqtt_operation_statistic_state_flags new_state_flags); diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h new file mode 100644 index 00000000..ccef756b --- /dev/null +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -0,0 +1,111 @@ +#ifndef AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H +#define AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +struct aws_mqtt_client_connection; + +struct aws_mqtt_client_connection_vtable { + + int (*set_will_fn)( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload); + + int (*set_login_fn)(void *impl, const struct aws_byte_cursor *username, const struct aws_byte_cursor *password); + + int (*use_websockets_fn)( + void *impl, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_ud, + aws_mqtt_validate_websocket_handshake_fn *validator, + void *validator_ud); + + int (*set_http_proxy_options_fn)(void *impl, struct aws_http_proxy_options *proxy_options); + + int (*set_host_resolution_options_fn)(void *impl, struct aws_host_resolution_config *host_resolution_config); + + int (*set_reconnect_timeout_fn)(void *impl, uint64_t min_timeout, uint64_t max_timeout); + + int (*set_connection_interruption_handlers_fn)( + void *impl, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_ud, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_ud); + + int (*set_connection_closed_handler_fn)( + void *impl, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_ud); + + int (*set_on_any_publish_handler_fn)( + void *impl, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_ud); + + int (*connect_fn)(void *impl, const struct aws_mqtt_connection_options *connection_options); + + int (*reconnect_fn)(void *impl, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, void *userdata); + + int (*disconnect_fn)(void *impl, aws_mqtt_client_on_disconnect_fn *on_disconnect, void *userdata); + + uint16_t (*subscribe_multiple_fn)( + void *impl, + const struct aws_array_list *topic_filters, + aws_mqtt_suback_multi_fn *on_suback, + void *on_suback_ud); + + uint16_t (*subscribe_fn)( + void *impl, + const struct aws_byte_cursor *topic_filter, + enum aws_mqtt_qos qos, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud); + + uint16_t (*subscribe_local_fn)( + void *impl, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud); + + uint16_t (*resubscribe_existing_topics_fn)(void *impl, aws_mqtt_suback_multi_fn *on_suback, void *on_suback_ud); + + uint16_t (*unsubscribe_fn)( + void *impl, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_op_complete_fn *on_unsuback, + void *on_unsuback_ud); + + uint16_t (*publish_fn)( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload, + aws_mqtt_op_complete_fn *on_complete, + void *userdata); + + int (*get_stats_fn)(void *impl, struct aws_mqtt_connection_operation_statistics *stats); +}; + +struct aws_mqtt_client_connection { + struct aws_mqtt_client_connection_vtable *vtable; + void *impl; + struct aws_ref_count ref_count; +}; + +#endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H */ diff --git a/include/aws/mqtt/private/mqtt_client_test_helper.h b/include/aws/mqtt/private/mqtt_client_test_helper.h index 9dc2f129..c3c430d2 100644 --- a/include/aws/mqtt/private/mqtt_client_test_helper.h +++ b/include/aws/mqtt/private/mqtt_client_test_helper.h @@ -10,7 +10,7 @@ struct aws_allocator; struct aws_byte_cursor; -struct aws_mqtt_client_connection; +struct aws_mqtt_client_connection_311_impl; struct aws_string; AWS_EXTERN_C_BEGIN diff --git a/source/client.c b/source/client.c index c028effc..7a07343f 100644 --- a/source/client.c +++ b/source/client.c @@ -40,20 +40,20 @@ static const uint64_t s_default_ping_timeout_ns = 3000000000; static const uint16_t s_default_keep_alive_sec = 1200; static int s_mqtt_client_connect( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, void *userdata); /******************************************************************************* * Helper functions ******************************************************************************/ -void mqtt_connection_lock_synced_data(struct aws_mqtt_client_connection *connection) { +void mqtt_connection_lock_synced_data(struct aws_mqtt_client_connection_311_impl *connection) { int err = aws_mutex_lock(&connection->synced_data.lock); AWS_ASSERT(!err); (void)err; } -void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection *connection) { +void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection_311_impl *connection) { ASSERT_SYNCED_DATA_LOCK_HELD(connection); int err = aws_mutex_unlock(&connection->synced_data.lock); @@ -61,7 +61,7 @@ void mqtt_connection_unlock_synced_data(struct aws_mqtt_client_connection *conne (void)err; } -static void s_aws_mqtt_schedule_reconnect_task(struct aws_mqtt_client_connection *connection) { +static void s_aws_mqtt_schedule_reconnect_task(struct aws_mqtt_client_connection_311_impl *connection) { uint64_t next_attempt_ns = 0; aws_high_res_clock_get_ticks(&next_attempt_ns); next_attempt_ns += aws_timestamp_convert( @@ -84,7 +84,7 @@ static void s_aws_mqtt_client_destroy(struct aws_mqtt_client *client) { } void mqtt_connection_set_state( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, enum aws_mqtt_client_connection_state state) { ASSERT_SYNCED_DATA_LOCK_HELD(connection); if (connection->synced_data.state == state) { @@ -99,7 +99,7 @@ struct request_timeout_wrapper; /* used for timeout task */ struct request_timeout_task_arg { uint16_t packet_id; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct request_timeout_wrapper *task_arg_wrapper; }; @@ -116,7 +116,7 @@ struct request_timeout_wrapper { static void s_request_timeout(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status) { (void)channel_task; struct request_timeout_task_arg *timeout_task_arg = arg; - struct aws_mqtt_client_connection *connection = timeout_task_arg->connection; + struct aws_mqtt_client_connection_311_impl *connection = timeout_task_arg->connection; if (status == AWS_TASK_STATUS_RUN_READY) { if (timeout_task_arg->task_arg_wrapper != NULL) { @@ -139,7 +139,7 @@ static void s_request_timeout(struct aws_channel_task *channel_task, void *arg, } static struct request_timeout_task_arg *s_schedule_timeout_task( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, uint16_t packet_id) { /* schedule a timeout task to run, in case server consider the publish is not received */ struct aws_channel_task *request_timeout_task = NULL; @@ -259,7 +259,7 @@ static void s_mqtt_client_shutdown( (void)bootstrap; (void)channel; - struct aws_mqtt_client_connection *connection = user_data; + struct aws_mqtt_client_connection_311_impl *connection = user_data; AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Channel has been shutdown with error code %d", (void *)connection, error_code); @@ -366,7 +366,7 @@ static void s_mqtt_client_shutdown( struct aws_mqtt_request *request = AWS_CONTAINER_OF(current, struct aws_mqtt_request, list_node); if (request->on_complete) { request->on_complete( - connection, + &connection->base, request->packet_id, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION, request->on_complete_ud); @@ -463,7 +463,7 @@ static void s_mqtt_client_shutdown( break; } /* The connection can die now. Release the refcount */ - aws_mqtt_client_connection_release(connection); + aws_mqtt_client_connection_release(&connection->base); } } @@ -475,7 +475,7 @@ static void s_mqtt_client_shutdown( * for a CONNACK, kill it off. In the case that the connection died between scheduling this task and it being executed * the status will always be CANCELED because this task will be canceled when the owning channel goes away. */ static void s_connack_received_timeout(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status) { - struct aws_mqtt_client_connection *connection = arg; + struct aws_mqtt_client_connection_311_impl *connection = arg; if (status == AWS_TASK_STATUS_RUN_READY) { bool time_out = false; @@ -511,7 +511,7 @@ static void s_mqtt_client_init( /* Setup callback contract is: if error_code is non-zero then channel is NULL. */ AWS_FATAL_ASSERT((error_code != 0) == (channel == NULL)); - struct aws_mqtt_client_connection *connection = user_data; + struct aws_mqtt_client_connection_311_impl *connection = user_data; if (error_code != AWS_OP_SUCCESS) { /* client shutdown already handles this case, so just call that. */ @@ -686,7 +686,7 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ (void)task; struct aws_mqtt_reconnect_task *reconnect = userdata; - struct aws_mqtt_client_connection *connection = aws_atomic_load_ptr(&reconnect->connection_ptr); + struct aws_mqtt_client_connection_311_impl *connection = aws_atomic_load_ptr(&reconnect->connection_ptr); /* If the task is not cancelled and a connection has not succeeded, attempt reconnect */ if (status == AWS_TASK_STATUS_RUN_READY && connection) { @@ -726,7 +726,7 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ if (perform_full_destroy) { MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); - aws_mqtt_client_connection_release(connection); + aws_mqtt_client_connection_release(&connection->base); } return; } @@ -765,7 +765,7 @@ static void s_attempt_reconnect(struct aws_task *task, void *userdata, enum aws_ } } -void aws_create_reconnect_task(struct aws_mqtt_client_connection *connection) { +void aws_create_reconnect_task(struct aws_mqtt_client_connection_311_impl *connection) { if (connection->reconnect_task == NULL) { connection->reconnect_task = aws_mem_calloc(connection->allocator, 1, sizeof(struct aws_mqtt_reconnect_task)); AWS_FATAL_ASSERT(connection->reconnect_task != NULL); @@ -785,7 +785,9 @@ static bool s_uint16_t_eq(const void *a, const void *b) { return *(uint16_t *)a == *(uint16_t *)b; } -static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connection *connection) { +static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connection *base_connection) { + + struct aws_mqtt_client_connection_311_impl *connection = base_connection->impl; AWS_PRECONDITION(!connection || connection->allocator); if (!connection) { return; @@ -829,7 +831,7 @@ static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connec /* Fire the callback and clean up the memory, as the connection get destroyed. */ if (request->on_complete) { request->on_complete( - connection, request->packet_id, AWS_ERROR_MQTT_CONNECTION_DESTROYED, request->on_complete_ud); + &connection->base, request->packet_id, AWS_ERROR_MQTT_CONNECTION_DESTROYED, request->on_complete_ud); } aws_memory_pool_release(&connection->synced_data.requests_pool, request); } @@ -857,7 +859,7 @@ static void s_on_final_disconnect(struct aws_mqtt_client_connection *connection, s_mqtt_client_connection_destroy_final(connection); } -static void s_mqtt_client_connection_start_destroy(struct aws_mqtt_client_connection *connection) { +static void s_mqtt_client_connection_start_destroy(struct aws_mqtt_client_connection_311_impl *connection) { bool call_destroy_final = false; AWS_LOGF_DEBUG( @@ -889,125 +891,7 @@ static void s_mqtt_client_connection_start_destroy(struct aws_mqtt_client_connec } /* END CRITICAL SECTION */ if (call_destroy_final) { - s_mqtt_client_connection_destroy_final(connection); - } -} - -struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqtt_client *client) { - AWS_PRECONDITION(client); - - struct aws_mqtt_client_connection *connection = - aws_mem_calloc(client->allocator, 1, sizeof(struct aws_mqtt_client_connection)); - if (!connection) { - return NULL; - } - - AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Creating new connection", (void *)connection); - - /* Initialize the client */ - connection->allocator = client->allocator; - aws_ref_count_init( - &connection->ref_count, connection, (aws_simple_completion_callback *)s_mqtt_client_connection_start_destroy); - connection->client = aws_mqtt_client_acquire(client); - AWS_ZERO_STRUCT(connection->synced_data); - connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; - connection->reconnect_timeouts.min_sec = 1; - connection->reconnect_timeouts.current_sec = 1; - connection->reconnect_timeouts.max_sec = 128; - aws_linked_list_init(&connection->synced_data.pending_requests_list); - aws_linked_list_init(&connection->thread_data.ongoing_requests_list); - s_init_statistics(&connection->operation_statistics_impl); - - if (aws_mutex_init(&connection->synced_data.lock)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Failed to initialize mutex, error %d (%s)", - (void *)connection, - aws_last_error(), - aws_error_name(aws_last_error())); - goto failed_init_mutex; - } - - if (aws_mqtt_topic_tree_init(&connection->thread_data.subscriptions, connection->allocator)) { - - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Failed to initialize subscriptions topic_tree, error %d (%s)", - (void *)connection, - aws_last_error(), - aws_error_name(aws_last_error())); - goto failed_init_subscriptions; - } - - if (aws_memory_pool_init( - &connection->synced_data.requests_pool, connection->allocator, 32, sizeof(struct aws_mqtt_request))) { - - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Failed to initialize request pool, error %d (%s)", - (void *)connection, - aws_last_error(), - aws_error_name(aws_last_error())); - goto failed_init_requests_pool; - } - - if (aws_hash_table_init( - &connection->synced_data.outstanding_requests_table, - connection->allocator, - sizeof(struct aws_mqtt_request *), - s_hash_uint16_t, - s_uint16_t_eq, - NULL, - NULL)) { - - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Failed to initialize outstanding requests table, error %d (%s)", - (void *)connection, - aws_last_error(), - aws_error_name(aws_last_error())); - goto failed_init_outstanding_requests_table; - } - - connection->loop = aws_event_loop_group_get_next_loop(client->bootstrap->event_loop_group); - - connection->host_resolution_config = aws_host_resolver_init_default_resolution_config(); - connection->host_resolution_config.resolve_frequency_ns = - aws_timestamp_convert(connection->reconnect_timeouts.max_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); - - /* Initialize the handler */ - connection->handler.alloc = connection->allocator; - connection->handler.vtable = aws_mqtt_get_client_channel_vtable(); - connection->handler.impl = connection; - - return connection; - -failed_init_outstanding_requests_table: - aws_memory_pool_clean_up(&connection->synced_data.requests_pool); - -failed_init_requests_pool: - aws_mqtt_topic_tree_clean_up(&connection->thread_data.subscriptions); - -failed_init_subscriptions: - aws_mutex_clean_up(&connection->synced_data.lock); - -failed_init_mutex: - aws_mem_release(client->allocator, connection); - - return NULL; -} - -struct aws_mqtt_client_connection *aws_mqtt_client_connection_acquire(struct aws_mqtt_client_connection *connection) { - if (connection != NULL) { - aws_ref_count_acquire(&connection->ref_count); - } - - return connection; -} - -void aws_mqtt_client_connection_release(struct aws_mqtt_client_connection *connection) { - if (connection != NULL) { - aws_ref_count_release(&connection->ref_count); + s_mqtt_client_connection_destroy_final(&connection->base); } } @@ -1016,7 +900,7 @@ void aws_mqtt_client_connection_release(struct aws_mqtt_client_connection *conne ******************************************************************************/ /* To configure the connection, ensure the state is DISCONNECTED or CONNECTED */ -static int s_check_connection_state_for_configuration(struct aws_mqtt_client_connection *connection) { +static int s_check_connection_state_for_configuration(struct aws_mqtt_client_connection_311_impl *connection) { int result = AWS_OP_SUCCESS; { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); @@ -1035,13 +919,15 @@ static int s_check_connection_state_for_configuration(struct aws_mqtt_client_con return result; } -int aws_mqtt_client_connection_set_will( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_will( + void *impl, const struct aws_byte_cursor *topic, enum aws_mqtt_qos qos, bool retain, const struct aws_byte_cursor *payload) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); AWS_PRECONDITION(topic); if (s_check_connection_state_for_configuration(connection)) { @@ -1100,11 +986,13 @@ int aws_mqtt_client_connection_set_will( return result; } -int aws_mqtt_client_connection_set_login( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_login( + void *impl, const struct aws_byte_cursor *username, const struct aws_byte_cursor *password) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); AWS_PRECONDITION(username); if (s_check_connection_state_for_configuration(connection)) { @@ -1153,11 +1041,13 @@ int aws_mqtt_client_connection_set_login( return result; } -int aws_mqtt_client_connection_set_reconnect_timeout( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_reconnect_timeout( + void *impl, uint64_t min_timeout, uint64_t max_timeout) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (s_check_connection_state_for_configuration(connection)) { return aws_raise_error(AWS_ERROR_INVALID_STATE); @@ -1175,13 +1065,15 @@ int aws_mqtt_client_connection_set_reconnect_timeout( return AWS_OP_SUCCESS; } -int aws_mqtt_client_connection_set_connection_interruption_handlers( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_connection_interruption_handlers( + void *impl, aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, void *on_interrupted_ud, aws_mqtt_client_on_connection_resumed_fn *on_resumed, void *on_resumed_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (s_check_connection_state_for_configuration(connection)) { return aws_raise_error(AWS_ERROR_INVALID_STATE); @@ -1197,11 +1089,13 @@ int aws_mqtt_client_connection_set_connection_interruption_handlers( return AWS_OP_SUCCESS; } -int aws_mqtt_client_connection_set_connection_closed_handler( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_connection_closed_handler( + void *impl, aws_mqtt_client_on_connection_closed_fn *on_closed, void *on_closed_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (s_check_connection_state_for_configuration(connection)) { return aws_raise_error(AWS_ERROR_INVALID_STATE); @@ -1214,11 +1108,13 @@ int aws_mqtt_client_connection_set_connection_closed_handler( return AWS_OP_SUCCESS; } -int aws_mqtt_client_connection_set_on_any_publish_handler( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_on_any_publish_handler( + void *impl, aws_mqtt_client_publish_received_fn *on_any_publish, void *on_any_publish_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); { /* BEGIN CRITICAL SECTION */ mqtt_connection_lock_synced_data(connection); @@ -1248,13 +1144,15 @@ int aws_mqtt_client_connection_set_on_any_publish_handler( ******************************************************************************/ #ifdef AWS_MQTT_WITH_WEBSOCKETS -int aws_mqtt_client_connection_use_websockets( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_use_websockets( + void *impl, aws_mqtt_transform_websocket_handshake_fn *transformer, void *transformer_ud, aws_mqtt_validate_websocket_handshake_fn *validator, void *validator_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + connection->websocket.handshake_transformer = transformer; connection->websocket.handshake_transformer_ud = transformer_ud; connection->websocket.handshake_validator = validator; @@ -1266,10 +1164,12 @@ int aws_mqtt_client_connection_use_websockets( return AWS_OP_SUCCESS; } -int aws_mqtt_client_connection_set_http_proxy_options( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_http_proxy_options( + void *impl, struct aws_http_proxy_options *proxy_options) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + /* If there is existing proxy options, nuke em */ if (connection->http_proxy_config) { aws_http_proxy_config_destroy(connection->http_proxy_config); @@ -1282,17 +1182,19 @@ int aws_mqtt_client_connection_set_http_proxy_options( return connection->http_proxy_config != NULL ? AWS_OP_SUCCESS : AWS_OP_ERR; } -int aws_mqtt_client_connection_set_host_resolution_options( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_set_host_resolution_options( + void *impl, struct aws_host_resolution_config *host_resolution_config) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + connection->host_resolution_config = *host_resolution_config; return AWS_OP_SUCCESS; } static void s_on_websocket_shutdown(struct aws_websocket *websocket, int error_code, void *user_data) { - struct aws_mqtt_client_connection *connection = user_data; + struct aws_mqtt_client_connection_311_impl *connection = user_data; struct aws_channel *channel = connection->slot ? connection->slot->channel : NULL; @@ -1308,7 +1210,7 @@ static void s_on_websocket_setup(const struct aws_websocket_on_connection_setup_ /* Setup callback contract is: if error_code is non-zero then websocket is NULL. */ AWS_FATAL_ASSERT((setup->error_code != 0) == (setup->websocket == NULL)); - struct aws_mqtt_client_connection *connection = user_data; + struct aws_mqtt_client_connection_311_impl *connection = user_data; struct aws_channel *channel = NULL; if (connection->websocket.handshake_request) { @@ -1339,7 +1241,7 @@ static void s_on_websocket_setup(const struct aws_websocket_on_connection_setup_ AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Validating websocket handshake response.", (void *)connection); if (connection->websocket.handshake_validator( - connection, + &connection->base, setup->handshake_response_header_array, setup->num_handshake_response_headers, connection->websocket.handshake_validator_ud)) { @@ -1366,7 +1268,7 @@ static void s_on_websocket_setup(const struct aws_websocket_on_connection_setup_ static aws_mqtt_transform_websocket_handshake_complete_fn s_websocket_handshake_transform_complete; /* fwd declare */ -static int s_websocket_connect(struct aws_mqtt_client_connection *connection) { +static int s_websocket_connect(struct aws_mqtt_client_connection_311_impl *connection) { AWS_ASSERT(connection->websocket.enabled); /* Build websocket handshake request */ @@ -1413,7 +1315,7 @@ static void s_websocket_handshake_transform_complete( int error_code, void *complete_ctx) { - struct aws_mqtt_client_connection *connection = complete_ctx; + struct aws_mqtt_client_connection_311_impl *connection = complete_ctx; if (error_code) { AWS_LOGF_ERROR( @@ -1469,7 +1371,7 @@ error:; #else /* AWS_MQTT_WITH_WEBSOCKETS */ int aws_mqtt_client_connection_use_websockets( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_transform_websocket_handshake_fn *transformer, void *transformer_ud, aws_mqtt_validate_websocket_handshake_fn *validator, @@ -1490,7 +1392,7 @@ int aws_mqtt_client_connection_use_websockets( } int aws_mqtt_client_connection_set_websocket_proxy_options( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_http_proxy_options *proxy_options) { (void)connection; @@ -1509,10 +1411,12 @@ int aws_mqtt_client_connection_set_websocket_proxy_options( * Connect ******************************************************************************/ -int aws_mqtt_client_connection_connect( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_connect( + void *impl, const struct aws_mqtt_connection_options *connection_options) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + /* TODO: Do we need to support resuming the connection if user connect to the same connection & endpoint and the * clean_session is false? * If not, the broker will resume the connection in this case, and we pretend we are making a new connection, which @@ -1656,7 +1560,7 @@ int aws_mqtt_client_connection_connect( request->packet_id); if (request->on_complete) { request->on_complete( - connection, + &connection->base, request->packet_id, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION, request->on_complete_ud); @@ -1678,14 +1582,14 @@ int aws_mqtt_client_connection_connect( } /* Begin the connecting process, acquire the connection to keep it alive until we disconnected */ - aws_mqtt_client_connection_acquire(connection); + aws_mqtt_client_connection_acquire(&connection->base); if (s_mqtt_client_connect(connection, connection_options->on_connection_complete, connection_options->user_data)) { /* * An error calling s_mqtt_client_connect should (must) be mutually exclusive with s_mqtt_client_shutdown(). * So it should be safe and correct to call release now to undo the pinning we did a few lines above. */ - aws_mqtt_client_connection_release(connection); + aws_mqtt_client_connection_release(&connection->base); /* client_id has been updated with something but it will get cleaned up when the connection gets cleaned up * so we don't need to worry about it here*/ @@ -1710,7 +1614,7 @@ int aws_mqtt_client_connection_connect( } static int s_mqtt_client_connect( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, void *userdata) { connection->on_connection_complete = on_connection_complete; @@ -1765,11 +1669,11 @@ static int s_mqtt_client_connect( * Reconnect DEPRECATED ******************************************************************************/ -int aws_mqtt_client_connection_reconnect( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_reconnect( + void *impl, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, void *userdata) { - (void)connection; + (void)impl; (void)on_connection_complete; (void)userdata; @@ -1782,11 +1686,13 @@ int aws_mqtt_client_connection_reconnect( * Disconnect ******************************************************************************/ -int aws_mqtt_client_connection_disconnect( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_disconnect( + void *impl, aws_mqtt_client_on_disconnect_fn *on_disconnect, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: user called disconnect.", (void *)connection); { /* BEGIN CRITICAL SECTION */ @@ -1834,7 +1740,7 @@ static void s_on_publish_client_wrapper( /* Call out to the user callback */ if (task_topic->request.on_publish) { task_topic->request.on_publish( - task_topic->connection, topic, payload, dup, qos, retain, task_topic->request.on_publish_ud); + &task_topic->connection->base, topic, payload, dup, qos, retain, task_topic->request.on_publish_ud); } } @@ -1981,11 +1887,12 @@ static enum aws_mqtt_client_request_state s_subscribe_send(uint16_t packet_id, b } static void s_subscribe_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; struct subscribe_task_arg *task_arg = userdata; struct subscribe_task_topic *topic = NULL; @@ -2012,11 +1919,16 @@ static void s_subscribe_complete( err |= aws_array_list_push_back(&cb_list, &subscription); } AWS_ASSUME(!err); - task_arg->on_suback.multi(connection, packet_id, &cb_list, error_code, task_arg->on_suback_ud); + task_arg->on_suback.multi(&connection->base, packet_id, &cb_list, error_code, task_arg->on_suback_ud); aws_array_list_clean_up(&cb_list); } else if (task_arg->on_suback.single) { task_arg->on_suback.single( - connection, packet_id, &topic->request.topic, topic->request.qos, error_code, task_arg->on_suback_ud); + &connection->base, + packet_id, + &topic->request.topic, + topic->request.qos, + error_code, + task_arg->on_suback_ud); } for (size_t i = 0; i < list_len; i++) { aws_array_list_get_at(&task_arg->topics, &topic, i); @@ -2027,12 +1939,14 @@ static void s_subscribe_complete( aws_mem_release(task_arg->connection->allocator, task_arg); } -uint16_t aws_mqtt_client_connection_subscribe_multiple( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_client_connection_311_subscribe_multiple( + void *impl, const struct aws_array_list *topic_filters, aws_mqtt_suback_multi_fn *on_suback, void *on_suback_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); struct subscribe_task_arg *task_arg = aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_task_arg)); @@ -2151,11 +2065,12 @@ uint16_t aws_mqtt_client_connection_subscribe_multiple( ******************************************************************************/ static void s_subscribe_single_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; struct subscribe_task_arg *task_arg = userdata; AWS_LOGF_DEBUG( @@ -2172,7 +2087,13 @@ static void s_subscribe_single_complete( if (task_arg->on_suback.single) { AWS_ASSUME(aws_string_is_valid(topic->filter)); aws_mqtt_suback_fn *suback = task_arg->on_suback.single; - suback(connection, packet_id, &topic->request.topic, topic->request.qos, error_code, task_arg->on_suback_ud); + suback( + &connection->base, + packet_id, + &topic->request.topic, + topic->request.qos, + error_code, + task_arg->on_suback_ud); } s_task_topic_release(topic); aws_array_list_clean_up(&task_arg->topics); @@ -2180,8 +2101,8 @@ static void s_subscribe_single_complete( aws_mem_release(task_arg->connection->allocator, task_arg); } -uint16_t aws_mqtt_client_connection_subscribe( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_client_connection_311_subscribe( + void *impl, const struct aws_byte_cursor *topic_filter, enum aws_mqtt_qos qos, aws_mqtt_client_publish_received_fn *on_publish, @@ -2190,6 +2111,8 @@ uint16_t aws_mqtt_client_connection_subscribe( aws_mqtt_suback_fn *on_suback, void *on_suback_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { @@ -2299,7 +2222,7 @@ uint16_t aws_mqtt_client_connection_subscribe( /* The lifetime of this struct is from subscribe -> suback */ struct subscribe_local_task_arg { - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct subscribe_task_topic *task_topic; @@ -2364,11 +2287,13 @@ static enum aws_mqtt_client_request_state s_subscribe_local_send( } static void s_subscribe_local_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; + struct subscribe_local_task_arg *task_arg = userdata; AWS_LOGF_DEBUG( @@ -2381,15 +2306,21 @@ static void s_subscribe_local_complete( struct subscribe_task_topic *topic = task_arg->task_topic; if (task_arg->on_suback) { aws_mqtt_suback_fn *suback = task_arg->on_suback; - suback(connection, packet_id, &topic->request.topic, topic->request.qos, error_code, task_arg->on_suback_ud); + suback( + &connection->base, + packet_id, + &topic->request.topic, + topic->request.qos, + error_code, + task_arg->on_suback_ud); } s_task_topic_release(topic); aws_mem_release(task_arg->connection->allocator, task_arg); } -uint16_t aws_mqtt_client_connection_subscribe_local( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_client_connection_311_subscribe_local( + void *impl, const struct aws_byte_cursor *topic_filter, aws_mqtt_client_publish_received_fn *on_publish, void *on_publish_ud, @@ -2397,6 +2328,8 @@ uint16_t aws_mqtt_client_connection_subscribe_local( aws_mqtt_suback_fn *on_suback, void *on_suback_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { @@ -2601,11 +2534,13 @@ static enum aws_mqtt_client_request_state s_resubscribe_send( } static void s_resubscribe_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; + struct subscribe_task_arg *task_arg = userdata; const size_t list_len = aws_array_list_length(&task_arg->topics); @@ -2636,11 +2571,16 @@ static void s_resubscribe_complete( err |= aws_array_list_push_back(&cb_list, &subscription); } AWS_ASSUME(!err); - task_arg->on_suback.multi(connection, packet_id, &cb_list, error_code, task_arg->on_suback_ud); + task_arg->on_suback.multi(&connection->base, packet_id, &cb_list, error_code, task_arg->on_suback_ud); aws_array_list_clean_up(&cb_list); } else if (task_arg->on_suback.single) { task_arg->on_suback.single( - connection, packet_id, &topic->request.topic, topic->request.qos, error_code, task_arg->on_suback_ud); + &connection->base, + packet_id, + &topic->request.topic, + topic->request.qos, + error_code, + task_arg->on_suback_ud); } clean_up: @@ -2656,11 +2596,13 @@ static void s_resubscribe_complete( aws_mem_release(task_arg->connection->allocator, task_arg); } -uint16_t aws_mqtt_resubscribe_existing_topics( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_311_resubscribe_existing_topics( + void *impl, aws_mqtt_suback_multi_fn *on_suback, void *on_suback_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + struct subscribe_task_arg *task_arg = aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_task_arg)); if (!task_arg) { AWS_LOGF_ERROR( @@ -2718,7 +2660,7 @@ uint16_t aws_mqtt_resubscribe_existing_topics( ******************************************************************************/ struct unsubscribe_task_arg { - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct aws_string *filter_string; struct aws_byte_cursor filter; bool is_local; @@ -2862,11 +2804,13 @@ static enum aws_mqtt_client_request_state s_unsubscribe_send( } static void s_unsubscribe_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; + struct unsubscribe_task_arg *task_arg = userdata; AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Unsubscribe %" PRIu16 " complete", (void *)connection, packet_id); @@ -2883,7 +2827,7 @@ static void s_unsubscribe_complete( } if (task_arg->on_unsuback) { - task_arg->on_unsuback(connection, packet_id, error_code, task_arg->on_unsuback_ud); + task_arg->on_unsuback(&connection->base, packet_id, error_code, task_arg->on_unsuback_ud); } aws_string_destroy(task_arg->filter_string); @@ -2891,12 +2835,14 @@ static void s_unsubscribe_complete( aws_mem_release(task_arg->connection->allocator, task_arg); } -uint16_t aws_mqtt_client_connection_unsubscribe( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_client_connection_311_unsubscribe( + void *impl, const struct aws_byte_cursor *topic_filter, aws_mqtt_op_complete_fn *on_unsuback, void *on_unsuback_ud) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { @@ -2955,7 +2901,7 @@ uint16_t aws_mqtt_client_connection_unsubscribe( ******************************************************************************/ struct publish_task_arg { - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_311_impl *connection; struct aws_string *topic_string; struct aws_byte_cursor topic; enum aws_mqtt_qos qos; @@ -2974,7 +2920,7 @@ struct publish_task_arg { /* should only be called by tests */ static int s_get_stuff_from_outstanding_requests_table( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, uint16_t packet_id, struct aws_allocator *allocator, struct aws_byte_buf *result_buf, @@ -3009,29 +2955,29 @@ static int s_get_stuff_from_outstanding_requests_table( /* should only be called by tests */ int aws_mqtt_client_get_payload_for_outstanding_publish_packet( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, struct aws_allocator *allocator, struct aws_byte_buf *result) { AWS_ZERO_STRUCT(*result); - return s_get_stuff_from_outstanding_requests_table(connection, packet_id, allocator, result, NULL); + return s_get_stuff_from_outstanding_requests_table(connection_base->impl, packet_id, allocator, result, NULL); } /* should only be called by tests */ int aws_mqtt_client_get_topic_for_outstanding_publish_packet( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, struct aws_allocator *allocator, struct aws_string **result) { *result = NULL; - return s_get_stuff_from_outstanding_requests_table(connection, packet_id, allocator, NULL, result); + return s_get_stuff_from_outstanding_requests_table(connection_base->impl, packet_id, allocator, NULL, result); } static enum aws_mqtt_client_request_state s_publish_send(uint16_t packet_id, bool is_first_attempt, void *userdata) { struct publish_task_arg *task_arg = userdata; - struct aws_mqtt_client_connection *connection = task_arg->connection; + struct aws_mqtt_client_connection_311_impl *connection = task_arg->connection; AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, @@ -3123,16 +3069,19 @@ static enum aws_mqtt_client_request_state s_publish_send(uint16_t packet_id, boo } static void s_publish_complete( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection *connection_base, uint16_t packet_id, int error_code, void *userdata) { + + struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; + struct publish_task_arg *task_arg = userdata; AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Publish %" PRIu16 " complete", (void *)connection, packet_id); if (task_arg->on_complete) { - task_arg->on_complete(connection, packet_id, error_code, task_arg->userdata); + task_arg->on_complete(&connection->base, packet_id, error_code, task_arg->userdata); } /* @@ -3151,8 +3100,8 @@ static void s_publish_complete( aws_mem_release(connection->allocator, task_arg); } -uint16_t aws_mqtt_client_connection_publish( - struct aws_mqtt_client_connection *connection, +static uint16_t s_aws_mqtt_client_connection_311_publish( + void *impl, const struct aws_byte_cursor *topic, enum aws_mqtt_qos qos, bool retain, @@ -3160,6 +3109,8 @@ uint16_t aws_mqtt_client_connection_publish( aws_mqtt_op_complete_fn *on_complete, void *userdata) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + AWS_PRECONDITION(connection); if (!aws_mqtt_is_valid_topic(topic)) { @@ -3232,7 +3183,7 @@ uint16_t aws_mqtt_client_connection_publish( ******************************************************************************/ static void s_pingresp_received_timeout(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status) { - struct aws_mqtt_client_connection *connection = arg; + struct aws_mqtt_client_connection_311_impl *connection = arg; if (status == AWS_TASK_STATUS_RUN_READY) { /* Check that a pingresp has been received since pingreq was sent */ @@ -3252,7 +3203,7 @@ static enum aws_mqtt_client_request_state s_pingreq_send(uint16_t packet_id, boo (void)is_first_attempt; AWS_PRECONDITION(is_first_attempt); - struct aws_mqtt_client_connection *connection = userdata; + struct aws_mqtt_client_connection_311_impl *connection = userdata; AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: pingreq send", (void *)connection); struct aws_mqtt_packet_connection pingreq; @@ -3295,7 +3246,7 @@ static enum aws_mqtt_client_request_state s_pingreq_send(uint16_t packet_id, boo return AWS_MQTT_CLIENT_REQUEST_ERROR; } -int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connection) { +int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection_311_impl *connection) { AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Starting ping", (void *)connection); @@ -3312,7 +3263,7 @@ int aws_mqtt_client_connection_ping(struct aws_mqtt_client_connection *connectio ******************************************************************************/ void aws_mqtt_connection_statistics_change_operation_statistic_state( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_mqtt_request *request, enum aws_mqtt_operation_statistic_state_flags new_state_flags) { @@ -3370,9 +3321,12 @@ void aws_mqtt_connection_statistics_change_operation_statistic_state( } } -int aws_mqtt_client_connection_get_stats( - struct aws_mqtt_client_connection *connection, +static int s_aws_mqtt_client_connection_311_get_stats( + void *impl, struct aws_mqtt_connection_operation_statistics *stats) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + // Error checking if (!connection) { AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "Invalid MQTT311 connection used when trying to get operation statistics"); @@ -3399,7 +3353,7 @@ int aws_mqtt_client_connection_get_stats( } int aws_mqtt_client_connection_set_on_operation_statistics_handler( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_on_operation_statistics_fn *on_operation_statistics, void *on_operation_statistics_ud) { @@ -3410,3 +3364,136 @@ int aws_mqtt_client_connection_set_on_operation_statistics_handler( return AWS_OP_SUCCESS; } + +static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311_vtable = { + .set_will_fn = s_aws_mqtt_client_connection_311_set_will, + .set_login_fn = s_aws_mqtt_client_connection_311_set_login, + .use_websockets_fn = s_aws_mqtt_client_connection_311_use_websockets, + .set_http_proxy_options_fn = s_aws_mqtt_client_connection_311_set_http_proxy_options, + .set_host_resolution_options_fn = s_aws_mqtt_client_connection_311_set_host_resolution_options, + .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_311_set_reconnect_timeout, + .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_311_set_connection_interruption_handlers, + .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_311_set_connection_closed_handler, + .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_311_set_on_any_publish_handler, + .connect_fn = s_aws_mqtt_client_connection_311_connect, + .reconnect_fn = s_aws_mqtt_client_connection_311_reconnect, + .disconnect_fn = s_aws_mqtt_client_connection_311_disconnect, + .subscribe_multiple_fn = s_aws_mqtt_client_connection_311_subscribe_multiple, + .subscribe_fn = s_aws_mqtt_client_connection_311_subscribe, + .subscribe_local_fn = s_aws_mqtt_client_connection_311_subscribe_local, + .resubscribe_existing_topics_fn = s_aws_mqtt_311_resubscribe_existing_topics, + .unsubscribe_fn = s_aws_mqtt_client_connection_311_unsubscribe, + .publish_fn = s_aws_mqtt_client_connection_311_publish, + .get_stats_fn = s_aws_mqtt_client_connection_311_get_stats, +}; + +static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_311_vtable_ptr = + &s_aws_mqtt_client_connection_311_vtable; + +struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqtt_client *client) { + AWS_PRECONDITION(client); + + struct aws_mqtt_client_connection_311_impl *connection = + aws_mem_calloc(client->allocator, 1, sizeof(struct aws_mqtt_client_connection_311_impl)); + if (!connection) { + return NULL; + } + + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Creating new mqtt 311 connection", (void *)connection); + + /* Initialize the client */ + connection->allocator = client->allocator; + connection->base.vtable = s_aws_mqtt_client_connection_311_vtable_ptr; + connection->base.impl = connection; + aws_ref_count_init( + &connection->base.ref_count, + connection, + (aws_simple_completion_callback *)s_mqtt_client_connection_start_destroy); + connection->client = aws_mqtt_client_acquire(client); + AWS_ZERO_STRUCT(connection->synced_data); + connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; + connection->reconnect_timeouts.min_sec = 1; + connection->reconnect_timeouts.current_sec = 1; + connection->reconnect_timeouts.max_sec = 128; + aws_linked_list_init(&connection->synced_data.pending_requests_list); + aws_linked_list_init(&connection->thread_data.ongoing_requests_list); + s_init_statistics(&connection->operation_statistics_impl); + + if (aws_mutex_init(&connection->synced_data.lock)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Failed to initialize mutex, error %d (%s)", + (void *)connection, + aws_last_error(), + aws_error_name(aws_last_error())); + goto failed_init_mutex; + } + + if (aws_mqtt_topic_tree_init(&connection->thread_data.subscriptions, connection->allocator)) { + + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Failed to initialize subscriptions topic_tree, error %d (%s)", + (void *)connection, + aws_last_error(), + aws_error_name(aws_last_error())); + goto failed_init_subscriptions; + } + + if (aws_memory_pool_init( + &connection->synced_data.requests_pool, connection->allocator, 32, sizeof(struct aws_mqtt_request))) { + + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Failed to initialize request pool, error %d (%s)", + (void *)connection, + aws_last_error(), + aws_error_name(aws_last_error())); + goto failed_init_requests_pool; + } + + if (aws_hash_table_init( + &connection->synced_data.outstanding_requests_table, + connection->allocator, + sizeof(struct aws_mqtt_request *), + s_hash_uint16_t, + s_uint16_t_eq, + NULL, + NULL)) { + + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Failed to initialize outstanding requests table, error %d (%s)", + (void *)connection, + aws_last_error(), + aws_error_name(aws_last_error())); + goto failed_init_outstanding_requests_table; + } + + connection->loop = aws_event_loop_group_get_next_loop(client->bootstrap->event_loop_group); + + connection->host_resolution_config = aws_host_resolver_init_default_resolution_config(); + connection->host_resolution_config.resolve_frequency_ns = + aws_timestamp_convert(connection->reconnect_timeouts.max_sec, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + + /* Initialize the handler */ + connection->handler.alloc = connection->allocator; + connection->handler.vtable = aws_mqtt_get_client_channel_vtable(); + connection->handler.impl = connection; + + return &connection->base; + +failed_init_outstanding_requests_table: + aws_memory_pool_clean_up(&connection->synced_data.requests_pool); + +failed_init_requests_pool: + aws_mqtt_topic_tree_clean_up(&connection->thread_data.subscriptions); + +failed_init_subscriptions: + aws_mutex_clean_up(&connection->synced_data.lock); + +failed_init_mutex: + aws_mem_release(client->allocator, connection); + + return NULL; +} diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 62b5b063..fa2237bc 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -25,7 +25,7 @@ ******************************************************************************/ /* Caches the socket write time for ping scheduling purposes */ -static void s_update_next_ping_time(struct aws_mqtt_client_connection *connection) { +static void s_update_next_ping_time(struct aws_mqtt_client_connection_311_impl *connection) { if (connection->slot != NULL && connection->slot->channel != NULL) { aws_channel_current_clock_time(connection->slot->channel, &connection->next_ping_time); aws_add_u64_checked(connection->next_ping_time, connection->keep_alive_time_ns, &connection->next_ping_time); @@ -36,10 +36,11 @@ static void s_update_next_ping_time(struct aws_mqtt_client_connection *connectio * Packet State Machine ******************************************************************************/ -typedef int(packet_handler_fn)(struct aws_mqtt_client_connection *connection, struct aws_byte_cursor message_cursor); +typedef int( + packet_handler_fn)(struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor); static int s_packet_handler_default( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { (void)connection; (void)message_cursor; @@ -49,7 +50,7 @@ static int s_packet_handler_default( } static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status); -static void s_schedule_ping(struct aws_mqtt_client_connection *connection) { +static void s_schedule_ping(struct aws_mqtt_client_connection_311_impl *connection) { aws_channel_task_init(&connection->ping_task, s_on_time_to_ping, connection, "mqtt_ping"); uint64_t now = 0; @@ -71,7 +72,7 @@ static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, (void)channel_task; if (status == AWS_TASK_STATUS_RUN_READY) { - struct aws_mqtt_client_connection *connection = arg; + struct aws_mqtt_client_connection_311_impl *connection = arg; uint64_t now = 0; aws_channel_current_clock_time(connection->slot->channel, &now); @@ -94,7 +95,7 @@ static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, } } static int s_packet_handler_connack( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: CONNACK received", (void *)connection); @@ -208,7 +209,7 @@ static int s_packet_handler_connack( } static int s_packet_handler_publish( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { /* TODO: need to handle the QoS 2 message to avoid processing the message a second time */ @@ -279,7 +280,9 @@ static int s_packet_handler_publish( return AWS_OP_SUCCESS; } -static int s_packet_handler_ack(struct aws_mqtt_client_connection *connection, struct aws_byte_cursor message_cursor) { +static int s_packet_handler_ack( + struct aws_mqtt_client_connection_311_impl *connection, + struct aws_byte_cursor message_cursor) { struct aws_mqtt_packet_ack ack; if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { return AWS_OP_ERR; @@ -294,7 +297,7 @@ static int s_packet_handler_ack(struct aws_mqtt_client_connection *connection, s } static int s_packet_handler_suback( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { struct aws_mqtt_packet_suback suback; if (aws_mqtt_packet_suback_init(&suback, connection->allocator, 0 /* fake packet_id */)) { @@ -354,7 +357,7 @@ static int s_packet_handler_suback( } static int s_packet_handler_pubrec( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { struct aws_mqtt_packet_ack ack; @@ -392,7 +395,7 @@ static int s_packet_handler_pubrec( } static int s_packet_handler_pubrel( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { struct aws_mqtt_packet_ack ack; @@ -427,7 +430,7 @@ static int s_packet_handler_pubrel( } static int s_packet_handler_pingresp( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor) { (void)message_cursor; @@ -462,7 +465,7 @@ static packet_handler_fn *s_packet_handlers[] = { ******************************************************************************/ static int s_process_mqtt_packet( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, enum aws_mqtt_packet_type packet_type, struct aws_byte_cursor packet) { { /* BEGIN CRITICAL SECTION */ @@ -502,7 +505,7 @@ static int s_process_read_message( struct aws_channel_slot *slot, struct aws_io_message *message) { - struct aws_mqtt_client_connection *connection = handler->impl; + struct aws_mqtt_client_connection_311_impl *connection = handler->impl; if (message->message_type != AWS_IO_MESSAGE_APPLICATION_DATA || message->message_data.len < 1) { return AWS_OP_ERR; @@ -623,7 +626,7 @@ static int s_shutdown( int error_code, bool free_scarce_resources_immediately) { - struct aws_mqtt_client_connection *connection = handler->impl; + struct aws_mqtt_client_connection_311_impl *connection = handler->impl; if (dir == AWS_CHANNEL_DIR_WRITE) { /* On closing write direction, send out disconnect packet before closing connection. */ @@ -678,7 +681,7 @@ static size_t s_initial_window_size(struct aws_channel_handler *handler) { static void s_destroy(struct aws_channel_handler *handler) { - struct aws_mqtt_client_connection *connection = handler->impl; + struct aws_mqtt_client_connection_311_impl *connection = handler->impl; (void)connection; } @@ -707,7 +710,7 @@ struct aws_channel_handler_vtable *aws_mqtt_get_client_channel_vtable(void) { ******************************************************************************/ struct aws_io_message *mqtt_get_message_for_packet( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, struct aws_mqtt_fixed_header *header) { const size_t required_length = 3 + header->remaining_length; @@ -732,7 +735,7 @@ struct aws_io_message *mqtt_get_message_for_packet( static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, enum aws_task_status status) { struct aws_mqtt_request *request = arg; - struct aws_mqtt_client_connection *connection = request->connection; + struct aws_mqtt_client_connection_311_impl *connection = request->connection; if (status == AWS_TASK_STATUS_CANCELED) { /* Connection lost before the request ever get send, check the request needs to be retried or not */ @@ -767,7 +770,7 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en /* Fire the callback and clean up the memory, as the connection get destroyed. */ if (request->on_complete) { request->on_complete( - connection, request->packet_id, AWS_ERROR_MQTT_NOT_CONNECTED, request->on_complete_ud); + &connection->base, request->packet_id, AWS_ERROR_MQTT_NOT_CONNECTED, request->on_complete_ud); } { /* BEGIN CRITICAL SECTION */ @@ -811,7 +814,7 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en /* If the send_request function reports the request is complete, * remove from the hash table and call the callback. */ if (request->on_complete) { - request->on_complete(connection, request->packet_id, error_code, request->on_complete_ud); + request->on_complete(&connection->base, request->packet_id, error_code, request->on_complete_ud); } { /* BEGIN CRITICAL SECTION */ @@ -858,7 +861,7 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en } uint16_t mqtt_create_request( - struct aws_mqtt_client_connection *connection, + struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_send_request_fn *send_request, void *send_request_ud, aws_mqtt_op_complete_fn *on_complete, @@ -996,7 +999,7 @@ uint16_t mqtt_create_request( return next_request->packet_id; } -void mqtt_request_complete(struct aws_mqtt_client_connection *connection, int error_code, uint16_t packet_id) { +void mqtt_request_complete(struct aws_mqtt_client_connection_311_impl *connection, int error_code, uint16_t packet_id) { AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, @@ -1046,7 +1049,7 @@ void mqtt_request_complete(struct aws_mqtt_client_connection *connection, int er /* Invoke the complete callback. */ if (on_complete) { - on_complete(connection, packet_id, error_code, on_complete_ud); + on_complete(&connection->base, packet_id, error_code, on_complete_ud); } } @@ -1060,7 +1063,7 @@ static void s_mqtt_disconnect_task(struct aws_channel_task *channel_task, void * (void)status; struct mqtt_shutdown_task *task = AWS_CONTAINER_OF(channel_task, struct mqtt_shutdown_task, task); - struct aws_mqtt_client_connection *connection = arg; + struct aws_mqtt_client_connection_311_impl *connection = arg; AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Doing disconnect", (void *)connection); { /* BEGIN CRITICAL SECTION */ @@ -1084,7 +1087,7 @@ static void s_mqtt_disconnect_task(struct aws_channel_task *channel_task, void * aws_mem_release(connection->allocator, task); } -void mqtt_disconnect_impl(struct aws_mqtt_client_connection *connection, int error_code) { +void mqtt_disconnect_impl(struct aws_mqtt_client_connection_311_impl *connection, int error_code) { if (connection->slot) { struct mqtt_shutdown_task *shutdown_task = aws_mem_calloc(connection->allocator, 1, sizeof(struct mqtt_shutdown_task)); diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c new file mode 100644 index 00000000..cd68af41 --- /dev/null +++ b/source/client_impl_shared.c @@ -0,0 +1,194 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +struct aws_mqtt_client_connection *aws_mqtt_client_connection_acquire(struct aws_mqtt_client_connection *connection) { + if (connection != NULL) { + aws_ref_count_acquire(&connection->ref_count); + } + + return connection; +} + +void aws_mqtt_client_connection_release(struct aws_mqtt_client_connection *connection) { + if (connection != NULL) { + aws_ref_count_release(&connection->ref_count); + } +} + +int aws_mqtt_client_connection_set_will( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload) { + + return (*connection->vtable->set_will_fn)(connection->impl, topic, qos, retain, payload); +} + +int aws_mqtt_client_connection_set_login( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *username, + const struct aws_byte_cursor *password) { + + return (*connection->vtable->set_login_fn)(connection->impl, username, password); +} + +int aws_mqtt_client_connection_use_websockets( + struct aws_mqtt_client_connection *connection, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_ud, + aws_mqtt_validate_websocket_handshake_fn *validator, + void *validator_ud) { + + return (*connection->vtable->use_websockets_fn)( + connection->impl, transformer, transformer_ud, validator, validator_ud); +} + +int aws_mqtt_client_connection_set_http_proxy_options( + struct aws_mqtt_client_connection *connection, + struct aws_http_proxy_options *proxy_options) { + + return (*connection->vtable->set_http_proxy_options_fn)(connection->impl, proxy_options); +} + +int aws_mqtt_client_connection_set_host_resolution_options( + struct aws_mqtt_client_connection *connection, + struct aws_host_resolution_config *host_resolution_config) { + + return (*connection->vtable->set_host_resolution_options_fn)(connection->impl, host_resolution_config); +} + +int aws_mqtt_client_connection_set_reconnect_timeout( + struct aws_mqtt_client_connection *connection, + uint64_t min_timeout, + uint64_t max_timeout) { + + return (*connection->vtable->set_reconnect_timeout_fn)(connection->impl, min_timeout, max_timeout); +} + +int aws_mqtt_client_connection_set_connection_interruption_handlers( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_ud, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_ud) { + + return (*connection->vtable->set_connection_interruption_handlers_fn)( + connection->impl, on_interrupted, on_interrupted_ud, on_resumed, on_resumed_ud); +} + +int aws_mqtt_client_connection_set_connection_closed_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_ud) { + + return (*connection->vtable->set_connection_closed_handler_fn)(connection->impl, on_closed, on_closed_ud); +} + +int aws_mqtt_client_connection_set_on_any_publish_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_ud) { + + return (*connection->vtable->set_on_any_publish_handler_fn)(connection->impl, on_any_publish, on_any_publish_ud); +} + +int aws_mqtt_client_connection_connect( + struct aws_mqtt_client_connection *connection, + const struct aws_mqtt_connection_options *connection_options) { + + return (*connection->vtable->connect_fn)(connection->impl, connection_options); +} + +int aws_mqtt_client_connection_reconnect( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_complete_fn *on_connection_complete, + void *userdata) { + + return (*connection->vtable->reconnect_fn)(connection->impl, on_connection_complete, userdata); +} + +int aws_mqtt_client_connection_disconnect( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_disconnect_fn *on_disconnect, + void *userdata) { + + return (*connection->vtable->disconnect_fn)(connection->impl, on_disconnect, userdata); +} + +uint16_t aws_mqtt_client_connection_subscribe_multiple( + struct aws_mqtt_client_connection *connection, + const struct aws_array_list *topic_filters, + aws_mqtt_suback_multi_fn *on_suback, + void *on_suback_ud) { + + return (*connection->vtable->subscribe_multiple_fn)(connection->impl, topic_filters, on_suback, on_suback_ud); +} + +uint16_t aws_mqtt_client_connection_subscribe( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic_filter, + enum aws_mqtt_qos qos, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud) { + + return (*connection->vtable->subscribe_fn)( + connection->impl, topic_filter, qos, on_publish, on_publish_ud, on_ud_cleanup, on_suback, on_suback_ud); +} + +uint16_t aws_mqtt_client_connection_subscribe_local( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud) { + + return (*connection->vtable->subscribe_local_fn)( + connection->impl, topic_filter, on_publish, on_publish_ud, on_ud_cleanup, on_suback, on_suback_ud); +} + +uint16_t aws_mqtt_resubscribe_existing_topics( + struct aws_mqtt_client_connection *connection, + aws_mqtt_suback_multi_fn *on_suback, + void *on_suback_ud) { + + return (*connection->vtable->resubscribe_existing_topics_fn)(connection->impl, on_suback, on_suback_ud); +} + +uint16_t aws_mqtt_client_connection_unsubscribe( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_op_complete_fn *on_unsuback, + void *on_unsuback_ud) { + + return (*connection->vtable->unsubscribe_fn)(connection->impl, topic_filter, on_unsuback, on_unsuback_ud); +} + +uint16_t aws_mqtt_client_connection_publish( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload, + aws_mqtt_op_complete_fn *on_complete, + void *userdata) { + + return (*connection->vtable->publish_fn)(connection->impl, topic, qos, retain, payload, on_complete, userdata); +} + +int aws_mqtt_client_connection_get_stats( + struct aws_mqtt_client_connection *connection, + struct aws_mqtt_connection_operation_statistics *stats) { + + return (*connection->vtable->get_stats_fn)(connection->impl, stats); +} diff --git a/tests/v3/operation_statistics_test.c b/tests/v3/operation_statistics_test.c index 849277d7..c63d3423 100644 --- a/tests/v3/operation_statistics_test.c +++ b/tests/v3/operation_statistics_test.c @@ -1223,13 +1223,15 @@ AWS_TEST_CASE_FIXTURE( /* ========== OTHER TESTS ========== */ -static void s_test_operation_statistics_simple_callback(struct aws_mqtt_client_connection *connection, void *userdata) { +static void s_test_operation_statistics_simple_callback( + struct aws_mqtt_client_connection_311_impl *connection, + void *userdata) { struct aws_atomic_var *statistics_count = (struct aws_atomic_var *)userdata; aws_atomic_fetch_add(statistics_count, 1); // Confirm we can get the operation statistics from the callback struct aws_mqtt_connection_operation_statistics operation_statistics; - aws_mqtt_client_connection_get_stats(connection, &operation_statistics); + aws_mqtt_client_connection_get_stats(&connection->base, &operation_statistics); (void)operation_statistics; } @@ -1262,7 +1264,7 @@ static int s_test_mqtt_operation_statistics_simple_callback(struct aws_allocator struct aws_atomic_var statistics_count; aws_atomic_store_int(&statistics_count, 0); aws_mqtt_client_connection_set_on_operation_statistics_handler( - state_test_data->mqtt_connection, s_test_operation_statistics_simple_callback, &statistics_count); + state_test_data->mqtt_connection->impl, s_test_operation_statistics_simple_callback, &statistics_count); // /* Stop ACKS so we make sure the operation statistics has time to allow us to identify we sent a packet */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); From b672bff0907603987bc93dae946c4f121c80e14c Mon Sep 17 00:00:00 2001 From: Waqar Ahmed Khan Date: Mon, 15 May 2023 13:32:38 -0700 Subject: [PATCH 64/98] Fix warnings in public headers (#282) --- .github/workflows/ci.yml | 11 ++++++++++- CMakeLists.txt | 2 +- include/aws/mqtt/client.h | 3 +++ include/aws/mqtt/mqtt.h | 3 +++ include/aws/mqtt/v5/mqtt5_client.h | 3 +++ include/aws/mqtt/v5/mqtt5_listener.h | 3 +++ include/aws/mqtt/v5/mqtt5_packet_storage.h | 3 +++ include/aws/mqtt/v5/mqtt5_types.h | 3 +++ 8 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 564f6ff0..aaf876f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - 'main' env: - BUILDER_VERSION: v0.9.40 + BUILDER_VERSION: v0.9.43 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt @@ -36,6 +36,7 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ matrix.image }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON + linux-compiler-compat: runs-on: ubuntu-20.04 # latest strategy: @@ -58,6 +59,7 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler }} --cmake-extra=-DASSERT_LOCK_HELD=ON + clang-sanitizers: runs-on: ubuntu-20.04 # latest strategy: @@ -69,6 +71,7 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=clang-11 --cmake-extra=-DENABLE_SANITIZERS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DSANITIZERS="${{ matrix.sanitizers }}" + linux-shared-libs: runs-on: ubuntu-20.04 # latest steps: @@ -77,6 +80,7 @@ jobs: run: | aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh ./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON + windows: runs-on: windows-2022 # latest steps: @@ -84,6 +88,7 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON + windows-vc14: runs-on: windows-2019 # windows-2019 is last env with Visual Studio 2015 (v14.0) strategy: @@ -94,6 +99,7 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --target windows-${{ matrix.arch }} --compiler msvc-14 --cmake-extra=-DASSERT_LOCK_HELD=ON + windows-shared-libs: runs-on: windows-2022 # latest steps: @@ -101,6 +107,7 @@ jobs: run: | python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')" python builder.pyz build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBUILD_SHARED_LIBS=ON --cmake-extra=-DASSERT_LOCK_HELD=ON + windows-app-verifier: runs-on: windows-2022 # latest steps: @@ -111,6 +118,7 @@ jobs: - name: Run and check AppVerifier run: | python .\aws-c-mqtt\build\deps\aws-c-common\scripts\appverifier_ctest.py --build_directory .\aws-c-mqtt\build\aws-c-mqtt + osx: runs-on: macos-12 # latest steps: @@ -119,6 +127,7 @@ jobs: python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" chmod a+x builder ./builder build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DASSERT_LOCK_HELD=ON + # Test downstream repos. # This should not be required because we can run into a chicken and egg problem if there is a change that needs some fix in a downstream repo. downstream: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e4b7cfb..17c9cd0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,7 @@ file(GLOB MQTT_SRC add_library(${PROJECT_NAME} ${MQTT_HEADERS} ${MQTT_SRC}) aws_set_common_properties(${PROJECT_NAME}) aws_prepare_symbol_visibility_args(${PROJECT_NAME} "AWS_MQTT") -aws_check_headers(${PROJECT_NAME} ${MQTT_HEADERS}) +aws_check_headers(${PROJECT_NAME} ${AWS_MQTT_HEADERS} ${AWS_MQTT5_HEADERS}) aws_add_sanitizers(${PROJECT_NAME}) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 374d853b..cb13c64d 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -17,6 +17,8 @@ #include +AWS_PUSH_SANE_WARNING_LEVEL + /* forward declares */ struct aws_client_bootstrap; struct aws_http_header; @@ -651,5 +653,6 @@ int aws_mqtt_client_connection_get_stats( struct aws_mqtt_connection_operation_statistics *stats); AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_CLIENT_H */ diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 22a63ce2..6d14b020 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -11,6 +11,8 @@ #include +AWS_PUSH_SANE_WARNING_LEVEL + #define AWS_C_MQTT_PACKAGE_ID 5 /* Quality of Service associated with a publish action or subscription [MQTT-4.3]. */ @@ -116,5 +118,6 @@ AWS_MQTT_API void aws_mqtt_fatal_assert_library_initialized(void); AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_MQTT_H */ diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 3b4cc83e..f74ce177 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -19,6 +19,8 @@ #include #include +AWS_PUSH_SANE_WARNING_LEVEL + struct aws_allocator; struct aws_client_bootstrap; struct aws_host_resolution_config; @@ -811,5 +813,6 @@ AWS_MQTT_API int aws_mqtt5_negotiated_settings_copy( AWS_MQTT_API void aws_mqtt5_negotiated_settings_clean_up(struct aws_mqtt5_negotiated_settings *negotiated_settings); AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_MQTT5_CLIENT_H */ diff --git a/include/aws/mqtt/v5/mqtt5_listener.h b/include/aws/mqtt/v5/mqtt5_listener.h index 8d0498ce..7eb9664c 100644 --- a/include/aws/mqtt/v5/mqtt5_listener.h +++ b/include/aws/mqtt/v5/mqtt5_listener.h @@ -10,6 +10,8 @@ #include +AWS_PUSH_SANE_WARNING_LEVEL + /* * Callback signature for when an mqtt5 listener has completely destroyed itself. */ @@ -81,5 +83,6 @@ AWS_MQTT_API struct aws_mqtt5_listener *aws_mqtt5_listener_acquire(struct aws_mq AWS_MQTT_API struct aws_mqtt5_listener *aws_mqtt5_listener_release(struct aws_mqtt5_listener *listener); AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_MQTT5_LISTENER_H */ diff --git a/include/aws/mqtt/v5/mqtt5_packet_storage.h b/include/aws/mqtt/v5/mqtt5_packet_storage.h index 9a7028f4..4b540926 100644 --- a/include/aws/mqtt/v5/mqtt5_packet_storage.h +++ b/include/aws/mqtt/v5/mqtt5_packet_storage.h @@ -18,6 +18,8 @@ #include +AWS_PUSH_SANE_WARNING_LEVEL + struct aws_mqtt5_user_property_set { struct aws_array_list properties; }; @@ -332,5 +334,6 @@ AWS_MQTT_API void aws_mqtt5_packet_unsuback_storage_clean_up( struct aws_mqtt5_packet_unsuback_storage *unsuback_storage); AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_MQTT5_PACKET_STORAGE_H */ diff --git a/include/aws/mqtt/v5/mqtt5_types.h b/include/aws/mqtt/v5/mqtt5_types.h index f8db3951..d374b9ba 100644 --- a/include/aws/mqtt/v5/mqtt5_types.h +++ b/include/aws/mqtt/v5/mqtt5_types.h @@ -19,6 +19,8 @@ #include #include +AWS_PUSH_SANE_WARNING_LEVEL + /** * Some artificial (non-MQTT spec specified) limits that we place on input packets (publish, subscribe, unsubscibe) * which lets us safely do the various packet size calculations with a bare minimum of checked arithmetic. @@ -482,5 +484,6 @@ struct aws_mqtt5_packet_unsuback_view { size_t reason_code_count; const enum aws_mqtt5_unsuback_reason_code *reason_codes; }; +AWS_POP_SANE_WARNING_LEVEL #endif /* AWS_MQTT_MQTT5_TYPES_H */ From 8c520b36673115390d1d22efeca99373b744c5e4 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 15 May 2023 17:49:25 -0700 Subject: [PATCH 65/98] Stub mqtt3-to-mqtt5 adapter (#281) * Stub mqtt3-to-5 adapter, no vtable implementation yet --------- Co-authored-by: Bret Ambrose --- include/aws/mqtt/client.h | 11 ++ source/mqtt3_to_mqtt5_adapter.c | 179 ++++++++++++++++++++++++ tests/CMakeLists.txt | 5 + tests/v5/mqtt3_to_mqtt5_adapter_tests.c | 89 ++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 source/mqtt3_to_mqtt5_adapter.c create mode 100644 tests/v5/mqtt3_to_mqtt5_adapter_tests.c diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index cb13c64d..2506f201 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -24,6 +24,7 @@ struct aws_client_bootstrap; struct aws_http_header; struct aws_http_message; struct aws_http_proxy_options; +struct aws_mqtt5_client; struct aws_socket_options; struct aws_tls_connection_options; @@ -309,6 +310,16 @@ void aws_mqtt_client_release(struct aws_mqtt_client *client); AWS_MQTT_API struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqtt_client *client); +/** + * Creates a new MQTT311 connection object that uses an MQTT5 client under the hood + * + * \param[in] client The mqtt5 client to create the connection from + * + * \returns a new mqtt (311) connection on success, NULL otherwise + */ +AWS_MQTT_API +struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_client(struct aws_mqtt5_client *client); + /** * Increments the ref count to an mqtt client connection, allowing the caller to take a reference to it * diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c new file mode 100644 index 00000000..f30dcae4 --- /dev/null +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -0,0 +1,179 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include + +enum aws_mqtt5_adapter_state { + AWS_MQTT5_AS_ENABLED, + AWS_MQTT5_AS_DISABLED, +}; + +struct aws_mqtt_client_connection_5_impl { + struct aws_allocator *allocator; + + struct aws_mqtt_client_connection base; + + struct aws_mqtt5_client *client; + struct aws_mqtt5_listener *listener; + struct aws_event_loop *loop; + + struct aws_mutex state_lock; + enum aws_mqtt5_adapter_state state; +}; + +static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +static bool s_aws_mqtt5_listener_publish_received_adapter( + const struct aws_mqtt5_packet_publish_view *publish, + void *user_data) { + (void)publish; + (void)user_data; + + return false; +} + +static void s_disable_adapter(struct aws_mqtt_client_connection_5_impl *adapter) { + /* + * The lock is held during callbacks to prevent invoking into something that is in the process of + * destruction. In general this isn't a performance worry since callbacks are invoked from a single + * thread: the event loop that the client and adapter are seated on. + * + * But since we don't have recursive mutexes on all platforms, we need to be careful about the disable + * API since if we naively always locked, then an adapter release inside a callback would deadlock. + * + * On the surface, it seems reasonable that if we're in the event loop thread we could just skip + * locking entirely (because we've already locked it at the start of the callback). Unfortunately, this isn't safe + * because we don't actually truly know we're in our mqtt5 client's callback; we could be in some other + * client/connection's callback that happens to be seated on the same event loop. And while it's true that because + * of the thread seating, nothing will be interfering with our shared state manipulation, there's one final + * consideration which forces us to *try* to lock: + * + * Dependent on the memory model of the CPU architecture, changes to shared state, even if "safe" from data + * races across threads, may not become visible to other cores on the same CPU unless some kind of synchronization + * primitive (memory barrier) is invoked. So in this extremely unlikely case, we use try-lock to guarantee that a + * synchronization primitive is invoked when disable is coming through a callback from something else on the same + * event loop. + * + * In the case that we're in our mqtt5 client's callback, the lock is already held, try fails, and the unlock at + * the end of the callback will suffice for cache flush and synchronization. + * + * In the case that we're in something else's callback on the same thread, the try succeeds and its followup + * unlock here will suffice for cache flush and synchronization. + */ + if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { + bool lock_succeeded = aws_mutex_try_lock(&adapter->state_lock) == AWS_OP_SUCCESS; + adapter->state = AWS_MQTT5_AS_DISABLED; + if (lock_succeeded) { + aws_mutex_unlock(&adapter->state_lock); + } + } else { + aws_mutex_lock(&adapter->state_lock); + adapter->state = AWS_MQTT5_AS_DISABLED; + aws_mutex_unlock(&adapter->state_lock); + } +} + +static void s_mqtt_client_connection_5_impl_finish_destroy(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + adapter->client = aws_mqtt5_client_release(adapter->client); + aws_mutex_clean_up(&adapter->state_lock); + + aws_mem_release(adapter->allocator, adapter); +} + +/* + * When the adapter's ref count goes to zero, here's what we want to do: + * + * (1) Put the adapter into the disabled mode, which tells it to stop processing callbacks from the mqtt5 client + * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user of it) + * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we are + * guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the mqtt5 + * client. + * (4) Synchronously clean up all further resources. + * + * This function does steps (1) and (2). (3) and (4) are accomplished via + * s_mqtt_client_connection_5_impl_finish_destroy above. + */ +static void s_mqtt_client_connection_5_impl_start_destroy(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + s_disable_adapter(adapter); + + aws_mqtt5_listener_release(adapter->listener); +} + +static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { + .set_will_fn = NULL, + .set_login_fn = NULL, + .use_websockets_fn = NULL, + .set_http_proxy_options_fn = NULL, + .set_host_resolution_options_fn = NULL, + .set_reconnect_timeout_fn = NULL, + .set_connection_interruption_handlers_fn = NULL, + .set_connection_closed_handler_fn = NULL, + .set_on_any_publish_handler_fn = NULL, + .connect_fn = NULL, + .reconnect_fn = NULL, + .disconnect_fn = NULL, + .subscribe_multiple_fn = NULL, + .subscribe_fn = NULL, + .subscribe_local_fn = NULL, + .resubscribe_existing_topics_fn = NULL, + .unsubscribe_fn = NULL, + .publish_fn = NULL, + .get_stats_fn = NULL, +}; + +static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_5_vtable_ptr = + &s_aws_mqtt_client_connection_5_vtable; + +struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_client(struct aws_mqtt5_client *client) { + struct aws_allocator *allocator = client->allocator; + struct aws_mqtt_client_connection_5_impl *adapter = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_client_connection_5_impl)); + + adapter->allocator = allocator; + + adapter->base.vtable = s_aws_mqtt_client_connection_5_vtable_ptr; + adapter->base.impl = adapter; + aws_ref_count_init( + &adapter->base.ref_count, + adapter, + (aws_simple_completion_callback *)s_mqtt_client_connection_5_impl_start_destroy); + + adapter->client = aws_mqtt5_client_acquire(client); + adapter->loop = client->loop; + + aws_mutex_init(&adapter->state_lock); + + /* + * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." + * In that case, we don't want callbacks coming back before construction is even over, so instead we "cork" + * things by starting in the disabled state. We'll enable the adapter as soon as they try to connect. + */ + adapter->state = AWS_MQTT5_AS_DISABLED; + + struct aws_mqtt5_listener_config listener_config = { + .client = client, + .listener_callbacks = + { + .listener_publish_received_handler = s_aws_mqtt5_listener_publish_received_adapter, + .listener_publish_received_handler_user_data = adapter, + .lifecycle_event_handler = s_aws_mqtt5_client_connection_event_callback_adapter, + .lifecycle_event_handler_user_data = adapter, + }, + .termination_callback = s_mqtt_client_connection_5_impl_finish_destroy, + .termination_callback_user_data = adapter, + }; + adapter->listener = aws_mqtt5_listener_new(allocator, &listener_config); + + return &adapter->base; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 63568959..79b4b891 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -352,6 +352,11 @@ add_test_case(rate_limiter_token_bucket_large_fractional_iteration) add_test_case(rate_limiter_token_bucket_real_iteration) add_test_case(rate_limiter_token_bucket_reset) +# mqtt3 to 5 adapter tests + +add_test_case(mqtt3to5_adapter_create_destroy) +add_test_case(mqtt3to5_adapter_create_destroy_delayed) + generate_test_driver(${PROJECT_NAME}-tests) set(TEST_PAHO_CLIENT_BINARY_NAME ${PROJECT_NAME}-paho-client) diff --git a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c new file mode 100644 index 00000000..87a4ff54 --- /dev/null +++ b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c @@ -0,0 +1,89 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +#include "mqtt5_testing_utils.h" + +void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_packet_connect_view local_connect_options = { + .keep_alive_interval_seconds = 30, + .clean_start = true, + }; + + struct aws_mqtt5_client_options client_options = { + .connect_options = &local_connect_options, + .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .publish_received_handler = s_mqtt3to5_publish_received_callback, + .publish_received_handler_user_data = NULL, + .ping_timeout_ms = 10000, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { + .client_options = &client_options, + }; + + struct aws_mqtt5_client_mock_test_fixture test_fixture; + AWS_ZERO_STRUCT(test_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); + + struct aws_mqtt_client_connection *connection = + aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener attachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt_client_connection_release(connection); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener detachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) + +static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( + allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) From 16b7c61bcffba677b9acb9716bdff3ceaad7c75d Mon Sep 17 00:00:00 2001 From: TwistedTwigleg Date: Fri, 19 May 2023 15:09:48 -0400 Subject: [PATCH 66/98] Use UUID for socket endpoints (#283) Adjust tests to use UUID instead of timestamps for socket endpoints --- tests/v3/connection_state_test.c | 18 +----------------- tests/v3/operation_statistics_test.c | 15 +-------------- tests/v5/mqtt5_testing_utils.c | 15 +-------------- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 2355fb02..da59b860 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -17,7 +17,6 @@ #include #include -#include static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; @@ -238,22 +237,7 @@ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); - struct aws_byte_buf endpoint_buf = - aws_byte_buf_from_empty_array(state_test_data->endpoint.address, sizeof(state_test_data->endpoint.address)); -#ifdef _WIN32 - AWS_FATAL_ASSERT( - aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str("\\\\.\\pipe\\testsock"))); -#else - AWS_FATAL_ASSERT(aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str("testsock"))); -#endif - /* Use UUID to generate a random endpoint for the socket */ - struct aws_uuid uuid; - ASSERT_SUCCESS(aws_uuid_init(&uuid)); - ASSERT_SUCCESS(aws_uuid_to_str(&uuid, &endpoint_buf)); - -#ifndef _WIN32 - AWS_FATAL_ASSERT(aws_byte_buf_write_from_whole_cursor(&endpoint_buf, aws_byte_cursor_from_c_str(".sock"))); -#endif + aws_socket_endpoint_init_local_address_for_test(&state_test_data->endpoint); struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { .bootstrap = state_test_data->server_bootstrap, diff --git a/tests/v3/operation_statistics_test.c b/tests/v3/operation_statistics_test.c index c63d3423..a3bf9865 100644 --- a/tests/v3/operation_statistics_test.c +++ b/tests/v3/operation_statistics_test.c @@ -16,12 +16,6 @@ #include -#ifdef _WIN32 -# define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" -#else -# define LOCAL_SOCK_TEST_PATTERN "testsock%llu.sock" -#endif - static const int TEST_LOG_SUBJECT = 60000; static const int ONE_SEC = 1000000000; @@ -211,14 +205,7 @@ static int s_operation_statistics_setup_mqtt_server_fn(struct aws_allocator *all ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); - uint64_t timestamp = 0; - ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); - - snprintf( - state_test_data->endpoint.address, - sizeof(state_test_data->endpoint.address), - LOCAL_SOCK_TEST_PATTERN, - (long long unsigned)timestamp); + aws_socket_endpoint_init_local_address_for_test(&state_test_data->endpoint); struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { .bootstrap = state_test_data->server_bootstrap, diff --git a/tests/v5/mqtt5_testing_utils.c b/tests/v5/mqtt5_testing_utils.c index 42e5fd27..b32eb3a5 100644 --- a/tests/v5/mqtt5_testing_utils.c +++ b/tests/v5/mqtt5_testing_utils.c @@ -1019,12 +1019,6 @@ static int s_aws_mqtt5_mock_test_fixture_on_packet_received_fn( return result; } -#ifdef _WIN32 -# define LOCAL_SOCK_TEST_PATTERN "\\\\.\\pipe\\testsock%llu" -#else -# define LOCAL_SOCK_TEST_PATTERN "testsock%llu.sock" -#endif - static int s_process_read_message( struct aws_channel_handler *handler, struct aws_channel_slot *slot, @@ -1360,14 +1354,7 @@ int aws_mqtt5_client_mock_test_fixture_init( test_fixture->client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); - uint64_t timestamp = 0; - ASSERT_SUCCESS(aws_sys_clock_get_ticks(×tamp)); - - snprintf( - test_fixture->endpoint.address, - sizeof(test_fixture->endpoint.address), - LOCAL_SOCK_TEST_PATTERN, - (long long unsigned)timestamp); + aws_socket_endpoint_init_local_address_for_test(&test_fixture->endpoint); struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { .bootstrap = test_fixture->server_bootstrap, From 1941fe086d62083ccc9db95764deccaa62d3d090 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Fri, 19 May 2023 15:02:41 -0700 Subject: [PATCH 67/98] Misc refactors involving mqtt5 needed for the adapter (#284) --- include/aws/mqtt/private/v5/mqtt5_client_impl.h | 2 +- include/aws/mqtt/private/v5/mqtt5_options_storage.h | 2 +- source/v5/mqtt5_client.c | 10 ++++++---- source/v5/mqtt5_options_storage.c | 10 ++++++---- tests/v5/mqtt5_operation_validation_failure_tests.c | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index 7c083549..b63bdb02 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -342,7 +342,7 @@ struct aws_mqtt5_client { /* * Client configuration */ - const struct aws_mqtt5_client_options_storage *config; + struct aws_mqtt5_client_options_storage *config; /* * The recurrent task that runs all client logic outside of external event callbacks. Bound to the client's diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index 91c2b087..57b68c07 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -170,7 +170,7 @@ struct aws_mqtt5_client_options_storage { struct aws_mqtt5_client_topic_alias_options topic_aliasing_options; - struct aws_mqtt5_packet_connect_storage connect; + struct aws_mqtt5_packet_connect_storage *connect; aws_mqtt5_client_connection_event_callback_fn *lifecycle_event_handler; void *lifecycle_event_handler_user_data; diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 9138a6c4..79701bbc 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -991,8 +991,6 @@ static void s_websocket_handshake_transform_complete( task->handshake = handshake_request; aws_event_loop_schedule_task_now(client->loop, &task->task); - - aws_mqtt5_client_release(client); } static int s_websocket_connect(struct aws_mqtt5_client *client) { @@ -1016,7 +1014,11 @@ static int s_websocket_connect(struct aws_mqtt5_client *client) { AWS_LOGF_TRACE(AWS_LS_MQTT5_CLIENT, "id=%p: Transforming websocket handshake request.", (void *)client); - aws_mqtt5_client_acquire(client); + /* + * There is no need to inc the client's ref count here since this state (AWS_MCS_CONNECTING) is uninterruptible by + * the async destruction process. Only a completion of the chain of connection establishment callbacks can cause + * this state to be left by the client. + */ client->config->websocket_handshake_transform( handshake, client->config->websocket_handshake_transform_user_data, @@ -1204,7 +1206,7 @@ static void s_change_current_state_to_mqtt_connect(struct aws_mqtt5_client *clie aws_mqtt5_decoder_reset(&client->decoder); bool resume_session = s_should_resume_session(client); - struct aws_mqtt5_packet_connect_view connect_view = client->config->connect.storage_view; + struct aws_mqtt5_packet_connect_view connect_view = client->config->connect->storage_view; connect_view.clean_start = !resume_session; if (aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default( diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index e81da80a..206377b0 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -1313,7 +1313,7 @@ static int s_aws_mqtt5_packet_disconnect_view_validate_vs_connection_settings( * cannot set a non-zero value here if you sent a 0-value or no value in the CONNECT (presumably allows * the server to skip tracking session state, and we can't undo that now) */ - const uint32_t *session_expiry_ptr = client->config->connect.storage_view.session_expiry_interval_seconds; + const uint32_t *session_expiry_ptr = client->config->connect->storage_view.session_expiry_interval_seconds; if (*disconnect_view->session_expiry_interval_seconds > 0 && (session_expiry_ptr == NULL || *session_expiry_ptr == 0)) { AWS_LOGF_ERROR( @@ -3794,7 +3794,7 @@ void aws_mqtt5_client_options_storage_log( "id=%p: aws_mqtt5_client_options_storage connect options:", (void *)options_storage); - aws_mqtt5_packet_connect_view_log(&options_storage->connect.storage_view, level); + aws_mqtt5_packet_connect_view_log(&options_storage->connect->storage_view, level); AWS_LOGUF( log_handle, @@ -3816,7 +3816,8 @@ void aws_mqtt5_client_options_storage_destroy(struct aws_mqtt5_client_options_st aws_tls_connection_options_clean_up(&options_storage->tls_options); aws_http_proxy_config_destroy(options_storage->http_proxy_config); - aws_mqtt5_packet_connect_storage_clean_up(&options_storage->connect); + aws_mqtt5_packet_connect_storage_clean_up(options_storage->connect); + aws_mem_release(options_storage->connect->allocator, options_storage->connect); aws_mem_release(options_storage->allocator, options_storage); } @@ -3944,7 +3945,8 @@ struct aws_mqtt5_client_options_storage *aws_mqtt5_client_options_storage_new( options_storage->topic_aliasing_options = *options->topic_aliasing_options; } - if (aws_mqtt5_packet_connect_storage_init(&options_storage->connect, allocator, options->connect_options)) { + options_storage->connect = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); + if (aws_mqtt5_packet_connect_storage_init(options_storage->connect, allocator, options->connect_options)) { goto error; } diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c index 1de9ab8a..f0509004 100644 --- a/tests/v5/mqtt5_operation_validation_failure_tests.c +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -1550,7 +1550,7 @@ static int mqtt5_operation_disconnect_connection_settings_validation_failure_pro ASSERT_SUCCESS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); ((struct aws_mqtt5_client_options_storage *)dummy_client.config) - ->connect.storage_view.session_expiry_interval_seconds = NULL; + ->connect->storage_view.session_expiry_interval_seconds = NULL; ASSERT_FAILS(aws_mqtt5_operation_validate_vs_connection_settings(&operation->base, &dummy_client)); From 4885afb850ec3f9ba97a1e79c6bdfbf8e54971d2 Mon Sep 17 00:00:00 2001 From: Joseph Klix Date: Mon, 22 May 2023 16:00:28 -0700 Subject: [PATCH 68/98] update time to ancient (#287) --- .github/workflows/stale_issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale_issue.yml b/.github/workflows/stale_issue.yml index 857a4b0f..5624eec4 100644 --- a/.github/workflows/stale_issue.yml +++ b/.github/workflows/stale_issue.yml @@ -32,7 +32,7 @@ jobs: # Issue timing days-before-stale: 2 days-before-close: 5 - days-before-ancient: 365 + days-before-ancient: 36500 # If you don't want to mark a issue as being ancient based on a # threshold of "upvotes", you can set this here. An "upvote" is From a661c9df4e4516f9bf80665f8933d132462a5dc5 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 24 May 2023 09:07:41 -0700 Subject: [PATCH 69/98] Move acquire/release to the connection vtable (#285) * Move acquire/release to the vtable so that we can add special semantics to the adapter's release and shutdown process --- include/aws/mqtt/private/client_impl.h | 2 + include/aws/mqtt/private/client_impl_shared.h | 5 +- source/client.c | 20 ++- source/client_impl_shared.c | 6 +- source/mqtt3_to_mqtt5_adapter.c | 145 ++++++++++-------- 5 files changed, 108 insertions(+), 70 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 09f92d38..15f971f4 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -184,6 +184,8 @@ struct aws_mqtt_client_connection_311_impl { struct aws_mqtt_client_connection base; + struct aws_ref_count ref_count; + struct aws_mqtt_client *client; /* Channel handler information */ diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index ccef756b..b72c32b7 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -12,6 +12,10 @@ struct aws_mqtt_client_connection; struct aws_mqtt_client_connection_vtable { + struct aws_mqtt_client_connection *(*acquire_fn)(void *impl); + + void (*release_fn)(void *impl); + int (*set_will_fn)( void *impl, const struct aws_byte_cursor *topic, @@ -105,7 +109,6 @@ struct aws_mqtt_client_connection_vtable { struct aws_mqtt_client_connection { struct aws_mqtt_client_connection_vtable *vtable; void *impl; - struct aws_ref_count ref_count; }; #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H */ diff --git a/source/client.c b/source/client.c index 7a07343f..d1175e4e 100644 --- a/source/client.c +++ b/source/client.c @@ -3365,7 +3365,23 @@ int aws_mqtt_client_connection_set_on_operation_statistics_handler( return AWS_OP_SUCCESS; } +static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_311_acquire(void *impl) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + + aws_ref_count_acquire(&connection->ref_count); + + return &connection->base; +} + +static void s_aws_mqtt_client_connection_311_release(void *impl) { + struct aws_mqtt_client_connection_311_impl *connection = impl; + + aws_ref_count_release(&connection->ref_count); +} + static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311_vtable = { + .acquire_fn = s_aws_mqtt_client_connection_311_acquire, + .release_fn = s_aws_mqtt_client_connection_311_release, .set_will_fn = s_aws_mqtt_client_connection_311_set_will, .set_login_fn = s_aws_mqtt_client_connection_311_set_login, .use_websockets_fn = s_aws_mqtt_client_connection_311_use_websockets, @@ -3406,9 +3422,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt connection->base.vtable = s_aws_mqtt_client_connection_311_vtable_ptr; connection->base.impl = connection; aws_ref_count_init( - &connection->base.ref_count, - connection, - (aws_simple_completion_callback *)s_mqtt_client_connection_start_destroy); + &connection->ref_count, connection, (aws_simple_completion_callback *)s_mqtt_client_connection_start_destroy); connection->client = aws_mqtt_client_acquire(client); AWS_ZERO_STRUCT(connection->synced_data); connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index cd68af41..67b20310 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -8,15 +8,15 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_acquire(struct aws_mqtt_client_connection *connection) { if (connection != NULL) { - aws_ref_count_acquire(&connection->ref_count); + return (*connection->vtable->acquire_fn)(connection->impl); } - return connection; + return NULL; } void aws_mqtt_client_connection_release(struct aws_mqtt_client_connection *connection) { if (connection != NULL) { - aws_ref_count_release(&connection->ref_count); + (*connection->vtable->release_fn)(connection->impl); } } diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index f30dcae4..b8df1fbb 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -22,8 +22,12 @@ struct aws_mqtt_client_connection_5_impl { struct aws_mqtt5_listener *listener; struct aws_event_loop *loop; - struct aws_mutex state_lock; - enum aws_mqtt5_adapter_state state; + struct aws_mutex lock; + + struct { + enum aws_mqtt5_adapter_state state; + uint64_t ref_count; + } synced_data; }; static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { @@ -39,78 +43,96 @@ static bool s_aws_mqtt5_listener_publish_received_adapter( return false; } -static void s_disable_adapter(struct aws_mqtt_client_connection_5_impl *adapter) { - /* - * The lock is held during callbacks to prevent invoking into something that is in the process of - * destruction. In general this isn't a performance worry since callbacks are invoked from a single - * thread: the event loop that the client and adapter are seated on. - * - * But since we don't have recursive mutexes on all platforms, we need to be careful about the disable - * API since if we naively always locked, then an adapter release inside a callback would deadlock. - * - * On the surface, it seems reasonable that if we're in the event loop thread we could just skip - * locking entirely (because we've already locked it at the start of the callback). Unfortunately, this isn't safe - * because we don't actually truly know we're in our mqtt5 client's callback; we could be in some other - * client/connection's callback that happens to be seated on the same event loop. And while it's true that because - * of the thread seating, nothing will be interfering with our shared state manipulation, there's one final - * consideration which forces us to *try* to lock: - * - * Dependent on the memory model of the CPU architecture, changes to shared state, even if "safe" from data - * races across threads, may not become visible to other cores on the same CPU unless some kind of synchronization - * primitive (memory barrier) is invoked. So in this extremely unlikely case, we use try-lock to guarantee that a - * synchronization primitive is invoked when disable is coming through a callback from something else on the same - * event loop. - * - * In the case that we're in our mqtt5 client's callback, the lock is already held, try fails, and the unlock at - * the end of the callback will suffice for cache flush and synchronization. - * - * In the case that we're in something else's callback on the same thread, the try succeeds and its followup - * unlock here will suffice for cache flush and synchronization. - */ - if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { - bool lock_succeeded = aws_mutex_try_lock(&adapter->state_lock) == AWS_OP_SUCCESS; - adapter->state = AWS_MQTT5_AS_DISABLED; - if (lock_succeeded) { - aws_mutex_unlock(&adapter->state_lock); - } - } else { - aws_mutex_lock(&adapter->state_lock); - adapter->state = AWS_MQTT5_AS_DISABLED; - aws_mutex_unlock(&adapter->state_lock); - } -} - static void s_mqtt_client_connection_5_impl_finish_destroy(void *context) { struct aws_mqtt_client_connection_5_impl *adapter = context; adapter->client = aws_mqtt5_client_release(adapter->client); - aws_mutex_clean_up(&adapter->state_lock); + aws_mutex_clean_up(&adapter->lock); aws_mem_release(adapter->allocator, adapter); } +static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + aws_mutex_lock(&adapter->lock); + AWS_FATAL_ASSERT(adapter->synced_data.ref_count > 0); + ++adapter->synced_data.ref_count; + aws_mutex_unlock(&adapter->lock); + + return &adapter->base; +} + /* - * When the adapter's ref count goes to zero, here's what we want to do: + * The lock is held during callbacks to prevent invoking into something that is in the process of + * destruction. In general this isn't a performance worry since callbacks are invoked from a single + * thread: the event loop that the client and adapter are seated on. * - * (1) Put the adapter into the disabled mode, which tells it to stop processing callbacks from the mqtt5 client - * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user of it) - * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we are - * guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the mqtt5 - * client. - * (4) Synchronously clean up all further resources. + * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown + * process since if we naively always locked, then an adapter release inside a callback would deadlock. * - * This function does steps (1) and (2). (3) and (4) are accomplished via - * s_mqtt_client_connection_5_impl_finish_destroy above. + * On the surface, it seems reasonable that if we're in the event loop thread we could just skip + * locking entirely (because we've already locked it at the start of the callback). Unfortunately, this isn't safe + * because we don't actually truly know we're in our mqtt5 client's callback; we could be in some other + * client/connection's callback that happens to be seated on the same event loop. And while it's true that because + * of the thread seating, nothing will be interfering with our shared state manipulation, there's one final + * consideration which forces us to *try* to lock: + * + * Dependent on the memory model of the CPU architecture, changes to shared state, even if "safe" from data + * races across threads, may not become visible to other cores on the same CPU unless some kind of synchronization + * primitive (memory barrier) is invoked. So in this extremely unlikely case, we use try-lock to guarantee that a + * synchronization primitive is invoked when disable is coming through a callback from something else on the same + * event loop. + * + * In the case that we're in our mqtt5 client's callback, the lock is already held, try fails, and the unlock at + * the end of the callback will suffice for cache flush and synchronization. + * + * In the case that we're in something else's callback on the same thread, the try succeeds and its followup + * unlock here will suffice for cache flush and synchronization. + * + * Some after-the-fact analysis hints that this extra step (in the case where we are in the event loop) may + * be unnecessary because the only reader of the state change is the event loop thread itself. I don't feel + * confident enough in the memory semantics of thread<->CPU core bindings to relax this though. */ -static void s_mqtt_client_connection_5_impl_start_destroy(void *context) { - struct aws_mqtt_client_connection_5_impl *adapter = context; +static void s_aws_mqtt_client_connection_5_release(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; - s_disable_adapter(adapter); + bool start_shutdown = false; + bool lock_succeeded = aws_mutex_try_lock(&adapter->lock) == AWS_OP_SUCCESS; - aws_mqtt5_listener_release(adapter->listener); + AWS_FATAL_ASSERT(adapter->synced_data.ref_count > 0); + --adapter->synced_data.ref_count; + if (adapter->synced_data.ref_count == 0) { + adapter->synced_data.state = AWS_MQTT5_AS_DISABLED; + start_shutdown = true; + } + + if (lock_succeeded) { + aws_mutex_unlock(&adapter->lock); + } + + if (start_shutdown) { + /* + * When the adapter's ref count goes to zero, here's what we want to do: + * + * (1) Put the adapter into the disabled mode, which tells it to stop processing callbacks from the mqtt5 + * client (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user + * of it) (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we + * are guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the mqtt5 + * client. + * (4) Synchronously clean up all further resources. + * + * Step (1) was done within the lock above. + * Step (2) is done here. + * Steps (3) and (4) are accomplished via s_mqtt_client_connection_5_impl_finish_destroy. + */ + aws_mqtt5_listener_release(adapter->listener); + } } static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { + .acquire_fn = s_aws_mqtt_client_connection_5_acquire, + .release_fn = s_aws_mqtt_client_connection_5_release, .set_will_fn = NULL, .set_login_fn = NULL, .use_websockets_fn = NULL, @@ -144,22 +166,19 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli adapter->base.vtable = s_aws_mqtt_client_connection_5_vtable_ptr; adapter->base.impl = adapter; - aws_ref_count_init( - &adapter->base.ref_count, - adapter, - (aws_simple_completion_callback *)s_mqtt_client_connection_5_impl_start_destroy); adapter->client = aws_mqtt5_client_acquire(client); adapter->loop = client->loop; - aws_mutex_init(&adapter->state_lock); + aws_mutex_init(&adapter->lock); /* * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." * In that case, we don't want callbacks coming back before construction is even over, so instead we "cork" * things by starting in the disabled state. We'll enable the adapter as soon as they try to connect. */ - adapter->state = AWS_MQTT5_AS_DISABLED; + adapter->synced_data.state = AWS_MQTT5_AS_DISABLED; + adapter->synced_data.ref_count = 1; struct aws_mqtt5_listener_config listener_config = { .client = client, From 305239f97d776c1a63879b416c094ff4da1f5c0a Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 24 May 2023 11:29:18 -0700 Subject: [PATCH 70/98] Mqtt3-to-5 Adapter Config APIs (#286) * Inplementation of mqtt311 config APIs for the 3-to-5 adapter --- include/aws/mqtt/client.h | 2 +- include/aws/mqtt/private/client_impl_shared.h | 2 +- source/client.c | 6 +- source/client_impl_shared.c | 2 +- source/mqtt3_to_mqtt5_adapter.c | 832 +++++++++++++++++- tests/CMakeLists.txt | 3 + tests/v5/mqtt3_to_mqtt5_adapter_tests.c | 89 -- tests/v5/mqtt5_client_tests.c | 285 ++++++ tests/v5/mqtt5_testing_utils.c | 68 +- 9 files changed, 1149 insertions(+), 140 deletions(-) delete mode 100644 tests/v5/mqtt3_to_mqtt5_adapter_tests.c diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 2506f201..534d677b 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -405,7 +405,7 @@ int aws_mqtt_client_connection_set_http_proxy_options( AWS_MQTT_API int aws_mqtt_client_connection_set_host_resolution_options( struct aws_mqtt_client_connection *connection, - struct aws_host_resolution_config *host_resolution_config); + const struct aws_host_resolution_config *host_resolution_config); /** * Sets the minimum and maximum reconnect timeouts. diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index b72c32b7..1234f7db 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -34,7 +34,7 @@ struct aws_mqtt_client_connection_vtable { int (*set_http_proxy_options_fn)(void *impl, struct aws_http_proxy_options *proxy_options); - int (*set_host_resolution_options_fn)(void *impl, struct aws_host_resolution_config *host_resolution_config); + int (*set_host_resolution_options_fn)(void *impl, const struct aws_host_resolution_config *host_resolution_config); int (*set_reconnect_timeout_fn)(void *impl, uint64_t min_timeout, uint64_t max_timeout); diff --git a/source/client.c b/source/client.c index d1175e4e..b7215bca 100644 --- a/source/client.c +++ b/source/client.c @@ -1184,7 +1184,7 @@ static int s_aws_mqtt_client_connection_311_set_http_proxy_options( static int s_aws_mqtt_client_connection_311_set_host_resolution_options( void *impl, - struct aws_host_resolution_config *host_resolution_config) { + const struct aws_host_resolution_config *host_resolution_config) { struct aws_mqtt_client_connection_311_impl *connection = impl; @@ -1373,13 +1373,13 @@ error:; int aws_mqtt_client_connection_use_websockets( struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_transform_websocket_handshake_fn *transformer, - void *transformer_ud, + void *transformer_user_data, aws_mqtt_validate_websocket_handshake_fn *validator, void *validator_ud) { (void)connection; (void)transformer; - (void)transformer_ud; + (void)transformer_user_data; (void)validator; (void)validator_ud; diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index 67b20310..4172e976 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -58,7 +58,7 @@ int aws_mqtt_client_connection_set_http_proxy_options( int aws_mqtt_client_connection_set_host_resolution_options( struct aws_mqtt_client_connection *connection, - struct aws_host_resolution_config *host_resolution_config) { + const struct aws_host_resolution_config *host_resolution_config) { return (*connection->vtable->set_host_resolution_options_fn)(connection->impl, host_resolution_config); } diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index b8df1fbb..170df7ff 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -28,6 +28,25 @@ struct aws_mqtt_client_connection_5_impl { enum aws_mqtt5_adapter_state state; uint64_t ref_count; } synced_data; + + /* 311 interface callbacks */ + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; + void *on_interrupted_user_data; + + aws_mqtt_client_on_connection_resumed_fn *on_resumed; + void *on_resumed_user_data; + + aws_mqtt_client_on_connection_closed_fn *on_closed; + void *on_closed_user_data; + + aws_mqtt_client_publish_received_fn *on_any_publish; + void *on_any_publish_user_data; + + aws_mqtt_transform_websocket_handshake_fn *websocket_handshake_transformer; + void *websocket_handshake_transformer_user_data; + + aws_mqtt5_transform_websocket_handshake_complete_fn *mqtt5_websocket_handshake_completion_function; + void *mqtt5_websocket_handshake_completion_user_data; }; static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { @@ -46,12 +65,791 @@ static bool s_aws_mqtt5_listener_publish_received_adapter( static void s_mqtt_client_connection_5_impl_finish_destroy(void *context) { struct aws_mqtt_client_connection_5_impl *adapter = context; + if (adapter->client->config->websocket_handshake_transform_user_data == adapter) { + /* + * If the mqtt5 client is pointing to us for websocket transform, then erase that. The callback + * is invoked from our pinned event loop so this is safe. + */ + adapter->client->config->websocket_handshake_transform = NULL; + adapter->client->config->websocket_handshake_transform_user_data = NULL; + } + adapter->client = aws_mqtt5_client_release(adapter->client); aws_mutex_clean_up(&adapter->lock); aws_mem_release(adapter->allocator, adapter); } +struct aws_mqtt_set_interruption_handlers_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; + void *on_interrupted_user_data; + + aws_mqtt_client_on_connection_resumed_fn *on_resumed; + void *on_resumed_user_data; +}; + +static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_interruption_handlers_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + connection->on_interrupted = set_task->on_interrupted; + connection->on_interrupted_user_data = set_task->on_interrupted_user_data; + connection->on_resumed = set_task->on_resumed; + connection->on_resumed_user_data = set_task->on_resumed_user_data; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interruption_handlers_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_user_data, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_user_data) { + + struct aws_mqtt_set_interruption_handlers_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_interruption_handlers_task)); + + aws_task_init( + &set_task->task, s_set_interruption_handlers_task_fn, (void *)set_task, "SetInterruptionHandlersTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->on_interrupted = on_interrupted; + set_task->on_interrupted_user_data = on_interrupted_user_data; + set_task->on_resumed = on_resumed; + set_task->on_resumed_user_data = on_resumed_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_interruption_handlers( + void *impl, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_user_data, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_user_data) { + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_interruption_handlers_task *task = s_aws_mqtt_set_interruption_handlers_task_new( + connection->allocator, connection, on_interrupted, on_interrupted_user_data, on_resumed, on_resumed_user_data); + if (task == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, "id=%p: failed to create set interruption handlers task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_on_closed_handler_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + aws_mqtt_client_on_connection_closed_fn *on_closed; + void *on_closed_user_data; +}; + +static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_on_closed_handler_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + connection->on_closed = set_task->on_closed; + connection->on_closed_user_data = set_task->on_closed_user_data; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_handler_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_user_data) { + + struct aws_mqtt_set_on_closed_handler_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_closed_handler_task)); + + aws_task_init(&set_task->task, s_set_on_closed_handler_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->on_closed = on_closed; + set_task->on_closed_user_data = on_closed_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_on_closed_handler( + void *impl, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_user_data) { + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_on_closed_handler_task *task = + s_aws_mqtt_set_on_closed_handler_task_new(connection->allocator, connection, on_closed, on_closed_user_data); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on closed handler task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_on_any_publish_handler_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + aws_mqtt_client_publish_received_fn *on_any_publish; + void *on_any_publish_user_data; +}; + +static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_on_any_publish_handler_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + connection->on_any_publish = set_task->on_any_publish; + connection->on_any_publish_user_data = set_task->on_any_publish_user_data; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_publish_handler_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_user_data) { + + struct aws_mqtt_set_on_any_publish_handler_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_any_publish_handler_task)); + + aws_task_init( + &set_task->task, s_set_on_any_publish_handler_task_fn, (void *)set_task, "SetOnAnyPublishHandlerTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->on_any_publish = on_any_publish; + set_task->on_any_publish_user_data = on_any_publish_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_on_any_publish_handler( + void *impl, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_user_data) { + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_on_any_publish_handler_task *task = s_aws_mqtt_set_on_any_publish_handler_task_new( + connection->allocator, connection, on_any_publish, on_any_publish_user_data); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on any publish task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_reconnect_timeout_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + uint64_t min_timeout; + uint64_t max_timeout; +}; + +static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_reconnect_timeout_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + connection->client->config->min_reconnect_delay_ms = set_task->min_timeout; + connection->client->config->max_reconnect_delay_ms = set_task->max_timeout; + connection->client->current_reconnect_delay_ms = set_task->min_timeout; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_timeout_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + uint64_t min_timeout, + uint64_t max_timeout) { + + struct aws_mqtt_set_reconnect_timeout_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_reconnect_timeout_task)); + + aws_task_init(&set_task->task, s_set_reconnect_timeout_task_fn, (void *)set_task, "SetReconnectTimeoutTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->min_timeout = aws_min_u64(min_timeout, max_timeout); + set_task->max_timeout = aws_max_u64(min_timeout, max_timeout); + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_reconnect_timeout( + void *impl, + uint64_t min_timeout, + uint64_t max_timeout) { + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_reconnect_timeout_task *task = + s_aws_mqtt_set_reconnect_timeout_task_new(connection->allocator, connection, min_timeout, max_timeout); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_http_proxy_options_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + struct aws_http_proxy_config *proxy_config; +}; + +static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_http_proxy_options_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + aws_http_proxy_config_destroy(connection->client->config->http_proxy_config); + + /* move the proxy config from the set task to the client's config */ + connection->client->config->http_proxy_config = set_task->proxy_config; + if (connection->client->config->http_proxy_config != NULL) { + aws_http_proxy_options_init_from_config( + &connection->client->config->http_proxy_options, connection->client->config->http_proxy_config); + } + + /* don't clean up the proxy config if it was successfully assigned to the mqtt5 client */ + set_task->proxy_config = NULL; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + /* If the task was canceled we need to clean this up because it didn't get assigned to the mqtt5 client */ + aws_http_proxy_config_destroy(set_task->proxy_config); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_options_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + struct aws_http_proxy_options *proxy_options) { + + struct aws_http_proxy_config *proxy_config = + aws_http_proxy_config_new_tunneling_from_proxy_options(allocator, proxy_options); + if (proxy_config == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_mqtt_set_http_proxy_options_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_http_proxy_options_task)); + + aws_task_init(&set_task->task, s_set_http_proxy_options_task_fn, (void *)set_task, "SetHttpProxyOptionsTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->proxy_config = proxy_config; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_http_proxy_options( + void *impl, + struct aws_http_proxy_options *proxy_options) { + + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_http_proxy_options_task *task = + s_aws_mqtt_set_http_proxy_options_task_new(connection->allocator, connection, proxy_options); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set http proxy options task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_use_websockets_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + aws_mqtt_transform_websocket_handshake_fn *transformer; + void *transformer_user_data; +}; + +static void s_aws_mqtt5_adapter_websocket_handshake_completion_fn( + struct aws_http_message *request, + int error_code, + void *complete_ctx) { + + struct aws_mqtt_client_connection_5_impl *adapter = complete_ctx; + + (*adapter->mqtt5_websocket_handshake_completion_function)( + request, error_code, adapter->mqtt5_websocket_handshake_completion_user_data); + + aws_mqtt_client_connection_release(&adapter->base); +} + +static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + + struct aws_mqtt_client_connection_5_impl *adapter = user_data; + + bool chain_callback = false; + struct aws_http_message *completion_request = NULL; + int completion_error_code = AWS_ERROR_SUCCESS; + aws_mutex_lock(&adapter->lock); + + if (adapter->synced_data.state != AWS_MQTT5_AS_ENABLED) { + completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; + } else if (adapter->websocket_handshake_transformer == NULL) { + completion_request = request; + } else { + ++adapter->synced_data.ref_count; + chain_callback = true; + } + + aws_mutex_unlock(&adapter->lock); + + if (chain_callback) { + adapter->mqtt5_websocket_handshake_completion_function = complete_fn; + adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; + + (*adapter->websocket_handshake_transformer)( + request, user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); + } else { + (*complete_fn)(completion_request, completion_error_code, complete_ctx); + } +} + +static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_use_websockets_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + connection->websocket_handshake_transformer = set_task->transformer; + connection->websocket_handshake_transformer_user_data = set_task->transformer_user_data; + + /* we're in the mqtt5 client's event loop; it's safe to access its internal state */ + connection->client->config->websocket_handshake_transform = s_aws_mqtt5_adapter_transform_websocket_handshake_fn; + connection->client->config->websocket_handshake_transform_user_data = connection; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_user_data) { + + struct aws_mqtt_set_use_websockets_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_use_websockets_task)); + + aws_task_init(&set_task->task, s_set_use_websockets_task_fn, (void *)set_task, "SetUseWebsocketsTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->transformer = transformer; + set_task->transformer_user_data = transformer_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_use_websockets( + void *impl, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_user_data, + aws_mqtt_validate_websocket_handshake_fn *validator, + void *validator_user_data) { + + /* mqtt5 doesn't use these */ + (void)validator; + (void)validator_user_data; + + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_use_websockets_task *task = + s_aws_mqtt_set_use_websockets_task_new(connection->allocator, connection, transformer, transformer_user_data); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set use websockets task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_host_resolution_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + struct aws_host_resolution_config host_resolution_config; +}; + +static void s_set_host_resolution_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_host_resolution_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + adapter->client->config->host_resolution_override = set_task->host_resolution_config; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_host_resolution_task *s_aws_mqtt_set_host_resolution_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_host_resolution_config *host_resolution_config) { + + struct aws_mqtt_set_host_resolution_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_host_resolution_task)); + + aws_task_init(&set_task->task, s_set_host_resolution_task_fn, (void *)set_task, "SetHostResolutionTask"); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->host_resolution_config = *host_resolution_config; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_host_resolution_options( + void *impl, + const struct aws_host_resolution_config *host_resolution_config) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_host_resolution_task *task = + s_aws_mqtt_set_host_resolution_task_new(adapter->allocator, adapter, host_resolution_config); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)adapter); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_will_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + struct aws_byte_buf topic_buffer; + enum aws_mqtt_qos qos; + bool retain; + struct aws_byte_buf payload_buffer; +}; + +static void s_aws_mqtt_set_will_task_destroy(struct aws_mqtt_set_will_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up(&task->topic_buffer); + aws_byte_buf_clean_up(&task->payload_buffer); + + aws_mem_release(task->allocator, task); +} + +static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_will_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + struct aws_mqtt5_packet_connect_storage *connect = connection->client->config->connect; + + /* clean up the old will if necessary */ + if (connect->will != NULL) { + aws_mqtt5_packet_publish_storage_clean_up(connect->will); + aws_mem_release(connect->allocator, connect->will); + connect->will = NULL; + } + + struct aws_mqtt5_packet_publish_view will = { + .topic = aws_byte_cursor_from_buf(&set_task->topic_buffer), + .qos = (enum aws_mqtt5_qos)set_task->qos, + .retain = set_task->retain, + .payload = aws_byte_cursor_from_buf(&set_task->payload_buffer), + }; + + /* make a new will */ + connect->will = aws_mem_calloc(connect->allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); + aws_mqtt5_packet_publish_storage_init(connect->will, connect->allocator, &will); + + /* manually update the storage view's will reference */ + connect->storage_view.will = &connect->will->storage_view; + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + s_aws_mqtt_set_will_task_destroy(set_task); +} + +static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload) { + + if (topic == NULL) { + return NULL; + } + + struct aws_mqtt_set_will_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_will_task)); + + aws_task_init(&set_task->task, s_set_will_task_fn, (void *)set_task, "SetWillTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + + set_task->qos = qos; + set_task->retain = retain; + aws_byte_buf_init_copy_from_cursor(&set_task->topic_buffer, allocator, *topic); + if (payload != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->payload_buffer, allocator, *payload); + } + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_will( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload) { + + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_will_task *task = + s_aws_mqtt_set_will_task_new(connection->allocator, connection, topic, qos, retain, payload); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set will task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_login_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; + + struct aws_byte_buf username_buffer; + struct aws_byte_buf password_buffer; +}; + +static void s_aws_mqtt_set_login_task_destroy(struct aws_mqtt_set_login_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up_secure(&task->username_buffer); + aws_byte_buf_clean_up_secure(&task->password_buffer); + + aws_mem_release(task->allocator, task); +} + +static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_login_task *set_task = arg; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_byte_cursor username_cursor = aws_byte_cursor_from_buf(&set_task->username_buffer); + struct aws_byte_cursor password_cursor = aws_byte_cursor_from_buf(&set_task->password_buffer); + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + struct aws_mqtt5_packet_connect_storage *old_connect = connection->client->config->connect; + + /* + * Packet storage stores binary data in a single buffer. The safest way to replace some binary data is + * to make a new storage from the old storage, deleting the old storage after construction is complete. + */ + struct aws_mqtt5_packet_connect_view new_connect_view = old_connect->storage_view; + + if (set_task->username_buffer.len > 0) { + new_connect_view.username = &username_cursor; + } else { + new_connect_view.username = NULL; + } + + if (set_task->password_buffer.len > 0) { + new_connect_view.password = &password_cursor; + } else { + new_connect_view.password = NULL; + } + + if (aws_mqtt5_packet_connect_view_validate(&new_connect_view)) { + goto done; + } + + struct aws_mqtt5_packet_connect_storage *new_connect = + aws_mem_calloc(connection->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); + aws_mqtt5_packet_connect_storage_init(new_connect, connection->allocator, &new_connect_view); + + connection->client->config->connect = new_connect; + aws_mqtt5_packet_connect_storage_clean_up(old_connect); + aws_mem_release(old_connect->allocator, old_connect); + +done: + + aws_mqtt_client_connection_release(set_task->connection); + + s_aws_mqtt_set_login_task_destroy(set_task); +} + +static struct aws_mqtt_set_login_task *s_aws_mqtt_set_login_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *connection, + const struct aws_byte_cursor *username, + const struct aws_byte_cursor *password) { + + struct aws_mqtt_set_login_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_login_task)); + + aws_task_init(&set_task->task, s_set_login_task_fn, (void *)set_task, "SetLoginTask"); + set_task->allocator = connection->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + + if (username != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->username_buffer, allocator, *username); + } + + if (password != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->password_buffer, allocator, *password); + } + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_login( + void *impl, + const struct aws_byte_cursor *username, + const struct aws_byte_cursor *password) { + + struct aws_mqtt_client_connection_5_impl *connection = impl; + + struct aws_mqtt_set_login_task *task = + s_aws_mqtt_set_login_task_new(connection->allocator, connection, username, password); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set login task", (void *)connection); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(connection->loop, &task->task); + + return AWS_OP_SUCCESS; +} + static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire(void *impl) { struct aws_mqtt_client_connection_5_impl *adapter = impl; @@ -116,10 +914,12 @@ static void s_aws_mqtt_client_connection_5_release(void *impl) { * When the adapter's ref count goes to zero, here's what we want to do: * * (1) Put the adapter into the disabled mode, which tells it to stop processing callbacks from the mqtt5 - * client (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user - * of it) (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we - * are guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the mqtt5 - * client. + * client + * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user + * of it) + * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we + * are guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the + * mqtt5 client. * (4) Synchronously clean up all further resources. * * Step (1) was done within the lock above. @@ -133,15 +933,15 @@ static void s_aws_mqtt_client_connection_5_release(void *impl) { static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { .acquire_fn = s_aws_mqtt_client_connection_5_acquire, .release_fn = s_aws_mqtt_client_connection_5_release, - .set_will_fn = NULL, - .set_login_fn = NULL, - .use_websockets_fn = NULL, - .set_http_proxy_options_fn = NULL, - .set_host_resolution_options_fn = NULL, - .set_reconnect_timeout_fn = NULL, - .set_connection_interruption_handlers_fn = NULL, - .set_connection_closed_handler_fn = NULL, - .set_on_any_publish_handler_fn = NULL, + .set_will_fn = s_aws_mqtt_client_connection_5_set_will, + .set_login_fn = s_aws_mqtt_client_connection_5_set_login, + .use_websockets_fn = s_aws_mqtt_client_connection_5_use_websockets, + .set_http_proxy_options_fn = s_aws_mqtt_client_connection_5_set_http_proxy_options, + .set_host_resolution_options_fn = s_aws_mqtt_client_connection_5_set_host_resolution_options, + .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_5_set_reconnect_timeout, + .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, + .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, + .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, .connect_fn = NULL, .reconnect_fn = NULL, .disconnect_fn = NULL, @@ -174,8 +974,10 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli /* * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." - * In that case, we don't want callbacks coming back before construction is even over, so instead we "cork" - * things by starting in the disabled state. We'll enable the adapter as soon as they try to connect. + * We'll enable the adapter as soon as they try to connect via the 311 interface. This + * also ties in to how we simulate the 311 implementation's don't-reconnect-if-initial-connect-fails logic. + * The 5 client will continue to try and reconnect, but the adapter will go disabled making it seem to the 311 + * user that is is offline. */ adapter->synced_data.state = AWS_MQTT5_AS_DISABLED; adapter->synced_data.ref_count = 1; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79b4b891..a74bda9d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -356,6 +356,9 @@ add_test_case(rate_limiter_token_bucket_reset) add_test_case(mqtt3to5_adapter_create_destroy) add_test_case(mqtt3to5_adapter_create_destroy_delayed) +add_test_case(mqtt3to5_adapter_set_will) +add_test_case(mqtt3to5_adapter_set_login) +add_test_case(mqtt3to5_adapter_set_reconnect_timeout) generate_test_driver(${PROJECT_NAME}-tests) diff --git a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c deleted file mode 100644 index 87a4ff54..00000000 --- a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -#include - -#include -#include - -#include "mqtt5_testing_utils.h" - -void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { - (void)event; -} - -void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { - (void)publish; - (void)user_data; -} - -static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { - aws_mqtt_library_init(allocator); - - struct aws_mqtt5_packet_connect_view local_connect_options = { - .keep_alive_interval_seconds = 30, - .clean_start = true, - }; - - struct aws_mqtt5_client_options client_options = { - .connect_options = &local_connect_options, - .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, - .lifecycle_event_handler_user_data = NULL, - .publish_received_handler = s_mqtt3to5_publish_received_callback, - .publish_received_handler_user_data = NULL, - .ping_timeout_ms = 10000, - }; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { - .client_options = &client_options, - }; - - struct aws_mqtt5_client_mock_test_fixture test_fixture; - AWS_ZERO_STRUCT(test_fixture); - - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); - - struct aws_mqtt_client_connection *connection = - aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener attachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt_client_connection_release(connection); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener detachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); - - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) - -static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( - allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 5a62ec41..8075f7b3 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -6090,3 +6091,287 @@ static int s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn(struct aws AWS_TEST_CASE( mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a, s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn) + +void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_packet_connect_view local_connect_options = { + .keep_alive_interval_seconds = 30, + .clean_start = true, + }; + + struct aws_mqtt5_client_options client_options = { + .connect_options = &local_connect_options, + .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .publish_received_handler = s_mqtt3to5_publish_received_callback, + .publish_received_handler_user_data = NULL, + .ping_timeout_ms = 10000, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { + .client_options = &client_options, + }; + + struct aws_mqtt5_client_mock_test_fixture test_fixture; + AWS_ZERO_STRUCT(test_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); + + struct aws_mqtt_client_connection *connection = + aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener attachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt_client_connection_release(connection); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener detachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) + +static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( + allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) + +typedef int (*mqtt3to5_adapter_config_test_setup_fn)( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect); + +static int s_do_mqtt3to5_adapter_config_test( + struct aws_allocator *allocator, + mqtt3to5_adapter_config_test_setup_fn setup_fn) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); + + struct aws_mqtt5_packet_connect_storage expected_connect_storage; + ASSERT_SUCCESS((*setup_fn)(allocator, adapter, &expected_connect_storage)); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_connected_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + struct aws_mqtt5_mock_server_packet_record expected_packets[] = { + { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = &expected_connect_storage, + }, + }; + ASSERT_SUCCESS( + s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); + + aws_mqtt_client_connection_release(adapter); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_set_will_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor topic_cursor = { + .ptr = s_alias_topic1, + .len = AWS_ARRAY_SIZE(s_alias_topic1) - 1, + }; + + struct aws_byte_cursor payload_cursor = { + .ptr = s_sub_pub_unsub_publish_payload, + .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, + }; + + ASSERT_SUCCESS( + aws_mqtt_client_connection_set_will(adapter, &topic_cursor, AWS_MQTT_QOS_AT_LEAST_ONCE, true, &payload_cursor)); + + struct aws_mqtt5_packet_publish_view expected_will = { + .payload = payload_cursor, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = topic_cursor, + }; + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(s_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .will = &expected_will, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_set_will_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_will_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_will, s_mqtt3to5_adapter_set_will_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_username, "MyUsername"); +AWS_STATIC_STRING_FROM_LITERAL(s_password, "TopTopSecret"); + +static int s_mqtt3to5_adapter_set_login_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor username_cursor = aws_byte_cursor_from_string(s_username); + struct aws_byte_cursor password_cursor = aws_byte_cursor_from_string(s_password); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(adapter, &username_cursor, &password_cursor)); + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(s_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .username = &username_cursor, + .password = &password_cursor, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_set_login_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_login_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_login, s_mqtt3to5_adapter_set_login_fn) + +static int s_mqtt3to5_adapter_set_reconnect_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* + * This is a variant of the mqtt5_client_reconnect_failure_backoff test. + * + * The primary change is that we configure the mqtt5 client with "wrong" (fast) reconnect delays and then use + * the adapter API to configure with the "right" ones that will let the test pass. + */ + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + s_mqtt5_client_test_init_default_options(&test_options); + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = 10; + test_options.client_options.max_reconnect_delay_ms = 50; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_always_fail; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); + + aws_mqtt_client_connection_set_reconnect_timeout(adapter, RECONNECT_TEST_MIN_BACKOFF, RECONNECT_TEST_MAX_BACKOFF); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + s_wait_for_n_connection_failure_lifecycle_events(&test_context, 6); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + s_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(s_verify_reconnection_exponential_backoff_timestamps(&test_context)); + + /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt_client_connection_release(adapter); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_reconnect_timeout, s_mqtt3to5_adapter_set_reconnect_timeout_fn) \ No newline at end of file diff --git a/tests/v5/mqtt5_testing_utils.c b/tests/v5/mqtt5_testing_utils.c index b32eb3a5..e8e72f3f 100644 --- a/tests/v5/mqtt5_testing_utils.c +++ b/tests/v5/mqtt5_testing_utils.c @@ -1533,6 +1533,35 @@ void aws_mqtt5_client_mock_test_fixture_clean_up(struct aws_mqtt5_client_mock_te return false; \ } +static bool s_aws_publish_packets_equal( + const struct aws_mqtt5_packet_publish_view *lhs, + const struct aws_mqtt5_packet_publish_view *rhs) { + + // Don't check packet id intentionally + + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->payload, rhs->payload); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->qos, rhs->qos); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->duplicate, rhs->duplicate); + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->retain, rhs->retain); + AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->topic, rhs->topic); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->payload_format, rhs->payload_format); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( + lhs->message_expiry_interval_seconds, rhs->message_expiry_interval_seconds); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->topic_alias, rhs->topic_alias); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->response_topic, rhs->response_topic); + AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->correlation_data, rhs->correlation_data); + + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifier_count, rhs->subscription_identifier_count); + for (size_t i = 0; i < lhs->subscription_identifier_count; ++i) { + AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifiers[i], rhs->subscription_identifiers[i]); + } + + AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( + lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); + + return true; +} + static bool s_aws_connect_packets_equal( const struct aws_mqtt5_packet_connect_view *lhs, const struct aws_mqtt5_packet_connect_view *rhs) { @@ -1551,7 +1580,15 @@ static bool s_aws_connect_packets_equal( AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->maximum_packet_size_bytes, rhs->maximum_packet_size_bytes); AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->will_delay_interval_seconds, rhs->will_delay_interval_seconds); - /* TODO: Check Will */ + if ((lhs->will != NULL) != (rhs->will != NULL)) { + return false; + } + + if (lhs->will) { + if (!s_aws_publish_packets_equal(lhs->will, rhs->will)) { + return false; + } + } AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); @@ -1634,35 +1671,6 @@ static bool s_aws_unsubscribe_packets_equal( return true; } -static bool s_aws_publish_packets_equal( - const struct aws_mqtt5_packet_publish_view *lhs, - const struct aws_mqtt5_packet_publish_view *rhs) { - - // Don't check packet id intentionally - - AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->payload, rhs->payload); - AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->qos, rhs->qos); - AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->duplicate, rhs->duplicate); - AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->retain, rhs->retain); - AWS_MQTT5_CLIENT_TEST_CHECK_CURSOR_EQUALS(lhs->topic, rhs->topic); - AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->payload_format, rhs->payload_format); - AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS( - lhs->message_expiry_interval_seconds, rhs->message_expiry_interval_seconds); - AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_INT_EQUALS(lhs->topic_alias, rhs->topic_alias); - AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->response_topic, rhs->response_topic); - AWS_MQTT5_CLIENT_TEST_CHECK_OPTIONAL_CURSOR_EQUALS(lhs->correlation_data, rhs->correlation_data); - - AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifier_count, rhs->subscription_identifier_count); - for (size_t i = 0; i < lhs->subscription_identifier_count; ++i) { - AWS_MQTT5_CLIENT_TEST_CHECK_INT_EQUALS(lhs->subscription_identifiers[i], rhs->subscription_identifiers[i]); - } - - AWS_MQTT5_CLIENT_TEST_CHECK_USER_PROPERTIES( - lhs->user_properties, lhs->user_property_count, rhs->user_properties, rhs->user_property_count); - - return true; -} - static bool s_aws_puback_packets_equal( const struct aws_mqtt5_packet_puback_view *lhs, const struct aws_mqtt5_packet_puback_view *rhs) { From 2cbfe87589c3185ad370a41a4903518fea96c0cc Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 24 May 2023 14:41:55 -0700 Subject: [PATCH 71/98] Remove unused local subscribe API, connection -> adapter term, remove no-websocket build option (#292) Co-authored-by: Bret Ambrose --- CMakeLists.txt | 8 +- include/aws/mqtt/client.h | 26 -- include/aws/mqtt/private/client_impl.h | 1 - include/aws/mqtt/private/client_impl_shared.h | 9 - source/client.c | 323 ++---------------- source/client_impl_shared.c | 13 - source/mqtt.c | 12 +- source/mqtt3_to_mqtt5_adapter.c | 178 +++++----- 8 files changed, 128 insertions(+), 442 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 17c9cd0a..268681b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,6 @@ cmake_minimum_required(VERSION 3.1) project(aws-c-mqtt C) -option(MQTT_WITH_WEBSOCKETS "Enable MQTT connections over websockets. Requires aws-c-http. " ON) - if (POLICY CMP0069) cmake_policy(SET CMP0069 NEW) # Enable LTO/IPO if available in the compiler, see AwsCFlags endif() @@ -89,11 +87,7 @@ target_include_directories(${PROJECT_NAME} PUBLIC $) aws_use_package(aws-c-io) - -if (MQTT_WITH_WEBSOCKETS) - target_compile_definitions(${PROJECT_NAME} PUBLIC "-DAWS_MQTT_WITH_WEBSOCKETS") - aws_use_package(aws-c-http) -endif () +aws_use_package(aws-c-http) target_link_libraries(${PROJECT_NAME} PUBLIC ${DEP_AWS_LIBS}) aws_prepare_shared_lib_exports(${PROJECT_NAME}) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 534d677b..5ee893f7 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -567,32 +567,6 @@ uint16_t aws_mqtt_client_connection_subscribe( aws_mqtt_suback_fn *on_suback, void *on_suback_ud); -/** - * Subscribe to a single topic filter WITHOUT sending a SUBSCRIBE packet. - * This is useful if you expect the broker to send PUBLISHES without first subscribing. - * on_publish will be called when a PUBLISH matching topic_filter is received. - * - * \param[in] connection The connection to subscribe on - * \param[in] topic_filter The topic filter to subscribe on. This resource must persist until on_suback. - * \param[in] on_publish (nullable) Called when a PUBLISH packet matching topic_filter is received - * \param[in] on_publish_ud (nullable) Passed to on_publish - * \param[in] on_ud_cleanup (nullable) Called when a subscription is removed, on_publish_ud is passed. - * \param[in] on_suback (nullable) Called when a SUBACK has been received from the server and the subscription is - * complete - * \param[in] on_suback_ud (nullable) Passed to on_suback - * - * \returns The "packet id" of the operation if successfully initiated, otherwise 0. - */ -AWS_MQTT_API -uint16_t aws_mqtt_client_connection_subscribe_local( - struct aws_mqtt_client_connection *connection, - const struct aws_byte_cursor *topic_filter, - aws_mqtt_client_publish_received_fn *on_publish, - void *on_publish_ud, - aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, - aws_mqtt_suback_fn *on_suback, - void *on_suback_ud); - /** * Resubscribe to all topics currently subscribed to. This is to help when resuming a connection with a clean session. * diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 15f971f4..5822fa6c 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -174,7 +174,6 @@ struct subscribe_task_topic { struct aws_mqtt_topic_subscription request; struct aws_string *filter; - bool is_local; struct aws_ref_count ref_count; }; diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index 1234f7db..8c1116e8 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -77,15 +77,6 @@ struct aws_mqtt_client_connection_vtable { aws_mqtt_suback_fn *on_suback, void *on_suback_ud); - uint16_t (*subscribe_local_fn)( - void *impl, - const struct aws_byte_cursor *topic_filter, - aws_mqtt_client_publish_received_fn *on_publish, - void *on_publish_ud, - aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, - aws_mqtt_suback_fn *on_suback, - void *on_suback_ud); - uint16_t (*resubscribe_existing_topics_fn)(void *impl, aws_mqtt_suback_multi_fn *on_suback, void *on_suback_ud); uint16_t (*unsubscribe_fn)( diff --git a/source/client.c b/source/client.c index b7215bca..291f4557 100644 --- a/source/client.c +++ b/source/client.c @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include @@ -23,11 +25,6 @@ #include -#ifdef AWS_MQTT_WITH_WEBSOCKETS -# include -# include -#endif - #ifdef _MSC_VER # pragma warning(disable : 4204) # pragma warning(disable : 4996) /* allow strncpy() */ @@ -1142,7 +1139,6 @@ static int s_aws_mqtt_client_connection_311_set_on_any_publish_handler( /******************************************************************************* * Websockets ******************************************************************************/ -#ifdef AWS_MQTT_WITH_WEBSOCKETS static int s_aws_mqtt_client_connection_311_use_websockets( void *impl, @@ -1369,44 +1365,6 @@ error:; s_on_websocket_setup(&websocket_setup, connection); } -#else /* AWS_MQTT_WITH_WEBSOCKETS */ -int aws_mqtt_client_connection_use_websockets( - struct aws_mqtt_client_connection_311_impl *connection, - aws_mqtt_transform_websocket_handshake_fn *transformer, - void *transformer_user_data, - aws_mqtt_validate_websocket_handshake_fn *validator, - void *validator_ud) { - - (void)connection; - (void)transformer; - (void)transformer_user_data; - (void)validator; - (void)validator_ud; - - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Cannot use websockets unless library is built with MQTT_WITH_WEBSOCKETS option.", - (void *)connection); - - return aws_raise_error(AWS_ERROR_MQTT_BUILT_WITHOUT_WEBSOCKETS); -} - -int aws_mqtt_client_connection_set_websocket_proxy_options( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_http_proxy_options *proxy_options) { - - (void)connection; - (void)proxy_options; - - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Cannot use websockets unless library is built with MQTT_WITH_WEBSOCKETS option.", - (void *)connection); - - return aws_raise_error(AWS_ERROR_MQTT_BUILT_WITHOUT_WEBSOCKETS); -} -#endif /* AWS_MQTT_WITH_WEBSOCKETS */ - /******************************************************************************* * Connect ******************************************************************************/ @@ -1621,12 +1579,9 @@ static int s_mqtt_client_connect( connection->on_connection_complete_ud = userdata; int result = 0; -#ifdef AWS_MQTT_WITH_WEBSOCKETS if (connection->websocket.enabled) { result = s_websocket_connect(connection); - } else -#endif /* AWS_MQTT_WITH_WEBSOCKETS */ - { + } else { struct aws_socket_channel_bootstrap_options channel_options; AWS_ZERO_STRUCT(channel_options); channel_options.bootstrap = connection->client->bootstrap; @@ -2215,209 +2170,6 @@ static uint16_t s_aws_mqtt_client_connection_311_subscribe( return 0; } -/******************************************************************************* - * Subscribe Local - ******************************************************************************/ - -/* The lifetime of this struct is from subscribe -> suback */ -struct subscribe_local_task_arg { - - struct aws_mqtt_client_connection_311_impl *connection; - - struct subscribe_task_topic *task_topic; - - aws_mqtt_suback_fn *on_suback; - void *on_suback_ud; -}; - -static enum aws_mqtt_client_request_state s_subscribe_local_send( - uint16_t packet_id, - bool is_first_attempt, - void *userdata) { - - (void)is_first_attempt; - - struct subscribe_local_task_arg *task_arg = userdata; - - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, - "id=%p: Attempting save of local subscribe %" PRIu16 " (%s)", - (void *)task_arg->connection, - packet_id, - is_first_attempt ? "first attempt" : "redo"); - - struct subscribe_task_topic *topic = task_arg->task_topic; - - struct aws_byte_cursor filter_cursor = aws_byte_cursor_from_string(topic->filter); - if (s_is_topic_shared_topic(&filter_cursor)) { - struct aws_string *normal_topic = s_get_normal_topic_from_shared_topic(topic->filter); - if (normal_topic == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Topic is shared subscription topic but topic could not be parsed from " - "shared subscription topic.", - (void *)task_arg->connection); - return AWS_MQTT_CLIENT_REQUEST_ERROR; - } - if (aws_mqtt_topic_tree_insert( - &task_arg->connection->thread_data.subscriptions, - normal_topic, - topic->request.qos, - s_on_publish_client_wrapper, - s_task_topic_release, - topic)) { - aws_string_destroy(normal_topic); - return AWS_MQTT_CLIENT_REQUEST_ERROR; - } - aws_string_destroy(normal_topic); - } else { - if (aws_mqtt_topic_tree_insert( - &task_arg->connection->thread_data.subscriptions, - topic->filter, - topic->request.qos, - s_on_publish_client_wrapper, - s_task_topic_release, - topic)) { - return AWS_MQTT_CLIENT_REQUEST_ERROR; - } - } - aws_ref_count_acquire(&topic->ref_count); - - return AWS_MQTT_CLIENT_REQUEST_COMPLETE; -} - -static void s_subscribe_local_complete( - struct aws_mqtt_client_connection *connection_base, - uint16_t packet_id, - int error_code, - void *userdata) { - - struct aws_mqtt_client_connection_311_impl *connection = connection_base->impl; - - struct subscribe_local_task_arg *task_arg = userdata; - - AWS_LOGF_DEBUG( - AWS_LS_MQTT_CLIENT, - "id=%p: Local subscribe %" PRIu16 " completed with error code %d", - (void *)connection, - packet_id, - error_code); - - struct subscribe_task_topic *topic = task_arg->task_topic; - if (task_arg->on_suback) { - aws_mqtt_suback_fn *suback = task_arg->on_suback; - suback( - &connection->base, - packet_id, - &topic->request.topic, - topic->request.qos, - error_code, - task_arg->on_suback_ud); - } - s_task_topic_release(topic); - - aws_mem_release(task_arg->connection->allocator, task_arg); -} - -static uint16_t s_aws_mqtt_client_connection_311_subscribe_local( - void *impl, - const struct aws_byte_cursor *topic_filter, - aws_mqtt_client_publish_received_fn *on_publish, - void *on_publish_ud, - aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, - aws_mqtt_suback_fn *on_suback, - void *on_suback_ud) { - - struct aws_mqtt_client_connection_311_impl *connection = impl; - - AWS_PRECONDITION(connection); - - if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { - aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); - return 0; - } - - struct subscribe_task_topic *task_topic = NULL; - - struct subscribe_local_task_arg *task_arg = - aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_local_task_arg)); - - if (!task_arg) { - goto handle_error; - } - AWS_ZERO_STRUCT(*task_arg); - - task_arg->connection = connection; - task_arg->on_suback = on_suback; - task_arg->on_suback_ud = on_suback_ud; - task_topic = aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_task_topic)); - if (!task_topic) { - goto handle_error; - } - aws_ref_count_init(&task_topic->ref_count, task_topic, (aws_simple_completion_callback *)s_task_topic_clean_up); - task_arg->task_topic = task_topic; - - task_topic->filter = aws_string_new_from_array(connection->allocator, topic_filter->ptr, topic_filter->len); - if (!task_topic->filter) { - goto handle_error; - } - - task_topic->connection = connection; - task_topic->is_local = true; - task_topic->request.topic = aws_byte_cursor_from_string(task_topic->filter); - task_topic->request.on_publish = on_publish; - task_topic->request.on_cleanup = on_ud_cleanup; - task_topic->request.on_publish_ud = on_publish_ud; - - /* Calculate the size of the (local) subscribe packet - * The fixed header is 2 bytes, the packet ID is 2 bytes - * the topic filter is always 3 bytes (1 for QoS, 2 for Length MSB/LSB) - * - plus the size of the topic filter */ - uint64_t subscribe_packet_size = 7 + topic_filter->len; - - uint16_t packet_id = mqtt_create_request( - task_arg->connection, - s_subscribe_local_send, - task_arg, - &s_subscribe_local_complete, - task_arg, - false, /* noRetry */ - subscribe_packet_size); - - if (packet_id == 0) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Failed to start local subscribe on topic " PRInSTR " with error %s", - (void *)connection, - AWS_BYTE_CURSOR_PRI(task_topic->request.topic), - aws_error_debug_str(aws_last_error())); - goto handle_error; - } - - AWS_LOGF_DEBUG( - AWS_LS_MQTT_CLIENT, - "id=%p: Starting local subscribe %" PRIu16 " on topic " PRInSTR, - (void *)connection, - packet_id, - AWS_BYTE_CURSOR_PRI(task_topic->request.topic)); - return packet_id; - -handle_error: - - if (task_topic) { - if (task_topic->filter) { - aws_string_destroy(task_topic->filter); - } - aws_mem_release(connection->allocator, task_topic); - } - - if (task_arg) { - aws_mem_release(connection->allocator, task_arg); - } - - return 0; -} - /******************************************************************************* * Resubscribe ******************************************************************************/ @@ -2663,7 +2415,7 @@ struct unsubscribe_task_arg { struct aws_mqtt_client_connection_311_impl *connection; struct aws_string *filter_string; struct aws_byte_cursor filter; - bool is_local; + /* Packet to populate */ struct aws_mqtt_packet_unsubscribe unsubscribe; @@ -2737,58 +2489,54 @@ static enum aws_mqtt_client_request_state s_unsubscribe_send( goto handle_error; } } - - task_arg->is_local = topic ? topic->is_local : false; } - if (!task_arg->is_local) { - if (task_arg->unsubscribe.fixed_header.packet_type == 0) { - /* If unsubscribe packet is uninitialized, init it */ - if (aws_mqtt_packet_unsubscribe_init(&task_arg->unsubscribe, task_arg->connection->allocator, packet_id)) { - goto handle_error; - } - if (aws_mqtt_packet_unsubscribe_add_topic(&task_arg->unsubscribe, task_arg->filter)) { - goto handle_error; - } - } - - message = mqtt_get_message_for_packet(task_arg->connection, &task_arg->unsubscribe.fixed_header); - if (!message) { + if (task_arg->unsubscribe.fixed_header.packet_type == 0) { + /* If unsubscribe packet is uninitialized, init it */ + if (aws_mqtt_packet_unsubscribe_init(&task_arg->unsubscribe, task_arg->connection->allocator, packet_id)) { goto handle_error; } - - if (aws_mqtt_packet_unsubscribe_encode(&message->message_data, &task_arg->unsubscribe)) { + if (aws_mqtt_packet_unsubscribe_add_topic(&task_arg->unsubscribe, task_arg->filter)) { goto handle_error; } + } - if (aws_channel_slot_send_message(task_arg->connection->slot, message, AWS_CHANNEL_DIR_WRITE)) { - goto handle_error; - } + message = mqtt_get_message_for_packet(task_arg->connection, &task_arg->unsubscribe.fixed_header); + if (!message) { + goto handle_error; + } - /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion - * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly - * fire the on_completion callbacks. */ - struct request_timeout_task_arg *timeout_task_arg = s_schedule_timeout_task(task_arg->connection, packet_id); - if (!timeout_task_arg) { - return AWS_MQTT_CLIENT_REQUEST_ERROR; - } + if (aws_mqtt_packet_unsubscribe_encode(&message->message_data, &task_arg->unsubscribe)) { + goto handle_error; + } - /* - * Set up mutual references between the operation task args and the timeout task args. Whoever runs first - * "wins", does its logic, and then breaks the connection between the two. - */ - task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; - timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; + if (aws_channel_slot_send_message(task_arg->connection->slot, message, AWS_CHANNEL_DIR_WRITE)) { + goto handle_error; } + /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion + * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly + * fire the on_completion callbacks. */ + struct request_timeout_task_arg *timeout_task_arg = s_schedule_timeout_task(task_arg->connection, packet_id); + if (!timeout_task_arg) { + return AWS_MQTT_CLIENT_REQUEST_ERROR; + } + + /* + * Set up mutual references between the operation task args and the timeout task args. Whoever runs first + * "wins", does its logic, and then breaks the connection between the two. + */ + task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; + timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; + if (!task_arg->tree_updated) { aws_mqtt_topic_tree_transaction_commit(&task_arg->connection->thread_data.subscriptions, &transaction); task_arg->tree_updated = true; } aws_array_list_clean_up(&transaction); - /* If the subscribe is local-only, don't wait for a SUBACK to come back. */ - return task_arg->is_local ? AWS_MQTT_CLIENT_REQUEST_COMPLETE : AWS_MQTT_CLIENT_REQUEST_ONGOING; + + return AWS_MQTT_CLIENT_REQUEST_ONGOING; handle_error: @@ -3396,7 +3144,6 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311 .disconnect_fn = s_aws_mqtt_client_connection_311_disconnect, .subscribe_multiple_fn = s_aws_mqtt_client_connection_311_subscribe_multiple, .subscribe_fn = s_aws_mqtt_client_connection_311_subscribe, - .subscribe_local_fn = s_aws_mqtt_client_connection_311_subscribe_local, .resubscribe_existing_topics_fn = s_aws_mqtt_311_resubscribe_existing_topics, .unsubscribe_fn = s_aws_mqtt_client_connection_311_unsubscribe, .publish_fn = s_aws_mqtt_client_connection_311_publish, diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index 4172e976..2bffd896 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -144,19 +144,6 @@ uint16_t aws_mqtt_client_connection_subscribe( connection->impl, topic_filter, qos, on_publish, on_publish_ud, on_ud_cleanup, on_suback, on_suback_ud); } -uint16_t aws_mqtt_client_connection_subscribe_local( - struct aws_mqtt_client_connection *connection, - const struct aws_byte_cursor *topic_filter, - aws_mqtt_client_publish_received_fn *on_publish, - void *on_publish_ud, - aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, - aws_mqtt_suback_fn *on_suback, - void *on_suback_ud) { - - return (*connection->vtable->subscribe_local_fn)( - connection->impl, topic_filter, on_publish, on_publish_ud, on_ud_cleanup, on_suback, on_suback_ud); -} - uint16_t aws_mqtt_resubscribe_existing_topics( struct aws_mqtt_client_connection *connection, aws_mqtt_suback_multi_fn *on_suback, diff --git a/source/mqtt.c b/source/mqtt.c index fd39a827..ef11253f 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -5,11 +5,9 @@ #include -#include +#include -#ifdef AWS_MQTT_WITH_WEBSOCKETS -# include -#endif +#include /******************************************************************************* * Topic Validation @@ -250,9 +248,8 @@ void aws_mqtt_library_init(struct aws_allocator *allocator) { if (!s_mqtt_library_initialized) { s_mqtt_library_initialized = true; aws_io_library_init(allocator); -#ifdef AWS_MQTT_WITH_WEBSOCKETS aws_http_library_init(allocator); -#endif + aws_register_error_info(&s_error_list); aws_register_log_subject_info_list(&s_logging_subjects_list); } @@ -264,9 +261,8 @@ void aws_mqtt_library_clean_up(void) { aws_thread_join_all_managed(); aws_unregister_error_info(&s_error_list); aws_unregister_log_subject_info_list(&s_logging_subjects_list); -#ifdef AWS_MQTT_WITH_WEBSOCKETS + aws_http_library_clean_up(); -#endif aws_io_library_clean_up(); } } diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index 170df7ff..fa181492 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -100,12 +100,12 @@ static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - connection->on_interrupted = set_task->on_interrupted; - connection->on_interrupted_user_data = set_task->on_interrupted_user_data; - connection->on_resumed = set_task->on_resumed; - connection->on_resumed_user_data = set_task->on_resumed_user_data; + adapter->on_interrupted = set_task->on_interrupted; + adapter->on_interrupted_user_data = set_task->on_interrupted_user_data; + adapter->on_resumed = set_task->on_resumed; + adapter->on_resumed_user_data = set_task->on_resumed_user_data; done: @@ -116,7 +116,7 @@ static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interruption_handlers_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, void *on_interrupted_user_data, aws_mqtt_client_on_connection_resumed_fn *on_resumed, @@ -127,8 +127,8 @@ static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interrupti aws_task_init( &set_task->task, s_set_interruption_handlers_task_fn, (void *)set_task, "SetInterruptionHandlersTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->on_interrupted = on_interrupted; set_task->on_interrupted_user_data = on_interrupted_user_data; set_task->on_resumed = on_resumed; @@ -143,17 +143,16 @@ static int s_aws_mqtt_client_connection_5_set_interruption_handlers( void *on_interrupted_user_data, aws_mqtt_client_on_connection_resumed_fn *on_resumed, void *on_resumed_user_data) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_interruption_handlers_task *task = s_aws_mqtt_set_interruption_handlers_task_new( - connection->allocator, connection, on_interrupted, on_interrupted_user_data, on_resumed, on_resumed_user_data); + adapter->allocator, adapter, on_interrupted, on_interrupted_user_data, on_resumed, on_resumed_user_data); if (task == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, "id=%p: failed to create set interruption handlers task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set interruption handlers task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -175,10 +174,10 @@ static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, en goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - connection->on_closed = set_task->on_closed; - connection->on_closed_user_data = set_task->on_closed_user_data; + adapter->on_closed = set_task->on_closed; + adapter->on_closed_user_data = set_task->on_closed_user_data; done: @@ -189,7 +188,7 @@ static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, en static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_handler_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, aws_mqtt_client_on_connection_closed_fn *on_closed, void *on_closed_user_data) { @@ -197,8 +196,8 @@ static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_hand aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_closed_handler_task)); aws_task_init(&set_task->task, s_set_on_closed_handler_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->on_closed = on_closed; set_task->on_closed_user_data = on_closed_user_data; @@ -209,16 +208,16 @@ static int s_aws_mqtt_client_connection_5_set_on_closed_handler( void *impl, aws_mqtt_client_on_connection_closed_fn *on_closed, void *on_closed_user_data) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_on_closed_handler_task *task = - s_aws_mqtt_set_on_closed_handler_task_new(connection->allocator, connection, on_closed, on_closed_user_data); + s_aws_mqtt_set_on_closed_handler_task_new(adapter->allocator, adapter, on_closed, on_closed_user_data); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on closed handler task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on closed handler task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -240,10 +239,10 @@ static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *ar goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - connection->on_any_publish = set_task->on_any_publish; - connection->on_any_publish_user_data = set_task->on_any_publish_user_data; + adapter->on_any_publish = set_task->on_any_publish; + adapter->on_any_publish_user_data = set_task->on_any_publish_user_data; done: @@ -254,7 +253,7 @@ static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *ar static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_publish_handler_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, aws_mqtt_client_publish_received_fn *on_any_publish, void *on_any_publish_user_data) { @@ -263,8 +262,8 @@ static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_pu aws_task_init( &set_task->task, s_set_on_any_publish_handler_task_fn, (void *)set_task, "SetOnAnyPublishHandlerTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->on_any_publish = on_any_publish; set_task->on_any_publish_user_data = on_any_publish_user_data; @@ -275,16 +274,16 @@ static int s_aws_mqtt_client_connection_5_set_on_any_publish_handler( void *impl, aws_mqtt_client_publish_received_fn *on_any_publish, void *on_any_publish_user_data) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_on_any_publish_handler_task *task = s_aws_mqtt_set_on_any_publish_handler_task_new( - connection->allocator, connection, on_any_publish, on_any_publish_user_data); + adapter->allocator, adapter, on_any_publish, on_any_publish_user_data); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on any publish task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on any publish task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -306,12 +305,12 @@ static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, en goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - connection->client->config->min_reconnect_delay_ms = set_task->min_timeout; - connection->client->config->max_reconnect_delay_ms = set_task->max_timeout; - connection->client->current_reconnect_delay_ms = set_task->min_timeout; + adapter->client->config->min_reconnect_delay_ms = set_task->min_timeout; + adapter->client->config->max_reconnect_delay_ms = set_task->max_timeout; + adapter->client->current_reconnect_delay_ms = set_task->min_timeout; done: @@ -322,7 +321,7 @@ static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, en static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_timeout_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, uint64_t min_timeout, uint64_t max_timeout) { @@ -330,8 +329,8 @@ static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_time aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_reconnect_timeout_task)); aws_task_init(&set_task->task, s_set_reconnect_timeout_task_fn, (void *)set_task, "SetReconnectTimeoutTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->min_timeout = aws_min_u64(min_timeout, max_timeout); set_task->max_timeout = aws_max_u64(min_timeout, max_timeout); @@ -342,16 +341,16 @@ static int s_aws_mqtt_client_connection_5_set_reconnect_timeout( void *impl, uint64_t min_timeout, uint64_t max_timeout) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_reconnect_timeout_task *task = - s_aws_mqtt_set_reconnect_timeout_task_new(connection->allocator, connection, min_timeout, max_timeout); + s_aws_mqtt_set_reconnect_timeout_task_new(adapter->allocator, adapter, min_timeout, max_timeout); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -372,16 +371,16 @@ static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, e goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - aws_http_proxy_config_destroy(connection->client->config->http_proxy_config); + aws_http_proxy_config_destroy(adapter->client->config->http_proxy_config); /* move the proxy config from the set task to the client's config */ - connection->client->config->http_proxy_config = set_task->proxy_config; - if (connection->client->config->http_proxy_config != NULL) { + adapter->client->config->http_proxy_config = set_task->proxy_config; + if (adapter->client->config->http_proxy_config != NULL) { aws_http_proxy_options_init_from_config( - &connection->client->config->http_proxy_options, connection->client->config->http_proxy_config); + &adapter->client->config->http_proxy_options, adapter->client->config->http_proxy_config); } /* don't clean up the proxy config if it was successfully assigned to the mqtt5 client */ @@ -399,7 +398,7 @@ static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, e static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_options_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, struct aws_http_proxy_options *proxy_options) { struct aws_http_proxy_config *proxy_config = @@ -413,8 +412,8 @@ static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_op aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_http_proxy_options_task)); aws_task_init(&set_task->task, s_set_http_proxy_options_task_fn, (void *)set_task, "SetHttpProxyOptionsTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->proxy_config = proxy_config; return set_task; @@ -424,16 +423,16 @@ static int s_aws_mqtt_client_connection_5_set_http_proxy_options( void *impl, struct aws_http_proxy_options *proxy_options) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_http_proxy_options_task *task = - s_aws_mqtt_set_http_proxy_options_task_new(connection->allocator, connection, proxy_options); + s_aws_mqtt_set_http_proxy_options_task_new(adapter->allocator, adapter, proxy_options); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set http proxy options task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set http proxy options task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -503,14 +502,14 @@ static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - connection->websocket_handshake_transformer = set_task->transformer; - connection->websocket_handshake_transformer_user_data = set_task->transformer_user_data; + adapter->websocket_handshake_transformer = set_task->transformer; + adapter->websocket_handshake_transformer_user_data = set_task->transformer_user_data; /* we're in the mqtt5 client's event loop; it's safe to access its internal state */ - connection->client->config->websocket_handshake_transform = s_aws_mqtt5_adapter_transform_websocket_handshake_fn; - connection->client->config->websocket_handshake_transform_user_data = connection; + adapter->client->config->websocket_handshake_transform = s_aws_mqtt5_adapter_transform_websocket_handshake_fn; + adapter->client->config->websocket_handshake_transform_user_data = adapter; done: @@ -521,7 +520,7 @@ static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, aws_mqtt_transform_websocket_handshake_fn *transformer, void *transformer_user_data) { @@ -529,8 +528,8 @@ static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_ta aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_use_websockets_task)); aws_task_init(&set_task->task, s_set_use_websockets_task_fn, (void *)set_task, "SetUseWebsocketsTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->transformer = transformer; set_task->transformer_user_data = transformer_user_data; @@ -548,16 +547,16 @@ static int s_aws_mqtt_client_connection_5_use_websockets( (void)validator; (void)validator_user_data; - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_use_websockets_task *task = - s_aws_mqtt_set_use_websockets_task_new(connection->allocator, connection, transformer, transformer_user_data); + s_aws_mqtt_set_use_websockets_task_new(adapter->allocator, adapter, transformer, transformer_user_data); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set use websockets task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set use websockets task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -654,10 +653,10 @@ static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_s goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - struct aws_mqtt5_packet_connect_storage *connect = connection->client->config->connect; + struct aws_mqtt5_packet_connect_storage *connect = adapter->client->config->connect; /* clean up the old will if necessary */ if (connect->will != NULL) { @@ -689,7 +688,7 @@ static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_s static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, const struct aws_byte_cursor *topic, enum aws_mqtt_qos qos, bool retain, @@ -702,8 +701,8 @@ static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( struct aws_mqtt_set_will_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_will_task)); aws_task_init(&set_task->task, s_set_will_task_fn, (void *)set_task, "SetWillTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); set_task->qos = qos; set_task->retain = retain; @@ -722,16 +721,16 @@ static int s_aws_mqtt_client_connection_5_set_will( bool retain, const struct aws_byte_cursor *payload) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_will_task *task = - s_aws_mqtt_set_will_task_new(connection->allocator, connection, topic, qos, retain, payload); + s_aws_mqtt_set_will_task_new(adapter->allocator, adapter, topic, qos, retain, payload); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set will task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set will task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -764,12 +763,12 @@ static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_ goto done; } - struct aws_mqtt_client_connection_5_impl *connection = set_task->connection->impl; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; struct aws_byte_cursor username_cursor = aws_byte_cursor_from_buf(&set_task->username_buffer); struct aws_byte_cursor password_cursor = aws_byte_cursor_from_buf(&set_task->password_buffer); /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - struct aws_mqtt5_packet_connect_storage *old_connect = connection->client->config->connect; + struct aws_mqtt5_packet_connect_storage *old_connect = adapter->client->config->connect; /* * Packet storage stores binary data in a single buffer. The safest way to replace some binary data is @@ -794,10 +793,10 @@ static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_ } struct aws_mqtt5_packet_connect_storage *new_connect = - aws_mem_calloc(connection->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); - aws_mqtt5_packet_connect_storage_init(new_connect, connection->allocator, &new_connect_view); + aws_mem_calloc(adapter->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); + aws_mqtt5_packet_connect_storage_init(new_connect, adapter->allocator, &new_connect_view); - connection->client->config->connect = new_connect; + adapter->client->config->connect = new_connect; aws_mqtt5_packet_connect_storage_clean_up(old_connect); aws_mem_release(old_connect->allocator, old_connect); @@ -810,15 +809,15 @@ static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_ static struct aws_mqtt_set_login_task *s_aws_mqtt_set_login_task_new( struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *connection, + struct aws_mqtt_client_connection_5_impl *adapter, const struct aws_byte_cursor *username, const struct aws_byte_cursor *password) { struct aws_mqtt_set_login_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_login_task)); aws_task_init(&set_task->task, s_set_login_task_fn, (void *)set_task, "SetLoginTask"); - set_task->allocator = connection->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&connection->base); + set_task->allocator = adapter->allocator; + set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); if (username != NULL) { aws_byte_buf_init_copy_from_cursor(&set_task->username_buffer, allocator, *username); @@ -836,16 +835,16 @@ static int s_aws_mqtt_client_connection_5_set_login( const struct aws_byte_cursor *username, const struct aws_byte_cursor *password) { - struct aws_mqtt_client_connection_5_impl *connection = impl; + struct aws_mqtt_client_connection_5_impl *adapter = impl; struct aws_mqtt_set_login_task *task = - s_aws_mqtt_set_login_task_new(connection->allocator, connection, username, password); + s_aws_mqtt_set_login_task_new(adapter->allocator, adapter, username, password); if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set login task", (void *)connection); + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set login task", (void *)adapter); return AWS_OP_ERR; } - aws_event_loop_schedule_task_now(connection->loop, &task->task); + aws_event_loop_schedule_task_now(adapter->loop, &task->task); return AWS_OP_SUCCESS; } @@ -947,7 +946,6 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_v .disconnect_fn = NULL, .subscribe_multiple_fn = NULL, .subscribe_fn = NULL, - .subscribe_local_fn = NULL, .resubscribe_existing_topics_fn = NULL, .unsubscribe_fn = NULL, .publish_fn = NULL, From a760f425fc939d8fe9db87e73cca810bf7abd2f7 Mon Sep 17 00:00:00 2001 From: Dengke Tang Date: Wed, 24 May 2023 16:31:22 -0700 Subject: [PATCH 72/98] fix cmake config (#294) - `MQTT_WITH_WEBSOCKETS` has been removed, but the cmake config was not updated with it - Also, remove the dependency on aws-c-io, as it's covered by aws-c-http already. --- CMakeLists.txt | 1 - cmake/aws-c-mqtt-config.cmake | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 268681b8..b33f24d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,7 +86,6 @@ target_include_directories(${PROJECT_NAME} PUBLIC $ $) -aws_use_package(aws-c-io) aws_use_package(aws-c-http) target_link_libraries(${PROJECT_NAME} PUBLIC ${DEP_AWS_LIBS}) diff --git a/cmake/aws-c-mqtt-config.cmake b/cmake/aws-c-mqtt-config.cmake index c7993376..b3d54f66 100644 --- a/cmake/aws-c-mqtt-config.cmake +++ b/cmake/aws-c-mqtt-config.cmake @@ -1,24 +1,20 @@ include(CMakeFindDependencyMacro) -find_dependency(aws-c-io) - -if (@MQTT_WITH_WEBSOCKETS@) - find_dependency(aws-c-http) -endif() +find_dependency(aws-c-http) macro(aws_load_targets type) include(${CMAKE_CURRENT_LIST_DIR}/${type}/@PROJECT_NAME@-targets.cmake) endmacro() # try to load the lib follow BUILD_SHARED_LIBS. Fall back if not exist. -if (BUILD_SHARED_LIBS) - if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/shared") +if(BUILD_SHARED_LIBS) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/shared") aws_load_targets(shared) else() aws_load_targets(static) endif() else() - if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/static") + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/static") aws_load_targets(static) else() aws_load_targets(shared) From b3ac03e881135dd290585db9a4edb77c1610ea3f Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Mon, 5 Jun 2023 09:00:50 -0700 Subject: [PATCH 73/98] Fix a bug when unsub from a non-subscribed parent topic (#297) * fix corner case * clang-format * revert unwanted changes * improve comments --- source/topic_tree.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/topic_tree.c b/source/topic_tree.c index ca75c902..d2104ed3 100644 --- a/source/topic_tree.c +++ b/source/topic_tree.c @@ -433,6 +433,8 @@ static void s_topic_tree_action_commit(struct topic_tree_action *action, struct /* Clean up and delete */ s_topic_node_destroy(node, tree->allocator); } else { + // We do not delete the current node immediately as we would like to use + // it to update the topic filter of the remaining nodes destroy_current = true; } } else { @@ -450,8 +452,9 @@ static void s_topic_tree_action_commit(struct topic_tree_action *action, struct } } - /* If current owns the full string, go fixup the pointer references. */ - if (nodes_left > 0) { + /* If at least one node is destroyed and there is node(s) remaining in the branch, + * go fixup the topic filter reference . */ + if (nodes_left > 0 && destroy_current) { /* If a new viable topic filter is found once, it can be used for all parents. */ const struct aws_string *new_topic_filter = NULL; @@ -465,7 +468,7 @@ static void s_topic_tree_action_commit(struct topic_tree_action *action, struct size_t topic_offset = parent->topic.ptr - aws_string_bytes(parent->topic_filter) + parent->topic.len + 1; - /* -1 to avoid touching current */ + /* Loop through all remaining nodes to update the topic filters */ for (size_t i = nodes_left; i > 0; --i) { aws_array_list_get_at(&action->to_remove, &parent, i); AWS_ASSUME(parent); /* Must be in bounds */ @@ -493,7 +496,8 @@ static void s_topic_tree_action_commit(struct topic_tree_action *action, struct &parent->subtopics, s_topic_node_string_finder, (void *)&new_topic_filter); /* This would only happen if there is only one topic in subtopics (current's) and - * it has no children (in which case it should have been removed above). */ + * it has no children (in which case it should have been removed above as + `destroy_current` is set to true). */ AWS_ASSERT(new_topic_filter != old_topic_filter); /* Now that the new string has been found, the old one can be destroyed. */ From d3377e46af5eee1fde31e50aaeeac9103d2f451a Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Mon, 5 Jun 2023 10:29:40 -0700 Subject: [PATCH 74/98] Unit Test for #297 (#298) * update tests for unsubscribe crash * update comments --- tests/v3/topic_tree_test.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/v3/topic_tree_test.c b/tests/v3/topic_tree_test.c index 9858f934..8fabfa9f 100644 --- a/tests/v3/topic_tree_test.c +++ b/tests/v3/topic_tree_test.c @@ -141,6 +141,12 @@ static int s_mqtt_topic_tree_unsubscribe_fn(struct aws_allocator *allocator, voi ASSERT_SUCCESS(aws_mqtt_topic_tree_insert( &tree, topic_a_a_a, AWS_MQTT_QOS_AT_MOST_ONCE, &on_publish, s_string_clean_up, topic_a_a_a)); + /* At this moment, the topic_a_a was not inserted. Though the remove returns a success, the topic_a_a_a should still + * remained in the tree. + * The test is inspired by the ticket: https://github.com/awslabs/aws-crt-nodejs/issues/405. There was a crash when + * we unsubscribe from an unsubscribed parent topic. + * We fixed the issue in https://github.com/awslabs/aws-c-mqtt/pull/297 */ + ASSERT_SUCCESS(aws_mqtt_topic_tree_remove(&tree, &s_topic_a_a)); ASSERT_SUCCESS(aws_mqtt_topic_tree_remove(&tree, &s_topic_a_a_a)); /* Re-create, it was nuked by remove. */ topic_a_a_a = aws_string_new_from_array(allocator, s_topic_a_a_a.ptr, s_topic_a_a_a.len); From 8e40355e03b456bb7b4bf0cd37c5d878afa2476f Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 5 Jun 2023 12:34:05 -0700 Subject: [PATCH 75/98] 311 adapter lifecycle foundations (#293) * mqtt5 connection reset support * Rework adapter callback and ref counting Co-authored-by: Bret Ambrose --- include/aws/mqtt/mqtt.h | 1 + .../aws/mqtt/private/v5/mqtt5_client_impl.h | 36 +- source/mqtt.c | 6 + source/mqtt3_to_mqtt5_adapter.c | 489 ++++++++++++------ source/v5/mqtt5_client.c | 26 + 5 files changed, 403 insertions(+), 155 deletions(-) diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 6d14b020..76d38e6a 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -78,6 +78,7 @@ enum aws_mqtt_error { AWS_ERROR_MQTT5_INVALID_INBOUND_TOPIC_ALIAS, AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, AWS_ERROR_MQTT5_INVALID_UTF8_STRING, + AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index b63bdb02..b7824dee 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -489,6 +489,29 @@ struct aws_mqtt5_client { * with clean start set to false. */ bool has_connected_successfully; + + /* + * A flag that allows in-thread observers (currently the mqtt3_to_5 adapter) to signal that the connection + * should be torn down and re-established. Only relevant to the CONNECTING state which is not interruptible: + * + * If the mqtt5 client is in the CONNECTING state (ie waiting for bootstrap to complete) and the 3-adapter + * is asked to connect, then we *MUST* discard the in-progress connection attempt in order to guarantee the + * connection we establish uses all of the configuration parameters that are passed during the mqtt3 API's connect + * call (host, port, tls options, socket options, etc...). Since we can't interrupt the CONNECTING state, we + * instead set a flag that tells the mqtt5 client to tear down the connection as soon as the initial bootstrap + * completes. The reconnect will establish the requested connection using the parameters passed to + * the mqtt3 API. + * + * Rather than try and catch every escape path from CONNECTING, we lazily reset this flag to false when we + * enter the CONNECTING state. On a similar note, we only check this flag as we transition to MQTT_CONNECT. + * + * This flag is ultimately only needed when the 3 adapter and 5 client are used out-of-sync. If you use the + * 3 adapter exclusively after 5 client creation, it never comes into play. + * + * Even the adapter shouldn't manipulate this directly. Instead, use the aws_mqtt5_client_reset_connection private + * API to tear down an in-progress or established connection in response to a connect() request on the adapter. + */ + bool should_reset_connection; }; AWS_EXTERN_C_BEGIN @@ -638,10 +661,17 @@ AWS_MQTT_API void aws_mqtt5_client_statistics_change_operation_statistic_state( */ AWS_MQTT_API const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_client_state state); -/* - * Temporary, private API to turn on total incoming packet logging at the byte level. +/** + * An internal API used by the MQTT3 adapter to force any existing-or-in-progress connection to + * be torn down and re-established. Necessary because the MQTT3 interface allows overrides on a large number + * of configuration parameters through the connect() call. We must honor those parameters and the safest thing + * to do is to just throw away the current connection (if it exists) and make a new one. In the case that an MQTT5 + * client is being driven entirely by the MQTT3 adapter, this case never actually happens. + * + * @param client client to reset an existing or in-progress connection for + * @return true if a connection reset was triggered, false if there was nothing to do */ -AWS_MQTT_API void aws_mqtt5_client_enable_full_packet_logging(struct aws_mqtt5_client *client); +AWS_MQTT_API bool aws_mqtt5_client_reset_connection(struct aws_mqtt5_client *client); AWS_EXTERN_C_END diff --git a/source/mqtt.c b/source/mqtt.c index ef11253f..bd5f5824 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -214,6 +214,12 @@ bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter) AWS_DEFINE_ERROR_INFO_MQTT( AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, "Outgoing publish contained an invalid (too large or unknown) topic alias"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT5_INVALID_UTF8_STRING, + "Outbound packet contains invalid utf-8 data in a field that must be utf-8"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT, + "Mqtt5 connection was reset by the Mqtt3 adapter in order to guarantee correct connection configuration"), }; /* clang-format on */ #undef AWS_DEFINE_ERROR_INFO_MQTT diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index fa181492..3138badf 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -4,16 +4,45 @@ */ #include + +#include #include #include #include -enum aws_mqtt5_adapter_state { - AWS_MQTT5_AS_ENABLED, - AWS_MQTT5_AS_DISABLED, +/* + * The adapter maintains a notion of state based on how its 311 API has been used. This state guides how it handles + * external lifecycle events. + * + * Operational events are always relayed unless the adapter has been terminated. + */ +enum aws_mqtt_adapter_state { + + /* + * The 311 API has had connect() called but that connect has not yet resolved. + * + * If it resolves successfully we will move to the STAY_CONNECTED state which will relay lifecycle callbacks + * transparently. + * + * If it resolves unsuccessfully, we will move to the STAY_DISCONNECTED state where we will ignore lifecycle + * events because, from the 311 API's perspective, nothing should be getting emitted. + */ + AWS_MQTT_AS_FIRST_CONNECT, + + /* + * A call to the 311 connect API has resolved successfully. Relay all lifecycle events until told otherwise. + */ + AWS_MQTT_AS_STAY_CONNECTED, + + /* + * We have not observed a successful initial connection attempt via the 311 API (or disconnect has been + * invoked afterwards). Ignore all lifecycle events. + */ + AWS_MQTT_AS_STAY_DISCONNECTED, }; struct aws_mqtt_client_connection_5_impl { + struct aws_allocator *allocator; struct aws_mqtt_client_connection base; @@ -22,13 +51,61 @@ struct aws_mqtt_client_connection_5_impl { struct aws_mqtt5_listener *listener; struct aws_event_loop *loop; - struct aws_mutex lock; + /* + * An event-loop-internal flag that we can read to check to see if we're in the scope of a callback + * that has already locked the adapter's lock. Can only be referenced from the event loop thread. + * + * We use the flag to avoid deadlock in a few cases where we can re-enter the adapter logic from within a callback. + * It also provides a nice solution for the fact that we cannot safely upgrade a read lock to a write lock. + */ + bool in_synchronous_callback; + + /* + * What state the mqtt311 interface wants to be in. Always either AWS_MCS_CONNECTED or AWS_MCS_STOPPED. + * Tracked separately from the underlying mqtt5 client state in order to provide a coherent view that is + * narrowed solely to the adapter interface. + */ + enum aws_mqtt_adapter_state adapter_state; + + /* + * Tracks all references from external sources (ie users). Incremented and decremented by the public + * acquire/release APIs of the 311 connection. + * + * When this value drops to zero, the terminated flag is set and no further callbacks will be invoked. This + * also starts the asynchronous destruction process for the adapter. + */ + struct aws_ref_count external_refs; + /* + * Tracks all references to the adapter from internal sources (temporary async processes that need the + * adapter to stay alive for an interval of time, like sending tasks across thread boundaries). + * + * Starts with a single reference that is held until the adapter's listener has fully detached from the mqtt5 + * client. + * + * Once the internal ref count drops to zero, the adapter may be destroyed synchronously. + */ + struct aws_ref_count internal_refs; + + /* + * We use the adapter lock to guarantee that we can synchronously sever all callbacks from the mqtt5 client even + * though adapter shutdown is an asynchronous process. This means the lock is held during callbacks which is a + * departure from our normal usage patterns. We prevent deadlock (due to logical re-entry) by using the + * in_synchronous_callback flag. + * + * We hold a read lock when invoking callbacks and a write lock when setting terminated from false to true. + */ + struct aws_rw_lock lock; + + /* + * Synchronized data protected by the adapter lock. + */ struct { - enum aws_mqtt5_adapter_state state; - uint64_t ref_count; + bool terminated; } synced_data; + /* All fields after here are internal to the adapter event loop thread */ + /* 311 interface callbacks */ aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; void *on_interrupted_user_data; @@ -49,41 +126,157 @@ struct aws_mqtt_client_connection_5_impl { void *mqtt5_websocket_handshake_completion_user_data; }; -static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { - (void)event; -} - -static bool s_aws_mqtt5_listener_publish_received_adapter( - const struct aws_mqtt5_packet_publish_view *publish, - void *user_data) { - (void)publish; - (void)user_data; +struct aws_mqtt_adapter_final_destroy_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; +}; - return false; -} +static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + (void)status; -static void s_mqtt_client_connection_5_impl_finish_destroy(void *context) { - struct aws_mqtt_client_connection_5_impl *adapter = context; + struct aws_mqtt_adapter_final_destroy_task *destroy_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = destroy_task->connection->impl; if (adapter->client->config->websocket_handshake_transform_user_data == adapter) { /* * If the mqtt5 client is pointing to us for websocket transform, then erase that. The callback * is invoked from our pinned event loop so this is safe. + * + * TODO: It is possible that multiple adapters may have sequentially side-affected the websocket handshake. + * For now, in that case, subsequent connection attempts will probably not succeed. */ adapter->client->config->websocket_handshake_transform = NULL; adapter->client->config->websocket_handshake_transform_user_data = NULL; } adapter->client = aws_mqtt5_client_release(adapter->client); - aws_mutex_clean_up(&adapter->lock); + aws_rw_lock_clean_up(&adapter->lock); aws_mem_release(adapter->allocator, adapter); + + aws_mem_release(destroy_task->allocator, destroy_task); +} + +static struct aws_mqtt_adapter_final_destroy_task *s_aws_mqtt_adapter_final_destroy_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter) { + + struct aws_mqtt_adapter_final_destroy_task *destroy_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_final_destroy_task)); + + aws_task_init( + &destroy_task->task, s_mqtt_adapter_final_destroy_task_fn, (void *)destroy_task, "MqttAdapterFinalDestroy"); + destroy_task->allocator = adapter->allocator; + destroy_task->connection = &adapter->base; /* Do not acquire, we're at zero external and internal ref counts */ + + return destroy_task; +} + +static void s_aws_mqtt_adapter_final_destroy(struct aws_mqtt_client_connection_5_impl *adapter) { + + struct aws_mqtt_adapter_final_destroy_task *task = + s_aws_mqtt_adapter_final_destroy_task_new(adapter->allocator, adapter); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter final destroy task", (void *)adapter); + return; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); +} + +typedef int (*adapter_callback_fn)(struct aws_mqtt_client_connection_5_impl *adapter, void *context); + +/* + * The state/ref-count lock is held during synchronous callbacks to prevent invoking into something that is in the + * process of destruction. In general this isn't a performance worry since callbacks are invoked from a single thread: + * the event loop that the client and adapter are seated on. + * + * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown + * process since if we naively always locked, then an adapter release from within a callback would deadlock. + * + * We need a way to tell if locking will result in a deadlock. The specific case is invoking a synchronous + * callback from the event loop that re-enters the adapter logic via releasing the connection. We can recognize + * this scenario by setting/clearing an internal flag (in_synchronous_callback) and checking it only if we're + * in the event loop thread. If it's true, we know we've already locked at the beginning of the synchronous callback + * and we can safely skip locking, otherwise we must lock. + * + * This function gives us a helper for making these kinds of safe callbacks. We use it in: + * (1) Releasing the connection + * (2) Websocket handshake transform + * (3) Making lifecycle and operation callbacks on the mqtt311 interface + * + * It works by + * (1) Correctly determining if locking would deadlock and skipping lock only in that case, otherwise locking + * (2) Invoke the callback + * (3) Unlock if we locked in step (1) + * + * It also properly sets/clears the in_synchronous_callback flag if we're in the event loop and are not in + * a callback already. + */ +static int s_aws_mqtt5_adapter_perform_safe_callback( + struct aws_mqtt_client_connection_5_impl *adapter, + bool use_write_lock, + adapter_callback_fn callback_fn, + void *callback_user_data) { + + /* Step (1) - conditionally lock and manipulate the in_synchronous_callback flag */ + bool should_unlock = true; + bool clear_synchronous_callback_flag = false; + if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { + if (adapter->in_synchronous_callback) { + should_unlock = false; + } else { + adapter->in_synchronous_callback = true; + clear_synchronous_callback_flag = true; + } + } + + if (should_unlock) { + if (use_write_lock) { + aws_rw_lock_wlock(&adapter->lock); + } else { + aws_rw_lock_rlock(&adapter->lock); + } + } + + // Step (2) - perform the callback + int result = (*callback_fn)(adapter, callback_user_data); + + // Step (3) - undo anything we did in step (1) + if (should_unlock) { + if (use_write_lock) { + aws_rw_lock_wunlock(&adapter->lock); + } else { + aws_rw_lock_runlock(&adapter->lock); + } + } + + if (clear_synchronous_callback_flag) { + adapter->in_synchronous_callback = false; + } + + return result; +} + +static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +static bool s_aws_mqtt5_listener_publish_received_adapter( + const struct aws_mqtt5_packet_publish_view *publish, + void *user_data) { + (void)publish; + (void)user_data; + + return false; } struct aws_mqtt_set_interruption_handlers_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; void *on_interrupted_user_data; @@ -96,12 +289,12 @@ static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg (void)task; struct aws_mqtt_set_interruption_handlers_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - adapter->on_interrupted = set_task->on_interrupted; adapter->on_interrupted_user_data = set_task->on_interrupted_user_data; adapter->on_resumed = set_task->on_resumed; @@ -109,7 +302,7 @@ static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -128,7 +321,7 @@ static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interrupti aws_task_init( &set_task->task, s_set_interruption_handlers_task_fn, (void *)set_task, "SetInterruptionHandlersTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->on_interrupted = on_interrupted; set_task->on_interrupted_user_data = on_interrupted_user_data; set_task->on_resumed = on_resumed; @@ -160,7 +353,7 @@ static int s_aws_mqtt_client_connection_5_set_interruption_handlers( struct aws_mqtt_set_on_closed_handler_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; aws_mqtt_client_on_connection_closed_fn *on_closed; void *on_closed_user_data; @@ -170,18 +363,17 @@ static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, en (void)task; struct aws_mqtt_set_on_closed_handler_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - adapter->on_closed = set_task->on_closed; adapter->on_closed_user_data = set_task->on_closed_user_data; done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -197,7 +389,7 @@ static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_hand aws_task_init(&set_task->task, s_set_on_closed_handler_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->on_closed = on_closed; set_task->on_closed_user_data = on_closed_user_data; @@ -225,7 +417,7 @@ static int s_aws_mqtt_client_connection_5_set_on_closed_handler( struct aws_mqtt_set_on_any_publish_handler_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; aws_mqtt_client_publish_received_fn *on_any_publish; void *on_any_publish_user_data; @@ -235,18 +427,17 @@ static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *ar (void)task; struct aws_mqtt_set_on_any_publish_handler_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - adapter->on_any_publish = set_task->on_any_publish; adapter->on_any_publish_user_data = set_task->on_any_publish_user_data; done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -263,7 +454,7 @@ static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_pu aws_task_init( &set_task->task, s_set_on_any_publish_handler_task_fn, (void *)set_task, "SetOnAnyPublishHandlerTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->on_any_publish = on_any_publish; set_task->on_any_publish_user_data = on_any_publish_user_data; @@ -291,7 +482,7 @@ static int s_aws_mqtt_client_connection_5_set_on_any_publish_handler( struct aws_mqtt_set_reconnect_timeout_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; uint64_t min_timeout; uint64_t max_timeout; @@ -301,12 +492,11 @@ static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, en (void)task; struct aws_mqtt_set_reconnect_timeout_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ adapter->client->config->min_reconnect_delay_ms = set_task->min_timeout; adapter->client->config->max_reconnect_delay_ms = set_task->max_timeout; @@ -314,7 +504,7 @@ static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, en done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -330,7 +520,7 @@ static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_time aws_task_init(&set_task->task, s_set_reconnect_timeout_task_fn, (void *)set_task, "SetReconnectTimeoutTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->min_timeout = aws_min_u64(min_timeout, max_timeout); set_task->max_timeout = aws_max_u64(min_timeout, max_timeout); @@ -358,7 +548,7 @@ static int s_aws_mqtt_client_connection_5_set_reconnect_timeout( struct aws_mqtt_set_http_proxy_options_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; struct aws_http_proxy_config *proxy_config; }; @@ -367,12 +557,11 @@ static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, e (void)task; struct aws_mqtt_set_http_proxy_options_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ aws_http_proxy_config_destroy(adapter->client->config->http_proxy_config); @@ -388,7 +577,7 @@ static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, e done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); /* If the task was canceled we need to clean this up because it didn't get assigned to the mqtt5 client */ aws_http_proxy_config_destroy(set_task->proxy_config); @@ -413,7 +602,7 @@ static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_op aws_task_init(&set_task->task, s_set_http_proxy_options_task_fn, (void *)set_task, "SetHttpProxyOptionsTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->proxy_config = proxy_config; return set_task; @@ -440,7 +629,7 @@ static int s_aws_mqtt_client_connection_5_set_http_proxy_options( struct aws_mqtt_set_use_websockets_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; aws_mqtt_transform_websocket_handshake_fn *transformer; void *transformer_user_data; @@ -456,7 +645,29 @@ static void s_aws_mqtt5_adapter_websocket_handshake_completion_fn( (*adapter->mqtt5_websocket_handshake_completion_function)( request, error_code, adapter->mqtt5_websocket_handshake_completion_user_data); - aws_mqtt_client_connection_release(&adapter->base); + aws_ref_count_release(&adapter->internal_refs); +} + +struct aws_mqtt5_adapter_websocket_handshake_args { + bool chain_callback; + struct aws_http_message *input_request; + struct aws_http_message *output_request; + int completion_error_code; +}; + +static int s_safe_websocket_handshake_fn(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { + struct aws_mqtt5_adapter_websocket_handshake_args *args = context; + + if (adapter->synced_data.terminated) { + args->completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; + } else if (adapter->websocket_handshake_transformer == NULL) { + args->output_request = args->input_request; + } else { + aws_ref_count_acquire(&adapter->internal_refs); + args->chain_callback = true; + } + + return AWS_OP_SUCCESS; } static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( @@ -467,30 +678,20 @@ static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( struct aws_mqtt_client_connection_5_impl *adapter = user_data; - bool chain_callback = false; - struct aws_http_message *completion_request = NULL; - int completion_error_code = AWS_ERROR_SUCCESS; - aws_mutex_lock(&adapter->lock); - - if (adapter->synced_data.state != AWS_MQTT5_AS_ENABLED) { - completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; - } else if (adapter->websocket_handshake_transformer == NULL) { - completion_request = request; - } else { - ++adapter->synced_data.ref_count; - chain_callback = true; - } + struct aws_mqtt5_adapter_websocket_handshake_args args = { + .input_request = request, + }; - aws_mutex_unlock(&adapter->lock); + s_aws_mqtt5_adapter_perform_safe_callback(adapter, false, s_safe_websocket_handshake_fn, &args); - if (chain_callback) { + if (args.chain_callback) { adapter->mqtt5_websocket_handshake_completion_function = complete_fn; adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; (*adapter->websocket_handshake_transformer)( request, user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); } else { - (*complete_fn)(completion_request, completion_error_code, complete_ctx); + (*complete_fn)(args.output_request, args.completion_error_code, complete_ctx); } } @@ -498,12 +699,11 @@ static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum (void)task; struct aws_mqtt_set_use_websockets_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - adapter->websocket_handshake_transformer = set_task->transformer; adapter->websocket_handshake_transformer_user_data = set_task->transformer_user_data; @@ -513,7 +713,7 @@ static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -529,7 +729,7 @@ static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_ta aws_task_init(&set_task->task, s_set_use_websockets_task_fn, (void *)set_task, "SetUseWebsocketsTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->transformer = transformer; set_task->transformer_user_data = transformer_user_data; @@ -564,7 +764,7 @@ static int s_aws_mqtt_client_connection_5_use_websockets( struct aws_mqtt_set_host_resolution_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; struct aws_host_resolution_config host_resolution_config; }; @@ -573,18 +773,17 @@ static void s_set_host_resolution_task_fn(struct aws_task *task, void *arg, enum (void)task; struct aws_mqtt_set_host_resolution_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ adapter->client->config->host_resolution_override = set_task->host_resolution_config; done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); aws_mem_release(set_task->allocator, set_task); } @@ -599,7 +798,7 @@ static struct aws_mqtt_set_host_resolution_task *s_aws_mqtt_set_host_resolution_ aws_task_init(&set_task->task, s_set_host_resolution_task_fn, (void *)set_task, "SetHostResolutionTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->host_resolution_config = *host_resolution_config; return set_task; @@ -626,7 +825,7 @@ static int s_aws_mqtt_client_connection_5_set_host_resolution_options( struct aws_mqtt_set_will_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; struct aws_byte_buf topic_buffer; enum aws_mqtt_qos qos; @@ -649,12 +848,11 @@ static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_s (void)task; struct aws_mqtt_set_will_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ struct aws_mqtt5_packet_connect_storage *connect = adapter->client->config->connect; @@ -681,7 +879,7 @@ static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_s done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); s_aws_mqtt_set_will_task_destroy(set_task); } @@ -702,7 +900,7 @@ static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( aws_task_init(&set_task->task, s_set_will_task_fn, (void *)set_task, "SetWillTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); set_task->qos = qos; set_task->retain = retain; @@ -738,7 +936,7 @@ static int s_aws_mqtt_client_connection_5_set_will( struct aws_mqtt_set_login_task { struct aws_task task; struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; + struct aws_mqtt_client_connection_5_impl *adapter; struct aws_byte_buf username_buffer; struct aws_byte_buf password_buffer; @@ -759,11 +957,11 @@ static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_ (void)task; struct aws_mqtt_set_login_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; if (status != AWS_TASK_STATUS_RUN_READY) { goto done; } - struct aws_mqtt_client_connection_5_impl *adapter = set_task->connection->impl; struct aws_byte_cursor username_cursor = aws_byte_cursor_from_buf(&set_task->username_buffer); struct aws_byte_cursor password_cursor = aws_byte_cursor_from_buf(&set_task->password_buffer); @@ -802,7 +1000,7 @@ static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_ done: - aws_mqtt_client_connection_release(set_task->connection); + aws_ref_count_release(&adapter->internal_refs); s_aws_mqtt_set_login_task_destroy(set_task); } @@ -817,7 +1015,7 @@ static struct aws_mqtt_set_login_task *s_aws_mqtt_set_login_task_new( aws_task_init(&set_task->task, s_set_login_task_fn, (void *)set_task, "SetLoginTask"); set_task->allocator = adapter->allocator; - set_task->connection = aws_mqtt_client_connection_acquire(&adapter->base); + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); if (username != NULL) { aws_byte_buf_init_copy_from_cursor(&set_task->username_buffer, allocator, *username); @@ -849,84 +1047,68 @@ static int s_aws_mqtt_client_connection_5_set_login( return AWS_OP_SUCCESS; } +static void s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + s_aws_mqtt_adapter_final_destroy(adapter); +} + +static void s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + /* + * Release the single internal reference that we started with. Only ephemeral references for cross-thread + * tasks might remain, and they will disappear quickly. + */ + aws_ref_count_release(&adapter->internal_refs); +} + static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire(void *impl) { struct aws_mqtt_client_connection_5_impl *adapter = impl; - aws_mutex_lock(&adapter->lock); - AWS_FATAL_ASSERT(adapter->synced_data.ref_count > 0); - ++adapter->synced_data.ref_count; - aws_mutex_unlock(&adapter->lock); + aws_ref_count_acquire(&adapter->external_refs); return &adapter->base; } -/* - * The lock is held during callbacks to prevent invoking into something that is in the process of - * destruction. In general this isn't a performance worry since callbacks are invoked from a single - * thread: the event loop that the client and adapter are seated on. - * - * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown - * process since if we naively always locked, then an adapter release inside a callback would deadlock. - * - * On the surface, it seems reasonable that if we're in the event loop thread we could just skip - * locking entirely (because we've already locked it at the start of the callback). Unfortunately, this isn't safe - * because we don't actually truly know we're in our mqtt5 client's callback; we could be in some other - * client/connection's callback that happens to be seated on the same event loop. And while it's true that because - * of the thread seating, nothing will be interfering with our shared state manipulation, there's one final - * consideration which forces us to *try* to lock: - * - * Dependent on the memory model of the CPU architecture, changes to shared state, even if "safe" from data - * races across threads, may not become visible to other cores on the same CPU unless some kind of synchronization - * primitive (memory barrier) is invoked. So in this extremely unlikely case, we use try-lock to guarantee that a - * synchronization primitive is invoked when disable is coming through a callback from something else on the same - * event loop. - * - * In the case that we're in our mqtt5 client's callback, the lock is already held, try fails, and the unlock at - * the end of the callback will suffice for cache flush and synchronization. - * - * In the case that we're in something else's callback on the same thread, the try succeeds and its followup - * unlock here will suffice for cache flush and synchronization. - * - * Some after-the-fact analysis hints that this extra step (in the case where we are in the event loop) may - * be unnecessary because the only reader of the state change is the event loop thread itself. I don't feel - * confident enough in the memory semantics of thread<->CPU core bindings to relax this though. - */ -static void s_aws_mqtt_client_connection_5_release(void *impl) { +static int s_decref_for_shutdown(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { + (void)context; + + adapter->synced_data.terminated = true; + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt3_to_mqtt5_adapter_on_zero_external_refs(void *impl) { struct aws_mqtt_client_connection_5_impl *adapter = impl; - bool start_shutdown = false; - bool lock_succeeded = aws_mutex_try_lock(&adapter->lock) == AWS_OP_SUCCESS; + s_aws_mqtt5_adapter_perform_safe_callback(adapter, true, s_decref_for_shutdown, NULL); - AWS_FATAL_ASSERT(adapter->synced_data.ref_count > 0); - --adapter->synced_data.ref_count; - if (adapter->synced_data.ref_count == 0) { - adapter->synced_data.state = AWS_MQTT5_AS_DISABLED; - start_shutdown = true; - } + /* + * When the adapter's exernal ref count goes to zero, here's what we want to do: + * + * (1) Put the adapter into the terminated state, which tells it to stop processing callbacks from the mqtt5 + * client + * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user + * of it) + * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we + * are guaranteed that no more callbacks from the mqtt5 client will reach us. + * (4) Release the single internal ref we started with when the adapter was created. + * (5) On last internal ref, we can safely release the mqtt5 client and synchronously clean up all other + * resources + * + * Step (1) was done within the lock-guarded safe callback above. + * Step (2) is done here. + * Steps (3) and (4) are accomplished by s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached + * Step (5) is completed by s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs + */ + aws_mqtt5_listener_release(adapter->listener); +} - if (lock_succeeded) { - aws_mutex_unlock(&adapter->lock); - } +static void s_aws_mqtt_client_connection_5_release(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; - if (start_shutdown) { - /* - * When the adapter's ref count goes to zero, here's what we want to do: - * - * (1) Put the adapter into the disabled mode, which tells it to stop processing callbacks from the mqtt5 - * client - * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user - * of it) - * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we - * are guaranteed that no more callbacks from the mqtt5 client will reach us. We can safely release the - * mqtt5 client. - * (4) Synchronously clean up all further resources. - * - * Step (1) was done within the lock above. - * Step (2) is done here. - * Steps (3) and (4) are accomplished via s_mqtt_client_connection_5_impl_finish_destroy. - */ - aws_mqtt5_listener_release(adapter->listener); - } + aws_ref_count_release(&adapter->external_refs); } static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { @@ -967,18 +1149,21 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli adapter->client = aws_mqtt5_client_acquire(client); adapter->loop = client->loop; + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + + aws_ref_count_init(&adapter->external_refs, adapter, s_aws_mqtt3_to_mqtt5_adapter_on_zero_external_refs); + aws_ref_count_init(&adapter->internal_refs, adapter, s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs); - aws_mutex_init(&adapter->lock); + aws_rw_lock_init(&adapter->lock); /* * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." * We'll enable the adapter as soon as they try to connect via the 311 interface. This * also ties in to how we simulate the 311 implementation's don't-reconnect-if-initial-connect-fails logic. * The 5 client will continue to try and reconnect, but the adapter will go disabled making it seem to the 311 - * user that is is offline. + * user that it is offline. */ - adapter->synced_data.state = AWS_MQTT5_AS_DISABLED; - adapter->synced_data.ref_count = 1; + adapter->synced_data.terminated = false; struct aws_mqtt5_listener_config listener_config = { .client = client, @@ -989,7 +1174,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli .lifecycle_event_handler = s_aws_mqtt5_client_connection_event_callback_adapter, .lifecycle_event_handler_user_data = adapter, }, - .termination_callback = s_mqtt_client_connection_5_impl_finish_destroy, + .termination_callback = s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached, .termination_callback_user_data = adapter, }; adapter->listener = aws_mqtt5_listener_new(allocator, &listener_config); diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 79701bbc..50aa43aa 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -1039,6 +1039,7 @@ static void s_change_current_state_to_connecting(struct aws_mqtt5_client *client client->current_state = AWS_MCS_CONNECTING; client->clean_disconnect_error_code = AWS_ERROR_SUCCESS; + client->should_reset_connection = false; s_aws_mqtt5_client_emit_connecting_lifecycle_event(client); @@ -1200,6 +1201,11 @@ static void s_change_current_state_to_mqtt_connect(struct aws_mqtt5_client *clie AWS_FATAL_ASSERT(client->operational_state.current_operation == NULL); client->current_state = AWS_MCS_MQTT_CONNECT; + if (client->should_reset_connection) { + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT); + return; + } + client->operational_state.pending_write_completion = false; aws_mqtt5_encoder_reset(&client->encoder); @@ -3345,3 +3351,23 @@ void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt stats->unacked_operation_size = (uint64_t)aws_atomic_load_int(&client->operation_statistics_impl.unacked_operation_size_atomic); } + +bool aws_mqtt5_client_reset_connection(struct aws_mqtt5_client *client) { + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); + + switch (client->current_state) { + case AWS_MCS_MQTT_CONNECT: + case AWS_MCS_CONNECTED: + s_aws_mqtt5_client_shutdown_channel(client, AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT); + return true; + + case AWS_MCS_CONNECTING: + client->should_reset_connection = true; + return true; + + default: + break; + } + + return false; +} From 59a1e891a9be856e389f2efa79b6426d2c2971d8 Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Fri, 23 Jun 2023 10:59:33 -0700 Subject: [PATCH 76/98] Extra Connection Callbacks in 311 (#300) * connection success/failure callbacks * setup vtable * update CR and trigger on_connection_success when connection resumes * update logging --- include/aws/mqtt/client.h | 38 +++++ include/aws/mqtt/private/client_impl.h | 4 + include/aws/mqtt/private/client_impl_shared.h | 7 + source/client.c | 26 ++++ source/client_channel_handler.c | 11 ++ source/client_impl_shared.c | 15 ++ source/mqtt3_to_mqtt5_adapter.c | 1 + tests/CMakeLists.txt | 2 + tests/v3/connection_state_test.c | 138 ++++++++++++++++++ 9 files changed, 242 insertions(+) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 5ee893f7..0dd2bc4f 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -66,6 +66,27 @@ typedef void(aws_mqtt_client_on_connection_complete_fn)( bool session_present, void *userdata); +/* Called when a connection attempt succeed (with a successful CONNACK) + * + * The callback is derived from aws_mqtt_client_on_connection_complete_fn. + * It gets triggered when connection succeed (with a successful CONNACK) + */ +typedef void(aws_mqtt_client_on_connection_success_fn)( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata); + +/* Called if the connection attempt failed. + * + * The callback is derived from aws_mqtt_client_on_connection_complete_fn. + * It gets triggered when connection failed. + */ +typedef void(aws_mqtt_client_on_connection_failure_fn)( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata); + /* Called if the connection to the server is lost. */ typedef void(aws_mqtt_client_on_connection_interrupted_fn)( struct aws_mqtt_client_connection *connection, @@ -422,6 +443,23 @@ int aws_mqtt_client_connection_set_reconnect_timeout( uint64_t min_timeout, uint64_t max_timeout); +/** + * Sets the callbacks to call when a connection succeeds or fails + * + * \param[in] connection The connection object + * \param[in] on_connection_success The function to call when a connection is successful or gets resumed + * \param[in] on_connection_success_ud Userdata for on_connection_success + * \param[in] on_connection_failure The function to call when a connection fails + * \param[in] on_connection_failure_ud Userdata for on_connection_failure + */ +AWS_MQTT_API +int aws_mqtt_client_connection_set_connection_result_handlers( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_ud, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_ud); + /** * Sets the callbacks to call when a connection is interrupted and resumed. * diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 5822fa6c..169dadb9 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -230,6 +230,10 @@ struct aws_mqtt_client_connection_311_impl { /* User connection callbacks */ aws_mqtt_client_on_connection_complete_fn *on_connection_complete; void *on_connection_complete_ud; + aws_mqtt_client_on_connection_success_fn *on_connection_success; + void *on_connection_success_ud; + aws_mqtt_client_on_connection_failure_fn *on_connection_failure; + void *on_connection_failure_ud; aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; void *on_interrupted_ud; aws_mqtt_client_on_connection_resumed_fn *on_resumed; diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index 8c1116e8..10ac73dc 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -45,6 +45,13 @@ struct aws_mqtt_client_connection_vtable { aws_mqtt_client_on_connection_resumed_fn *on_resumed, void *on_resumed_ud); + int (*set_connection_result_handlers)( + void *impl, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_ud, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_ud); + int (*set_connection_closed_handler_fn)( void *impl, aws_mqtt_client_on_connection_closed_fn *on_closed, diff --git a/source/client.c b/source/client.c index 291f4557..126c8ae2 100644 --- a/source/client.c +++ b/source/client.c @@ -455,6 +455,7 @@ static void s_mqtt_client_shutdown( "id=%p: Initial connection attempt failed, calling callback", (void *)connection); MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_connection_complete, error_code, 0, false); + MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_connection_failure, error_code); break; default: break; @@ -671,6 +672,7 @@ static void s_mqtt_client_init( handle_error: MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_connection_complete, aws_last_error(), 0, false); + MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_connection_failure, aws_last_error()); aws_channel_shutdown(channel, aws_last_error()); if (message) { @@ -1062,6 +1064,29 @@ static int s_aws_mqtt_client_connection_311_set_reconnect_timeout( return AWS_OP_SUCCESS; } +static int s_aws_mqtt_client_connection_311_set_connection_result_handlers( + void *impl, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_ud, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_ud) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + + AWS_PRECONDITION(connection); + if (s_check_connection_state_for_configuration(connection)) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Setting connection success and failure handlers", (void *)connection); + + connection->on_connection_success = on_connection_success; + connection->on_connection_success_ud = on_connection_success_ud; + connection->on_connection_failure = on_connection_failure; + connection->on_connection_failure_ud = on_connection_failure_ud; + + return AWS_OP_SUCCESS; +} + static int s_aws_mqtt_client_connection_311_set_connection_interruption_handlers( void *impl, aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, @@ -3136,6 +3161,7 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311 .set_http_proxy_options_fn = s_aws_mqtt_client_connection_311_set_http_proxy_options, .set_host_resolution_options_fn = s_aws_mqtt_client_connection_311_set_host_resolution_options, .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_311_set_reconnect_timeout, + .set_connection_result_handlers = s_aws_mqtt_client_connection_311_set_connection_result_handlers, .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_311_set_connection_interruption_handlers, .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_311_set_connection_closed_handler, .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_311_set_on_any_publish_handler, diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index fa2237bc..70b77c1b 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -201,6 +201,17 @@ static int s_packet_handler_connack( connection, on_connection_complete, AWS_OP_SUCCESS, connack.connect_return_code, connack.session_present); } + /* + * The on_connection_success would get triggered on the successful CONNACK. It invoked with both the first connect + * attempt and reconnection attempt as Mqtt5 does not have on_resume callback for reconnection. + */ + AWS_LOGF_TRACE( + AWS_LS_MQTT_CLIENT, + "id=%p: received a successful CONNACK, invoking on_connection_success callback", + (void *)connection); + MQTT_CLIENT_CALL_CALLBACK_ARGS( + connection, on_connection_success, connack.connect_return_code, connack.session_present); + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: connection callback completed", (void *)connection); s_update_next_ping_time(connection); diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index 2bffd896..c7d8b483 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -71,6 +71,21 @@ int aws_mqtt_client_connection_set_reconnect_timeout( return (*connection->vtable->set_reconnect_timeout_fn)(connection->impl, min_timeout, max_timeout); } +int aws_mqtt_client_connection_set_connection_result_handlers( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_ud, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_ud) { + + return (*connection->vtable->set_connection_result_handlers)( + connection->impl, + on_connection_success, + on_connection_success_ud, + on_connection_failure, + on_connection_failure_ud); +} + int aws_mqtt_client_connection_set_connection_interruption_handlers( struct aws_mqtt_client_connection *connection, aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index 3138badf..0c75510e 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -1120,6 +1120,7 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_v .set_http_proxy_options_fn = s_aws_mqtt_client_connection_5_set_http_proxy_options, .set_host_resolution_options_fn = s_aws_mqtt_client_connection_5_set_host_resolution_options, .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_5_set_reconnect_timeout, + .set_connection_result_handlers = NULL, // TODO: Need update with introduction of mqtt5 lifeCycleEventCallback .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a74bda9d..2a0eab99 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,8 @@ add_test_case(mqtt_connection_interrupted) add_test_case(mqtt_connection_any_publish) add_test_case(mqtt_connection_timeout) add_test_case(mqtt_connection_connack_timeout) +add_test_case(mqtt_connection_failure_callback) +add_test_case(mqtt_connection_success_callback) add_test_case(mqtt_connect_subscribe) add_test_case(mqtt_connect_subscribe_fail_from_broker) add_test_case(mqtt_connect_subscribe_multi) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index da59b860..854b0b59 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -52,6 +52,8 @@ struct mqtt_connection_state_test { bool session_present; bool connection_completed; + bool connection_success; + bool connection_failure; bool client_disconnect_completed; bool server_disconnect_completed; bool connection_interrupted; @@ -209,6 +211,61 @@ static void s_wait_for_reconnect_to_complete(struct mqtt_connection_state_test * aws_mutex_unlock(&state_test_data->lock); } +static void s_on_connection_success( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->session_present = session_present; + state_test_data->mqtt_return_code = return_code; + state_test_data->connection_success = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_on_connection_failure(struct aws_mqtt_client_connection *connection, int error_code, void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->error = error_code; + state_test_data->connection_failure = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_connection_succeed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_success; +} + +static bool s_is_connection_failed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_failure; +} + +static void s_wait_for_connection_to_succeed(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_succeed, state_test_data); + state_test_data->connection_success = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_wait_for_connection_to_fail(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_failed, state_test_data); + state_test_data->connection_failure = false; + aws_mutex_unlock(&state_test_data->lock); +} + /** sets up a unix domain socket server and socket options. Creates an mqtt connection configured to use * the domain socket. */ @@ -278,6 +335,13 @@ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { s_on_connection_resumed, state_test_data)); + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_result_handlers( + state_test_data->mqtt_connection, + s_on_connection_success, + state_test_data, + s_on_connection_failure, + state_test_data)); + ASSERT_SUCCESS(aws_mqtt_client_connection_set_on_any_publish_handler( state_test_data->mqtt_connection, s_on_any_publish_received, state_test_data)); @@ -971,6 +1035,80 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) +/* Use the connack timeout to test the connection failure callback */ +static int s_test_mqtt_connection_failure_callback_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + }; + + mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_fail(state_test_data); + + ASSERT_INT_EQUALS(AWS_ERROR_MQTT_TIMEOUT, state_test_data->error); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_failure_callback, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_failure_callback_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/* Quick test the connection succeed callback */ +static int s_test_mqtt_connection_success_callback_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_succeed(state_test_data); + + /* Decode all received packets by mock server */ + ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); + + ASSERT_UINT_EQUALS(1, mqtt_mock_server_decoded_packets_count(state_test_data->mock_server)); + struct mqtt_decoded_packet *received_packet = + mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 0); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_CONNECT, received_packet->type); + ASSERT_UINT_EQUALS(connection_options.clean_session, received_packet->clean_session); + ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->client_identifier, &connection_options.client_id)); + + // Disconnect and finish + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_success_callback, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_success_callback_fn, + s_clean_up_mqtt_server_fn, + &test_data) + /* Subscribe to a topic prior to connection, make a CONNECT, have the server send PUBLISH messages, * make sure they're received, then send a DISCONNECT. */ static int s_test_mqtt_subscribe_fn(struct aws_allocator *allocator, void *ctx) { From 83e7abe4fbc34c0e4737c1472e585fdf47a5f4dd Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Sat, 24 Jun 2023 19:51:26 -0700 Subject: [PATCH 77/98] Adapter lifecycle take2 (#296) Mqtt3 to 5 adapter lifecycle implementation Maps the MQTT5 lifecycle event set as best it can to the MQTT311 implementation's event set. Given that the two interfaces can undermine one another, this is a best effort implementation; there are multiple situations where we have to make a judgement call. --- .../aws/mqtt/private/v5/mqtt5_client_impl.h | 13 + .../mqtt/private/v5/mqtt5_options_storage.h | 4 + source/mqtt3_to_mqtt5_adapter.c | 502 +++++- source/v5/mqtt5_client.c | 30 +- source/v5/mqtt5_options_storage.c | 39 +- tests/CMakeLists.txt | 13 + tests/v5/mqtt3_to_mqtt5_adapter_tests.c | 1476 +++++++++++++++++ tests/v5/mqtt5_client_tests.c | 698 +++----- tests/v5/mqtt5_testing_utils.h | 64 + 9 files changed, 2307 insertions(+), 532 deletions(-) create mode 100644 tests/v5/mqtt3_to_mqtt5_adapter_tests.c diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index b7824dee..19b5e875 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -673,6 +673,19 @@ AWS_MQTT_API const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_clien */ AWS_MQTT_API bool aws_mqtt5_client_reset_connection(struct aws_mqtt5_client *client); +/** + * Event-loop-internal API used to switch the client's desired state. Used by both start() and stop() cross-thread + * tasks as well as by the 3-to-5 adapter to make changes synchronously (when in the event loop). + * + * @param client mqtt5 client to update desired state for + * @param desired_state new desired state + * @param disconnect_op optional description of a DISCONNECT packet to send as part of a stop command + */ +AWS_MQTT_API void aws_mqtt5_client_change_desired_state( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state desired_state, + struct aws_mqtt5_operation_disconnect *disconnect_op); + AWS_EXTERN_C_END #endif /* AWS_MQTT_MQTT5_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index 57b68c07..ed159972 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -341,6 +341,10 @@ AWS_MQTT_API void aws_mqtt5_client_options_storage_log( const struct aws_mqtt5_client_options_storage *options_storage, enum aws_log_level level); +AWS_MQTT_API bool aws_mqtt5_client_keep_alive_options_are_valid( + uint16_t keep_alive_interval_seconds, + uint32_t ping_timeout_ms); + AWS_EXTERN_C_END #endif /* AWS_MQTT_MQTT5_OPERATION_H */ diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c index 0c75510e..ef353525 100644 --- a/source/mqtt3_to_mqtt5_adapter.c +++ b/source/mqtt3_to_mqtt5_adapter.c @@ -5,7 +5,9 @@ #include +#include #include + #include #include #include @@ -61,9 +63,9 @@ struct aws_mqtt_client_connection_5_impl { bool in_synchronous_callback; /* - * What state the mqtt311 interface wants to be in. Always either AWS_MCS_CONNECTED or AWS_MCS_STOPPED. - * Tracked separately from the underlying mqtt5 client state in order to provide a coherent view that is - * narrowed solely to the adapter interface. + * The current adapter state based on the sequence of connect(), disconnect(), and connection completion events. + * This affects how the adapter reacts to incoming mqtt5 events. Under certain conditions, we may change + * this state value based on unexpected events (stopping the mqtt5 client underneath the adapter, for example) */ enum aws_mqtt_adapter_state adapter_state; @@ -124,6 +126,13 @@ struct aws_mqtt_client_connection_5_impl { aws_mqtt5_transform_websocket_handshake_complete_fn *mqtt5_websocket_handshake_completion_function; void *mqtt5_websocket_handshake_completion_user_data; + + /* (mutually exclusive) 311 interface one-time transient callbacks */ + aws_mqtt_client_on_disconnect_fn *on_disconnect; + void *on_disconnect_user_data; + + aws_mqtt_client_on_connection_complete_fn *on_connection_complete; + void *on_connection_complete_user_data; }; struct aws_mqtt_adapter_final_destroy_task { @@ -260,8 +269,485 @@ static int s_aws_mqtt5_adapter_perform_safe_callback( return result; } -static void s_aws_mqtt5_client_connection_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { - (void)event; +struct aws_mqtt_adapter_disconnect_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_disconnect_fn *on_disconnect; + void *on_disconnect_user_data; +}; + +static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); + +static struct aws_mqtt_adapter_disconnect_task *s_aws_mqtt_adapter_disconnect_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_disconnect_fn *on_disconnect, + void *on_disconnect_user_data) { + + struct aws_mqtt_adapter_disconnect_task *disconnect_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_disconnect_task)); + + aws_task_init( + &disconnect_task->task, s_adapter_disconnect_task_fn, (void *)disconnect_task, "AdapterDisconnectTask"); + disconnect_task->allocator = adapter->allocator; + disconnect_task->adapter = + (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + disconnect_task->on_disconnect = on_disconnect; + disconnect_task->on_disconnect_user_data = on_disconnect_user_data; + + return disconnect_task; +} + +static int s_aws_mqtt_client_connection_5_disconnect( + void *impl, + aws_mqtt_client_on_disconnect_fn *on_disconnect, + void *on_disconnect_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_adapter_disconnect_task *task = + s_aws_mqtt_adapter_disconnect_task_new(adapter->allocator, adapter, on_disconnect, on_disconnect_user_data); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter disconnect task", (void *)adapter); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_adapter_connect_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_byte_buf host_name; + uint16_t port; + struct aws_socket_options socket_options; + struct aws_tls_connection_options *tls_options_ptr; + struct aws_tls_connection_options tls_options; + + struct aws_byte_buf client_id; + uint16_t keep_alive_time_secs; + uint32_t ping_timeout_ms; + uint32_t protocol_operation_timeout_ms; + aws_mqtt_client_on_connection_complete_fn *on_connection_complete; + void *on_connection_complete_user_data; + bool clean_session; +}; + +static void s_aws_mqtt_adapter_connect_task_destroy(struct aws_mqtt_adapter_connect_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up(&task->host_name); + aws_byte_buf_clean_up(&task->client_id); + + if (task->tls_options_ptr) { + aws_tls_connection_options_clean_up(task->tls_options_ptr); + } + + aws_mem_release(task->allocator, task); +} + +static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); + +static struct aws_mqtt_adapter_connect_task *s_aws_mqtt_adapter_connect_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_mqtt_connection_options *connection_options) { + + struct aws_mqtt_adapter_connect_task *connect_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_connect_task)); + + aws_task_init(&connect_task->task, s_adapter_connect_task_fn, (void *)connect_task, "AdapterConnectTask"); + connect_task->allocator = adapter->allocator; + connect_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + aws_byte_buf_init_copy_from_cursor(&connect_task->host_name, allocator, connection_options->host_name); + connect_task->port = connection_options->port; + connect_task->socket_options = *connection_options->socket_options; + if (connection_options->tls_options) { + aws_tls_connection_options_copy(&connect_task->tls_options, connection_options->tls_options); + connect_task->tls_options_ptr = &connect_task->tls_options; + } + + aws_byte_buf_init_copy_from_cursor(&connect_task->client_id, allocator, connection_options->client_id); + + connect_task->keep_alive_time_secs = connection_options->keep_alive_time_secs; + connect_task->ping_timeout_ms = connection_options->ping_timeout_ms; + connect_task->protocol_operation_timeout_ms = connection_options->protocol_operation_timeout_ms; + connect_task->on_connection_complete = connection_options->on_connection_complete; + connect_task->on_connection_complete_user_data = connection_options->user_data; + connect_task->clean_session = connection_options->clean_session; + + return connect_task; +} + +static int s_validate_adapter_connection_options(const struct aws_mqtt_connection_options *connection_options) { + if (connection_options == NULL) { + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (connection_options->host_name.len == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "host name not set in MQTT client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + /* forbid no-timeout until someone convinces me otherwise */ + if (connection_options->socket_options != NULL) { + if (connection_options->socket_options->type == AWS_SOCKET_DGRAM || + connection_options->socket_options->connect_timeout_ms == 0) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid socket options in MQTT client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (!aws_mqtt5_client_keep_alive_options_are_valid( + connection_options->keep_alive_time_secs, connection_options->ping_timeout_ms)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt_client_connection_5_connect( + void *impl, + const struct aws_mqtt_connection_options *connection_options) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (s_validate_adapter_connection_options(connection_options)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_adapter_connect_task *task = + s_aws_mqtt_adapter_connect_task_new(adapter->allocator, adapter, connection_options); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter connect task", (void *)adapter); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt3_to_mqtt5_adapter_safe_lifecycle_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + const struct aws_mqtt5_client_lifecycle_event *event = context; + + /* + * Never invoke a callback after termination + */ + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + switch (event->event_type) { + + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + /* + * If the 311 view is that this is an initial connection attempt, then invoke the completion callback + * and move to the stay-connected state. + */ + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, + event->error_code, + 0, + event->settings->rejoined_session, + adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + adapter->adapter_state = AWS_MQTT_AS_STAY_CONNECTED; + } else if (adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED) { + /* + * If the 311 view is that we're in the stay-connected state (ie we've successfully done or simulated + * an initial connection), then invoke the connection resumption callback. + */ + if (adapter->on_resumed != NULL) { + (*adapter->on_resumed)( + &adapter->base, 0, event->settings->rejoined_session, adapter->on_resumed_user_data); + } + } + break; + + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + /* + * The MQTT311 interface only cares about connection failures when it's the initial connection attempt + * after a call to connect(). Since an adapter connect() can sever an existing connection (with an + * error code of AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) we only react to connection failures + * if + * (1) the error code is not AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT and + * (2) we're in the FIRST_CONNECT state + * + * Only if both of these are true should we invoke the connection completion callback with a failure and + * put the adapter into the "disconnected" state, simulating the way the 311 client stops after an + * initial connection failure. + */ + if (event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT && + adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, event->error_code, 0, false, adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + } + break; + + case AWS_MQTT5_CLET_DISCONNECTION: + /* + * If the 311 view is that we're in the stay-connected state (ie we've successfully done or simulated + * an initial connection), then invoke the connection interrupted callback. + */ + if (adapter->on_interrupted != NULL && adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED && + event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) { + + (*adapter->on_interrupted)(&adapter->base, event->error_code, adapter->on_interrupted_user_data); + } + break; + + case AWS_MQTT5_CLET_STOPPED: + /* If an MQTT311-view user is waiting on a disconnect callback, invoke it */ + if (adapter->on_disconnect) { + (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); + + adapter->on_disconnect = NULL; + adapter->on_disconnect_user_data = NULL; + } + + if (adapter->on_closed) { + (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); + } + + /* + * Judgement call: If the mqtt5 client is stopped behind our back, it seems better to transition to the + * disconnected state (which only requires a connect() to restart) then stay in the STAY_CONNECTED state + * which currently requires a disconnect() and then a connect() to restore connectivity. + * + * ToDo: what if we disabled mqtt5 client start/stop somehow while the adapter is attached, preventing + * the potential to backstab each other? Unfortunately neither start() nor stop() have an error reporting + * mechanism. + */ + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + break; + + default: + break; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_client_lifecycle_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { + struct aws_mqtt_client_connection_5_impl *adapter = event->user_data; + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_lifecycle_handler, (void *)event); +} + +static int s_aws_mqtt3_to_mqtt5_adapter_safe_disconnect_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + struct aws_mqtt_adapter_disconnect_task *disconnect_task = context; + + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + /* + * If we're already disconnected (from the 311 perspective only), then invoke the callback and return + */ + if (adapter->adapter_state == AWS_MQTT_AS_STAY_DISCONNECTED) { + if (disconnect_task->on_disconnect) { + (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); + } + + return AWS_OP_SUCCESS; + } + + /* + * If we had a pending first connect, then notify failure + */ + if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, + AWS_ERROR_MQTT_CONNECTION_SHUTDOWN, + 0, + false, + adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + } + + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + + bool invoke_callbacks = true; + if (adapter->client->desired_state != AWS_MCS_STOPPED) { + aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_STOPPED, NULL); + + adapter->on_disconnect = disconnect_task->on_disconnect; + adapter->on_disconnect_user_data = disconnect_task->on_disconnect_user_data; + invoke_callbacks = false; + } + + if (invoke_callbacks) { + if (disconnect_task->on_disconnect != NULL) { + (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); + } + + if (adapter->on_closed) { + (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); + } + } + + return AWS_OP_SUCCESS; +} + +static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_adapter_disconnect_task *disconnect_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = disconnect_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_disconnect_handler, disconnect_task); + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(disconnect_task->allocator, disconnect_task); +} + +static void s_aws_mqtt3_to_mqtt5_adapter_update_config_on_connect( + struct aws_mqtt_client_connection_5_impl *adapter, + struct aws_mqtt_adapter_connect_task *connect_task) { + struct aws_mqtt5_client_options_storage *config = adapter->client->config; + + aws_string_destroy(config->host_name); + config->host_name = aws_string_new_from_buf(adapter->allocator, &connect_task->host_name); + config->port = connect_task->port; + config->socket_options = connect_task->socket_options; + + if (config->tls_options_ptr) { + aws_tls_connection_options_clean_up(&config->tls_options); + config->tls_options_ptr = NULL; + } + + if (connect_task->tls_options_ptr) { + aws_tls_connection_options_copy(&config->tls_options, connect_task->tls_options_ptr); + config->tls_options_ptr = &config->tls_options; + } + + aws_byte_buf_clean_up(&adapter->client->negotiated_settings.client_id_storage); + aws_byte_buf_init_copy_from_cursor( + &adapter->client->negotiated_settings.client_id_storage, + adapter->allocator, + aws_byte_cursor_from_buf(&connect_task->client_id)); + + config->connect->storage_view.keep_alive_interval_seconds = connect_task->keep_alive_time_secs; + config->ping_timeout_ms = connect_task->ping_timeout_ms; + config->ack_timeout_seconds = aws_max_u64( + 1, + aws_timestamp_convert( + connect_task->protocol_operation_timeout_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL)); + if (connect_task->clean_session) { + config->session_behavior = AWS_MQTT5_CSBT_CLEAN; + config->connect->storage_view.session_expiry_interval_seconds = NULL; + } else { + config->session_behavior = AWS_MQTT5_CSBT_REJOIN_ALWAYS; + /* This is a judgement call to translate session expiry to the maximum possible allowed by AWS IoT Core */ + config->connect->session_expiry_interval_seconds = 7 * 24 * 60 * 60; + config->connect->storage_view.session_expiry_interval_seconds = + &config->connect->session_expiry_interval_seconds; + } +} + +static int s_aws_mqtt3_to_mqtt5_adapter_safe_connect_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + struct aws_mqtt_adapter_connect_task *connect_task = context; + + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + if (adapter->adapter_state != AWS_MQTT_AS_STAY_DISCONNECTED) { + if (connect_task->on_connection_complete) { + (*connect_task->on_connection_complete)( + &adapter->base, + AWS_ERROR_MQTT_ALREADY_CONNECTED, + 0, + false, + connect_task->on_connection_complete_user_data); + } + return AWS_OP_SUCCESS; + } + + if (adapter->on_disconnect) { + (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); + + adapter->on_disconnect = NULL; + adapter->on_disconnect_user_data = NULL; + } + + adapter->adapter_state = AWS_MQTT_AS_FIRST_CONNECT; + + /* Update mqtt5 config */ + s_aws_mqtt3_to_mqtt5_adapter_update_config_on_connect(adapter, connect_task); + + aws_mqtt5_client_reset_connection(adapter->client); + + aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_CONNECTED, NULL); + + adapter->on_connection_complete = connect_task->on_connection_complete; + adapter->on_connection_complete_user_data = connect_task->on_connection_complete_user_data; + + return AWS_OP_SUCCESS; +} + +static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_adapter_connect_task *connect_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = connect_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_connect_handler, connect_task); + +done: + + aws_ref_count_release(&adapter->internal_refs); + + s_aws_mqtt_adapter_connect_task_destroy(connect_task); } static bool s_aws_mqtt5_listener_publish_received_adapter( @@ -1124,9 +1610,9 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_v .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, - .connect_fn = NULL, + .connect_fn = s_aws_mqtt_client_connection_5_connect, .reconnect_fn = NULL, - .disconnect_fn = NULL, + .disconnect_fn = s_aws_mqtt_client_connection_5_disconnect, .subscribe_multiple_fn = NULL, .subscribe_fn = NULL, .resubscribe_existing_topics_fn = NULL, @@ -1172,7 +1658,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli { .listener_publish_received_handler = s_aws_mqtt5_listener_publish_received_adapter, .listener_publish_received_handler_user_data = adapter, - .lifecycle_event_handler = s_aws_mqtt5_client_connection_event_callback_adapter, + .lifecycle_event_handler = s_aws_mqtt5_client_lifecycle_event_callback_adapter, .lifecycle_event_handler_user_data = adapter, }, .termination_callback = s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached, diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 50aa43aa..6e3f6c8b 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -2155,15 +2155,11 @@ struct aws_mqtt_change_desired_state_task { struct aws_mqtt5_operation_disconnect *disconnect_operation; }; -static void s_change_state_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_change_desired_state_task *change_state_task = arg; - struct aws_mqtt5_client *client = change_state_task->client; - enum aws_mqtt5_client_state desired_state = change_state_task->desired_state; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } +void aws_mqtt5_client_change_desired_state( + struct aws_mqtt5_client *client, + enum aws_mqtt5_client_state desired_state, + struct aws_mqtt5_operation_disconnect *disconnect_op) { + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); if (client->desired_state != desired_state) { AWS_LOGF_INFO( @@ -2175,7 +2171,6 @@ static void s_change_state_task_fn(struct aws_task *task, void *arg, enum aws_ta client->desired_state = desired_state; - struct aws_mqtt5_operation_disconnect *disconnect_op = change_state_task->disconnect_operation; if (desired_state == AWS_MCS_STOPPED && disconnect_op != NULL) { s_aws_mqtt5_client_shutdown_channel_with_disconnect( client, AWS_ERROR_MQTT5_USER_REQUESTED_STOP, disconnect_op); @@ -2183,6 +2178,19 @@ static void s_change_state_task_fn(struct aws_task *task, void *arg, enum aws_ta s_reevaluate_service_task(client); } +} + +static void s_change_state_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_change_desired_state_task *change_state_task = arg; + struct aws_mqtt5_client *client = change_state_task->client; + enum aws_mqtt5_client_state desired_state = change_state_task->desired_state; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + aws_mqtt5_client_change_desired_state(client, desired_state, change_state_task->disconnect_operation); done: @@ -3355,6 +3363,8 @@ void aws_mqtt5_client_get_stats(struct aws_mqtt5_client *client, struct aws_mqtt bool aws_mqtt5_client_reset_connection(struct aws_mqtt5_client *client) { AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); + client->current_reconnect_delay_ms = client->config->min_reconnect_delay_ms; + switch (client->current_state) { case AWS_MCS_MQTT_CONNECT: case AWS_MCS_CONNECTED: diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 206377b0..95af47bb 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -3388,6 +3388,26 @@ struct aws_mqtt5_operation_pingreq *aws_mqtt5_operation_pingreq_new(struct aws_a return pingreq_op; } +bool aws_mqtt5_client_keep_alive_options_are_valid(uint16_t keep_alive_interval_seconds, uint32_t ping_timeout_ms) { + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (keep_alive_interval_seconds > 0) { + uint64_t keep_alive_ms = + aws_timestamp_convert(keep_alive_interval_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); + uint64_t one_second_ms = aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); + + if (ping_timeout_ms == 0) { + ping_timeout_ms = AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS; + } + + if ((uint64_t)ping_timeout_ms + one_second_ms > keep_alive_ms) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); + return false; + } + } + + return true; +} + /********************************************************************************************************************* * Client storage options ********************************************************************************************************************/ @@ -3441,24 +3461,15 @@ int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *opt if (aws_mqtt5_packet_connect_view_validate(options->connect_options)) { AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid CONNECT options in mqtt5 client configuration"); + /* connect validation failure will have already rasied the appropriate error */ return AWS_OP_ERR; } /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ - if (options->connect_options->keep_alive_interval_seconds > 0) { - uint64_t keep_alive_ms = aws_timestamp_convert( - options->connect_options->keep_alive_interval_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); - uint64_t one_second_ms = aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL); - - uint64_t ping_timeout_ms = options->ping_timeout_ms; - if (ping_timeout_ms == 0) { - ping_timeout_ms = AWS_MQTT5_CLIENT_DEFAULT_PING_TIMEOUT_MS; - } - - if (ping_timeout_ms + one_second_ms > keep_alive_ms) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); - return AWS_OP_ERR; - } + if (!aws_mqtt5_client_keep_alive_options_are_valid( + options->connect_options->keep_alive_interval_seconds, options->ping_timeout_ms)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); } if (options->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2a0eab99..91d3e0be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -361,6 +361,19 @@ add_test_case(mqtt3to5_adapter_create_destroy_delayed) add_test_case(mqtt3to5_adapter_set_will) add_test_case(mqtt3to5_adapter_set_login) add_test_case(mqtt3to5_adapter_set_reconnect_timeout) +add_test_case(mqtt3to5_adapter_connect_success) +add_test_case(mqtt3to5_adapter_connect_success_disconnect_success) +add_test_case(mqtt3to5_adapter_connect_success_disconnect_success_thrice) +add_test_case(mqtt3to5_adapter_connect_success_connect_failure) +add_test_case(mqtt3to5_adapter_connect_success_sloppy_shutdown) +add_test_case(mqtt3to5_adapter_connect_bad_connectivity) +add_test_case(mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart) +add_test_case(mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5) +add_test_case(mqtt3to5_adapter_connect_failure_bad_config_success_good_config) +add_test_case(mqtt3to5_adapter_connect_success_disconnect_connect) +add_test_case(mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success) +add_test_case(mqtt3to5_adapter_disconnect_success) +add_test_case(mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success) generate_test_driver(${PROJECT_NAME}-tests) diff --git a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c new file mode 100644 index 00000000..abb2d76b --- /dev/null +++ b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c @@ -0,0 +1,1476 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt5_testing_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +enum aws_mqtt3_lifecycle_event_type { + AWS_MQTT3_LET_CONNECTION_COMPLETE, + AWS_MQTT3_LET_INTERRUPTED, + AWS_MQTT3_LET_RESUMED, + AWS_MQTT3_LET_CLOSED, + AWS_MQTT3_LET_DISCONNECTION_COMPLETE, +}; + +struct aws_mqtt3_lifecycle_event { + enum aws_mqtt3_lifecycle_event_type type; + + uint64_t timestamp; + int error_code; + enum aws_mqtt_connect_return_code return_code; + bool session_present; + + bool skip_error_code_equality; +}; + +struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config { + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; + aws_mqtt_client_on_connection_resumed_fn *on_resumed; + aws_mqtt_client_on_connection_closed_fn *on_closed; + + void *callback_user_data; +}; + +struct aws_mqtt3_to_mqtt5_adapter_test_fixture { + struct aws_mqtt5_client_mock_test_fixture mqtt5_fixture; + + struct aws_mqtt_client_connection *connection; + + struct aws_array_list lifecycle_events; + + struct aws_mutex lock; + struct aws_condition_variable signal; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config config; +}; + +static void s_init_adapter_connection_options_from_fixture( + struct aws_mqtt_connection_options *connection_options, + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { + AWS_ZERO_STRUCT(*connection_options); + + connection_options->host_name = aws_byte_cursor_from_c_str(fixture->mqtt5_fixture.endpoint.address); + connection_options->port = fixture->mqtt5_fixture.endpoint.port; + connection_options->socket_options = &fixture->mqtt5_fixture.socket_options; + connection_options->keep_alive_time_secs = 30; + connection_options->ping_timeout_ms = 10000; + connection_options->clean_session = true; +} + +struct n_lifeycle_event_wait_context { + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture; + enum aws_mqtt3_lifecycle_event_type type; + size_t count; +}; + +static bool s_wait_for_n_adapter_lifecycle_events_predicate(void *context) { + struct n_lifeycle_event_wait_context *wait_context = context; + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = wait_context->fixture; + + size_t actual_count = 0; + size_t event_count = aws_array_list_length(&fixture->lifecycle_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + if (actual_event->type == wait_context->type) { + ++actual_count; + } + } + + return actual_count >= wait_context->count; +} + +static void s_wait_for_n_adapter_lifecycle_events( + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, + enum aws_mqtt3_lifecycle_event_type type, + size_t count) { + struct n_lifeycle_event_wait_context wait_context = { + .fixture = fixture, + .type = type, + .count = count, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred( + &fixture->signal, &fixture->lock, s_wait_for_n_adapter_lifecycle_events_predicate, &wait_context); + aws_mutex_unlock(&fixture->lock); +} + +static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event( + struct aws_mqtt3_lifecycle_event *expected_event, + struct aws_mqtt3_lifecycle_event *actual_event) { + ASSERT_INT_EQUALS(actual_event->type, expected_event->type); + if (expected_event->skip_error_code_equality) { + /* some error scenarios lead to different values cross-platform, so just verify yes/no in that case */ + ASSERT_TRUE((actual_event->error_code != 0) == (expected_event->error_code != 0)); + } else { + ASSERT_INT_EQUALS(actual_event->error_code, expected_event->error_code); + } + + ASSERT_INT_EQUALS(actual_event->return_code, expected_event->return_code); + ASSERT_TRUE(actual_event->session_present == expected_event->session_present); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events, + size_t maximum_event_count) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + ASSERT_TRUE(actual_event_count <= maximum_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_ends_with( + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + + size_t actual_index = i + (actual_event_count - expected_event_count); + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), actual_index); + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_closed_handler( + struct aws_mqtt_client_connection *connection, + struct on_connection_closed_data *data, + void *userdata) { + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CLOSED; + aws_high_res_clock_get_ticks(&event.timestamp); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); + + /* invoke user callback if registered */ + if (fixture->config.on_closed) { + (*fixture->config.on_closed)(connection, data, fixture->config.callback_user_data); + } +} + +static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_interrupted_handler( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_INTERRUPTED; + aws_high_res_clock_get_ticks(&event.timestamp); + event.error_code = error_code; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); + + /* invoke user callback if registered */ + if (fixture->config.on_interrupted) { + (*fixture->config.on_interrupted)(connection, error_code, fixture->config.callback_user_data); + } +} + +static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_resumed_handler( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_RESUMED; + aws_high_res_clock_get_ticks(&event.timestamp); + event.return_code = return_code; + event.session_present = session_present; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); + + /* invoke user callback if registered */ + if (fixture->config.on_resumed) { + (*fixture->config.on_resumed)(connection, return_code, session_present, fixture->config.callback_user_data); + } +} + +static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete( + struct aws_mqtt_client_connection *connection, + int error_code, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *user_data) { + (void)connection; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = user_data; + + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CONNECTION_COMPLETE; + aws_high_res_clock_get_ticks(&event.timestamp); + event.error_code = error_code; + event.return_code = return_code; + event.session_present = session_present; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete( + struct aws_mqtt_client_connection *connection, + void *user_data) { + (void)connection; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = user_data; + + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE; + aws_high_res_clock_get_ticks(&event.timestamp); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +int aws_mqtt3_to_mqtt5_adapter_test_fixture_init( + struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *mqtt5_fixture_config, + struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config *config) { + AWS_ZERO_STRUCT(*fixture); + + if (aws_mqtt5_client_mock_test_fixture_init(&fixture->mqtt5_fixture, allocator, mqtt5_fixture_config)) { + return AWS_OP_ERR; + } + + fixture->connection = aws_mqtt_client_connection_new_from_mqtt5_client(fixture->mqtt5_fixture.client); + if (fixture->connection == NULL) { + return AWS_OP_ERR; + } + + aws_array_list_init_dynamic(&fixture->lifecycle_events, allocator, 10, sizeof(struct aws_mqtt3_lifecycle_event)); + + aws_mutex_init(&fixture->lock); + aws_condition_variable_init(&fixture->signal); + + if (config) { + fixture->config = *config; + } + + aws_mqtt_client_connection_set_connection_closed_handler( + fixture->connection, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_closed_handler, fixture); + aws_mqtt_client_connection_set_connection_interruption_handlers( + fixture->connection, + s_aws_mqtt3_to_mqtt5_adapter_test_fixture_interrupted_handler, + fixture, + s_aws_mqtt3_to_mqtt5_adapter_test_fixture_resumed_handler, + fixture); + + return AWS_OP_SUCCESS; +} + +void aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { + aws_mqtt_client_connection_release(fixture->connection); + + aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->mqtt5_fixture); + + aws_array_list_clean_up(&fixture->lifecycle_events); + + aws_mutex_clean_up(&fixture->lock); + aws_condition_variable_clean_up(&fixture->signal); +} + +void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_packet_connect_view local_connect_options = { + .keep_alive_interval_seconds = 30, + .clean_start = true, + }; + + struct aws_mqtt5_client_options client_options = { + .connect_options = &local_connect_options, + .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .publish_received_handler = s_mqtt3to5_publish_received_callback, + .publish_received_handler_user_data = NULL, + .ping_timeout_ms = 10000, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { + .client_options = &client_options, + }; + + struct aws_mqtt5_client_mock_test_fixture test_fixture; + AWS_ZERO_STRUCT(test_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); + + struct aws_mqtt_client_connection *connection = + aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener attachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt_client_connection_release(connection); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener detachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) + +static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( + allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) + +typedef int (*mqtt3to5_adapter_config_test_setup_fn)( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect); + +static int s_do_mqtt3to5_adapter_config_test( + struct aws_allocator *allocator, + mqtt3to5_adapter_config_test_setup_fn setup_fn) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture test_fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&test_fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt5_client *client = test_fixture.mqtt5_fixture.client; + + struct aws_mqtt_client_connection *adapter = test_fixture.connection; + + struct aws_mqtt5_packet_connect_storage expected_connect_storage; + ASSERT_SUCCESS((*setup_fn)(allocator, adapter, &expected_connect_storage)); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&test_fixture.mqtt5_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_fixture.mqtt5_fixture); + + struct aws_mqtt5_mock_server_packet_record expected_packets[] = { + { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = &expected_connect_storage, + }, + }; + ASSERT_SUCCESS(aws_verify_received_packet_sequence( + &test_fixture.mqtt5_fixture, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_simple_topic, "Hello/World"); +AWS_STATIC_STRING_FROM_LITERAL(s_simple_payload, "A Payload"); + +static int s_mqtt3to5_adapter_set_will_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor topic_cursor = aws_byte_cursor_from_string(s_simple_topic); + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_string(s_simple_payload); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_set_will(adapter, &topic_cursor, AWS_MQTT_QOS_AT_LEAST_ONCE, true, &payload_cursor)); + + struct aws_mqtt5_packet_publish_view expected_will = { + .payload = payload_cursor, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = topic_cursor, + }; + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(g_default_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .will = &expected_will, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_set_will_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_will_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_will, s_mqtt3to5_adapter_set_will_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_username, "MyUsername"); +AWS_STATIC_STRING_FROM_LITERAL(s_password, "TopTopSecret"); + +static int s_mqtt3to5_adapter_set_login_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor username_cursor = aws_byte_cursor_from_string(s_username); + struct aws_byte_cursor password_cursor = aws_byte_cursor_from_string(s_password); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(adapter, &username_cursor, &password_cursor)); + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(g_default_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .username = &username_cursor, + .password = &password_cursor, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt3to5_adapter_set_login_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_login_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_login, s_mqtt3to5_adapter_set_login_fn) + +static int s_mqtt3to5_adapter_set_reconnect_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* + * This is a variant of the mqtt5_client_reconnect_failure_backoff test. + * + * The primary change is that we configure the mqtt5 client with "wrong" (fast) reconnect delays and then use + * the adapter API to configure with the "right" ones that will let the test pass. + */ + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = 10; + test_options.client_options.max_reconnect_delay_ms = 50; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_always_fail; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); + + aws_mqtt_client_connection_set_reconnect_timeout(adapter, RECONNECT_TEST_MIN_BACKOFF, RECONNECT_TEST_MAX_BACKOFF); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 6); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_verify_reconnection_exponential_backoff_timestamps(&test_context)); + + /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt_client_connection_release(adapter); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_set_reconnect_timeout, s_mqtt3to5_adapter_set_reconnect_timeout_fn) + +/* + * Basic successful connection test + */ +static int s_mqtt3to5_adapter_connect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_connect_success, s_mqtt3to5_adapter_connect_success_fn) + +static int s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle( + struct aws_allocator *allocator, + size_t iterations) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + for (size_t i = 0; i < iterations; ++i) { + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = + s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, i + 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, i + 1); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, i + 1); + + struct aws_mqtt3_lifecycle_event expected_event_sequence[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CLOSED, + }, + }; + + size_t expected_event_count = (i + 1) * 3; + struct aws_mqtt3_lifecycle_event *expected_events = + aws_mem_calloc(allocator, expected_event_count, sizeof(struct aws_mqtt3_lifecycle_event)); + for (size_t j = 0; j < i + 1; ++j) { + for (size_t k = 0; k < 3; ++k) { + *(expected_events + j * 3 + k) = expected_event_sequence[k]; + } + } + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, expected_event_count, expected_events, expected_event_count)); + + aws_mem_release(allocator, expected_events); + } + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * A couple of simple connect-disconnect cycle tests. The first does a single cycle while the second does several. + * Verifies proper lifecycle event sequencing. + */ +static int s_mqtt3to5_adapter_connect_success_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle(allocator, 1)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_success_disconnect_success, + s_mqtt3to5_adapter_connect_success_disconnect_success_fn) + +static int s_mqtt3to5_adapter_connect_success_disconnect_success_thrice_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle(allocator, 3)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_success_disconnect_success_thrice, + s_mqtt3to5_adapter_connect_success_disconnect_success_thrice_fn) + +/* + * Verifies that calling connect() while connected yields a connection completion callback with the + * appropriate already-connected error code. Note that in the mqtt311 impl, this error is synchronous. + */ +static int s_mqtt3to5_adapter_connect_success_connect_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT_ALREADY_CONNECTED, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_connect_success_connect_failure, s_mqtt3to5_adapter_connect_success_connect_failure_fn) + +/* + * A non-deterministic test that starts the connect process and immediately drops the last external adapter + * reference. Intended to stochastically shake out shutdown race conditions. + */ +static int s_mqtt3to5_adapter_connect_success_sloppy_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_connect_success_sloppy_shutdown, s_mqtt3to5_adapter_connect_success_sloppy_shutdown_fn) + +static int s_aws_mqtt5_server_disconnect_after_connect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + + struct aws_mqtt5_packet_disconnect_view disconnect = { + .reason_code = AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN, + }; + + int result = aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_DISCONNECT, &disconnect); + + return result; +} + +static int s_verify_bad_connectivity_callbacks(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + { + .type = AWS_MQTT3_LET_RESUMED, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + { + .type = AWS_MQTT3_LET_RESUMED, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, + }, + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CLOSED, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + return AWS_OP_SUCCESS; +} + +static int s_do_bad_connectivity_basic_test(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { + struct aws_mqtt_client_connection *adapter = fixture->connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_INTERRUPTED, 3); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, fixture); + + s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_CLOSED, 1); + + ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(fixture)); + + return AWS_OP_SUCCESS; +} + +/* + * A test where each successful connection is immediately dropped after the connack is sent. Allows us to verify + * proper interrupt/resume sequencing. + */ +static int s_mqtt3to5_adapter_connect_bad_connectivity_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ + test_options.client_options.min_reconnect_delay_ms = 500; + test_options.client_options.max_reconnect_delay_ms = 1000; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_server_disconnect_after_connect; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_connect_bad_connectivity, s_mqtt3to5_adapter_connect_bad_connectivity_fn) + +/* + * A variant of the bad connectivity test where we restart the mqtt5 client after the main test is over and verify + * we don't get any interrupt/resume callbacks. + */ +static int s_mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ + test_options.client_options.min_reconnect_delay_ms = 500; + test_options.client_options.max_reconnect_delay_ms = 1000; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_server_disconnect_after_connect; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); + + /* + * Now restart the 5 client, wait for a few more connection success/disconnect cycles, and then verify that no + * further adapter callbacks were invoked because of this. + */ + aws_mqtt5_client_start(fixture.mqtt5_fixture.client); + + aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 6); + + aws_thread_current_sleep(aws_timestamp_convert(2, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(&fixture)); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart, + s_mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart_fn) + +int aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct aws_mqtt5_mock_server_reconnect_state *context = user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + if (context->connection_attempts >= context->required_connection_failure_count) { + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + aws_high_res_clock_get_ticks(&context->connect_timestamp); + } else { + connack_view.reason_code = AWS_MQTT5_CRC_NOT_AUTHORIZED; + } + + ++context->connection_attempts; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* + * Test where the initial connect is rejected, which should put the adapter to sleep. Meanwhile followup attempts + * are successful and the mqtt5 client itself becomes connected. + */ +static int s_mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { + .required_connection_failure_count = 1, + }; + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &mock_server_state, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + // wait for and verify a connection failure + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // wait for the mqtt5 client to successfully connect on the second try + aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 1); + + // verify we didn't get any callbacks on the adapter + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // "connect" on the adapter, wait for and verify success + aws_mqtt_client_connection_connect(adapter, &connection_options); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, + AWS_ARRAY_SIZE(expected_reconnect_events), + expected_reconnect_events, + AWS_ARRAY_SIZE(expected_reconnect_events))); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5, + s_mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_bad_host_name, "derpity_derp"); + +/* + * Fails to connect with a bad config. Follow up with a good config. Verifies that config is re-evaluated with + * each connect() invocation. + */ +static int s_mqtt3to5_adapter_connect_failure_bad_config_success_good_config_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + struct aws_byte_cursor good_host_name = connection_options.host_name; + connection_options.host_name = aws_byte_cursor_from_string(s_bad_host_name); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + // wait for and verify a connection failure + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // reconnect with a good host the adapter, wait for and verify success + connection_options.host_name = good_host_name; + aws_mqtt_client_connection_connect(adapter, &connection_options); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, + AWS_ARRAY_SIZE(expected_reconnect_events), + expected_reconnect_events, + AWS_ARRAY_SIZE(expected_reconnect_events))); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_failure_bad_config_success_good_config, + s_mqtt3to5_adapter_connect_failure_bad_config_success_good_config_fn) + +/* + * Connect successfully then disconnect followed by a connect with no intervening wait. Verifies simple reliable + * action and event sequencing. + */ +static int s_mqtt3to5_adapter_connect_success_disconnect_connect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + /* + * depending on timing there may or may not be a closed event in between, so just check beginning and end for + * expected events + */ + + struct aws_mqtt3_lifecycle_event expected_sequence_beginning[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + &fixture, AWS_ARRAY_SIZE(expected_sequence_beginning), expected_sequence_beginning)); + + struct aws_mqtt3_lifecycle_event expected_sequence_ending[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_ends_with( + &fixture, AWS_ARRAY_SIZE(expected_sequence_ending), expected_sequence_ending)); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_success_disconnect_connect, + s_mqtt3to5_adapter_connect_success_disconnect_connect_fn) + +/* + * Calls disconnect() on an adapter that successfully connected but then had the mqtt5 client stopped behind the + * adapter's back. Verifies that we still get a completion callback. + */ +static int s_mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); + + aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success, + s_mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success_fn) + +/* + * Call disconnect on a newly-created adapter. Verifies that we get a completion callback. + */ +static int s_mqtt3to5_adapter_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt3to5_adapter_disconnect_success, s_mqtt3to5_adapter_disconnect_success_fn) + +/* + * Use the adapter to successfully connect then call disconnect multiple times. Verify that all disconnect + * invocations generate expected lifecycle events. Verifies that disconnects after a disconnect are properly handled. + */ +static int s_mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); + + aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 3); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, 1); + + aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success, + s_mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success_fn) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 8075f7b3..a5bb2a03 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -28,7 +28,7 @@ static bool s_is_within_percentage_of(uint64_t expected_time, uint64_t actual_ti return fabs(actual_percent) <= percentage; } -static int s_aws_mqtt5_mock_server_send_packet( +int aws_mqtt5_mock_server_send_packet( struct aws_mqtt5_server_mock_connection_context *connection, enum aws_mqtt5_packet_type packet_type, void *packet) { @@ -52,7 +52,7 @@ static int s_aws_mqtt5_mock_server_send_packet( return AWS_OP_SUCCESS; } -static int s_aws_mqtt5_mock_server_handle_connect_always_succeed( +int aws_mqtt5_mock_server_handle_connect_always_succeed( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -64,7 +64,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_always_succeed( connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } static int s_aws_mqtt5_mock_server_handle_pingreq_always_respond( @@ -74,7 +74,7 @@ static int s_aws_mqtt5_mock_server_handle_pingreq_always_respond( (void)packet; (void)user_data; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PINGRESP, NULL); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PINGRESP, NULL); } static int s_aws_mqtt5_mock_server_handle_disconnect( @@ -97,16 +97,9 @@ void s_publish_received_callback(const struct aws_mqtt5_packet_publish_view *pub (void)user_data; } -AWS_STATIC_STRING_FROM_LITERAL(s_client_id, "HelloWorld"); +AWS_STRING_FROM_LITERAL(g_default_client_id, "HelloWorld"); -struct mqtt5_client_test_options { - struct aws_mqtt5_client_topic_alias_options topic_aliasing_options; - struct aws_mqtt5_packet_connect_view connect_options; - struct aws_mqtt5_client_options client_options; - struct aws_mqtt5_mock_server_vtable server_function_table; -}; - -static void s_mqtt5_client_test_init_default_options(struct mqtt5_client_test_options *test_options) { +void aws_mqtt5_client_test_init_default_options(struct mqtt5_client_test_options *test_options) { struct aws_mqtt5_client_topic_alias_options local_topic_aliasing_options = { .outbound_topic_alias_behavior = AWS_MQTT5_COTABT_DISABLED, @@ -116,7 +109,7 @@ static void s_mqtt5_client_test_init_default_options(struct mqtt5_client_test_op struct aws_mqtt5_packet_connect_view local_connect_options = { .keep_alive_interval_seconds = 30, - .client_id = aws_byte_cursor_from_string(s_client_id), + .client_id = aws_byte_cursor_from_string(g_default_client_id), .clean_start = true, }; @@ -141,7 +134,7 @@ static void s_mqtt5_client_test_init_default_options(struct mqtt5_client_test_op struct aws_mqtt5_mock_server_vtable local_server_function_table = { .packet_handlers = { NULL, /* RESERVED = 0 */ - &s_aws_mqtt5_mock_server_handle_connect_always_succeed, /* CONNECT */ + &aws_mqtt5_mock_server_handle_connect_always_succeed, /* CONNECT */ NULL, /* CONNACK */ NULL, /* PUBLISH */ NULL, /* PUBACK */ @@ -167,7 +160,7 @@ static int s_aws_mqtt5_client_test_init_default_connect_storage( struct aws_mqtt5_packet_connect_view connect_view = { .keep_alive_interval_seconds = 30, - .client_id = aws_byte_cursor_from_string(s_client_id), + .client_id = aws_byte_cursor_from_string(g_default_client_id), .clean_start = true, }; @@ -232,7 +225,7 @@ static bool s_last_lifecycle_event_is_connected(void *arg) { return s_last_life_cycle_event_is(test_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS); } -static void s_wait_for_connected_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { +void aws_wait_for_connected_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { aws_mutex_lock(&test_context->lock); aws_condition_variable_wait_pred( &test_context->signal, &test_context->lock, s_last_lifecycle_event_is_connected, test_context); @@ -245,7 +238,7 @@ static bool s_last_lifecycle_event_is_stopped(void *arg) { return s_last_life_cycle_event_is(test_fixture, AWS_MQTT5_CLET_STOPPED); } -static void s_wait_for_stopped_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { +void aws_wait_for_stopped_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context) { aws_mutex_lock(&test_context->lock); aws_condition_variable_wait_pred( &test_context->signal, &test_context->lock, s_last_lifecycle_event_is_stopped, test_context); @@ -317,7 +310,7 @@ static void s_wait_for_disconnect_completion(struct aws_mqtt5_client_mock_test_f aws_mutex_unlock(&test_context->lock); } -static int s_verify_client_state_sequence( +int aws_verify_client_state_sequence( struct aws_mqtt5_client_mock_test_fixture *test_context, enum aws_mqtt5_client_state *expected_states, size_t expected_states_count) { @@ -361,7 +354,7 @@ static int s_verify_simple_lifecycle_event_sequence( return AWS_OP_SUCCESS; } -static int s_verify_received_packet_sequence( +int aws_verify_received_packet_sequence( struct aws_mqtt5_client_mock_test_fixture *test_context, struct aws_mqtt5_mock_server_packet_record *expected_packets, size_t expected_packets_count) { @@ -400,7 +393,7 @@ static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *alloca aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -413,7 +406,7 @@ static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *alloca struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_disconnect_view disconnect_options = { .reason_code = AWS_MQTT5_DRC_DISCONNECT_WITH_WILL_MESSAGE, @@ -426,7 +419,7 @@ static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *alloca ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_options, &completion_options)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); s_wait_for_disconnect_completion(&test_context); s_wait_for_mock_server_to_receive_disconnect_packet(&test_context); @@ -457,7 +450,7 @@ static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *alloca AWS_MCS_STOPPED, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); struct aws_mqtt5_packet_connect_storage expected_connect_storage; ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_default_connect_storage(&expected_connect_storage, allocator)); @@ -477,7 +470,7 @@ static int s_mqtt5_client_direct_connect_success_fn(struct aws_allocator *alloca }, }; ASSERT_SUCCESS( - s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + aws_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); @@ -501,7 +494,7 @@ static int s_mqtt5_client_simple_failure_test_fn( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -528,7 +521,7 @@ static int s_mqtt5_client_simple_failure_test_fn( ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { { @@ -546,7 +539,7 @@ static int s_mqtt5_client_simple_failure_test_fn( AWS_MCS_CONNECTING, AWS_MCS_PENDING_RECONNECT, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -747,7 +740,7 @@ static int s_mqtt5_client_websocket_connect_handshake_failure_fn(struct aws_allo AWS_TEST_CASE(mqtt5_client_websocket_connect_handshake_failure, s_mqtt5_client_websocket_connect_handshake_failure_fn) -static int s_aws_mqtt5_mock_server_handle_connect_always_fail( +int aws_mqtt5_mock_server_handle_connect_always_fail( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -759,7 +752,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_always_fail( connack_view.reason_code = AWS_MQTT5_CRC_BANNED; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } /* Connection failure test where overall connection fails due to a CONNACK error code */ @@ -769,10 +762,10 @@ static int s_mqtt5_client_direct_connect_connack_refusal_fn(struct aws_allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_always_fail; + aws_mqtt5_mock_server_handle_connect_always_fail; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -790,7 +783,7 @@ static int s_mqtt5_client_direct_connect_connack_refusal_fn(struct aws_allocator ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { { @@ -809,7 +802,7 @@ static int s_mqtt5_client_direct_connect_connack_refusal_fn(struct aws_allocator AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -826,7 +819,7 @@ static int s_mqtt5_client_direct_connect_connack_timeout_fn(struct aws_allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* fast CONNACK timeout and don't response to the CONNECT packet */ test_options.client_options.connack_timeout_ms = 2000; @@ -848,7 +841,7 @@ static int s_mqtt5_client_direct_connect_connack_timeout_fn(struct aws_allocator ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { { @@ -867,7 +860,7 @@ static int s_mqtt5_client_direct_connect_connack_timeout_fn(struct aws_allocator AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -898,7 +891,7 @@ static void s_server_disconnect_service_fn( .reason_code = AWS_MQTT5_DRC_PACKET_TOO_LARGE, }; - s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); + aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); } static int s_aws_mqtt5_server_disconnect_on_connect( @@ -910,7 +903,7 @@ static int s_aws_mqtt5_server_disconnect_on_connect( * We intercept the CONNECT in order to correctly set the connack_sent test state. Otherwise we risk sometimes * sending the DISCONNECT before the CONNACK */ - int result = s_aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + int result = aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); struct aws_mqtt5_server_disconnect_test_context *test_context = user_data; test_context->connack_sent = true; @@ -925,7 +918,7 @@ static int s_mqtt5_client_direct_connect_from_server_disconnect_fn(struct aws_al aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* mock server sends a DISCONNECT packet back to the client after a successful CONNECTION establishment */ test_options.server_function_table.service_task_fn = s_server_disconnect_service_fn; @@ -953,7 +946,7 @@ static int s_mqtt5_client_direct_connect_from_server_disconnect_fn(struct aws_al ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { { @@ -976,7 +969,7 @@ static int s_mqtt5_client_direct_connect_from_server_disconnect_fn(struct aws_al AWS_MCS_CONNECTED, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -1059,7 +1052,7 @@ static int s_aws_mqtt5_client_test_init_ping_test_connect_storage( struct aws_mqtt5_packet_connect_view connect_view = { .keep_alive_interval_seconds = (uint16_t)aws_timestamp_convert(TEST_PING_INTERVAL_MS, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL), - .client_id = aws_byte_cursor_from_string(s_client_id), + .client_id = aws_byte_cursor_from_string(g_default_client_id), .clean_start = true, }; @@ -1078,7 +1071,7 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* fast keep alive in order keep tests reasonably short */ uint16_t keep_alive_seconds = @@ -1105,7 +1098,7 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); s_wait_for_n_pingreqs(&ping_context); /* @@ -1127,7 +1120,7 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_view, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); s_wait_for_mock_server_to_receive_disconnect_packet(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { @@ -1156,7 +1149,7 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_STOPPED, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); struct aws_mqtt5_packet_connect_storage expected_connect_storage; ASSERT_SUCCESS(s_aws_mqtt5_client_test_init_ping_test_connect_storage(&expected_connect_storage, allocator)); @@ -1190,7 +1183,7 @@ static int s_mqtt5_client_ping_sequence_fn(struct aws_allocator *allocator, void }, }; ASSERT_SUCCESS( - s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + aws_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); ASSERT_SUCCESS(s_verify_ping_sequence_timing(&test_context)); @@ -1268,7 +1261,7 @@ static int s_mqtt5_client_ping_timeout_fn(struct aws_allocator *allocator, void aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* fast keep alive in order keep tests reasonably short */ uint16_t keep_alive_seconds = @@ -1292,12 +1285,12 @@ static int s_mqtt5_client_ping_timeout_fn(struct aws_allocator *allocator, void struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); s_wait_for_disconnection_lifecycle_event(&test_context); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); struct aws_mqtt5_client_lifecycle_event expected_events[] = { { @@ -1326,7 +1319,7 @@ static int s_mqtt5_client_ping_timeout_fn(struct aws_allocator *allocator, void AWS_MCS_CLEAN_DISCONNECT, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -1336,49 +1329,47 @@ static int s_mqtt5_client_ping_timeout_fn(struct aws_allocator *allocator, void AWS_TEST_CASE(mqtt5_client_ping_timeout, s_mqtt5_client_ping_timeout_fn) -struct aws_connection_failure_wait_context { - size_t number_of_failures; +struct aws_lifecycle_event_wait_context { + enum aws_mqtt5_client_lifecycle_event_type type; + size_t count; struct aws_mqtt5_client_mock_test_fixture *test_fixture; }; -static bool s_received_at_least_n_connection_failures(void *arg) { - struct aws_connection_failure_wait_context *context = arg; +static bool s_received_at_least_n_events(void *arg) { + struct aws_lifecycle_event_wait_context *context = arg; struct aws_mqtt5_client_mock_test_fixture *test_fixture = context->test_fixture; - size_t failure_count = 0; + size_t actual_count = 0; size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); for (size_t i = 0; i < event_count; ++i) { struct aws_mqtt5_lifecycle_event_record *record = NULL; aws_array_list_get_at(&test_fixture->lifecycle_events, &record, i); - if (record->event.event_type == AWS_MQTT5_CLET_CONNECTION_FAILURE) { - failure_count++; + if (record->event.event_type == context->type) { + actual_count++; } } - return failure_count >= context->number_of_failures; + return actual_count >= context->count; } -static void s_wait_for_n_connection_failure_lifecycle_events( +void aws_mqtt5_wait_for_n_lifecycle_events( struct aws_mqtt5_client_mock_test_fixture *test_context, - size_t failure_count) { - struct aws_connection_failure_wait_context context = { - .number_of_failures = failure_count, + enum aws_mqtt5_client_lifecycle_event_type type, + size_t count) { + struct aws_lifecycle_event_wait_context context = { + .type = type, + .count = count, .test_fixture = test_context, }; aws_mutex_lock(&test_context->lock); aws_condition_variable_wait_pred( - &test_context->signal, &test_context->lock, s_received_at_least_n_connection_failures, &context); + &test_context->signal, &test_context->lock, s_received_at_least_n_events, &context); aws_mutex_unlock(&test_context->lock); } -#define RECONNECT_TEST_MIN_BACKOFF 500 -#define RECONNECT_TEST_MAX_BACKOFF 5000 -#define RECONNECT_TEST_BACKOFF_RESET_DELAY 5000 - -static int s_verify_reconnection_exponential_backoff_timestamps( - struct aws_mqtt5_client_mock_test_fixture *test_fixture) { +int aws_verify_reconnection_exponential_backoff_timestamps(struct aws_mqtt5_client_mock_test_fixture *test_fixture) { aws_mutex_lock(&test_fixture->lock); size_t event_count = aws_array_list_length(&test_fixture->lifecycle_events); @@ -1421,7 +1412,7 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; @@ -1430,7 +1421,7 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_always_fail; + aws_mqtt5_mock_server_handle_connect_always_fail; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -1444,11 +1435,11 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_n_connection_failure_lifecycle_events(&test_context, 6); + aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 6); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* 6 (connecting, connection failure) pairs */ struct aws_mqtt5_client_lifecycle_event expected_events[] = { @@ -1498,7 +1489,7 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all ASSERT_SUCCESS( s_verify_simple_lifecycle_event_sequence(&test_context, expected_events, AWS_ARRAY_SIZE(expected_events))); - ASSERT_SUCCESS(s_verify_reconnection_exponential_backoff_timestamps(&test_context)); + ASSERT_SUCCESS(aws_verify_reconnection_exponential_backoff_timestamps(&test_context)); /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ enum aws_mqtt5_client_state expected_states[] = { @@ -1509,7 +1500,7 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -1518,16 +1509,7 @@ static int s_mqtt5_client_reconnect_failure_backoff_fn(struct aws_allocator *all AWS_TEST_CASE(mqtt5_client_reconnect_failure_backoff, s_mqtt5_client_reconnect_failure_backoff_fn) -struct aws_mqtt5_mock_server_reconnect_state { - size_t required_connection_failure_count; - - size_t connection_attempts; - uint64_t connect_timestamp; - - uint64_t successful_connection_disconnect_delay_ms; -}; - -static int s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth( +int aws_mqtt5_mock_server_handle_connect_succeed_on_nth( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -1547,7 +1529,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth( ++context->connection_attempts; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } static void s_aws_mqtt5_mock_server_disconnect_after_n_ms( @@ -1574,7 +1556,7 @@ static void s_aws_mqtt5_mock_server_disconnect_after_n_ms( .reason_code = AWS_MQTT5_DRC_PACKET_TOO_LARGE, }; - s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); + aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_DISCONNECT, &disconnect); context->connect_timestamp = 0; } } @@ -1631,7 +1613,7 @@ static int s_mqtt5_client_reconnect_backoff_insufficient_reset_fn(struct aws_all aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { .required_connection_failure_count = 6, @@ -1646,7 +1628,7 @@ static int s_mqtt5_client_reconnect_backoff_insufficient_reset_fn(struct aws_all test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth; + aws_mqtt5_mock_server_handle_connect_succeed_on_nth; test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_disconnect_after_n_ms; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { @@ -1662,11 +1644,11 @@ static int s_mqtt5_client_reconnect_backoff_insufficient_reset_fn(struct aws_all ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_n_connection_failure_lifecycle_events(&test_context, 7); + aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 7); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* 6 (connecting, connection failure) pairs, followed by a successful connection, then a disconnect and reconnect */ struct aws_mqtt5_client_lifecycle_event expected_events[] = { @@ -1751,7 +1733,7 @@ static int s_mqtt5_client_reconnect_backoff_sufficient_reset_fn(struct aws_alloc aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { .required_connection_failure_count = 6, @@ -1766,7 +1748,7 @@ static int s_mqtt5_client_reconnect_backoff_sufficient_reset_fn(struct aws_alloc test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_succeed_on_nth; + aws_mqtt5_mock_server_handle_connect_succeed_on_nth; test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_disconnect_after_n_ms; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { @@ -1782,11 +1764,11 @@ static int s_mqtt5_client_reconnect_backoff_sufficient_reset_fn(struct aws_alloc ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_n_connection_failure_lifecycle_events(&test_context, 7); + aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 7); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* 6 (connecting, connection failure) pairs, followed by a successful connection, then a disconnect and reconnect */ struct aws_mqtt5_client_lifecycle_event expected_events[] = { @@ -1925,7 +1907,7 @@ static int s_aws_mqtt5_server_send_suback_on_subscribe( .reason_codes = s_suback_reason_codes, }; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); } /* Connection test where we succeed, send a SUBSCRIBE, and wait for a SUBACK */ @@ -1935,7 +1917,7 @@ static int s_mqtt5_client_subscribe_success_fn(struct aws_allocator *allocator, aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = s_aws_mqtt5_server_send_suback_on_subscribe; @@ -1958,7 +1940,7 @@ static int s_mqtt5_client_subscribe_success_fn(struct aws_allocator *allocator, struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_view subscribe_view = { .subscriptions = s_subscriptions, @@ -1975,7 +1957,7 @@ static int s_mqtt5_client_subscribe_success_fn(struct aws_allocator *allocator, ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -2000,7 +1982,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size( connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; connack_view.maximum_packet_size = &maximum_packet_size; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } void s_aws_mqtt5_subscribe_complete_packet_size_too_small_fn( @@ -2025,7 +2007,7 @@ static int s_mqtt5_client_subscribe_fail_packet_too_big_fn(struct aws_allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size; @@ -2048,7 +2030,7 @@ static int s_mqtt5_client_subscribe_fail_packet_too_big_fn(struct aws_allocator struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_view subscribe_view = { .subscriptions = s_subscriptions, @@ -2065,7 +2047,7 @@ static int s_mqtt5_client_subscribe_fail_packet_too_big_fn(struct aws_allocator ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -2087,7 +2069,7 @@ static int s_mqtt5_client_disconnect_fail_packet_too_big_fn(struct aws_allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_succeed_maximum_packet_size; @@ -2110,7 +2092,7 @@ static int s_mqtt5_client_disconnect_fail_packet_too_big_fn(struct aws_allocator struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_byte_cursor long_reason_string_cursor = aws_byte_cursor_from_c_str( "Not valid because it includes the 0-terminator but we don't check utf-8 so who cares"); @@ -2127,7 +2109,7 @@ static int s_mqtt5_client_disconnect_fail_packet_too_big_fn(struct aws_allocator ASSERT_SUCCESS(aws_mqtt5_client_stop(client, &disconnect_view, &completion_options)); s_wait_for_disconnect_completion(&test_context); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -2157,7 +2139,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_succeed_receive_maximum( connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; connack_view.receive_maximum = &receive_maximum; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } struct send_puback_task { @@ -2185,7 +2167,7 @@ void send_puback_fn(struct aws_task *task, void *arg, enum aws_task_status statu .packet_id = puback_response_task->packet_id, }; - s_aws_mqtt5_mock_server_send_packet(puback_response_task->connection, AWS_MQTT5_PT_PUBACK, &puback_view); + aws_mqtt5_mock_server_send_packet(puback_response_task->connection, AWS_MQTT5_PT_PUBACK, &puback_view); done: @@ -2281,7 +2263,7 @@ static int s_mqtt5_client_flow_control_receive_maximum_fn(struct aws_allocator * aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* send delayed pubacks */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = @@ -2309,7 +2291,7 @@ static int s_mqtt5_client_flow_control_receive_maximum_fn(struct aws_allocator * struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); /* send a bunch of publishes */ for (size_t i = 0; i < RECEIVE_MAXIMUM_PUBLISH_COUNT; ++i) { @@ -2339,7 +2321,7 @@ static int s_mqtt5_client_flow_control_receive_maximum_fn(struct aws_allocator * ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* * verify that the maximum number of in-progress qos1 publishes on the server was never more than what the @@ -2434,7 +2416,7 @@ static int s_mqtt5_client_publish_timeout_fn(struct aws_allocator *allocator, vo aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_mock_server_handle_timeout_publish; @@ -2454,7 +2436,7 @@ static int s_mqtt5_client_publish_timeout_fn(struct aws_allocator *allocator, vo struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_publish_completion_options completion_options = { .completion_callback = &s_publish_timeout_publish_completion_fn, @@ -2488,7 +2470,7 @@ static int s_mqtt5_client_publish_timeout_fn(struct aws_allocator *allocator, vo ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -2514,7 +2496,7 @@ static int s_aws_mqtt5_mock_server_handle_publish_puback( .packet_id = publish_view->packet_id, }; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); } #define IOT_CORE_THROUGHPUT_PACKETS 21 @@ -2524,7 +2506,7 @@ static uint8_t s_large_packet_payload[127 * 1024]; static int s_do_iot_core_throughput_test(struct aws_allocator *allocator, bool use_iot_core_limits) { struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* send pubacks */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = @@ -2553,7 +2535,7 @@ static int s_do_iot_core_throughput_test(struct aws_allocator *allocator, bool u struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); /* send a bunch of large publishes */ aws_secure_zero(s_large_packet_payload, AWS_ARRAY_SIZE(s_large_packet_payload)); @@ -2586,7 +2568,7 @@ static int s_do_iot_core_throughput_test(struct aws_allocator *allocator, bool u ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); @@ -2627,7 +2609,7 @@ AWS_TEST_CASE(mqtt5_client_flow_control_iot_core_throughput, s_mqtt5_client_flow static int s_do_iot_core_publish_tps_test(struct aws_allocator *allocator, bool use_iot_core_limits) { struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* send pubacks */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = @@ -2656,7 +2638,7 @@ static int s_do_iot_core_publish_tps_test(struct aws_allocator *allocator, bool struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); /* send a bunch of tiny publishes */ for (size_t i = 0; i < IOT_CORE_PUBLISH_TPS_PACKETS; ++i) { @@ -2686,7 +2668,7 @@ static int s_do_iot_core_publish_tps_test(struct aws_allocator *allocator, bool ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); @@ -2753,7 +2735,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success( connack_view.session_present = !connect_packet->clean_start; } - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } static int s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( @@ -2771,7 +2753,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; connack_view.session_present = !connect_packet->clean_start; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } struct aws_mqtt5_wait_for_n_lifecycle_events_context { @@ -2836,7 +2818,7 @@ static int s_aws_mqtt5_client_test_init_resume_session_connect_storage( struct aws_mqtt5_packet_connect_view connect_view = { .keep_alive_interval_seconds = 30, - .client_id = aws_byte_cursor_from_string(s_client_id), + .client_id = aws_byte_cursor_from_string(g_default_client_id), .clean_start = false, }; @@ -2851,7 +2833,7 @@ static int s_do_mqtt5_client_session_resumption_test( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.session_behavior = session_behavior; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = @@ -2899,7 +2881,7 @@ static int s_do_mqtt5_client_session_resumption_test( } ASSERT_SUCCESS( - s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + aws_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); aws_mqtt5_packet_connect_storage_clean_up(&clean_start_connect_storage); aws_mqtt5_packet_connect_storage_clean_up(&resume_session_connect_storage); @@ -3150,7 +3132,7 @@ static int s_aws_mqtt5_server_send_unsuback_on_unsubscribe( .reason_codes = s_unsuback_reason_codes, }; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); } #define FORWARDED_PUBLISH_PACKET_ID 32768 @@ -3171,7 +3153,7 @@ static int s_aws_mqtt5_server_send_puback_and_forward_on_publish( .reason_code = AWS_MQTT5_PARC_SUCCESS, }; - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { return AWS_OP_ERR; } } @@ -3182,7 +3164,7 @@ static int s_aws_mqtt5_server_send_puback_and_forward_on_publish( reflect_publish_view.packet_id = FORWARDED_PUBLISH_PACKET_ID; } - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflect_publish_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflect_publish_view); } void s_sub_pub_unsub_publish_complete_fn( @@ -3273,7 +3255,7 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_client_mock_test_fixture test_context; struct aws_mqtt5_sub_pub_unsub_context full_test_context = { @@ -3301,7 +3283,7 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_storage expected_subscribe_storage; AWS_ZERO_STRUCT(expected_subscribe_storage); @@ -3320,7 +3302,7 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* verify packets that server received: connect,subscribe, publish, puback(if qos1), unsubscribe */ struct aws_array_list expected_packets; @@ -3358,7 +3340,7 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt }; aws_array_list_push_back(&expected_packets, &unsubscribe_record); - ASSERT_SUCCESS(s_verify_received_packet_sequence( + ASSERT_SUCCESS(aws_verify_received_packet_sequence( &test_context, expected_packets.data, aws_array_list_length(&expected_packets))); /* verify client received the publish that we sent */ @@ -3429,7 +3411,7 @@ static int s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe( .reason_codes = s_unsubscribe_success_reason_codes, }; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); } static int s_mqtt5_client_unsubscribe_success_fn(struct aws_allocator *allocator, void *ctx) { @@ -3438,7 +3420,7 @@ static int s_mqtt5_client_unsubscribe_success_fn(struct aws_allocator *allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe; @@ -3459,7 +3441,7 @@ static int s_mqtt5_client_unsubscribe_success_fn(struct aws_allocator *allocator struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { .topic_filters = s_sub_pub_unsub_topic_filters, @@ -3476,7 +3458,7 @@ static int s_mqtt5_client_unsubscribe_success_fn(struct aws_allocator *allocator ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -3537,14 +3519,14 @@ static void s_aws_mqtt5_mock_server_send_qos1_publish( .packet_id = s_puback_packet_id, }; - s_aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_PUBLISH, &qos1_publish_view); + aws_mqtt5_mock_server_send_packet(mock_server, AWS_MQTT5_PT_PUBLISH, &qos1_publish_view); } static int s_aws_mqtt5_server_send_qos1_publish_on_connect( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { - int result = s_aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + int result = aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); struct aws_mqtt5_server_send_qos1_publish_context *test_context = user_data; test_context->connack_sent = true; @@ -3571,7 +3553,7 @@ static int mqtt5_client_receive_qos1_return_puback_test_fn(struct aws_allocator aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* mock server sends a PUBLISH packet to the client */ test_options.server_function_table.service_task_fn = s_aws_mqtt5_mock_server_send_qos1_publish; @@ -3599,13 +3581,13 @@ static int mqtt5_client_receive_qos1_return_puback_test_fn(struct aws_allocator struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); s_publish_qos1_wait_for_puback(&publish_context); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -3628,7 +3610,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_session_present( connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; connack_view.session_present = true; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } /* When client receives a CONNACK with existing session state when one isn't present it should disconnect */ @@ -3638,7 +3620,7 @@ static int mqtt5_client_receive_nonexisting_session_state_fn(struct aws_allocato aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* mock server returns a CONNACK indicating a session is being resumed */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = @@ -3659,14 +3641,14 @@ static int mqtt5_client_receive_nonexisting_session_state_fn(struct aws_allocato ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); enum aws_mqtt5_client_state expected_states[] = { AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -3707,7 +3689,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_assigned_client_id( test_context->assigned_client_id_checked = true; } - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } /* @@ -3720,7 +3702,7 @@ static int mqtt5_client_receive_assigned_client_id_fn(struct aws_allocator *allo aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* Empty the Client ID for connect */ test_options.connect_options.client_id.len = 0; @@ -3747,7 +3729,7 @@ static int mqtt5_client_receive_assigned_client_id_fn(struct aws_allocator *allo ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_byte_cursor assigned_client_id = aws_byte_cursor_from_c_str(s_receive_assigned_client_id_client_id); struct aws_byte_cursor negotiated_settings_client_id = @@ -3761,18 +3743,18 @@ static int mqtt5_client_receive_assigned_client_id_fn(struct aws_allocator *allo ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* Check for Assigned Client ID on reconnect */ ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); ASSERT_TRUE(assinged_id_context.assigned_client_id_checked); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -3802,7 +3784,7 @@ static int s_aws_mqtt5_mock_server_handle_publish_no_puback_on_first_connect( struct aws_mqtt5_packet_puback_view puback_view = { .packet_id = publish_view->packet_id, }; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); } return AWS_OP_SUCCESS; @@ -3854,7 +3836,7 @@ static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *al aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* Set to rejoin */ test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; @@ -3879,7 +3861,7 @@ static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *al ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { struct aws_mqtt5_packet_publish_view qos1_publish_view = { @@ -3908,11 +3890,11 @@ static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *al ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); aws_mutex_lock(&test_context.lock); size_t event_count = aws_array_list_length(&test_context.lifecycle_events); @@ -3923,7 +3905,7 @@ static int mqtt5_client_no_session_after_client_stop_fn(struct aws_allocator *al ASSERT_FALSE(record->connack_storage.storage_view.session_present); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -3939,7 +3921,7 @@ static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_ aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* Set to rejoin */ test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; @@ -3969,7 +3951,7 @@ static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_ ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { struct aws_mqtt5_packet_publish_view qos1_publish_view = { @@ -4000,12 +3982,12 @@ static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_ s_wait_for_disconnection_lifecycle_event(&test_context); /* Reconnect from a disconnect automatically */ - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); s_wait_for_n_successful_publishes(&wait_context); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); enum aws_mqtt5_client_state expected_states[] = { AWS_MCS_CONNECTING, @@ -4021,7 +4003,7 @@ static int mqtt5_client_restore_session_on_ping_timeout_reconnect_fn(struct aws_ AWS_MCS_STOPPED, }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -4040,7 +4022,7 @@ static int mqtt5_client_discard_session_on_server_clean_start_fn(struct aws_allo aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); /* Set to rejoin */ test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; @@ -4062,7 +4044,7 @@ static int mqtt5_client_discard_session_on_server_clean_start_fn(struct aws_allo ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); for (size_t i = 0; i < TEST_PUBLISH_COUNT; ++i) { struct aws_mqtt5_packet_publish_view qos1_publish_view = { @@ -4091,17 +4073,17 @@ static int mqtt5_client_discard_session_on_server_clean_start_fn(struct aws_allo /* Disconnect with unacked publishes */ ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* Reconnect with a Client Stored Session */ ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); /* Provide time for Client to process any queued operations */ aws_thread_current_sleep(1000000000); ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); /* Check that no publishes were resent after the initial batch on first connect */ ASSERT_INT_EQUALS(test_context.publishes_received, TEST_PUBLISH_COUNT); @@ -4179,7 +4161,7 @@ static int s_mqtt5_client_statistics_subscribe_fn(struct aws_allocator *allocato aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = s_aws_mqtt5_server_send_suback_on_subscribe; @@ -4202,7 +4184,7 @@ static int s_mqtt5_client_statistics_subscribe_fn(struct aws_allocator *allocato struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_view subscribe_view = { .subscriptions = s_subscriptions, @@ -4219,7 +4201,7 @@ static int s_mqtt5_client_statistics_subscribe_fn(struct aws_allocator *allocato ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_client_statistics( &test_context, s_subscribe_test_statistics, AWS_ARRAY_SIZE(s_subscribe_test_statistics))); @@ -4253,7 +4235,7 @@ static int s_mqtt5_client_statistics_unsubscribe_fn(struct aws_allocator *alloca aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = s_aws_mqtt5_server_send_not_subscribe_unsuback_on_unsubscribe; @@ -4274,7 +4256,7 @@ static int s_mqtt5_client_statistics_unsubscribe_fn(struct aws_allocator *alloca struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { .topic_filters = s_sub_pub_unsub_topic_filters, @@ -4291,7 +4273,7 @@ static int s_mqtt5_client_statistics_unsubscribe_fn(struct aws_allocator *alloca ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_client_statistics( &test_context, s_unsubscribe_test_statistics, AWS_ARRAY_SIZE(s_unsubscribe_test_statistics))); @@ -4327,7 +4309,7 @@ static int s_do_mqtt5_client_statistics_publish_test( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_server_send_puback_and_forward_on_publish; @@ -4348,7 +4330,7 @@ static int s_do_mqtt5_client_statistics_publish_test( struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_publish_view publish_view = { .qos = qos, @@ -4374,7 +4356,7 @@ static int s_do_mqtt5_client_statistics_publish_test( ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_client_statistics(&test_context, expected_stats, expected_stats_count)); @@ -4469,7 +4451,7 @@ static int s_aws_mqtt5_server_disconnect_on_first_publish_puback_after( .reason_code = AWS_MQTT5_PARC_SUCCESS, }; - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { return AWS_OP_ERR; } } @@ -4483,7 +4465,7 @@ static int s_mqtt5_client_statistics_publish_qos1_requeue_fn(struct aws_allocato aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_POST_SUCCESS; @@ -4508,7 +4490,7 @@ static int s_mqtt5_client_statistics_publish_qos1_requeue_fn(struct aws_allocato struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_publish_view publish_view = { .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, @@ -4534,7 +4516,7 @@ static int s_mqtt5_client_statistics_publish_qos1_requeue_fn(struct aws_allocato ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_client_statistics( &test_context, s_publish_qos1_requeue_test_statistics, AWS_ARRAY_SIZE(s_publish_qos1_requeue_test_statistics))); @@ -4573,7 +4555,7 @@ static int s_aws_mqtt5_server_send_multiple_publishes( }, }; - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } } @@ -4636,7 +4618,7 @@ static int s_mqtt5_client_puback_ordering_fn(struct aws_allocator *allocator, vo aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_server_send_multiple_publishes; @@ -4657,7 +4639,7 @@ static int s_mqtt5_client_puback_ordering_fn(struct aws_allocator *allocator, vo struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_publish_view publish_view = { .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, @@ -4679,7 +4661,7 @@ static int s_mqtt5_client_puback_ordering_fn(struct aws_allocator *allocator, vo ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_mock_puback_order(&test_context)); @@ -4737,7 +4719,7 @@ static int s_aws_mqtt5_mock_server_reflect_publish( reflected_view.packet_id = 1; } - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflected_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &reflected_view)) { return AWS_OP_ERR; } @@ -4812,7 +4794,7 @@ static int s_mqtt5_client_listeners_fn(struct aws_allocator *allocator, void *ct aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_mock_server_reflect_publish; @@ -4837,7 +4819,7 @@ static int s_mqtt5_client_listeners_fn(struct aws_allocator *allocator, void *ct struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_publish_view qos0_publish_view = { .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, @@ -4916,7 +4898,7 @@ static int s_mqtt5_client_listeners_fn(struct aws_allocator *allocator, void *ct ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); s_aws_mqtt5_listeners_test_context_clean_up(&full_test_context); @@ -5155,7 +5137,7 @@ static int s_mqtt5_client_offline_operation_submission_fail_all_fn(struct aws_al aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; @@ -5196,7 +5178,7 @@ static int s_mqtt5_client_offline_operation_submission_fail_qos0_fn(struct aws_a aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; @@ -5242,7 +5224,7 @@ static int s_mqtt5_client_offline_operation_submission_fail_non_qos1_fn(struct a aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; @@ -5286,7 +5268,7 @@ static int s_mqtt5_client_offline_operation_submission_then_connect_fn(struct aw aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = s_aws_mqtt5_server_send_puback_and_forward_on_publish; @@ -5319,7 +5301,7 @@ static int s_mqtt5_client_offline_operation_submission_then_connect_fn(struct aw ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -5399,7 +5381,7 @@ static int s_aws_mqtt5_server_send_aliased_publish_sequence( .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; // just to be thorough, send a suback - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { return AWS_OP_ERR; } @@ -5416,13 +5398,13 @@ static int s_aws_mqtt5_server_send_aliased_publish_sequence( }; // establish an alias with id 1 - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } // alias alone AWS_ZERO_STRUCT(publish_view.topic); - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } @@ -5430,13 +5412,13 @@ static int s_aws_mqtt5_server_send_aliased_publish_sequence( publish_view.topic.ptr = s_alias_topic2; publish_view.topic.len = AWS_ARRAY_SIZE(s_alias_topic2) - 1; - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } // alias alone AWS_ZERO_STRUCT(publish_view.topic); - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } @@ -5510,7 +5492,7 @@ static int s_mqtt5_client_inbound_alias_success_fn(struct aws_allocator *allocat aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = s_aws_mqtt5_server_send_aliased_publish_sequence; @@ -5541,7 +5523,7 @@ static int s_mqtt5_client_inbound_alias_success_fn(struct aws_allocator *allocat struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_view subscribe = { .subscriptions = s_alias_subscriptions, @@ -5554,7 +5536,7 @@ static int s_mqtt5_client_inbound_alias_success_fn(struct aws_allocator *allocat ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); ASSERT_SUCCESS(s_verify_aliased_publish_sequence(&full_test_context)); @@ -5594,7 +5576,7 @@ static int s_aws_mqtt5_server_send_aliased_publish_failure( .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; // just to be thorough, send a suback - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { return AWS_OP_ERR; } @@ -5622,7 +5604,7 @@ static int s_aws_mqtt5_server_send_aliased_publish_failure( .packet_id = 1, .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, .topic = topic_cursor, .topic_alias = &alias_id}; // establish an alias with id 1 - if (s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &publish_view)) { return AWS_OP_ERR; } @@ -5662,7 +5644,7 @@ static int s_do_inbound_alias_failure_test( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = s_aws_mqtt5_server_send_aliased_publish_failure; @@ -5693,7 +5675,7 @@ static int s_do_inbound_alias_failure_test( struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); struct aws_mqtt5_packet_subscribe_view subscribe = { .subscriptions = s_alias_subscriptions, @@ -5706,7 +5688,7 @@ static int s_do_inbound_alias_failure_test( ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -5788,7 +5770,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_allow_aliasing( uint16_t topic_alias_maximum = SEQUENCE_TEST_CACHE_SIZE; connack_view.topic_alias_maximum = &topic_alias_maximum; - return s_aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } static int s_do_mqtt5_client_outbound_alias_failure_test( @@ -5798,7 +5780,7 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; @@ -5820,7 +5802,7 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); uint16_t topic_alias = 1; struct aws_mqtt5_packet_publish_view packet_publish_view = { @@ -5849,7 +5831,7 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -5976,7 +5958,7 @@ static int s_perform_outbound_alias_sequence_test( aws_mqtt_library_init(allocator); struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); + aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; @@ -5998,7 +5980,7 @@ static int s_perform_outbound_alias_sequence_test( struct aws_mqtt5_client *client = test_context.client; ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_connected_lifecycle_event(&test_context); + aws_wait_for_connected_lifecycle_event(&test_context); ASSERT_SUCCESS(s_perform_outbound_alias_sequence(&test_context, publishes, publish_count)); @@ -6038,7 +6020,7 @@ static int s_perform_outbound_alias_sequence_test( ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - s_wait_for_stopped_lifecycle_event(&test_context); + aws_wait_for_stopped_lifecycle_event(&test_context); aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); aws_mqtt_library_clean_up(); @@ -6091,287 +6073,3 @@ static int s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn(struct aws AWS_TEST_CASE( mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a, s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn) - -void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { - (void)event; -} - -void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { - (void)publish; - (void)user_data; -} - -static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { - aws_mqtt_library_init(allocator); - - struct aws_mqtt5_packet_connect_view local_connect_options = { - .keep_alive_interval_seconds = 30, - .clean_start = true, - }; - - struct aws_mqtt5_client_options client_options = { - .connect_options = &local_connect_options, - .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, - .lifecycle_event_handler_user_data = NULL, - .publish_received_handler = s_mqtt3to5_publish_received_callback, - .publish_received_handler_user_data = NULL, - .ping_timeout_ms = 10000, - }; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { - .client_options = &client_options, - }; - - struct aws_mqtt5_client_mock_test_fixture test_fixture; - AWS_ZERO_STRUCT(test_fixture); - - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); - - struct aws_mqtt_client_connection *connection = - aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener attachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt_client_connection_release(connection); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener detachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); - - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) - -static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( - allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) - -typedef int (*mqtt3to5_adapter_config_test_setup_fn)( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect); - -static int s_do_mqtt3to5_adapter_config_test( - struct aws_allocator *allocator, - mqtt3to5_adapter_config_test_setup_fn setup_fn) { - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt5_client_mock_test_fixture test_context; - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); - - struct aws_mqtt5_client *client = test_context.client; - - struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); - - struct aws_mqtt5_packet_connect_storage expected_connect_storage; - ASSERT_SUCCESS((*setup_fn)(allocator, adapter, &expected_connect_storage)); - - ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - - s_wait_for_connected_lifecycle_event(&test_context); - - ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - - s_wait_for_stopped_lifecycle_event(&test_context); - - struct aws_mqtt5_mock_server_packet_record expected_packets[] = { - { - .packet_type = AWS_MQTT5_PT_CONNECT, - .packet_storage = &expected_connect_storage, - }, - }; - ASSERT_SUCCESS( - s_verify_received_packet_sequence(&test_context, expected_packets, AWS_ARRAY_SIZE(expected_packets))); - - aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); - - aws_mqtt_client_connection_release(adapter); - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_set_will_setup( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect) { - - struct aws_byte_cursor topic_cursor = { - .ptr = s_alias_topic1, - .len = AWS_ARRAY_SIZE(s_alias_topic1) - 1, - }; - - struct aws_byte_cursor payload_cursor = { - .ptr = s_sub_pub_unsub_publish_payload, - .len = AWS_ARRAY_SIZE(s_sub_pub_unsub_publish_payload) - 1, - }; - - ASSERT_SUCCESS( - aws_mqtt_client_connection_set_will(adapter, &topic_cursor, AWS_MQTT_QOS_AT_LEAST_ONCE, true, &payload_cursor)); - - struct aws_mqtt5_packet_publish_view expected_will = { - .payload = payload_cursor, - .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, - .retain = true, - .topic = topic_cursor, - }; - - struct aws_mqtt5_packet_connect_view expected_connect_view = { - .client_id = aws_byte_cursor_from_string(s_client_id), - .keep_alive_interval_seconds = 30, - .clean_start = true, - .will = &expected_will, - }; - - ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_set_will_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_will_setup)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_will, s_mqtt3to5_adapter_set_will_fn) - -AWS_STATIC_STRING_FROM_LITERAL(s_username, "MyUsername"); -AWS_STATIC_STRING_FROM_LITERAL(s_password, "TopTopSecret"); - -static int s_mqtt3to5_adapter_set_login_setup( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect) { - - struct aws_byte_cursor username_cursor = aws_byte_cursor_from_string(s_username); - struct aws_byte_cursor password_cursor = aws_byte_cursor_from_string(s_password); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(adapter, &username_cursor, &password_cursor)); - - struct aws_mqtt5_packet_connect_view expected_connect_view = { - .client_id = aws_byte_cursor_from_string(s_client_id), - .keep_alive_interval_seconds = 30, - .clean_start = true, - .username = &username_cursor, - .password = &password_cursor, - }; - - ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_set_login_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_login_setup)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_login, s_mqtt3to5_adapter_set_login_fn) - -static int s_mqtt3to5_adapter_set_reconnect_timeout_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - /* - * This is a variant of the mqtt5_client_reconnect_failure_backoff test. - * - * The primary change is that we configure the mqtt5 client with "wrong" (fast) reconnect delays and then use - * the adapter API to configure with the "right" ones that will let the test pass. - */ - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - s_mqtt5_client_test_init_default_options(&test_options); - - /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ - test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; - test_options.client_options.min_reconnect_delay_ms = 10; - test_options.client_options.max_reconnect_delay_ms = 50; - test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; - - test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_always_fail; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt5_client_mock_test_fixture test_context; - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); - - struct aws_mqtt5_client *client = test_context.client; - - struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); - - aws_mqtt_client_connection_set_reconnect_timeout(adapter, RECONNECT_TEST_MIN_BACKOFF, RECONNECT_TEST_MAX_BACKOFF); - - ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - - s_wait_for_n_connection_failure_lifecycle_events(&test_context, 6); - - ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - - s_wait_for_stopped_lifecycle_event(&test_context); - - ASSERT_SUCCESS(s_verify_reconnection_exponential_backoff_timestamps(&test_context)); - - /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ - enum aws_mqtt5_client_state expected_states[] = { - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, - }; - ASSERT_SUCCESS(s_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); - - aws_mqtt_client_connection_release(adapter); - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_reconnect_timeout, s_mqtt3to5_adapter_set_reconnect_timeout_fn) \ No newline at end of file diff --git a/tests/v5/mqtt5_testing_utils.h b/tests/v5/mqtt5_testing_utils.h index 4ccb1b43..52e8859e 100644 --- a/tests/v5/mqtt5_testing_utils.h +++ b/tests/v5/mqtt5_testing_utils.h @@ -121,6 +121,22 @@ struct aws_mqtt5_client_mock_test_fixture { uint32_t server_current_inflight_publishes; }; +struct mqtt5_client_test_options { + struct aws_mqtt5_client_topic_alias_options topic_aliasing_options; + struct aws_mqtt5_packet_connect_view connect_options; + struct aws_mqtt5_client_options client_options; + struct aws_mqtt5_mock_server_vtable server_function_table; +}; + +struct aws_mqtt5_mock_server_reconnect_state { + size_t required_connection_failure_count; + + size_t connection_attempts; + uint64_t connect_timestamp; + + uint64_t successful_connection_disconnect_delay_ms; +}; + int aws_mqtt5_test_verify_user_properties_raw( size_t property_count, const struct aws_mqtt5_user_property *properties, @@ -145,4 +161,52 @@ bool aws_mqtt5_client_test_are_packets_equal( size_t aws_mqtt5_linked_list_length(struct aws_linked_list *list); +void aws_mqtt5_client_test_init_default_options(struct mqtt5_client_test_options *test_options); + +void aws_wait_for_connected_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context); +void aws_wait_for_stopped_lifecycle_event(struct aws_mqtt5_client_mock_test_fixture *test_context); + +int aws_verify_received_packet_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_context, + struct aws_mqtt5_mock_server_packet_record *expected_packets, + size_t expected_packets_count); + +int aws_mqtt5_mock_server_handle_connect_always_fail( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +void aws_mqtt5_wait_for_n_lifecycle_events( + struct aws_mqtt5_client_mock_test_fixture *test_context, + enum aws_mqtt5_client_lifecycle_event_type type, + size_t count); + +int aws_verify_reconnection_exponential_backoff_timestamps(struct aws_mqtt5_client_mock_test_fixture *test_fixture); + +int aws_verify_client_state_sequence( + struct aws_mqtt5_client_mock_test_fixture *test_context, + enum aws_mqtt5_client_state *expected_states, + size_t expected_states_count); + +int aws_mqtt5_mock_server_handle_connect_always_succeed( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +int aws_mqtt5_mock_server_send_packet( + struct aws_mqtt5_server_mock_connection_context *connection, + enum aws_mqtt5_packet_type packet_type, + void *packet); + +int aws_mqtt5_mock_server_handle_connect_succeed_on_nth( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +extern const struct aws_string *g_default_client_id; + +#define RECONNECT_TEST_MIN_BACKOFF 500 +#define RECONNECT_TEST_MAX_BACKOFF 5000 +#define RECONNECT_TEST_BACKOFF_RESET_DELAY 5000 + #endif /* MQTT_MQTT5_TESTING_UTILS_H */ From 7aaae0ce8c644faf12fcb6544f8f105787596101 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 18 Jul 2023 10:05:10 -0700 Subject: [PATCH 78/98] Run forgotten content type tests (#309) * Run forgotten content type tests --------- Co-authored-by: Bret Ambrose --- tests/CMakeLists.txt | 2 ++ tests/v5/mqtt5_encoding_tests.c | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 91d3e0be..dc757dfb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -203,6 +203,8 @@ add_test_case(mqtt5_operation_publish_validation_failure_response_topic_too_long add_test_case(mqtt5_operation_publish_validation_failure_invalid_response_topic) add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_response_topic) add_test_case(mqtt5_operation_publish_validation_failure_correlation_data_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_content_type_too_long) +add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_content_type) add_test_case(mqtt5_operation_publish_validation_failure_subscription_identifier_exists) add_test_case(mqtt5_operation_publish_validation_failure_topic_alias_zero) add_test_case(mqtt5_operation_publish_validation_failure_user_properties_name_too_long) diff --git a/tests/v5/mqtt5_encoding_tests.c b/tests/v5/mqtt5_encoding_tests.c index daa25141..879a5e9d 100644 --- a/tests/v5/mqtt5_encoding_tests.c +++ b/tests/v5/mqtt5_encoding_tests.c @@ -14,12 +14,6 @@ #include -/* - * AWS_MQTT_API enum aws_mqtt5_decode_result_type aws_mqtt5_decode_vli(struct aws_byte_cursor *cursor, uint32_t *dest); - * AWS_MQTT_API int aws_mqtt5_encode_variable_length_integer(struct aws_byte_buf *buf, uint32_t value); - * AWS_MQTT_API int aws_mqtt5_get_variable_length_encode_size(size_t value, size_t *encode_size); - */ - static int s_mqtt5_vli_size_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; (void)allocator; @@ -47,9 +41,6 @@ static int s_mqtt5_vli_size_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16384, &encode_size)); ASSERT_INT_EQUALS(3, encode_size); - ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16384, &encode_size)); - ASSERT_INT_EQUALS(3, encode_size); - ASSERT_SUCCESS(aws_mqtt5_get_variable_length_encode_size(16385, &encode_size)); ASSERT_INT_EQUALS(3, encode_size); From d5c268f70aeccf38e75d3e74ce4eb9629df02e2a Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 26 Jul 2023 13:10:29 -0700 Subject: [PATCH 79/98] Mqtt311 decoder (#311) * Fix a framing bug that led to dead/broken connections * Refactor the framing logic used by the mqtt311 implementation to use an encapsulated, testable subsystem (aws_mqtt311_decoder). --------- Co-authored-by: Bret Ambrose --- include/aws/mqtt/private/client_impl.h | 6 +- include/aws/mqtt/private/fixed_header.h | 2 + include/aws/mqtt/private/mqtt311_decoder.h | 135 ++++ source/client.c | 11 + source/client_channel_handler.c | 322 +++++----- source/fixed_header.c | 5 +- source/mqtt311_decoder.c | 211 +++++++ tests/CMakeLists.txt | 10 + tests/v3/packet_framing_tests.c | 693 +++++++++++++++++++++ 9 files changed, 1219 insertions(+), 176 deletions(-) create mode 100644 include/aws/mqtt/private/mqtt311_decoder.h create mode 100644 source/mqtt311_decoder.c create mode 100644 tests/v3/packet_framing_tests.c diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 169dadb9..68efb114 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -261,8 +262,7 @@ struct aws_mqtt_client_connection_311_impl { /* Only the event-loop thread may touch this data */ struct { - /* If an incomplete packet arrives, store the data here. */ - struct aws_byte_buf pending_packet; + struct aws_mqtt311_decoder decoder; bool waiting_on_ping_response; @@ -416,4 +416,6 @@ void aws_mqtt_connection_statistics_change_operation_statistic_state( struct aws_mqtt_request *request, enum aws_mqtt_operation_statistic_state_flags new_state_flags); +AWS_MQTT_API const struct aws_mqtt_client_connection_packet_handlers *aws_mqtt311_get_default_packet_handlers(void); + #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/fixed_header.h b/include/aws/mqtt/private/fixed_header.h index 4944c86f..98e2758a 100644 --- a/include/aws/mqtt/private/fixed_header.h +++ b/include/aws/mqtt/private/fixed_header.h @@ -59,4 +59,6 @@ AWS_MQTT_API int aws_mqtt_fixed_header_encode(struct aws_byte_buf *buf, const st */ AWS_MQTT_API int aws_mqtt_fixed_header_decode(struct aws_byte_cursor *cur, struct aws_mqtt_fixed_header *header); +AWS_MQTT_API int aws_mqtt311_decode_remaining_length(struct aws_byte_cursor *cur, size_t *remaining_length_out); + #endif /* AWS_MQTT_PRIVATE_FIXED_HEADER_H */ diff --git a/include/aws/mqtt/private/mqtt311_decoder.h b/include/aws/mqtt/private/mqtt311_decoder.h new file mode 100644 index 00000000..4147168d --- /dev/null +++ b/include/aws/mqtt/private/mqtt311_decoder.h @@ -0,0 +1,135 @@ +#ifndef AWS_MQTT_PRIVATE_MQTT311_DECODER_H +#define AWS_MQTT_PRIVATE_MQTT311_DECODER_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +/* + * Per-packet-type callback signature. message_cursor contains the entire packet's data. + */ +typedef int(packet_handler_fn)(struct aws_byte_cursor message_cursor, void *user_data); + +/* + * Wrapper for a set of packet handlers for each possible MQTT packet type. Some values are invalid in 311 (15), and + * some values are only valid from the perspective of the server or client. + */ +struct aws_mqtt_client_connection_packet_handlers { + packet_handler_fn *handlers_by_packet_type[16]; +}; + +/* + * Internal state of the 311 decoder/framing logic. + * + * When a packet is fragmented across multiple io buffers, state moves circularly: + * first byte -> remaining length -> body -> first byte etc... + * + * When a packet is completely contained inside a single io buffer, the entire packet is processed within + * the READ_FIRST_BYTE state. + */ +enum aws_mqtt_311_decoder_state_type { + + /* + * The decoder is expecting the first byte of the fixed header of an MQTT control packet + */ + AWS_MDST_READ_FIRST_BYTE, + + /* + * The decoder is expecting the vli-encoded total remaining length field of the fixed header on an MQTT control + * packet. + */ + AWS_MDST_READ_REMAINING_LENGTH, + + /* + * The decoder is expecting the "rest" of the MQTT packet's data based on the remaining length value that has + * already been read. + */ + AWS_MDST_READ_BODY, + + /* + * Terminal state for when a protocol error has been encountered by the decoder. The only way to leave this + * state is to reset the decoder via the aws_mqtt311_decoder_reset_for_new_connection() API. + */ + AWS_MDST_PROTOCOL_ERROR, +}; + +/* + * Configuration options for the decoder. When used by the actual implementation, handler_user_data is the + * connection object and the packet handlers are channel functions that hook into reactionary behavior and user + * callbacks. + */ +struct aws_mqtt311_decoder_options { + const struct aws_mqtt_client_connection_packet_handlers *packet_handlers; + void *handler_user_data; +}; + +/* + * Simple MQTT311 decoder structure. Actual decoding is deferred to per-packet functions that expect the whole + * packet in a buffer. The primary function of this sub-system is correctly framing a stream of bytes into the + * constituent packets. + */ +struct aws_mqtt311_decoder { + struct aws_mqtt311_decoder_options config; + + enum aws_mqtt_311_decoder_state_type state; + + /* + * If zero, not valid. If non-zero, represents the number of bytes that need to be read to finish the packet. + * This includes the total encoding size of the fixed header. + */ + size_t total_packet_length; + + /* scratch buffer to hold individual packets when they fragment across incoming data frame boundaries */ + struct aws_byte_buf packet_buffer; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Initialize function for the MQTT311 decoder + * + * @param decoder decoder to initialize + * @param allocator memory allocator to use + * @param options additional decoder configuration options + */ +AWS_MQTT_API void aws_mqtt311_decoder_init( + struct aws_mqtt311_decoder *decoder, + struct aws_allocator *allocator, + const struct aws_mqtt311_decoder_options *options); + +/** + * Clean up function for an MQTT311 decoder + * + * @param decoder decoder to release resources for + */ +AWS_MQTT_API void aws_mqtt311_decoder_clean_up(struct aws_mqtt311_decoder *decoder); + +/** + * Callback function to decode the incoming data stream of an MQTT311 connection. Handles packet framing and + * correct decoder/handler function dispatch. + * + * @param decoder decoder to decode with + * @param data raw plaintext bytes of a connection operating on the MQTT311 protocol + * @return success/failure, failure represents a protocol error and implies the connection must be shut down + */ +AWS_MQTT_API int aws_mqtt311_decoder_on_bytes_received( + struct aws_mqtt311_decoder *decoder, + struct aws_byte_cursor data); + +/** + * Resets a decoder's state to its initial values. If using a decoder across multiple network connections (within + * the same client), you must invoke this when setting up a new connection, before any MQTT protocol bytes are + * processed. Breaks the decoder out of any previous protocol error terminal state. + * + * @param decoder decoder to reset + */ +AWS_MQTT_API void aws_mqtt311_decoder_reset_for_new_connection(struct aws_mqtt311_decoder *decoder); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_PRIVATE_MQTT311_DECODER_H */ diff --git a/source/client.c b/source/client.c index 126c8ae2..dd1acb80 100644 --- a/source/client.c +++ b/source/client.c @@ -575,6 +575,8 @@ static void s_mqtt_client_init( goto handle_error; } + aws_mqtt311_decoder_reset_for_new_connection(&connection->thread_data.decoder); + AWS_LOGF_DEBUG( AWS_LS_MQTT_CLIENT, "id=%p: Connection successfully opened, sending CONNECT packet", (void *)connection); @@ -822,6 +824,8 @@ static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connec /* Free all of the active subscriptions */ aws_mqtt_topic_tree_clean_up(&connection->thread_data.subscriptions); + aws_mqtt311_decoder_clean_up(&connection->thread_data.decoder); + aws_hash_table_clean_up(&connection->synced_data.outstanding_requests_table); /* clean up the pending_requests if it's not empty */ while (!aws_linked_list_empty(&connection->synced_data.pending_requests_list)) { @@ -3197,6 +3201,7 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt aws_ref_count_init( &connection->ref_count, connection, (aws_simple_completion_callback *)s_mqtt_client_connection_start_destroy); connection->client = aws_mqtt_client_acquire(client); + AWS_ZERO_STRUCT(connection->synced_data); connection->synced_data.state = AWS_MQTT_CLIENT_STATE_DISCONNECTED; connection->reconnect_timeouts.min_sec = 1; @@ -3216,6 +3221,12 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt goto failed_init_mutex; } + struct aws_mqtt311_decoder_options config = { + .packet_handlers = aws_mqtt311_get_default_packet_handlers(), + .handler_user_data = connection, + }; + aws_mqtt311_decoder_init(&connection->thread_data.decoder, client->allocator, &config); + if (aws_mqtt_topic_tree_init(&connection->thread_data.subscriptions, connection->allocator)) { AWS_LOGF_ERROR( diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 70b77c1b..6f5502d6 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -36,15 +36,10 @@ static void s_update_next_ping_time(struct aws_mqtt_client_connection_311_impl * * Packet State Machine ******************************************************************************/ -typedef int( - packet_handler_fn)(struct aws_mqtt_client_connection_311_impl *connection, struct aws_byte_cursor message_cursor); - -static int s_packet_handler_default( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { - (void)connection; +static int s_packet_handler_default(struct aws_byte_cursor message_cursor, void *user_data) { (void)message_cursor; + struct aws_mqtt_client_connection_311_impl *connection = user_data; AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Unhandled packet type received", (void *)connection); return aws_raise_error(AWS_ERROR_MQTT_INVALID_PACKET_TYPE); } @@ -94,11 +89,45 @@ static void s_on_time_to_ping(struct aws_channel_task *channel_task, void *arg, s_schedule_ping(connection); } } -static int s_packet_handler_connack( + +static int s_validate_received_packet_type( struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { + enum aws_mqtt_packet_type packet_type) { + { /* BEGIN CRITICAL SECTION */ + mqtt_connection_lock_synced_data(connection); + /* [MQTT-3.2.0-1] The first packet sent from the Server to the Client MUST be a CONNACK Packet */ + if (connection->synced_data.state == AWS_MQTT_CLIENT_STATE_CONNECTING && + packet_type != AWS_MQTT_PACKET_CONNACK) { + mqtt_connection_unlock_synced_data(connection); + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: First message received from the server was not a CONNACK. Terminating connection.", + (void *)connection); + return aws_raise_error(AWS_ERROR_MQTT_PROTOCOL_ERROR); + } + mqtt_connection_unlock_synced_data(connection); + } /* END CRITICAL SECTION */ + + if (AWS_UNLIKELY(packet_type > AWS_MQTT_PACKET_DISCONNECT || packet_type < AWS_MQTT_PACKET_CONNECT)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_CLIENT, + "id=%p: Invalid packet type received %d. Terminating connection.", + (void *)connection, + packet_type); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_PACKET_TYPE); + } - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: CONNACK received", (void *)connection); + /* Handle the packet */ + return AWS_OP_SUCCESS; +} + +static int s_packet_handler_connack(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: CONNACK received", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_CONNACK)) { + return AWS_OP_ERR; + } struct aws_mqtt_packet_connack connack; if (aws_mqtt_packet_connack_decode(&message_cursor, &connack)) { @@ -219,9 +248,12 @@ static int s_packet_handler_connack( return AWS_OP_SUCCESS; } -static int s_packet_handler_publish( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_publish(struct aws_byte_cursor message_cursor, void *user_data) { + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: PUBLISH received", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_PUBLISH)) { + return AWS_OP_ERR; + } /* TODO: need to handle the QoS 2 message to avoid processing the message a second time */ struct aws_mqtt_packet_publish publish; @@ -291,9 +323,14 @@ static int s_packet_handler_publish( return AWS_OP_SUCCESS; } -static int s_packet_handler_ack( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_puback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a PUBACK", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_PUBACK)) { + return AWS_OP_ERR; + } + struct aws_mqtt_packet_ack ack; if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { return AWS_OP_ERR; @@ -307,9 +344,14 @@ static int s_packet_handler_ack( return AWS_OP_SUCCESS; } -static int s_packet_handler_suback( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_suback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a SUBACK", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_SUBACK)) { + return AWS_OP_ERR; + } + struct aws_mqtt_packet_suback suback; if (aws_mqtt_packet_suback_init(&suback, connection->allocator, 0 /* fake packet_id */)) { return AWS_OP_ERR; @@ -367,9 +409,34 @@ static int s_packet_handler_suback( return AWS_OP_ERR; } -static int s_packet_handler_pubrec( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_unsuback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a UNSUBACK", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_UNSUBACK)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_ack ack; + if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_CLIENT, "id=%p: received ack for message id %" PRIu16, (void *)connection, ack.packet_identifier); + + mqtt_request_complete(connection, AWS_ERROR_SUCCESS, ack.packet_identifier); + + return AWS_OP_SUCCESS; +} + +static int s_packet_handler_pubrec(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a PUBREC", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_PUBREC)) { + return AWS_OP_ERR; + } struct aws_mqtt_packet_ack ack; if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { @@ -405,9 +472,13 @@ static int s_packet_handler_pubrec( return AWS_OP_ERR; } -static int s_packet_handler_pubrel( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_pubrel(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a PUBREL", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_PUBREL)) { + return AWS_OP_ERR; + } struct aws_mqtt_packet_ack ack; if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { @@ -440,13 +511,33 @@ static int s_packet_handler_pubrel( return AWS_OP_ERR; } -static int s_packet_handler_pingresp( - struct aws_mqtt_client_connection_311_impl *connection, - struct aws_byte_cursor message_cursor) { +static int s_packet_handler_pubcomp(struct aws_byte_cursor message_cursor, void *user_data) { + + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: received a PUBCOMP", (void *)connection); + if (s_validate_received_packet_type(connection, AWS_MQTT_PACKET_PUBCOMP)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_ack ack; + if (aws_mqtt_packet_ack_decode(&message_cursor, &ack)) { + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_CLIENT, "id=%p: received ack for message id %" PRIu16, (void *)connection, ack.packet_identifier); + + mqtt_request_complete(connection, AWS_ERROR_SUCCESS, ack.packet_identifier); + + return AWS_OP_SUCCESS; +} + +static int s_packet_handler_pingresp(struct aws_byte_cursor message_cursor, void *user_data) { (void)message_cursor; - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: PINGRESP received", (void *)connection); + struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: PINGRESP received", (void *)connection); connection->thread_data.waiting_on_ping_response = false; @@ -454,60 +545,32 @@ static int s_packet_handler_pingresp( } /* Bake up a big ol' function table just like Gramma used to make */ -static packet_handler_fn *s_packet_handlers[] = { - [AWS_MQTT_PACKET_CONNECT] = &s_packet_handler_default, - [AWS_MQTT_PACKET_CONNACK] = &s_packet_handler_connack, - [AWS_MQTT_PACKET_PUBLISH] = &s_packet_handler_publish, - [AWS_MQTT_PACKET_PUBACK] = &s_packet_handler_ack, - [AWS_MQTT_PACKET_PUBREC] = &s_packet_handler_pubrec, - [AWS_MQTT_PACKET_PUBREL] = &s_packet_handler_pubrel, - [AWS_MQTT_PACKET_PUBCOMP] = &s_packet_handler_ack, - [AWS_MQTT_PACKET_SUBSCRIBE] = &s_packet_handler_default, - [AWS_MQTT_PACKET_SUBACK] = &s_packet_handler_suback, - [AWS_MQTT_PACKET_UNSUBSCRIBE] = &s_packet_handler_default, - [AWS_MQTT_PACKET_UNSUBACK] = &s_packet_handler_ack, - [AWS_MQTT_PACKET_PINGREQ] = &s_packet_handler_default, - [AWS_MQTT_PACKET_PINGRESP] = &s_packet_handler_pingresp, - [AWS_MQTT_PACKET_DISCONNECT] = &s_packet_handler_default, -}; +static struct aws_mqtt_client_connection_packet_handlers s_default_packet_handlers = { + .handlers_by_packet_type = { + [AWS_MQTT_PACKET_CONNECT] = &s_packet_handler_default, + [AWS_MQTT_PACKET_CONNACK] = &s_packet_handler_connack, + [AWS_MQTT_PACKET_PUBLISH] = &s_packet_handler_publish, + [AWS_MQTT_PACKET_PUBACK] = &s_packet_handler_puback, + [AWS_MQTT_PACKET_PUBREC] = &s_packet_handler_pubrec, + [AWS_MQTT_PACKET_PUBREL] = &s_packet_handler_pubrel, + [AWS_MQTT_PACKET_PUBCOMP] = &s_packet_handler_pubcomp, + [AWS_MQTT_PACKET_SUBSCRIBE] = &s_packet_handler_default, + [AWS_MQTT_PACKET_SUBACK] = &s_packet_handler_suback, + [AWS_MQTT_PACKET_UNSUBSCRIBE] = &s_packet_handler_default, + [AWS_MQTT_PACKET_UNSUBACK] = &s_packet_handler_unsuback, + [AWS_MQTT_PACKET_PINGREQ] = &s_packet_handler_default, + [AWS_MQTT_PACKET_PINGRESP] = &s_packet_handler_pingresp, + [AWS_MQTT_PACKET_DISCONNECT] = &s_packet_handler_default, + }}; + +const struct aws_mqtt_client_connection_packet_handlers *aws_mqtt311_get_default_packet_handlers(void) { + return &s_default_packet_handlers; +} /******************************************************************************* * Channel Handler ******************************************************************************/ -static int s_process_mqtt_packet( - struct aws_mqtt_client_connection_311_impl *connection, - enum aws_mqtt_packet_type packet_type, - struct aws_byte_cursor packet) { - { /* BEGIN CRITICAL SECTION */ - mqtt_connection_lock_synced_data(connection); - /* [MQTT-3.2.0-1] The first packet sent from the Server to the Client MUST be a CONNACK Packet */ - if (connection->synced_data.state == AWS_MQTT_CLIENT_STATE_CONNECTING && - packet_type != AWS_MQTT_PACKET_CONNACK) { - mqtt_connection_unlock_synced_data(connection); - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: First message received from the server was not a CONNACK. Terminating connection.", - (void *)connection); - aws_channel_shutdown(connection->slot->channel, AWS_ERROR_MQTT_PROTOCOL_ERROR); - return aws_raise_error(AWS_ERROR_MQTT_PROTOCOL_ERROR); - } - mqtt_connection_unlock_synced_data(connection); - } /* END CRITICAL SECTION */ - - if (AWS_UNLIKELY(packet_type > AWS_MQTT_PACKET_DISCONNECT || packet_type < AWS_MQTT_PACKET_CONNECT)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT_CLIENT, - "id=%p: Invalid packet type received %d. Terminating connection.", - (void *)connection, - packet_type); - return aws_raise_error(AWS_ERROR_MQTT_INVALID_PACKET_TYPE); - } - - /* Handle the packet */ - return s_packet_handlers[packet_type](connection, packet); -} - /** * Handles incoming messages from the server. */ @@ -531,103 +594,18 @@ static int s_process_read_message( /* This cursor will be updated as we read through the message. */ struct aws_byte_cursor message_cursor = aws_byte_cursor_from_buf(&message->message_data); - /* If there's pending packet left over from last time, attempt to complete it. */ - if (connection->thread_data.pending_packet.len) { - int result = AWS_OP_SUCCESS; - - /* This determines how much to read from the message (min(expected_remaining, message.len)) */ - size_t to_read = connection->thread_data.pending_packet.capacity - connection->thread_data.pending_packet.len; - /* This will be set to false if this message still won't complete the packet object. */ - bool packet_complete = true; - if (to_read > message_cursor.len) { - to_read = message_cursor.len; - packet_complete = false; - } - - /* Write the chunk to the buffer. - * This will either complete the packet, or be the entirety of message if more data is required. */ - struct aws_byte_cursor chunk = aws_byte_cursor_advance(&message_cursor, to_read); - AWS_ASSERT(chunk.ptr); /* Guaranteed to be in bounds */ - result = (int)aws_byte_buf_write_from_whole_cursor(&connection->thread_data.pending_packet, chunk) - 1; - if (result) { - goto handle_error; - } - - /* If the packet is still incomplete, don't do anything with the data. */ - if (!packet_complete) { - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, - "id=%p: partial message is still incomplete, waiting on another read.", - (void *)connection); - - goto cleanup; - } - - /* Handle the completed pending packet */ - struct aws_byte_cursor packet_data = aws_byte_cursor_from_buf(&connection->thread_data.pending_packet); - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: full mqtt packet re-assembled, dispatching.", (void *)connection); - result = s_process_mqtt_packet(connection, aws_mqtt_get_packet_type(packet_data.ptr), packet_data); - - handle_error: - /* Clean up the pending packet */ - aws_byte_buf_clean_up(&connection->thread_data.pending_packet); - AWS_ZERO_STRUCT(connection->thread_data.pending_packet); - - if (result) { - return AWS_OP_ERR; - } - } + int result = aws_mqtt311_decoder_on_bytes_received(&connection->thread_data.decoder, message_cursor); - while (message_cursor.len) { - - /* Temp byte cursor so we can decode the header without advancing message_cursor. */ - struct aws_byte_cursor header_decode = message_cursor; - - struct aws_mqtt_fixed_header packet_header; - AWS_ZERO_STRUCT(packet_header); - int result = aws_mqtt_fixed_header_decode(&header_decode, &packet_header); - - /* Calculate how much data was read. */ - const size_t fixed_header_size = message_cursor.len - header_decode.len; - - if (result) { - if (aws_last_error() == AWS_ERROR_SHORT_BUFFER) { - /* Message data too short, store data and come back later. */ - AWS_LOGF_TRACE( - AWS_LS_MQTT_CLIENT, "id=%p: message is incomplete, waiting on another read.", (void *)connection); - if (aws_byte_buf_init( - &connection->thread_data.pending_packet, - connection->allocator, - fixed_header_size + packet_header.remaining_length)) { - - return AWS_OP_ERR; - } - - /* Write the partial packet. */ - if (!aws_byte_buf_write_from_whole_cursor(&connection->thread_data.pending_packet, message_cursor)) { - aws_byte_buf_clean_up(&connection->thread_data.pending_packet); - return AWS_OP_ERR; - } - - aws_reset_error(); - goto cleanup; - } else { - return AWS_OP_ERR; - } - } - - struct aws_byte_cursor packet_data = - aws_byte_cursor_advance(&message_cursor, fixed_header_size + packet_header.remaining_length); - AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: full mqtt packet read, dispatching.", (void *)connection); - s_process_mqtt_packet(connection, packet_header.packet_type, packet_data); + if (result == AWS_OP_SUCCESS) { + /* Do cleanup */ + size_t message_data_length = message->message_data.len; + aws_mem_release(message->allocator, message); + aws_channel_slot_increment_read_window(slot, message_data_length); + } else { + aws_channel_shutdown(connection->slot->channel, aws_last_error()); } -cleanup: - /* Do cleanup */ - aws_channel_slot_increment_read_window(slot, message->message_data.len); - aws_mem_release(message->allocator, message); - - return AWS_OP_SUCCESS; + return result; } static int s_shutdown( diff --git a/source/fixed_header.c b/source/fixed_header.c index 22372f41..670ae978 100644 --- a/source/fixed_header.c +++ b/source/fixed_header.c @@ -29,7 +29,8 @@ static int s_encode_remaining_length(struct aws_byte_buf *buf, size_t remaining_ return AWS_OP_SUCCESS; } -static int s_decode_remaining_length(struct aws_byte_cursor *cur, size_t *remaining_length_out) { + +int aws_mqtt311_decode_remaining_length(struct aws_byte_cursor *cur, size_t *remaining_length_out) { AWS_PRECONDITION(cur); @@ -128,7 +129,7 @@ int aws_mqtt_fixed_header_decode(struct aws_byte_cursor *cur, struct aws_mqtt_fi header->flags = byte_1 & 0xF; /* Read remaining length */ - if (s_decode_remaining_length(cur, &header->remaining_length)) { + if (aws_mqtt311_decode_remaining_length(cur, &header->remaining_length)) { return AWS_OP_ERR; } if (cur->len < header->remaining_length) { diff --git a/source/mqtt311_decoder.c b/source/mqtt311_decoder.c new file mode 100644 index 00000000..cb1f26c4 --- /dev/null +++ b/source/mqtt311_decoder.c @@ -0,0 +1,211 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +static void s_aws_mqtt311_decoder_reset(struct aws_mqtt311_decoder *decoder) { + decoder->state = AWS_MDST_READ_FIRST_BYTE; + decoder->total_packet_length = 0; + aws_byte_buf_reset(&decoder->packet_buffer, false); +} + +void aws_mqtt311_decoder_init( + struct aws_mqtt311_decoder *decoder, + struct aws_allocator *allocator, + const struct aws_mqtt311_decoder_options *options) { + + aws_byte_buf_init(&decoder->packet_buffer, allocator, 5); + decoder->config = *options; + + s_aws_mqtt311_decoder_reset(decoder); +} + +void aws_mqtt311_decoder_clean_up(struct aws_mqtt311_decoder *decoder) { + aws_byte_buf_clean_up(&decoder->packet_buffer); +} + +static void s_aws_mqtt311_decoder_reset_for_new_packet(struct aws_mqtt311_decoder *decoder) { + if (decoder->state != AWS_MDST_PROTOCOL_ERROR) { + s_aws_mqtt311_decoder_reset(decoder); + } +} + +enum aws_mqtt311_decoding_directive { AWS_MDD_CONTINUE, AWS_MDD_OUT_OF_DATA, AWS_MDD_PROTOCOL_ERROR }; + +static enum aws_mqtt311_decoding_directive aws_result_to_mqtt311_decoding_directive(int result) { + return (result == AWS_OP_SUCCESS) ? AWS_MDD_CONTINUE : AWS_MDD_PROTOCOL_ERROR; +} + +static int s_aws_mqtt311_decoder_safe_packet_handle( + struct aws_mqtt311_decoder *decoder, + enum aws_mqtt_packet_type packet_type, + struct aws_byte_cursor packet_cursor) { + packet_handler_fn *handler = decoder->config.packet_handlers->handlers_by_packet_type[packet_type]; + if (handler != NULL) { + return handler(packet_cursor, decoder->config.handler_user_data); + } else { + return aws_raise_error(AWS_ERROR_MQTT_PROTOCOL_ERROR); + } +} + +static enum aws_mqtt311_decoding_directive s_handle_decoder_read_first_byte( + struct aws_mqtt311_decoder *decoder, + struct aws_byte_cursor *data) { + AWS_FATAL_ASSERT(decoder->packet_buffer.len == 0); + if (data->len == 0) { + return AWS_MDD_OUT_OF_DATA; + } + + /* + * Do a greedy check to see if the whole MQTT packet is contained within the received data. If it is, decode it + * directly from the incoming data cursor without buffering it first. Otherwise, the packet is fragmented + * across multiple received data calls, and so we must use the packet buffer and copy everything first via the + * full decoder state machine. + * + * A corollary of this is that the decoder is only ever in the AWS_MDST_READ_REMAINING_LENGTH or AWS_MDST_READ_BODY + * states if the current MQTT packet was received in a fragmented manner. + */ + struct aws_byte_cursor temp_cursor = *data; + struct aws_mqtt_fixed_header packet_header; + AWS_ZERO_STRUCT(packet_header); + if (!aws_mqtt_fixed_header_decode(&temp_cursor, &packet_header) && + temp_cursor.len >= packet_header.remaining_length) { + + /* figure out the cursor that spans the full packet */ + size_t fixed_header_length = temp_cursor.ptr - data->ptr; + struct aws_byte_cursor whole_packet_cursor = *data; + whole_packet_cursor.len = fixed_header_length + packet_header.remaining_length; + + /* advance the external, mutable data cursor to the start of the next packet */ + aws_byte_cursor_advance(data, whole_packet_cursor.len); + + /* + * if this fails, the decoder goes into an error state. If it succeeds we'll loop again into the same state + * because we'll be back at the beginning of the next packet (if it exists). + */ + enum aws_mqtt_packet_type packet_type = aws_mqtt_get_packet_type(whole_packet_cursor.ptr); + return aws_result_to_mqtt311_decoding_directive( + s_aws_mqtt311_decoder_safe_packet_handle(decoder, packet_type, whole_packet_cursor)); + } + + /* + * The packet is fragmented, spanning more than this io message. So we buffer it and use the + * simple state machine to decode. + */ + uint8_t byte = *data->ptr; + aws_byte_cursor_advance(data, 1); + aws_byte_buf_append_byte_dynamic(&decoder->packet_buffer, byte); + + decoder->state = AWS_MDST_READ_REMAINING_LENGTH; + + return AWS_MDD_CONTINUE; +} + +static enum aws_mqtt311_decoding_directive s_handle_decoder_read_remaining_length( + struct aws_mqtt311_decoder *decoder, + struct aws_byte_cursor *data) { + AWS_FATAL_ASSERT(decoder->total_packet_length == 0); + if (data->len == 0) { + return AWS_MDD_OUT_OF_DATA; + } + + uint8_t byte = *data->ptr; + aws_byte_cursor_advance(data, 1); + aws_byte_buf_append_byte_dynamic(&decoder->packet_buffer, byte); + + struct aws_byte_cursor vli_cursor = aws_byte_cursor_from_buf(&decoder->packet_buffer); + aws_byte_cursor_advance(&vli_cursor, 1); + + size_t remaining_length = 0; + if (aws_mqtt311_decode_remaining_length(&vli_cursor, &remaining_length) == AWS_OP_ERR) { + /* anything other than a short buffer error (not enough data yet) is a terminal error */ + if (aws_last_error() == AWS_ERROR_SHORT_BUFFER) { + return AWS_MDD_CONTINUE; + } else { + return AWS_MDD_PROTOCOL_ERROR; + } + } + + /* + * If we successfully decoded a variable-length integer, we now know exactly how many bytes we need to receive in + * order to have the full packet. + */ + decoder->total_packet_length = remaining_length + decoder->packet_buffer.len; + AWS_FATAL_ASSERT(decoder->total_packet_length > 0); + decoder->state = AWS_MDST_READ_BODY; + + return AWS_MDD_CONTINUE; +} + +static enum aws_mqtt311_decoding_directive s_handle_decoder_read_body( + struct aws_mqtt311_decoder *decoder, + struct aws_byte_cursor *data) { + AWS_FATAL_ASSERT(decoder->total_packet_length > 0); + + size_t buffer_length = decoder->packet_buffer.len; + size_t amount_to_read = aws_min_size(decoder->total_packet_length - buffer_length, data->len); + + struct aws_byte_cursor copy_cursor = aws_byte_cursor_advance(data, amount_to_read); + aws_byte_buf_append_dynamic(&decoder->packet_buffer, ©_cursor); + + if (decoder->packet_buffer.len == decoder->total_packet_length) { + + /* We have the full packet in the scratch buffer, invoke the correct handler to decode and process it */ + struct aws_byte_cursor packet_data = aws_byte_cursor_from_buf(&decoder->packet_buffer); + enum aws_mqtt_packet_type packet_type = aws_mqtt_get_packet_type(packet_data.ptr); + if (s_aws_mqtt311_decoder_safe_packet_handle(decoder, packet_type, packet_data) == AWS_OP_ERR) { + return AWS_MDD_PROTOCOL_ERROR; + } + + s_aws_mqtt311_decoder_reset_for_new_packet(decoder); + return AWS_MDD_CONTINUE; + } + + return AWS_MDD_OUT_OF_DATA; +} + +int aws_mqtt311_decoder_on_bytes_received(struct aws_mqtt311_decoder *decoder, struct aws_byte_cursor data) { + struct aws_byte_cursor data_cursor = data; + + enum aws_mqtt311_decoding_directive decode_directive = AWS_MDD_CONTINUE; + while (decode_directive == AWS_MDD_CONTINUE) { + switch (decoder->state) { + case AWS_MDST_READ_FIRST_BYTE: + decode_directive = s_handle_decoder_read_first_byte(decoder, &data_cursor); + break; + + case AWS_MDST_READ_REMAINING_LENGTH: + decode_directive = s_handle_decoder_read_remaining_length(decoder, &data_cursor); + break; + + case AWS_MDST_READ_BODY: + decode_directive = s_handle_decoder_read_body(decoder, &data_cursor); + break; + + default: + decode_directive = AWS_MDD_PROTOCOL_ERROR; + break; + } + + /* + * Protocol error is a terminal failure state until aws_mqtt311_decoder_reset_for_new_connection() is called. + */ + if (decode_directive == AWS_MDD_PROTOCOL_ERROR) { + decoder->state = AWS_MDST_PROTOCOL_ERROR; + if (aws_last_error() == AWS_ERROR_SUCCESS) { + aws_raise_error(AWS_ERROR_MQTT_PROTOCOL_ERROR); + } + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +void aws_mqtt311_decoder_reset_for_new_connection(struct aws_mqtt311_decoder *decoder) { + s_aws_mqtt311_decoder_reset(decoder); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dc757dfb..42e74775 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,16 @@ add_test_case(mqtt_packet_pingreq) add_test_case(mqtt_packet_pingresp) add_test_case(mqtt_packet_disconnect) +add_test_case(mqtt_frame_and_decode_publish) +add_test_case(mqtt_frame_and_decode_suback) +add_test_case(mqtt_frame_and_decode_unsuback) +add_test_case(mqtt_frame_and_decode_puback) +add_test_case(mqtt_frame_and_decode_pingresp) +add_test_case(mqtt_frame_and_decode_connack) +add_test_case(mqtt_frame_and_decode_bad_remaining_length) +add_test_case(mqtt_frame_and_decode_unsupported_packet_type) +add_test_case(mqtt_frame_and_decode_bad_flags_for_packet_type) + add_test_case(mqtt_topic_tree_match) add_test_case(mqtt_topic_tree_unsubscribe) add_test_case(mqtt_topic_tree_duplicate_transactions) diff --git a/tests/v3/packet_framing_tests.c b/tests/v3/packet_framing_tests.c new file mode 100644 index 00000000..6101ecea --- /dev/null +++ b/tests/v3/packet_framing_tests.c @@ -0,0 +1,693 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include + +struct mqtt_311_decoding_test_context { + struct aws_allocator *allocator; + struct aws_mqtt311_decoder decoder; + + void *expected_packet; + size_t packet_count[16]; +}; + +static int s_compare_fixed_header( + struct aws_mqtt_fixed_header *expected_header, + struct aws_mqtt_fixed_header *actual_header) { + ASSERT_INT_EQUALS(expected_header->packet_type, actual_header->packet_type); + ASSERT_INT_EQUALS(expected_header->remaining_length, actual_header->remaining_length); + ASSERT_INT_EQUALS(expected_header->flags, actual_header->flags); + + return AWS_OP_SUCCESS; +} + +static int s_decoding_test_handle_publish(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_publish publish; + if (aws_mqtt_packet_publish_decode(&message_cursor, &publish)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_publish *expected_publish = context->expected_packet; + + ASSERT_SUCCESS(s_compare_fixed_header(&expected_publish->fixed_header, &publish.fixed_header)); + + ASSERT_INT_EQUALS(expected_publish->packet_identifier, publish.packet_identifier); + + ASSERT_BIN_ARRAYS_EQUALS( + expected_publish->topic_name.ptr, + expected_publish->topic_name.len, + publish.topic_name.ptr, + publish.topic_name.len); + ASSERT_BIN_ARRAYS_EQUALS( + expected_publish->payload.ptr, expected_publish->payload.len, publish.payload.ptr, publish.payload.len); + + ++context->packet_count[AWS_MQTT_PACKET_PUBLISH]; + + return AWS_OP_SUCCESS; +} + +static int s_decoding_test_handle_suback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_suback suback; + if (aws_mqtt_packet_suback_init(&suback, context->allocator, 0 /* fake packet_id */)) { + return AWS_OP_ERR; + } + + int result = AWS_OP_ERR; + if (aws_mqtt_packet_suback_decode(&message_cursor, &suback)) { + goto done; + } + + struct aws_mqtt_packet_suback *expected_suback = context->expected_packet; + + ASSERT_INT_EQUALS(expected_suback->packet_identifier, suback.packet_identifier); + + size_t expected_ack_count = aws_array_list_length(&expected_suback->return_codes); + size_t actual_ack_count = aws_array_list_length(&suback.return_codes); + ASSERT_INT_EQUALS(expected_ack_count, actual_ack_count); + + for (size_t i = 0; i < expected_ack_count; ++i) { + uint8_t expected_return_code = 0; + aws_array_list_get_at(&expected_suback->return_codes, &expected_return_code, i); + + uint8_t actual_return_code = 0; + aws_array_list_get_at(&suback.return_codes, &actual_return_code, i); + + ASSERT_INT_EQUALS(expected_return_code, actual_return_code); + } + + ++context->packet_count[AWS_MQTT_PACKET_SUBACK]; + + result = AWS_OP_SUCCESS; + +done: + + aws_mqtt_packet_suback_clean_up(&suback); + + return result; +} + +static int s_decoding_test_handle_unsuback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_ack unsuback; + if (aws_mqtt_packet_unsuback_init(&unsuback, 0 /* fake packet_id */)) { + return AWS_OP_ERR; + } + + if (aws_mqtt_packet_ack_decode(&message_cursor, &unsuback)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_ack *expected_unsuback = context->expected_packet; + + ASSERT_INT_EQUALS(expected_unsuback->packet_identifier, unsuback.packet_identifier); + + ++context->packet_count[AWS_MQTT_PACKET_UNSUBACK]; + + return AWS_OP_SUCCESS; +} + +static int s_decoding_test_handle_puback(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_ack puback; + if (aws_mqtt_packet_puback_init(&puback, 0 /* fake packet_id */)) { + return AWS_OP_ERR; + } + + if (aws_mqtt_packet_ack_decode(&message_cursor, &puback)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_ack *expected_puback = context->expected_packet; + + ASSERT_INT_EQUALS(expected_puback->packet_identifier, puback.packet_identifier); + + ++context->packet_count[AWS_MQTT_PACKET_PUBACK]; + + return AWS_OP_SUCCESS; +} + +static int s_decoding_test_handle_pingresp(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_connection pingresp; + if (aws_mqtt_packet_pingresp_init(&pingresp)) { + return AWS_OP_ERR; + } + + if (aws_mqtt_packet_connection_decode(&message_cursor, &pingresp)) { + return AWS_OP_ERR; + } + + ++context->packet_count[AWS_MQTT_PACKET_PINGRESP]; + + return AWS_OP_SUCCESS; +} + +static int s_decoding_test_handle_connack(struct aws_byte_cursor message_cursor, void *user_data) { + + struct mqtt_311_decoding_test_context *context = user_data; + (void)context; + + struct aws_mqtt_packet_connack connack; + if (aws_mqtt_packet_connack_init(&connack, false, 0)) { + return AWS_OP_ERR; + } + + if (aws_mqtt_packet_connack_decode(&message_cursor, &connack)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_packet_connack *expected_connack = context->expected_packet; + + ASSERT_INT_EQUALS(expected_connack->session_present, connack.session_present); + ASSERT_INT_EQUALS(expected_connack->connect_return_code, connack.connect_return_code); + + ++context->packet_count[AWS_MQTT_PACKET_CONNACK]; + + return AWS_OP_SUCCESS; +} + +static struct aws_mqtt_client_connection_packet_handlers s_decoding_test_packet_handlers = { + .handlers_by_packet_type = { + [AWS_MQTT_PACKET_PUBLISH] = &s_decoding_test_handle_publish, + [AWS_MQTT_PACKET_SUBACK] = &s_decoding_test_handle_suback, + [AWS_MQTT_PACKET_UNSUBACK] = &s_decoding_test_handle_unsuback, + [AWS_MQTT_PACKET_PUBACK] = &s_decoding_test_handle_puback, + [AWS_MQTT_PACKET_PINGRESP] = &s_decoding_test_handle_pingresp, + [AWS_MQTT_PACKET_CONNACK] = &s_decoding_test_handle_connack, + }}; + +static void s_init_decoding_test_context( + struct mqtt_311_decoding_test_context *context, + struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*context); + + context->allocator = allocator; + + struct aws_mqtt311_decoder_options config = { + .packet_handlers = &s_decoding_test_packet_handlers, + .handler_user_data = context, + }; + + aws_mqtt311_decoder_init(&context->decoder, allocator, &config); +} + +static void s_clean_up_decoding_test_context(struct mqtt_311_decoding_test_context *context) { + + aws_mqtt311_decoder_clean_up(&context->decoder); +} + +#define TEST_ADJACENT_PACKET_COUNT 4 + +static int s_mqtt_frame_and_decode_publish_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + /* + * For completeness, run the test with payload sizes that lead to a remaining length VLI encoding of 1, 2, 3, and + * 4 bytes. + */ + size_t payload_sizes[] = {35, 1234, 1 << 16, 1 << 21}; + + for (size_t i = 0; i < AWS_ARRAY_SIZE(payload_sizes); ++i) { + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + size_t publish_payload_size = payload_sizes[i]; + + /* Intentionally don't initialize so we have lots of garbage */ + uint8_t *raw_payload = aws_mem_acquire(allocator, publish_payload_size); + + struct aws_mqtt_packet_publish publish_packet; + ASSERT_SUCCESS(aws_mqtt_packet_publish_init( + &publish_packet, + true, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + aws_byte_cursor_from_c_str("Hello/World"), + 12, + aws_byte_cursor_from_array(raw_payload, publish_payload_size))); + + test_context.expected_packet = &publish_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, (publish_payload_size + 100) * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_publish_encode(&encoded_buffer, &publish_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23, 37, 67, 131}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_PUBLISH]); + + aws_byte_buf_clean_up(&encoded_buffer); + aws_mem_release(allocator, raw_payload); + + s_clean_up_decoding_test_context(&test_context); + } + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_publish, s_mqtt_frame_and_decode_publish_fn) + +static int s_mqtt_frame_and_decode_suback_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + struct aws_mqtt_packet_suback suback_packet; + ASSERT_SUCCESS(aws_mqtt_packet_suback_init(&suback_packet, allocator, 1234)); + + uint8_t sample_return_codes[] = {0x00, 0x01, 0x02, 0x80, 0x01}; + for (size_t i = 0; i < AWS_ARRAY_SIZE(sample_return_codes); ++i) { + aws_mqtt_packet_suback_add_return_code(&suback_packet, sample_return_codes[i]); + } + + test_context.expected_packet = &suback_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_suback_encode(&encoded_buffer, &suback_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23, 37, 67, 143}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_SUBACK]); + + aws_mqtt_packet_suback_clean_up(&suback_packet); + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_suback, s_mqtt_frame_and_decode_suback_fn) + +static int s_mqtt_frame_and_decode_unsuback_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + struct aws_mqtt_packet_ack unsuback_packet; + ASSERT_SUCCESS(aws_mqtt_packet_unsuback_init(&unsuback_packet, 1234)); + + test_context.expected_packet = &unsuback_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_ack_encode(&encoded_buffer, &unsuback_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_UNSUBACK]); + + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_unsuback, s_mqtt_frame_and_decode_unsuback_fn) + +static int s_mqtt_frame_and_decode_puback_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + struct aws_mqtt_packet_ack puback_packet; + ASSERT_SUCCESS(aws_mqtt_packet_puback_init(&puback_packet, 1234)); + + test_context.expected_packet = &puback_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_ack_encode(&encoded_buffer, &puback_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_PUBACK]); + + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_puback, s_mqtt_frame_and_decode_puback_fn) + +static int s_mqtt_frame_and_decode_pingresp_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + struct aws_mqtt_packet_connection pingresp_packet; + ASSERT_SUCCESS(aws_mqtt_packet_pingresp_init(&pingresp_packet)); + + test_context.expected_packet = &pingresp_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_connection_encode(&encoded_buffer, &pingresp_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_PINGRESP]); + + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_pingresp, s_mqtt_frame_and_decode_pingresp_fn) + +static int s_mqtt_frame_and_decode_connack_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + struct aws_mqtt_packet_connack connack_packet; + ASSERT_SUCCESS(aws_mqtt_packet_connack_init(&connack_packet, true, AWS_MQTT_CONNECT_NOT_AUTHORIZED)); + + test_context.expected_packet = &connack_packet; + + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + for (size_t j = 0; j < TEST_ADJACENT_PACKET_COUNT; ++j) { + ASSERT_SUCCESS(aws_mqtt_packet_connack_encode(&encoded_buffer, &connack_packet)); + } + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + + ASSERT_INT_EQUALS(4 * AWS_ARRAY_SIZE(fragment_lengths), test_context.packet_count[AWS_MQTT_PACKET_CONNACK]); + + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_connack, s_mqtt_frame_and_decode_connack_fn) + +static int s_mqtt_frame_and_decode_bad_remaining_length_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + /* QoS 0 Publish "Packet" data where the remaining length vli-encoding is illegal */ + uint8_t bad_packet_data[] = {0x30, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00}; + + size_t fragment_lengths[] = {1, 2, 3, 5, 13}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + aws_mqtt311_decoder_reset_for_new_connection(decoder); + + size_t fragment_length = fragment_lengths[j]; + struct aws_byte_cursor packet_cursor = + aws_byte_cursor_from_array(bad_packet_data, AWS_ARRAY_SIZE(bad_packet_data)); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + /* If this or a previous call contains the final 0x80 of the invalid vli encoding, then decode must fail */ + bool should_fail = (fragment_cursor.ptr + fragment_cursor.len) - bad_packet_data > 4; + if (should_fail) { + ASSERT_FAILS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } else { + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + } + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_bad_remaining_length, s_mqtt_frame_and_decode_bad_remaining_length_fn) + +static int s_mqtt_frame_and_decode_unsupported_packet_type_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + /* Pingreq packet, no handler installed */ + uint8_t pingreq_packet_data[] = {192, 0}; + + size_t fragment_lengths[] = {1, 2}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + aws_mqtt311_decoder_reset_for_new_connection(decoder); + + size_t fragment_length = fragment_lengths[j]; + struct aws_byte_cursor packet_cursor = + aws_byte_cursor_from_array(pingreq_packet_data, AWS_ARRAY_SIZE(pingreq_packet_data)); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + /* If this is the final call, it should fail-but-not-crash as there's no handler */ + bool should_fail = packet_cursor.len == 0; + if (should_fail) { + ASSERT_FAILS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } else { + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + } + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_unsupported_packet_type, s_mqtt_frame_and_decode_unsupported_packet_type_fn) + +static int s_mqtt_frame_and_decode_bad_flags_for_packet_type_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_311_decoding_test_context test_context; + s_init_decoding_test_context(&test_context, allocator); + + struct aws_mqtt311_decoder *decoder = &test_context.decoder; + + /* start with a valid suback */ + struct aws_mqtt_packet_suback suback_packet; + ASSERT_SUCCESS(aws_mqtt_packet_suback_init(&suback_packet, allocator, 1234)); + + uint8_t sample_return_codes[] = {0x00, 0x01, 0x02, 0x80, 0x01}; + for (size_t i = 0; i < AWS_ARRAY_SIZE(sample_return_codes); ++i) { + aws_mqtt_packet_suback_add_return_code(&suback_packet, sample_return_codes[i]); + } + + /* encode it */ + struct aws_byte_buf encoded_buffer; + aws_byte_buf_init(&encoded_buffer, allocator, 100 * TEST_ADJACENT_PACKET_COUNT); + + ASSERT_SUCCESS(aws_mqtt_packet_suback_encode(&encoded_buffer, &suback_packet)); + + /* suback flags should be zero; mess that up */ + encoded_buffer.buffer[0] |= 0x05; + + size_t fragment_lengths[] = {1, 2, 3, 5, 7, 11, 23}; + + for (size_t j = 0; j < AWS_ARRAY_SIZE(fragment_lengths); ++j) { + size_t fragment_length = fragment_lengths[j]; + + aws_mqtt311_decoder_reset_for_new_connection(decoder); + + struct aws_byte_cursor packet_cursor = aws_byte_cursor_from_buf(&encoded_buffer); + while (packet_cursor.len > 0) { + size_t advance = aws_min_size(packet_cursor.len, fragment_length); + struct aws_byte_cursor fragment_cursor = aws_byte_cursor_advance(&packet_cursor, advance); + + /* If this is the final call, it should fail-but-not-crash as the full decode should fail */ + bool should_fail = packet_cursor.len == 0; + if (should_fail) { + ASSERT_FAILS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } else { + ASSERT_SUCCESS(aws_mqtt311_decoder_on_bytes_received(decoder, fragment_cursor)); + } + } + } + + aws_mqtt_packet_suback_clean_up(&suback_packet); + aws_byte_buf_clean_up(&encoded_buffer); + + s_clean_up_decoding_test_context(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_frame_and_decode_bad_flags_for_packet_type, s_mqtt_frame_and_decode_bad_flags_for_packet_type_fn) From f09d932f9f53a29a0eb77c33a883f33894c5abb0 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 26 Jul 2023 13:51:22 -0700 Subject: [PATCH 80/98] Adapter log subject and large-scale name change (#312) * Publish, subscribe, unsubscribe support * Adapter resubscribe and get_stats * Connection result callbacks, augment existing tests, add reconnect failure test * Best effort translation from mqtt5 to mqtt3 error codes * Additional logging, missing error codes * Adapter log subject and large-scale name change --------- Co-authored-by: Bret Ambrose --- include/aws/mqtt/mqtt.h | 2 + include/aws/mqtt/private/client_impl_shared.h | 6 + .../aws/mqtt/private/mqtt_subscription_set.h | 227 + .../aws/mqtt/private/v5/mqtt5_client_impl.h | 14 + .../private/v5/mqtt5_to_mqtt3_adapter_impl.h | 358 ++ source/client.c | 16 +- source/client_impl_shared.c | 15 + source/mqtt.c | 4 + source/mqtt3_to_mqtt5_adapter.c | 1670 ------- source/mqtt_subscription_set.c | 431 ++ source/v5/mqtt5_client.c | 58 +- source/v5/mqtt5_to_mqtt3_adapter.c | 3030 ++++++++++++ source/v5/mqtt5_topic_alias.c | 10 +- tests/CMakeLists.txt | 77 +- tests/v5/mqtt3_to_mqtt5_adapter_tests.c | 1476 ------ tests/v5/mqtt5_client_tests.c | 41 +- tests/v5/mqtt5_testing_utils.h | 17 +- tests/v5/mqtt5_to_mqtt3_adapter_tests.c | 4041 +++++++++++++++++ tests/v5/mqtt_subscription_set_tests.c | 832 ++++ 19 files changed, 9089 insertions(+), 3236 deletions(-) create mode 100644 include/aws/mqtt/private/mqtt_subscription_set.h create mode 100644 include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h delete mode 100644 source/mqtt3_to_mqtt5_adapter.c create mode 100644 source/mqtt_subscription_set.c create mode 100644 source/v5/mqtt5_to_mqtt3_adapter.c delete mode 100644 tests/v5/mqtt3_to_mqtt5_adapter_tests.c create mode 100644 tests/v5/mqtt5_to_mqtt3_adapter_tests.c create mode 100644 tests/v5/mqtt_subscription_set_tests.c diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 76d38e6a..7385b772 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -79,6 +79,7 @@ enum aws_mqtt_error { AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS, AWS_ERROR_MQTT5_INVALID_UTF8_STRING, AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT, + AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; @@ -90,6 +91,7 @@ enum aws_mqtt_log_subject { AWS_LS_MQTT5_GENERAL, AWS_LS_MQTT5_CLIENT, AWS_LS_MQTT5_CANARY, + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, }; /** Function called on cleanup of a userdata. */ diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index 10ac73dc..8c343436 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -109,4 +109,10 @@ struct aws_mqtt_client_connection { void *impl; }; +AWS_MQTT_API uint64_t aws_mqtt_hash_uint16_t(const void *item); + +AWS_MQTT_API bool aws_mqtt_compare_uint16_t_eq(const void *a, const void *b); + +AWS_MQTT_API bool aws_mqtt_byte_cursor_hash_equality(const void *a, const void *b); + #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H */ diff --git a/include/aws/mqtt/private/mqtt_subscription_set.h b/include/aws/mqtt/private/mqtt_subscription_set.h new file mode 100644 index 00000000..2ccb5d7e --- /dev/null +++ b/include/aws/mqtt/private/mqtt_subscription_set.h @@ -0,0 +1,227 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT3_TO_MQTT5_ADAPTER_SUBSCRIPTION_SET_H +#define AWS_MQTT_MQTT3_TO_MQTT5_ADAPTER_SUBSCRIPTION_SET_H + +#include "aws/mqtt/mqtt.h" + +#include "aws/mqtt/client.h" +#include "aws/mqtt/v5/mqtt5_types.h" +#include + +/** + * (Transient) configuration options about a single persistent MQTT topic filter subscription + */ +struct aws_mqtt_subscription_set_subscription_options { + struct aws_byte_cursor topic_filter; + + enum aws_mqtt5_qos qos; + + bool no_local; + bool retain_as_published; + enum aws_mqtt5_retain_handling_type retain_handling_type; + + /* Callback invoked when this subscription matches an incoming publish */ + aws_mqtt_client_publish_received_fn *on_publish_received; + + /* Callback invoked when this subscription is removed from the set */ + aws_mqtt_userdata_cleanup_fn *on_cleanup; + + void *callback_user_data; +}; + +/** + * Persistent structure to track a single MQTT topic filter subscription + */ +struct aws_mqtt_subscription_set_subscription_record { + struct aws_allocator *allocator; + struct aws_byte_buf topic_filter; + + struct aws_mqtt_subscription_set_subscription_options subscription_view; +}; + +/** + * (Transient) configuration options about an incoming publish message + */ +struct aws_mqtt_subscription_set_publish_received_options { + struct aws_mqtt_client_connection *connection; + + struct aws_byte_cursor topic; + enum aws_mqtt_qos qos; + bool retain; + bool dup; + + struct aws_byte_cursor payload; +}; + +/** + * A node in the topic trie maintained by the subscription set. Each node represents a single "path segment" in a + * topic filter "path." Segments can be empty. + * + * Some examples (topic filter -> path segments): + * + * "hello/world" -> [ "hello", "world" ] + * "a/b/" -> [ "a", "b", "" ] + * "/b/" -> [ "", "b", "" ] + * "a/#/c" -> [ "a", "#", "c" ] + * + * On incoming publish, we walk the tree invoking callbacks based on topic vs. topic filter matching, segment by + * segment. + * + */ +struct aws_mqtt_subscription_set_topic_tree_node { + struct aws_allocator *allocator; + + struct aws_byte_cursor topic_segment_cursor; /* segment can be empty */ + struct aws_byte_buf topic_segment; + + struct aws_mqtt_subscription_set_topic_tree_node *parent; + struct aws_hash_table children; /* (embedded topic_segment -> containing node) */ + + /* + * A node starts with a ref count of one and is incremented every time a new, overlapping path is added + * to the subscription set. When the ref count goes to zero, that means there are not subscriptions using the + * segment (or path suffix) represented by this node and therefor it can be deleted without any additional + * analysis. + * + * Replacing an existing path does not change the ref count. + */ + size_t ref_count; + + bool is_subscription; + + aws_mqtt_client_publish_received_fn *on_publish_received; + aws_mqtt_userdata_cleanup_fn *on_cleanup; + + void *callback_user_data; +}; + +/** + * Utility type to track a client's subscriptions. + * + * The topic tree supports per-subscription callbacks as used by the MQTT311 implementation. + * + * The subscriptions table supports resubscribe APIs for both MQTT311 and MQTT5 by tracking the subscription + * details on a per-topic-filter basis. + */ +struct aws_mqtt_subscription_set { + struct aws_allocator *allocator; + + /* a permanent ref */ + struct aws_mqtt_subscription_set_topic_tree_node *root; + + /* embedded topic_filter_cursor -> persistent subscription */ + struct aws_hash_table subscriptions; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Creates a new subscription set + * + * @param allocator allocator to use + * @return a new subscription set or NULL + */ +AWS_MQTT_API struct aws_mqtt_subscription_set *aws_mqtt_subscription_set_new(struct aws_allocator *allocator); + +/** + * Destroys a subscription set + * + * @param subscription_set subscription set to destroy + */ +AWS_MQTT_API void aws_mqtt_subscription_set_destroy(struct aws_mqtt_subscription_set *subscription_set); + +/** + * Checks if a topic filter exists in the subscription set's hash table of subscriptions + * + * @param subscription_set subscription set to check + * @param topic_filter topic filter to check for existence in the set + * @return true if the topic filter exists in the table of subscriptions, false otherwise + */ +AWS_MQTT_API bool aws_mqtt_subscription_set_is_subscribed( + const struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter); + +/** + * Checks if a topic filter exists as a subscription (has a publish received handler) in the set's topic tree + * + * @param subscription_set subscription set to check + * @param topic_filter topic filter to check for existence in the set's topic tree + * @return true if the set's topic tree contains a publish received callback for the topic filter, false otherwise + */ +AWS_MQTT_API bool aws_mqtt_subscription_set_is_in_topic_tree( + const struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter); + +/** + * Adds a subscription to the subscription set. If a subscription already exists with a matching topic filter, it + * will be overwritten. + * + * @param subscription_set subscription set to add a subscription to + * @param subscription_options options for the new subscription + */ +AWS_MQTT_API void aws_mqtt_subscription_set_add_subscription( + struct aws_mqtt_subscription_set *subscription_set, + const struct aws_mqtt_subscription_set_subscription_options *subscription_options); + +/** + * Removes a subscription from the subscription set + * + * @param subscription_set subscription set to remove a subscription from + * @param topic_filter topic filter to remove subscription information for + */ +AWS_MQTT_API void aws_mqtt_subscription_set_remove_subscription( + struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter); + +/** + * Given a publish message, invokes all publish received handlers for matching subscriptions in the subscription set + * + * @param subscription_set subscription set to invoke publish received callbacks for + * @param publish_options received publish message properties + */ +AWS_MQTT_API void aws_mqtt_subscription_set_on_publish_received( + const struct aws_mqtt_subscription_set *subscription_set, + const struct aws_mqtt_subscription_set_publish_received_options *publish_options); + +/** + * Queries the properties of all subscriptions tracked by this subscription set. Used to implement re-subscribe + * behavior. + * + * @param subscription_set subscription set to query the subscriptions on + * @param subscriptions uninitialized array list to hold the subscriptions. + * + * The caller must invoke the cleanup function for array lists on the result. The list elements are of type + * 'struct aws_mqtt_subscription_set_subscription_options' and the topic filter cursor points to the subscription set's + * internal record. This means that the result must be used and cleaned up in local scope. + */ +AWS_MQTT_API void aws_mqtt_subscription_set_get_subscriptions( + struct aws_mqtt_subscription_set *subscription_set, + struct aws_array_list *subscriptions); + +/** + * Creates a new subscription record. A subscription record tracks all information about a single MQTT topic filter + * subscription + * + * @param allocator memory allocator to use + * @param subscription all relevant information about the subscription + * @return a new persistent subscription record + */ +AWS_MQTT_API struct aws_mqtt_subscription_set_subscription_record *aws_mqtt_subscription_set_subscription_record_new( + struct aws_allocator *allocator, + const struct aws_mqtt_subscription_set_subscription_options *subscription); + +/** + * Destroys a subscription record + * + * @param record subscription record to destroy + */ +AWS_MQTT_API void aws_mqtt_subscription_set_subscription_record_destroy( + struct aws_mqtt_subscription_set_subscription_record *record); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT3_TO_MQTT5_ADAPTER_SUBSCRIPTION_SET_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index 19b5e875..e251aee2 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -686,6 +686,20 @@ AWS_MQTT_API void aws_mqtt5_client_change_desired_state( enum aws_mqtt5_client_state desired_state, struct aws_mqtt5_operation_disconnect *disconnect_op); +/** + * Event-loop-internal API to add an operation to the client's queue. Used by the 3-to-5 adapter to synchnrously + * inject the MQTT5 operation once the adapter operation has reached the event loop. + * + * @param client MQTT5 client to submit an operation to + * @param operation MQTT5 operation to submit + * @param is_terminated flag that indicates whether the submitter is shutting down or not. Needed to differentiate + * between adapter submissions and MQTT5 client API submissions and correctly handle ref count adjustments. + */ +AWS_MQTT_API void aws_mqtt5_client_submit_operation_internal( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + bool is_terminated); + AWS_EXTERN_C_END #endif /* AWS_MQTT_MQTT5_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h new file mode 100644 index 00000000..311c0b20 --- /dev/null +++ b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h @@ -0,0 +1,358 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT5_TO_MQTT3_ADAPTER_IMPL_H +#define AWS_MQTT_MQTT5_TO_MQTT3_ADAPTER_IMPL_H + +#include + +#include +#include +#include +#include +#include +#include + +struct aws_mqtt_subscription_set; + +struct aws_mqtt5_to_mqtt3_adapter_publish_options { + struct aws_mqtt_client_connection_5_impl *adapter; + + const struct aws_byte_cursor topic; + enum aws_mqtt_qos qos; + bool retain; + const struct aws_byte_cursor payload; + + aws_mqtt_op_complete_fn *on_complete; + void *on_complete_userdata; +}; + +/* + * A subscribe with no subscriptions represents a re-subscribe of all internally tracked topics. While this + * is a bit hacky, the alternative is to copy-and-paste almost the entire multi-subscribe adapter operation and + * supporting logic, which is approximately 300 lines. + */ +struct aws_mqtt5_to_mqtt3_adapter_subscribe_options { + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_mqtt_topic_subscription *subscriptions; + size_t subscription_count; + + aws_mqtt_suback_fn *on_suback; + void *on_suback_user_data; + + aws_mqtt_suback_multi_fn *on_multi_suback; + void *on_multi_suback_user_data; +}; + +struct aws_mqtt5_to_mqtt3_adapter_unsubscribe_options { + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_byte_cursor topic_filter; + + aws_mqtt_op_complete_fn *on_unsuback; + void *on_unsuback_user_data; +}; + +enum aws_mqtt5_to_mqtt3_adapter_operation_type { + AWS_MQTT5TO3_AOT_PUBLISH, + AWS_MQTT5TO3_AOT_SUBSCRIBE, + AWS_MQTT5TO3_AOT_UNSUBSCRIBE, +}; + +struct aws_mqtt5_to_mqtt3_adapter_operation_base { + struct aws_allocator *allocator; + struct aws_ref_count ref_count; + void *impl; + + /* + * Holds an internal reference to the adapter while traveling to the event loop. Reference gets released + * after intake on the event loop. + * + * We avoid calling back into a deleted adapter by zeroing out the + * mqtt5 operation callbacks for everything we've submitted before final mqtt5 client release. + */ + struct aws_mqtt_client_connection_5_impl *adapter; + bool holding_adapter_ref; + + struct aws_task submission_task; + + enum aws_mqtt5_to_mqtt3_adapter_operation_type type; + uint16_t id; +}; + +struct aws_mqtt5_to_mqtt3_adapter_operation_publish { + struct aws_mqtt5_to_mqtt3_adapter_operation_base base; + + /* + * holds a reference to the MQTT5 client publish operation until the operation completes or our adapter + * goes away. + * + * In the case where we're going away, we zero out the MQTT5 operation callbacks to prevent crash-triggering + * notifications. + */ + struct aws_mqtt5_operation_publish *publish_op; + + aws_mqtt_op_complete_fn *on_publish_complete; + void *on_publish_complete_user_data; +}; + +struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe { + struct aws_mqtt5_to_mqtt3_adapter_operation_base base; + + /* + * holds a reference to the MQTT5 client subscribe operation until the operation completes or our adapter + * goes away. + * + * In the case where we're going away, we zero out the MQTT5 operation callbacks to prevent crash-triggering + * notifications. + */ + struct aws_mqtt5_operation_subscribe *subscribe_op; + + /* aws_array_list */ + struct aws_array_list subscriptions; + + aws_mqtt_suback_fn *on_suback; + void *on_suback_user_data; + + aws_mqtt_suback_multi_fn *on_multi_suback; + void *on_multi_suback_user_data; +}; + +struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe { + struct aws_mqtt5_to_mqtt3_adapter_operation_base base; + + /* + * holds a reference to the MQTT5 client unsubscribe operation until the operation completes or our adapter + * goes away. + * + * In the case where we're going away, we zero out the MQTT5 operation callbacks to prevent crash-triggering + * notifications. + */ + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op; + + struct aws_byte_buf topic_filter; + + aws_mqtt_op_complete_fn *on_unsuback; + void *on_unsuback_user_data; +}; + +/* + + Sequencing (PUBLISH example): + + Mqtt311 public API call + Create cross thread task + Create adapter op -> Create and attach mqtt5 op + allocate id and add operation to adapter table + Add adapter op's internal ref to adapter + submit cross thread task to event loop + return id or 0 + + Adapter Op reaches event loop task function: (from this point, all callbacks must be safe-guarded) + terminated = true + Safe handler: + If adapter not terminated: + terminated = false + Synchronously enqueue operation to mqtt5 client + if terminated: + remove adapter op from table + destroy adapter op + Release adapter op's internal ref to adapter + + On publish completion: + Safe handler: + If not terminated: + invoke mqtt311 callback + Remove adapter op from table + Destroy adapter op + + On final destroy (zero internal refs): + Iterate all incomplete adapter operations and cancel them: zero callbacks and remove from queue if in queue and + unbound + Destroy all adapter ops + Clear table +*/ + +struct aws_mqtt5_to_mqtt3_adapter_operation_table { + struct aws_mutex lock; + + struct aws_hash_table operations; + uint16_t next_id; +}; + +/* + * The adapter maintains a notion of state based on how its 311 API has been used. This state guides how it handles + * external lifecycle events. + * + * Operational (sourced from the adapter) events are always relayed unless the adapter has been terminated. + */ +enum aws_mqtt_adapter_state { + + /* + * The 311 API has had connect() called but that connect has not yet resolved. + * + * If it resolves successfully we will move to the STAY_CONNECTED state which will relay lifecycle callbacks + * transparently. + * + * If it resolves unsuccessfully, we will move to the STAY_DISCONNECTED state where we will ignore lifecycle + * events because, from the 311 API's perspective, nothing should be getting emitted. + */ + AWS_MQTT_AS_FIRST_CONNECT, + + /* + * A call to the 311 connect API has resolved successfully. Relay all lifecycle events until told otherwise. + */ + AWS_MQTT_AS_STAY_CONNECTED, + + /* + * We have not observed a successful initial connection attempt via the 311 API (or disconnect has been + * invoked afterwards). Ignore all lifecycle events. + */ + AWS_MQTT_AS_STAY_DISCONNECTED, +}; + +struct aws_mqtt_client_connection_5_impl { + + struct aws_allocator *allocator; + + struct aws_mqtt_client_connection base; + + struct aws_mqtt5_client *client; + struct aws_mqtt5_listener *listener; + struct aws_event_loop *loop; + + /* + * An event-loop-internal flag that we can read to check to see if we're in the scope of a callback + * that has already locked the adapter's lock. Can only be referenced from the event loop thread. + * + * We use the flag to avoid deadlock in a few cases where we can re-enter the adapter logic from within a callback. + * It also provides a nice solution for the fact that we cannot safely upgrade a read lock to a write lock. + */ + bool in_synchronous_callback; + + /* + * The current adapter state based on the sequence of connect(), disconnect(), and connection completion events. + * This affects how the adapter reacts to incoming mqtt5 events. Under certain conditions, we may change + * this state value based on unexpected events (stopping the mqtt5 client underneath the adapter, for example) + */ + enum aws_mqtt_adapter_state adapter_state; + + /* + * Tracks all references from external sources (ie users). Incremented and decremented by the public + * acquire/release APIs of the 311 connection. + * + * When this value drops to zero, the terminated flag is set and no further callbacks will be invoked. This + * also starts the asynchronous destruction process for the adapter. + */ + struct aws_ref_count external_refs; + + /* + * Tracks all references to the adapter from internal sources (temporary async processes that need the + * adapter to stay alive for an interval of time, like sending tasks across thread boundaries). + * + * Starts with a single reference that is held until the adapter's listener has fully detached from the mqtt5 + * client. + * + * Once the internal ref count drops to zero, the adapter may be destroyed synchronously. + */ + struct aws_ref_count internal_refs; + + /* + * We use the adapter lock to guarantee that we can synchronously sever all callbacks from the mqtt5 client even + * though adapter shutdown is an asynchronous process. This means the lock is held during callbacks which is a + * departure from our normal usage patterns. We prevent deadlock (due to logical re-entry) by using the + * in_synchronous_callback flag. + * + * We hold a read lock when invoking callbacks and a write lock when setting terminated from false to true. + */ + struct aws_rw_lock lock; + + /* + * Synchronized data protected by the adapter lock. + */ + struct { + bool terminated; + } synced_data; + + struct aws_mqtt5_to_mqtt3_adapter_operation_table operational_state; + + struct aws_mqtt_subscription_set *subscriptions; + + /* All fields after here are internal to the adapter event loop thread */ + + /* 311 interface callbacks */ + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; + void *on_interrupted_user_data; + + aws_mqtt_client_on_connection_resumed_fn *on_resumed; + void *on_resumed_user_data; + + aws_mqtt_client_on_connection_closed_fn *on_closed; + void *on_closed_user_data; + + aws_mqtt_client_on_connection_success_fn *on_connection_success; + void *on_connection_success_user_data; + + aws_mqtt_client_on_connection_failure_fn *on_connection_failure; + void *on_connection_failure_user_data; + + aws_mqtt_client_publish_received_fn *on_any_publish; + void *on_any_publish_user_data; + + aws_mqtt_transform_websocket_handshake_fn *websocket_handshake_transformer; + void *websocket_handshake_transformer_user_data; + + aws_mqtt5_transform_websocket_handshake_complete_fn *mqtt5_websocket_handshake_completion_function; + void *mqtt5_websocket_handshake_completion_user_data; + + /* (mutually exclusive) 311 interface one-time transient callbacks */ + aws_mqtt_client_on_disconnect_fn *on_disconnect; + void *on_disconnect_user_data; + + aws_mqtt_client_on_connection_complete_fn *on_connection_complete; + void *on_connection_complete_user_data; +}; + +AWS_EXTERN_C_BEGIN + +AWS_MQTT_API void aws_mqtt5_to_mqtt3_adapter_operation_table_init( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + struct aws_allocator *allocator); + +AWS_MQTT_API void aws_mqtt5_to_mqtt3_adapter_operation_table_clean_up( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table); + +AWS_MQTT_API int aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation); + +AWS_MQTT_API void aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + uint16_t operation_id); + +AWS_MQTT_API struct aws_mqtt5_to_mqtt3_adapter_operation_publish *aws_mqtt5_to_mqtt3_adapter_operation_new_publish( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_publish_options *options); + +AWS_MQTT_API struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_subscribe_options *options, + struct aws_mqtt_client_connection_5_impl *adapter); + +AWS_MQTT_API struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe * + aws_mqtt5_to_mqtt3_adapter_operation_new_unsubscribe( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_unsubscribe_options *options); + +AWS_MQTT_API struct aws_mqtt5_to_mqtt3_adapter_operation_base *aws_mqtt5_to_mqtt3_adapter_operation_release( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation); + +AWS_MQTT_API struct aws_mqtt5_to_mqtt3_adapter_operation_base *aws_mqtt5_to_mqtt3_adapter_operation_acquire( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_MQTT5_TO_MQTT3_ADAPTER_IMPL_H */ diff --git a/source/client.c b/source/client.c index dd1acb80..8968a8cc 100644 --- a/source/client.c +++ b/source/client.c @@ -36,6 +36,8 @@ static const uint64_t s_default_ping_timeout_ns = 3000000000; /* 20 minutes - This is the default (and max) for AWS IoT as of 2020.02.18 */ static const uint16_t s_default_keep_alive_sec = 1200; +#define DEFAULT_MQTT311_OPERATION_TABLE_SIZE 100 + static int s_mqtt_client_connect( struct aws_mqtt_client_connection_311_impl *connection, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, @@ -778,14 +780,6 @@ void aws_create_reconnect_task(struct aws_mqtt_client_connection_311_impl *conne } } -static uint64_t s_hash_uint16_t(const void *item) { - return *(uint16_t *)item; -} - -static bool s_uint16_t_eq(const void *a, const void *b) { - return *(uint16_t *)a == *(uint16_t *)b; -} - static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connection *base_connection) { struct aws_mqtt_client_connection_311_impl *connection = base_connection->impl; @@ -3253,9 +3247,9 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt if (aws_hash_table_init( &connection->synced_data.outstanding_requests_table, connection->allocator, - sizeof(struct aws_mqtt_request *), - s_hash_uint16_t, - s_uint16_t_eq, + DEFAULT_MQTT311_OPERATION_TABLE_SIZE, + aws_mqtt_hash_uint16_t, + aws_mqtt_compare_uint16_t_eq, NULL, NULL)) { diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index c7d8b483..f18e77ee 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -194,3 +194,18 @@ int aws_mqtt_client_connection_get_stats( return (*connection->vtable->get_stats_fn)(connection->impl, stats); } + +uint64_t aws_mqtt_hash_uint16_t(const void *item) { + return *(uint16_t *)item; +} + +bool aws_mqtt_compare_uint16_t_eq(const void *a, const void *b) { + return *(uint16_t *)a == *(uint16_t *)b; +} + +bool aws_mqtt_byte_cursor_hash_equality(const void *a, const void *b) { + const struct aws_byte_cursor *a_cursor = a; + const struct aws_byte_cursor *b_cursor = b; + + return aws_byte_cursor_eq(a_cursor, b_cursor); +} diff --git a/source/mqtt.c b/source/mqtt.c index bd5f5824..95a1a088 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -220,6 +220,9 @@ bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter) AWS_DEFINE_ERROR_INFO_MQTT( AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT, "Mqtt5 connection was reset by the Mqtt3 adapter in order to guarantee correct connection configuration"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, + "Resubscribe was called when there were no subscriptions"), }; /* clang-format on */ #undef AWS_DEFINE_ERROR_INFO_MQTT @@ -237,6 +240,7 @@ static struct aws_error_info_list s_error_list = { DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_GENERAL, "mqtt5-general", "Misc MQTT5 logging"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CLIENT, "mqtt5-client", "MQTT5 client and connections"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CANARY, "mqtt5-canary", "MQTT5 canary logging"), + DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "mqtt5-to-mqtt3-adapter", "MQTT5-To-MQTT3 adapter logging"), }; /* clang-format on */ diff --git a/source/mqtt3_to_mqtt5_adapter.c b/source/mqtt3_to_mqtt5_adapter.c deleted file mode 100644 index ef353525..00000000 --- a/source/mqtt3_to_mqtt5_adapter.c +++ /dev/null @@ -1,1670 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -#include - -#include -#include - -#include -#include -#include - -/* - * The adapter maintains a notion of state based on how its 311 API has been used. This state guides how it handles - * external lifecycle events. - * - * Operational events are always relayed unless the adapter has been terminated. - */ -enum aws_mqtt_adapter_state { - - /* - * The 311 API has had connect() called but that connect has not yet resolved. - * - * If it resolves successfully we will move to the STAY_CONNECTED state which will relay lifecycle callbacks - * transparently. - * - * If it resolves unsuccessfully, we will move to the STAY_DISCONNECTED state where we will ignore lifecycle - * events because, from the 311 API's perspective, nothing should be getting emitted. - */ - AWS_MQTT_AS_FIRST_CONNECT, - - /* - * A call to the 311 connect API has resolved successfully. Relay all lifecycle events until told otherwise. - */ - AWS_MQTT_AS_STAY_CONNECTED, - - /* - * We have not observed a successful initial connection attempt via the 311 API (or disconnect has been - * invoked afterwards). Ignore all lifecycle events. - */ - AWS_MQTT_AS_STAY_DISCONNECTED, -}; - -struct aws_mqtt_client_connection_5_impl { - - struct aws_allocator *allocator; - - struct aws_mqtt_client_connection base; - - struct aws_mqtt5_client *client; - struct aws_mqtt5_listener *listener; - struct aws_event_loop *loop; - - /* - * An event-loop-internal flag that we can read to check to see if we're in the scope of a callback - * that has already locked the adapter's lock. Can only be referenced from the event loop thread. - * - * We use the flag to avoid deadlock in a few cases where we can re-enter the adapter logic from within a callback. - * It also provides a nice solution for the fact that we cannot safely upgrade a read lock to a write lock. - */ - bool in_synchronous_callback; - - /* - * The current adapter state based on the sequence of connect(), disconnect(), and connection completion events. - * This affects how the adapter reacts to incoming mqtt5 events. Under certain conditions, we may change - * this state value based on unexpected events (stopping the mqtt5 client underneath the adapter, for example) - */ - enum aws_mqtt_adapter_state adapter_state; - - /* - * Tracks all references from external sources (ie users). Incremented and decremented by the public - * acquire/release APIs of the 311 connection. - * - * When this value drops to zero, the terminated flag is set and no further callbacks will be invoked. This - * also starts the asynchronous destruction process for the adapter. - */ - struct aws_ref_count external_refs; - - /* - * Tracks all references to the adapter from internal sources (temporary async processes that need the - * adapter to stay alive for an interval of time, like sending tasks across thread boundaries). - * - * Starts with a single reference that is held until the adapter's listener has fully detached from the mqtt5 - * client. - * - * Once the internal ref count drops to zero, the adapter may be destroyed synchronously. - */ - struct aws_ref_count internal_refs; - - /* - * We use the adapter lock to guarantee that we can synchronously sever all callbacks from the mqtt5 client even - * though adapter shutdown is an asynchronous process. This means the lock is held during callbacks which is a - * departure from our normal usage patterns. We prevent deadlock (due to logical re-entry) by using the - * in_synchronous_callback flag. - * - * We hold a read lock when invoking callbacks and a write lock when setting terminated from false to true. - */ - struct aws_rw_lock lock; - - /* - * Synchronized data protected by the adapter lock. - */ - struct { - bool terminated; - } synced_data; - - /* All fields after here are internal to the adapter event loop thread */ - - /* 311 interface callbacks */ - aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; - void *on_interrupted_user_data; - - aws_mqtt_client_on_connection_resumed_fn *on_resumed; - void *on_resumed_user_data; - - aws_mqtt_client_on_connection_closed_fn *on_closed; - void *on_closed_user_data; - - aws_mqtt_client_publish_received_fn *on_any_publish; - void *on_any_publish_user_data; - - aws_mqtt_transform_websocket_handshake_fn *websocket_handshake_transformer; - void *websocket_handshake_transformer_user_data; - - aws_mqtt5_transform_websocket_handshake_complete_fn *mqtt5_websocket_handshake_completion_function; - void *mqtt5_websocket_handshake_completion_user_data; - - /* (mutually exclusive) 311 interface one-time transient callbacks */ - aws_mqtt_client_on_disconnect_fn *on_disconnect; - void *on_disconnect_user_data; - - aws_mqtt_client_on_connection_complete_fn *on_connection_complete; - void *on_connection_complete_user_data; -}; - -struct aws_mqtt_adapter_final_destroy_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection *connection; -}; - -static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - (void)status; - - struct aws_mqtt_adapter_final_destroy_task *destroy_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = destroy_task->connection->impl; - - if (adapter->client->config->websocket_handshake_transform_user_data == adapter) { - /* - * If the mqtt5 client is pointing to us for websocket transform, then erase that. The callback - * is invoked from our pinned event loop so this is safe. - * - * TODO: It is possible that multiple adapters may have sequentially side-affected the websocket handshake. - * For now, in that case, subsequent connection attempts will probably not succeed. - */ - adapter->client->config->websocket_handshake_transform = NULL; - adapter->client->config->websocket_handshake_transform_user_data = NULL; - } - - adapter->client = aws_mqtt5_client_release(adapter->client); - aws_rw_lock_clean_up(&adapter->lock); - - aws_mem_release(adapter->allocator, adapter); - - aws_mem_release(destroy_task->allocator, destroy_task); -} - -static struct aws_mqtt_adapter_final_destroy_task *s_aws_mqtt_adapter_final_destroy_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter) { - - struct aws_mqtt_adapter_final_destroy_task *destroy_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_final_destroy_task)); - - aws_task_init( - &destroy_task->task, s_mqtt_adapter_final_destroy_task_fn, (void *)destroy_task, "MqttAdapterFinalDestroy"); - destroy_task->allocator = adapter->allocator; - destroy_task->connection = &adapter->base; /* Do not acquire, we're at zero external and internal ref counts */ - - return destroy_task; -} - -static void s_aws_mqtt_adapter_final_destroy(struct aws_mqtt_client_connection_5_impl *adapter) { - - struct aws_mqtt_adapter_final_destroy_task *task = - s_aws_mqtt_adapter_final_destroy_task_new(adapter->allocator, adapter); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter final destroy task", (void *)adapter); - return; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); -} - -typedef int (*adapter_callback_fn)(struct aws_mqtt_client_connection_5_impl *adapter, void *context); - -/* - * The state/ref-count lock is held during synchronous callbacks to prevent invoking into something that is in the - * process of destruction. In general this isn't a performance worry since callbacks are invoked from a single thread: - * the event loop that the client and adapter are seated on. - * - * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown - * process since if we naively always locked, then an adapter release from within a callback would deadlock. - * - * We need a way to tell if locking will result in a deadlock. The specific case is invoking a synchronous - * callback from the event loop that re-enters the adapter logic via releasing the connection. We can recognize - * this scenario by setting/clearing an internal flag (in_synchronous_callback) and checking it only if we're - * in the event loop thread. If it's true, we know we've already locked at the beginning of the synchronous callback - * and we can safely skip locking, otherwise we must lock. - * - * This function gives us a helper for making these kinds of safe callbacks. We use it in: - * (1) Releasing the connection - * (2) Websocket handshake transform - * (3) Making lifecycle and operation callbacks on the mqtt311 interface - * - * It works by - * (1) Correctly determining if locking would deadlock and skipping lock only in that case, otherwise locking - * (2) Invoke the callback - * (3) Unlock if we locked in step (1) - * - * It also properly sets/clears the in_synchronous_callback flag if we're in the event loop and are not in - * a callback already. - */ -static int s_aws_mqtt5_adapter_perform_safe_callback( - struct aws_mqtt_client_connection_5_impl *adapter, - bool use_write_lock, - adapter_callback_fn callback_fn, - void *callback_user_data) { - - /* Step (1) - conditionally lock and manipulate the in_synchronous_callback flag */ - bool should_unlock = true; - bool clear_synchronous_callback_flag = false; - if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { - if (adapter->in_synchronous_callback) { - should_unlock = false; - } else { - adapter->in_synchronous_callback = true; - clear_synchronous_callback_flag = true; - } - } - - if (should_unlock) { - if (use_write_lock) { - aws_rw_lock_wlock(&adapter->lock); - } else { - aws_rw_lock_rlock(&adapter->lock); - } - } - - // Step (2) - perform the callback - int result = (*callback_fn)(adapter, callback_user_data); - - // Step (3) - undo anything we did in step (1) - if (should_unlock) { - if (use_write_lock) { - aws_rw_lock_wunlock(&adapter->lock); - } else { - aws_rw_lock_runlock(&adapter->lock); - } - } - - if (clear_synchronous_callback_flag) { - adapter->in_synchronous_callback = false; - } - - return result; -} - -struct aws_mqtt_adapter_disconnect_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - aws_mqtt_client_on_disconnect_fn *on_disconnect; - void *on_disconnect_user_data; -}; - -static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); - -static struct aws_mqtt_adapter_disconnect_task *s_aws_mqtt_adapter_disconnect_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - aws_mqtt_client_on_disconnect_fn *on_disconnect, - void *on_disconnect_user_data) { - - struct aws_mqtt_adapter_disconnect_task *disconnect_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_disconnect_task)); - - aws_task_init( - &disconnect_task->task, s_adapter_disconnect_task_fn, (void *)disconnect_task, "AdapterDisconnectTask"); - disconnect_task->allocator = adapter->allocator; - disconnect_task->adapter = - (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - - disconnect_task->on_disconnect = on_disconnect; - disconnect_task->on_disconnect_user_data = on_disconnect_user_data; - - return disconnect_task; -} - -static int s_aws_mqtt_client_connection_5_disconnect( - void *impl, - aws_mqtt_client_on_disconnect_fn *on_disconnect, - void *on_disconnect_user_data) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_adapter_disconnect_task *task = - s_aws_mqtt_adapter_disconnect_task_new(adapter->allocator, adapter, on_disconnect, on_disconnect_user_data); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter disconnect task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_adapter_connect_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - struct aws_byte_buf host_name; - uint16_t port; - struct aws_socket_options socket_options; - struct aws_tls_connection_options *tls_options_ptr; - struct aws_tls_connection_options tls_options; - - struct aws_byte_buf client_id; - uint16_t keep_alive_time_secs; - uint32_t ping_timeout_ms; - uint32_t protocol_operation_timeout_ms; - aws_mqtt_client_on_connection_complete_fn *on_connection_complete; - void *on_connection_complete_user_data; - bool clean_session; -}; - -static void s_aws_mqtt_adapter_connect_task_destroy(struct aws_mqtt_adapter_connect_task *task) { - if (task == NULL) { - return; - } - - aws_byte_buf_clean_up(&task->host_name); - aws_byte_buf_clean_up(&task->client_id); - - if (task->tls_options_ptr) { - aws_tls_connection_options_clean_up(task->tls_options_ptr); - } - - aws_mem_release(task->allocator, task); -} - -static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); - -static struct aws_mqtt_adapter_connect_task *s_aws_mqtt_adapter_connect_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - const struct aws_mqtt_connection_options *connection_options) { - - struct aws_mqtt_adapter_connect_task *connect_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_connect_task)); - - aws_task_init(&connect_task->task, s_adapter_connect_task_fn, (void *)connect_task, "AdapterConnectTask"); - connect_task->allocator = adapter->allocator; - connect_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - - aws_byte_buf_init_copy_from_cursor(&connect_task->host_name, allocator, connection_options->host_name); - connect_task->port = connection_options->port; - connect_task->socket_options = *connection_options->socket_options; - if (connection_options->tls_options) { - aws_tls_connection_options_copy(&connect_task->tls_options, connection_options->tls_options); - connect_task->tls_options_ptr = &connect_task->tls_options; - } - - aws_byte_buf_init_copy_from_cursor(&connect_task->client_id, allocator, connection_options->client_id); - - connect_task->keep_alive_time_secs = connection_options->keep_alive_time_secs; - connect_task->ping_timeout_ms = connection_options->ping_timeout_ms; - connect_task->protocol_operation_timeout_ms = connection_options->protocol_operation_timeout_ms; - connect_task->on_connection_complete = connection_options->on_connection_complete; - connect_task->on_connection_complete_user_data = connection_options->user_data; - connect_task->clean_session = connection_options->clean_session; - - return connect_task; -} - -static int s_validate_adapter_connection_options(const struct aws_mqtt_connection_options *connection_options) { - if (connection_options == NULL) { - return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); - } - - if (connection_options->host_name.len == 0) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "host name not set in MQTT client configuration"); - return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); - } - - /* forbid no-timeout until someone convinces me otherwise */ - if (connection_options->socket_options != NULL) { - if (connection_options->socket_options->type == AWS_SOCKET_DGRAM || - connection_options->socket_options->connect_timeout_ms == 0) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid socket options in MQTT client configuration"); - return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); - } - } - - /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ - if (!aws_mqtt5_client_keep_alive_options_are_valid( - connection_options->keep_alive_time_secs, connection_options->ping_timeout_ms)) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "keep alive interval is too small relative to ping timeout interval"); - return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); - } - - return AWS_OP_SUCCESS; -} - -static int s_aws_mqtt_client_connection_5_connect( - void *impl, - const struct aws_mqtt_connection_options *connection_options) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ - if (s_validate_adapter_connection_options(connection_options)) { - return AWS_OP_ERR; - } - - struct aws_mqtt_adapter_connect_task *task = - s_aws_mqtt_adapter_connect_task_new(adapter->allocator, adapter, connection_options); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create adapter connect task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -static int s_aws_mqtt3_to_mqtt5_adapter_safe_lifecycle_handler( - struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - const struct aws_mqtt5_client_lifecycle_event *event = context; - - /* - * Never invoke a callback after termination - */ - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } - - switch (event->event_type) { - - case AWS_MQTT5_CLET_CONNECTION_SUCCESS: - if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { - /* - * If the 311 view is that this is an initial connection attempt, then invoke the completion callback - * and move to the stay-connected state. - */ - if (adapter->on_connection_complete != NULL) { - (*adapter->on_connection_complete)( - &adapter->base, - event->error_code, - 0, - event->settings->rejoined_session, - adapter->on_connection_complete_user_data); - - adapter->on_connection_complete = NULL; - adapter->on_connection_complete_user_data = NULL; - } - adapter->adapter_state = AWS_MQTT_AS_STAY_CONNECTED; - } else if (adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED) { - /* - * If the 311 view is that we're in the stay-connected state (ie we've successfully done or simulated - * an initial connection), then invoke the connection resumption callback. - */ - if (adapter->on_resumed != NULL) { - (*adapter->on_resumed)( - &adapter->base, 0, event->settings->rejoined_session, adapter->on_resumed_user_data); - } - } - break; - - case AWS_MQTT5_CLET_CONNECTION_FAILURE: - /* - * The MQTT311 interface only cares about connection failures when it's the initial connection attempt - * after a call to connect(). Since an adapter connect() can sever an existing connection (with an - * error code of AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) we only react to connection failures - * if - * (1) the error code is not AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT and - * (2) we're in the FIRST_CONNECT state - * - * Only if both of these are true should we invoke the connection completion callback with a failure and - * put the adapter into the "disconnected" state, simulating the way the 311 client stops after an - * initial connection failure. - */ - if (event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT && - adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { - - if (adapter->on_connection_complete != NULL) { - (*adapter->on_connection_complete)( - &adapter->base, event->error_code, 0, false, adapter->on_connection_complete_user_data); - - adapter->on_connection_complete = NULL; - adapter->on_connection_complete_user_data = NULL; - } - - adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; - } - break; - - case AWS_MQTT5_CLET_DISCONNECTION: - /* - * If the 311 view is that we're in the stay-connected state (ie we've successfully done or simulated - * an initial connection), then invoke the connection interrupted callback. - */ - if (adapter->on_interrupted != NULL && adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED && - event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) { - - (*adapter->on_interrupted)(&adapter->base, event->error_code, adapter->on_interrupted_user_data); - } - break; - - case AWS_MQTT5_CLET_STOPPED: - /* If an MQTT311-view user is waiting on a disconnect callback, invoke it */ - if (adapter->on_disconnect) { - (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); - - adapter->on_disconnect = NULL; - adapter->on_disconnect_user_data = NULL; - } - - if (adapter->on_closed) { - (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); - } - - /* - * Judgement call: If the mqtt5 client is stopped behind our back, it seems better to transition to the - * disconnected state (which only requires a connect() to restart) then stay in the STAY_CONNECTED state - * which currently requires a disconnect() and then a connect() to restore connectivity. - * - * ToDo: what if we disabled mqtt5 client start/stop somehow while the adapter is attached, preventing - * the potential to backstab each other? Unfortunately neither start() nor stop() have an error reporting - * mechanism. - */ - adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; - break; - - default: - break; - } - - return AWS_OP_SUCCESS; -} - -static void s_aws_mqtt5_client_lifecycle_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { - struct aws_mqtt_client_connection_5_impl *adapter = event->user_data; - - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_lifecycle_handler, (void *)event); -} - -static int s_aws_mqtt3_to_mqtt5_adapter_safe_disconnect_handler( - struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - struct aws_mqtt_adapter_disconnect_task *disconnect_task = context; - - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } - - /* - * If we're already disconnected (from the 311 perspective only), then invoke the callback and return - */ - if (adapter->adapter_state == AWS_MQTT_AS_STAY_DISCONNECTED) { - if (disconnect_task->on_disconnect) { - (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); - } - - return AWS_OP_SUCCESS; - } - - /* - * If we had a pending first connect, then notify failure - */ - if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { - if (adapter->on_connection_complete != NULL) { - (*adapter->on_connection_complete)( - &adapter->base, - AWS_ERROR_MQTT_CONNECTION_SHUTDOWN, - 0, - false, - adapter->on_connection_complete_user_data); - - adapter->on_connection_complete = NULL; - adapter->on_connection_complete_user_data = NULL; - } - } - - adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; - - bool invoke_callbacks = true; - if (adapter->client->desired_state != AWS_MCS_STOPPED) { - aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_STOPPED, NULL); - - adapter->on_disconnect = disconnect_task->on_disconnect; - adapter->on_disconnect_user_data = disconnect_task->on_disconnect_user_data; - invoke_callbacks = false; - } - - if (invoke_callbacks) { - if (disconnect_task->on_disconnect != NULL) { - (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); - } - - if (adapter->on_closed) { - (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); - } - } - - return AWS_OP_SUCCESS; -} - -static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_adapter_disconnect_task *disconnect_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = disconnect_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_disconnect_handler, disconnect_task); - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(disconnect_task->allocator, disconnect_task); -} - -static void s_aws_mqtt3_to_mqtt5_adapter_update_config_on_connect( - struct aws_mqtt_client_connection_5_impl *adapter, - struct aws_mqtt_adapter_connect_task *connect_task) { - struct aws_mqtt5_client_options_storage *config = adapter->client->config; - - aws_string_destroy(config->host_name); - config->host_name = aws_string_new_from_buf(adapter->allocator, &connect_task->host_name); - config->port = connect_task->port; - config->socket_options = connect_task->socket_options; - - if (config->tls_options_ptr) { - aws_tls_connection_options_clean_up(&config->tls_options); - config->tls_options_ptr = NULL; - } - - if (connect_task->tls_options_ptr) { - aws_tls_connection_options_copy(&config->tls_options, connect_task->tls_options_ptr); - config->tls_options_ptr = &config->tls_options; - } - - aws_byte_buf_clean_up(&adapter->client->negotiated_settings.client_id_storage); - aws_byte_buf_init_copy_from_cursor( - &adapter->client->negotiated_settings.client_id_storage, - adapter->allocator, - aws_byte_cursor_from_buf(&connect_task->client_id)); - - config->connect->storage_view.keep_alive_interval_seconds = connect_task->keep_alive_time_secs; - config->ping_timeout_ms = connect_task->ping_timeout_ms; - config->ack_timeout_seconds = aws_max_u64( - 1, - aws_timestamp_convert( - connect_task->protocol_operation_timeout_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, NULL)); - if (connect_task->clean_session) { - config->session_behavior = AWS_MQTT5_CSBT_CLEAN; - config->connect->storage_view.session_expiry_interval_seconds = NULL; - } else { - config->session_behavior = AWS_MQTT5_CSBT_REJOIN_ALWAYS; - /* This is a judgement call to translate session expiry to the maximum possible allowed by AWS IoT Core */ - config->connect->session_expiry_interval_seconds = 7 * 24 * 60 * 60; - config->connect->storage_view.session_expiry_interval_seconds = - &config->connect->session_expiry_interval_seconds; - } -} - -static int s_aws_mqtt3_to_mqtt5_adapter_safe_connect_handler( - struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - struct aws_mqtt_adapter_connect_task *connect_task = context; - - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } - - if (adapter->adapter_state != AWS_MQTT_AS_STAY_DISCONNECTED) { - if (connect_task->on_connection_complete) { - (*connect_task->on_connection_complete)( - &adapter->base, - AWS_ERROR_MQTT_ALREADY_CONNECTED, - 0, - false, - connect_task->on_connection_complete_user_data); - } - return AWS_OP_SUCCESS; - } - - if (adapter->on_disconnect) { - (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); - - adapter->on_disconnect = NULL; - adapter->on_disconnect_user_data = NULL; - } - - adapter->adapter_state = AWS_MQTT_AS_FIRST_CONNECT; - - /* Update mqtt5 config */ - s_aws_mqtt3_to_mqtt5_adapter_update_config_on_connect(adapter, connect_task); - - aws_mqtt5_client_reset_connection(adapter->client); - - aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_CONNECTED, NULL); - - adapter->on_connection_complete = connect_task->on_connection_complete; - adapter->on_connection_complete_user_data = connect_task->on_connection_complete_user_data; - - return AWS_OP_SUCCESS; -} - -static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_adapter_connect_task *connect_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = connect_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt3_to_mqtt5_adapter_safe_connect_handler, connect_task); - -done: - - aws_ref_count_release(&adapter->internal_refs); - - s_aws_mqtt_adapter_connect_task_destroy(connect_task); -} - -static bool s_aws_mqtt5_listener_publish_received_adapter( - const struct aws_mqtt5_packet_publish_view *publish, - void *user_data) { - (void)publish; - (void)user_data; - - return false; -} - -struct aws_mqtt_set_interruption_handlers_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; - void *on_interrupted_user_data; - - aws_mqtt_client_on_connection_resumed_fn *on_resumed; - void *on_resumed_user_data; -}; - -static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_interruption_handlers_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - adapter->on_interrupted = set_task->on_interrupted; - adapter->on_interrupted_user_data = set_task->on_interrupted_user_data; - adapter->on_resumed = set_task->on_resumed; - adapter->on_resumed_user_data = set_task->on_resumed_user_data; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interruption_handlers_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, - void *on_interrupted_user_data, - aws_mqtt_client_on_connection_resumed_fn *on_resumed, - void *on_resumed_user_data) { - - struct aws_mqtt_set_interruption_handlers_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_interruption_handlers_task)); - - aws_task_init( - &set_task->task, s_set_interruption_handlers_task_fn, (void *)set_task, "SetInterruptionHandlersTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->on_interrupted = on_interrupted; - set_task->on_interrupted_user_data = on_interrupted_user_data; - set_task->on_resumed = on_resumed; - set_task->on_resumed_user_data = on_resumed_user_data; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_interruption_handlers( - void *impl, - aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, - void *on_interrupted_user_data, - aws_mqtt_client_on_connection_resumed_fn *on_resumed, - void *on_resumed_user_data) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_interruption_handlers_task *task = s_aws_mqtt_set_interruption_handlers_task_new( - adapter->allocator, adapter, on_interrupted, on_interrupted_user_data, on_resumed, on_resumed_user_data); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set interruption handlers task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_on_closed_handler_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - aws_mqtt_client_on_connection_closed_fn *on_closed; - void *on_closed_user_data; -}; - -static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_on_closed_handler_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - adapter->on_closed = set_task->on_closed; - adapter->on_closed_user_data = set_task->on_closed_user_data; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_handler_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - aws_mqtt_client_on_connection_closed_fn *on_closed, - void *on_closed_user_data) { - - struct aws_mqtt_set_on_closed_handler_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_closed_handler_task)); - - aws_task_init(&set_task->task, s_set_on_closed_handler_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->on_closed = on_closed; - set_task->on_closed_user_data = on_closed_user_data; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_on_closed_handler( - void *impl, - aws_mqtt_client_on_connection_closed_fn *on_closed, - void *on_closed_user_data) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_on_closed_handler_task *task = - s_aws_mqtt_set_on_closed_handler_task_new(adapter->allocator, adapter, on_closed, on_closed_user_data); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on closed handler task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_on_any_publish_handler_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - aws_mqtt_client_publish_received_fn *on_any_publish; - void *on_any_publish_user_data; -}; - -static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_on_any_publish_handler_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - adapter->on_any_publish = set_task->on_any_publish; - adapter->on_any_publish_user_data = set_task->on_any_publish_user_data; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_publish_handler_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - aws_mqtt_client_publish_received_fn *on_any_publish, - void *on_any_publish_user_data) { - - struct aws_mqtt_set_on_any_publish_handler_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_any_publish_handler_task)); - - aws_task_init( - &set_task->task, s_set_on_any_publish_handler_task_fn, (void *)set_task, "SetOnAnyPublishHandlerTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->on_any_publish = on_any_publish; - set_task->on_any_publish_user_data = on_any_publish_user_data; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_on_any_publish_handler( - void *impl, - aws_mqtt_client_publish_received_fn *on_any_publish, - void *on_any_publish_user_data) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_on_any_publish_handler_task *task = s_aws_mqtt_set_on_any_publish_handler_task_new( - adapter->allocator, adapter, on_any_publish, on_any_publish_user_data); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set on any publish task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_reconnect_timeout_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - uint64_t min_timeout; - uint64_t max_timeout; -}; - -static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_reconnect_timeout_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - adapter->client->config->min_reconnect_delay_ms = set_task->min_timeout; - adapter->client->config->max_reconnect_delay_ms = set_task->max_timeout; - adapter->client->current_reconnect_delay_ms = set_task->min_timeout; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_timeout_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - uint64_t min_timeout, - uint64_t max_timeout) { - - struct aws_mqtt_set_reconnect_timeout_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_reconnect_timeout_task)); - - aws_task_init(&set_task->task, s_set_reconnect_timeout_task_fn, (void *)set_task, "SetReconnectTimeoutTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->min_timeout = aws_min_u64(min_timeout, max_timeout); - set_task->max_timeout = aws_max_u64(min_timeout, max_timeout); - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_reconnect_timeout( - void *impl, - uint64_t min_timeout, - uint64_t max_timeout) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_reconnect_timeout_task *task = - s_aws_mqtt_set_reconnect_timeout_task_new(adapter->allocator, adapter, min_timeout, max_timeout); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_http_proxy_options_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - struct aws_http_proxy_config *proxy_config; -}; - -static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_http_proxy_options_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - aws_http_proxy_config_destroy(adapter->client->config->http_proxy_config); - - /* move the proxy config from the set task to the client's config */ - adapter->client->config->http_proxy_config = set_task->proxy_config; - if (adapter->client->config->http_proxy_config != NULL) { - aws_http_proxy_options_init_from_config( - &adapter->client->config->http_proxy_options, adapter->client->config->http_proxy_config); - } - - /* don't clean up the proxy config if it was successfully assigned to the mqtt5 client */ - set_task->proxy_config = NULL; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - /* If the task was canceled we need to clean this up because it didn't get assigned to the mqtt5 client */ - aws_http_proxy_config_destroy(set_task->proxy_config); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_options_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - struct aws_http_proxy_options *proxy_options) { - - struct aws_http_proxy_config *proxy_config = - aws_http_proxy_config_new_tunneling_from_proxy_options(allocator, proxy_options); - if (proxy_config == NULL) { - aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); - return NULL; - } - - struct aws_mqtt_set_http_proxy_options_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_http_proxy_options_task)); - - aws_task_init(&set_task->task, s_set_http_proxy_options_task_fn, (void *)set_task, "SetHttpProxyOptionsTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->proxy_config = proxy_config; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_http_proxy_options( - void *impl, - struct aws_http_proxy_options *proxy_options) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_http_proxy_options_task *task = - s_aws_mqtt_set_http_proxy_options_task_new(adapter->allocator, adapter, proxy_options); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set http proxy options task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_use_websockets_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - aws_mqtt_transform_websocket_handshake_fn *transformer; - void *transformer_user_data; -}; - -static void s_aws_mqtt5_adapter_websocket_handshake_completion_fn( - struct aws_http_message *request, - int error_code, - void *complete_ctx) { - - struct aws_mqtt_client_connection_5_impl *adapter = complete_ctx; - - (*adapter->mqtt5_websocket_handshake_completion_function)( - request, error_code, adapter->mqtt5_websocket_handshake_completion_user_data); - - aws_ref_count_release(&adapter->internal_refs); -} - -struct aws_mqtt5_adapter_websocket_handshake_args { - bool chain_callback; - struct aws_http_message *input_request; - struct aws_http_message *output_request; - int completion_error_code; -}; - -static int s_safe_websocket_handshake_fn(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { - struct aws_mqtt5_adapter_websocket_handshake_args *args = context; - - if (adapter->synced_data.terminated) { - args->completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; - } else if (adapter->websocket_handshake_transformer == NULL) { - args->output_request = args->input_request; - } else { - aws_ref_count_acquire(&adapter->internal_refs); - args->chain_callback = true; - } - - return AWS_OP_SUCCESS; -} - -static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( - struct aws_http_message *request, - void *user_data, - aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, - void *complete_ctx) { - - struct aws_mqtt_client_connection_5_impl *adapter = user_data; - - struct aws_mqtt5_adapter_websocket_handshake_args args = { - .input_request = request, - }; - - s_aws_mqtt5_adapter_perform_safe_callback(adapter, false, s_safe_websocket_handshake_fn, &args); - - if (args.chain_callback) { - adapter->mqtt5_websocket_handshake_completion_function = complete_fn; - adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; - - (*adapter->websocket_handshake_transformer)( - request, user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); - } else { - (*complete_fn)(args.output_request, args.completion_error_code, complete_ctx); - } -} - -static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_use_websockets_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - adapter->websocket_handshake_transformer = set_task->transformer; - adapter->websocket_handshake_transformer_user_data = set_task->transformer_user_data; - - /* we're in the mqtt5 client's event loop; it's safe to access its internal state */ - adapter->client->config->websocket_handshake_transform = s_aws_mqtt5_adapter_transform_websocket_handshake_fn; - adapter->client->config->websocket_handshake_transform_user_data = adapter; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - aws_mqtt_transform_websocket_handshake_fn *transformer, - void *transformer_user_data) { - - struct aws_mqtt_set_use_websockets_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_use_websockets_task)); - - aws_task_init(&set_task->task, s_set_use_websockets_task_fn, (void *)set_task, "SetUseWebsocketsTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->transformer = transformer; - set_task->transformer_user_data = transformer_user_data; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_use_websockets( - void *impl, - aws_mqtt_transform_websocket_handshake_fn *transformer, - void *transformer_user_data, - aws_mqtt_validate_websocket_handshake_fn *validator, - void *validator_user_data) { - - /* mqtt5 doesn't use these */ - (void)validator; - (void)validator_user_data; - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_use_websockets_task *task = - s_aws_mqtt_set_use_websockets_task_new(adapter->allocator, adapter, transformer, transformer_user_data); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set use websockets task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_host_resolution_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - struct aws_host_resolution_config host_resolution_config; -}; - -static void s_set_host_resolution_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_host_resolution_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - adapter->client->config->host_resolution_override = set_task->host_resolution_config; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - aws_mem_release(set_task->allocator, set_task); -} - -static struct aws_mqtt_set_host_resolution_task *s_aws_mqtt_set_host_resolution_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - const struct aws_host_resolution_config *host_resolution_config) { - - struct aws_mqtt_set_host_resolution_task *set_task = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_host_resolution_task)); - - aws_task_init(&set_task->task, s_set_host_resolution_task_fn, (void *)set_task, "SetHostResolutionTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - set_task->host_resolution_config = *host_resolution_config; - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_host_resolution_options( - void *impl, - const struct aws_host_resolution_config *host_resolution_config) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_host_resolution_task *task = - s_aws_mqtt_set_host_resolution_task_new(adapter->allocator, adapter, host_resolution_config); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set reconnect timeout task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_will_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - struct aws_byte_buf topic_buffer; - enum aws_mqtt_qos qos; - bool retain; - struct aws_byte_buf payload_buffer; -}; - -static void s_aws_mqtt_set_will_task_destroy(struct aws_mqtt_set_will_task *task) { - if (task == NULL) { - return; - } - - aws_byte_buf_clean_up(&task->topic_buffer); - aws_byte_buf_clean_up(&task->payload_buffer); - - aws_mem_release(task->allocator, task); -} - -static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_will_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - struct aws_mqtt5_packet_connect_storage *connect = adapter->client->config->connect; - - /* clean up the old will if necessary */ - if (connect->will != NULL) { - aws_mqtt5_packet_publish_storage_clean_up(connect->will); - aws_mem_release(connect->allocator, connect->will); - connect->will = NULL; - } - - struct aws_mqtt5_packet_publish_view will = { - .topic = aws_byte_cursor_from_buf(&set_task->topic_buffer), - .qos = (enum aws_mqtt5_qos)set_task->qos, - .retain = set_task->retain, - .payload = aws_byte_cursor_from_buf(&set_task->payload_buffer), - }; - - /* make a new will */ - connect->will = aws_mem_calloc(connect->allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); - aws_mqtt5_packet_publish_storage_init(connect->will, connect->allocator, &will); - - /* manually update the storage view's will reference */ - connect->storage_view.will = &connect->will->storage_view; - -done: - - aws_ref_count_release(&adapter->internal_refs); - - s_aws_mqtt_set_will_task_destroy(set_task); -} - -static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - const struct aws_byte_cursor *topic, - enum aws_mqtt_qos qos, - bool retain, - const struct aws_byte_cursor *payload) { - - if (topic == NULL) { - return NULL; - } - - struct aws_mqtt_set_will_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_will_task)); - - aws_task_init(&set_task->task, s_set_will_task_fn, (void *)set_task, "SetWillTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - - set_task->qos = qos; - set_task->retain = retain; - aws_byte_buf_init_copy_from_cursor(&set_task->topic_buffer, allocator, *topic); - if (payload != NULL) { - aws_byte_buf_init_copy_from_cursor(&set_task->payload_buffer, allocator, *payload); - } - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_will( - void *impl, - const struct aws_byte_cursor *topic, - enum aws_mqtt_qos qos, - bool retain, - const struct aws_byte_cursor *payload) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_will_task *task = - s_aws_mqtt_set_will_task_new(adapter->allocator, adapter, topic, qos, retain, payload); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set will task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -struct aws_mqtt_set_login_task { - struct aws_task task; - struct aws_allocator *allocator; - struct aws_mqtt_client_connection_5_impl *adapter; - - struct aws_byte_buf username_buffer; - struct aws_byte_buf password_buffer; -}; - -static void s_aws_mqtt_set_login_task_destroy(struct aws_mqtt_set_login_task *task) { - if (task == NULL) { - return; - } - - aws_byte_buf_clean_up_secure(&task->username_buffer); - aws_byte_buf_clean_up_secure(&task->password_buffer); - - aws_mem_release(task->allocator, task); -} - -static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - struct aws_mqtt_set_login_task *set_task = arg; - struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; - if (status != AWS_TASK_STATUS_RUN_READY) { - goto done; - } - - struct aws_byte_cursor username_cursor = aws_byte_cursor_from_buf(&set_task->username_buffer); - struct aws_byte_cursor password_cursor = aws_byte_cursor_from_buf(&set_task->password_buffer); - - /* we're in the mqtt5 client's event loop; it's safe to access internal state */ - struct aws_mqtt5_packet_connect_storage *old_connect = adapter->client->config->connect; - - /* - * Packet storage stores binary data in a single buffer. The safest way to replace some binary data is - * to make a new storage from the old storage, deleting the old storage after construction is complete. - */ - struct aws_mqtt5_packet_connect_view new_connect_view = old_connect->storage_view; - - if (set_task->username_buffer.len > 0) { - new_connect_view.username = &username_cursor; - } else { - new_connect_view.username = NULL; - } - - if (set_task->password_buffer.len > 0) { - new_connect_view.password = &password_cursor; - } else { - new_connect_view.password = NULL; - } - - if (aws_mqtt5_packet_connect_view_validate(&new_connect_view)) { - goto done; - } - - struct aws_mqtt5_packet_connect_storage *new_connect = - aws_mem_calloc(adapter->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); - aws_mqtt5_packet_connect_storage_init(new_connect, adapter->allocator, &new_connect_view); - - adapter->client->config->connect = new_connect; - aws_mqtt5_packet_connect_storage_clean_up(old_connect); - aws_mem_release(old_connect->allocator, old_connect); - -done: - - aws_ref_count_release(&adapter->internal_refs); - - s_aws_mqtt_set_login_task_destroy(set_task); -} - -static struct aws_mqtt_set_login_task *s_aws_mqtt_set_login_task_new( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection_5_impl *adapter, - const struct aws_byte_cursor *username, - const struct aws_byte_cursor *password) { - - struct aws_mqtt_set_login_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_login_task)); - - aws_task_init(&set_task->task, s_set_login_task_fn, (void *)set_task, "SetLoginTask"); - set_task->allocator = adapter->allocator; - set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); - - if (username != NULL) { - aws_byte_buf_init_copy_from_cursor(&set_task->username_buffer, allocator, *username); - } - - if (password != NULL) { - aws_byte_buf_init_copy_from_cursor(&set_task->password_buffer, allocator, *password); - } - - return set_task; -} - -static int s_aws_mqtt_client_connection_5_set_login( - void *impl, - const struct aws_byte_cursor *username, - const struct aws_byte_cursor *password) { - - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - struct aws_mqtt_set_login_task *task = - s_aws_mqtt_set_login_task_new(adapter->allocator, adapter, username, password); - if (task == NULL) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: failed to create set login task", (void *)adapter); - return AWS_OP_ERR; - } - - aws_event_loop_schedule_task_now(adapter->loop, &task->task); - - return AWS_OP_SUCCESS; -} - -static void s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs(void *context) { - struct aws_mqtt_client_connection_5_impl *adapter = context; - - s_aws_mqtt_adapter_final_destroy(adapter); -} - -static void s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached(void *context) { - struct aws_mqtt_client_connection_5_impl *adapter = context; - - /* - * Release the single internal reference that we started with. Only ephemeral references for cross-thread - * tasks might remain, and they will disappear quickly. - */ - aws_ref_count_release(&adapter->internal_refs); -} - -static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire(void *impl) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - aws_ref_count_acquire(&adapter->external_refs); - - return &adapter->base; -} - -static int s_decref_for_shutdown(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { - (void)context; - - adapter->synced_data.terminated = true; - - return AWS_OP_SUCCESS; -} - -static void s_aws_mqtt3_to_mqtt5_adapter_on_zero_external_refs(void *impl) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - s_aws_mqtt5_adapter_perform_safe_callback(adapter, true, s_decref_for_shutdown, NULL); - - /* - * When the adapter's exernal ref count goes to zero, here's what we want to do: - * - * (1) Put the adapter into the terminated state, which tells it to stop processing callbacks from the mqtt5 - * client - * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user - * of it) - * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we - * are guaranteed that no more callbacks from the mqtt5 client will reach us. - * (4) Release the single internal ref we started with when the adapter was created. - * (5) On last internal ref, we can safely release the mqtt5 client and synchronously clean up all other - * resources - * - * Step (1) was done within the lock-guarded safe callback above. - * Step (2) is done here. - * Steps (3) and (4) are accomplished by s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached - * Step (5) is completed by s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs - */ - aws_mqtt5_listener_release(adapter->listener); -} - -static void s_aws_mqtt_client_connection_5_release(void *impl) { - struct aws_mqtt_client_connection_5_impl *adapter = impl; - - aws_ref_count_release(&adapter->external_refs); -} - -static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { - .acquire_fn = s_aws_mqtt_client_connection_5_acquire, - .release_fn = s_aws_mqtt_client_connection_5_release, - .set_will_fn = s_aws_mqtt_client_connection_5_set_will, - .set_login_fn = s_aws_mqtt_client_connection_5_set_login, - .use_websockets_fn = s_aws_mqtt_client_connection_5_use_websockets, - .set_http_proxy_options_fn = s_aws_mqtt_client_connection_5_set_http_proxy_options, - .set_host_resolution_options_fn = s_aws_mqtt_client_connection_5_set_host_resolution_options, - .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_5_set_reconnect_timeout, - .set_connection_result_handlers = NULL, // TODO: Need update with introduction of mqtt5 lifeCycleEventCallback - .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, - .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, - .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, - .connect_fn = s_aws_mqtt_client_connection_5_connect, - .reconnect_fn = NULL, - .disconnect_fn = s_aws_mqtt_client_connection_5_disconnect, - .subscribe_multiple_fn = NULL, - .subscribe_fn = NULL, - .resubscribe_existing_topics_fn = NULL, - .unsubscribe_fn = NULL, - .publish_fn = NULL, - .get_stats_fn = NULL, -}; - -static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_5_vtable_ptr = - &s_aws_mqtt_client_connection_5_vtable; - -struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_client(struct aws_mqtt5_client *client) { - struct aws_allocator *allocator = client->allocator; - struct aws_mqtt_client_connection_5_impl *adapter = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_client_connection_5_impl)); - - adapter->allocator = allocator; - - adapter->base.vtable = s_aws_mqtt_client_connection_5_vtable_ptr; - adapter->base.impl = adapter; - - adapter->client = aws_mqtt5_client_acquire(client); - adapter->loop = client->loop; - adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; - - aws_ref_count_init(&adapter->external_refs, adapter, s_aws_mqtt3_to_mqtt5_adapter_on_zero_external_refs); - aws_ref_count_init(&adapter->internal_refs, adapter, s_aws_mqtt3_to_mqtt5_adapter_on_zero_internal_refs); - - aws_rw_lock_init(&adapter->lock); - - /* - * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." - * We'll enable the adapter as soon as they try to connect via the 311 interface. This - * also ties in to how we simulate the 311 implementation's don't-reconnect-if-initial-connect-fails logic. - * The 5 client will continue to try and reconnect, but the adapter will go disabled making it seem to the 311 - * user that it is offline. - */ - adapter->synced_data.terminated = false; - - struct aws_mqtt5_listener_config listener_config = { - .client = client, - .listener_callbacks = - { - .listener_publish_received_handler = s_aws_mqtt5_listener_publish_received_adapter, - .listener_publish_received_handler_user_data = adapter, - .lifecycle_event_handler = s_aws_mqtt5_client_lifecycle_event_callback_adapter, - .lifecycle_event_handler_user_data = adapter, - }, - .termination_callback = s_aws_mqtt3_to_mqtt5_adapter_on_listener_detached, - .termination_callback_user_data = adapter, - }; - adapter->listener = aws_mqtt5_listener_new(allocator, &listener_config); - - return &adapter->base; -} diff --git a/source/mqtt_subscription_set.c b/source/mqtt_subscription_set.c new file mode 100644 index 00000000..1e946fc5 --- /dev/null +++ b/source/mqtt_subscription_set.c @@ -0,0 +1,431 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "aws/mqtt/private/mqtt_subscription_set.h" + +#include "aws/mqtt/private/client_impl_shared.h" + +#define SUBSCRIPTION_SET_DEFAULT_BRANCH_FACTOR 10 +#define SUBSCRIPTION_SET_DEFAULT_ENTRY_COUNT 50 + +struct aws_mqtt_subscription_set_subscription_record *aws_mqtt_subscription_set_subscription_record_new( + struct aws_allocator *allocator, + const struct aws_mqtt_subscription_set_subscription_options *subscription) { + struct aws_mqtt_subscription_set_subscription_record *record = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_subscription_set_subscription_record)); + + record->allocator = allocator; + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, subscription->topic_filter); + record->subscription_view = *subscription; + record->subscription_view.topic_filter = aws_byte_cursor_from_buf(&record->topic_filter); + + return record; +} + +void aws_mqtt_subscription_set_subscription_record_destroy( + struct aws_mqtt_subscription_set_subscription_record *record) { + if (record == NULL) { + return; + } + + aws_byte_buf_clean_up(&record->topic_filter); + aws_mem_release(record->allocator, record); +} + +static void s_aws_mqtt_subscription_set_subscription_record_hash_destroy(void *element) { + struct aws_mqtt_subscription_set_subscription_record *record = element; + + aws_mqtt_subscription_set_subscription_record_destroy(record); +} + +static struct aws_mqtt_subscription_set_topic_tree_node *s_aws_mqtt_subscription_set_node_new( + struct aws_allocator *allocator, + struct aws_mqtt_subscription_set_topic_tree_node *parent) { + + struct aws_mqtt_subscription_set_topic_tree_node *node = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_subscription_set_topic_tree_node)); + node->allocator = allocator; + aws_hash_table_init( + &node->children, + allocator, + SUBSCRIPTION_SET_DEFAULT_BRANCH_FACTOR, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + NULL); + node->ref_count = 1; + node->parent = parent; + + return node; +} + +struct aws_mqtt_subscription_set *aws_mqtt_subscription_set_new(struct aws_allocator *allocator) { + + struct aws_mqtt_subscription_set *subscription_set = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_subscription_set)); + + subscription_set->allocator = allocator; + subscription_set->root = s_aws_mqtt_subscription_set_node_new(allocator, NULL); + + aws_hash_table_init( + &subscription_set->subscriptions, + allocator, + SUBSCRIPTION_SET_DEFAULT_ENTRY_COUNT, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_mqtt_subscription_set_subscription_record_hash_destroy); + + return subscription_set; +} + +static int s_subscription_set_node_destroy_hash_foreach_wrap(void *context, struct aws_hash_element *elem); + +static void s_aws_mqtt_subscription_set_node_destroy_node(struct aws_mqtt_subscription_set_topic_tree_node *node) { + aws_hash_table_foreach(&node->children, s_subscription_set_node_destroy_hash_foreach_wrap, NULL); + aws_hash_table_clean_up(&node->children); + + if (node->on_cleanup && node->callback_user_data) { + node->on_cleanup(node->callback_user_data); + } + + aws_byte_buf_clean_up(&node->topic_segment); + + aws_mem_release(node->allocator, node); +} + +static void s_aws_mqtt_subscription_set_node_destroy_tree(struct aws_mqtt_subscription_set_topic_tree_node *tree) { + if (tree == NULL) { + return; + } + + if (tree->parent != NULL) { + aws_hash_table_remove(&tree->parent->children, &tree->topic_segment, NULL, NULL); + } + + s_aws_mqtt_subscription_set_node_destroy_node(tree); +} + +static int s_subscription_set_node_destroy_hash_foreach_wrap(void *context, struct aws_hash_element *elem) { + (void)context; + + s_aws_mqtt_subscription_set_node_destroy_node(elem->value); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE | AWS_COMMON_HASH_TABLE_ITER_DELETE; +} + +void aws_mqtt_subscription_set_destroy(struct aws_mqtt_subscription_set *subscription_set) { + if (subscription_set == NULL) { + return; + } + + s_aws_mqtt_subscription_set_node_destroy_tree(subscription_set->root); + + aws_hash_table_clean_up(&subscription_set->subscriptions); + + aws_mem_release(subscription_set->allocator, subscription_set); +} + +static struct aws_mqtt_subscription_set_topic_tree_node *s_aws_mqtt_subscription_set_get_existing_subscription_node( + const struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter) { + + struct aws_mqtt_subscription_set_topic_tree_node *current_node = subscription_set->root; + + struct aws_byte_cursor topic_segment; + AWS_ZERO_STRUCT(topic_segment); + while (aws_byte_cursor_next_split(&topic_filter, '/', &topic_segment)) { + struct aws_hash_element *hash_element = NULL; + aws_hash_table_find(¤t_node->children, &topic_segment, &hash_element); + + if (hash_element == NULL) { + return NULL; + } else { + current_node = hash_element->value; + } + } + + if (!current_node->is_subscription) { + return NULL; + } + + return current_node; +} + +bool aws_mqtt_subscription_set_is_subscribed( + const struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter) { + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&subscription_set->subscriptions, &topic_filter, &element); + + return element && (element->value != NULL); +} + +bool aws_mqtt_subscription_set_is_in_topic_tree( + const struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter) { + struct aws_mqtt_subscription_set_topic_tree_node *existing_node = + s_aws_mqtt_subscription_set_get_existing_subscription_node(subscription_set, topic_filter); + + return existing_node != NULL; +} + +/* + * Walks the existing tree creating nodes as necessary to reach the subscription leaf implied by the topic filter. + * Returns the node representing the final level of the topic filter. Each existing node has its ref count increased by + * one. Newly-created nodes start with a ref count of one. Given that the topic filter has been validated, the only + * possible error is a memory allocation error which is a crash anyways. + * + * If the leaf node already exists and has a cleanup callback, it will be invoked and both the callback and its user + * data will be cleared . The returned node will always have is_subscription set to true. + */ +static struct aws_mqtt_subscription_set_topic_tree_node * + s_aws_mqtt_subscription_set_create_or_reference_topic_filter_path( + struct aws_mqtt_subscription_set_topic_tree_node *root, + struct aws_byte_cursor topic_filter) { + + struct aws_mqtt_subscription_set_topic_tree_node *current_node = root; + ++root->ref_count; + + /* + * Invariants: + * (1) No failure allowed (allocation failure = crash) + * (2) The ref count of current_node is always correct *before* the loop condition is evaluated + */ + struct aws_byte_cursor topic_segment; + AWS_ZERO_STRUCT(topic_segment); + while (aws_byte_cursor_next_split(&topic_filter, '/', &topic_segment)) { + + struct aws_hash_element *hash_element = NULL; + aws_hash_table_find(¤t_node->children, &topic_segment, &hash_element); + + if (hash_element == NULL) { + struct aws_mqtt_subscription_set_topic_tree_node *new_node = + s_aws_mqtt_subscription_set_node_new(current_node->allocator, current_node); + + aws_byte_buf_init_copy_from_cursor(&new_node->topic_segment, new_node->allocator, topic_segment); + new_node->topic_segment_cursor = aws_byte_cursor_from_buf(&new_node->topic_segment); + + aws_hash_table_put(¤t_node->children, &new_node->topic_segment_cursor, new_node, NULL); + + current_node = new_node; + } else { + current_node = hash_element->value; + ++current_node->ref_count; + } + } + + return current_node; +} + +void aws_mqtt_subscription_set_add_subscription( + struct aws_mqtt_subscription_set *subscription_set, + const struct aws_mqtt_subscription_set_subscription_options *subscription_options) { + + AWS_FATAL_ASSERT(aws_mqtt_is_valid_topic_filter(&subscription_options->topic_filter)); + + aws_hash_table_remove(&subscription_set->subscriptions, &subscription_options->topic_filter, NULL, NULL); + + struct aws_mqtt_subscription_set_subscription_record *record = + aws_mqtt_subscription_set_subscription_record_new(subscription_set->allocator, subscription_options); + aws_hash_table_put(&subscription_set->subscriptions, &record->topic_filter, record, NULL); + + struct aws_mqtt_subscription_set_topic_tree_node *subscription_node = + s_aws_mqtt_subscription_set_get_existing_subscription_node( + subscription_set, subscription_options->topic_filter); + if (subscription_node == NULL) { + subscription_node = s_aws_mqtt_subscription_set_create_or_reference_topic_filter_path( + subscription_set->root, subscription_options->topic_filter); + } + + if (subscription_node->on_cleanup) { + (*subscription_node->on_cleanup)(subscription_node->callback_user_data); + subscription_node->on_cleanup = NULL; + } + + subscription_node->is_subscription = true; + + subscription_node->on_publish_received = subscription_options->on_publish_received; + subscription_node->on_cleanup = subscription_options->on_cleanup; + subscription_node->callback_user_data = subscription_options->callback_user_data; +} + +void aws_mqtt_subscription_set_remove_subscription( + struct aws_mqtt_subscription_set *subscription_set, + struct aws_byte_cursor topic_filter) { + + aws_hash_table_remove(&subscription_set->subscriptions, &topic_filter, NULL, NULL); + + if (!aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, topic_filter)) { + return; + } + + struct aws_mqtt_subscription_set_topic_tree_node *current_node = subscription_set->root; + + struct aws_byte_cursor topic_segment; + AWS_ZERO_STRUCT(topic_segment); + while (aws_byte_cursor_next_split(&topic_filter, '/', &topic_segment)) { + --current_node->ref_count; + + if (current_node->ref_count == 0) { + s_aws_mqtt_subscription_set_node_destroy_tree(current_node); + return; + } + + struct aws_hash_element *hash_element = NULL; + aws_hash_table_find(¤t_node->children, &topic_segment, &hash_element); + + /* We previously validated the full path; this must exist */ + current_node = hash_element->value; + } + + --current_node->ref_count; + if (current_node->ref_count == 0) { + s_aws_mqtt_subscription_set_node_destroy_tree(current_node); + return; + } + + if (current_node->on_cleanup) { + (*current_node->on_cleanup)(current_node->callback_user_data); + current_node->on_cleanup = NULL; + } + + current_node->on_publish_received = NULL; + current_node->is_subscription = false; +} + +struct aws_mqtt_subscription_set_path_continuation { + struct aws_byte_cursor current_fragment; + struct aws_mqtt_subscription_set_topic_tree_node *current_node; +}; + +static void s_add_subscription_set_path_continuation( + struct aws_array_list *paths, + struct aws_byte_cursor fragment, + struct aws_mqtt_subscription_set_topic_tree_node *node) { + if (node == NULL) { + return; + } + + struct aws_mqtt_subscription_set_path_continuation path = { + .current_fragment = fragment, + .current_node = node, + }; + + aws_array_list_push_back(paths, &path); +} + +#define SUBSCRIPTION_SET_PATH_FRAGMENT_DEFAULT 10 + +AWS_STATIC_STRING_FROM_LITERAL(s_single_level_wildcard, "+"); +AWS_STATIC_STRING_FROM_LITERAL(s_multi_level_wildcard, "#"); + +static struct aws_mqtt_subscription_set_topic_tree_node *s_aws_mqtt_subscription_set_node_find_child( + struct aws_mqtt_subscription_set_topic_tree_node *node, + struct aws_byte_cursor fragment) { + struct aws_hash_element *element = NULL; + aws_hash_table_find(&node->children, &fragment, &element); + + if (element == NULL) { + return NULL; + } + + return element->value; +} + +static void s_invoke_on_publish_received( + struct aws_mqtt_subscription_set_topic_tree_node *node, + const struct aws_mqtt_subscription_set_publish_received_options *publish_options) { + if (node == NULL || !node->is_subscription || node->on_publish_received == NULL) { + return; + } + + (*node->on_publish_received)( + publish_options->connection, + &publish_options->topic, + &publish_options->payload, + publish_options->dup, + publish_options->qos, + publish_options->retain, + node->callback_user_data); +} + +void aws_mqtt_subscription_set_on_publish_received( + const struct aws_mqtt_subscription_set *subscription_set, + const struct aws_mqtt_subscription_set_publish_received_options *publish_options) { + + struct aws_byte_cursor slw_cursor = aws_byte_cursor_from_string(s_single_level_wildcard); + struct aws_byte_cursor mlw_cursor = aws_byte_cursor_from_string(s_multi_level_wildcard); + + struct aws_array_list tree_paths; + aws_array_list_init_dynamic( + &tree_paths, + subscription_set->allocator, + SUBSCRIPTION_SET_PATH_FRAGMENT_DEFAULT, + sizeof(struct aws_mqtt_subscription_set_path_continuation)); + + struct aws_byte_cursor empty_cursor; + AWS_ZERO_STRUCT(empty_cursor); + s_add_subscription_set_path_continuation(&tree_paths, empty_cursor, subscription_set->root); + + while (aws_array_list_length(&tree_paths) > 0) { + struct aws_mqtt_subscription_set_path_continuation path_continuation; + AWS_ZERO_STRUCT(path_continuation); + + size_t path_count = aws_array_list_length(&tree_paths); + aws_array_list_get_at(&tree_paths, &path_continuation, path_count - 1); + aws_array_list_pop_back(&tree_paths); + + /* + * Invoke multi-level wildcard check before checking split result; this allows a subscription like + * 'a/b/#' to match an incoming 'a/b' + */ + struct aws_mqtt_subscription_set_topic_tree_node *mlw_node = + s_aws_mqtt_subscription_set_node_find_child(path_continuation.current_node, mlw_cursor); + s_invoke_on_publish_received(mlw_node, publish_options); + + struct aws_byte_cursor next_fragment = path_continuation.current_fragment; + if (!aws_byte_cursor_next_split(&publish_options->topic, '/', &next_fragment)) { + s_invoke_on_publish_received(path_continuation.current_node, publish_options); + continue; + } + + struct aws_mqtt_subscription_set_topic_tree_node *slw_node = + s_aws_mqtt_subscription_set_node_find_child(path_continuation.current_node, slw_cursor); + s_add_subscription_set_path_continuation(&tree_paths, next_fragment, slw_node); + + struct aws_mqtt_subscription_set_topic_tree_node *fragment_node = + s_aws_mqtt_subscription_set_node_find_child(path_continuation.current_node, next_fragment); + s_add_subscription_set_path_continuation(&tree_paths, next_fragment, fragment_node); + } + + aws_array_list_clean_up(&tree_paths); +} + +static int s_subscription_set_subscriptions_hash_get_wrap(void *context, struct aws_hash_element *elem) { + struct aws_array_list *subscriptions = context; + struct aws_mqtt_subscription_set_subscription_record *record = elem->value; + + aws_array_list_push_back(subscriptions, &record->subscription_view); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +void aws_mqtt_subscription_set_get_subscriptions( + struct aws_mqtt_subscription_set *subscription_set, + struct aws_array_list *subscriptions) { + AWS_ZERO_STRUCT(*subscriptions); + + size_t subscription_count = aws_hash_table_get_entry_count(&subscription_set->subscriptions); + aws_array_list_init_dynamic( + subscriptions, + subscription_set->allocator, + subscription_count, + sizeof(struct aws_mqtt_subscription_set_subscription_options)); + + aws_hash_table_foreach( + &subscription_set->subscriptions, s_subscription_set_subscriptions_hash_get_wrap, subscriptions); +} diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 6e3f6c8b..74afcb6b 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #define AWS_MQTT5_IO_MESSAGE_DEFAULT_LENGTH 4096 #define AWS_MQTT5_DEFAULT_CONNACK_PACKET_TIMEOUT_MS 10000 +#define DEFAULT_MQTT5_OPERATION_TABLE_SIZE 200 const char *aws_mqtt5_client_state_to_c_string(enum aws_mqtt5_client_state state) { switch (state) { @@ -156,14 +158,6 @@ static int s_aws_mqtt5_client_change_desired_state( enum aws_mqtt5_client_state desired_state, struct aws_mqtt5_operation_disconnect *disconnect_operation); -static uint64_t s_hash_uint16_t(const void *item) { - return *(uint16_t *)item; -} - -static bool s_uint16_t_eq(const void *a, const void *b) { - return *(uint16_t *)a == *(uint16_t *)b; -} - static uint64_t s_aws_mqtt5_client_compute_operational_state_service_time( const struct aws_mqtt5_client_operational_state *client_operational_state, uint64_t now); @@ -2314,51 +2308,51 @@ struct aws_mqtt5_submit_operation_task { struct aws_mqtt5_operation *operation; }; -static void s_mqtt5_submit_operation_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { - (void)task; - - int completion_error_code = AWS_ERROR_MQTT5_CLIENT_TERMINATED; - struct aws_mqtt5_submit_operation_task *submit_operation_task = arg; +void aws_mqtt5_client_submit_operation_internal( + struct aws_mqtt5_client *client, + struct aws_mqtt5_operation *operation, + bool is_terminated) { /* * Take a ref to the operation that represents the client taking ownership * If we subsequently reject it (task cancel or offline queue policy), then the operation completion * will undo this ref acquisition. */ - aws_mqtt5_operation_acquire(submit_operation_task->operation); + aws_mqtt5_operation_acquire(operation); - if (status != AWS_TASK_STATUS_RUN_READY) { - goto error; + if (is_terminated) { + s_complete_operation(NULL, operation, AWS_ERROR_MQTT5_CLIENT_TERMINATED, AWS_MQTT5_PT_NONE, NULL); + return; } /* * If we're offline and this operation doesn't meet the requirements of the offline queue retention policy, * fail it immediately. */ - struct aws_mqtt5_client *client = submit_operation_task->client; - struct aws_mqtt5_operation *operation = submit_operation_task->operation; if (client->current_state != AWS_MCS_CONNECTED) { if (!s_aws_mqtt5_operation_satisfies_offline_queue_retention_policy( operation, client->config->offline_queue_behavior)) { - completion_error_code = AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY; - goto error; + s_complete_operation( + NULL, operation, AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, AWS_MQTT5_PT_NONE, NULL); + return; } } /* newly-submitted operations must have a 0 packet id */ - aws_mqtt5_operation_set_packet_id(submit_operation_task->operation, 0); - - s_enqueue_operation_back(submit_operation_task->client, submit_operation_task->operation); - aws_mqtt5_client_statistics_change_operation_statistic_state( - submit_operation_task->client, submit_operation_task->operation, AWS_MQTT5_OSS_INCOMPLETE); + aws_mqtt5_operation_set_packet_id(operation, 0); - goto done; + s_enqueue_operation_back(client, operation); + aws_mqtt5_client_statistics_change_operation_statistic_state(client, operation, AWS_MQTT5_OSS_INCOMPLETE); +} -error: +static void s_mqtt5_submit_operation_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; - s_complete_operation(NULL, submit_operation_task->operation, completion_error_code, AWS_MQTT5_PT_NONE, NULL); + struct aws_mqtt5_submit_operation_task *submit_operation_task = arg; + struct aws_mqtt5_client *client = submit_operation_task->client; + struct aws_mqtt5_operation *operation = submit_operation_task->operation; -done: + aws_mqtt5_client_submit_operation_internal(client, operation, status != AWS_TASK_STATUS_RUN_READY); aws_mqtt5_operation_release(submit_operation_task->operation); aws_mqtt5_client_release(submit_operation_task->client); @@ -2544,9 +2538,9 @@ int aws_mqtt5_client_operational_state_init( if (aws_hash_table_init( &client_operational_state->unacked_operations_table, allocator, - sizeof(struct aws_mqtt5_operation *), - s_hash_uint16_t, - s_uint16_t_eq, + DEFAULT_MQTT5_OPERATION_TABLE_SIZE, + aws_mqtt_hash_uint16_t, + aws_mqtt_compare_uint16_t_eq, NULL, NULL)) { return AWS_OP_ERR; diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c new file mode 100644 index 00000000..3f91974c --- /dev/null +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -0,0 +1,3030 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include + +/* + * A best-effort-but-not-100%-accurate translation from mqtt5 error codes to mqtt311 error codes. + */ +static int s_translate_mqtt5_error_code_to_mqtt311(int error_code) { + switch (error_code) { + case AWS_ERROR_MQTT5_ENCODE_FAILURE: + case AWS_ERROR_MQTT5_DECODE_PROTOCOL_ERROR: + return AWS_ERROR_MQTT_PROTOCOL_ERROR; + + case AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED: + return AWS_ERROR_MQTT_PROTOCOL_ERROR; /* a decidedly strange choice by the 311 implementation */ + + case AWS_ERROR_MQTT5_CONNACK_TIMEOUT: + case AWS_ERROR_MQTT5_PING_RESPONSE_TIMEOUT: + return AWS_ERROR_MQTT_TIMEOUT; + + case AWS_ERROR_MQTT5_USER_REQUESTED_STOP: + case AWS_ERROR_MQTT5_CLIENT_TERMINATED: + return AWS_IO_SOCKET_CLOSED; + + case AWS_ERROR_MQTT5_DISCONNECT_RECEIVED: + return AWS_ERROR_MQTT_UNEXPECTED_HANGUP; + + case AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY: + return AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION; + + case AWS_ERROR_MQTT5_ENCODE_SIZE_UNSUPPORTED_PACKET_TYPE: + return AWS_ERROR_MQTT_INVALID_PACKET_TYPE; + + case AWS_ERROR_MQTT5_OPERATION_PROCESSING_FAILURE: + return AWS_ERROR_MQTT_PROTOCOL_ERROR; + + case AWS_ERROR_MQTT5_INVALID_UTF8_STRING: + return AWS_ERROR_MQTT_INVALID_TOPIC; + + default: + return error_code; + } +} + +struct aws_mqtt_adapter_final_destroy_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection *connection; +}; + +static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + (void)status; + + struct aws_mqtt_adapter_final_destroy_task *destroy_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = destroy_task->connection->impl; + + AWS_LOGF_DEBUG(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: Final destruction of mqtt3-to-5 adapter", (void *)adapter); + + if (adapter->client->config->websocket_handshake_transform_user_data == adapter) { + /* + * If the mqtt5 client is pointing to us for websocket transform, then erase that. The callback + * is invoked from our pinned event loop so this is safe. + * + * TODO: It is possible that multiple adapters may have sequentially side-affected the websocket handshake. + * For now, in that case, subsequent connection attempts will probably not succeed. + */ + adapter->client->config->websocket_handshake_transform = NULL; + adapter->client->config->websocket_handshake_transform_user_data = NULL; + } + + aws_mqtt_subscription_set_destroy(adapter->subscriptions); + aws_mqtt5_to_mqtt3_adapter_operation_table_clean_up(&adapter->operational_state); + + adapter->client = aws_mqtt5_client_release(adapter->client); + aws_rw_lock_clean_up(&adapter->lock); + + aws_mem_release(adapter->allocator, adapter); + + aws_mem_release(destroy_task->allocator, destroy_task); +} + +static struct aws_mqtt_adapter_final_destroy_task *s_aws_mqtt_adapter_final_destroy_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter) { + + struct aws_mqtt_adapter_final_destroy_task *destroy_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_final_destroy_task)); + + aws_task_init( + &destroy_task->task, s_mqtt_adapter_final_destroy_task_fn, (void *)destroy_task, "MqttAdapterFinalDestroy"); + destroy_task->allocator = adapter->allocator; + destroy_task->connection = &adapter->base; /* Do not acquire, we're at zero external and internal ref counts */ + + return destroy_task; +} + +static void s_aws_mqtt_adapter_final_destroy(struct aws_mqtt_client_connection_5_impl *adapter) { + + struct aws_mqtt_adapter_final_destroy_task *task = + s_aws_mqtt_adapter_final_destroy_task_new(adapter->allocator, adapter); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create adapter final destroy task, last_error: %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); +} + +typedef int (*adapter_callback_fn)(struct aws_mqtt_client_connection_5_impl *adapter, void *context); + +/* + * The state/ref-count lock is held during synchronous callbacks to prevent invoking into something that is in the + * process of destruction. In general this isn't a performance worry since callbacks are invoked from a single thread: + * the event loop that the client and adapter are seated on. + * + * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown + * process since if we naively always locked, then an adapter release from within a callback would deadlock. + * + * We need a way to tell if locking will result in a deadlock. The specific case is invoking a synchronous + * callback from the event loop that re-enters the adapter logic via releasing the connection. We can recognize + * this scenario by setting/clearing an internal flag (in_synchronous_callback) and checking it only if we're + * in the event loop thread. If it's true, we know we've already locked at the beginning of the synchronous callback + * and we can safely skip locking, otherwise we must lock. + * + * This function gives us a helper for making these kinds of safe callbacks. We use it in: + * (1) Releasing the connection + * (2) Websocket handshake transform + * (3) Making lifecycle and operation callbacks on the mqtt311 interface + * + * It works by + * (1) Correctly determining if locking would deadlock and skipping lock only in that case, otherwise locking + * (2) Invoke the callback + * (3) Unlock if we locked in step (1) + * + * It also properly sets/clears the in_synchronous_callback flag if we're in the event loop and are not in + * a callback already. + */ +static int s_aws_mqtt5_adapter_perform_safe_callback( + struct aws_mqtt_client_connection_5_impl *adapter, + bool use_write_lock, + adapter_callback_fn callback_fn, + void *callback_user_data) { + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5 adapter performing safe user callback", (void *)adapter); + + /* Step (1) - conditionally lock and manipulate the in_synchronous_callback flag */ + bool should_unlock = true; + bool clear_synchronous_callback_flag = false; + if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { + if (adapter->in_synchronous_callback) { + should_unlock = false; + } else { + adapter->in_synchronous_callback = true; + clear_synchronous_callback_flag = true; + } + } + + if (should_unlock) { + if (use_write_lock) { + aws_rw_lock_wlock(&adapter->lock); + } else { + aws_rw_lock_rlock(&adapter->lock); + } + } + + // Step (2) - perform the callback + int result = (*callback_fn)(adapter, callback_user_data); + + // Step (3) - undo anything we did in step (1) + if (should_unlock) { + if (use_write_lock) { + aws_rw_lock_wunlock(&adapter->lock); + } else { + aws_rw_lock_runlock(&adapter->lock); + } + } + + if (clear_synchronous_callback_flag) { + adapter->in_synchronous_callback = false; + } + + return result; +} + +struct aws_mqtt_adapter_disconnect_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_disconnect_fn *on_disconnect; + void *on_disconnect_user_data; +}; + +static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); + +static struct aws_mqtt_adapter_disconnect_task *s_aws_mqtt_adapter_disconnect_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_disconnect_fn *on_disconnect, + void *on_disconnect_user_data) { + + struct aws_mqtt_adapter_disconnect_task *disconnect_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_disconnect_task)); + + aws_task_init( + &disconnect_task->task, s_adapter_disconnect_task_fn, (void *)disconnect_task, "AdapterDisconnectTask"); + disconnect_task->allocator = adapter->allocator; + disconnect_task->adapter = + (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + disconnect_task->on_disconnect = on_disconnect; + disconnect_task->on_disconnect_user_data = on_disconnect_user_data; + + return disconnect_task; +} + +static int s_aws_mqtt_client_connection_5_disconnect( + void *impl, + aws_mqtt_client_on_disconnect_fn *on_disconnect, + void *on_disconnect_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_adapter_disconnect_task *task = + s_aws_mqtt_adapter_disconnect_task_new(adapter->allocator, adapter, on_disconnect, on_disconnect_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create adapter disconnect task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_adapter_connect_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_byte_buf host_name; + uint16_t port; + struct aws_socket_options socket_options; + struct aws_tls_connection_options *tls_options_ptr; + struct aws_tls_connection_options tls_options; + + struct aws_byte_buf client_id; + uint16_t keep_alive_time_secs; + uint32_t ping_timeout_ms; + uint32_t protocol_operation_timeout_ms; + aws_mqtt_client_on_connection_complete_fn *on_connection_complete; + void *on_connection_complete_user_data; + bool clean_session; +}; + +static void s_aws_mqtt_adapter_connect_task_destroy(struct aws_mqtt_adapter_connect_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up(&task->host_name); + aws_byte_buf_clean_up(&task->client_id); + + if (task->tls_options_ptr) { + aws_tls_connection_options_clean_up(task->tls_options_ptr); + } + + aws_mem_release(task->allocator, task); +} + +static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status); + +static struct aws_mqtt_adapter_connect_task *s_aws_mqtt_adapter_connect_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_mqtt_connection_options *connection_options) { + + struct aws_mqtt_adapter_connect_task *connect_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_adapter_connect_task)); + + aws_task_init(&connect_task->task, s_adapter_connect_task_fn, (void *)connect_task, "AdapterConnectTask"); + connect_task->allocator = adapter->allocator; + connect_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + aws_byte_buf_init_copy_from_cursor(&connect_task->host_name, allocator, connection_options->host_name); + connect_task->port = connection_options->port; + connect_task->socket_options = *connection_options->socket_options; + if (connection_options->tls_options) { + aws_tls_connection_options_copy(&connect_task->tls_options, connection_options->tls_options); + connect_task->tls_options_ptr = &connect_task->tls_options; + } + + aws_byte_buf_init_copy_from_cursor(&connect_task->client_id, allocator, connection_options->client_id); + + connect_task->keep_alive_time_secs = connection_options->keep_alive_time_secs; + connect_task->ping_timeout_ms = connection_options->ping_timeout_ms; + connect_task->protocol_operation_timeout_ms = connection_options->protocol_operation_timeout_ms; + connect_task->on_connection_complete = connection_options->on_connection_complete; + connect_task->on_connection_complete_user_data = connection_options->user_data; + connect_task->clean_session = connection_options->clean_session; + + return connect_task; +} + +static int s_validate_adapter_connection_options( + const struct aws_mqtt_connection_options *connection_options, + struct aws_mqtt_client_connection_5_impl *adapter) { + if (connection_options == NULL) { + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (connection_options->host_name.len == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - host name not set in MQTT client configuration", + (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + /* forbid no-timeout until someone convinces me otherwise */ + if (connection_options->socket_options != NULL) { + if (connection_options->socket_options->type == AWS_SOCKET_DGRAM || + connection_options->socket_options->connect_timeout_ms == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - invalid socket options in MQTT client configuration", + (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (!aws_mqtt5_client_keep_alive_options_are_valid( + connection_options->keep_alive_time_secs, connection_options->ping_timeout_ms)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - keep alive interval is too small relative to ping timeout interval", + (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt_client_connection_5_connect( + void *impl, + const struct aws_mqtt_connection_options *connection_options) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + /* The client will not behave properly if ping timeout is not significantly shorter than the keep alive interval */ + if (s_validate_adapter_connection_options(connection_options, adapter)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_adapter_connect_task *task = + s_aws_mqtt_adapter_connect_task_new(adapter->allocator, adapter, connection_options); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - failed to create adapter connect task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_safe_lifecycle_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + const struct aws_mqtt5_client_lifecycle_event *event = context; + + /* + * Never invoke a callback after termination + */ + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + switch (event->event_type) { + + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - received on connection success event from mqtt5 client, adapter in state " + "(%d)", + (void *)adapter, + (int)adapter->adapter_state); + if (adapter->adapter_state != AWS_MQTT_AS_STAY_DISCONNECTED) { + if (adapter->on_connection_success != NULL) { + (*adapter->on_connection_success)( + &adapter->base, 0, event->settings->rejoined_session, adapter->on_connection_success_user_data); + } + + if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + /* + * If the 311 view is that this is an initial connection attempt, then invoke the completion + * callback and move to the stay-connected state. + */ + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, + event->error_code, + 0, + event->settings->rejoined_session, + adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + adapter->adapter_state = AWS_MQTT_AS_STAY_CONNECTED; + } else if (adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED) { + /* + * If the 311 view is that we're in the stay-connected state (ie we've successfully done or + * simulated an initial connection), then invoke the connection resumption callback. + */ + if (adapter->on_resumed != NULL) { + (*adapter->on_resumed)( + &adapter->base, 0, event->settings->rejoined_session, adapter->on_resumed_user_data); + } + } + } + break; + + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - received on connection failure event from mqtt5 client, adapter in state " + "(%d)", + (void *)adapter, + (int)adapter->adapter_state); + + /* + * The MQTT311 interface only cares about connection failures when it's the initial connection attempt + * after a call to connect(). Since an adapter connect() can sever an existing connection (with an + * error code of AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) we only react to connection failures + * if + * (1) the error code is not AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT and + * (2) we're in the FIRST_CONNECT state + * + * Only if both of these are true should we invoke the connection completion callback with a failure and + * put the adapter into the "disconnected" state, simulating the way the 311 client stops after an + * initial connection failure. + */ + if (event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) { + if (adapter->adapter_state != AWS_MQTT_AS_STAY_DISCONNECTED) { + int mqtt311_error_code = s_translate_mqtt5_error_code_to_mqtt311(event->error_code); + + if (adapter->on_connection_failure != NULL) { + (*adapter->on_connection_failure)( + &adapter->base, mqtt311_error_code, adapter->on_connection_failure_user_data); + } + + if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, + mqtt311_error_code, + 0, + false, + adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + } + } + } + + break; + + case AWS_MQTT5_CLET_DISCONNECTION: + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - received on disconnection event from mqtt5 client, adapter in state (%d), " + "error code (%d)", + (void *)adapter, + (int)adapter->adapter_state, + event->error_code); + /* + * If the 311 view is that we're in the stay-connected state (ie we've successfully done or simulated + * an initial connection), then invoke the connection interrupted callback. + */ + if (adapter->on_interrupted != NULL && adapter->adapter_state == AWS_MQTT_AS_STAY_CONNECTED && + event->error_code != AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT) { + + (*adapter->on_interrupted)( + &adapter->base, + s_translate_mqtt5_error_code_to_mqtt311(event->error_code), + adapter->on_interrupted_user_data); + } + break; + + case AWS_MQTT5_CLET_STOPPED: + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - received on stopped event from mqtt5 client, adapter in state (%d)", + (void *)adapter, + (int)adapter->adapter_state); + + /* If an MQTT311-view user is waiting on a disconnect callback, invoke it */ + if (adapter->on_disconnect) { + (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); + + adapter->on_disconnect = NULL; + adapter->on_disconnect_user_data = NULL; + } + + if (adapter->on_closed) { + (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); + } + + /* + * Judgement call: If the mqtt5 client is stopped behind our back, it seems better to transition to the + * disconnected state (which only requires a connect() to restart) then stay in the STAY_CONNECTED state + * which currently requires a disconnect() and then a connect() to restore connectivity. + * + * ToDo: what if we disabled mqtt5 client start/stop somehow while the adapter is attached, preventing + * the potential to backstab each other? Unfortunately neither start() nor stop() have an error reporting + * mechanism. + */ + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + break; + + default: + break; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_client_lifecycle_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { + struct aws_mqtt_client_connection_5_impl *adapter = event->user_data; + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_lifecycle_handler, (void *)event); +} + +static int s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + struct aws_mqtt_adapter_disconnect_task *disconnect_task = context; + + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - performing disconnect safe callback, adapter in state (%d)", + (void *)adapter, + (int)adapter->adapter_state); + + /* + * If we're already disconnected (from the 311 perspective only), then invoke the callback and return + */ + if (adapter->adapter_state == AWS_MQTT_AS_STAY_DISCONNECTED) { + if (disconnect_task->on_disconnect) { + (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); + } + + return AWS_OP_SUCCESS; + } + + /* + * If we had a pending first connect, then notify failure + */ + if (adapter->adapter_state == AWS_MQTT_AS_FIRST_CONNECT) { + if (adapter->on_connection_complete != NULL) { + (*adapter->on_connection_complete)( + &adapter->base, + AWS_ERROR_MQTT_CONNECTION_SHUTDOWN, + 0, + false, + adapter->on_connection_complete_user_data); + + adapter->on_connection_complete = NULL; + adapter->on_connection_complete_user_data = NULL; + } + } + + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + + bool invoke_callbacks = true; + if (adapter->client->desired_state != AWS_MCS_STOPPED) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - disconnect forwarding stop request to mqtt5 client", + (void *)adapter); + + aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_STOPPED, NULL); + + adapter->on_disconnect = disconnect_task->on_disconnect; + adapter->on_disconnect_user_data = disconnect_task->on_disconnect_user_data; + invoke_callbacks = false; + } + + if (invoke_callbacks) { + if (disconnect_task->on_disconnect != NULL) { + (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); + } + + if (adapter->on_closed) { + (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); + } + } + + return AWS_OP_SUCCESS; +} + +static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_adapter_disconnect_task *disconnect_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = disconnect_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler, disconnect_task); + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(disconnect_task->allocator, disconnect_task); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_update_config_on_connect( + struct aws_mqtt_client_connection_5_impl *adapter, + struct aws_mqtt_adapter_connect_task *connect_task) { + struct aws_mqtt5_client_options_storage *config = adapter->client->config; + + aws_string_destroy(config->host_name); + config->host_name = aws_string_new_from_buf(adapter->allocator, &connect_task->host_name); + config->port = connect_task->port; + config->socket_options = connect_task->socket_options; + + if (config->tls_options_ptr) { + aws_tls_connection_options_clean_up(&config->tls_options); + config->tls_options_ptr = NULL; + } + + if (connect_task->tls_options_ptr) { + aws_tls_connection_options_copy(&config->tls_options, connect_task->tls_options_ptr); + config->tls_options_ptr = &config->tls_options; + } + + aws_byte_buf_clean_up(&adapter->client->negotiated_settings.client_id_storage); + aws_byte_buf_init_copy_from_cursor( + &adapter->client->negotiated_settings.client_id_storage, + adapter->allocator, + aws_byte_cursor_from_buf(&connect_task->client_id)); + + config->connect->storage_view.keep_alive_interval_seconds = connect_task->keep_alive_time_secs; + config->ping_timeout_ms = connect_task->ping_timeout_ms; + + /* Override timeout, rounding up as necessary */ + config->ack_timeout_seconds = aws_timestamp_convert( + connect_task->protocol_operation_timeout_ms + AWS_TIMESTAMP_MILLIS - 1, + AWS_TIMESTAMP_MILLIS, + AWS_TIMESTAMP_SECS, + NULL); + + if (connect_task->clean_session) { + config->session_behavior = AWS_MQTT5_CSBT_CLEAN; + config->connect->storage_view.session_expiry_interval_seconds = NULL; + } else { + config->session_behavior = AWS_MQTT5_CSBT_REJOIN_ALWAYS; + /* This is a judgement call to translate session expiry to the maximum possible allowed by AWS IoT Core */ + config->connect->session_expiry_interval_seconds = 7 * 24 * 60 * 60; + config->connect->storage_view.session_expiry_interval_seconds = + &config->connect->session_expiry_interval_seconds; + } +} + +static int s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler( + struct aws_mqtt_client_connection_5_impl *adapter, + void *context) { + struct aws_mqtt_adapter_connect_task *connect_task = context; + + if (adapter->synced_data.terminated) { + return AWS_OP_SUCCESS; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - performing connect safe callback, adapter in state (%d)", + (void *)adapter, + (int)adapter->adapter_state); + + if (adapter->adapter_state != AWS_MQTT_AS_STAY_DISCONNECTED) { + if (connect_task->on_connection_complete) { + (*connect_task->on_connection_complete)( + &adapter->base, + AWS_ERROR_MQTT_ALREADY_CONNECTED, + 0, + false, + connect_task->on_connection_complete_user_data); + } + return AWS_OP_SUCCESS; + } + + if (adapter->on_disconnect) { + (*adapter->on_disconnect)(&adapter->base, adapter->on_disconnect_user_data); + + adapter->on_disconnect = NULL; + adapter->on_disconnect_user_data = NULL; + } + + adapter->adapter_state = AWS_MQTT_AS_FIRST_CONNECT; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - resetting mqtt5 client connection and requesting start", + (void *)adapter); + + /* Update mqtt5 config */ + s_aws_mqtt5_to_mqtt3_adapter_update_config_on_connect(adapter, connect_task); + + aws_mqtt5_client_reset_connection(adapter->client); + + aws_mqtt5_client_change_desired_state(adapter->client, AWS_MCS_CONNECTED, NULL); + + adapter->on_connection_complete = connect_task->on_connection_complete; + adapter->on_connection_complete_user_data = connect_task->on_connection_complete_user_data; + + return AWS_OP_SUCCESS; +} + +static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_adapter_connect_task *connect_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = connect_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + s_aws_mqtt5_adapter_perform_safe_callback( + adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler, connect_task); + +done: + + aws_ref_count_release(&adapter->internal_refs); + + s_aws_mqtt_adapter_connect_task_destroy(connect_task); +} + +static bool s_aws_mqtt5_listener_publish_received_adapter( + const struct aws_mqtt5_packet_publish_view *publish, + void *user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = user_data; + struct aws_mqtt_client_connection *connection = &adapter->base; + + struct aws_mqtt_subscription_set_publish_received_options incoming_publish_options = { + .connection = connection, + .topic = publish->topic, + .qos = (enum aws_mqtt_qos)publish->qos, + .retain = publish->retain, + .dup = publish->duplicate, + .payload = publish->payload, + }; + + aws_mqtt_subscription_set_on_publish_received(adapter->subscriptions, &incoming_publish_options); + + if (adapter->on_any_publish) { + (*adapter->on_any_publish)( + connection, + &publish->topic, + &publish->payload, + publish->duplicate, + (enum aws_mqtt_qos)publish->qos, + publish->retain, + adapter->on_any_publish_user_data); + } + + return false; +} + +struct aws_mqtt_set_interruption_handlers_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; + void *on_interrupted_user_data; + + aws_mqtt_client_on_connection_resumed_fn *on_resumed; + void *on_resumed_user_data; +}; + +static void s_set_interruption_handlers_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_interruption_handlers_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->on_interrupted = set_task->on_interrupted; + adapter->on_interrupted_user_data = set_task->on_interrupted_user_data; + adapter->on_resumed = set_task->on_resumed; + adapter->on_resumed_user_data = set_task->on_resumed_user_data; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_interruption_handlers_task *s_aws_mqtt_set_interruption_handlers_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_user_data, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_user_data) { + + struct aws_mqtt_set_interruption_handlers_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_interruption_handlers_task)); + + aws_task_init( + &set_task->task, s_set_interruption_handlers_task_fn, (void *)set_task, "SetInterruptionHandlersTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->on_interrupted = on_interrupted; + set_task->on_interrupted_user_data = on_interrupted_user_data; + set_task->on_resumed = on_resumed; + set_task->on_resumed_user_data = on_resumed_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_interruption_handlers( + void *impl, + aws_mqtt_client_on_connection_interrupted_fn *on_interrupted, + void *on_interrupted_user_data, + aws_mqtt_client_on_connection_resumed_fn *on_resumed, + void *on_resumed_user_data) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_interruption_handlers_task *task = s_aws_mqtt_set_interruption_handlers_task_new( + adapter->allocator, adapter, on_interrupted, on_interrupted_user_data, on_resumed, on_resumed_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set interruption handlers task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_connection_result_handlers_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_connection_success_fn *on_connection_success; + void *on_connection_success_user_data; + + aws_mqtt_client_on_connection_failure_fn *on_connection_failure; + void *on_connection_failure_user_data; +}; + +static void s_set_connection_result_handlers_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_connection_result_handlers_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->on_connection_success = set_task->on_connection_success; + adapter->on_connection_success_user_data = set_task->on_connection_success_user_data; + adapter->on_connection_failure = set_task->on_connection_failure; + adapter->on_connection_failure_user_data = set_task->on_connection_failure_user_data; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_connection_result_handlers_task *s_aws_mqtt_set_connection_result_handlers_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_user_data, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_user_data) { + + struct aws_mqtt_set_connection_result_handlers_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_connection_result_handlers_task)); + + aws_task_init( + &set_task->task, s_set_connection_result_handlers_task_fn, (void *)set_task, "SetConnectionResultHandlersTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->on_connection_success = on_connection_success; + set_task->on_connection_success_user_data = on_connection_success_user_data; + set_task->on_connection_failure = on_connection_failure; + set_task->on_connection_failure_user_data = on_connection_failure_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_connection_result_handlers( + void *impl, + aws_mqtt_client_on_connection_success_fn *on_connection_success, + void *on_connection_success_user_data, + aws_mqtt_client_on_connection_failure_fn *on_connection_failure, + void *on_connection_failure_user_data) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_connection_result_handlers_task *task = s_aws_mqtt_set_connection_result_handlers_task_new( + adapter->allocator, + adapter, + on_connection_success, + on_connection_success_user_data, + on_connection_failure, + on_connection_failure_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set connection result handlers task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_on_closed_handler_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_connection_closed_fn *on_closed; + void *on_closed_user_data; +}; + +static void s_set_on_closed_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_on_closed_handler_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->on_closed = set_task->on_closed; + adapter->on_closed_user_data = set_task->on_closed_user_data; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_on_closed_handler_task *s_aws_mqtt_set_on_closed_handler_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_user_data) { + + struct aws_mqtt_set_on_closed_handler_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_closed_handler_task)); + + aws_task_init(&set_task->task, s_set_on_closed_handler_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->on_closed = on_closed; + set_task->on_closed_user_data = on_closed_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_on_closed_handler( + void *impl, + aws_mqtt_client_on_connection_closed_fn *on_closed, + void *on_closed_user_data) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_on_closed_handler_task *task = + s_aws_mqtt_set_on_closed_handler_task_new(adapter->allocator, adapter, on_closed, on_closed_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set on closed handler task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_on_any_publish_handler_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_publish_received_fn *on_any_publish; + void *on_any_publish_user_data; +}; + +static void s_set_on_any_publish_handler_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_on_any_publish_handler_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->on_any_publish = set_task->on_any_publish; + adapter->on_any_publish_user_data = set_task->on_any_publish_user_data; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_on_any_publish_handler_task *s_aws_mqtt_set_on_any_publish_handler_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_user_data) { + + struct aws_mqtt_set_on_any_publish_handler_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_any_publish_handler_task)); + + aws_task_init( + &set_task->task, s_set_on_any_publish_handler_task_fn, (void *)set_task, "SetOnAnyPublishHandlerTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->on_any_publish = on_any_publish; + set_task->on_any_publish_user_data = on_any_publish_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_on_any_publish_handler( + void *impl, + aws_mqtt_client_publish_received_fn *on_any_publish, + void *on_any_publish_user_data) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_on_any_publish_handler_task *task = s_aws_mqtt_set_on_any_publish_handler_task_new( + adapter->allocator, adapter, on_any_publish, on_any_publish_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set on any publish task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_reconnect_timeout_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + uint64_t min_timeout; + uint64_t max_timeout; +}; + +static void s_set_reconnect_timeout_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_reconnect_timeout_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + adapter->client->config->min_reconnect_delay_ms = set_task->min_timeout; + adapter->client->config->max_reconnect_delay_ms = set_task->max_timeout; + adapter->client->current_reconnect_delay_ms = set_task->min_timeout; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_reconnect_timeout_task *s_aws_mqtt_set_reconnect_timeout_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + uint64_t min_timeout, + uint64_t max_timeout) { + + struct aws_mqtt_set_reconnect_timeout_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_reconnect_timeout_task)); + + aws_task_init(&set_task->task, s_set_reconnect_timeout_task_fn, (void *)set_task, "SetReconnectTimeoutTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->min_timeout = aws_min_u64(min_timeout, max_timeout); + set_task->max_timeout = aws_max_u64(min_timeout, max_timeout); + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_reconnect_timeout( + void *impl, + uint64_t min_timeout, + uint64_t max_timeout) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_reconnect_timeout_task *task = + s_aws_mqtt_set_reconnect_timeout_task_new(adapter->allocator, adapter, min_timeout, max_timeout); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set reconnect timeout task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_http_proxy_options_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_http_proxy_config *proxy_config; +}; + +static void s_set_http_proxy_options_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_http_proxy_options_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + aws_http_proxy_config_destroy(adapter->client->config->http_proxy_config); + + /* move the proxy config from the set task to the client's config */ + adapter->client->config->http_proxy_config = set_task->proxy_config; + if (adapter->client->config->http_proxy_config != NULL) { + aws_http_proxy_options_init_from_config( + &adapter->client->config->http_proxy_options, adapter->client->config->http_proxy_config); + } + + /* don't clean up the proxy config if it was successfully assigned to the mqtt5 client */ + set_task->proxy_config = NULL; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + /* If the task was canceled we need to clean this up because it didn't get assigned to the mqtt5 client */ + aws_http_proxy_config_destroy(set_task->proxy_config); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_http_proxy_options_task *s_aws_mqtt_set_http_proxy_options_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + struct aws_http_proxy_options *proxy_options) { + + struct aws_http_proxy_config *proxy_config = + aws_http_proxy_config_new_tunneling_from_proxy_options(allocator, proxy_options); + if (proxy_config == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_mqtt_set_http_proxy_options_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_http_proxy_options_task)); + + aws_task_init(&set_task->task, s_set_http_proxy_options_task_fn, (void *)set_task, "SetHttpProxyOptionsTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->proxy_config = proxy_config; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_http_proxy_options( + void *impl, + struct aws_http_proxy_options *proxy_options) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_http_proxy_options_task *task = + s_aws_mqtt_set_http_proxy_options_task_new(adapter->allocator, adapter, proxy_options); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set http proxy options task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_use_websockets_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_transform_websocket_handshake_fn *transformer; + void *transformer_user_data; +}; + +static void s_aws_mqtt5_adapter_websocket_handshake_completion_fn( + struct aws_http_message *request, + int error_code, + void *complete_ctx) { + + struct aws_mqtt_client_connection_5_impl *adapter = complete_ctx; + + (*adapter->mqtt5_websocket_handshake_completion_function)( + request, + s_translate_mqtt5_error_code_to_mqtt311(error_code), + adapter->mqtt5_websocket_handshake_completion_user_data); + + aws_ref_count_release(&adapter->internal_refs); +} + +struct aws_mqtt5_adapter_websocket_handshake_args { + bool chain_callback; + struct aws_http_message *input_request; + struct aws_http_message *output_request; + int completion_error_code; +}; + +static int s_safe_websocket_handshake_fn(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { + struct aws_mqtt5_adapter_websocket_handshake_args *args = context; + + if (adapter->synced_data.terminated) { + args->completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; + } else if (adapter->websocket_handshake_transformer == NULL) { + args->output_request = args->input_request; + } else { + aws_ref_count_acquire(&adapter->internal_refs); + args->chain_callback = true; + } + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( + struct aws_http_message *request, + void *user_data, + aws_mqtt5_transform_websocket_handshake_complete_fn *complete_fn, + void *complete_ctx) { + + struct aws_mqtt_client_connection_5_impl *adapter = user_data; + + struct aws_mqtt5_adapter_websocket_handshake_args args = { + .input_request = request, + }; + + s_aws_mqtt5_adapter_perform_safe_callback(adapter, false, s_safe_websocket_handshake_fn, &args); + + if (args.chain_callback) { + adapter->mqtt5_websocket_handshake_completion_function = complete_fn; + adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; + + (*adapter->websocket_handshake_transformer)( + request, user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); + } else { + (*complete_fn)(args.output_request, args.completion_error_code, complete_ctx); + } +} + +static void s_set_use_websockets_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_use_websockets_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->websocket_handshake_transformer = set_task->transformer; + adapter->websocket_handshake_transformer_user_data = set_task->transformer_user_data; + + /* we're in the mqtt5 client's event loop; it's safe to access its internal state */ + adapter->client->config->websocket_handshake_transform = s_aws_mqtt5_adapter_transform_websocket_handshake_fn; + adapter->client->config->websocket_handshake_transform_user_data = adapter; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_use_websockets_task *s_aws_mqtt_set_use_websockets_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_user_data) { + + struct aws_mqtt_set_use_websockets_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_use_websockets_task)); + + aws_task_init(&set_task->task, s_set_use_websockets_task_fn, (void *)set_task, "SetUseWebsocketsTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->transformer = transformer; + set_task->transformer_user_data = transformer_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_use_websockets( + void *impl, + aws_mqtt_transform_websocket_handshake_fn *transformer, + void *transformer_user_data, + aws_mqtt_validate_websocket_handshake_fn *validator, + void *validator_user_data) { + + /* mqtt5 doesn't use these */ + (void)validator; + (void)validator_user_data; + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_use_websockets_task *task = + s_aws_mqtt_set_use_websockets_task_new(adapter->allocator, adapter, transformer, transformer_user_data); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set use websockets task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_host_resolution_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_host_resolution_config host_resolution_config; +}; + +static void s_set_host_resolution_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_host_resolution_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + adapter->client->config->host_resolution_override = set_task->host_resolution_config; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_host_resolution_task *s_aws_mqtt_set_host_resolution_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_host_resolution_config *host_resolution_config) { + + struct aws_mqtt_set_host_resolution_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_host_resolution_task)); + + aws_task_init(&set_task->task, s_set_host_resolution_task_fn, (void *)set_task, "SetHostResolutionTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->host_resolution_config = *host_resolution_config; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_host_resolution_options( + void *impl, + const struct aws_host_resolution_config *host_resolution_config) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_host_resolution_task *task = + s_aws_mqtt_set_host_resolution_task_new(adapter->allocator, adapter, host_resolution_config); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set reconnect timeout task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_will_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_byte_buf topic_buffer; + enum aws_mqtt_qos qos; + bool retain; + struct aws_byte_buf payload_buffer; +}; + +static void s_aws_mqtt_set_will_task_destroy(struct aws_mqtt_set_will_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up(&task->topic_buffer); + aws_byte_buf_clean_up(&task->payload_buffer); + + aws_mem_release(task->allocator, task); +} + +static void s_set_will_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_will_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + struct aws_mqtt5_packet_connect_storage *connect = adapter->client->config->connect; + + /* clean up the old will if necessary */ + if (connect->will != NULL) { + aws_mqtt5_packet_publish_storage_clean_up(connect->will); + aws_mem_release(connect->allocator, connect->will); + connect->will = NULL; + } + + struct aws_mqtt5_packet_publish_view will = { + .topic = aws_byte_cursor_from_buf(&set_task->topic_buffer), + .qos = (enum aws_mqtt5_qos)set_task->qos, + .retain = set_task->retain, + .payload = aws_byte_cursor_from_buf(&set_task->payload_buffer), + }; + + /* make a new will */ + connect->will = aws_mem_calloc(connect->allocator, 1, sizeof(struct aws_mqtt5_packet_publish_storage)); + aws_mqtt5_packet_publish_storage_init(connect->will, connect->allocator, &will); + + /* manually update the storage view's will reference */ + connect->storage_view.will = &connect->will->storage_view; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + s_aws_mqtt_set_will_task_destroy(set_task); +} + +static struct aws_mqtt_set_will_task *s_aws_mqtt_set_will_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload) { + + if (topic == NULL) { + return NULL; + } + + struct aws_mqtt_set_will_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_will_task)); + + aws_task_init(&set_task->task, s_set_will_task_fn, (void *)set_task, "SetWillTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + set_task->qos = qos; + set_task->retain = retain; + aws_byte_buf_init_copy_from_cursor(&set_task->topic_buffer, allocator, *topic); + if (payload != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->payload_buffer, allocator, *payload); + } + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_will( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + /* check qos */ + if (qos < 0 || qos > AWS_MQTT_QOS_EXACTLY_ONCE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invalid qos for will", (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_QOS); + } + + /* check topic */ + if (!aws_mqtt_is_valid_topic(topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invalid topic for will", (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); + } + + struct aws_mqtt_set_will_task *task = + s_aws_mqtt_set_will_task_new(adapter->allocator, adapter, topic, qos, retain, payload); + if (task == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: failed to create set will task", (void *)adapter); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_set_login_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + struct aws_byte_buf username_buffer; + struct aws_byte_buf password_buffer; +}; + +static void s_aws_mqtt_set_login_task_destroy(struct aws_mqtt_set_login_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up_secure(&task->username_buffer); + aws_byte_buf_clean_up_secure(&task->password_buffer); + + aws_mem_release(task->allocator, task); +} + +static void s_set_login_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_set_login_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + struct aws_byte_cursor username_cursor = aws_byte_cursor_from_buf(&set_task->username_buffer); + struct aws_byte_cursor password_cursor = aws_byte_cursor_from_buf(&set_task->password_buffer); + + /* we're in the mqtt5 client's event loop; it's safe to access internal state */ + struct aws_mqtt5_packet_connect_storage *old_connect = adapter->client->config->connect; + + /* + * Packet storage stores binary data in a single buffer. The safest way to replace some binary data is + * to make a new storage from the old storage, deleting the old storage after construction is complete. + */ + struct aws_mqtt5_packet_connect_view new_connect_view = old_connect->storage_view; + + if (set_task->username_buffer.len > 0) { + new_connect_view.username = &username_cursor; + } else { + new_connect_view.username = NULL; + } + + if (set_task->password_buffer.len > 0) { + new_connect_view.password = &password_cursor; + } else { + new_connect_view.password = NULL; + } + + if (aws_mqtt5_packet_connect_view_validate(&new_connect_view)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - invalid CONNECT username or password", + (void *)adapter); + goto done; + } + + struct aws_mqtt5_packet_connect_storage *new_connect = + aws_mem_calloc(adapter->allocator, 1, sizeof(struct aws_mqtt5_packet_connect_storage)); + aws_mqtt5_packet_connect_storage_init(new_connect, adapter->allocator, &new_connect_view); + + adapter->client->config->connect = new_connect; + aws_mqtt5_packet_connect_storage_clean_up(old_connect); + aws_mem_release(old_connect->allocator, old_connect); + +done: + + aws_ref_count_release(&adapter->internal_refs); + + s_aws_mqtt_set_login_task_destroy(set_task); +} + +static struct aws_mqtt_set_login_task *s_aws_mqtt_set_login_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + const struct aws_byte_cursor *username, + const struct aws_byte_cursor *password) { + + struct aws_mqtt_set_login_task *set_task = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_login_task)); + + aws_task_init(&set_task->task, s_set_login_task_fn, (void *)set_task, "SetLoginTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + + if (username != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->username_buffer, allocator, *username); + } + + if (password != NULL) { + aws_byte_buf_init_copy_from_cursor(&set_task->password_buffer, allocator, *password); + } + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_login( + void *impl, + const struct aws_byte_cursor *username, + const struct aws_byte_cursor *password) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_login_task *task = + s_aws_mqtt_set_login_task_new(adapter->allocator, adapter, username, password); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set login task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + s_aws_mqtt_adapter_final_destroy(adapter); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached(void *context) { + struct aws_mqtt_client_connection_5_impl *adapter = context; + + /* + * Release the single internal reference that we started with. Only ephemeral references for cross-thread + * tasks might remain, and they will disappear quickly. + */ + aws_ref_count_release(&adapter->internal_refs); +} + +static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + aws_ref_count_acquire(&adapter->external_refs); + + return &adapter->base; +} + +static int s_decref_for_shutdown(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { + (void)context; + + adapter->synced_data.terminated = true; + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_to_mqtt3_adapter_on_zero_external_refs(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + s_aws_mqtt5_adapter_perform_safe_callback(adapter, true, s_decref_for_shutdown, NULL); + + /* + * When the adapter's exernal ref count goes to zero, here's what we want to do: + * + * (1) Put the adapter into the terminated state, which tells it to stop processing callbacks from the mqtt5 + * client + * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user + * of it) + * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we + * are guaranteed that no more callbacks from the mqtt5 client will reach us. + * (4) Release the single internal ref we started with when the adapter was created. + * (5) On last internal ref, we can safely release the mqtt5 client and synchronously clean up all other + * resources + * + * Step (1) was done within the lock-guarded safe callback above. + * Step (2) is done here. + * Steps (3) and (4) are accomplished by s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached + * Step (5) is completed by s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs + */ + aws_mqtt5_listener_release(adapter->listener); +} + +static void s_aws_mqtt_client_connection_5_release(void *impl) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + aws_ref_count_release(&adapter->external_refs); +} + +/* + * When submitting an operation (across threads), we not only need to keep the adapter alive, we also need to keep + * the operation alive since it's technically already being tracked within the adapter's operational state. + * + * Note: we may not truly need the operation ref but it's safer to keep it. + */ +static void s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation) { + if (!operation->holding_adapter_ref) { + operation->holding_adapter_ref = true; + aws_ref_count_acquire(&operation->adapter->internal_refs); + } + + aws_mqtt5_to_mqtt3_adapter_operation_acquire(operation); +} + +/* + * Once an operation has been received on the adapter's event loop, whether reject or accepted, we must release the + * transient references to the operation and adapter + */ +static void s_aws_mqtt5_to_mqtt3_adapter_operation_release_cross_thread_refs( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation) { + if (operation->holding_adapter_ref) { + operation->holding_adapter_ref = false; + aws_ref_count_release(&operation->adapter->internal_refs); + } + + aws_mqtt5_to_mqtt3_adapter_operation_release(operation); +} + +static void s_adapter_publish_operation_destroy(void *context) { + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = context; + if (operation == NULL) { + return; + } + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish_op = operation->impl; + + struct aws_mqtt_client_connection_5_impl *adapter_to_release = NULL; + if (publish_op->base.holding_adapter_ref) { + adapter_to_release = publish_op->base.adapter; + } + + /* We're going away before our MQTT5 operation, make sure it doesn't try to call us back when it completes */ + publish_op->publish_op->completion_options.completion_callback = NULL; + publish_op->publish_op->completion_options.completion_user_data = NULL; + + aws_mqtt5_operation_release(&publish_op->publish_op->base); + + aws_mem_release(operation->allocator, operation); + + if (adapter_to_release != NULL) { + aws_ref_count_release(&adapter_to_release->internal_refs); + } +} + +static void s_aws_mqtt5_to_mqtt3_adapter_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + + (void)packet_type; + (void)packet; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish_op = complete_ctx; + + if (publish_op->on_publish_complete != NULL) { + (*publish_op->on_publish_complete)( + &publish_op->base.adapter->base, + publish_op->base.id, + error_code, + publish_op->on_publish_complete_user_data); + } + + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + &publish_op->base.adapter->operational_state, publish_op->base.id); +} + +struct aws_mqtt5_to_mqtt3_adapter_operation_publish *aws_mqtt5_to_mqtt3_adapter_operation_new_publish( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_publish_options *options) { + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_to_mqtt3_adapter_operation_publish)); + + publish_op->base.allocator = allocator; + aws_ref_count_init(&publish_op->base.ref_count, publish_op, s_adapter_publish_operation_destroy); + publish_op->base.impl = publish_op; + publish_op->base.type = AWS_MQTT5TO3_AOT_PUBLISH; + publish_op->base.adapter = options->adapter; + publish_op->base.holding_adapter_ref = false; + + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = options->topic, + .qos = (enum aws_mqtt5_qos)options->qos, + .payload = options->payload, + .retain = options->retain, + }; + + struct aws_mqtt5_publish_completion_options publish_completion_options = { + .completion_callback = s_aws_mqtt5_to_mqtt3_adapter_publish_completion_fn, + .completion_user_data = publish_op, + }; + + publish_op->publish_op = aws_mqtt5_operation_publish_new( + allocator, options->adapter->client, &publish_view, &publish_completion_options); + if (publish_op->publish_op == NULL) { + goto error; + } + + publish_op->on_publish_complete = options->on_complete; + publish_op->on_publish_complete_user_data = options->on_complete_userdata; + + return publish_op; + +error: + + aws_mqtt5_to_mqtt3_adapter_operation_release(&publish_op->base); + + return NULL; +} + +void s_adapter_publish_submission_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *operation = arg; + + struct aws_mqtt_client_connection_5_impl *adapter = operation->base.adapter; + + aws_mqtt5_client_submit_operation_internal( + adapter->client, &operation->publish_op->base, status != AWS_TASK_STATUS_RUN_READY); + + /* + * The operation has been submitted in-thread. We can release the transient refs (operation, adapter) needed to + * keep things alive during the handover + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_release_cross_thread_refs(&operation->base); +} + +static uint16_t s_aws_mqtt_client_connection_5_publish( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload, + aws_mqtt_op_complete_fn *on_complete, + void *userdata) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + AWS_LOGF_DEBUG(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invoking publish API", (void *)adapter); + + /* check qos */ + if (qos < 0 || qos > AWS_MQTT_QOS_EXACTLY_ONCE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invalid qos for publish", (void *)adapter); + aws_raise_error(AWS_ERROR_MQTT_INVALID_QOS); + return 0; + } + + if (!aws_mqtt_is_valid_topic(topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invalid topic for publish", (void *)adapter); + aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); + return 0; + } + + struct aws_byte_cursor topic_cursor = *topic; + struct aws_byte_cursor payload_cursor; + AWS_ZERO_STRUCT(payload_cursor); + if (payload != NULL) { + payload_cursor = *payload; + } + + struct aws_mqtt5_to_mqtt3_adapter_publish_options publish_options = { + .adapter = adapter, + .topic = topic_cursor, + .qos = qos, + .retain = retain, + .payload = payload_cursor, + .on_complete = on_complete, + .on_complete_userdata = userdata, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *operation = + aws_mqtt5_to_mqtt3_adapter_operation_new_publish(adapter->allocator, &publish_options); + if (operation == NULL) { + return 0; + } + + if (aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(&adapter->operational_state, &operation->base)) { + goto error; + } + + uint16_t synthetic_id = operation->base.id; + + /* + * While in-transit to the adapter event loop, we take refs to both the operation and the adapter so that we + * are guaranteed they are still alive when the cross-thread submission task is run. + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs(&operation->base); + + aws_task_init( + &operation->base.submission_task, + s_adapter_publish_submission_fn, + operation, + "Mqtt5ToMqtt3AdapterPublishSubmission"); + + aws_event_loop_schedule_task_now(adapter->loop, &operation->base.submission_task); + + return synthetic_id; + +error: + + aws_mqtt5_to_mqtt3_adapter_operation_release(&operation->base); + + return 0; +} + +static void s_adapter_subscribe_operation_destroy(void *context) { + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = context; + if (operation == NULL) { + return; + } + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = operation->impl; + + size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_subscription_set_subscription_record *record = NULL; + aws_array_list_get_at(&subscribe_op->subscriptions, &record, i); + + aws_mqtt_subscription_set_subscription_record_destroy(record); + } + aws_array_list_clean_up(&subscribe_op->subscriptions); + + struct aws_mqtt_client_connection_5_impl *adapter_to_release = NULL; + if (subscribe_op->base.holding_adapter_ref) { + adapter_to_release = subscribe_op->base.adapter; + } + + /* We're going away before our MQTT5 operation, make sure it doesn't try to call us back when it completes */ + if (subscribe_op->subscribe_op != NULL) { + subscribe_op->subscribe_op->completion_options.completion_callback = NULL; + subscribe_op->subscribe_op->completion_options.completion_user_data = NULL; + + aws_mqtt5_operation_release(&subscribe_op->subscribe_op->base); + } + + aws_mem_release(operation->allocator, operation); + + if (adapter_to_release != NULL) { + aws_ref_count_release(&adapter_to_release->internal_refs); + } +} + +static enum aws_mqtt_qos s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos( + enum aws_mqtt5_suback_reason_code reason_code) { + switch (reason_code) { + case AWS_MQTT5_SARC_GRANTED_QOS_0: + case AWS_MQTT5_SARC_GRANTED_QOS_1: + case AWS_MQTT5_SARC_GRANTED_QOS_2: + return (enum aws_mqtt_qos)reason_code; + + default: + return AWS_MQTT_QOS_FAILURE; + } +} + +static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + (void)suback; + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = complete_ctx; + struct aws_mqtt_client_connection_5_impl *adapter = subscribe_op->base.adapter; + + if (subscribe_op->on_suback != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, completing single-topic subscribe", + (void *)adapter); + + struct aws_byte_cursor topic_filter; + AWS_ZERO_STRUCT(topic_filter); + enum aws_mqtt_qos granted_qos = AWS_MQTT_QOS_AT_MOST_ONCE; + + size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); + if (subscription_count > 0) { + struct aws_mqtt_subscription_set_subscription_record *record = NULL; + aws_array_list_get_at(&subscribe_op->subscriptions, &record, 0); + + topic_filter = record->subscription_view.topic_filter; + } + + if (suback->reason_code_count > 0) { + granted_qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[0]); + } + + (*subscribe_op->on_suback)( + &adapter->base, + subscribe_op->base.id, + &topic_filter, + granted_qos, + error_code, + subscribe_op->on_suback_user_data); + } + + if (subscribe_op->on_multi_suback != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, completing multi-topic subscribe", + (void *)adapter); + + AWS_VARIABLE_LENGTH_ARRAY( + struct aws_mqtt_topic_subscription, multi_sub_subscription_buf, suback->reason_code_count); + AWS_VARIABLE_LENGTH_ARRAY( + struct aws_mqtt_topic_subscription *, multi_sub_subscription_ptr_buf, suback->reason_code_count); + struct aws_mqtt_topic_subscription *subscription_ptr = + (struct aws_mqtt_topic_subscription *)multi_sub_subscription_buf; + + struct aws_array_list multi_sub_list; + aws_array_list_init_static( + &multi_sub_list, + multi_sub_subscription_ptr_buf, + suback->reason_code_count, + sizeof(struct aws_mqtt_topic_subscription *)); + + size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); + + for (size_t i = 0; i < suback->reason_code_count; ++i) { + struct aws_mqtt_topic_subscription *subscription = subscription_ptr + i; + AWS_ZERO_STRUCT(*subscription); + + subscription->qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[i]); + + if (i < subscription_count) { + struct aws_mqtt_subscription_set_subscription_record *record = NULL; + aws_array_list_get_at(&subscribe_op->subscriptions, &record, i); + + subscription->topic = record->subscription_view.topic_filter; + subscription->on_publish = record->subscription_view.on_publish_received; + subscription->on_publish_ud = record->subscription_view.callback_user_data; + subscription->on_cleanup = record->subscription_view.on_cleanup; + } + + aws_array_list_push_back(&multi_sub_list, &subscription); + } + + (*subscribe_op->on_multi_suback)( + &adapter->base, + subscribe_op->base.id, + &multi_sub_list, + error_code, + subscribe_op->on_multi_suback_user_data); + } + + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + &subscribe_op->base.adapter->operational_state, subscribe_op->base.id); +} + +static int s_validate_adapter_subscribe_options( + size_t subscription_count, + struct aws_mqtt_topic_subscription *subscriptions, + struct aws_mqtt_client_connection_5_impl *adapter) { + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_topic_subscription *subscription = subscriptions + i; + + /* check qos */ + if (subscription->qos < 0 || subscription->qos > AWS_MQTT_QOS_EXACTLY_ONCE) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, invalid qos for subscribe", (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_QOS); + } + + /* check topic */ + if (!aws_mqtt_is_valid_topic_filter(&subscription->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, invalid topic filter for subscribe", + (void *)adapter); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); + } + } + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_build_subscribe( + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op, + size_t subscription_count, + struct aws_mqtt_topic_subscription *subscriptions) { + struct aws_allocator *allocator = subscribe_op->base.allocator; + + /* make persistent adapter sub array */ + aws_array_list_init_dynamic( + &subscribe_op->subscriptions, + allocator, + subscription_count, + sizeof(struct aws_mqtt_subscription_set_subscription_record *)); + + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_topic_subscription *subscription_options = &subscriptions[i]; + + struct aws_mqtt_subscription_set_subscription_options subscription_record_options = { + .topic_filter = subscription_options->topic, + .qos = (enum aws_mqtt5_qos)subscription_options->qos, + .on_publish_received = subscription_options->on_publish, + .callback_user_data = subscription_options->on_publish_ud, + .on_cleanup = subscription_options->on_cleanup, + }; + struct aws_mqtt_subscription_set_subscription_record *record = + aws_mqtt_subscription_set_subscription_record_new(allocator, &subscription_record_options); + + aws_array_list_push_back(&subscribe_op->subscriptions, &record); + } + + /* make temp mqtt5 subscription view array */ + AWS_VARIABLE_LENGTH_ARRAY(struct aws_mqtt5_subscription_view, mqtt5_subscription_buffer, subscription_count); + struct aws_mqtt5_subscription_view *subscription_ptr = mqtt5_subscription_buffer; + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt5_subscription_view *subscription = subscription_ptr + i; + AWS_ZERO_STRUCT(*subscription); + + subscription->topic_filter = subscriptions[i].topic; + subscription->qos = (enum aws_mqtt5_qos)subscriptions[i].qos; + } + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = subscription_ptr, + .subscription_count = subscription_count, + }; + + struct aws_mqtt5_subscribe_completion_options subscribe_completion_options = { + .completion_callback = s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn, + .completion_user_data = subscribe_op, + }; + + subscribe_op->subscribe_op = aws_mqtt5_operation_subscribe_new( + allocator, subscribe_op->base.adapter->client, &subscribe_view, &subscribe_completion_options); + + if (subscribe_op->subscribe_op == NULL) { + /* subscribe options validation will have been raised as the error */ + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_subscribe_options *options, + struct aws_mqtt_client_connection_5_impl *adapter) { + + if (s_validate_adapter_subscribe_options(options->subscription_count, options->subscriptions, adapter)) { + return NULL; + } + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe)); + + subscribe_op->base.allocator = allocator; + aws_ref_count_init(&subscribe_op->base.ref_count, subscribe_op, s_adapter_subscribe_operation_destroy); + subscribe_op->base.impl = subscribe_op; + subscribe_op->base.type = AWS_MQTT5TO3_AOT_SUBSCRIBE; + subscribe_op->base.adapter = options->adapter; + subscribe_op->base.holding_adapter_ref = false; + + /* + * If we're a regular subscribe, build the mqtt5 operation now. Otherwise, we have to wait until + * we're on the event loop thread and it's safe to query the subscription set. + */ + if (options->subscription_count > 0) { + if (s_aws_mqtt5_to_mqtt3_adapter_build_subscribe( + subscribe_op, options->subscription_count, options->subscriptions)) { + goto error; + } + } + + subscribe_op->on_suback = options->on_suback; + subscribe_op->on_suback_user_data = options->on_suback_user_data; + subscribe_op->on_multi_suback = options->on_multi_suback; + subscribe_op->on_multi_suback_user_data = options->on_multi_suback_user_data; + + return subscribe_op; + +error: + + aws_mqtt5_to_mqtt3_adapter_operation_release(&subscribe_op->base); + + return NULL; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_build_resubscribe( + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op, + struct aws_array_list *full_subscriptions) { + size_t subscription_count = aws_array_list_length(full_subscriptions); + + AWS_VARIABLE_LENGTH_ARRAY(struct aws_mqtt_topic_subscription, multi_sub_subscriptions, subscription_count); + + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_subscription_set_subscription_options *existing_subscription = NULL; + aws_array_list_get_at_ptr(full_subscriptions, (void **)&existing_subscription, i); + + multi_sub_subscriptions[i].topic = existing_subscription->topic_filter; + multi_sub_subscriptions[i].qos = (enum aws_mqtt_qos)existing_subscription->qos; + multi_sub_subscriptions[i].on_publish = existing_subscription->on_publish_received; + multi_sub_subscriptions[i].on_cleanup = existing_subscription->on_cleanup; + multi_sub_subscriptions[i].on_publish_ud = existing_subscription->callback_user_data; + } + + return s_aws_mqtt5_to_mqtt3_adapter_build_subscribe(subscribe_op, subscription_count, multi_sub_subscriptions); +} + +void s_adapter_subscribe_submission_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *operation = arg; + struct aws_mqtt_client_connection_5_impl *adapter = operation->base.adapter; + + struct aws_array_list full_subscriptions; + AWS_ZERO_STRUCT(full_subscriptions); + + /* If we're a re-subscribe, it's now safe to build the subscription set and MQTT5 subscribe op */ + if (operation->subscribe_op == NULL) { + aws_mqtt_subscription_set_get_subscriptions(adapter->subscriptions, &full_subscriptions); + size_t subscription_count = aws_array_list_length(&full_subscriptions); + if (subscription_count == 0 || s_aws_mqtt5_to_mqtt3_adapter_build_resubscribe(operation, &full_subscriptions)) { + /* There's either nothing to do (no subscriptions) or we failed to build the op (should never happen) */ + int error_code = aws_last_error(); + if (subscription_count == 0) { + error_code = AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS; + } + + if (operation->on_multi_suback) { + (*operation->on_multi_suback)( + &adapter->base, operation->base.id, NULL, error_code, operation->on_multi_suback_user_data); + } + + /* + * Remove the persistent ref represented by being seated in the incomplete operations table. + * The other (transient) ref gets released at the end of the function. + */ + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + &adapter->operational_state, operation->base.id); + goto complete; + } + } + + size_t subscription_count = aws_array_list_length(&operation->subscriptions); + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_subscription_set_subscription_record *record = NULL; + aws_array_list_get_at(&operation->subscriptions, &record, i); + + aws_mqtt_subscription_set_add_subscription(adapter->subscriptions, &record->subscription_view); + } + + aws_mqtt5_client_submit_operation_internal( + adapter->client, &operation->subscribe_op->base, status != AWS_TASK_STATUS_RUN_READY); + +complete: + + aws_array_list_clean_up(&full_subscriptions); + + /* + * The operation has been submitted in-thread. We can release the transient refs (operation, adapter) needed to + * keep things alive during the handover + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_release_cross_thread_refs(&operation->base); +} + +static uint16_t s_aws_mqtt_client_connection_5_subscribe( + void *impl, + const struct aws_byte_cursor *topic_filter, + enum aws_mqtt_qos qos, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, single-topic subscribe API invoked", + (void *)adapter); + + struct aws_mqtt_topic_subscription subscription = { + .topic = *topic_filter, + .qos = qos, + .on_publish = on_publish, + .on_cleanup = on_ud_cleanup, + .on_publish_ud = on_publish_ud, + }; + + struct aws_mqtt5_to_mqtt3_adapter_subscribe_options subscribe_options = { + .adapter = adapter, + .subscriptions = &subscription, + .subscription_count = 1, + .on_suback = on_suback, + .on_suback_user_data = on_suback_user_data, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *operation = + aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe(adapter->allocator, &subscribe_options, adapter); + if (operation == NULL) { + return 0; + } + + if (aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(&adapter->operational_state, &operation->base)) { + goto error; + } + + uint16_t synthetic_id = operation->base.id; + + /* + * While in-transit to the adapter event loop, we take refs to both the operation and the adapter so that we + * are guaranteed they are still alive when the cross-thread submission task is run. + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs(&operation->base); + + aws_task_init( + &operation->base.submission_task, + s_adapter_subscribe_submission_fn, + operation, + "Mqtt5ToMqtt3AdapterSubscribeSubmission"); + + aws_event_loop_schedule_task_now(adapter->loop, &operation->base.submission_task); + + return synthetic_id; + +error: + + ; + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, single-topic subscribe failed synchronously, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + + aws_mqtt5_to_mqtt3_adapter_operation_release(&operation->base); + + return 0; +} + +static uint16_t s_aws_mqtt_client_connection_5_subscribe_multiple( + void *impl, + const struct aws_array_list *topic_filters, + aws_mqtt_suback_multi_fn *on_suback, + void *on_suback_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, multi-topic subscribe API invoked", (void *)adapter); + + if (topic_filters == NULL || aws_array_list_length(topic_filters) == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter multi-topic subscribe empty", (void *)adapter); + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return 0; + } + + struct aws_mqtt_topic_subscription *subscriptions = topic_filters->data; + + struct aws_mqtt5_to_mqtt3_adapter_subscribe_options subscribe_options = { + .adapter = adapter, + .subscriptions = subscriptions, + .subscription_count = aws_array_list_length(topic_filters), + .on_multi_suback = on_suback, + .on_multi_suback_user_data = on_suback_user_data, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *operation = + aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe(adapter->allocator, &subscribe_options, adapter); + if (operation == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, multi-topic subscribe operation creation failed, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return 0; + } + + if (aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(&adapter->operational_state, &operation->base)) { + goto error; + } + + uint16_t synthetic_id = operation->base.id; + + /* + * While in-transit to the adapter event loop, we take refs to both the operation and the adapter so that we + * are guaranteed they are still alive when the cross-thread submission task is run. + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs(&operation->base); + + aws_task_init( + &operation->base.submission_task, + s_adapter_subscribe_submission_fn, + operation, + "Mqtt5ToMqtt3AdapterSubscribeMultipleSubmission"); + + aws_event_loop_schedule_task_now(adapter->loop, &operation->base.submission_task); + + return synthetic_id; + +error: + + ; + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, multi-topic subscribe failed, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + + aws_mqtt5_to_mqtt3_adapter_operation_release(&operation->base); + + return 0; +} + +static void s_adapter_unsubscribe_operation_destroy(void *context) { + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = context; + if (operation == NULL) { + return; + } + + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *unsubscribe_op = operation->impl; + + aws_byte_buf_clean_up(&unsubscribe_op->topic_filter); + + struct aws_mqtt_client_connection_5_impl *adapter_to_release = NULL; + if (unsubscribe_op->base.holding_adapter_ref) { + adapter_to_release = unsubscribe_op->base.adapter; + } + + /* We're going away before our MQTT5 operation, make sure it doesn't try to call us back when it completes */ + unsubscribe_op->unsubscribe_op->completion_options.completion_callback = NULL; + unsubscribe_op->unsubscribe_op->completion_options.completion_user_data = NULL; + + aws_mqtt5_operation_release(&unsubscribe_op->unsubscribe_op->base); + + aws_mem_release(operation->allocator, operation); + + if (adapter_to_release != NULL) { + aws_ref_count_release(&adapter_to_release->internal_refs); + } +} + +static void s_aws_mqtt5_to_mqtt3_adapter_unsubscribe_completion_fn( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + (void)unsuback; + + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *unsubscribe_op = complete_ctx; + + if (unsubscribe_op->on_unsuback != NULL) { + (*unsubscribe_op->on_unsuback)( + &unsubscribe_op->base.adapter->base, + unsubscribe_op->base.id, + error_code, + unsubscribe_op->on_unsuback_user_data); + } + + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + &unsubscribe_op->base.adapter->operational_state, unsubscribe_op->base.id); +} + +struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *aws_mqtt5_to_mqtt3_adapter_operation_new_unsubscribe( + struct aws_allocator *allocator, + const struct aws_mqtt5_to_mqtt3_adapter_unsubscribe_options *options) { + + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *unsubscribe_op = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe)); + + unsubscribe_op->base.allocator = allocator; + aws_ref_count_init(&unsubscribe_op->base.ref_count, unsubscribe_op, s_adapter_unsubscribe_operation_destroy); + unsubscribe_op->base.impl = unsubscribe_op; + unsubscribe_op->base.type = AWS_MQTT5TO3_AOT_UNSUBSCRIBE; + unsubscribe_op->base.adapter = options->adapter; + unsubscribe_op->base.holding_adapter_ref = false; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = &options->topic_filter, + .topic_filter_count = 1, + }; + + struct aws_mqtt5_unsubscribe_completion_options unsubscribe_completion_options = { + .completion_callback = s_aws_mqtt5_to_mqtt3_adapter_unsubscribe_completion_fn, + .completion_user_data = unsubscribe_op, + }; + + unsubscribe_op->unsubscribe_op = aws_mqtt5_operation_unsubscribe_new( + allocator, options->adapter->client, &unsubscribe_view, &unsubscribe_completion_options); + if (unsubscribe_op->unsubscribe_op == NULL) { + goto error; + } + + unsubscribe_op->on_unsuback = options->on_unsuback; + unsubscribe_op->on_unsuback_user_data = options->on_unsuback_user_data; + + aws_byte_buf_init_copy_from_cursor(&unsubscribe_op->topic_filter, allocator, options->topic_filter); + + return unsubscribe_op; + +error: + + aws_mqtt5_to_mqtt3_adapter_operation_release(&unsubscribe_op->base); + + return NULL; +} + +void s_adapter_unsubscribe_submission_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *operation = arg; + + struct aws_mqtt_client_connection_5_impl *adapter = operation->base.adapter; + + aws_mqtt_subscription_set_remove_subscription( + adapter->subscriptions, aws_byte_cursor_from_buf(&operation->topic_filter)); + + aws_mqtt5_client_submit_operation_internal( + adapter->client, &operation->unsubscribe_op->base, status != AWS_TASK_STATUS_RUN_READY); + + /* + * The operation has been submitted in-thread. We can release the transient refs (operation, adapter) needed to + * keep things alive during the handover + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_release_cross_thread_refs(&operation->base); +} + +static uint16_t s_aws_mqtt_client_connection_5_unsubscribe( + void *impl, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_op_complete_fn *on_unsuback, + void *on_unsuback_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + AWS_LOGF_DEBUG(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, unsubscribe called", (void *)adapter); + + if (!aws_mqtt_is_valid_topic_filter(topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, unsubscribe failed, invalid topic filter", + (void *)adapter); + aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); + return 0; + } + + struct aws_mqtt5_to_mqtt3_adapter_unsubscribe_options unsubscribe_options = { + .adapter = adapter, + .topic_filter = *topic_filter, + .on_unsuback = on_unsuback, + .on_unsuback_user_data = on_unsuback_user_data, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *operation = + aws_mqtt5_to_mqtt3_adapter_operation_new_unsubscribe(adapter->allocator, &unsubscribe_options); + if (operation == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, unsubscribe operation creation failed, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return 0; + } + + if (aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(&adapter->operational_state, &operation->base)) { + goto error; + } + + uint16_t synthetic_id = operation->base.id; + + /* + * While in-transit to the adapter event loop, we take refs to both the operation and the adapter so that we + * are guaranteed they are still alive when the cross-thread submission task is run. + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs(&operation->base); + + aws_task_init( + &operation->base.submission_task, + s_adapter_unsubscribe_submission_fn, + operation, + "Mqtt5ToMqtt3AdapterUnsubscribeSubmission"); + + aws_event_loop_schedule_task_now(adapter->loop, &operation->base.submission_task); + + return synthetic_id; + +error: + + ; + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, unsubscribe failed, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + + aws_mqtt5_to_mqtt3_adapter_operation_release(&operation->base); + + return 0; +} + +static int s_aws_mqtt_client_connection_5_reconnect( + void *impl, + aws_mqtt_client_on_connection_complete_fn *on_connection_complete, + void *userdata) { + (void)impl; + (void)on_connection_complete; + (void)userdata; + + /* DEPRECATED, connection will reconnect automatically now. */ + AWS_LOGF_ERROR(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "aws_mqtt_client_connection_reconnect has been DEPRECATED."); + return aws_raise_error(AWS_ERROR_UNSUPPORTED_OPERATION); +} + +static int s_aws_mqtt_client_connection_5_get_stats( + void *impl, + struct aws_mqtt_connection_operation_statistics *stats) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + // Error checking + if (!adapter) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "Invalid MQTT3-to-5 adapter used when trying to get operation statistics"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + AWS_LOGF_DEBUG(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5-adapter, get_stats invoked", (void *)adapter); + + if (!stats) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: Invalid MQTT311 statistics struct used when trying to get operation statistics", + (void *)adapter); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_mqtt5_client_operation_statistics mqtt5_stats; + AWS_ZERO_STRUCT(mqtt5_stats); + aws_mqtt5_client_get_stats(adapter->client, &mqtt5_stats); + + stats->incomplete_operation_count = mqtt5_stats.incomplete_operation_count; + stats->incomplete_operation_size = mqtt5_stats.incomplete_operation_size; + stats->unacked_operation_count = mqtt5_stats.unacked_operation_count; + stats->unacked_operation_size = mqtt5_stats.unacked_operation_size; + + return AWS_OP_SUCCESS; +} + +static uint16_t s_aws_mqtt_5_resubscribe_existing_topics( + void *impl, + aws_mqtt_suback_multi_fn *on_suback, + void *on_suback_user_data) { + + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, resubscribe_existing_topics invoked", + (void *)adapter); + + struct aws_mqtt5_to_mqtt3_adapter_subscribe_options subscribe_options = { + .adapter = adapter, + .subscriptions = NULL, + .subscription_count = 0, + .on_multi_suback = on_suback, + .on_multi_suback_user_data = on_suback_user_data, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *operation = + aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe(adapter->allocator, &subscribe_options, adapter); + if (operation == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, resubscribe_existing_topics failed on operation creation, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return 0; + } + + if (aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(&adapter->operational_state, &operation->base)) { + goto error; + } + + uint16_t synthetic_id = operation->base.id; + + /* + * While in-transit to the adapter event loop, we take refs to both the operation and the adapter so that we + * are guaranteed they are still alive when the cross-thread submission task is run. + */ + s_aws_mqtt5_to_mqtt3_adapter_operation_acquire_cross_thread_refs(&operation->base); + + aws_task_init( + &operation->base.submission_task, + s_adapter_subscribe_submission_fn, + operation, + "Mqtt5ToMqtt3AdapterSubscribeResubscribe"); + + aws_event_loop_schedule_task_now(adapter->loop, &operation->base.submission_task); + + return synthetic_id; + +error: + + ; + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter, resubscribe_existing_topics failed, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + + aws_mqtt5_to_mqtt3_adapter_operation_release(&operation->base); + + return 0; +} + +static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { + .acquire_fn = s_aws_mqtt_client_connection_5_acquire, + .release_fn = s_aws_mqtt_client_connection_5_release, + .set_will_fn = s_aws_mqtt_client_connection_5_set_will, + .set_login_fn = s_aws_mqtt_client_connection_5_set_login, + .use_websockets_fn = s_aws_mqtt_client_connection_5_use_websockets, + .set_http_proxy_options_fn = s_aws_mqtt_client_connection_5_set_http_proxy_options, + .set_host_resolution_options_fn = s_aws_mqtt_client_connection_5_set_host_resolution_options, + .set_reconnect_timeout_fn = s_aws_mqtt_client_connection_5_set_reconnect_timeout, + .set_connection_result_handlers = s_aws_mqtt_client_connection_5_set_connection_result_handlers, + .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, + .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, + .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, + .connect_fn = s_aws_mqtt_client_connection_5_connect, + .reconnect_fn = s_aws_mqtt_client_connection_5_reconnect, + .disconnect_fn = s_aws_mqtt_client_connection_5_disconnect, + .subscribe_multiple_fn = s_aws_mqtt_client_connection_5_subscribe_multiple, + .subscribe_fn = s_aws_mqtt_client_connection_5_subscribe, + .resubscribe_existing_topics_fn = s_aws_mqtt_5_resubscribe_existing_topics, + .unsubscribe_fn = s_aws_mqtt_client_connection_5_unsubscribe, + .publish_fn = s_aws_mqtt_client_connection_5_publish, + .get_stats_fn = s_aws_mqtt_client_connection_5_get_stats, +}; + +static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_5_vtable_ptr = + &s_aws_mqtt_client_connection_5_vtable; + +struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_client(struct aws_mqtt5_client *client) { + struct aws_allocator *allocator = client->allocator; + struct aws_mqtt_client_connection_5_impl *adapter = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_client_connection_5_impl)); + + adapter->allocator = allocator; + + adapter->base.vtable = s_aws_mqtt_client_connection_5_vtable_ptr; + adapter->base.impl = adapter; + + adapter->client = aws_mqtt5_client_acquire(client); + adapter->loop = client->loop; + adapter->adapter_state = AWS_MQTT_AS_STAY_DISCONNECTED; + + aws_ref_count_init(&adapter->external_refs, adapter, s_aws_mqtt5_to_mqtt3_adapter_on_zero_external_refs); + aws_ref_count_init(&adapter->internal_refs, adapter, s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs); + + aws_rw_lock_init(&adapter->lock); + + aws_mqtt5_to_mqtt3_adapter_operation_table_init(&adapter->operational_state, allocator); + + adapter->subscriptions = aws_mqtt_subscription_set_new(allocator); + + /* + * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." + * We'll enable the adapter as soon as they try to connect via the 311 interface. This + * also ties in to how we simulate the 311 implementation's don't-reconnect-if-initial-connect-fails logic. + * The 5 client will continue to try and reconnect, but the adapter will go disabled making it seem to the 311 + * user that it is offline. + */ + adapter->synced_data.terminated = false; + + struct aws_mqtt5_listener_config listener_config = { + .client = client, + .listener_callbacks = + { + .listener_publish_received_handler = s_aws_mqtt5_listener_publish_received_adapter, + .listener_publish_received_handler_user_data = adapter, + .lifecycle_event_handler = s_aws_mqtt5_client_lifecycle_event_callback_adapter, + .lifecycle_event_handler_user_data = adapter, + }, + .termination_callback = s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached, + .termination_callback_user_data = adapter, + }; + adapter->listener = aws_mqtt5_listener_new(allocator, &listener_config); + + return &adapter->base; +} + +#define DEFAULT_MQTT_ADAPTER_OPERATION_TABLE_SIZE 100 + +void aws_mqtt5_to_mqtt3_adapter_operation_table_init( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + struct aws_allocator *allocator) { + aws_mutex_init(&table->lock); + aws_hash_table_init( + &table->operations, + allocator, + DEFAULT_MQTT_ADAPTER_OPERATION_TABLE_SIZE, + aws_mqtt_hash_uint16_t, + aws_mqtt_compare_uint16_t_eq, + NULL, + NULL); + table->next_id = 1; +} + +static int s_adapter_operation_clean_up(void *context, struct aws_hash_element *operation_element) { + (void)context; + + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = operation_element->value; + + aws_mqtt5_to_mqtt3_adapter_operation_release(operation); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +void aws_mqtt5_to_mqtt3_adapter_operation_table_clean_up(struct aws_mqtt5_to_mqtt3_adapter_operation_table *table) { + aws_hash_table_foreach(&table->operations, s_adapter_operation_clean_up, table); + + aws_hash_table_clean_up(&table->operations); + + aws_mutex_clean_up(&table->lock); +} + +static uint16_t s_next_adapter_id(uint16_t current_id) { + if (++current_id == 0) { + current_id = 1; + } + + return current_id; +} + +int aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation) { + + operation->id = 0; + + aws_mutex_lock(&table->lock); + + uint16_t current_id = table->next_id; + struct aws_hash_element *elem = NULL; + for (uint16_t i = 0; i < UINT16_MAX; ++i) { + aws_hash_table_find(&table->operations, ¤t_id, &elem); + + if (elem == NULL) { + operation->id = current_id; + table->next_id = s_next_adapter_id(current_id); + + if (aws_hash_table_put(&table->operations, &operation->id, operation, NULL)) { + operation->id = 0; + } + + goto done; + } + + current_id = s_next_adapter_id(current_id); + } + +done: + + aws_mutex_unlock(&table->lock); + + return (operation->id != 0) ? AWS_OP_SUCCESS : aws_raise_error(AWS_ERROR_MQTT_QUEUE_FULL); +} + +void aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( + struct aws_mqtt5_to_mqtt3_adapter_operation_table *table, + uint16_t operation_id) { + struct aws_hash_element existing_element; + AWS_ZERO_STRUCT(existing_element); + + aws_mutex_lock(&table->lock); + aws_hash_table_remove(&table->operations, &operation_id, &existing_element, NULL); + aws_mutex_unlock(&table->lock); + + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = existing_element.value; + if (operation != NULL) { + aws_mqtt5_to_mqtt3_adapter_operation_release(operation); + } +} + +struct aws_mqtt5_to_mqtt3_adapter_operation_base *aws_mqtt5_to_mqtt3_adapter_operation_release( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation) { + if (operation != NULL) { + aws_ref_count_release(&operation->ref_count); + } + + return NULL; +} + +struct aws_mqtt5_to_mqtt3_adapter_operation_base *aws_mqtt5_to_mqtt3_adapter_operation_acquire( + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation) { + if (operation != NULL) { + aws_ref_count_acquire(&operation->ref_count); + } + + return operation; +} diff --git a/source/v5/mqtt5_topic_alias.c b/source/v5/mqtt5_topic_alias.c index 928c7719..91ec8c60 100644 --- a/source/v5/mqtt5_topic_alias.c +++ b/source/v5/mqtt5_topic_alias.c @@ -7,6 +7,7 @@ #include #include +#include #include int aws_mqtt5_inbound_topic_alias_resolver_init( @@ -450,13 +451,6 @@ static void s_destroy_assignment_value(void *value) { s_aws_topic_alias_assignment_destroy(value); } -static bool s_topic_hash_equality_fn(const void *a, const void *b) { - const struct aws_byte_cursor *a_cursor = a; - const struct aws_byte_cursor *b_cursor = b; - - return aws_byte_cursor_eq(a_cursor, b_cursor); -} - static int s_aws_mqtt5_outbound_topic_alias_resolver_lru_reset( struct aws_mqtt5_outbound_topic_alias_resolver *resolver, uint16_t topic_alias_maximum) { @@ -471,7 +465,7 @@ static int s_aws_mqtt5_outbound_topic_alias_resolver_lru_reset( lru_resolver->lru_cache = aws_cache_new_lru( lru_resolver->base.allocator, aws_hash_byte_cursor_ptr, - s_topic_hash_equality_fn, + aws_mqtt_byte_cursor_hash_equality, NULL, s_destroy_assignment_value, topic_alias_maximum); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 42e74775..407bd07f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -366,26 +366,65 @@ add_test_case(rate_limiter_token_bucket_large_fractional_iteration) add_test_case(rate_limiter_token_bucket_real_iteration) add_test_case(rate_limiter_token_bucket_reset) -# mqtt3 to 5 adapter tests +# mqtt5 to 3 adapter tests -add_test_case(mqtt3to5_adapter_create_destroy) -add_test_case(mqtt3to5_adapter_create_destroy_delayed) -add_test_case(mqtt3to5_adapter_set_will) -add_test_case(mqtt3to5_adapter_set_login) -add_test_case(mqtt3to5_adapter_set_reconnect_timeout) -add_test_case(mqtt3to5_adapter_connect_success) -add_test_case(mqtt3to5_adapter_connect_success_disconnect_success) -add_test_case(mqtt3to5_adapter_connect_success_disconnect_success_thrice) -add_test_case(mqtt3to5_adapter_connect_success_connect_failure) -add_test_case(mqtt3to5_adapter_connect_success_sloppy_shutdown) -add_test_case(mqtt3to5_adapter_connect_bad_connectivity) -add_test_case(mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart) -add_test_case(mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5) -add_test_case(mqtt3to5_adapter_connect_failure_bad_config_success_good_config) -add_test_case(mqtt3to5_adapter_connect_success_disconnect_connect) -add_test_case(mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success) -add_test_case(mqtt3to5_adapter_disconnect_success) -add_test_case(mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success) +add_test_case(mqtt5to3_adapter_create_destroy) +add_test_case(mqtt5to3_adapter_create_destroy_delayed) +add_test_case(mqtt5to3_adapter_set_will) +add_test_case(mqtt5to3_adapter_set_login) +add_test_case(mqtt5to3_adapter_set_reconnect_timeout) +add_test_case(mqtt5to3_adapter_connect_success) +add_test_case(mqtt5to3_adapter_connect_success_disconnect_success) +add_test_case(mqtt5to3_adapter_connect_success_disconnect_success_thrice) +add_test_case(mqtt5to3_adapter_connect_success_connect_failure) +add_test_case(mqtt5to3_adapter_connect_success_sloppy_shutdown) +add_test_case(mqtt5to3_adapter_connect_bad_connectivity) +add_test_case(mqtt5to3_adapter_connect_bad_connectivity_with_mqtt5_restart) +add_test_case(mqtt5to3_adapter_connect_failure_connect_success_via_mqtt5) +add_test_case(mqtt5to3_adapter_connect_failure_bad_config_success_good_config) +add_test_case(mqtt5to3_adapter_connect_reconnect_failures) +add_test_case(mqtt5to3_adapter_connect_success_disconnect_connect) +add_test_case(mqtt5to3_adapter_connect_success_stop_mqtt5_disconnect_success) +add_test_case(mqtt5to3_adapter_disconnect_success) +add_test_case(mqtt5to3_adapter_connect_success_disconnect_success_disconnect_success) +add_test_case(mqtt5to3_adapter_operation_allocation_simple) +add_test_case(mqtt5to3_adapter_operation_allocation_wraparound) +add_test_case(mqtt5to3_adapter_operation_allocation_exhaustion) +add_test_case(mqtt5to3_adapter_publish_failure_invalid) +add_test_case(mqtt5to3_adapter_publish_failure_offline_queue_policy) +add_test_case(mqtt5to3_adapter_publish_success_qos0) +add_test_case(mqtt5to3_adapter_publish_success_qos1) +add_test_case(mqtt5to3_adapter_publish_no_ack) +add_test_case(mqtt5to3_adapter_publish_interrupted) +add_test_case(mqtt5to3_adapter_subscribe_single_success) +add_test_case(mqtt5to3_adapter_subscribe_multi_success) +add_test_case(mqtt5to3_adapter_subscribe_single_failure) +add_test_case(mqtt5to3_adapter_subscribe_single_invalid) +add_test_case(mqtt5to3_adapter_subscribe_multi_failure) +add_test_case(mqtt5to3_adapter_subscribe_multi_invalid) +add_test_case(mqtt5to3_adapter_subscribe_single_publish) +add_test_case(mqtt5to3_adapter_subscribe_multi_overlapping_publish) +add_test_case(mqtt5to3_adapter_unsubscribe_success) +add_test_case(mqtt5to3_adapter_unsubscribe_failure) +add_test_case(mqtt5to3_adapter_unsubscribe_invalid) +add_test_case(mqtt5to3_adapter_unsubscribe_overlapped) +add_test_case(mqtt5to3_adapter_get_stats) +add_test_case(mqtt5to3_adapter_resubscribe_nothing) +add_test_case(mqtt5to3_adapter_resubscribe_something) + +add_test_case(mqtt_subscription_set_add_empty_not_subbed) +add_test_case(mqtt_subscription_set_add_single_path) +add_test_case(mqtt_subscription_set_add_overlapped_branching_paths) +add_test_case(mqtt_subscription_set_remove_overlapping_path) +add_test_case(mqtt_subscription_set_remove_branching_path) +add_test_case(mqtt_subscription_set_remove_invalid) +add_test_case(mqtt_subscription_set_remove_empty_segments) +add_test_case(mqtt_subscription_set_add_remove_repeated) +add_test_case(mqtt_subscription_set_publish_single_path) +add_test_case(mqtt_subscription_set_publish_multi_path) +add_test_case(mqtt_subscription_set_publish_single_level_wildcards) +add_test_case(mqtt_subscription_set_publish_multi_level_wildcards) +add_test_case(mqtt_subscription_set_get_subscriptions) generate_test_driver(${PROJECT_NAME}-tests) diff --git a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c b/tests/v5/mqtt3_to_mqtt5_adapter_tests.c deleted file mode 100644 index abb2d76b..00000000 --- a/tests/v5/mqtt3_to_mqtt5_adapter_tests.c +++ /dev/null @@ -1,1476 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -#include "mqtt5_testing_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -enum aws_mqtt3_lifecycle_event_type { - AWS_MQTT3_LET_CONNECTION_COMPLETE, - AWS_MQTT3_LET_INTERRUPTED, - AWS_MQTT3_LET_RESUMED, - AWS_MQTT3_LET_CLOSED, - AWS_MQTT3_LET_DISCONNECTION_COMPLETE, -}; - -struct aws_mqtt3_lifecycle_event { - enum aws_mqtt3_lifecycle_event_type type; - - uint64_t timestamp; - int error_code; - enum aws_mqtt_connect_return_code return_code; - bool session_present; - - bool skip_error_code_equality; -}; - -struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config { - aws_mqtt_client_on_connection_interrupted_fn *on_interrupted; - aws_mqtt_client_on_connection_resumed_fn *on_resumed; - aws_mqtt_client_on_connection_closed_fn *on_closed; - - void *callback_user_data; -}; - -struct aws_mqtt3_to_mqtt5_adapter_test_fixture { - struct aws_mqtt5_client_mock_test_fixture mqtt5_fixture; - - struct aws_mqtt_client_connection *connection; - - struct aws_array_list lifecycle_events; - - struct aws_mutex lock; - struct aws_condition_variable signal; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config config; -}; - -static void s_init_adapter_connection_options_from_fixture( - struct aws_mqtt_connection_options *connection_options, - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { - AWS_ZERO_STRUCT(*connection_options); - - connection_options->host_name = aws_byte_cursor_from_c_str(fixture->mqtt5_fixture.endpoint.address); - connection_options->port = fixture->mqtt5_fixture.endpoint.port; - connection_options->socket_options = &fixture->mqtt5_fixture.socket_options; - connection_options->keep_alive_time_secs = 30; - connection_options->ping_timeout_ms = 10000; - connection_options->clean_session = true; -} - -struct n_lifeycle_event_wait_context { - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture; - enum aws_mqtt3_lifecycle_event_type type; - size_t count; -}; - -static bool s_wait_for_n_adapter_lifecycle_events_predicate(void *context) { - struct n_lifeycle_event_wait_context *wait_context = context; - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = wait_context->fixture; - - size_t actual_count = 0; - size_t event_count = aws_array_list_length(&fixture->lifecycle_events); - for (size_t i = 0; i < event_count; ++i) { - struct aws_mqtt3_lifecycle_event *actual_event = NULL; - aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); - if (actual_event->type == wait_context->type) { - ++actual_count; - } - } - - return actual_count >= wait_context->count; -} - -static void s_wait_for_n_adapter_lifecycle_events( - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, - enum aws_mqtt3_lifecycle_event_type type, - size_t count) { - struct n_lifeycle_event_wait_context wait_context = { - .fixture = fixture, - .type = type, - .count = count, - }; - - aws_mutex_lock(&fixture->lock); - aws_condition_variable_wait_pred( - &fixture->signal, &fixture->lock, s_wait_for_n_adapter_lifecycle_events_predicate, &wait_context); - aws_mutex_unlock(&fixture->lock); -} - -static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event( - struct aws_mqtt3_lifecycle_event *expected_event, - struct aws_mqtt3_lifecycle_event *actual_event) { - ASSERT_INT_EQUALS(actual_event->type, expected_event->type); - if (expected_event->skip_error_code_equality) { - /* some error scenarios lead to different values cross-platform, so just verify yes/no in that case */ - ASSERT_TRUE((actual_event->error_code != 0) == (expected_event->error_code != 0)); - } else { - ASSERT_INT_EQUALS(actual_event->error_code, expected_event->error_code); - } - - ASSERT_INT_EQUALS(actual_event->return_code, expected_event->return_code); - ASSERT_TRUE(actual_event->session_present == expected_event->session_present); - - return AWS_OP_SUCCESS; -} - -static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, - size_t expected_event_count, - struct aws_mqtt3_lifecycle_event *expected_events, - size_t maximum_event_count) { - - aws_mutex_lock(&fixture->lock); - - size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); - ASSERT_TRUE(expected_event_count <= actual_event_count); - ASSERT_TRUE(actual_event_count <= maximum_event_count); - - for (size_t i = 0; i < expected_event_count; ++i) { - struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; - struct aws_mqtt3_lifecycle_event *actual_event = NULL; - aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); - } - - aws_mutex_unlock(&fixture->lock); - - return AWS_OP_SUCCESS; -} - -static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_starts_with( - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, - size_t expected_event_count, - struct aws_mqtt3_lifecycle_event *expected_events) { - - aws_mutex_lock(&fixture->lock); - - size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); - ASSERT_TRUE(expected_event_count <= actual_event_count); - - for (size_t i = 0; i < expected_event_count; ++i) { - struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; - struct aws_mqtt3_lifecycle_event *actual_event = NULL; - aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); - } - - aws_mutex_unlock(&fixture->lock); - - return AWS_OP_SUCCESS; -} - -static int s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_ends_with( - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, - size_t expected_event_count, - struct aws_mqtt3_lifecycle_event *expected_events) { - - aws_mutex_lock(&fixture->lock); - - size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); - ASSERT_TRUE(expected_event_count <= actual_event_count); - - for (size_t i = 0; i < expected_event_count; ++i) { - struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; - - size_t actual_index = i + (actual_event_count - expected_event_count); - struct aws_mqtt3_lifecycle_event *actual_event = NULL; - aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), actual_index); - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); - } - - aws_mutex_unlock(&fixture->lock); - - return AWS_OP_SUCCESS; -} - -static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_closed_handler( - struct aws_mqtt_client_connection *connection, - struct on_connection_closed_data *data, - void *userdata) { - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; - - /* record the event */ - struct aws_mqtt3_lifecycle_event event; - AWS_ZERO_STRUCT(event); - - event.type = AWS_MQTT3_LET_CLOSED; - aws_high_res_clock_get_ticks(&event.timestamp); - - aws_mutex_lock(&fixture->lock); - aws_array_list_push_back(&fixture->lifecycle_events, &event); - aws_mutex_unlock(&fixture->lock); - aws_condition_variable_notify_all(&fixture->signal); - - /* invoke user callback if registered */ - if (fixture->config.on_closed) { - (*fixture->config.on_closed)(connection, data, fixture->config.callback_user_data); - } -} - -static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_interrupted_handler( - struct aws_mqtt_client_connection *connection, - int error_code, - void *userdata) { - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; - - /* record the event */ - struct aws_mqtt3_lifecycle_event event; - AWS_ZERO_STRUCT(event); - - event.type = AWS_MQTT3_LET_INTERRUPTED; - aws_high_res_clock_get_ticks(&event.timestamp); - event.error_code = error_code; - - aws_mutex_lock(&fixture->lock); - aws_array_list_push_back(&fixture->lifecycle_events, &event); - aws_mutex_unlock(&fixture->lock); - aws_condition_variable_notify_all(&fixture->signal); - - /* invoke user callback if registered */ - if (fixture->config.on_interrupted) { - (*fixture->config.on_interrupted)(connection, error_code, fixture->config.callback_user_data); - } -} - -static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_resumed_handler( - struct aws_mqtt_client_connection *connection, - enum aws_mqtt_connect_return_code return_code, - bool session_present, - void *userdata) { - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = userdata; - - /* record the event */ - struct aws_mqtt3_lifecycle_event event; - AWS_ZERO_STRUCT(event); - - event.type = AWS_MQTT3_LET_RESUMED; - aws_high_res_clock_get_ticks(&event.timestamp); - event.return_code = return_code; - event.session_present = session_present; - - aws_mutex_lock(&fixture->lock); - aws_array_list_push_back(&fixture->lifecycle_events, &event); - aws_mutex_unlock(&fixture->lock); - aws_condition_variable_notify_all(&fixture->signal); - - /* invoke user callback if registered */ - if (fixture->config.on_resumed) { - (*fixture->config.on_resumed)(connection, return_code, session_present, fixture->config.callback_user_data); - } -} - -static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete( - struct aws_mqtt_client_connection *connection, - int error_code, - enum aws_mqtt_connect_return_code return_code, - bool session_present, - void *user_data) { - (void)connection; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = user_data; - - struct aws_mqtt3_lifecycle_event event; - AWS_ZERO_STRUCT(event); - - event.type = AWS_MQTT3_LET_CONNECTION_COMPLETE; - aws_high_res_clock_get_ticks(&event.timestamp); - event.error_code = error_code; - event.return_code = return_code; - event.session_present = session_present; - - aws_mutex_lock(&fixture->lock); - aws_array_list_push_back(&fixture->lifecycle_events, &event); - aws_mutex_unlock(&fixture->lock); - aws_condition_variable_notify_all(&fixture->signal); -} - -static void s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete( - struct aws_mqtt_client_connection *connection, - void *user_data) { - (void)connection; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture = user_data; - - struct aws_mqtt3_lifecycle_event event; - AWS_ZERO_STRUCT(event); - - event.type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE; - aws_high_res_clock_get_ticks(&event.timestamp); - - aws_mutex_lock(&fixture->lock); - aws_array_list_push_back(&fixture->lifecycle_events, &event); - aws_mutex_unlock(&fixture->lock); - aws_condition_variable_notify_all(&fixture->signal); -} - -int aws_mqtt3_to_mqtt5_adapter_test_fixture_init( - struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture, - struct aws_allocator *allocator, - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *mqtt5_fixture_config, - struct aws_mqtt3_to_mqtt5_adapter_test_fixture_config *config) { - AWS_ZERO_STRUCT(*fixture); - - if (aws_mqtt5_client_mock_test_fixture_init(&fixture->mqtt5_fixture, allocator, mqtt5_fixture_config)) { - return AWS_OP_ERR; - } - - fixture->connection = aws_mqtt_client_connection_new_from_mqtt5_client(fixture->mqtt5_fixture.client); - if (fixture->connection == NULL) { - return AWS_OP_ERR; - } - - aws_array_list_init_dynamic(&fixture->lifecycle_events, allocator, 10, sizeof(struct aws_mqtt3_lifecycle_event)); - - aws_mutex_init(&fixture->lock); - aws_condition_variable_init(&fixture->signal); - - if (config) { - fixture->config = *config; - } - - aws_mqtt_client_connection_set_connection_closed_handler( - fixture->connection, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_closed_handler, fixture); - aws_mqtt_client_connection_set_connection_interruption_handlers( - fixture->connection, - s_aws_mqtt3_to_mqtt5_adapter_test_fixture_interrupted_handler, - fixture, - s_aws_mqtt3_to_mqtt5_adapter_test_fixture_resumed_handler, - fixture); - - return AWS_OP_SUCCESS; -} - -void aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { - aws_mqtt_client_connection_release(fixture->connection); - - aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->mqtt5_fixture); - - aws_array_list_clean_up(&fixture->lifecycle_events); - - aws_mutex_clean_up(&fixture->lock); - aws_condition_variable_clean_up(&fixture->signal); -} - -void s_mqtt3to5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { - (void)event; -} - -void s_mqtt3to5_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { - (void)publish; - (void)user_data; -} - -static int s_do_mqtt3to5_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { - aws_mqtt_library_init(allocator); - - struct aws_mqtt5_packet_connect_view local_connect_options = { - .keep_alive_interval_seconds = 30, - .clean_start = true, - }; - - struct aws_mqtt5_client_options client_options = { - .connect_options = &local_connect_options, - .lifecycle_event_handler = s_mqtt3to5_lifecycle_event_callback, - .lifecycle_event_handler_user_data = NULL, - .publish_received_handler = s_mqtt3to5_publish_received_callback, - .publish_received_handler_user_data = NULL, - .ping_timeout_ms = 10000, - }; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { - .client_options = &client_options, - }; - - struct aws_mqtt5_client_mock_test_fixture test_fixture; - AWS_ZERO_STRUCT(test_fixture); - - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); - - struct aws_mqtt_client_connection *connection = - aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener attachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt_client_connection_release(connection); - - if (sleep_nanos > 0) { - /* sleep a little just to let the listener detachment resolve */ - aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - } - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); - - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy(allocator, 0)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy, s_mqtt3to5_adapter_create_destroy_fn) - -static int s_mqtt3to5_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_create_destroy( - allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_create_destroy_delayed, s_mqtt3to5_adapter_create_destroy_delayed_fn) - -typedef int (*mqtt3to5_adapter_config_test_setup_fn)( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect); - -static int s_do_mqtt3to5_adapter_config_test( - struct aws_allocator *allocator, - mqtt3to5_adapter_config_test_setup_fn setup_fn) { - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture test_fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&test_fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt5_client *client = test_fixture.mqtt5_fixture.client; - - struct aws_mqtt_client_connection *adapter = test_fixture.connection; - - struct aws_mqtt5_packet_connect_storage expected_connect_storage; - ASSERT_SUCCESS((*setup_fn)(allocator, adapter, &expected_connect_storage)); - - ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - - aws_wait_for_connected_lifecycle_event(&test_fixture.mqtt5_fixture); - - ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - - aws_wait_for_stopped_lifecycle_event(&test_fixture.mqtt5_fixture); - - struct aws_mqtt5_mock_server_packet_record expected_packets[] = { - { - .packet_type = AWS_MQTT5_PT_CONNECT, - .packet_storage = &expected_connect_storage, - }, - }; - ASSERT_SUCCESS(aws_verify_received_packet_sequence( - &test_fixture.mqtt5_fixture, expected_packets, AWS_ARRAY_SIZE(expected_packets))); - - aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&test_fixture); - - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_STATIC_STRING_FROM_LITERAL(s_simple_topic, "Hello/World"); -AWS_STATIC_STRING_FROM_LITERAL(s_simple_payload, "A Payload"); - -static int s_mqtt3to5_adapter_set_will_setup( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect) { - - struct aws_byte_cursor topic_cursor = aws_byte_cursor_from_string(s_simple_topic); - struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_string(s_simple_payload); - - ASSERT_SUCCESS( - aws_mqtt_client_connection_set_will(adapter, &topic_cursor, AWS_MQTT_QOS_AT_LEAST_ONCE, true, &payload_cursor)); - - struct aws_mqtt5_packet_publish_view expected_will = { - .payload = payload_cursor, - .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, - .retain = true, - .topic = topic_cursor, - }; - - struct aws_mqtt5_packet_connect_view expected_connect_view = { - .client_id = aws_byte_cursor_from_string(g_default_client_id), - .keep_alive_interval_seconds = 30, - .clean_start = true, - .will = &expected_will, - }; - - ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_set_will_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_will_setup)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_will, s_mqtt3to5_adapter_set_will_fn) - -AWS_STATIC_STRING_FROM_LITERAL(s_username, "MyUsername"); -AWS_STATIC_STRING_FROM_LITERAL(s_password, "TopTopSecret"); - -static int s_mqtt3to5_adapter_set_login_setup( - struct aws_allocator *allocator, - struct aws_mqtt_client_connection *adapter, - struct aws_mqtt5_packet_connect_storage *expected_connect) { - - struct aws_byte_cursor username_cursor = aws_byte_cursor_from_string(s_username); - struct aws_byte_cursor password_cursor = aws_byte_cursor_from_string(s_password); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(adapter, &username_cursor, &password_cursor)); - - struct aws_mqtt5_packet_connect_view expected_connect_view = { - .client_id = aws_byte_cursor_from_string(g_default_client_id), - .keep_alive_interval_seconds = 30, - .clean_start = true, - .username = &username_cursor, - .password = &password_cursor, - }; - - ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); - - return AWS_OP_SUCCESS; -} - -static int s_mqtt3to5_adapter_set_login_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_config_test(allocator, s_mqtt3to5_adapter_set_login_setup)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_login, s_mqtt3to5_adapter_set_login_fn) - -static int s_mqtt3to5_adapter_set_reconnect_timeout_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - /* - * This is a variant of the mqtt5_client_reconnect_failure_backoff test. - * - * The primary change is that we configure the mqtt5 client with "wrong" (fast) reconnect delays and then use - * the adapter API to configure with the "right" ones that will let the test pass. - */ - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ - test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; - test_options.client_options.min_reconnect_delay_ms = 10; - test_options.client_options.max_reconnect_delay_ms = 50; - test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; - - test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - aws_mqtt5_mock_server_handle_connect_always_fail; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt5_client_mock_test_fixture test_context; - ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); - - struct aws_mqtt5_client *client = test_context.client; - - struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); - - aws_mqtt_client_connection_set_reconnect_timeout(adapter, RECONNECT_TEST_MIN_BACKOFF, RECONNECT_TEST_MAX_BACKOFF); - - ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - - aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 6); - - ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); - - aws_wait_for_stopped_lifecycle_event(&test_context); - - ASSERT_SUCCESS(aws_verify_reconnection_exponential_backoff_timestamps(&test_context)); - - /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ - enum aws_mqtt5_client_state expected_states[] = { - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, - AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, - }; - ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); - - aws_mqtt_client_connection_release(adapter); - - aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_set_reconnect_timeout, s_mqtt3to5_adapter_set_reconnect_timeout_fn) - -/* - * Basic successful connection test - */ -static int s_mqtt3to5_adapter_connect_success_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_connect_success, s_mqtt3to5_adapter_connect_success_fn) - -static int s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle( - struct aws_allocator *allocator, - size_t iterations) { - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - for (size_t i = 0; i < iterations; ++i) { - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = - s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, i + 1); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, i + 1); - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, i + 1); - - struct aws_mqtt3_lifecycle_event expected_event_sequence[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_CLOSED, - }, - }; - - size_t expected_event_count = (i + 1) * 3; - struct aws_mqtt3_lifecycle_event *expected_events = - aws_mem_calloc(allocator, expected_event_count, sizeof(struct aws_mqtt3_lifecycle_event)); - for (size_t j = 0; j < i + 1; ++j) { - for (size_t k = 0; k < 3; ++k) { - *(expected_events + j * 3 + k) = expected_event_sequence[k]; - } - } - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, expected_event_count, expected_events, expected_event_count)); - - aws_mem_release(allocator, expected_events); - } - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -/* - * A couple of simple connect-disconnect cycle tests. The first does a single cycle while the second does several. - * Verifies proper lifecycle event sequencing. - */ -static int s_mqtt3to5_adapter_connect_success_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle(allocator, 1)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_success_disconnect_success, - s_mqtt3to5_adapter_connect_success_disconnect_success_fn) - -static int s_mqtt3to5_adapter_connect_success_disconnect_success_thrice_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt3to5_adapter_connect_success_disconnect_success_cycle(allocator, 3)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_success_disconnect_success_thrice, - s_mqtt3to5_adapter_connect_success_disconnect_success_thrice_fn) - -/* - * Verifies that calling connect() while connected yields a connection completion callback with the - * appropriate already-connected error code. Note that in the mqtt311 impl, this error is synchronous. - */ -static int s_mqtt3to5_adapter_connect_success_connect_failure_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); - - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - .error_code = AWS_ERROR_MQTT_ALREADY_CONNECTED, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_connect_success_connect_failure, s_mqtt3to5_adapter_connect_success_connect_failure_fn) - -/* - * A non-deterministic test that starts the connect process and immediately drops the last external adapter - * reference. Intended to stochastically shake out shutdown race conditions. - */ -static int s_mqtt3to5_adapter_connect_success_sloppy_shutdown_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_connect_success_sloppy_shutdown, s_mqtt3to5_adapter_connect_success_sloppy_shutdown_fn) - -static int s_aws_mqtt5_server_disconnect_after_connect( - void *packet, - struct aws_mqtt5_server_mock_connection_context *connection, - void *user_data) { - aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); - - struct aws_mqtt5_packet_disconnect_view disconnect = { - .reason_code = AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN, - }; - - int result = aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_DISCONNECT, &disconnect); - - return result; -} - -static int s_verify_bad_connectivity_callbacks(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_INTERRUPTED, - .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, - }, - { - .type = AWS_MQTT3_LET_RESUMED, - }, - { - .type = AWS_MQTT3_LET_INTERRUPTED, - .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, - }, - { - .type = AWS_MQTT3_LET_RESUMED, - }, - { - .type = AWS_MQTT3_LET_INTERRUPTED, - .error_code = AWS_ERROR_MQTT5_DISCONNECT_RECEIVED, - }, - { - .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_CLOSED, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - return AWS_OP_SUCCESS; -} - -static int s_do_bad_connectivity_basic_test(struct aws_mqtt3_to_mqtt5_adapter_test_fixture *fixture) { - struct aws_mqtt_client_connection *adapter = fixture->connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_INTERRUPTED, 3); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, fixture); - - s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_CLOSED, 1); - - ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(fixture)); - - return AWS_OP_SUCCESS; -} - -/* - * A test where each successful connection is immediately dropped after the connack is sent. Allows us to verify - * proper interrupt/resume sequencing. - */ -static int s_mqtt3to5_adapter_connect_bad_connectivity_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ - test_options.client_options.min_reconnect_delay_ms = 500; - test_options.client_options.max_reconnect_delay_ms = 1000; - - test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_server_disconnect_after_connect; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_connect_bad_connectivity, s_mqtt3to5_adapter_connect_bad_connectivity_fn) - -/* - * A variant of the bad connectivity test where we restart the mqtt5 client after the main test is over and verify - * we don't get any interrupt/resume callbacks. - */ -static int s_mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart_fn( - struct aws_allocator *allocator, - void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ - test_options.client_options.min_reconnect_delay_ms = 500; - test_options.client_options.max_reconnect_delay_ms = 1000; - - test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_server_disconnect_after_connect; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); - - /* - * Now restart the 5 client, wait for a few more connection success/disconnect cycles, and then verify that no - * further adapter callbacks were invoked because of this. - */ - aws_mqtt5_client_start(fixture.mqtt5_fixture.client); - - aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 6); - - aws_thread_current_sleep(aws_timestamp_convert(2, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); - - ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(&fixture)); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart, - s_mqtt3to5_adapter_connect_bad_connectivity_with_mqtt5_restart_fn) - -int aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth( - void *packet, - struct aws_mqtt5_server_mock_connection_context *connection, - void *user_data) { - (void)packet; - - struct aws_mqtt5_mock_server_reconnect_state *context = user_data; - - struct aws_mqtt5_packet_connack_view connack_view; - AWS_ZERO_STRUCT(connack_view); - - if (context->connection_attempts >= context->required_connection_failure_count) { - connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; - aws_high_res_clock_get_ticks(&context->connect_timestamp); - } else { - connack_view.reason_code = AWS_MQTT5_CRC_NOT_AUTHORIZED; - } - - ++context->connection_attempts; - - return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); -} - -/* - * Test where the initial connect is rejected, which should put the adapter to sleep. Meanwhile followup attempts - * are successful and the mqtt5 client itself becomes connected. - */ -static int s_mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { - .required_connection_failure_count = 1, - }; - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth; - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - .mock_server_user_data = &mock_server_state, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - // wait for and verify a connection failure - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - // wait for the mqtt5 client to successfully connect on the second try - aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 1); - - // verify we didn't get any callbacks on the adapter - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - // "connect" on the adapter, wait for and verify success - aws_mqtt_client_connection_connect(adapter, &connection_options); - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); - - struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - .error_code = AWS_ERROR_MQTT5_CONNACK_CONNECTION_REFUSED, - }, - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, - AWS_ARRAY_SIZE(expected_reconnect_events), - expected_reconnect_events, - AWS_ARRAY_SIZE(expected_reconnect_events))); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5, - s_mqtt3to5_adapter_connect_failure_connect_success_via_mqtt5_fn) - -AWS_STATIC_STRING_FROM_LITERAL(s_bad_host_name, "derpity_derp"); - -/* - * Fails to connect with a bad config. Follow up with a good config. Verifies that config is re-evaluated with - * each connect() invocation. - */ -static int s_mqtt3to5_adapter_connect_failure_bad_config_success_good_config_fn( - struct aws_allocator *allocator, - void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - struct aws_byte_cursor good_host_name = connection_options.host_name; - connection_options.host_name = aws_byte_cursor_from_string(s_bad_host_name); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - // wait for and verify a connection failure - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - .error_code = AWS_ERROR_FILE_INVALID_PATH, - .skip_error_code_equality = true, /* the error code here is platform-dependent */ - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - // reconnect with a good host the adapter, wait for and verify success - connection_options.host_name = good_host_name; - aws_mqtt_client_connection_connect(adapter, &connection_options); - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); - - struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - .error_code = AWS_ERROR_FILE_INVALID_PATH, - .skip_error_code_equality = true, /* the error code here is platform-dependent */ - }, - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - }; - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, - AWS_ARRAY_SIZE(expected_reconnect_events), - expected_reconnect_events, - AWS_ARRAY_SIZE(expected_reconnect_events))); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_failure_bad_config_success_good_config, - s_mqtt3to5_adapter_connect_failure_bad_config_success_good_config_fn) - -/* - * Connect successfully then disconnect followed by a connect with no intervening wait. Verifies simple reliable - * action and event sequencing. - */ -static int s_mqtt3to5_adapter_connect_success_disconnect_connect_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); - - /* - * depending on timing there may or may not be a closed event in between, so just check beginning and end for - * expected events - */ - - struct aws_mqtt3_lifecycle_event expected_sequence_beginning[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - { - .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, - }, - }; - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_starts_with( - &fixture, AWS_ARRAY_SIZE(expected_sequence_beginning), expected_sequence_beginning)); - - struct aws_mqtt3_lifecycle_event expected_sequence_ending[] = { - { - .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, - }, - }; - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence_ends_with( - &fixture, AWS_ARRAY_SIZE(expected_sequence_ending), expected_sequence_ending)); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_success_disconnect_connect, - s_mqtt3to5_adapter_connect_success_disconnect_connect_fn) - -/* - * Calls disconnect() on an adapter that successfully connected but then had the mqtt5 client stopped behind the - * adapter's back. Verifies that we still get a completion callback. - */ -static int s_mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success_fn( - struct aws_allocator *allocator, - void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); - - aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success, - s_mqtt3to5_adapter_connect_success_stop_mqtt5_disconnect_success_fn) - -/* - * Call disconnect on a newly-created adapter. Verifies that we get a completion callback. - */ -static int s_mqtt3to5_adapter_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); - - struct aws_mqtt3_lifecycle_event expected_events[] = { - { - .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, - }, - }; - - ASSERT_SUCCESS(s_aws_mqtt3_to_mqtt5_adapter_test_fixture_verify_lifecycle_sequence( - &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt3to5_adapter_disconnect_success, s_mqtt3to5_adapter_disconnect_success_fn) - -/* - * Use the adapter to successfully connect then call disconnect multiple times. Verify that all disconnect - * invocations generate expected lifecycle events. Verifies that disconnects after a disconnect are properly handled. - */ -static int s_mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success_fn( - struct aws_allocator *allocator, - void *ctx) { - (void)ctx; - - aws_mqtt_library_init(allocator); - - struct mqtt5_client_test_options test_options; - aws_mqtt5_client_test_init_default_options(&test_options); - - struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { - .client_options = &test_options.client_options, - .server_function_table = &test_options.server_function_table, - }; - - struct aws_mqtt3_to_mqtt5_adapter_test_fixture fixture; - ASSERT_SUCCESS(aws_mqtt3_to_mqtt5_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options, NULL)); - - struct aws_mqtt_client_connection *adapter = fixture.connection; - - struct aws_mqtt_connection_options connection_options; - s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); - - connection_options.on_connection_complete = s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_connection_complete; - connection_options.user_data = &fixture; - - aws_mqtt_client_connection_connect(adapter, &connection_options); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); - - aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); - - aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - aws_mqtt_client_connection_disconnect( - adapter, s_aws_mqtt3_to_mqtt5_adapter_test_fixture_record_disconnection_complete, &fixture); - - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 3); - s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, 1); - - aws_mqtt3_to_mqtt5_adapter_test_fixture_clean_up(&fixture); - aws_mqtt_library_clean_up(); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success, - s_mqtt3to5_adapter_connect_success_disconnect_success_disconnect_success_fn) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index a5bb2a03..78d9c935 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -1520,7 +1520,7 @@ int aws_mqtt5_mock_server_handle_connect_succeed_on_nth( struct aws_mqtt5_packet_connack_view connack_view; AWS_ZERO_STRUCT(connack_view); - if (context->connection_attempts == context->required_connection_failure_count) { + if (context->connection_attempts == context->required_connection_count_threshold) { connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; aws_high_res_clock_get_ticks(&context->connect_timestamp); } else { @@ -1616,7 +1616,7 @@ static int s_mqtt5_client_reconnect_backoff_insufficient_reset_fn(struct aws_all aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { - .required_connection_failure_count = 6, + .required_connection_count_threshold = 6, /* quick disconnect should not reset reconnect delay */ .successful_connection_disconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY / 5, }; @@ -1736,7 +1736,7 @@ static int s_mqtt5_client_reconnect_backoff_sufficient_reset_fn(struct aws_alloc aws_mqtt5_client_test_init_default_options(&test_options); struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { - .required_connection_failure_count = 6, + .required_connection_count_threshold = 6, /* slow disconnect should reset reconnect delay */ .successful_connection_disconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY * 2, }; @@ -2480,7 +2480,7 @@ static int s_mqtt5_client_publish_timeout_fn(struct aws_allocator *allocator, vo AWS_TEST_CASE(mqtt5_client_publish_timeout, s_mqtt5_client_publish_timeout_fn) -static int s_aws_mqtt5_mock_server_handle_publish_puback( +int aws_mqtt5_mock_server_handle_publish_puback( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -2510,7 +2510,7 @@ static int s_do_iot_core_throughput_test(struct aws_allocator *allocator, bool u /* send pubacks */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_mock_server_handle_publish_puback; + aws_mqtt5_mock_server_handle_publish_puback; if (use_iot_core_limits) { test_options.client_options.extended_validation_and_flow_control_options = @@ -2613,7 +2613,7 @@ static int s_do_iot_core_publish_tps_test(struct aws_allocator *allocator, bool /* send pubacks */ test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_mock_server_handle_publish_puback; + aws_mqtt5_mock_server_handle_publish_puback; if (use_iot_core_limits) { test_options.client_options.extended_validation_and_flow_control_options = @@ -3113,11 +3113,7 @@ static void s_sub_pub_unsub_wait_for_publish_received(struct aws_mqtt5_sub_pub_u aws_mutex_unlock(&test_fixture->lock); } -static enum aws_mqtt5_unsuback_reason_code s_unsuback_reason_codes[] = { - AWS_MQTT5_UARC_SUCCESS, -}; - -static int s_aws_mqtt5_server_send_unsuback_on_unsubscribe( +int aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -3126,10 +3122,17 @@ static int s_aws_mqtt5_server_send_unsuback_on_unsubscribe( struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = packet; + AWS_VARIABLE_LENGTH_ARRAY( + enum aws_mqtt5_unsuback_reason_code, mqtt5_unsuback_codes, unsubscribe_view->topic_filter_count); + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + enum aws_mqtt5_unsuback_reason_code *reason_code_ptr = &mqtt5_unsuback_codes[i]; + *reason_code_ptr = AWS_MQTT5_UARC_SUCCESS; + } + struct aws_mqtt5_packet_unsuback_view unsuback_view = { .packet_id = unsubscribe_view->packet_id, - .reason_code_count = AWS_ARRAY_SIZE(s_unsuback_reason_codes), - .reason_codes = s_unsuback_reason_codes, + .reason_code_count = AWS_ARRAY_SIZE(mqtt5_unsuback_codes), + .reason_codes = mqtt5_unsuback_codes, }; return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); @@ -3137,7 +3140,7 @@ static int s_aws_mqtt5_server_send_unsuback_on_unsubscribe( #define FORWARDED_PUBLISH_PACKET_ID 32768 -static int s_aws_mqtt5_server_send_puback_and_forward_on_publish( +int aws_mqtt5_mock_server_handle_publish_puback_and_forward( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -3268,9 +3271,9 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = s_aws_mqtt5_server_send_suback_on_subscribe; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_server_send_puback_and_forward_on_publish; + aws_mqtt5_mock_server_handle_publish_puback_and_forward; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = - s_aws_mqtt5_server_send_unsuback_on_unsubscribe; + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -4312,7 +4315,7 @@ static int s_do_mqtt5_client_statistics_publish_test( aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_server_send_puback_and_forward_on_publish; + aws_mqtt5_mock_server_handle_publish_puback_and_forward; struct aws_mqtt5_client_mock_test_fixture test_context; struct aws_mqtt5_sub_pub_unsub_context full_test_context = { @@ -5271,7 +5274,7 @@ static int s_mqtt5_client_offline_operation_submission_then_connect_fn(struct aw aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_server_send_puback_and_forward_on_publish; + aws_mqtt5_mock_server_handle_publish_puback_and_forward; test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_NON_QOS1_PUBLISH_ON_DISCONNECT; @@ -5963,7 +5966,7 @@ static int s_perform_outbound_alias_sequence_test( test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = s_aws_mqtt5_mock_server_handle_connect_allow_aliasing; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = - s_aws_mqtt5_mock_server_handle_publish_puback; + aws_mqtt5_mock_server_handle_publish_puback; test_options.topic_aliasing_options.outbound_topic_alias_behavior = behavior_type; diff --git a/tests/v5/mqtt5_testing_utils.h b/tests/v5/mqtt5_testing_utils.h index 52e8859e..e4fa6de8 100644 --- a/tests/v5/mqtt5_testing_utils.h +++ b/tests/v5/mqtt5_testing_utils.h @@ -129,7 +129,7 @@ struct mqtt5_client_test_options { }; struct aws_mqtt5_mock_server_reconnect_state { - size_t required_connection_failure_count; + size_t required_connection_count_threshold; size_t connection_attempts; uint64_t connect_timestamp; @@ -203,6 +203,21 @@ int aws_mqtt5_mock_server_handle_connect_succeed_on_nth( struct aws_mqtt5_server_mock_connection_context *connection, void *user_data); +int aws_mqtt5_mock_server_handle_publish_puback( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +int aws_mqtt5_mock_server_handle_publish_puback_and_forward( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +int aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + extern const struct aws_string *g_default_client_id; #define RECONNECT_TEST_MIN_BACKOFF 500 diff --git a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c new file mode 100644 index 00000000..272b7326 --- /dev/null +++ b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c @@ -0,0 +1,4041 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt5_testing_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +enum aws_mqtt3_lifecycle_event_type { + AWS_MQTT3_LET_CONNECTION_COMPLETE, + AWS_MQTT3_LET_INTERRUPTED, + AWS_MQTT3_LET_RESUMED, + AWS_MQTT3_LET_CLOSED, + AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + AWS_MQTT3_LET_CONNECTION_SUCCESS, + AWS_MQTT3_LET_CONNECTION_FAILURE, +}; + +struct aws_mqtt3_lifecycle_event { + enum aws_mqtt3_lifecycle_event_type type; + + uint64_t timestamp; + int error_code; + enum aws_mqtt_connect_return_code return_code; + bool session_present; + + bool skip_error_code_equality; +}; + +enum aws_mqtt3_operation_event_type { + AWS_MQTT3_OET_PUBLISH_COMPLETE, + AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, +}; + +struct aws_mqtt3_operation_event { + enum aws_mqtt3_operation_event_type type; + + uint64_t timestamp; + int error_code; + + // publish received properties + enum aws_mqtt_qos qos; + struct aws_byte_buf topic; + struct aws_byte_cursor topic_cursor; + struct aws_byte_buf payload; + struct aws_byte_cursor payload_cursor; + + // subscribe complete properties + struct aws_array_list granted_subscriptions; + + struct aws_byte_buf topic_storage; + + /* + * Not a part of recorded events, instead used to help verification check number of occurrences. Only + * used by the "contains" verification function which wouldn't otherise be able to check if an exact + * event appears multiple times in the set. + */ + size_t expected_count; +}; + +static void s_aws_mqtt3_operation_event_clean_up(struct aws_mqtt3_operation_event *event) { + if (event == NULL) { + return; + } + + aws_byte_buf_clean_up(&event->topic); + aws_byte_buf_clean_up(&event->payload); + aws_byte_buf_clean_up(&event->topic_storage); + + aws_array_list_clean_up(&event->granted_subscriptions); +} + +static bool s_aws_mqtt3_operation_event_equals( + struct aws_mqtt3_operation_event *expected, + struct aws_mqtt3_operation_event *actual) { + if (expected->type != actual->type) { + return false; + } + + if (expected->error_code != actual->error_code) { + return false; + } + + if (expected->qos != actual->qos) { + return false; + } + + if (expected->topic_cursor.len != actual->topic_cursor.len) { + return false; + } + + if (expected->topic_cursor.len > 0) { + if (memcmp(expected->topic_cursor.ptr, actual->topic_cursor.ptr, expected->topic_cursor.len) != 0) { + return false; + } + } + + if (expected->payload_cursor.len != actual->payload_cursor.len) { + return false; + } + + if (expected->payload_cursor.len > 0) { + if (memcmp(expected->payload_cursor.ptr, actual->payload_cursor.ptr, expected->payload_cursor.len) != 0) { + return false; + } + } + + if (aws_array_list_length(&expected->granted_subscriptions) != + aws_array_list_length(&actual->granted_subscriptions)) { + return false; + } + + for (size_t i = 0; i < aws_array_list_length(&expected->granted_subscriptions); ++i) { + + struct aws_mqtt_topic_subscription expected_sub; + aws_array_list_get_at(&expected->granted_subscriptions, &expected_sub, i); + + bool found_match = false; + for (size_t j = 0; j < aws_array_list_length(&actual->granted_subscriptions); ++j) { + struct aws_mqtt_topic_subscription actual_sub; + aws_array_list_get_at(&actual->granted_subscriptions, &actual_sub, j); + + if (expected_sub.qos != actual_sub.qos) { + continue; + } + + if (expected_sub.topic.len != actual_sub.topic.len) { + continue; + } + + if (expected_sub.topic.len > 0) { + if (memcmp(expected_sub.topic.ptr, actual_sub.topic.ptr, expected_sub.topic.len) != 0) { + continue; + } + } + + found_match = true; + break; + } + + if (!found_match) { + return false; + } + } + + return true; +} + +struct aws_mqtt5_to_mqtt3_adapter_test_fixture { + struct aws_mqtt5_client_mock_test_fixture mqtt5_fixture; + + struct aws_mqtt_client_connection *connection; + + struct aws_array_list lifecycle_events; + + struct aws_array_list operation_events; + + struct aws_mutex lock; + struct aws_condition_variable signal; +}; + +static void s_init_adapter_connection_options_from_fixture( + struct aws_mqtt_connection_options *connection_options, + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture) { + AWS_ZERO_STRUCT(*connection_options); + + connection_options->host_name = aws_byte_cursor_from_c_str(fixture->mqtt5_fixture.endpoint.address); + connection_options->port = fixture->mqtt5_fixture.endpoint.port; + connection_options->socket_options = &fixture->mqtt5_fixture.socket_options; + connection_options->keep_alive_time_secs = 30; + connection_options->ping_timeout_ms = 10000; + connection_options->clean_session = true; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_operation_event *expected_events, + size_t maximum_event_count) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->operation_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + ASSERT_TRUE(actual_event_count <= maximum_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_operation_event *expected_event = expected_events + i; + struct aws_mqtt3_operation_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->operation_events, (void **)(&actual_event), i); + + ASSERT_TRUE(s_aws_mqtt3_operation_event_equals(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_operation_event *expected_events) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->operation_events); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_operation_event *expected_event = expected_events + i; + size_t match_count = 0; + + for (size_t j = 0; j < actual_event_count; ++j) { + struct aws_mqtt3_operation_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->operation_events, (void **)(&actual_event), j); + + if (s_aws_mqtt3_operation_event_equals(expected_event, actual_event)) { + ++match_count; + } + } + + if (expected_event->expected_count == 0) { + ASSERT_INT_EQUALS(1, match_count); + } else { + ASSERT_INT_EQUALS(expected_event->expected_count, match_count); + } + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +struct n_operation_event_wait_context { + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture; + enum aws_mqtt3_operation_event_type type; + size_t count; +}; + +static bool s_wait_for_n_adapter_operation_events_predicate(void *context) { + struct n_operation_event_wait_context *wait_context = context; + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = wait_context->fixture; + + size_t actual_count = 0; + size_t event_count = aws_array_list_length(&fixture->operation_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt3_operation_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->operation_events, (void **)(&actual_event), i); + if (actual_event->type == wait_context->type) { + ++actual_count; + } + } + + return actual_count >= wait_context->count; +} + +static void s_wait_for_n_adapter_operation_events( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + enum aws_mqtt3_operation_event_type type, + size_t count) { + struct n_operation_event_wait_context wait_context = { + .fixture = fixture, + .type = type, + .count = count, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred( + &fixture->signal, &fixture->lock, s_wait_for_n_adapter_operation_events_predicate, &wait_context); + aws_mutex_unlock(&fixture->lock); +} + +struct n_lifeycle_event_wait_context { + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture; + enum aws_mqtt3_lifecycle_event_type type; + size_t count; +}; + +static bool s_wait_for_n_adapter_lifecycle_events_predicate(void *context) { + struct n_lifeycle_event_wait_context *wait_context = context; + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = wait_context->fixture; + + size_t actual_count = 0; + size_t event_count = aws_array_list_length(&fixture->lifecycle_events); + for (size_t i = 0; i < event_count; ++i) { + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + if (actual_event->type == wait_context->type) { + ++actual_count; + } + } + + return actual_count >= wait_context->count; +} + +static void s_wait_for_n_adapter_lifecycle_events( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + enum aws_mqtt3_lifecycle_event_type type, + size_t count) { + struct n_lifeycle_event_wait_context wait_context = { + .fixture = fixture, + .type = type, + .count = count, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred( + &fixture->signal, &fixture->lock, s_wait_for_n_adapter_lifecycle_events_predicate, &wait_context); + aws_mutex_unlock(&fixture->lock); +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_event( + struct aws_mqtt3_lifecycle_event *expected_event, + struct aws_mqtt3_lifecycle_event *actual_event) { + ASSERT_INT_EQUALS(actual_event->type, expected_event->type); + if (expected_event->skip_error_code_equality) { + /* some error scenarios lead to different values cross-platform, so just verify yes/no in that case */ + ASSERT_TRUE((actual_event->error_code != 0) == (expected_event->error_code != 0)); + } else { + ASSERT_INT_EQUALS(actual_event->error_code, expected_event->error_code); + } + + ASSERT_INT_EQUALS(actual_event->return_code, expected_event->return_code); + ASSERT_TRUE(actual_event->session_present == expected_event->session_present); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events, + size_t maximum_event_count) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + ASSERT_TRUE(actual_event_count <= maximum_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), i); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_ends_with( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + size_t expected_event_count, + struct aws_mqtt3_lifecycle_event *expected_events) { + + aws_mutex_lock(&fixture->lock); + + size_t actual_event_count = aws_array_list_length(&fixture->lifecycle_events); + ASSERT_TRUE(expected_event_count <= actual_event_count); + + for (size_t i = 0; i < expected_event_count; ++i) { + struct aws_mqtt3_lifecycle_event *expected_event = expected_events + i; + + size_t actual_index = i + (actual_event_count - expected_event_count); + struct aws_mqtt3_lifecycle_event *actual_event = NULL; + aws_array_list_get_at_ptr(&fixture->lifecycle_events, (void **)(&actual_event), actual_index); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_event(expected_event, actual_event)); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_closed_handler( + struct aws_mqtt_client_connection *connection, + struct on_connection_closed_data *data, + void *userdata) { + + (void)connection; + (void)data; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CLOSED; + aws_high_res_clock_get_ticks(&event.timestamp); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_interrupted_handler( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_INTERRUPTED; + aws_high_res_clock_get_ticks(&event.timestamp); + event.error_code = error_code; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_resumed_handler( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_RESUMED; + aws_high_res_clock_get_ticks(&event.timestamp); + event.return_code = return_code; + event.session_present = session_present; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_connection_failure_handler( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CONNECTION_FAILURE; + aws_high_res_clock_get_ticks(&event.timestamp); + event.error_code = error_code; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_connection_success_handler( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CONNECTION_SUCCESS; + aws_high_res_clock_get_ticks(&event.timestamp); + event.return_code = return_code; + event.session_present = session_present; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete( + struct aws_mqtt_client_connection *connection, + int error_code, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *user_data) { + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = user_data; + + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_CONNECTION_COMPLETE; + aws_high_res_clock_get_ticks(&event.timestamp); + event.error_code = error_code; + event.return_code = return_code; + event.session_present = session_present; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete( + struct aws_mqtt_client_connection *connection, + void *user_data) { + (void)connection; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = user_data; + + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE; + aws_high_res_clock_get_ticks(&event.timestamp); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +int aws_mqtt5_to_mqtt3_adapter_test_fixture_init( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *mqtt5_fixture_config) { + AWS_ZERO_STRUCT(*fixture); + + if (aws_mqtt5_client_mock_test_fixture_init(&fixture->mqtt5_fixture, allocator, mqtt5_fixture_config)) { + return AWS_OP_ERR; + } + + fixture->connection = aws_mqtt_client_connection_new_from_mqtt5_client(fixture->mqtt5_fixture.client); + if (fixture->connection == NULL) { + return AWS_OP_ERR; + } + + aws_array_list_init_dynamic(&fixture->lifecycle_events, allocator, 10, sizeof(struct aws_mqtt3_lifecycle_event)); + aws_array_list_init_dynamic(&fixture->operation_events, allocator, 10, sizeof(struct aws_mqtt3_operation_event)); + + aws_mutex_init(&fixture->lock); + aws_condition_variable_init(&fixture->signal); + + aws_mqtt_client_connection_set_connection_closed_handler( + fixture->connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_closed_handler, fixture); + aws_mqtt_client_connection_set_connection_interruption_handlers( + fixture->connection, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_interrupted_handler, + fixture, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_resumed_handler, + fixture); + aws_mqtt_client_connection_set_connection_result_handlers( + fixture->connection, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_connection_success_handler, + fixture, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_connection_failure_handler, + fixture); + + return AWS_OP_SUCCESS; +} + +void aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture) { + aws_mqtt_client_connection_release(fixture->connection); + + aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->mqtt5_fixture); + + aws_array_list_clean_up(&fixture->lifecycle_events); + + size_t operation_event_count = aws_array_list_length(&fixture->operation_events); + for (size_t i = 0; i < operation_event_count; ++i) { + struct aws_mqtt3_operation_event *event = NULL; + aws_array_list_get_at_ptr(&fixture->operation_events, (void **)(&event), i); + + s_aws_mqtt3_operation_event_clean_up(event); + } + aws_array_list_clean_up(&fixture->operation_events); + + aws_mutex_clean_up(&fixture->lock); + aws_condition_variable_clean_up(&fixture->signal); +} + +void s_mqtt5to3_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + (void)event; +} + +void s_mqtt5to3_publish_received_callback(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static int s_do_mqtt5to3_adapter_create_destroy(struct aws_allocator *allocator, uint64_t sleep_nanos) { + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_packet_connect_view local_connect_options = { + .keep_alive_interval_seconds = 30, + .clean_start = true, + }; + + struct aws_mqtt5_client_options client_options = { + .connect_options = &local_connect_options, + .lifecycle_event_handler = s_mqtt5to3_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .publish_received_handler = s_mqtt5to3_publish_received_callback, + .publish_received_handler_user_data = NULL, + .ping_timeout_ms = 10000, + }; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_config = { + .client_options = &client_options, + }; + + struct aws_mqtt5_client_mock_test_fixture test_fixture; + AWS_ZERO_STRUCT(test_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_fixture, allocator, &test_fixture_config)); + + struct aws_mqtt_client_connection *connection = + aws_mqtt_client_connection_new_from_mqtt5_client(test_fixture.client); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener attachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt_client_connection_release(connection); + + if (sleep_nanos > 0) { + /* sleep a little just to let the listener detachment resolve */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5to3_adapter_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_create_destroy(allocator, 0)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_create_destroy, s_mqtt5to3_adapter_create_destroy_fn) + +static int s_mqtt5to3_adapter_create_destroy_delayed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_create_destroy( + allocator, aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_create_destroy_delayed, s_mqtt5to3_adapter_create_destroy_delayed_fn) + +typedef int (*mqtt5to3_adapter_config_test_setup_fn)( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect); + +static int s_do_mqtt5to3_adapter_config_test( + struct aws_allocator *allocator, + mqtt5to3_adapter_config_test_setup_fn setup_fn) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture test_fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&test_fixture, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_fixture.mqtt5_fixture.client; + + struct aws_mqtt_client_connection *adapter = test_fixture.connection; + + struct aws_mqtt5_packet_connect_storage expected_connect_storage; + ASSERT_SUCCESS((*setup_fn)(allocator, adapter, &expected_connect_storage)); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&test_fixture.mqtt5_fixture); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_fixture.mqtt5_fixture); + + struct aws_mqtt5_mock_server_packet_record expected_packets[] = { + { + .packet_type = AWS_MQTT5_PT_CONNECT, + .packet_storage = &expected_connect_storage, + }, + }; + ASSERT_SUCCESS(aws_verify_received_packet_sequence( + &test_fixture.mqtt5_fixture, expected_packets, AWS_ARRAY_SIZE(expected_packets))); + + aws_mqtt5_packet_connect_storage_clean_up(&expected_connect_storage); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&test_fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_STATIC_STRING_FROM_LITERAL(s_simple_topic, "Hello/World"); +AWS_STATIC_STRING_FROM_LITERAL(s_simple_payload, "A Payload"); + +static int s_mqtt5to3_adapter_set_will_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor topic_cursor = aws_byte_cursor_from_string(s_simple_topic); + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_string(s_simple_payload); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_set_will(adapter, &topic_cursor, AWS_MQTT_QOS_AT_LEAST_ONCE, true, &payload_cursor)); + + struct aws_mqtt5_packet_publish_view expected_will = { + .payload = payload_cursor, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .retain = true, + .topic = topic_cursor, + }; + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(g_default_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .will = &expected_will, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5to3_adapter_set_will_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_config_test(allocator, s_mqtt5to3_adapter_set_will_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_set_will, s_mqtt5to3_adapter_set_will_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_username, "MyUsername"); +AWS_STATIC_STRING_FROM_LITERAL(s_password, "TopTopSecret"); + +static int s_mqtt5to3_adapter_set_login_setup( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *adapter, + struct aws_mqtt5_packet_connect_storage *expected_connect) { + + struct aws_byte_cursor username_cursor = aws_byte_cursor_from_string(s_username); + struct aws_byte_cursor password_cursor = aws_byte_cursor_from_string(s_password); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(adapter, &username_cursor, &password_cursor)); + + struct aws_mqtt5_packet_connect_view expected_connect_view = { + .client_id = aws_byte_cursor_from_string(g_default_client_id), + .keep_alive_interval_seconds = 30, + .clean_start = true, + .username = &username_cursor, + .password = &password_cursor, + }; + + ASSERT_SUCCESS(aws_mqtt5_packet_connect_storage_init(expected_connect, allocator, &expected_connect_view)); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt5to3_adapter_set_login_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_config_test(allocator, s_mqtt5to3_adapter_set_login_setup)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_set_login, s_mqtt5to3_adapter_set_login_fn) + +static int s_mqtt5to3_adapter_set_reconnect_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* + * This is a variant of the mqtt5_client_reconnect_failure_backoff test. + * + * The primary change is that we configure the mqtt5 client with "wrong" (fast) reconnect delays and then use + * the adapter API to configure with the "right" ones that will let the test pass. + */ + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* backoff delay sequence: 500, 1000, 2000, 4000, 5000, ... */ + test_options.client_options.retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE; + test_options.client_options.min_reconnect_delay_ms = 10; + test_options.client_options.max_reconnect_delay_ms = 50; + test_options.client_options.min_connected_time_to_reset_reconnect_delay_ms = RECONNECT_TEST_BACKOFF_RESET_DELAY; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_always_fail; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_client_mock_test_fixture test_context; + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct aws_mqtt5_client *client = test_context.client; + + struct aws_mqtt_client_connection *adapter = aws_mqtt_client_connection_new_from_mqtt5_client(client); + + aws_mqtt_client_connection_set_reconnect_timeout(adapter, RECONNECT_TEST_MIN_BACKOFF, RECONNECT_TEST_MAX_BACKOFF); + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_mqtt5_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_FAILURE, 6); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_context); + + ASSERT_SUCCESS(aws_verify_reconnection_exponential_backoff_timestamps(&test_context)); + + /* 6 (connecting, mqtt_connect, channel_shutdown, pending_reconnect) tuples (minus the final pending_reconnect) */ + enum aws_mqtt5_client_state expected_states[] = { + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, AWS_MCS_PENDING_RECONNECT, + AWS_MCS_CONNECTING, AWS_MCS_MQTT_CONNECT, AWS_MCS_CHANNEL_SHUTDOWN, + }; + ASSERT_SUCCESS(aws_verify_client_state_sequence(&test_context, expected_states, AWS_ARRAY_SIZE(expected_states))); + + aws_mqtt_client_connection_release(adapter); + + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_set_reconnect_timeout, s_mqtt5to3_adapter_set_reconnect_timeout_fn) + +/* + * Basic successful connection test + */ +static int s_mqtt5to3_adapter_connect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_connect_success, s_mqtt5to3_adapter_connect_success_fn) + +static int s_do_mqtt5to3_adapter_connect_success_disconnect_success_cycle( + struct aws_allocator *allocator, + size_t iterations) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + for (size_t i = 0; i < iterations; ++i) { + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, i + 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, i + 1); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, i + 1); + + struct aws_mqtt3_lifecycle_event expected_event_sequence[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CLOSED, + }, + }; + size_t sequence_size = AWS_ARRAY_SIZE(expected_event_sequence); + + size_t expected_event_count = (i + 1) * sequence_size; + struct aws_mqtt3_lifecycle_event *expected_events = + aws_mem_calloc(allocator, expected_event_count, sizeof(struct aws_mqtt3_lifecycle_event)); + for (size_t j = 0; j < i + 1; ++j) { + for (size_t k = 0; k < sequence_size; ++k) { + *(expected_events + j * sequence_size + k) = expected_event_sequence[k]; + } + } + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, expected_event_count, expected_events, expected_event_count)); + + aws_mem_release(allocator, expected_events); + } + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * A couple of simple connect-disconnect cycle tests. The first does a single cycle while the second does several. + * Verifies proper lifecycle event sequencing. + */ +static int s_mqtt5to3_adapter_connect_success_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_connect_success_disconnect_success_cycle(allocator, 1)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_success_disconnect_success, + s_mqtt5to3_adapter_connect_success_disconnect_success_fn) + +static int s_mqtt5to3_adapter_connect_success_disconnect_success_thrice_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_mqtt5to3_adapter_connect_success_disconnect_success_cycle(allocator, 3)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_success_disconnect_success_thrice, + s_mqtt5to3_adapter_connect_success_disconnect_success_thrice_fn) + +/* + * Verifies that calling connect() while connected yields a connection completion callback with the + * appropriate already-connected error code. Note that in the mqtt311 impl, this error is synchronous. + */ +static int s_mqtt5to3_adapter_connect_success_connect_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT_ALREADY_CONNECTED, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_connect_success_connect_failure, s_mqtt5to3_adapter_connect_success_connect_failure_fn) + +/* + * A non-deterministic test that starts the connect process and immediately drops the last external adapter + * reference. Intended to stochastically shake out shutdown race conditions. + */ +static int s_mqtt5to3_adapter_connect_success_sloppy_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_connect_success_sloppy_shutdown, s_mqtt5to3_adapter_connect_success_sloppy_shutdown_fn) + +static int s_aws_mqtt5_server_disconnect_after_connect( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + aws_mqtt5_mock_server_handle_connect_always_succeed(packet, connection, user_data); + + struct aws_mqtt5_packet_disconnect_view disconnect = { + .reason_code = AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN, + }; + + int result = aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_DISCONNECT, &disconnect); + + return result; +} + +static int s_verify_bad_connectivity_callbacks(struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture) { + struct aws_mqtt3_lifecycle_event expected_events_start[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_RESUMED, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_RESUMED, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + fixture, AWS_ARRAY_SIZE(expected_events_start), expected_events_start)); + + struct aws_mqtt3_lifecycle_event expected_events_end[] = { + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_CLOSED, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_ends_with( + fixture, AWS_ARRAY_SIZE(expected_events_end), expected_events_end)); + + return AWS_OP_SUCCESS; +} + +static int s_do_bad_connectivity_basic_test(struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture) { + struct aws_mqtt_client_connection *adapter = fixture->connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_INTERRUPTED, 3); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, fixture); + + s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_CLOSED, 1); + + ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(fixture)); + + return AWS_OP_SUCCESS; +} + +/* + * A test where each successful connection is immediately dropped after the connack is sent. Allows us to verify + * proper interrupt/resume sequencing. + */ +static int s_mqtt5to3_adapter_connect_bad_connectivity_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ + test_options.client_options.min_reconnect_delay_ms = 500; + test_options.client_options.max_reconnect_delay_ms = 1000; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_server_disconnect_after_connect; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_connect_bad_connectivity, s_mqtt5to3_adapter_connect_bad_connectivity_fn) + +/* + * A variant of the bad connectivity test where we restart the mqtt5 client after the main test is over and verify + * we don't get any interrupt/resume callbacks. + */ +static int s_mqtt5to3_adapter_connect_bad_connectivity_with_mqtt5_restart_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* So that the test doesn't get excessively slow due to all the reconnects with backoff */ + test_options.client_options.min_reconnect_delay_ms = 500; + test_options.client_options.max_reconnect_delay_ms = 1000; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_server_disconnect_after_connect; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + ASSERT_SUCCESS(s_do_bad_connectivity_basic_test(&fixture)); + + /* + * Now restart the 5 client, wait for a few more connection success/disconnect cycles, and then verify that no + * further adapter callbacks were invoked because of this. + */ + aws_mqtt5_client_start(fixture.mqtt5_fixture.client); + + aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 6); + + aws_thread_current_sleep(aws_timestamp_convert(2, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + ASSERT_SUCCESS(s_verify_bad_connectivity_callbacks(&fixture)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_bad_connectivity_with_mqtt5_restart, + s_mqtt5to3_adapter_connect_bad_connectivity_with_mqtt5_restart_fn) + +int aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct aws_mqtt5_mock_server_reconnect_state *context = user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + if (context->connection_attempts >= context->required_connection_count_threshold) { + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + aws_high_res_clock_get_ticks(&context->connect_timestamp); + } else { + connack_view.reason_code = AWS_MQTT5_CRC_NOT_AUTHORIZED; + } + + ++context->connection_attempts; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* + * Test where the initial connect is rejected, which should put the adapter to sleep. Meanwhile followup attempts + * are successful and the mqtt5 client itself becomes connected. + */ +static int s_mqtt5to3_adapter_connect_failure_connect_success_via_mqtt5_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { + .required_connection_count_threshold = 1, + }; + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_succeed_on_or_after_nth; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &mock_server_state, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + // wait for and verify a connection failure + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // wait for the mqtt5 client to successfully connect on the second try + aws_mqtt5_wait_for_n_lifecycle_events(&fixture.mqtt5_fixture, AWS_MQTT5_CLET_CONNECTION_SUCCESS, 1); + + // verify we didn't get any callbacks on the adapter + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // "connect" on the adapter, wait for and verify success + aws_mqtt_client_connection_connect(adapter, &connection_options); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, + AWS_ARRAY_SIZE(expected_reconnect_events), + expected_reconnect_events, + AWS_ARRAY_SIZE(expected_reconnect_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_failure_connect_success_via_mqtt5, + s_mqtt5to3_adapter_connect_failure_connect_success_via_mqtt5_fn) + +AWS_STATIC_STRING_FROM_LITERAL(s_bad_host_name, "derpity_derp"); + +/* + * Fails to connect with a bad config. Follow up with a good config. Verifies that config is re-evaluated with + * each connect() invocation. + */ +static int s_mqtt5to3_adapter_connect_failure_bad_config_success_good_config_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + struct aws_byte_cursor good_host_name = connection_options.host_name; + connection_options.host_name = aws_byte_cursor_from_string(s_bad_host_name); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + // wait for and verify a connection failure + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + // reconnect with a good host the adapter, wait for and verify success + connection_options.host_name = good_host_name; + aws_mqtt_client_connection_connect(adapter, &connection_options); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + struct aws_mqtt3_lifecycle_event expected_reconnect_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + .error_code = AWS_ERROR_FILE_INVALID_PATH, + .skip_error_code_equality = true, /* the error code here is platform-dependent */ + }, + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, + AWS_ARRAY_SIZE(expected_reconnect_events), + expected_reconnect_events, + AWS_ARRAY_SIZE(expected_reconnect_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_failure_bad_config_success_good_config, + s_mqtt5to3_adapter_connect_failure_bad_config_success_good_config_fn) + +int aws_mqtt5_mock_server_handle_connect_fail_on_or_after_nth( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct aws_mqtt5_mock_server_reconnect_state *context = user_data; + + bool send_disconnect = false; + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + if (context->connection_attempts >= context->required_connection_count_threshold) { + connack_view.reason_code = AWS_MQTT5_CRC_NOT_AUTHORIZED; + } else { + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + aws_high_res_clock_get_ticks(&context->connect_timestamp); + send_disconnect = true; + } + + ++context->connection_attempts; + + aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); + + if (send_disconnect) { + struct aws_mqtt5_packet_disconnect_view disconnect = { + .reason_code = AWS_MQTT5_DRC_SERVER_SHUTTING_DOWN, + }; + + aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_DISCONNECT, &disconnect); + } + + return AWS_OP_SUCCESS; +} + +/* + * Establishes a successful connection then drops it followed by a perma-failure loop, verify we receive + * the new connection failure callbacks. + */ +static int s_mqtt5to3_adapter_connect_reconnect_failures_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt5_mock_server_reconnect_state mock_server_state = { + .required_connection_count_threshold = 1, + }; + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_fail_on_or_after_nth; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + .mock_server_user_data = &mock_server_state, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_FAILURE, 3); + + aws_mqtt_client_connection_disconnect(adapter, NULL, NULL); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_INTERRUPTED, + .error_code = AWS_ERROR_MQTT_UNEXPECTED_HANGUP, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_FAILURE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ERROR, + }, + }; + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_connect_reconnect_failures, s_mqtt5to3_adapter_connect_reconnect_failures_fn) + +/* + * Connect successfully then disconnect followed by a connect with no intervening wait. Verifies simple reliable + * action and event sequencing. + */ +static int s_mqtt5to3_adapter_connect_success_disconnect_connect_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 2); + + /* + * depending on timing there may or may not be a closed event in between, so just check beginning and end for + * expected events + */ + + struct aws_mqtt3_lifecycle_event expected_sequence_beginning[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_starts_with( + &fixture, AWS_ARRAY_SIZE(expected_sequence_beginning), expected_sequence_beginning)); + + struct aws_mqtt3_lifecycle_event expected_sequence_ending[] = { + { + .type = AWS_MQTT3_LET_CONNECTION_SUCCESS, + }, + { + .type = AWS_MQTT3_LET_CONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence_ends_with( + &fixture, AWS_ARRAY_SIZE(expected_sequence_ending), expected_sequence_ending)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_success_disconnect_connect, + s_mqtt5to3_adapter_connect_success_disconnect_connect_fn) + +/* + * Calls disconnect() on an adapter that successfully connected but then had the mqtt5 client stopped behind the + * adapter's back. Verifies that we still get a completion callback. + */ +static int s_mqtt5to3_adapter_connect_success_stop_mqtt5_disconnect_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); + + aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_success_stop_mqtt5_disconnect_success, + s_mqtt5to3_adapter_connect_success_stop_mqtt5_disconnect_success_fn) + +/* + * Call disconnect on a newly-created adapter. Verifies that we get a completion callback. + */ +static int s_mqtt5to3_adapter_disconnect_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + struct aws_mqtt3_lifecycle_event expected_events[] = { + { + .type = AWS_MQTT3_LET_DISCONNECTION_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_lifecycle_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_disconnect_success, s_mqtt5to3_adapter_disconnect_success_fn) + +/* + * Use the adapter to successfully connect then call disconnect multiple times. Verify that all disconnect + * invocations generate expected lifecycle events. Verifies that disconnects after a disconnect are properly handled. + */ +static int s_mqtt5to3_adapter_connect_success_disconnect_success_disconnect_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *adapter = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(adapter, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt5_client_stop(fixture.mqtt5_fixture.client, NULL, NULL); + + aws_wait_for_stopped_lifecycle_event(&fixture.mqtt5_fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + aws_mqtt_client_connection_disconnect( + adapter, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 3); + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CLOSED, 1); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_connect_success_disconnect_success_disconnect_success, + s_mqtt5to3_adapter_connect_success_disconnect_success_disconnect_success_fn) + +#define SIMPLE_ALLOCATION_COUNT 10 + +static int s_mqtt5to3_adapter_operation_allocation_simple_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + struct aws_mqtt_client_connection_5_impl *adapter = connection->impl; + struct aws_mqtt5_to_mqtt3_adapter_operation_table *operational_state = &adapter->operational_state; + + for (size_t i = 0; i < SIMPLE_ALLOCATION_COUNT; ++i) { + struct aws_mqtt5_to_mqtt3_adapter_publish_options publish_options = { + .adapter = adapter, + .topic = aws_byte_cursor_from_c_str("derp"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish = + aws_mqtt5_to_mqtt3_adapter_operation_new_publish(allocator, &publish_options); + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(operational_state, &publish->base)); + + ASSERT_INT_EQUALS(i + 1, (size_t)(publish->base.id)); + } + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_operation_allocation_simple, s_mqtt5to3_adapter_operation_allocation_simple_fn) + +#define ALLOCATION_WRAP_AROUND_ID_START 100 + +static int s_mqtt5to3_adapter_operation_allocation_wraparound_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + struct aws_mqtt_client_connection_5_impl *adapter = connection->impl; + struct aws_mqtt5_to_mqtt3_adapter_operation_table *operational_state = &adapter->operational_state; + + operational_state->next_id = ALLOCATION_WRAP_AROUND_ID_START; + + for (size_t i = 0; i < UINT16_MAX + 50; ++i) { + struct aws_mqtt5_to_mqtt3_adapter_publish_options publish_options = { + .adapter = adapter, + .topic = aws_byte_cursor_from_c_str("derp"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish = + aws_mqtt5_to_mqtt3_adapter_operation_new_publish(allocator, &publish_options); + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(operational_state, &publish->base)); + + size_t expected_id = (i + ALLOCATION_WRAP_AROUND_ID_START) % 65536; + if (i > UINT16_MAX - ALLOCATION_WRAP_AROUND_ID_START) { + ++expected_id; + } + + ASSERT_INT_EQUALS(expected_id, (size_t)(publish->base.id)); + + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation(operational_state, publish->base.id); + } + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_operation_allocation_wraparound, s_mqtt5to3_adapter_operation_allocation_wraparound_fn) + +static int s_mqtt5to3_adapter_operation_allocation_exhaustion_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + struct aws_mqtt_client_connection_5_impl *adapter = connection->impl; + struct aws_mqtt5_to_mqtt3_adapter_operation_table *operational_state = &adapter->operational_state; + + operational_state->next_id = ALLOCATION_WRAP_AROUND_ID_START; + + for (size_t i = 0; i < UINT16_MAX + 50; ++i) { + struct aws_mqtt5_to_mqtt3_adapter_publish_options publish_options = { + .adapter = adapter, + .topic = aws_byte_cursor_from_c_str("derp"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }; + + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish = + aws_mqtt5_to_mqtt3_adapter_operation_new_publish(allocator, &publish_options); + if (i >= UINT16_MAX) { + ASSERT_FAILS(aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(operational_state, &publish->base)); + aws_mqtt5_to_mqtt3_adapter_operation_release(&publish->base); + continue; + } + + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_operation_table_add_operation(operational_state, &publish->base)); + + size_t expected_id = (i + ALLOCATION_WRAP_AROUND_ID_START) % 65536; + if (i > UINT16_MAX - ALLOCATION_WRAP_AROUND_ID_START) { + ++expected_id; + } + + ASSERT_INT_EQUALS(expected_id, (size_t)(publish->base.id)); + } + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_operation_allocation_exhaustion, s_mqtt5to3_adapter_operation_allocation_exhaustion_fn) + +static int s_aws_mqtt5_mock_server_handle_connect_succeed_with_small_payload( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + uint32_t maximum_packet_size = 1024; + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.maximum_packet_size = &maximum_packet_size; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + + (void)connection; + (void)packet_id; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + struct aws_mqtt3_operation_event operation_event = { + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = error_code, + }; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->operation_events, &operation_event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static int s_mqtt5to3_adapter_publish_failure_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* only allow small payloads to force a publish error */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_aws_mqtt5_mock_server_handle_connect_succeed_with_small_payload; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + uint8_t payload_array[2 * 1024]; + struct aws_byte_cursor payload = aws_byte_cursor_from_array(payload_array, AWS_ARRAY_SIZE(payload_array)); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_MQTT5_PACKET_VALIDATION, + }}; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_publish_failure_invalid, s_mqtt5to3_adapter_publish_failure_invalid_fn) + +static int s_mqtt5to3_adapter_publish_failure_offline_queue_policy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_QOS0_PUBLISH_ON_DISCONNECT; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_client_connection_disconnect( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_MOST_ONCE, + false, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY, + }}; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_publish_failure_offline_queue_policy, + s_mqtt5to3_adapter_publish_failure_offline_queue_policy_fn) + +static int s_mqtt5to3_adapter_publish_success_qos0_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_MOST_ONCE, + false, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }}; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_publish_success_qos0, s_mqtt5to3_adapter_publish_success_qos0_fn) + +static int s_mqtt5to3_adapter_publish_success_qos1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }}; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_publish_success_qos1, s_mqtt5to3_adapter_publish_success_qos1_fn) + +static int s_mqtt5to3_adapter_publish_no_ack_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* Ignore publishes, triggering client-side timeout */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = NULL; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + connection_options.protocol_operation_timeout_ms = 5000; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }}; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_publish_no_ack, s_mqtt5to3_adapter_publish_no_ack_fn) + +static int s_mqtt5to3_adapter_publish_interrupted_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + /* Ignore publishes */ + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = NULL; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + /* + * wait for a little bit, we aren't going to get a response, shutdown while the operation is still pending + * While we don't verify anything afterwards, consequent race conditions and leaks would show up. + */ + aws_thread_current_sleep(aws_timestamp_convert(2, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_publish_interrupted, s_mqtt5to3_adapter_publish_interrupted_fn) + +void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + int error_code, + void *userdata) { + + (void)connection; + (void)packet_id; + (void)topic; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + struct aws_mqtt3_operation_event operation_event = { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = error_code, + }; + + aws_array_list_init_dynamic( + &operation_event.granted_subscriptions, + fixture->mqtt5_fixture.allocator, + 1, + sizeof(struct aws_mqtt_topic_subscription)); + + aws_byte_buf_init_copy_from_cursor(&operation_event.topic_storage, fixture->mqtt5_fixture.allocator, *topic); + + /* + * technically it's not safe to persist the topic cursor but they way the tests are built, the cursor will stay + * valid until the events are checked (as long as we don't delete the subscription internally) + */ + struct aws_mqtt_topic_subscription sub = { + .topic = aws_byte_cursor_from_buf(&operation_event.topic_storage), + .qos = qos, + }; + aws_array_list_push_back(&operation_event.granted_subscriptions, (void *)&sub); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->operation_events, &operation_event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_received( + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture, + enum aws_mqtt3_operation_event_type event_type, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload, + enum aws_mqtt_qos qos) { + + struct aws_mqtt3_operation_event operation_event = { + .type = event_type, + .qos = qos, + }; + + aws_byte_buf_init_copy_from_cursor(&operation_event.topic, fixture->mqtt5_fixture.allocator, topic); + operation_event.topic_cursor = aws_byte_cursor_from_buf(&operation_event.topic); + aws_byte_buf_init_copy_from_cursor(&operation_event.payload, fixture->mqtt5_fixture.allocator, payload); + operation_event.payload_cursor = aws_byte_cursor_from_buf(&operation_event.payload); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->operation_events, &operation_event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + + (void)connection; + (void)dup; + (void)retain; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_received( + fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, *topic, *payload, qos); +} + +void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + + (void)connection; + (void)dup; + (void)retain; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_received( + fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, *topic, *payload, qos); +} + +static int s_mqtt5_mock_server_handle_subscribe_suback_success( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + AWS_VARIABLE_LENGTH_ARRAY( + enum aws_mqtt5_suback_reason_code, mqtt5_suback_codes, subscribe_view->subscription_count); + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + enum aws_mqtt5_suback_reason_code *reason_code_ptr = &mqtt5_suback_codes[i]; + *reason_code_ptr = (enum aws_mqtt5_suback_reason_code)subscribe_view->subscriptions[i].qos; + } + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, + .reason_code_count = subscribe_view->subscription_count, + .reason_codes = mqtt5_suback_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static int s_mqtt5to3_adapter_subscribe_single_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt_topic_subscription expected_subs[1] = { + { + .topic = topic, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + }; + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + aws_array_list_init_static_from_initialized( + &expected_events[0].granted_subscriptions, + (void *)expected_subs, + 1, + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_success, s_mqtt5to3_adapter_subscribe_single_success_fn) + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_array_list *topic_subacks, /* contains aws_mqtt_topic_subscription pointers */ + int error_code, + void *userdata) { + + (void)connection; + (void)packet_id; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + struct aws_mqtt3_operation_event operation_event = { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = error_code, + }; + + if (error_code == AWS_ERROR_SUCCESS) { + size_t granted_count = aws_array_list_length(topic_subacks); + + aws_array_list_init_dynamic( + &operation_event.granted_subscriptions, + fixture->mqtt5_fixture.allocator, + granted_count, + sizeof(struct aws_mqtt_topic_subscription)); + + size_t topic_length = 0; + for (size_t i = 0; i < granted_count; ++i) { + struct aws_mqtt_topic_subscription *granted_sub = NULL; + aws_array_list_get_at(topic_subacks, &granted_sub, i); + + aws_array_list_push_back(&operation_event.granted_subscriptions, (void *)granted_sub); + topic_length += granted_sub->topic.len; + } + + aws_byte_buf_init(&operation_event.topic_storage, fixture->mqtt5_fixture.allocator, topic_length); + + for (size_t i = 0; i < granted_count; ++i) { + struct aws_mqtt_topic_subscription *granted_sub = NULL; + aws_array_list_get_at_ptr(&operation_event.granted_subscriptions, (void **)&granted_sub, i); + + aws_byte_buf_append_and_update(&operation_event.topic_storage, &granted_sub->topic); + } + } + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->operation_events, &operation_event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static int s_mqtt5to3_adapter_subscribe_multi_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt_topic_subscription subscriptions[] = { + { + .topic = aws_byte_cursor_from_c_str("topic/1"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + { + .topic = aws_byte_cursor_from_c_str("topic/2"), + .qos = AWS_MQTT_QOS_AT_MOST_ONCE, + }, + }; + + struct aws_array_list subscription_list; + aws_array_list_init_static_from_initialized( + &subscription_list, subscriptions, 2, sizeof(struct aws_mqtt_topic_subscription)); + + aws_mqtt_client_connection_subscribe_multiple( + connection, + &subscription_list, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + aws_array_list_init_static_from_initialized( + &expected_events[0].granted_subscriptions, + (void *)subscriptions, + 2, + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_multi_success, s_mqtt5to3_adapter_subscribe_multi_success_fn) + +static int s_mqtt5_mock_server_handle_subscribe_suback_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + AWS_VARIABLE_LENGTH_ARRAY( + enum aws_mqtt5_suback_reason_code, mqtt5_suback_codes, subscribe_view->subscription_count); + for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { + enum aws_mqtt5_suback_reason_code *reason_code_ptr = &mqtt5_suback_codes[i]; + *reason_code_ptr = (i % 2) ? (enum aws_mqtt5_suback_reason_code)subscribe_view->subscriptions[i].qos + : AWS_MQTT5_SARC_QUOTA_EXCEEDED; + } + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, + .reason_code_count = subscribe_view->subscription_count, + .reason_codes = mqtt5_suback_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static int s_mqtt5to3_adapter_subscribe_single_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_failure; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt_topic_subscription expected_subs[1] = { + { + .topic = topic, + .qos = AWS_MQTT_QOS_FAILURE, + }, + }; + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + + aws_array_list_init_static_from_initialized( + &expected_events[0].granted_subscriptions, + (void *)expected_subs, + 1, + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_failure, s_mqtt5to3_adapter_subscribe_single_failure_fn) + +static int s_mqtt5to3_adapter_subscribe_single_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor bad_topic = aws_byte_cursor_from_c_str("#/derp"); + + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_subscribe( + connection, + &bad_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_invalid, s_mqtt5to3_adapter_subscribe_single_invalid_fn) + +static int s_mqtt5to3_adapter_subscribe_multi_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_failure; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt_topic_subscription subscriptions[] = { + { + .topic = aws_byte_cursor_from_c_str("topic/1"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + { + .topic = aws_byte_cursor_from_c_str("topic/2"), + .qos = AWS_MQTT_QOS_AT_MOST_ONCE, + }, + }; + + struct aws_array_list subscription_list; + aws_array_list_init_static_from_initialized( + &subscription_list, subscriptions, 2, sizeof(struct aws_mqtt_topic_subscription)); + + aws_mqtt_client_connection_subscribe_multiple( + connection, + &subscription_list, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + /* reuse the subscriptions array for validation, but the first one will fail */ + subscriptions[0].qos = AWS_MQTT_QOS_FAILURE; + + struct aws_mqtt3_operation_event expected_events[] = {{ + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }}; + aws_array_list_init_static_from_initialized( + &expected_events[0].granted_subscriptions, + (void *)subscriptions, + 2, + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_multi_failure, s_mqtt5to3_adapter_subscribe_multi_failure_fn) + +static int s_mqtt5to3_adapter_subscribe_multi_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_failure; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt_topic_subscription subscriptions[] = { + { + .topic = aws_byte_cursor_from_c_str("topic/1"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + { + .topic = aws_byte_cursor_from_c_str("#/#"), + .qos = AWS_MQTT_QOS_AT_MOST_ONCE, + }, + }; + + struct aws_array_list subscription_list; + aws_array_list_init_static_from_initialized( + &subscription_list, subscriptions, 2, sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_subscribe_multiple( + connection, + &subscription_list, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, + &fixture)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_multi_invalid, s_mqtt5to3_adapter_subscribe_multi_invalid_fn) + +static int s_mqtt5to3_adapter_subscribe_single_publish_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("Payload!"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 1); + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_SUCCESS, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_publish, s_mqtt5to3_adapter_subscribe_single_publish_fn) + +static int s_mqtt5to3_adapter_subscribe_multi_overlapping_publish_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic1 = aws_byte_cursor_from_c_str("hello/world"); + struct aws_byte_cursor topic2 = aws_byte_cursor_from_c_str("hello/+"); + struct aws_byte_cursor topic3 = aws_byte_cursor_from_c_str("derp"); + + ASSERT_TRUE( + 0 != aws_mqtt_client_connection_subscribe( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture)); + + ASSERT_TRUE( + 0 != aws_mqtt_client_connection_subscribe( + connection, + &topic2, + AWS_MQTT_QOS_AT_MOST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture)); + + ASSERT_TRUE( + 0 != aws_mqtt_client_connection_subscribe( + connection, + &topic3, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture)); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 3); + + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload 1!"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload 2!"); + + aws_mqtt_client_connection_publish( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload1, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + aws_mqtt_client_connection_publish( + connection, + &topic3, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload2, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 2); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 2); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, 3); + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic3, + .payload_cursor = payload2, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + .expected_count = 2, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic3, + .payload_cursor = payload2, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_subscribe_multi_overlapping_publish, + s_mqtt5to3_adapter_subscribe_multi_overlapping_publish_fn) + +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + + (void)connection; + (void)packet_id; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + struct aws_mqtt3_operation_event operation_event = { + .type = AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + .error_code = error_code, + }; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->operation_events, &operation_event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static int s_mqtt5to3_adapter_unsubscribe_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("hello/world"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("Payload 1!"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, 1); + + struct aws_mqtt3_operation_event expected_events_before[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events_before), expected_events_before)); + + aws_mqtt_client_connection_unsubscribe( + connection, &topic, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete, &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, 1); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 2); + + struct aws_mqtt3_operation_event expected_events_after[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + .expected_count = 2, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic, + .payload_cursor = payload, + }, + { + .type = AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events_after), expected_events_after)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_unsubscribe_success, s_mqtt5to3_adapter_unsubscribe_success_fn) + +static int s_mqtt5_mock_server_handle_unsubscribe_unsuback_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view = packet; + + AWS_VARIABLE_LENGTH_ARRAY( + enum aws_mqtt5_unsuback_reason_code, mqtt5_unsuback_codes, unsubscribe_view->topic_filter_count); + for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { + enum aws_mqtt5_unsuback_reason_code *reason_code_ptr = &mqtt5_unsuback_codes[i]; + *reason_code_ptr = AWS_MQTT5_UARC_IMPLEMENTATION_SPECIFIC_ERROR; + } + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = unsubscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(mqtt5_unsuback_codes), + .reason_codes = mqtt5_unsuback_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); +} + +static int s_mqtt5to3_adapter_unsubscribe_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_mqtt5_mock_server_handle_unsubscribe_unsuback_failure; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("hello/world"); + + aws_mqtt_client_connection_unsubscribe( + connection, &topic, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete, &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_unsubscribe_failure, s_mqtt5to3_adapter_unsubscribe_failure_fn) + +static int s_mqtt5to3_adapter_unsubscribe_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_mqtt5_mock_server_handle_unsubscribe_unsuback_failure; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("#/bad"); + + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_unsubscribe( + connection, &topic, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete, &fixture)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_unsubscribe_invalid, s_mqtt5to3_adapter_unsubscribe_invalid_fn) + +static int s_mqtt5to3_adapter_unsubscribe_overlapped_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic1 = aws_byte_cursor_from_c_str("hello/world"); + struct aws_byte_cursor topic2 = aws_byte_cursor_from_c_str("hello/+"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + aws_mqtt_client_connection_subscribe( + connection, + &topic2, + AWS_MQTT_QOS_AT_MOST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 2); + + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload 1!"); + + aws_mqtt_client_connection_publish( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload1, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, 2); + + struct aws_mqtt3_operation_event expected_events_before[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + .expected_count = 2, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events_before), expected_events_before)); + + /* drop the wildcard subscription and publish again, should only get one more publish received subscribed */ + aws_mqtt_client_connection_unsubscribe( + connection, &topic2, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete, &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, 1); + + aws_mqtt_client_connection_publish( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload1, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 2); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, 2); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, 3); + + struct aws_mqtt3_operation_event expected_events_after[] = { + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_ANY, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + .expected_count = 2, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_RECEIVED_SUBSCRIBED, + .error_code = AWS_ERROR_SUCCESS, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .topic_cursor = topic1, + .payload_cursor = payload1, + .expected_count = 3, + }, + { + .type = AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + }, + }; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(expected_events_after), expected_events_after)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_unsubscribe_overlapped, s_mqtt5to3_adapter_unsubscribe_overlapped_fn) + +static int s_mqtt5_mock_server_handle_connect_publish_throttled( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_connack_view connack_view; + AWS_ZERO_STRUCT(connack_view); + + uint16_t receive_maximum = 1; + + connack_view.reason_code = AWS_MQTT5_CRC_SUCCESS; + connack_view.receive_maximum = &receive_maximum; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); +} + +/* + * In this test, we configure the server to only allow a single unacked QoS1 publish and to not respond to + * publishes. Then we throw three QoS 1 publishes at the client. This leads to the client being "paralyzed" waiting + * for a PUBACK for the first publish. We then query the client stats and expected to see one unacked operation and + * two additional (for a total of three) incomplete operations. + */ +static int s_mqtt5to3_adapter_get_stats_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + s_mqtt5_mock_server_handle_connect_publish_throttled; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = NULL; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("hi/there"); + struct aws_byte_cursor payload = aws_byte_cursor_from_c_str("something"); + + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + aws_mqtt_client_connection_publish( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + struct aws_mqtt_connection_operation_statistics stats; + aws_mqtt_client_connection_get_stats(connection, &stats); + + ASSERT_INT_EQUALS(1, stats.unacked_operation_count); + ASSERT_INT_EQUALS(3, stats.incomplete_operation_count); + ASSERT_TRUE(stats.unacked_operation_size > 0); + ASSERT_TRUE(stats.incomplete_operation_size > 0); + ASSERT_INT_EQUALS(stats.unacked_operation_size * 3, stats.incomplete_operation_size); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_get_stats, s_mqtt5to3_adapter_get_stats_fn) + +/* + * In this test we invoke a resubscribe while there are no active subscriptions. This hits a degenerate pathway + * that we have to handle specially inside the adapter since an empty subscribe is invalid. + */ +static int s_mqtt5to3_adapter_resubscribe_nothing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + aws_mqtt_resubscribe_existing_topics( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt3_operation_event resubscribe_ack[] = {{ + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, .error_code = AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, + /* no granted subscriptions */ + }}; + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(resubscribe_ack), resubscribe_ack)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_resubscribe_nothing, s_mqtt5to3_adapter_resubscribe_nothing_fn) + +/* + * In this test we subscribe individually to three separate topics, wait, then invoke resubscribe on the client and + * verify that we record 4 subacks, 3 for the individual and one 3-sized multi-sub for appropriate topics. + */ +static int s_mqtt5to3_adapter_resubscribe_something_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_mqtt5_mock_server_handle_subscribe_suback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + aws_mqtt_client_connection_set_on_any_publish_handler( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_on_any_publish, &fixture); + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic1 = aws_byte_cursor_from_c_str("hello/world"); + struct aws_byte_cursor topic2 = aws_byte_cursor_from_c_str("foo/bar"); + struct aws_byte_cursor topic3 = aws_byte_cursor_from_c_str("a/b/c"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + aws_mqtt_client_connection_subscribe( + connection, + &topic2, + AWS_MQTT_QOS_AT_MOST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + aws_mqtt_client_connection_subscribe( + connection, + &topic3, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 3); + + aws_mqtt_resubscribe_existing_topics( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, &fixture); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 4); + + struct aws_mqtt3_operation_event resubscribe_ack[] = {{ + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + }}; + + struct aws_mqtt_topic_subscription subscriptions[] = { + { + .topic = topic1, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + { + .topic = topic2, + .qos = AWS_MQTT_QOS_AT_MOST_ONCE, + }, + { + .topic = topic3, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + }; + + aws_array_list_init_static_from_initialized( + &resubscribe_ack[0].granted_subscriptions, + (void *)subscriptions, + AWS_ARRAY_SIZE(subscriptions), + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(resubscribe_ack), resubscribe_ack)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_resubscribe_something, s_mqtt5to3_adapter_resubscribe_something_fn) diff --git a/tests/v5/mqtt_subscription_set_tests.c b/tests/v5/mqtt_subscription_set_tests.c new file mode 100644 index 00000000..a914fb71 --- /dev/null +++ b/tests/v5/mqtt_subscription_set_tests.c @@ -0,0 +1,832 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "aws/mqtt/private/mqtt_subscription_set.h" +#include +#include + +#include + +struct subscription_test_context_callback_record { + struct aws_allocator *allocator; + + struct aws_byte_cursor topic; + struct aws_byte_buf topic_buffer; + + size_t callback_count; +}; + +static struct subscription_test_context_callback_record *s_subscription_test_context_callback_record_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic) { + struct subscription_test_context_callback_record *record = + aws_mem_calloc(allocator, 1, sizeof(struct subscription_test_context_callback_record)); + record->allocator = allocator; + record->callback_count = 1; + + aws_byte_buf_init_copy_from_cursor(&record->topic_buffer, allocator, topic); + record->topic = aws_byte_cursor_from_buf(&record->topic_buffer); + + return record; +} + +static void s_subscription_test_context_callback_record_destroy( + struct subscription_test_context_callback_record *record) { + if (record == NULL) { + return; + } + + aws_byte_buf_clean_up(&record->topic_buffer); + + aws_mem_release(record->allocator, record); +} + +static void s_destroy_callback_record(void *element) { + struct subscription_test_context_callback_record *record = element; + + s_subscription_test_context_callback_record_destroy(record); +} + +struct aws_mqtt_subscription_set_test_context { + struct aws_allocator *allocator; + + struct aws_hash_table callbacks; +}; + +static void s_aws_mqtt_subscription_set_test_context_init( + struct aws_mqtt_subscription_set_test_context *context, + struct aws_allocator *allocator) { + context->allocator = allocator; + + aws_hash_table_init( + &context->callbacks, + allocator, + 10, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_destroy_callback_record); +} + +static void s_aws_mqtt_subscription_set_test_context_clean_up(struct aws_mqtt_subscription_set_test_context *context) { + aws_hash_table_clean_up(&context->callbacks); +} + +static void s_aws_mqtt_subscription_set_test_context_record_callback( + struct aws_mqtt_subscription_set_test_context *context, + struct aws_byte_cursor topic) { + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&context->callbacks, &topic, &element); + + if (element == NULL) { + struct subscription_test_context_callback_record *record = + s_subscription_test_context_callback_record_new(context->allocator, topic); + aws_hash_table_put(&context->callbacks, &record->topic, record, NULL); + } else { + struct subscription_test_context_callback_record *record = element->value; + ++record->callback_count; + } +} + +static int s_aws_mqtt_subscription_set_test_context_validate_callbacks( + struct aws_mqtt_subscription_set_test_context *context, + struct subscription_test_context_callback_record *expected_records, + size_t expected_record_count) { + ASSERT_INT_EQUALS(expected_record_count, aws_hash_table_get_entry_count(&context->callbacks)); + + for (size_t i = 0; i < expected_record_count; ++i) { + struct subscription_test_context_callback_record *expected_record = expected_records + i; + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&context->callbacks, &expected_record->topic, &element); + + ASSERT_TRUE(element != NULL); + ASSERT_TRUE(element->value != NULL); + + struct subscription_test_context_callback_record *actual_record = element->value; + + ASSERT_INT_EQUALS(expected_record->callback_count, actual_record->callback_count); + } + + return AWS_OP_SUCCESS; +} + +static void s_subscription_set_test_on_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + (void)connection; + (void)payload; + (void)qos; + (void)dup; + (void)retain; + + struct aws_mqtt_subscription_set_test_context *context = userdata; + + s_aws_mqtt_subscription_set_test_context_record_callback(context, *topic); +} + +enum subscription_set_operation_type { + SSOT_ADD, + SSOT_REMOVE, + SSOT_PUBLISH, +}; + +struct subscription_set_operation { + enum subscription_set_operation_type type; + + const char *topic_filter; + + const char *topic; +}; + +static void s_subscription_set_perform_operations( + struct aws_mqtt_subscription_set_test_context *context, + struct aws_mqtt_subscription_set *subscription_set, + struct subscription_set_operation *operations, + size_t operation_count) { + for (size_t i = 0; i < operation_count; ++i) { + struct subscription_set_operation *operation = operations + i; + + switch (operation->type) { + case SSOT_ADD: { + struct aws_mqtt_subscription_set_subscription_options subscription_options = { + .topic_filter = aws_byte_cursor_from_c_str(operation->topic_filter), + .callback_user_data = context, + .on_publish_received = s_subscription_set_test_on_publish_received, + }; + aws_mqtt_subscription_set_add_subscription(subscription_set, &subscription_options); + break; + } + + case SSOT_REMOVE: + aws_mqtt_subscription_set_remove_subscription( + subscription_set, aws_byte_cursor_from_c_str(operation->topic_filter)); + break; + + case SSOT_PUBLISH: { + struct aws_mqtt_subscription_set_publish_received_options publish_options = { + .topic = aws_byte_cursor_from_c_str(operation->topic), + }; + aws_mqtt_subscription_set_on_publish_received(subscription_set, &publish_options); + break; + } + } + } +} + +static int s_mqtt_subscription_set_add_empty_not_subbed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("/"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("abc"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_add_empty_not_subbed, s_mqtt_subscription_set_add_empty_not_subbed_fn) + +static int s_mqtt_subscription_set_add_single_path_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + { + .type = SSOT_ADD, + .topic_filter = "a/b/c", + }, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("/"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("abc"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_add_single_path, s_mqtt_subscription_set_add_single_path_fn) + +static int s_mqtt_subscription_set_add_overlapped_branching_paths_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/+/c"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c"}, + {.type = SSOT_ADD, .topic_filter = "+/b/c"}, + {.type = SSOT_ADD, .topic_filter = "+/+/+"}, + {.type = SSOT_ADD, .topic_filter = "/"}, + {.type = SSOT_ADD, .topic_filter = " "}, + {.type = SSOT_ADD, .topic_filter = "a"}, + {.type = SSOT_ADD, .topic_filter = "#"}, + {.type = SSOT_ADD, .topic_filter = "a/#"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c/d/e/f/g"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c/"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/+/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/+"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("/"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str(" "))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/#"))); + ASSERT_TRUE( + aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e/f/g"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/"))); + + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/+"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/+/b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE( + aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e/f"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/+/+"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str(" /"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("/ "))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/"))); + ASSERT_FALSE( + aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e/f/g/"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt_subscription_set_add_overlapped_branching_paths, + s_mqtt_subscription_set_add_overlapped_branching_paths_fn) + +static int s_mqtt_subscription_set_remove_overlapping_path_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/b/c/d"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c"}, + {.type = SSOT_ADD, .topic_filter = "a/b"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c/d/e"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b/c")); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c/d/e"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_remove_overlapping_path, s_mqtt_subscription_set_remove_overlapping_path_fn) + +static int s_mqtt_subscription_set_remove_branching_path_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "+/+/#"}, + {.type = SSOT_ADD, .topic_filter = "#"}, + {.type = SSOT_ADD, .topic_filter = "+/b/c"}, + {.type = SSOT_ADD, .topic_filter = "+/+/c"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("+/b/c")); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("#")); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("+/+/#")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("+/+/c")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/b/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("+/+/c"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_remove_branching_path, s_mqtt_subscription_set_remove_branching_path_fn) + +static int s_mqtt_subscription_set_remove_invalid_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/b/c"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("+/b/c")); + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a")); + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b")); + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("#")); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("a/b/c")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_remove_invalid, s_mqtt_subscription_set_remove_invalid_fn) + +static int s_mqtt_subscription_set_remove_empty_segments_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + // Add '///' Subbed, Remove '///' NotSubbed + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "///"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("///"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("////"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("//"))); + + aws_mqtt_subscription_set_remove_subscription(subscription_set, aws_byte_cursor_from_c_str("///")); + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree(subscription_set, aws_byte_cursor_from_c_str("///"))); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_remove_empty_segments, s_mqtt_subscription_set_remove_empty_segments_fn) + +static int s_mqtt_subscription_set_add_remove_repeated_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + const char *topic_filters[] = {"+/+/#", "#", "+/+/c/d", "+/b/c", "+/+/c"}; + size_t filter_count = AWS_ARRAY_SIZE(topic_filters); + + for (size_t i = 0; i < filter_count; ++i) { + for (size_t j = 0; j < filter_count; ++j) { + /* + * this does not cover all permutations (n!) but lacking a permutation generator, this gets "reasonable" + * coverage (n * n) + */ + + /* Add the topic filters in a shifting sequence */ + for (size_t add_index = 0; add_index < filter_count; ++add_index) { + size_t final_index = (add_index + j) % filter_count; + + struct aws_mqtt_subscription_set_subscription_options subscription_options = { + .topic_filter = aws_byte_cursor_from_c_str(topic_filters[final_index]), + }; + aws_mqtt_subscription_set_add_subscription(subscription_set, &subscription_options); + } + + /* One-by-one, remove the topic filters in an independent shifting sequence */ + for (size_t remove_index = 0; remove_index < filter_count; ++remove_index) { + size_t final_remove_index = (remove_index + i) % filter_count; + + aws_mqtt_subscription_set_remove_subscription( + subscription_set, aws_byte_cursor_from_c_str(topic_filters[final_remove_index])); + + for (size_t validate_index = 0; validate_index < filter_count; ++validate_index) { + size_t final_validate_index = (validate_index + i) % filter_count; + if (validate_index <= remove_index) { + ASSERT_FALSE(aws_mqtt_subscription_set_is_in_topic_tree( + subscription_set, aws_byte_cursor_from_c_str(topic_filters[final_validate_index]))); + } else { + ASSERT_TRUE(aws_mqtt_subscription_set_is_in_topic_tree( + subscription_set, aws_byte_cursor_from_c_str(topic_filters[final_validate_index]))); + } + } + } + } + } + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_add_remove_repeated, s_mqtt_subscription_set_add_remove_repeated_fn) + +static int s_mqtt_subscription_set_publish_single_path_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/b/c/d"}, + {.type = SSOT_ADD, .topic_filter = "a"}, + {.type = SSOT_ADD, .topic_filter = "a/b"}, + {.type = SSOT_PUBLISH, .topic = "a/b/c"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + + struct subscription_test_context_callback_record expected_callback_records[] = { + {.topic = aws_byte_cursor_from_c_str("a"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("a/b/c/d"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("a/b"), .callback_count = 1}, + }; + ASSERT_SUCCESS(s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, expected_callback_records, 0)); + + for (size_t i = 0; i < AWS_ARRAY_SIZE(expected_callback_records); ++i) { + struct aws_mqtt_subscription_set_publish_received_options publish_options = { + .topic = expected_callback_records[i].topic, + }; + aws_mqtt_subscription_set_on_publish_received(subscription_set, &publish_options); + + ASSERT_SUCCESS( + s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, expected_callback_records, i + 1)); + } + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_publish_single_path, s_mqtt_subscription_set_publish_single_path_fn) + +static int s_mqtt_subscription_set_publish_multi_path_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/b/c/d"}, + {.type = SSOT_ADD, .topic_filter = "a/b/d"}, + {.type = SSOT_ADD, .topic_filter = "b"}, + {.type = SSOT_ADD, .topic_filter = "c/d"}, + {.type = SSOT_ADD, .topic_filter = "a/c"}, + {.type = SSOT_PUBLISH, .topic = "a"}, + {.type = SSOT_PUBLISH, .topic = "a/b"}, + {.type = SSOT_PUBLISH, .topic = "a/b/c"}, + {.type = SSOT_PUBLISH, .topic = "a/b/c/d/"}, + {.type = SSOT_PUBLISH, .topic = "b/c/d/"}, + {.type = SSOT_PUBLISH, .topic = "c"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + + struct subscription_test_context_callback_record expected_callback_records[] = { + {.topic = aws_byte_cursor_from_c_str("a/b/d"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("c/d"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("a/b/c/d"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("b"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("a/c"), .callback_count = 1}, + }; + + ASSERT_SUCCESS(s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, NULL, 0)); + + for (size_t i = 0; i < AWS_ARRAY_SIZE(expected_callback_records); ++i) { + struct aws_mqtt_subscription_set_publish_received_options publish_options = { + .topic = expected_callback_records[i].topic, + }; + aws_mqtt_subscription_set_on_publish_received(subscription_set, &publish_options); + + ASSERT_SUCCESS( + s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, expected_callback_records, i + 1)); + } + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_publish_multi_path, s_mqtt_subscription_set_publish_multi_path_fn) + +static int s_mqtt_subscription_set_publish_single_level_wildcards_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "a/b/c"}, + {.type = SSOT_ADD, .topic_filter = "+/b/c"}, + {.type = SSOT_ADD, .topic_filter = "a/+/c"}, + {.type = SSOT_ADD, .topic_filter = "a/b/+"}, + {.type = SSOT_ADD, .topic_filter = "+/+/+"}, + {.type = SSOT_ADD, .topic_filter = "a/+/+"}, + {.type = SSOT_PUBLISH, .topic = "a"}, + {.type = SSOT_PUBLISH, .topic = "a/b"}, + {.type = SSOT_PUBLISH, .topic = "a/b/c/d"}, + {.type = SSOT_PUBLISH, .topic = "a/b/c/d/"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + + struct subscription_test_context_callback_record expected_callback_records[] = { + {.topic = aws_byte_cursor_from_c_str("a/b/d"), .callback_count = 3}, + {.topic = aws_byte_cursor_from_c_str("b/c/d"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("a/c/d"), .callback_count = 2}, + {.topic = aws_byte_cursor_from_c_str("c/b/c"), .callback_count = 2}, + {.topic = aws_byte_cursor_from_c_str("a/b/c"), .callback_count = 6}, + }; + + ASSERT_SUCCESS(s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, NULL, 0)); + + for (size_t i = 0; i < AWS_ARRAY_SIZE(expected_callback_records); ++i) { + struct aws_mqtt_subscription_set_publish_received_options publish_options = { + .topic = expected_callback_records[i].topic, + }; + aws_mqtt_subscription_set_on_publish_received(subscription_set, &publish_options); + + ASSERT_SUCCESS( + s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, expected_callback_records, i + 1)); + } + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt_subscription_set_publish_single_level_wildcards, + s_mqtt_subscription_set_publish_single_level_wildcards_fn) + +static int s_mqtt_subscription_set_publish_multi_level_wildcards_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "#"}, + {.type = SSOT_ADD, .topic_filter = "a/#"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c/#"}, + {.type = SSOT_ADD, .topic_filter = "/#"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + + struct subscription_test_context_callback_record expected_callback_records[] = { + {.topic = aws_byte_cursor_from_c_str("b"), .callback_count = 1}, + {.topic = aws_byte_cursor_from_c_str("/"), .callback_count = 2}, + {.topic = aws_byte_cursor_from_c_str("a"), .callback_count = 2}, + {.topic = aws_byte_cursor_from_c_str("a/b"), .callback_count = 2}, + {.topic = aws_byte_cursor_from_c_str("a/b/c"), .callback_count = 3}, + {.topic = aws_byte_cursor_from_c_str("a/b/c/d"), .callback_count = 3}, + {.topic = aws_byte_cursor_from_c_str("/x/y/z"), .callback_count = 2}, + }; + + ASSERT_SUCCESS(s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, NULL, 0)); + + for (size_t i = 0; i < AWS_ARRAY_SIZE(expected_callback_records); ++i) { + struct aws_mqtt_subscription_set_publish_received_options publish_options = { + .topic = expected_callback_records[i].topic, + }; + aws_mqtt_subscription_set_on_publish_received(subscription_set, &publish_options); + + ASSERT_SUCCESS( + s_aws_mqtt_subscription_set_test_context_validate_callbacks(&context, expected_callback_records, i + 1)); + } + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt_subscription_set_publish_multi_level_wildcards, + s_mqtt_subscription_set_publish_multi_level_wildcards_fn) + +static int s_mqtt_subscription_set_get_subscriptions_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_subscription_set_test_context context; + s_aws_mqtt_subscription_set_test_context_init(&context, allocator); + + struct aws_mqtt_subscription_set *subscription_set = aws_mqtt_subscription_set_new(allocator); + + struct subscription_set_operation operations[] = { + {.type = SSOT_ADD, .topic_filter = "#"}, + {.type = SSOT_ADD, .topic_filter = "a/#"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c/#"}, + {.type = SSOT_ADD, .topic_filter = "/#"}, + {.type = SSOT_ADD, .topic_filter = "/"}, + {.type = SSOT_ADD, .topic_filter = "a/b/c"}, + {.type = SSOT_ADD, .topic_filter = "a/#"}, + {.type = SSOT_REMOVE, .topic_filter = "/#"}, + {.type = SSOT_REMOVE, .topic_filter = "/"}, + }; + + s_subscription_set_perform_operations(&context, subscription_set, operations, AWS_ARRAY_SIZE(operations)); + + ASSERT_TRUE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("a/#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("a/b/c/#"))); + ASSERT_TRUE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("a/b/c"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("/#"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("/"))); + ASSERT_FALSE(aws_mqtt_subscription_set_is_subscribed(subscription_set, aws_byte_cursor_from_c_str("a"))); + + struct aws_array_list subscriptions; + aws_mqtt_subscription_set_get_subscriptions(subscription_set, &subscriptions); + + size_t subscription_count = aws_array_list_length(&subscriptions); + ASSERT_INT_EQUALS(4, subscription_count); + for (size_t i = 0; i < subscription_count; ++i) { + struct aws_mqtt_subscription_set_subscription_options subscription; + aws_array_list_get_at(&subscriptions, &subscription, i); + + ASSERT_TRUE(aws_mqtt_subscription_set_is_subscribed(subscription_set, subscription.topic_filter)); + } + + aws_array_list_clean_up(&subscriptions); + + aws_mqtt_subscription_set_destroy(subscription_set); + + s_aws_mqtt_subscription_set_test_context_clean_up(&context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_subscription_set_get_subscriptions, s_mqtt_subscription_set_get_subscriptions_fn) \ No newline at end of file From b777be4aa52d233391781256d40006d353e848b9 Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Thu, 3 Aug 2023 16:08:34 -0700 Subject: [PATCH 81/98] Fix websocket transfer callback userdata & server name in tls_options (#313) * fix user data for websocket callbacks * cheat with tls_options server name * check for tls option copy error * update CR * update cr --- source/v5/mqtt5_to_mqtt3_adapter.c | 31 ++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index 3f91974c..55b47046 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -306,16 +306,31 @@ static struct aws_mqtt_adapter_connect_task *s_aws_mqtt_adapter_connect_task_new aws_task_init(&connect_task->task, s_adapter_connect_task_fn, (void *)connect_task, "AdapterConnectTask"); connect_task->allocator = adapter->allocator; - connect_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); aws_byte_buf_init_copy_from_cursor(&connect_task->host_name, allocator, connection_options->host_name); connect_task->port = connection_options->port; connect_task->socket_options = *connection_options->socket_options; if (connection_options->tls_options) { - aws_tls_connection_options_copy(&connect_task->tls_options, connection_options->tls_options); + if (aws_tls_connection_options_copy(&connect_task->tls_options, connection_options->tls_options)) { + goto error; + } connect_task->tls_options_ptr = &connect_task->tls_options; - } + /* Cheat and set the tls_options host_name to our copy if they're the same */ + if (!connect_task->tls_options.server_name) { + struct aws_byte_cursor host_name_cur = aws_byte_cursor_from_buf(&connect_task->host_name); + + if (aws_tls_connection_options_set_server_name( + &connect_task->tls_options, connect_task->allocator, &host_name_cur)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: mqtt3-to-5-adapter - Failed to set TLS Connection Options server name", + (void *)adapter); + goto error; + } + } + } + connect_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); aws_byte_buf_init_copy_from_cursor(&connect_task->client_id, allocator, connection_options->client_id); connect_task->keep_alive_time_secs = connection_options->keep_alive_time_secs; @@ -326,6 +341,11 @@ static struct aws_mqtt_adapter_connect_task *s_aws_mqtt_adapter_connect_task_new connect_task->clean_session = connection_options->clean_session; return connect_task; + +error: + s_aws_mqtt_adapter_connect_task_destroy(connect_task); + + return NULL; } static int s_validate_adapter_connection_options( @@ -1350,7 +1370,10 @@ static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; (*adapter->websocket_handshake_transformer)( - request, user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); + request, + adapter->websocket_handshake_transformer_user_data, + s_aws_mqtt5_adapter_websocket_handshake_completion_fn, + adapter); } else { (*complete_fn)(args.output_request, args.completion_error_code, complete_ctx); } From b7a323d30ccb12c69a49b70c5e033260054d3a74 Mon Sep 17 00:00:00 2001 From: xiazhvera Date: Thu, 10 Aug 2023 10:16:28 -0700 Subject: [PATCH 82/98] Pushoff keep alive on ack received (#314) * pushoff keep alive onack * make sure sync data is incritical section * set request send time for each request * updat tests * cast timestamp number * sleep to avoid jitter * fix comments --- include/aws/mqtt/private/client_impl.h | 5 ++ source/client_channel_handler.c | 33 ++++++-- tests/CMakeLists.txt | 1 + tests/v3/connection_state_test.c | 100 ++++++++++++++++++++++--- tests/v3/mqtt_mock_server_handler.c | 4 +- 5 files changed, 124 insertions(+), 19 deletions(-) diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 68efb114..68bbdf27 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -128,6 +128,11 @@ struct aws_mqtt_request { struct aws_channel_task outgoing_task; + /* + * The request send time. Currently used to push off keepalive packet. + */ + uint64_t request_send_timestamp; + /* How this operation is currently affecting the statistics of the connection */ enum aws_mqtt_operation_statistic_state_flags statistic_state_flags; /* The encoded size of the packet - used for operation statistics tracking */ diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 6f5502d6..543f0950 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -32,6 +32,28 @@ static void s_update_next_ping_time(struct aws_mqtt_client_connection_311_impl * } } +/* Caches the request send time. The `request_send_timestamp` will be used to push off ping request on request complete. + */ +static void s_update_request_send_time(struct aws_mqtt_request *request) { + if (request->connection != NULL && request->connection->slot != NULL && + request->connection->slot->channel != NULL) { + aws_channel_current_clock_time(request->connection->slot->channel, &request->request_send_timestamp); + } +} + +/* push off next ping time on ack received to last_request_send_timestamp_ns + keep_alive_time_ns + * The function must be called in critical section. */ +static void s_pushoff_next_ping_time( + struct aws_mqtt_client_connection_311_impl *connection, + uint64_t last_request_send_timestamp_ns) { + ASSERT_SYNCED_DATA_LOCK_HELD(connection); + aws_add_u64_checked( + last_request_send_timestamp_ns, connection->keep_alive_time_ns, &last_request_send_timestamp_ns); + if (last_request_send_timestamp_ns > connection->next_ping_time) { + connection->next_ping_time = last_request_send_timestamp_ns; + } +} + /******************************************************************************* * Packet State Machine ******************************************************************************/ @@ -426,7 +448,6 @@ static int s_packet_handler_unsuback(struct aws_byte_cursor message_cursor, void AWS_LS_MQTT_CLIENT, "id=%p: received ack for message id %" PRIu16, (void *)connection, ack.packet_identifier); mqtt_request_complete(connection, AWS_ERROR_SUCCESS, ack.packet_identifier); - return AWS_OP_SUCCESS; } @@ -528,7 +549,6 @@ static int s_packet_handler_pubcomp(struct aws_byte_cursor message_cursor, void AWS_LS_MQTT_CLIENT, "id=%p: received ack for message id %" PRIu16, (void *)connection, ack.packet_identifier); mqtt_request_complete(connection, AWS_ERROR_SUCCESS, ack.packet_identifier); - return AWS_OP_SUCCESS; } @@ -780,6 +800,8 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en /* Send the request */ enum aws_mqtt_client_request_state state = request->send_request(request->packet_id, !request->initiated, request->send_request_ud); + /* Update the request send time.*/ + s_update_request_send_time(request); request->initiated = true; int error_code = AWS_ERROR_SUCCESS; switch (state) { @@ -813,9 +835,6 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en aws_mqtt_connection_statistics_change_operation_statistic_state( request->connection, request, AWS_MQTT_OSS_NONE); - /* Since a request has complete, update the next ping time */ - s_update_next_ping_time(connection); - aws_hash_table_remove( &connection->synced_data.outstanding_requests_table, &request->packet_id, NULL, NULL); aws_memory_pool_release(&connection->synced_data.requests_pool, request); @@ -837,9 +856,6 @@ static void s_request_outgoing_task(struct aws_channel_task *task, void *arg, en aws_mqtt_connection_statistics_change_operation_statistic_state( request->connection, request, AWS_MQTT_OSS_INCOMPLETE | AWS_MQTT_OSS_UNACKED); - /* Since a request has complete, update the next ping time */ - s_update_next_ping_time(connection); - mqtt_connection_unlock_synced_data(connection); } /* END CRITICAL SECTION */ @@ -1016,6 +1032,7 @@ void mqtt_request_complete(struct aws_mqtt_client_connection_311_impl *connectio aws_mqtt_connection_statistics_change_operation_statistic_state( request->connection, request, AWS_MQTT_OSS_NONE); + s_pushoff_next_ping_time(connection, request->request_send_timestamp); /* clean up request resources */ aws_hash_table_remove_element(&connection->synced_data.outstanding_requests_table, elem); /* remove the request from the list, which is thread_data.ongoing_requests_list */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 407bd07f..7af8c4eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,6 +75,7 @@ add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) add_test_case(mqtt_connection_ping_norm) add_test_case(mqtt_connection_ping_no) +add_test_case(mqtt_connection_ping_noack) add_test_case(mqtt_connection_ping_basic_scenario) add_test_case(mqtt_connection_ping_double_scenario) add_test_case(mqtt_connection_close_callback_simple) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 854b0b59..6181d7e8 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -3343,7 +3343,6 @@ static int s_test_mqtt_connection_ping_norm_fn(struct aws_allocator *allocator, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); /* Wait for 4.5 seconds (to account for slight drift/jitter) */ aws_thread_current_sleep(4500000000); @@ -3366,8 +3365,8 @@ AWS_TEST_CASE_FIXTURE( &test_data) /** - * Makes a CONNECT, with 1 second keep alive ping interval, send a publish roughly every second, and then ensure NO - * pings were sent + * Makes a CONNECT, with 1 second keep alive ping interval. Publish QOS1 message for 4.5 seconds and then ensure NO + * pings were sent. (The ping time will be push off on ack ) */ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, void *ctx) { (void)allocator; @@ -3387,10 +3386,19 @@ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, vo ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); s_wait_for_connection_to_complete(state_test_data); - for (int i = 0; i < 4; i++) { - struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); - struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); - uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + + uint64_t begin_timestamp = 0; + uint64_t elapsed_time = 0; + uint64_t now = 0; + aws_high_res_clock_get_ticks(&begin_timestamp); + uint64_t test_duration = (uint64_t)4 * AWS_TIMESTAMP_NANOS; + + // Make sure we publish for 4 seconds; + while (elapsed_time < test_duration) { + /* Publish qos1*/ + uint16_t packet_id = aws_mqtt_client_connection_publish( state_test_data->mqtt_connection, &pub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, @@ -3398,12 +3406,16 @@ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, vo &payload_1, s_on_op_complete, state_test_data); - ASSERT_TRUE(packet_id_1 > 0); + ASSERT_TRUE(packet_id > 0); - /* Wait 0.8 seconds */ - aws_thread_current_sleep(800000000); + aws_thread_current_sleep(500000000); /* Sleep 0.5 seconds to avoid spamming*/ + + aws_high_res_clock_get_ticks(&now); + elapsed_time = now - begin_timestamp; } + aws_thread_current_sleep(250000000); /* Sleep 0.25 seconds to consider jitter*/ + /* Ensure the server got 0 PING packets */ ASSERT_INT_EQUALS(0, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); @@ -3421,6 +3433,74 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) +/** + * Makes a CONNECT, with 1 second keep alive ping interval, publish a qos0 messages for 4.5 seconds. + * We should send a total of 4 pings + */ +static int s_test_mqtt_connection_ping_noack_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + .keep_alive_time_secs = 1, + .ping_timeout_ms = 100, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + + uint64_t begin_timestamp = 0; + uint64_t elapsed_time = 0; + uint64_t now = 0; + aws_high_res_clock_get_ticks(&begin_timestamp); + uint64_t test_duration = (uint64_t)4 * AWS_TIMESTAMP_NANOS; + + // Make sure we publish for 4 seconds; + while (elapsed_time < test_duration) { + /* Publish qos0*/ + uint16_t packet_id = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_MOST_ONCE, + false, + &payload_1, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id > 0); + + aws_thread_current_sleep(500000000); /* Sleep 0.5 seconds to avoid spamming*/ + aws_high_res_clock_get_ticks(&now); + elapsed_time = now - begin_timestamp; + } + + aws_thread_current_sleep(250000000); /* Sleep 0.25 seconds to consider jitter*/ + + /* Ensure the server got 4 PING packets */ + ASSERT_INT_EQUALS(4, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_ping_noack, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_ping_noack_fn, + s_clean_up_mqtt_server_fn, + &test_data) + /** * Test to make sure the PING timing is correct if a publish/packet is sent near the end of the keep alive time. * Note: Because of socket write jitter and scheduling jitter, the times have a 0.25 (quarter of a second) delta range. diff --git a/tests/v3/mqtt_mock_server_handler.c b/tests/v3/mqtt_mock_server_handler.c index 13f7171f..51bb448f 100644 --- a/tests/v3/mqtt_mock_server_handler.c +++ b/tests/v3/mqtt_mock_server_handler.c @@ -187,7 +187,9 @@ static int s_mqtt_mock_server_handler_process_packet( bool auto_ack = server->synced.auto_ack; aws_mutex_unlock(&server->synced.lock); - if (auto_ack) { + uint8_t qos = (publish_packet.fixed_header.flags >> 1) & 0x3; + // Do not send puback if qos0 + if (auto_ack && qos != 0) { struct aws_io_message *puback_msg = aws_channel_acquire_message_from_pool(server->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, 256); struct aws_mqtt_packet_ack puback; From a2ee9a321fcafa19b0473b88a54e0ae8dde5fddf Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 10 Aug 2023 13:30:14 -0700 Subject: [PATCH 83/98] Remove Iot Core specific topic and client id validation checks in the mqtt5 client; preserve (and extend for shared subs) validation utility functions in case we decide to add support in the mqtt311 client (#315) --- .../mqtt/private/v5/mqtt5_options_storage.h | 9 -- include/aws/mqtt/private/v5/mqtt5_utils.h | 5 +- include/aws/mqtt/v5/mqtt5_client.h | 10 +- source/v5/mqtt5_options_storage.c | 107 +--------------- source/v5/mqtt5_utils.c | 87 +++++++------ tests/CMakeLists.txt | 6 - ...mqtt5_operation_validation_failure_tests.c | 118 ------------------ tests/v5/mqtt5_utils_tests.c | 49 ++++++-- 8 files changed, 94 insertions(+), 297 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index ed159972..313a2fd6 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -257,9 +257,6 @@ AWS_MQTT_API struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new AWS_MQTT_API int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish_view *publish_view); -AWS_MQTT_API int aws_mqtt5_packet_publish_view_validate_vs_iot_core( - const struct aws_mqtt5_packet_publish_view *publish_view); - AWS_MQTT_API void aws_mqtt5_packet_publish_view_log( const struct aws_mqtt5_packet_publish_view *publish_view, enum aws_log_level level); @@ -284,9 +281,6 @@ AWS_MQTT_API struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate(const struct aws_mqtt5_packet_subscribe_view *subscribe_view); -AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate_vs_iot_core( - const struct aws_mqtt5_packet_subscribe_view *subscribe_view); - AWS_MQTT_API void aws_mqtt5_packet_subscribe_view_log( const struct aws_mqtt5_packet_subscribe_view *subscribe_view, enum aws_log_level level); @@ -308,9 +302,6 @@ AWS_MQTT_API struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubsc AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate( const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view); -AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core( - const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view); - AWS_MQTT_API void aws_mqtt5_packet_unsubscribe_view_log( const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view, enum aws_log_level level); diff --git a/include/aws/mqtt/private/v5/mqtt5_utils.h b/include/aws/mqtt/private/v5/mqtt5_utils.h index be4c8ba2..3f7bebde 100644 --- a/include/aws/mqtt/private/v5/mqtt5_utils.h +++ b/include/aws/mqtt/private/v5/mqtt5_utils.h @@ -67,10 +67,8 @@ struct aws_mqtt5_negotiated_settings; #define AWS_MQTT5_SUBSCRIBE_FLAGS_QOS_BIT_MASK 0x03 /* Static AWS IoT Core Limit/Quota Values */ -#define AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH 128 #define AWS_IOT_CORE_MAXIMUM_TOPIC_LENGTH 256 #define AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS 8 -#define AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE 8 /* Dynamic IoT Core Limits */ #define AWS_IOT_CORE_PUBLISH_PER_SECOND_LIMIT 100 @@ -328,7 +326,8 @@ AWS_MQTT_API uint64_t aws_mqtt5_client_random_in_range(uint64_t from, uint64_t t * @param topic_cursor topic to get the non-rules suffix for * @return remaining part of the topic after the leading AWS IoT Rules prefix has been skipped, if present */ -AWS_MQTT_API struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte_cursor topic_cursor); +AWS_MQTT_API struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( + struct aws_byte_cursor topic_cursor); /** * Computes the number of topic segments in a topic or topic filter diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index f74ce177..97e1225f 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -181,16 +181,10 @@ enum aws_mqtt5_extended_validation_and_flow_control_options { AWS_MQTT5_EVAFCO_NONE, /** - * Apply additional client-side validation and operational flow control that respects the + * Apply additional client-side operational flow control that respects the * default AWS IoT Core limits. * - * Currently applies the following additional validation: - * (1) No more than 8 subscriptions per SUBSCRIBE packet - * (2) Topics and topic filters have a maximum of 7 slashes (8 segments), not counting any AWS rules prefix - * (3) Topics must be <= 256 bytes in length - * (4) Client id must be <= 128 bytes in length - * - * Also applies the following flow control: + * Applies the following flow control: * (1) Outbound throughput throttled to 512KB/s * (2) Outbound publish TPS throttled to 100 */ diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 95af47bb..465ccf1b 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -80,19 +80,6 @@ size_t aws_mqtt5_user_property_set_size(const struct aws_mqtt5_user_property_set return aws_array_list_length(&property_set->properties); } -int aws_mqtt5_user_property_set_get_property( - const struct aws_mqtt5_user_property_set *property_set, - size_t index, - struct aws_mqtt5_user_property *property_out) { - return aws_array_list_get_at(&property_set->properties, property_out, index); -} - -int aws_mqtt5_user_property_set_add_stored_property( - struct aws_mqtt5_user_property_set *property_set, - struct aws_mqtt5_user_property *property) { - return aws_array_list_push_back(&property_set->properties, property); -} - static void s_aws_mqtt5_user_property_set_log( struct aws_logger *log_handle, const struct aws_mqtt5_user_property *properties, @@ -1725,19 +1712,6 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish return AWS_OP_SUCCESS; } -int aws_mqtt5_packet_publish_view_validate_vs_iot_core(const struct aws_mqtt5_packet_publish_view *publish_view) { - if (!aws_mqtt_is_valid_topic_for_iot_core(publish_view->topic)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_publish_view - topic not valid for AWS Iot Core limits: \"" PRInSTR "\"", - (void *)publish_view, - AWS_BYTE_CURSOR_PRI(publish_view->topic)); - return AWS_OP_ERR; - } - - return AWS_OP_SUCCESS; -} - static int s_aws_mqtt5_packet_publish_view_validate_vs_connection_settings( const void *packet_view, const struct aws_mqtt5_client *client) { @@ -2114,6 +2088,7 @@ struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new( const struct aws_mqtt5_client *client, const struct aws_mqtt5_packet_publish_view *publish_options, const struct aws_mqtt5_publish_completion_options *completion_options) { + (void)client; AWS_PRECONDITION(allocator != NULL); AWS_PRECONDITION(publish_options != NULL); @@ -2130,12 +2105,6 @@ struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new( return NULL; } - if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { - if (aws_mqtt5_packet_publish_view_validate_vs_iot_core(publish_options)) { - return NULL; - } - } - struct aws_mqtt5_operation_publish *publish_op = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_publish)); if (publish_op == NULL) { @@ -2392,25 +2361,6 @@ int aws_mqtt5_packet_unsubscribe_view_validate(const struct aws_mqtt5_packet_uns return AWS_OP_SUCCESS; } -AWS_MQTT_API int aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core( - const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view) { - - for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { - const struct aws_byte_cursor *topic_filter = &unsubscribe_view->topic_filters[i]; - if (!aws_mqtt_is_valid_topic_filter_for_iot_core(*topic_filter)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_unsubscribe_view - topic filter not valid for AWS Iot Core limits: \"" PRInSTR - "\"", - (void *)unsubscribe_view, - AWS_BYTE_CURSOR_PRI(*topic_filter)); - return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); - } - } - - return AWS_OP_SUCCESS; -} - void aws_mqtt5_packet_unsubscribe_view_log( const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_view, enum aws_log_level level) { @@ -2597,6 +2547,7 @@ struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubscribe_new( const struct aws_mqtt5_client *client, const struct aws_mqtt5_packet_unsubscribe_view *unsubscribe_options, const struct aws_mqtt5_unsubscribe_completion_options *completion_options) { + (void)client; AWS_PRECONDITION(allocator != NULL); AWS_PRECONDITION(unsubscribe_options != NULL); @@ -2613,12 +2564,6 @@ struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubscribe_new( return NULL; } - if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { - if (aws_mqtt5_packet_unsubscribe_view_validate_vs_iot_core(unsubscribe_options)) { - return NULL; - } - } - struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_unsubscribe)); if (unsubscribe_op == NULL) { @@ -2774,37 +2719,6 @@ int aws_mqtt5_packet_subscribe_view_validate(const struct aws_mqtt5_packet_subsc return AWS_OP_SUCCESS; } -AWS_MQTT_API int aws_mqtt5_packet_subscribe_view_validate_vs_iot_core( - const struct aws_mqtt5_packet_subscribe_view *subscribe_view) { - - if (subscribe_view->subscription_count > AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_subscribe_view - number of subscriptions (%zu) exceeds default AWS IoT Core limit " - "(%d)", - (void *)subscribe_view, - subscribe_view->subscription_count, - (int)AWS_IOT_CORE_MAXIMUM_SUSBCRIPTIONS_PER_SUBSCRIBE); - return AWS_OP_ERR; - } - - for (size_t i = 0; i < subscribe_view->subscription_count; ++i) { - const struct aws_mqtt5_subscription_view *subscription = &subscribe_view->subscriptions[i]; - const struct aws_byte_cursor *topic_filter = &subscription->topic_filter; - if (!aws_mqtt_is_valid_topic_filter_for_iot_core(*topic_filter)) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_subscribe_view - topic filter not valid for AWS Iot Core limits: \"" PRInSTR - "\"", - (void *)subscribe_view, - AWS_BYTE_CURSOR_PRI(*topic_filter)); - return aws_raise_error(AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION); - } - } - - return AWS_OP_SUCCESS; -} - void aws_mqtt5_packet_subscribe_view_log( const struct aws_mqtt5_packet_subscribe_view *subscribe_view, enum aws_log_level level) { @@ -3016,6 +2930,7 @@ struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe_new( const struct aws_mqtt5_client *client, const struct aws_mqtt5_packet_subscribe_view *subscribe_options, const struct aws_mqtt5_subscribe_completion_options *completion_options) { + (void)client; AWS_PRECONDITION(allocator != NULL); AWS_PRECONDITION(subscribe_options != NULL); @@ -3032,12 +2947,6 @@ struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe_new( return NULL; } - if (client != NULL && client->config->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { - if (aws_mqtt5_packet_subscribe_view_validate_vs_iot_core(subscribe_options)) { - return NULL; - } - } - struct aws_mqtt5_operation_subscribe *subscribe_op = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_operation_subscribe)); if (subscribe_op == NULL) { @@ -3472,16 +3381,6 @@ int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *opt return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); } - if (options->extended_validation_and_flow_control_options != AWS_MQTT5_EVAFCO_NONE) { - if (options->connect_options->client_id.len > AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "AWS IoT Core limits client_id to be less than or equal to %d bytes in length", - (int)AWS_IOT_CORE_MAXIMUM_CLIENT_ID_LENGTH); - return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); - } - } - return AWS_OP_SUCCESS; } diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index 88de757c..848f7580 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -413,7 +413,7 @@ uint64_t aws_mqtt5_client_random_in_range(uint64_t from, uint64_t to) { static uint8_t s_aws_iot_core_rules_prefix[] = "$aws/rules/"; -struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte_cursor topic_cursor) { +static struct aws_byte_cursor s_aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte_cursor topic_cursor) { size_t prefix_length = AWS_ARRAY_SIZE(s_aws_iot_core_rules_prefix) - 1; /* skip 0-terminator */ struct aws_byte_cursor rules_prefix = { @@ -454,47 +454,18 @@ struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_rules_prefix(struct aws_byte return topic_cursor_copy; } -size_t aws_mqtt5_topic_get_segment_count(struct aws_byte_cursor topic_cursor) { - size_t segment_count = 0; - - struct aws_byte_cursor segment_cursor; - AWS_ZERO_STRUCT(segment_cursor); - - while (aws_byte_cursor_next_split(&topic_cursor, '/', &segment_cursor)) { - ++segment_count; - } - - return segment_count; -} - -bool aws_mqtt_is_valid_topic_filter_for_iot_core(struct aws_byte_cursor topic_filter_cursor) { - struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_rules_prefix(topic_filter_cursor); - return aws_mqtt5_topic_get_segment_count(post_rule_suffix) <= AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS; -} - -bool aws_mqtt_is_valid_topic_for_iot_core(struct aws_byte_cursor topic_cursor) { - struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_rules_prefix(topic_cursor); - if (aws_mqtt5_topic_get_segment_count(post_rule_suffix) > AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS) { - return false; - } - - return post_rule_suffix.len <= AWS_IOT_CORE_MAXIMUM_TOPIC_LENGTH; -} - static uint8_t s_shared_subscription_prefix[] = "$share"; static bool s_is_not_hash_or_plus(uint8_t byte) { return byte != '+' && byte != '#'; } -/* $share/{ShareName}/{filter} */ -bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_cursor) { - +static struct aws_byte_cursor s_aws_mqtt5_topic_skip_shared_prefix(struct aws_byte_cursor topic_cursor) { /* shared subscription filters must have an initial segment of "$share" */ struct aws_byte_cursor first_segment_cursor; AWS_ZERO_STRUCT(first_segment_cursor); if (!aws_byte_cursor_next_split(&topic_cursor, '/', &first_segment_cursor)) { - return false; + return topic_cursor; } struct aws_byte_cursor share_prefix_cursor = { @@ -503,7 +474,7 @@ bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_c }; if (!aws_byte_cursor_eq_ignore_case(&share_prefix_cursor, &first_segment_cursor)) { - return false; + return topic_cursor; } /* @@ -512,12 +483,12 @@ bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_c */ struct aws_byte_cursor second_segment_cursor = first_segment_cursor; if (!aws_byte_cursor_next_split(&topic_cursor, '/', &second_segment_cursor)) { - return false; + return topic_cursor; } if (second_segment_cursor.len == 0 || !aws_byte_cursor_satisfies_pred(&second_segment_cursor, s_is_not_hash_or_plus)) { - return false; + return topic_cursor; } /* @@ -527,11 +498,55 @@ bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_c size_t remaining_length = topic_cursor.ptr + topic_cursor.len - (second_segment_cursor.len + second_segment_cursor.ptr); if (remaining_length == 0) { - return false; + return topic_cursor; } aws_byte_cursor_advance(&remaining_cursor, topic_cursor.len - remaining_length + 1); + return remaining_cursor; +} + +struct aws_byte_cursor aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(struct aws_byte_cursor topic_cursor) { + struct aws_byte_cursor skip_shared = s_aws_mqtt5_topic_skip_shared_prefix(topic_cursor); + struct aws_byte_cursor skip_rules = s_aws_mqtt5_topic_skip_aws_iot_rules_prefix(skip_shared); + + return skip_rules; +} + +size_t aws_mqtt5_topic_get_segment_count(struct aws_byte_cursor topic_cursor) { + size_t segment_count = 0; + + struct aws_byte_cursor segment_cursor; + AWS_ZERO_STRUCT(segment_cursor); + + while (aws_byte_cursor_next_split(&topic_cursor, '/', &segment_cursor)) { + ++segment_count; + } + + return segment_count; +} + +bool aws_mqtt_is_valid_topic_filter_for_iot_core(struct aws_byte_cursor topic_filter_cursor) { + struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(topic_filter_cursor); + return aws_mqtt5_topic_get_segment_count(post_rule_suffix) <= AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS; +} + +bool aws_mqtt_is_valid_topic_for_iot_core(struct aws_byte_cursor topic_cursor) { + struct aws_byte_cursor post_rule_suffix = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(topic_cursor); + if (aws_mqtt5_topic_get_segment_count(post_rule_suffix) > AWS_IOT_CORE_MAXIMUM_TOPIC_SEGMENTS) { + return false; + } + + return post_rule_suffix.len <= AWS_IOT_CORE_MAXIMUM_TOPIC_LENGTH; +} + +/* $share/{ShareName}/{filter} */ +bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_cursor) { + struct aws_byte_cursor remaining_cursor = s_aws_mqtt5_topic_skip_shared_prefix(topic_cursor); + if (remaining_cursor.len == topic_cursor.len) { + return false; + } + if (!aws_mqtt_is_valid_topic_filter(&remaining_cursor)) { return false; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7af8c4eb..68cf7591 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -180,10 +180,8 @@ add_test_case(mqtt5_operation_connect_validation_failure_user_properties_value_i add_test_case(mqtt5_operation_connect_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_subscribe_validation_failure_no_subscriptions) add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscriptions) -add_test_case(mqtt5_operation_subscribe_validation_failure_too_many_subscriptions_for_iot_core) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_subscription_identifier) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter) -add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_topic_filter_for_iot_core) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_utf8_topic_filter) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_qos) add_test_case(mqtt5_operation_subscribe_validation_failure_invalid_retain_type) @@ -197,7 +195,6 @@ add_test_case(mqtt5_operation_unsubscribe_validation_failure_no_topic_filters) add_test_case(mqtt5_operation_unsubscribe_validation_failure_too_many_topic_filters) add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter) add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_utf8_topic_filter) -add_test_case(mqtt5_operation_unsubscribe_validation_failure_invalid_topic_filter_for_iot_core) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_name_too_long) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_name_invalid_utf8) add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_value_too_long) @@ -205,8 +202,6 @@ add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_val add_test_case(mqtt5_operation_unsubscribe_validation_failure_user_properties_too_many) add_test_case(mqtt5_operation_publish_validation_failure_invalid_topic) add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_topic) -add_test_case(mqtt5_operation_publish_validation_failure_topic_too_long_for_iot_core) -add_test_case(mqtt5_operation_publish_validation_failure_topic_too_many_slashes_for_iot_core) add_test_case(mqtt5_operation_publish_validation_failure_no_topic) add_test_case(mqtt5_operation_publish_validation_failure_invalid_payload_format) add_test_case(mqtt5_operation_publish_validation_failure_invalid_utf8_payload) @@ -231,7 +226,6 @@ add_test_case(mqtt5_client_options_validation_failure_no_publish_received) add_test_case(mqtt5_client_options_validation_failure_invalid_socket_options) add_test_case(mqtt5_client_options_validation_failure_invalid_connect) add_test_case(mqtt5_client_options_validation_failure_invalid_keep_alive) -add_test_case(mqtt5_client_options_validation_failure_client_id_too_long_for_iot_core) add_test_case(mqtt5_operation_subscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_unsubscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_packet_size) diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c index f0509004..6ef34c9f 100644 --- a/tests/v5/mqtt5_operation_validation_failure_tests.c +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -645,52 +645,6 @@ static void s_make_invalid_no_local_subscribe_view(struct aws_mqtt5_packet_subsc AWS_VALIDATION_FAILURE_TEST3(subscribe, invalid_no_local, s_good_subscribe_view, s_make_invalid_no_local_subscribe_view) -static uint8_t s_too_many_slashes_topic_filter[] = "///a///b///c/d/#"; - -static struct aws_mqtt5_subscription_view s_invalid_topic_filter_for_iot_core_subscription[] = { - { - .topic_filter = - { - .ptr = s_too_many_slashes_topic_filter, - .len = AWS_ARRAY_SIZE(s_too_many_slashes_topic_filter) - 1, - }, - }, -}; - -static void s_make_invalid_topic_filter_for_iot_core_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { - view->subscriptions = s_invalid_topic_filter_for_iot_core_subscription; - view->subscription_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_for_iot_core_subscription); -} - -AWS_IOT_CORE_VALIDATION_FAILURE( - subscribe, - invalid_topic_filter_for_iot_core, - s_good_subscribe_view, - s_make_invalid_topic_filter_for_iot_core_subscribe_view) - -static struct aws_mqtt5_subscription_view s_too_many_subscriptions_for_iot_core_subscription[9]; - -static void s_make_too_many_subscriptions_for_iot_core_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_many_subscriptions_for_iot_core_subscription); ++i) { - struct aws_mqtt5_subscription_view *subscription_view = &s_too_many_subscriptions_for_iot_core_subscription[i]; - - subscription_view->topic_filter = aws_byte_cursor_from_array(s_good_topic, AWS_ARRAY_SIZE(s_good_topic) - 1); - subscription_view->qos = AWS_MQTT5_QOS_AT_MOST_ONCE; - subscription_view->no_local = false; - subscription_view->retain_handling_type = AWS_MQTT5_RHT_SEND_ON_SUBSCRIBE; - subscription_view->retain_as_published = false; - } - - view->subscriptions = s_too_many_subscriptions_for_iot_core_subscription; - view->subscription_count = AWS_ARRAY_SIZE(s_too_many_subscriptions_for_iot_core_subscription); -} - -AWS_IOT_CORE_VALIDATION_FAILURE( - subscribe, - too_many_subscriptions_for_iot_core, - s_good_subscribe_view, - s_make_too_many_subscriptions_for_iot_core_subscribe_view) - static void s_make_user_properties_name_too_long_subscribe_view(struct aws_mqtt5_packet_subscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); view->user_properties = s_bad_user_properties_name; @@ -823,24 +777,6 @@ AWS_VALIDATION_FAILURE_TEST3( s_good_unsubscribe_view, s_make_invalid_utf8_topic_filter_unsubscribe_view) -static struct aws_byte_cursor s_invalid_topic_filter_for_iot_core[] = { - { - .ptr = s_too_many_slashes_topic_filter, - .len = AWS_ARRAY_SIZE(s_too_many_slashes_topic_filter) - 1, - }, -}; - -static void s_make_invalid_topic_filter_for_iot_core_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { - view->topic_filters = s_invalid_topic_filter_for_iot_core; - view->topic_filter_count = AWS_ARRAY_SIZE(s_invalid_topic_filter_for_iot_core); -} - -AWS_IOT_CORE_VALIDATION_FAILURE( - unsubscribe, - invalid_topic_filter_for_iot_core, - s_good_unsubscribe_view, - s_make_invalid_topic_filter_for_iot_core_unsubscribe_view) - static void s_make_user_properties_name_too_long_unsubscribe_view(struct aws_mqtt5_packet_unsubscribe_view *view) { view->user_property_count = AWS_ARRAY_SIZE(s_bad_user_properties_name); view->user_properties = s_bad_user_properties_name; @@ -918,36 +854,6 @@ static void s_make_invalid_utf8_topic_publish_view(struct aws_mqtt5_packet_publi AWS_VALIDATION_FAILURE_TEST3(publish, invalid_utf8_topic, s_good_publish_view, s_make_invalid_utf8_topic_publish_view) -static uint8_t s_topic_too_long_for_iot_core[258]; - -static void s_make_topic_too_long_for_iot_core_publish_view(struct aws_mqtt5_packet_publish_view *view) { - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_topic_too_long_for_iot_core); ++i) { - s_topic_too_long_for_iot_core[i] = 'b'; - } - - view->topic.ptr = s_topic_too_long_for_iot_core; - view->topic.len = AWS_ARRAY_SIZE(s_topic_too_long_for_iot_core) - 1; -} - -AWS_IOT_CORE_VALIDATION_FAILURE( - publish, - topic_too_long_for_iot_core, - s_good_publish_view, - s_make_topic_too_long_for_iot_core_publish_view) - -static uint8_t s_topic_too_many_slashes_for_iot_core[] = "a/b/c/d/efg/h/i/j/k/l/m"; - -static void s_make_topic_too_many_slashes_for_iot_core_publish_view(struct aws_mqtt5_packet_publish_view *view) { - view->topic.ptr = s_topic_too_many_slashes_for_iot_core; - view->topic.len = AWS_ARRAY_SIZE(s_topic_too_many_slashes_for_iot_core) - 1; -} - -AWS_IOT_CORE_VALIDATION_FAILURE( - publish, - topic_too_many_slashes_for_iot_core, - s_good_publish_view, - s_make_topic_too_many_slashes_for_iot_core_publish_view) - static void s_make_no_topic_publish_view(struct aws_mqtt5_packet_publish_view *view) { view->topic.ptr = NULL; view->topic.len = 0; @@ -1277,30 +1183,6 @@ AWS_CLIENT_CREATION_VALIDATION_FAILURE( s_good_client_options, s_make_invalid_keep_alive_client_options) -static uint8_t s_too_long_client_id[129]; - -static struct aws_mqtt5_packet_connect_view s_client_id_too_long_for_iot_core_connect_view = { - .client_id = - { - .ptr = s_too_long_client_id, - .len = AWS_ARRAY_SIZE(s_too_long_client_id), - }, -}; - -static void s_make_client_id_too_long_for_iot_core_client_options(struct aws_mqtt5_client_options *options) { - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_too_long_client_id); ++i) { - s_too_long_client_id[i] = 'a'; - } - - options->connect_options = &s_client_id_too_long_for_iot_core_connect_view; - options->extended_validation_and_flow_control_options = AWS_MQTT5_EVAFCO_AWS_IOT_CORE_DEFAULTS; -} - -AWS_CLIENT_CREATION_VALIDATION_FAILURE( - client_id_too_long_for_iot_core, - s_good_client_options, - s_make_client_id_too_long_for_iot_core_client_options) - #define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_PREFIX(packet_type, failure_reason, init_success_settings_fn) \ static int s_mqtt5_operation_##packet_type##_connection_settings_validation_failure_##failure_reason##_fn( \ struct aws_allocator *allocator, void *ctx) { \ diff --git a/tests/v5/mqtt5_utils_tests.c b/tests/v5/mqtt5_utils_tests.c index c761b45c..fd14d027 100644 --- a/tests/v5/mqtt5_utils_tests.c +++ b/tests/v5/mqtt5_utils_tests.c @@ -17,41 +17,64 @@ static int s_mqtt5_topic_skip_rules_prefix_fn(struct aws_allocator *allocator, v struct aws_byte_cursor expected_cursor; /* nothing should be skipped */ - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("dont/skip/anything")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("dont/skip/anything")); expected_cursor = aws_byte_cursor_from_c_str("dont/skip/anything"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("")); expected_cursor = aws_byte_cursor_from_c_str(""); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("/")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("/")); expected_cursor = aws_byte_cursor_from_c_str("/"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$aws")); expected_cursor = aws_byte_cursor_from_c_str("$aws"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$aws/rules")); expected_cursor = aws_byte_cursor_from_c_str("$aws/rules"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$aws/rules/")); expected_cursor = aws_byte_cursor_from_c_str("$aws/rules/"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename")); expected_cursor = aws_byte_cursor_from_c_str("$aws/rules/rulename"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$share")); + expected_cursor = aws_byte_cursor_from_c_str("$share"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$share/")); + expected_cursor = aws_byte_cursor_from_c_str("$share/"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$share/share-name")); + expected_cursor = aws_byte_cursor_from_c_str("$share/share-name"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + /* prefix should be skipped */ - skip_cursor = aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename/")); + skip_cursor = + aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix(aws_byte_cursor_from_c_str("$aws/rules/rulename/")); expected_cursor = aws_byte_cursor_from_c_str(""); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); - skip_cursor = - aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1/segment2")); + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( + aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1/segment2")); + expected_cursor = aws_byte_cursor_from_c_str("segment1/segment2"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( + aws_byte_cursor_from_c_str("$share/share-name/segment1/segment2")); + expected_cursor = aws_byte_cursor_from_c_str("segment1/segment2"); + ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); + + skip_cursor = aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( + aws_byte_cursor_from_c_str("$share/share-name/$aws/rules/some-rule/segment1/segment2")); expected_cursor = aws_byte_cursor_from_c_str("segment1/segment2"); ASSERT_TRUE(aws_byte_cursor_eq(&skip_cursor, &expected_cursor)); @@ -76,12 +99,12 @@ static int s_mqtt5_topic_get_segment_count_fn(struct aws_allocator *allocator, v ASSERT_INT_EQUALS( 2, - aws_mqtt5_topic_get_segment_count(aws_mqtt5_topic_skip_aws_iot_rules_prefix( + aws_mqtt5_topic_get_segment_count(aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1/segment2")))); ASSERT_INT_EQUALS( 1, - aws_mqtt5_topic_get_segment_count( - aws_mqtt5_topic_skip_aws_iot_rules_prefix(aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1")))); + aws_mqtt5_topic_get_segment_count(aws_mqtt5_topic_skip_aws_iot_core_uncounted_prefix( + aws_byte_cursor_from_c_str("$aws/rules/some-rule/segment1")))); return AWS_OP_SUCCESS; } From c2020271d2334051b7e15ea15bc0190311a06987 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 21 Aug 2023 10:12:40 -0700 Subject: [PATCH 84/98] Match format specifier to integer width in some logging (#266) --- source/v5/mqtt5_client.c | 41 ++++++---------------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index 74afcb6b..ae4ac841 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -208,41 +208,12 @@ static void s_check_timeouts(struct aws_mqtt5_client *client, uint64_t now) { if (operation->ack_timeout_timepoint_ns < now) { /* Timeout for this packet has been reached */ aws_mqtt5_packet_id_t packet_id = aws_mqtt5_operation_get_packet_id(operation); - - switch (operation->packet_type) { - case AWS_MQTT5_PT_SUBSCRIBE: - /* SUBSCRIBE has timed out. */ - AWS_LOGF_INFO( - AWS_LS_MQTT5_CLIENT, - "id=%p: SUBSCRIBE packet with id:%d has timed out", - (void *)client, - packet_id); - break; - - case AWS_MQTT5_PT_UNSUBSCRIBE: - /* UNSUBSCRIBE has timed out. */ - AWS_LOGF_INFO( - AWS_LS_MQTT5_CLIENT, - "id=%p: UNSUBSCRIBE packet with id:%d has timed out", - (void *)client, - packet_id); - break; - - case AWS_MQTT5_PT_PUBLISH: - /* PUBLISH has timed out. */ - AWS_LOGF_INFO( - AWS_LS_MQTT5_CLIENT, - "id=%p: PUBLISH packet with id:%d has timed out", - (void *)client, - packet_id); - - aws_mqtt5_client_flow_control_state_on_puback(client); - break; - - default: - /* something is wrong, there should be no other packet type in this linked list */ - break; - } + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: %s packet with id:%d has timed out", + (void *)client, + aws_mqtt5_packet_type_to_c_string(operation->packet_type), + (int)packet_id); struct aws_hash_element *elem = NULL; aws_hash_table_find(&client->operational_state.unacked_operations_table, &packet_id, &elem); From 9e2b12df0b394d28f3006965c03b880b34a01a25 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Mon, 21 Aug 2023 11:48:59 -0700 Subject: [PATCH 85/98] Address theoretical possibility of a TPS-throttled getting lost (#320) --- source/v5/mqtt5_client.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index ae4ac841..e2c4e7e6 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -2996,14 +2996,18 @@ int aws_mqtt5_client_service_operational_state(struct aws_mqtt5_client_operation struct aws_mqtt5_operation *next_operation = NULL; while (!aws_linked_list_empty(&client_operational_state->queued_operations)) { struct aws_linked_list_node *next_operation_node = - aws_linked_list_pop_front(&client_operational_state->queued_operations); + aws_linked_list_front(&client_operational_state->queued_operations); struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(next_operation_node, struct aws_mqtt5_operation, node); + /* If this is a publish and we're throttled, just quit out of the loop. */ if (s_apply_publish_tps_flow_control(client, operation)) { break; } + /* Wait until flow control has passed before actually dequeuing the operation. */ + aws_linked_list_pop_front(&client_operational_state->queued_operations); + if (!aws_mqtt5_operation_validate_vs_connection_settings(operation, client)) { next_operation = operation; break; From cbd9074a308d7175085b5692cea3cc58edcde4f1 Mon Sep 17 00:00:00 2001 From: Igor Abdrakhimov Date: Mon, 21 Aug 2023 16:28:11 -0700 Subject: [PATCH 86/98] Add termination callback to mqtt311 and mqtt5-to-mqtt3 adapter (#317) If set, on_connection_termination callback is called after the connection is fully destroyed. Co-authored-by: Igor Abdrakhimov Co-authored-by: Vera Xia --- include/aws/mqtt/client.h | 18 ++++ include/aws/mqtt/private/client_impl.h | 2 + include/aws/mqtt/private/client_impl_shared.h | 5 ++ .../private/v5/mqtt5_to_mqtt3_adapter_impl.h | 3 + source/client.c | 31 +++++++ source/client_impl_shared.c | 9 ++ source/v5/mqtt5_to_mqtt3_adapter.c | 82 +++++++++++++++++++ tests/CMakeLists.txt | 3 + tests/v3/connection_state_test.c | 55 +++++++++++++ tests/v5/mqtt5_to_mqtt3_adapter_tests.c | 21 +++++ 10 files changed, 229 insertions(+) diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index 0dd2bc4f..de2934a7 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -161,6 +161,11 @@ typedef void(aws_mqtt_client_publish_received_fn)( /** Called when a connection is closed, right before any resources are deleted */ typedef void(aws_mqtt_client_on_disconnect_fn)(struct aws_mqtt_client_connection *connection, void *userdata); +/** + * Signature of callback invoked on a connection destruction. + */ +typedef void(aws_mqtt_client_on_connection_termination_fn)(void *userdata); + /** * Function to invoke when the websocket handshake request transformation completes. * This function MUST be invoked or the application will soft-lock. @@ -506,6 +511,19 @@ int aws_mqtt_client_connection_set_on_any_publish_handler( aws_mqtt_client_publish_received_fn *on_any_publish, void *on_any_publish_ud); +/** + * Sets the callback to call on a connection destruction. + * + * \param[in] connection The connection object. + * \param[in] on_termination The function to call when a connection is destroyed. + * \param[in] on_termination_ud Userdata for on_termination. + */ +AWS_MQTT_API +int aws_mqtt_client_connection_set_connection_termination_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_ud); + /** * Opens the actual connection defined by aws_mqtt_client_connection_new. * Once the connection is opened, on_connack will be called. Only called when connection is disconnected. diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 68bbdf27..4dc13cf4 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -250,6 +250,8 @@ struct aws_mqtt_client_connection_311_impl { void *on_any_publish_ud; aws_mqtt_client_on_disconnect_fn *on_disconnect; void *on_disconnect_ud; + aws_mqtt_client_on_connection_termination_fn *on_termination; + void *on_termination_ud; aws_mqtt_on_operation_statistics_fn *on_any_operation_statistics; void *on_any_operation_statistics_ud; diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index 8c343436..d244cfe7 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -62,6 +62,11 @@ struct aws_mqtt_client_connection_vtable { aws_mqtt_client_publish_received_fn *on_any_publish, void *on_any_publish_ud); + int (*set_connection_termination_handler_fn)( + void *impl, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_ud); + int (*connect_fn)(void *impl, const struct aws_mqtt_connection_options *connection_options); int (*reconnect_fn)(void *impl, aws_mqtt_client_on_connection_complete_fn *on_connection_complete, void *userdata); diff --git a/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h index 311c0b20..c424db6a 100644 --- a/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h @@ -314,6 +314,9 @@ struct aws_mqtt_client_connection_5_impl { aws_mqtt_client_on_connection_complete_fn *on_connection_complete; void *on_connection_complete_user_data; + + aws_mqtt_client_on_connection_termination_fn *on_termination; + void *on_termination_user_data; }; AWS_EXTERN_C_BEGIN diff --git a/source/client.c b/source/client.c index 8968a8cc..ddaee74d 100644 --- a/source/client.c +++ b/source/client.c @@ -794,6 +794,13 @@ static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connec AWS_LOGF_DEBUG(AWS_LS_MQTT_CLIENT, "id=%p: Destroying connection", (void *)connection); + aws_mqtt_client_on_connection_termination_fn *termination_handler = NULL; + void *termination_handler_user_data = NULL; + if (connection->on_termination != NULL) { + termination_handler = connection->on_termination; + termination_handler_user_data = connection->on_termination_ud; + } + /* If the reconnect_task isn't freed, free it */ if (connection->reconnect_task) { aws_mem_release(connection->reconnect_task->allocator, connection->reconnect_task); @@ -848,6 +855,10 @@ static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connec /* Frees all allocated memory */ aws_mem_release(connection->allocator, connection); + + if (termination_handler != NULL) { + (*termination_handler)(termination_handler_user_data); + } } static void s_on_final_disconnect(struct aws_mqtt_client_connection *connection, void *userdata) { @@ -1159,6 +1170,25 @@ static int s_aws_mqtt_client_connection_311_set_on_any_publish_handler( return AWS_OP_SUCCESS; } +static int s_aws_mqtt_client_connection_311_set_connection_termination_handler( + void *impl, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_ud) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + + AWS_PRECONDITION(connection); + if (s_check_connection_state_for_configuration(connection)) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Setting connection termination handler", (void *)connection); + + connection->on_termination = on_termination; + connection->on_termination_ud = on_termination_ud; + + return AWS_OP_SUCCESS; +} + /******************************************************************************* * Websockets ******************************************************************************/ @@ -3163,6 +3193,7 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311 .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_311_set_connection_interruption_handlers, .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_311_set_connection_closed_handler, .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_311_set_on_any_publish_handler, + .set_connection_termination_handler_fn = s_aws_mqtt_client_connection_311_set_connection_termination_handler, .connect_fn = s_aws_mqtt_client_connection_311_connect, .reconnect_fn = s_aws_mqtt_client_connection_311_reconnect, .disconnect_fn = s_aws_mqtt_client_connection_311_disconnect, diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index f18e77ee..6f65eb88 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -113,6 +113,15 @@ int aws_mqtt_client_connection_set_on_any_publish_handler( return (*connection->vtable->set_on_any_publish_handler_fn)(connection->impl, on_any_publish, on_any_publish_ud); } +int aws_mqtt_client_connection_set_connection_termination_handler( + struct aws_mqtt_client_connection *connection, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_ud) { + + return (*connection->vtable->set_connection_termination_handler_fn)( + connection->impl, on_termination, on_termination_ud); +} + int aws_mqtt_client_connection_connect( struct aws_mqtt_client_connection *connection, const struct aws_mqtt_connection_options *connection_options) { diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index 55b47046..9986de2e 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -69,6 +69,13 @@ static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *ar AWS_LOGF_DEBUG(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: Final destruction of mqtt3-to-5 adapter", (void *)adapter); + aws_mqtt_client_on_connection_termination_fn *termination_handler = NULL; + void *termination_handler_user_data = NULL; + if (adapter->on_termination != NULL) { + termination_handler = adapter->on_termination; + termination_handler_user_data = adapter->on_termination_user_data; + } + if (adapter->client->config->websocket_handshake_transform_user_data == adapter) { /* * If the mqtt5 client is pointing to us for websocket transform, then erase that. The callback @@ -90,6 +97,11 @@ static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *ar aws_mem_release(adapter->allocator, adapter); aws_mem_release(destroy_task->allocator, destroy_task); + + /* trigger the termination callback */ + if (termination_handler) { + termination_handler(termination_handler_user_data); + } } static struct aws_mqtt_adapter_final_destroy_task *s_aws_mqtt_adapter_final_destroy_task_new( @@ -1075,6 +1087,75 @@ static int s_aws_mqtt_client_connection_5_set_on_closed_handler( return AWS_OP_SUCCESS; } +struct aws_mqtt_set_on_termination_handlers_task { + struct aws_task task; + struct aws_allocator *allocator; + struct aws_mqtt_client_connection_5_impl *adapter; + + aws_mqtt_client_on_connection_termination_fn *on_termination_callback; + void *on_termination_ud; +}; + +static void s_set_on_termination_handlers_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + struct aws_mqtt_set_on_termination_handlers_task *set_task = arg; + struct aws_mqtt_client_connection_5_impl *adapter = set_task->adapter; + if (status != AWS_TASK_STATUS_RUN_READY) { + goto done; + } + + adapter->on_termination = set_task->on_termination_callback; + adapter->on_termination_user_data = set_task->on_termination_ud; + +done: + + aws_ref_count_release(&adapter->internal_refs); + + aws_mem_release(set_task->allocator, set_task); +} + +static struct aws_mqtt_set_on_termination_handlers_task *s_aws_mqtt_set_on_termination_handler_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection_5_impl *adapter, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_user_data) { + + struct aws_mqtt_set_on_termination_handlers_task *set_task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_set_on_termination_handlers_task)); + + aws_task_init(&set_task->task, s_set_on_termination_handlers_task_fn, (void *)set_task, "SetOnClosedHandlerTask"); + set_task->allocator = adapter->allocator; + set_task->adapter = (struct aws_mqtt_client_connection_5_impl *)aws_ref_count_acquire(&adapter->internal_refs); + set_task->on_termination_callback = on_termination; + set_task->on_termination_ud = on_termination_user_data; + + return set_task; +} + +static int s_aws_mqtt_client_connection_5_set_termination_handler( + void *impl, + aws_mqtt_client_on_connection_termination_fn *on_termination, + void *on_termination_ud) { + struct aws_mqtt_client_connection_5_impl *adapter = impl; + + struct aws_mqtt_set_on_termination_handlers_task *task = + s_aws_mqtt_set_on_termination_handler_task_new(adapter->allocator, adapter, on_termination, on_termination_ud); + if (task == NULL) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + "id=%p: failed to create set on closed handler task, error code %d(%s)", + (void *)adapter, + error_code, + aws_error_debug_str(error_code)); + return AWS_OP_ERR; + } + + aws_event_loop_schedule_task_now(adapter->loop, &task->task); + + return AWS_OP_SUCCESS; +} + struct aws_mqtt_set_on_any_publish_handler_task { struct aws_task task; struct aws_allocator *allocator; @@ -2877,6 +2958,7 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_v .set_connection_result_handlers = s_aws_mqtt_client_connection_5_set_connection_result_handlers, .set_connection_interruption_handlers_fn = s_aws_mqtt_client_connection_5_set_interruption_handlers, .set_connection_closed_handler_fn = s_aws_mqtt_client_connection_5_set_on_closed_handler, + .set_connection_termination_handler_fn = s_aws_mqtt_client_connection_5_set_termination_handler, .set_on_any_publish_handler_fn = s_aws_mqtt_client_connection_5_set_on_any_publish_handler, .connect_fn = s_aws_mqtt_client_connection_5_connect, .reconnect_fn = s_aws_mqtt_client_connection_5_reconnect, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 68cf7591..961c313f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -96,6 +96,9 @@ add_test_case(mqtt_operation_statistics_simple_unsubscribe) add_test_case(mqtt_operation_statistics_simple_resubscribe) add_test_case(mqtt_operation_statistics_simple_callback) +# Connection termination tests +add_test_case(mqtt_connection_termination_callback_simple) + # MQTT5 tests # topic utilities diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 6181d7e8..5fa2b409 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -60,6 +60,7 @@ struct mqtt_connection_state_test { bool connection_resumed; bool subscribe_completed; bool listener_destroyed; + bool connection_terminated; int interruption_error; int subscribe_complete_error; int op_complete_error; @@ -80,6 +81,8 @@ struct mqtt_connection_state_test { size_t ops_completed; size_t expected_ops_completed; size_t connection_close_calls; /* All of the times on_connection_closed has been called */ + + size_t connection_termination_calls; /* How many times on_connection_termination has been called, should be 1 */ }; static struct mqtt_connection_state_test test_data = {0}; @@ -266,6 +269,29 @@ static void s_wait_for_connection_to_fail(struct mqtt_connection_state_test *sta aws_mutex_unlock(&state_test_data->lock); } +static bool s_is_termination_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_terminated; +} + +static void s_wait_for_termination_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_termination_completed, state_test_data); + state_test_data->connection_terminated = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_on_connection_termination_fn(void *userdata) { + struct mqtt_connection_state_test *state_test_data = (struct mqtt_connection_state_test *)userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_termination_calls += 1; + state_test_data->connection_terminated = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + /** sets up a unix domain socket server and socket options. Creates an mqtt connection configured to use * the domain socket. */ @@ -350,6 +376,10 @@ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_SUCCESS(aws_array_list_init_dynamic( &state_test_data->any_published_messages, allocator, 4, sizeof(struct received_publish_packet))); ASSERT_SUCCESS(aws_array_list_init_dynamic(&state_test_data->qos_returned, allocator, 2, sizeof(uint8_t))); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_termination_handler( + state_test_data->mqtt_connection, s_on_connection_termination_fn, state_test_data)); + return AWS_OP_SUCCESS; } @@ -373,6 +403,10 @@ static int s_clean_up_mqtt_server_fn(struct aws_allocator *allocator, int setup_ s_received_publish_packet_list_clean_up(&state_test_data->any_published_messages); aws_array_list_clean_up(&state_test_data->qos_returned); aws_mqtt_client_connection_release(state_test_data->mqtt_connection); + + s_wait_for_termination_to_complete(state_test_data); + ASSERT_UINT_EQUALS(1, state_test_data->connection_termination_calls); + aws_mqtt_client_release(state_test_data->mqtt_client); aws_client_bootstrap_release(state_test_data->client_bootstrap); aws_host_resolver_release(state_test_data->host_resolver); @@ -3682,3 +3716,24 @@ AWS_TEST_CASE_FIXTURE( s_test_mqtt_connection_ping_double_scenario_fn, s_clean_up_mqtt_server_fn, &test_data) + +/** + * Test that the connection termination callback is fired for the connection that was not actually connected ever. + * \note Other tests use on_connection_termination callback as well, so one simple dedicated case is enough. + */ +static int s_test_mqtt_connection_termination_callback_simple_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_termination_handler( + state_test_data->mqtt_connection, s_on_connection_termination_fn, state_test_data)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_termination_callback_simple, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_termination_callback_simple_fn, + s_clean_up_mqtt_server_fn, + &test_data) diff --git a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c index 272b7326..8c85e884 100644 --- a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c +++ b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c @@ -30,6 +30,7 @@ enum aws_mqtt3_lifecycle_event_type { AWS_MQTT3_LET_DISCONNECTION_COMPLETE, AWS_MQTT3_LET_CONNECTION_SUCCESS, AWS_MQTT3_LET_CONNECTION_FAILURE, + AWS_MQTT3_LET_TERMINATION, }; struct aws_mqtt3_lifecycle_event { @@ -441,6 +442,22 @@ static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_closed_handler( aws_condition_variable_notify_all(&fixture->signal); } +static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_termination_handler(void *userdata) { + struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture = userdata; + + /* record the event */ + struct aws_mqtt3_lifecycle_event event; + AWS_ZERO_STRUCT(event); + + event.type = AWS_MQTT3_LET_TERMINATION; + aws_high_res_clock_get_ticks(&event.timestamp); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->lifecycle_events, &event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_interrupted_handler( struct aws_mqtt_client_connection *connection, int error_code, @@ -602,6 +619,8 @@ int aws_mqtt5_to_mqtt3_adapter_test_fixture_init( aws_mutex_init(&fixture->lock); aws_condition_variable_init(&fixture->signal); + aws_mqtt_client_connection_set_connection_termination_handler( + fixture->connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_termination_handler, fixture); aws_mqtt_client_connection_set_connection_closed_handler( fixture->connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_closed_handler, fixture); aws_mqtt_client_connection_set_connection_interruption_handlers( @@ -623,6 +642,8 @@ int aws_mqtt5_to_mqtt3_adapter_test_fixture_init( void aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(struct aws_mqtt5_to_mqtt3_adapter_test_fixture *fixture) { aws_mqtt_client_connection_release(fixture->connection); + s_wait_for_n_adapter_lifecycle_events(fixture, AWS_MQTT3_LET_TERMINATION, 1); + aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->mqtt5_fixture); aws_array_list_clean_up(&fixture->lifecycle_events); From 9fc57a13bce0fbcc50d1fdb4cc52c9107d4c7dc9 Mon Sep 17 00:00:00 2001 From: Waqar Ahmed Khan Date: Wed, 23 Aug 2023 15:55:05 -0700 Subject: [PATCH 87/98] Explicit Private for target_link_libraries (#322) --- bin/elastipubsub/CMakeLists.txt | 2 +- bin/elastipubsub5/CMakeLists.txt | 2 +- bin/mqtt5canary/CMakeLists.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/elastipubsub/CMakeLists.txt b/bin/elastipubsub/CMakeLists.txt index f3708117..c011697c 100644 --- a/bin/elastipubsub/CMakeLists.txt +++ b/bin/elastipubsub/CMakeLists.txt @@ -15,7 +15,7 @@ target_include_directories(${ELASTIPUBSUB_PROJECT_NAME} PUBLIC $ $) -target_link_libraries(${ELASTIPUBSUB_PROJECT_NAME} aws-c-mqtt) +target_link_libraries(${ELASTIPUBSUB_PROJECT_NAME} PRIVATE aws-c-mqtt) if (BUILD_SHARED_LIBS AND NOT WIN32) message(INFO " elastiPUBSUB will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") diff --git a/bin/elastipubsub5/CMakeLists.txt b/bin/elastipubsub5/CMakeLists.txt index 727fdbbe..4d2f36ba 100644 --- a/bin/elastipubsub5/CMakeLists.txt +++ b/bin/elastipubsub5/CMakeLists.txt @@ -15,7 +15,7 @@ target_include_directories(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} PUBLIC $ $) -target_link_libraries(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} aws-c-mqtt) +target_link_libraries(${ELASTIPUBSUB_MQTT5_PROJECT_NAME} PRIVATE aws-c-mqtt) if (BUILD_SHARED_LIBS AND NOT WIN32) message(INFO " elastiPUBSUB will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") diff --git a/bin/mqtt5canary/CMakeLists.txt b/bin/mqtt5canary/CMakeLists.txt index c1d381b5..fcdd5412 100644 --- a/bin/mqtt5canary/CMakeLists.txt +++ b/bin/mqtt5canary/CMakeLists.txt @@ -15,7 +15,7 @@ target_include_directories(${MQTT5CANARY_PROJECT_NAME} PUBLIC $ $) -target_link_libraries(${MQTT5CANARY_PROJECT_NAME} aws-c-mqtt) +target_link_libraries(${MQTT5CANARY_PROJECT_NAME} PRIVATE aws-c-mqtt) if (BUILD_SHARED_LIBS AND NOT WIN32) message(INFO " mqtt5canary will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") From 6ee615f7eaed4195e6cb5b32b17588aad4014c4f Mon Sep 17 00:00:00 2001 From: Yasmine Talby <108372980+yasminetalby@users.noreply.github.com> Date: Fri, 25 Aug 2023 09:42:50 +0200 Subject: [PATCH 88/98] added workflow for handling answerable discussions (#318) Co-authored-by: shailja Co-authored-by: Waqar Ahmed Khan --- .github/workflows/handle-stale-discussions.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/handle-stale-discussions.yml diff --git a/.github/workflows/handle-stale-discussions.yml b/.github/workflows/handle-stale-discussions.yml new file mode 100644 index 00000000..2b89f2da --- /dev/null +++ b/.github/workflows/handle-stale-discussions.yml @@ -0,0 +1,18 @@ +name: HandleStaleDiscussions +on: + schedule: + - cron: '0 */4 * * *' + discussion_comment: + types: [created] + +jobs: + handle-stale-discussions: + name: Handle stale discussions + runs-on: ubuntu-latest + permissions: + discussions: write + steps: + - name: Stale discussions action + uses: aws-github-ops/handle-stale-discussions@v1 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file From ee06ce3db902d1760874c6d950e309cd936f8523 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 29 Aug 2023 10:14:36 -0700 Subject: [PATCH 89/98] Mqtt311 compliance * Adds outbound utf-8 validation to MQTT311 * Updates QoS 1 publish session resumption behavior to include dup flag * Adds many additional MQTT311 compliance tests Co-authored-by: Bret Ambrose --- include/aws/mqtt/mqtt.h | 9 + include/aws/mqtt/private/packets.h | 3 + include/aws/mqtt/private/v5/mqtt5_utils.h | 8 - include/aws/mqtt/v5/mqtt5_client.h | 12 - source/client.c | 52 ++- source/mqtt.c | 43 +- source/packets.c | 4 + source/v5/mqtt5_options_storage.c | 22 +- source/v5/mqtt5_utils.c | 53 --- tests/CMakeLists.txt | 19 +- tests/shared_utils_tests.c | 158 ++++++++ tests/v3/connection_state_test.c | 463 ++++++++++++++++++++-- tests/v3/mqtt_mock_server_handler.c | 26 +- tests/v3/mqtt_mock_server_handler.h | 14 + tests/v3/packet_encoding_test.c | 136 +++++++ tests/v3/topic_tree_test.c | 51 ++- tests/v5/mqtt5_utils_tests.c | 149 ------- 17 files changed, 944 insertions(+), 278 deletions(-) create mode 100644 tests/shared_utils_tests.c diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 7385b772..9fc8001c 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -101,9 +101,18 @@ AWS_EXTERN_C_BEGIN AWS_MQTT_API bool aws_mqtt_is_valid_topic(const struct aws_byte_cursor *topic); + AWS_MQTT_API bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter); +/** + * Validate utf-8 string under mqtt specs + * + * @param text + * @return AWS_OP_SUCCESS if the text is validate, otherwise AWS_OP_ERR + */ +AWS_MQTT_API int aws_mqtt_validate_utf8_text(struct aws_byte_cursor text); + /** * Initializes internal datastructures used by aws-c-mqtt. * Must be called before using any functionality in aws-c-mqtt. diff --git a/include/aws/mqtt/private/packets.h b/include/aws/mqtt/private/packets.h index 94a75912..c3b63ac5 100644 --- a/include/aws/mqtt/private/packets.h +++ b/include/aws/mqtt/private/packets.h @@ -219,6 +219,9 @@ int aws_mqtt_packet_publish_encode_headers(struct aws_byte_buf *buf, const struc AWS_MQTT_API int aws_mqtt_packet_publish_decode(struct aws_byte_cursor *cur, struct aws_mqtt_packet_publish *packet); +AWS_MQTT_API +void aws_mqtt_packet_publish_set_dup(struct aws_mqtt_packet_publish *packet); + AWS_MQTT_API bool aws_mqtt_packet_publish_get_dup(const struct aws_mqtt_packet_publish *packet); diff --git a/include/aws/mqtt/private/v5/mqtt5_utils.h b/include/aws/mqtt/private/v5/mqtt5_utils.h index 3f7bebde..9a05dedf 100644 --- a/include/aws/mqtt/private/v5/mqtt5_utils.h +++ b/include/aws/mqtt/private/v5/mqtt5_utils.h @@ -94,14 +94,6 @@ AWS_EXTERN_C_BEGIN */ AWS_MQTT_API extern struct aws_byte_cursor g_aws_mqtt5_connect_protocol_cursor; -/** - * Validate utf-8 string under mqtt5 specs - * - * @param text - * @return AWS_OP_SUCCESS if the text is validate, otherwise AWS_OP_ERR - */ -AWS_MQTT_API int aws_mqtt5_validate_utf8_text(struct aws_byte_cursor text); - /** * Simple helper function to compute the first byte of an MQTT packet encoding as a function of 4 bit flags * and the packet type. diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 97e1225f..025c03ba 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -787,18 +787,6 @@ AWS_MQTT_API int aws_mqtt5_negotiated_settings_init( struct aws_mqtt5_negotiated_settings *negotiated_settings, const struct aws_byte_cursor *client_id); -/** - * Makes an owning copy of a negotiated settings structure - * - * @param source settings to copy from - * @param dest settings to copy into. Must be in a zeroed or initialized state because it gets clean up - * called on it as the first step of the copy process. - * @return success/failure - */ -AWS_MQTT_API int aws_mqtt5_negotiated_settings_copy( - const struct aws_mqtt5_negotiated_settings *source, - struct aws_mqtt5_negotiated_settings *dest); - /** * Clean up owned memory in negotiated_settings * diff --git a/source/client.c b/source/client.c index ddaee74d..c332c4da 100644 --- a/source/client.c +++ b/source/client.c @@ -942,6 +942,16 @@ static int s_aws_mqtt_client_connection_311_set_will( return aws_raise_error(AWS_ERROR_INVALID_STATE); } + if (!aws_mqtt_is_valid_topic(topic)) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Will topic is invalid", (void *)connection); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); + } + + if (qos > AWS_MQTT_QOS_EXACTLY_ONCE) { + AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Will qos is invalid", (void *)connection); + return aws_raise_error(AWS_ERROR_MQTT_INVALID_QOS); + } + int result = AWS_OP_ERR; AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, @@ -949,11 +959,6 @@ static int s_aws_mqtt_client_connection_311_set_will( (void *)connection, AWS_BYTE_CURSOR_PRI(*topic)); - if (!aws_mqtt_is_valid_topic(topic)) { - AWS_LOGF_ERROR(AWS_LS_MQTT_CLIENT, "id=%p: Will topic is invalid", (void *)connection); - return aws_raise_error(AWS_ERROR_MQTT_INVALID_TOPIC); - } - struct aws_byte_buf local_topic_buf; struct aws_byte_buf local_payload_buf; AWS_ZERO_STRUCT(local_topic_buf); @@ -1007,6 +1012,12 @@ static int s_aws_mqtt_client_connection_311_set_login( return aws_raise_error(AWS_ERROR_INVALID_STATE); } + if (username != NULL && aws_mqtt_validate_utf8_text(*username) == AWS_OP_ERR) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_CLIENT, "id=%p: Invalid utf8 or forbidden codepoints in username", (void *)connection); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + int result = AWS_OP_ERR; AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: Setting username and password", (void *)connection); @@ -1428,6 +1439,16 @@ static int s_aws_mqtt_client_connection_311_connect( struct aws_mqtt_client_connection_311_impl *connection = impl; + if (connection_options == NULL) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (aws_mqtt_validate_utf8_text(connection_options->client_id) == AWS_OP_ERR) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_CLIENT, "id=%p: Invalid utf8 or forbidden codepoints in client id", (void *)connection); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + /* TODO: Do we need to support resuming the connection if user connect to the same connection & endpoint and the * clean_session is false? * If not, the broker will resume the connection in this case, and we pretend we are making a new connection, which @@ -1957,6 +1978,11 @@ static uint16_t s_aws_mqtt_client_connection_311_subscribe_multiple( AWS_PRECONDITION(connection); + if (topic_filters == NULL || aws_array_list_length(topic_filters) == 0) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return 0; + } + struct subscribe_task_arg *task_arg = aws_mem_calloc(connection->allocator, 1, sizeof(struct subscribe_task_arg)); if (!task_arg) { return 0; @@ -2804,6 +2830,8 @@ static enum aws_mqtt_client_request_state s_publish_send(uint16_t packet_id, boo return AWS_MQTT_CLIENT_REQUEST_ERROR; } + } else { + aws_mqtt_packet_publish_set_dup(&task_arg->publish); } struct aws_io_message *message = mqtt_get_message_for_packet(task_arg->connection, &task_arg->publish.fixed_header); @@ -2919,6 +2947,11 @@ static uint16_t s_aws_mqtt_client_connection_311_publish( return 0; } + if (qos > AWS_MQTT_QOS_EXACTLY_ONCE) { + aws_raise_error(AWS_ERROR_MQTT_INVALID_QOS); + return 0; + } + struct publish_task_arg *arg = aws_mem_calloc(connection->allocator, 1, sizeof(struct publish_task_arg)); if (!arg) { return 0; @@ -2929,7 +2962,14 @@ static uint16_t s_aws_mqtt_client_connection_311_publish( arg->topic = aws_byte_cursor_from_string(arg->topic_string); arg->qos = qos; arg->retain = retain; - if (aws_byte_buf_init_copy_from_cursor(&arg->payload_buf, connection->allocator, *payload)) { + + struct aws_byte_cursor payload_cursor; + AWS_ZERO_STRUCT(payload_cursor); + if (payload != NULL) { + payload_cursor = *payload; + } + + if (aws_byte_buf_init_copy_from_cursor(&arg->payload_buf, connection->allocator, payload_cursor)) { goto handle_error; } arg->payload = aws_byte_cursor_from_buf(&arg->payload_buf); diff --git a/source/mqtt.c b/source/mqtt.c index 95a1a088..ac8c4529 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -5,8 +5,8 @@ #include +#include #include - #include /******************************************************************************* @@ -14,12 +14,19 @@ ******************************************************************************/ static bool s_is_valid_topic(const struct aws_byte_cursor *topic, bool is_filter) { + if (topic == NULL) { + return false; + } /* [MQTT-4.7.3-1] Check existance and length */ if (!topic->ptr || !topic->len) { return false; } + if (aws_mqtt_validate_utf8_text(*topic) == AWS_OP_ERR) { + return false; + } + /* [MQTT-4.7.3-2] Check for the null character */ if (memchr(topic->ptr, 0, topic->len)) { return false; @@ -286,3 +293,37 @@ void aws_mqtt_fatal_assert_library_initialized(void) { AWS_FATAL_ASSERT(s_mqtt_library_initialized); } } + +/* UTF-8 encoded string validation respect to [MQTT-1.5.3-2]. */ +static int aws_mqtt_utf8_decoder(uint32_t codepoint, void *user_data) { + (void)user_data; + /* U+0000 - A UTF-8 Encoded String MUST NOT include an encoding of the null character U+0000. [MQTT-1.5.4-2] + * U+0001..U+001F control characters are not valid + */ + if (AWS_UNLIKELY(codepoint <= 0x001F)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + /* U+007F..U+009F control characters are not valid */ + if (AWS_UNLIKELY((codepoint >= 0x007F) && (codepoint <= 0x009F))) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + /* Unicode non-characters are not valid: https://www.unicode.org/faq/private_use.html#nonchar1 */ + if (AWS_UNLIKELY((codepoint & 0x00FFFF) >= 0x00FFFE)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + if (AWS_UNLIKELY(codepoint >= 0xFDD0 && codepoint <= 0xFDEF)) { + return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); + } + + return AWS_ERROR_SUCCESS; +} + +static struct aws_utf8_decoder_options s_aws_mqtt_utf8_decoder_options = { + .on_codepoint = aws_mqtt_utf8_decoder, +}; + +int aws_mqtt_validate_utf8_text(struct aws_byte_cursor text) { + return aws_decode_utf8(text, &s_aws_mqtt_utf8_decoder_options); +} diff --git a/source/packets.c b/source/packets.c index 7af2af26..1170bcca 100644 --- a/source/packets.c +++ b/source/packets.c @@ -595,6 +595,10 @@ bool aws_mqtt_packet_publish_get_dup(const struct aws_mqtt_packet_publish *packe return packet->fixed_header.flags & (1 << 3); /* bit 3 */ } +void aws_mqtt_packet_publish_set_dup(struct aws_mqtt_packet_publish *packet) { + packet->fixed_header.flags |= 0x08; +} + enum aws_mqtt_qos aws_mqtt_packet_publish_get_qos(const struct aws_mqtt_packet_publish *packet) { return (packet->fixed_header.flags >> 1) & 0x3; /* bits 2,1 */ } diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 465ccf1b..10625fa5 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -172,7 +172,7 @@ static int s_aws_mqtt5_user_property_set_validate( return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(property->name)) { + if (aws_mqtt_validate_utf8_text(property->name)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: %s - user property #%zu name not valid UTF8", log_context, log_prefix, i); return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); @@ -187,7 +187,7 @@ static int s_aws_mqtt5_user_property_set_validate( property->value.len); return aws_raise_error(AWS_ERROR_MQTT5_USER_PROPERTY_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(property->value)) { + if (aws_mqtt_validate_utf8_text(property->value)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: %s - user property #%zu value not valid UTF8", @@ -332,7 +332,7 @@ int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(connect_options->client_id)) { + if (aws_mqtt_validate_utf8_text(connect_options->client_id)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_connect_view - client id not valid UTF-8", @@ -349,7 +349,7 @@ int aws_mqtt5_packet_connect_view_validate(const struct aws_mqtt5_packet_connect return aws_raise_error(AWS_ERROR_MQTT5_CONNECT_OPTIONS_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(*connect_options->username)) { + if (aws_mqtt_validate_utf8_text(*connect_options->username)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_connect_view - username not valid UTF-8", @@ -1259,7 +1259,7 @@ int aws_mqtt5_packet_disconnect_view_validate(const struct aws_mqtt5_packet_disc return aws_raise_error(AWS_ERROR_MQTT5_DISCONNECT_OPTIONS_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(*disconnect_view->reason_string)) { + if (aws_mqtt_validate_utf8_text(*disconnect_view->reason_string)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_disconnect_view - reason string not valid UTF-8", @@ -1591,7 +1591,7 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - missing topic", (void *)publish_view); return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); - } else if (aws_mqtt5_validate_utf8_text(publish_view->topic)) { + } else if (aws_mqtt_validate_utf8_text(publish_view->topic)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - topic not valid UTF-8", (void *)publish_view); return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); @@ -1626,7 +1626,7 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish // Make sure the payload data is UTF-8 if the payload_format set to UTF8 if (*publish_view->payload_format == AWS_MQTT5_PFI_UTF8) { - if (aws_mqtt5_validate_utf8_text(publish_view->payload)) { + if (aws_mqtt_validate_utf8_text(publish_view->payload)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - payload value is not valid UTF-8 while payload format " @@ -1646,7 +1646,7 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(*publish_view->response_topic)) { + if (aws_mqtt_validate_utf8_text(*publish_view->response_topic)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - response topic not valid UTF-8", @@ -1692,7 +1692,7 @@ int aws_mqtt5_packet_publish_view_validate(const struct aws_mqtt5_packet_publish return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } - if (aws_mqtt5_validate_utf8_text(*publish_view->content_type)) { + if (aws_mqtt_validate_utf8_text(*publish_view->content_type)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_publish_view - content type not valid UTF-8", @@ -2332,7 +2332,7 @@ int aws_mqtt5_packet_unsubscribe_view_validate(const struct aws_mqtt5_packet_uns for (size_t i = 0; i < unsubscribe_view->topic_filter_count; ++i) { const struct aws_byte_cursor *topic_filter = &unsubscribe_view->topic_filters[i]; - if (aws_mqtt5_validate_utf8_text(*topic_filter)) { + if (aws_mqtt_validate_utf8_text(*topic_filter)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_unsubscribe_view - topic filter not valid UTF-8: \"" PRInSTR "\"", @@ -2603,7 +2603,7 @@ static int s_aws_mqtt5_validate_subscription( const struct aws_mqtt5_subscription_view *subscription, void *log_context) { - if (aws_mqtt5_validate_utf8_text(subscription->topic_filter)) { + if (aws_mqtt_validate_utf8_text(subscription->topic_filter)) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, "id=%p: aws_mqtt5_packet_subscribe_view - topic filter \"" PRInSTR "\" not valid UTF-8 in subscription", diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index 848f7580..d4444203 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -7,7 +7,6 @@ #include #include -#include #include uint8_t aws_mqtt5_compute_fixed_header_byte1(enum aws_mqtt5_packet_type packet_type, uint8_t flags) { @@ -124,24 +123,6 @@ int aws_mqtt5_negotiated_settings_init( return AWS_OP_SUCCESS; } -int aws_mqtt5_negotiated_settings_copy( - const struct aws_mqtt5_negotiated_settings *source, - struct aws_mqtt5_negotiated_settings *dest) { - aws_mqtt5_negotiated_settings_clean_up(dest); - - *dest = *source; - AWS_ZERO_STRUCT(dest->client_id_storage); - - if (source->client_id_storage.allocator != NULL) { - return aws_byte_buf_init_copy_from_cursor( - &dest->client_id_storage, - source->client_id_storage.allocator, - aws_byte_cursor_from_buf(&source->client_id_storage)); - } - - return AWS_OP_SUCCESS; -} - int aws_mqtt5_negotiated_settings_apply_client_id( struct aws_mqtt5_negotiated_settings *negotiated_settings, const struct aws_byte_cursor *client_id) { @@ -553,37 +534,3 @@ bool aws_mqtt_is_topic_filter_shared_subscription(struct aws_byte_cursor topic_c return true; } - -/* UTF-8 encoded string validation respect to [MQTT-1.5.3-2]. */ -static int aws_mqtt5_utf8_decoder(uint32_t codepoint, void *user_data) { - (void)user_data; - /* U+0000 - A UTF-8 Encoded String MUST NOT include an encoding of the null character U+0000. [MQTT-1.5.4-2] - * U+0001..U+001F control characters are not valid - */ - if (AWS_UNLIKELY(codepoint <= 0x001F)) { - return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); - } - - /* U+007F..U+009F control characters are not valid */ - if (AWS_UNLIKELY((codepoint >= 0x007F) && (codepoint <= 0x009F))) { - return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); - } - - /* Unicode non-characters are not valid: https://www.unicode.org/faq/private_use.html#nonchar1 */ - if (AWS_UNLIKELY((codepoint & 0x00FFFF) >= 0x00FFFE)) { - return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); - } - if (AWS_UNLIKELY(codepoint >= 0xFDD0 && codepoint <= 0xFDEF)) { - return aws_raise_error(AWS_ERROR_MQTT5_INVALID_UTF8_STRING); - } - - return AWS_ERROR_SUCCESS; -} - -struct aws_utf8_decoder_options g_aws_mqtt5_utf8_decoder_options = { - .on_codepoint = aws_mqtt5_utf8_decoder, -}; - -int aws_mqtt5_validate_utf8_text(struct aws_byte_cursor text) { - return aws_decode_utf8(text, &g_aws_mqtt5_utf8_decoder_options); -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 961c313f..e6354aad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ include(AwsLibFuzzer) enable_testing() file(GLOB TEST_HDRS "v3/*.h v5/*.h") -set(TEST_SRC v3/*.c v5/*.c) +set(TEST_SRC v3/*.c v5/*.c *.c) file(GLOB TESTS ${TEST_HDRS} ${TEST_SRC}) add_test_case(mqtt_packet_puback) @@ -17,6 +17,7 @@ add_test_case(mqtt_packet_connect) add_test_case(mqtt_packet_connect_will) add_test_case(mqtt_packet_connect_empty_payload_will) add_test_case(mqtt_packet_connect_password) +add_test_case(mqtt_packet_connect_all) add_test_case(mqtt_packet_connack) add_test_case(mqtt_packet_publish_qos0_dup) add_test_case(mqtt_packet_publish_qos2_retain) @@ -27,6 +28,10 @@ add_test_case(mqtt_packet_pingreq) add_test_case(mqtt_packet_pingresp) add_test_case(mqtt_packet_disconnect) +add_test_case(mqtt_packet_connack_decode_failure_reserved) +add_test_case(mqtt_packet_ack_decode_failure_reserved) +add_test_case(mqtt_packet_pingresp_decode_failure_reserved) + add_test_case(mqtt_frame_and_decode_publish) add_test_case(mqtt_frame_and_decode_suback) add_test_case(mqtt_frame_and_decode_unsuback) @@ -42,6 +47,7 @@ add_test_case(mqtt_topic_tree_unsubscribe) add_test_case(mqtt_topic_tree_duplicate_transactions) add_test_case(mqtt_topic_tree_transactions) add_test_case(mqtt_topic_validation) +add_test_case(mqtt_topic_filter_validation) add_test_case(mqtt_connect_disconnect) add_test_case(mqtt_connect_set_will_login) @@ -54,6 +60,7 @@ add_test_case(mqtt_connection_success_callback) add_test_case(mqtt_connect_subscribe) add_test_case(mqtt_connect_subscribe_fail_from_broker) add_test_case(mqtt_connect_subscribe_multi) +add_test_case(mqtt_connect_subscribe_incoming_dup) add_test_case(mqtt_connect_unsubscribe) add_test_case(mqtt_connect_resubscribe) add_test_case(mqtt_connect_publish) @@ -86,6 +93,14 @@ add_test_case(mqtt_connection_reconnection_backoff_unstable) add_test_case(mqtt_connection_reconnection_backoff_reset) add_test_case(mqtt_connection_reconnection_backoff_reset_after_disconnection) +add_test_case(mqtt_validation_failure_publish_qos) +add_test_case(mqtt_validation_failure_invalid_will_qos) +add_test_case(mqtt_validation_failure_subscribe_empty) +add_test_case(mqtt_validation_failure_unsubscribe_null) +add_test_case(mqtt_validation_failure_connect_invalid_client_id_utf8) +add_test_case(mqtt_validation_failure_invalid_will_topic_utf8) +add_test_case(mqtt_validation_failure_invalid_username_utf8) + # Operation statistics tests add_test_case(mqtt_operation_statistics_simple_publish) add_test_case(mqtt_operation_statistics_offline_publish) @@ -107,7 +122,7 @@ add_test_case(mqtt5_topic_get_segment_count) add_test_case(mqtt5_shared_subscription_validation) # utf8 utility -add_test_case(mqtt5_utf8_encoded_string_test) +add_test_case(mqtt_utf8_encoded_string_test) # topic aliasing add_test_case(mqtt5_inbound_topic_alias_register_failure) diff --git a/tests/shared_utils_tests.c b/tests/shared_utils_tests.c new file mode 100644 index 00000000..81ba7dab --- /dev/null +++ b/tests/shared_utils_tests.c @@ -0,0 +1,158 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +#include + +struct utf8_example { + const char *name; + struct aws_byte_cursor text; +}; + +static struct utf8_example s_valid_mqtt_utf8_examples[] = { + { + .name = "1 letter", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("a"), + }, + { + .name = "Several ascii letters", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("ascii word"), + }, + { + .name = "empty string", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(""), + }, + { + .name = "2 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\xA3"), + }, + { + .name = "3 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE2\x82\xAC"), + }, + { + .name = "4 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x8D\x88"), + }, + { + .name = "A variety of different length codepoints", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "\xF0\x90\x8D\x88\xE2\x82\xAC\xC2\xA3\x24\xC2\xA3\xE2\x82\xAC\xF0\x90\x8D\x88"), + }, + { + .name = "UTF8 BOM", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF"), + }, + { + .name = "UTF8 BOM plus extra", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF\x24\xC2\xA3"), + }, + { + .name = "First possible 3 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE0\xA0\x80"), + }, + { + .name = "First possible 4 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x80\x80"), + }, + { + .name = "Last possible 2 byte codepoint", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xDF\xBF"), + }, + { + .name = "Last valid codepoint before prohibited range U+D800 - U+DFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xED\x9F\xBF"), + }, + { + .name = "Next valid codepoint after prohibited range U+D800 - U+DFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEE\x80\x80"), + }, + { + .name = "Boundary condition", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBD"), + }, + { + .name = "Boundary condition", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF4\x90\x80\x80"), + }, +}; + +static struct utf8_example s_illegal_mqtt_utf8_examples[] = { + { + .name = "non character U+0000", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x00"), + }, + { + .name = "Codepoint in prohibited range U+0001 - U+001F (in the middle)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x04"), + }, + { + .name = "Codepoint in prohibited range U+0001 - U+001F (boundary)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x1F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (min: U+7F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x7F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (in the middle u+8F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x8F"), + }, + { + .name = "Codepoint in prohibited range U+007F - U+009F (boundary U+9F)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x9F"), + }, + { + .name = "non character end with U+FFFF", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBF"), + }, + { + .name = "non character end with U+FFFE", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF7\xBF\xBF\xBE"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (lower bound)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\x90"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (in middle)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xA1"), + }, + { + .name = "non character in U+FDD0 - U+FDEF (upper bound)", + .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xAF"), + }}; + +static int s_mqtt_utf8_encoded_string_test(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + /* Check the valid test cases */ + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt_utf8_examples); ++i) { + struct utf8_example example = s_valid_mqtt_utf8_examples[i]; + printf("valid example [%zu]: %s\n", i, example.name); + ASSERT_SUCCESS(aws_mqtt_validate_utf8_text(example.text)); + } + + /* Glue all the valid test cases together, they ought to pass */ + struct aws_byte_buf all_good_text; + aws_byte_buf_init(&all_good_text, allocator, 1024); + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt_utf8_examples); ++i) { + aws_byte_buf_append_dynamic(&all_good_text, &s_valid_mqtt_utf8_examples[i].text); + } + ASSERT_SUCCESS(aws_mqtt_validate_utf8_text(aws_byte_cursor_from_buf(&all_good_text))); + aws_byte_buf_clean_up(&all_good_text); + + /* Check the illegal test cases */ + for (size_t i = 0; i < AWS_ARRAY_SIZE(s_illegal_mqtt_utf8_examples); ++i) { + struct utf8_example example = s_illegal_mqtt_utf8_examples[i]; + printf("illegal example [%zu]: %s\n", i, example.name); + ASSERT_FAILS(aws_mqtt_validate_utf8_text(example.text)); + } + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_utf8_encoded_string_test, s_mqtt_utf8_encoded_string_test) \ No newline at end of file diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 5fa2b409..6a3e086f 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -1272,6 +1272,142 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) +static int s_test_mqtt_subscribe_incoming_dup_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + struct aws_byte_cursor subscribed_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor any_topic = aws_byte_cursor_from_c_str("/a/b/c"); + + uint16_t packet_id = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &subscribed_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_on_publish_received, + state_test_data, + NULL, + s_on_suback, + state_test_data); + ASSERT_TRUE(packet_id > 0); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + s_wait_for_subscribe_to_complete(state_test_data); + + state_test_data->expected_publishes = 4; + state_test_data->expected_any_publishes = 8; + + struct aws_byte_cursor subscribed_payload = aws_byte_cursor_from_c_str("Subscribed"); + for (size_t i = 0; i < 4; ++i) { + ASSERT_SUCCESS(mqtt_mock_server_send_publish_by_id( + state_test_data->mock_server, + 1111, + &subscribed_topic, + &subscribed_payload, + i > 0 /*dup*/, + AWS_MQTT_QOS_AT_LEAST_ONCE, + true /*retain*/)); + } + + struct aws_byte_cursor any_payload = aws_byte_cursor_from_c_str("Not subscribed. On-any only."); + for (size_t i = 0; i < 4; ++i) { + ASSERT_SUCCESS(mqtt_mock_server_send_publish_by_id( + state_test_data->mock_server, + 1234, + &any_topic, + &any_payload, + i > 0 /*dup*/, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false /*retain*/)); + } + + s_wait_for_publish(state_test_data); + s_wait_for_any_publish(state_test_data); + mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 8); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + /* Decode all received packets by mock server */ + ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); + + ASSERT_UINT_EQUALS(11, mqtt_mock_server_decoded_packets_count(state_test_data->mock_server)); + struct mqtt_decoded_packet *received_packet = + mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 0); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_CONNECT, received_packet->type); + ASSERT_UINT_EQUALS(connection_options.clean_session, received_packet->clean_session); + ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->client_identifier, &connection_options.client_id)); + + received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 1); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_SUBSCRIBE, received_packet->type); + ASSERT_UINT_EQUALS(1, aws_array_list_length(&received_packet->sub_topic_filters)); + struct aws_mqtt_subscription val; + ASSERT_SUCCESS(aws_array_list_front(&received_packet->sub_topic_filters, &val)); + ASSERT_TRUE(aws_byte_cursor_eq(&val.topic_filter, &subscribed_topic)); + ASSERT_UINT_EQUALS(AWS_MQTT_QOS_AT_LEAST_ONCE, val.qos); + ASSERT_UINT_EQUALS(packet_id, received_packet->packet_identifier); + + for (size_t i = 0; i < 8; ++i) { + received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 2 + i); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_PUBACK, received_packet->type); + } + + received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 10); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_DISCONNECT, received_packet->type); + + /* Check PUBLISH packets received via subscription callback */ + ASSERT_UINT_EQUALS(4, aws_array_list_length(&state_test_data->published_messages)); + + for (size_t i = 0; i < 4; ++i) { + struct received_publish_packet *publish_msg = NULL; + ASSERT_SUCCESS(aws_array_list_get_at_ptr(&state_test_data->published_messages, (void **)&publish_msg, i)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&subscribed_topic, &publish_msg->topic)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&subscribed_payload, &publish_msg->payload)); + ASSERT_INT_EQUALS((i != 0) ? 1 : 0, publish_msg->dup ? 1 : 0); + ASSERT_TRUE(publish_msg->retain); + } + + /* Check PUBLISH packets received via on_any_publish callback */ + ASSERT_UINT_EQUALS(8, aws_array_list_length(&state_test_data->any_published_messages)); + + for (size_t i = 0; i < 4; ++i) { + struct received_publish_packet *publish_msg = NULL; + ASSERT_SUCCESS(aws_array_list_get_at_ptr(&state_test_data->any_published_messages, (void **)&publish_msg, i)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&subscribed_topic, &publish_msg->topic)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&subscribed_payload, &publish_msg->payload)); + ASSERT_INT_EQUALS((i > 0) ? 1 : 0, publish_msg->dup ? 1 : 0); + ASSERT_TRUE(publish_msg->retain); + } + + for (size_t i = 4; i < 8; ++i) { + struct received_publish_packet *publish_msg = NULL; + ASSERT_SUCCESS(aws_array_list_get_at_ptr(&state_test_data->any_published_messages, (void **)&publish_msg, i)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&any_topic, &publish_msg->topic)); + ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&any_payload, &publish_msg->payload)); + ASSERT_INT_EQUALS((i > 4) ? 1 : 0, publish_msg->dup ? 1 : 0); + ASSERT_FALSE(publish_msg->retain); + } + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connect_subscribe_incoming_dup, + s_setup_mqtt_server_fn, + s_test_mqtt_subscribe_incoming_dup_fn, + s_clean_up_mqtt_server_fn, + &test_data) + /* Subscribe to a topic and broker returns a SUBACK with failure return code, the subscribe should fail */ static int s_test_mqtt_connect_subscribe_fail_from_broker_fn(struct aws_allocator *allocator, void *ctx) { (void)allocator; @@ -1790,7 +1926,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { s_wait_for_connection_to_complete(state_test_data); aws_mutex_lock(&state_test_data->lock); - state_test_data->expected_ops_completed = 2; + state_test_data->expected_ops_completed = 3; aws_mutex_unlock(&state_test_data->lock); uint16_t packet_id_1 = aws_mqtt_client_connection_publish( state_test_data->mqtt_connection, @@ -1811,6 +1947,17 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { state_test_data); ASSERT_TRUE(packet_id_2 > 0); + /* Null payload case */ + uint16_t packet_id_3 = aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + NULL, + s_on_op_complete, + state_test_data); + ASSERT_TRUE(packet_id_3 > 0); + s_wait_for_ops_completed(state_test_data); ASSERT_SUCCESS( @@ -1820,7 +1967,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); - ASSERT_UINT_EQUALS(4, mqtt_mock_server_decoded_packets_count(state_test_data->mock_server)); + ASSERT_UINT_EQUALS(5, mqtt_mock_server_decoded_packets_count(state_test_data->mock_server)); struct mqtt_decoded_packet *received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 0); ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_CONNECT, received_packet->type); @@ -1837,6 +1984,11 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->publish_payload, &payload_2)); received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 3); + ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_PUBLISH, received_packet->type); + ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->topic_name, &pub_topic)); + ASSERT_INT_EQUALS(0, received_packet->publish_payload.len); + + received_packet = mqtt_mock_server_get_decoded_packet_by_index(state_test_data->mock_server, 4); ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_DISCONNECT, received_packet->type); return AWS_OP_SUCCESS; @@ -2179,21 +2331,39 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) -/* helper to make sure ID 1 is received earlier than ID 2 and ID 2 is earlier than ID 3 */ -static int s_check_packets_received_order( +/* helper to make sure packets are received/resent in expected order and duplicat flag is appropriately set */ +static int s_check_resend_packets( struct aws_channel_handler *handler, size_t search_start_idx, - uint16_t packet_id_1, - uint16_t packet_id_2, - uint16_t packet_id_3) { + bool duplicate_publish_expected, + uint16_t *packet_ids, + size_t packet_id_count) { ASSERT_SUCCESS(mqtt_mock_server_decode_packets(handler)); - size_t packet_idx_1 = 0; - size_t packet_idx_2 = 0; - size_t packet_idx_3 = 0; - ASSERT_NOT_NULL(mqtt_mock_server_find_decoded_packet_by_id(handler, search_start_idx, packet_id_1, &packet_idx_1)); - ASSERT_NOT_NULL(mqtt_mock_server_find_decoded_packet_by_id(handler, search_start_idx, packet_id_2, &packet_idx_2)); - ASSERT_NOT_NULL(mqtt_mock_server_find_decoded_packet_by_id(handler, search_start_idx, packet_id_3, &packet_idx_3)); - ASSERT_TRUE(packet_idx_3 > packet_idx_2 && packet_idx_2 > packet_idx_1); + + if (packet_id_count == 0) { + return AWS_OP_SUCCESS; + } + + size_t previous_index = 0; + struct mqtt_decoded_packet *previous_packet = + mqtt_mock_server_find_decoded_packet_by_id(handler, search_start_idx, packet_ids[0], &previous_index); + if (previous_packet->type == AWS_MQTT_PACKET_PUBLISH) { + ASSERT_INT_EQUALS(duplicate_publish_expected, previous_packet->duplicate); + } + + for (size_t i = 1; i < packet_id_count; ++i) { + size_t current_index = 0; + struct mqtt_decoded_packet *current_packet = + mqtt_mock_server_find_decoded_packet_by_id(handler, search_start_idx, packet_ids[i], ¤t_index); + if (current_packet->type == AWS_MQTT_PACKET_PUBLISH) { + ASSERT_INT_EQUALS(duplicate_publish_expected, current_packet->duplicate); + } + + ASSERT_TRUE(current_index > previous_index); + previous_packet = current_packet; + previous_index = current_index; + } + return AWS_OP_SUCCESS; } @@ -2216,6 +2386,7 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; + struct aws_byte_cursor sub_topic = aws_byte_cursor_from_c_str("/test/topic/sub/#"); struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); struct aws_byte_cursor payload_2 = aws_byte_cursor_from_c_str("Test Message 2"); @@ -2226,19 +2397,40 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); - uint16_t packet_id_1 = aws_mqtt_client_connection_publish( + + uint16_t packet_ids[5]; + + packet_ids[0] = aws_mqtt_client_connection_publish( state_test_data->mqtt_connection, &pub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, NULL, NULL); - ASSERT_TRUE(packet_id_1 > 0); - uint16_t packet_id_2 = aws_mqtt_client_connection_publish( + ASSERT_TRUE(packet_ids[0] > 0); + + packet_ids[1] = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &sub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_on_publish_received, + state_test_data, + NULL, + s_on_suback, + state_test_data); + ASSERT_TRUE(packet_ids[1] > 0); + + packet_ids[2] = aws_mqtt_client_connection_publish( state_test_data->mqtt_connection, &pub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_2, NULL, NULL); - ASSERT_TRUE(packet_id_2 > 0); - uint16_t packet_id_3 = aws_mqtt_client_connection_publish( + ASSERT_TRUE(packet_ids[2] > 0); + + packet_ids[3] = aws_mqtt_client_connection_unsubscribe(state_test_data->mqtt_connection, &sub_topic, NULL, NULL); + ASSERT_TRUE(packet_ids[3] > 0); + + packet_ids[4] = aws_mqtt_client_connection_publish( state_test_data->mqtt_connection, &pub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_3, NULL, NULL); - ASSERT_TRUE(packet_id_3 > 0); + ASSERT_TRUE(packet_ids[4] > 0); + /* Wait for 1 sec. ensure all the publishes have been received by the server */ aws_thread_current_sleep(ONE_SEC); ASSERT_SUCCESS( - s_check_packets_received_order(state_test_data->mock_server, 0, packet_id_1, packet_id_2, packet_id_3)); + s_check_resend_packets(state_test_data->mock_server, 0, false, packet_ids, AWS_ARRAY_SIZE(packet_ids))); + size_t packet_count = mqtt_mock_server_decoded_packets_count(state_test_data->mock_server); /* shutdown the channel for some error */ @@ -2246,8 +2438,8 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca s_wait_for_reconnect_to_complete(state_test_data); /* Wait again, and ensure the publishes have been resent */ aws_thread_current_sleep(ONE_SEC); - ASSERT_SUCCESS(s_check_packets_received_order( - state_test_data->mock_server, packet_count, packet_id_1, packet_id_2, packet_id_3)); + ASSERT_SUCCESS(s_check_resend_packets( + state_test_data->mock_server, packet_count, true, packet_ids, AWS_ARRAY_SIZE(packet_ids))); ASSERT_SUCCESS( aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); @@ -3737,3 +3929,228 @@ AWS_TEST_CASE_FIXTURE( s_test_mqtt_connection_termination_callback_simple_fn, s_clean_up_mqtt_server_fn, &test_data) + +/* + * Verifies that calling publish with a bad qos results in a validation failure + */ +static int s_test_mqtt_validation_failure_publish_qos_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("a/b"); + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_publish( + state_test_data->mqtt_connection, + &topic, + (enum aws_mqtt_qos)3, + true, + NULL, + s_on_op_complete, + state_test_data)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_QOS, error_code); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_publish_qos, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_publish_qos_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/* + * Verifies that calling subscribe_multiple with no topics causes a validation failure + */ +static int s_test_mqtt_validation_failure_subscribe_empty_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + struct aws_array_list topic_filters; + size_t list_len = 2; + AWS_VARIABLE_LENGTH_ARRAY(uint8_t, static_buf, list_len * sizeof(struct aws_mqtt_topic_subscription)); + aws_array_list_init_static(&topic_filters, static_buf, list_len, sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_subscribe_multiple( + state_test_data->mqtt_connection, &topic_filters, s_on_multi_suback, state_test_data)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, error_code); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_subscribe_empty, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_subscribe_empty_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/* + * Verifies that calling unsubscribe with a null topic causes a validation failure (not a crash) + */ +static int s_test_mqtt_validation_failure_unsubscribe_null_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + s_wait_for_connection_to_complete(state_test_data); + + ASSERT_INT_EQUALS( + 0, + aws_mqtt_client_connection_unsubscribe( + state_test_data->mqtt_connection, NULL, s_on_op_complete, state_test_data)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_TOPIC, error_code); + + ASSERT_SUCCESS( + aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); + s_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_unsubscribe_null, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_unsubscribe_null_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +static struct aws_byte_cursor s_bad_client_id_utf8 = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x41\xED\xA0\x80\x41"); +static struct aws_byte_cursor s_bad_username_utf8 = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x41\x00\x41"); +static struct aws_byte_cursor s_bad_will_topic_utf8 = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x41\xED\xBF\xBF"); + +static int s_test_mqtt_validation_failure_connect_invalid_client_id_utf8_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = s_bad_client_id_utf8, + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = s_on_connection_complete_fn, + }; + + ASSERT_FAILS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, error_code); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_connect_invalid_client_id_utf8, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_connect_invalid_client_id_utf8_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_test_mqtt_validation_failure_invalid_will_topic_utf8_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_byte_cursor will_topic_cursor = s_bad_will_topic_utf8; + ASSERT_FAILS(aws_mqtt_client_connection_set_will( + state_test_data->mqtt_connection, &will_topic_cursor, AWS_MQTT_QOS_AT_MOST_ONCE, false, &will_topic_cursor)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_TOPIC, error_code); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_invalid_will_topic_utf8, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_invalid_will_topic_utf8_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_mqtt_validation_failure_invalid_will_qos_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_byte_cursor will_topic_cursor = aws_byte_cursor_from_c_str("a/b"); + + ASSERT_FAILS(aws_mqtt_client_connection_set_will( + state_test_data->mqtt_connection, &will_topic_cursor, 12, false, &will_topic_cursor)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_QOS, error_code); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_invalid_will_qos, + s_setup_mqtt_server_fn, + s_mqtt_validation_failure_invalid_will_qos_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +static int s_test_mqtt_validation_failure_invalid_username_utf8_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_byte_cursor login_cursor = s_bad_username_utf8; + ASSERT_FAILS(aws_mqtt_client_connection_set_login(state_test_data->mqtt_connection, &login_cursor, NULL)); + int error_code = aws_last_error(); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, error_code); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_validation_failure_invalid_username_utf8, + s_setup_mqtt_server_fn, + s_test_mqtt_validation_failure_invalid_username_utf8_fn, + s_clean_up_mqtt_server_fn, + &test_data) diff --git a/tests/v3/mqtt_mock_server_handler.c b/tests/v3/mqtt_mock_server_handler.c index 51bb448f..73576a7d 100644 --- a/tests/v3/mqtt_mock_server_handler.c +++ b/tests/v3/mqtt_mock_server_handler.c @@ -309,8 +309,9 @@ static struct mqtt_mock_server_send_args *s_mqtt_send_args_create(struct mqtt_mo return args; } -int mqtt_mock_server_send_publish( +int mqtt_mock_server_send_publish_by_id( struct aws_channel_handler *handler, + uint16_t packet_id, struct aws_byte_cursor *topic, struct aws_byte_cursor *payload, bool dup, @@ -321,12 +322,8 @@ int mqtt_mock_server_send_publish( struct mqtt_mock_server_send_args *args = s_mqtt_send_args_create(server); - aws_mutex_lock(&server->synced.lock); - uint16_t id = qos == 0 ? 0 : ++server->synced.last_packet_id; - aws_mutex_unlock(&server->synced.lock); - struct aws_mqtt_packet_publish publish; - ASSERT_SUCCESS(aws_mqtt_packet_publish_init(&publish, retain, qos, dup, *topic, id, *payload)); + ASSERT_SUCCESS(aws_mqtt_packet_publish_init(&publish, retain, qos, dup, *topic, packet_id, *payload)); ASSERT_SUCCESS(aws_mqtt_packet_publish_encode(&args->data, &publish)); aws_channel_schedule_task_now(server->slot->channel, &args->task); @@ -334,6 +331,22 @@ int mqtt_mock_server_send_publish( return AWS_OP_SUCCESS; } +int mqtt_mock_server_send_publish( + struct aws_channel_handler *handler, + struct aws_byte_cursor *topic, + struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain) { + + struct mqtt_mock_server_handler *server = handler->impl; + aws_mutex_lock(&server->synced.lock); + uint16_t id = qos == 0 ? 0 : ++server->synced.last_packet_id; + aws_mutex_unlock(&server->synced.lock); + + return mqtt_mock_server_send_publish_by_id(handler, id, topic, payload, dup, qos, retain); +} + int mqtt_mock_server_send_single_suback( struct aws_channel_handler *handler, uint16_t packet_id, @@ -707,6 +720,7 @@ int mqtt_mock_server_decode_packets(struct aws_channel_handler *handler) { packet->packet_identifier = publish_packet.packet_identifier; packet->topic_name = publish_packet.topic_name; packet->publish_payload = publish_packet.payload; + packet->duplicate = aws_mqtt_packet_publish_get_dup(&publish_packet); break; } case AWS_MQTT_PACKET_PUBACK: { diff --git a/tests/v3/mqtt_mock_server_handler.h b/tests/v3/mqtt_mock_server_handler.h index 6693e422..170201e6 100644 --- a/tests/v3/mqtt_mock_server_handler.h +++ b/tests/v3/mqtt_mock_server_handler.h @@ -35,6 +35,7 @@ struct mqtt_decoded_packet { struct aws_byte_cursor publish_payload; /* PUBLISH payload */ struct aws_array_list sub_topic_filters; /* list of aws_mqtt_subscription for SUBSCRIBE */ struct aws_array_list unsub_topic_filters; /* list of aws_byte_cursor for UNSUBSCRIBE */ + bool duplicate; /* PUBLISH only */ /* index of the received packet, indicating when it's received by the server */ size_t index; @@ -54,6 +55,19 @@ int mqtt_mock_server_send_publish( bool dup, enum aws_mqtt_qos qos, bool retain); + +/** + * Mock server sends a publish packet back to client with user-controlled packet id + */ +int mqtt_mock_server_send_publish_by_id( + struct aws_channel_handler *handler, + uint16_t packet_id, + struct aws_byte_cursor *topic, + struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain); + /** * Set max number of PINGRESP that mock server will send back to client */ diff --git a/tests/v3/packet_encoding_test.c b/tests/v3/packet_encoding_test.c index ba85ee89..60f91c63 100644 --- a/tests/v3/packet_encoding_test.c +++ b/tests/v3/packet_encoding_test.c @@ -390,6 +390,58 @@ static int s_test_connect_password_init(struct packet_test_fixture *fixture) { } PACKET_TEST_NAME(CONNECT, connect_password, connect, &s_test_connect_password_init, NULL, &s_test_connect_eq) +static int s_test_connect_all_init(struct packet_test_fixture *fixture) { + /* Init packet */ + ASSERT_SUCCESS(aws_mqtt_packet_connect_init( + fixture->in_packet, aws_byte_cursor_from_array(s_client_id, CLIENT_ID_LEN), false, 0)); + ASSERT_SUCCESS(aws_mqtt_packet_connect_add_will( + fixture->in_packet, + aws_byte_cursor_from_array(s_topic_name, TOPIC_NAME_LEN), + AWS_MQTT_QOS_EXACTLY_ONCE, + true /*retain*/, + aws_byte_cursor_from_array(s_payload, PAYLOAD_LEN))); + ASSERT_SUCCESS(aws_mqtt_packet_connect_add_credentials( + fixture->in_packet, + aws_byte_cursor_from_array(s_username, USERNAME_LEN), + aws_byte_cursor_from_array(s_password, PASSWORD_LEN))); + + /* Init buffer */ + /* clang-format off */ + uint8_t header[] = { + AWS_MQTT_PACKET_CONNECT << 4, /* Packet type */ + 10 + (2 + CLIENT_ID_LEN) + (2 + TOPIC_NAME_LEN) + (2 + PAYLOAD_LEN) + (2 + USERNAME_LEN) + (2 + PASSWORD_LEN), /* Remaining length */ + 0, 4, 'M', 'Q', 'T', 'T', /* Protocol name */ + 4, /* Protocol level */ + /* Connect Flags: */ + (1 << 2) /* Will flag, bit 2 */ + | (AWS_MQTT_QOS_EXACTLY_ONCE << 3)/* Will QoS, bits 4-3 */ + | (1 << 5) /* Will Retain, bit 5 */ + | (1 << 7) | (1 << 6), /* username bit 7, password bit 6 */ + 0, 0, /* Keep alive */ + }; + /* clang-format on */ + + aws_byte_buf_write(&fixture->buffer, header, sizeof(header)); + /* client identifier */ + aws_byte_buf_write_be16(&fixture->buffer, CLIENT_ID_LEN); + aws_byte_buf_write(&fixture->buffer, s_client_id, CLIENT_ID_LEN); + /* will topic */ + aws_byte_buf_write_be16(&fixture->buffer, TOPIC_NAME_LEN); + aws_byte_buf_write(&fixture->buffer, s_topic_name, TOPIC_NAME_LEN); + /* will payload */ + aws_byte_buf_write_be16(&fixture->buffer, PAYLOAD_LEN); + aws_byte_buf_write(&fixture->buffer, s_payload, PAYLOAD_LEN); + /* username */ + aws_byte_buf_write_be16(&fixture->buffer, USERNAME_LEN); + aws_byte_buf_write(&fixture->buffer, s_username, USERNAME_LEN); + /* password */ + aws_byte_buf_write_be16(&fixture->buffer, PASSWORD_LEN); + aws_byte_buf_write(&fixture->buffer, s_password, PASSWORD_LEN); + + return AWS_OP_SUCCESS; +} +PACKET_TEST_NAME(CONNECT, connect_all, connect, &s_test_connect_all_init, NULL, &s_test_connect_eq) + /*****************************************************************************/ /* Connack */ @@ -790,6 +842,90 @@ PACKET_TEST_CONNETION(PINGRESP, pingresp) PACKET_TEST_CONNETION(DISCONNECT, disconnect) #undef PACKET_TEST_CONNETION +static int s_mqtt_packet_connack_decode_failure_reserved_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_buf encoded_packet; + aws_byte_buf_init(&encoded_packet, allocator, 1024); + + struct aws_mqtt_packet_connack connack; + ASSERT_SUCCESS(aws_mqtt_packet_connack_init(&connack, true, AWS_MQTT_CONNECT_SERVER_UNAVAILABLE)); + + ASSERT_SUCCESS(aws_mqtt_packet_connack_encode(&encoded_packet, &connack)); + + struct aws_byte_cursor decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + struct aws_mqtt_packet_connack decoded_connack; + ASSERT_SUCCESS(aws_mqtt_packet_connack_decode(&decode_cursor, &decoded_connack)); + + /* mess up the fixed header reserved bits */ + encoded_packet.buffer[0] |= 0x01; + + decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + ASSERT_FAILS(aws_mqtt_packet_connack_decode(&decode_cursor, &decoded_connack)); + + aws_byte_buf_clean_up(&encoded_packet); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_packet_connack_decode_failure_reserved, s_mqtt_packet_connack_decode_failure_reserved_fn) + +static int s_mqtt_packet_ack_decode_failure_reserved_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_buf encoded_packet; + aws_byte_buf_init(&encoded_packet, allocator, 1024); + + struct aws_mqtt_packet_ack puback; + ASSERT_SUCCESS(aws_mqtt_packet_puback_init(&puback, 5)); + + ASSERT_SUCCESS(aws_mqtt_packet_ack_encode(&encoded_packet, &puback)); + + struct aws_byte_cursor decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + struct aws_mqtt_packet_ack decoded_ack; + ASSERT_SUCCESS(aws_mqtt_packet_ack_decode(&decode_cursor, &decoded_ack)); + + /* mess up the fixed header reserved bits */ + encoded_packet.buffer[0] |= 0x0F; + + decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + ASSERT_FAILS(aws_mqtt_packet_ack_decode(&decode_cursor, &decoded_ack)); + + aws_byte_buf_clean_up(&encoded_packet); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_packet_ack_decode_failure_reserved, s_mqtt_packet_ack_decode_failure_reserved_fn) + +static int s_mqtt_packet_pingresp_decode_failure_reserved_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_byte_buf encoded_packet; + aws_byte_buf_init(&encoded_packet, allocator, 1024); + + struct aws_mqtt_packet_connection pingresp; + ASSERT_SUCCESS(aws_mqtt_packet_pingresp_init(&pingresp)); + + ASSERT_SUCCESS(aws_mqtt_packet_connection_encode(&encoded_packet, &pingresp)); + + struct aws_byte_cursor decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + struct aws_mqtt_packet_connection decoded_pingresp; + ASSERT_SUCCESS(aws_mqtt_packet_connection_decode(&decode_cursor, &decoded_pingresp)); + + /* mess up the fixed header reserved bits */ + encoded_packet.buffer[0] |= 0x08; + + decode_cursor = aws_byte_cursor_from_buf(&encoded_packet); + ASSERT_FAILS(aws_mqtt_packet_connection_decode(&decode_cursor, &decoded_pingresp)); + + aws_byte_buf_clean_up(&encoded_packet); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_packet_pingresp_decode_failure_reserved, s_mqtt_packet_pingresp_decode_failure_reserved_fn) + #ifdef _MSC_VER # pragma warning(pop) #endif diff --git a/tests/v3/topic_tree_test.c b/tests/v3/topic_tree_test.c index 8fabfa9f..6e0bb853 100644 --- a/tests/v3/topic_tree_test.c +++ b/tests/v3/topic_tree_test.c @@ -316,18 +316,55 @@ static int s_mqtt_topic_validation_fn(struct aws_allocator *allocator, void *ctx struct aws_byte_cursor topic_cursor; \ topic_cursor.ptr = (uint8_t *)(topic); \ topic_cursor.len = strlen(topic); \ - ASSERT_##expected(aws_mqtt_is_valid_topic_filter(&topic_cursor)); \ + ASSERT_##expected(aws_mqtt_is_valid_topic(&topic_cursor)); \ } while (false) - ASSERT_TOPIC_VALIDITY(TRUE, "#"); - ASSERT_TOPIC_VALIDITY(TRUE, "sport/tennis/#"); + ASSERT_TOPIC_VALIDITY(TRUE, "/"); + ASSERT_TOPIC_VALIDITY(TRUE, "a/"); + ASSERT_TOPIC_VALIDITY(TRUE, "/b"); + ASSERT_TOPIC_VALIDITY(TRUE, "a/b/c"); + + ASSERT_TOPIC_VALIDITY(FALSE, "#"); + ASSERT_TOPIC_VALIDITY(FALSE, "sport/tennis/#"); ASSERT_TOPIC_VALIDITY(FALSE, "sport/tennis#"); ASSERT_TOPIC_VALIDITY(FALSE, "sport/tennis/#/ranking"); - - ASSERT_TOPIC_VALIDITY(TRUE, "+"); - ASSERT_TOPIC_VALIDITY(TRUE, "+/tennis/#"); - ASSERT_TOPIC_VALIDITY(TRUE, "sport/+/player1"); + ASSERT_TOPIC_VALIDITY(FALSE, ""); + ASSERT_TOPIC_VALIDITY(FALSE, "+"); + ASSERT_TOPIC_VALIDITY(FALSE, "+/tennis/#"); + ASSERT_TOPIC_VALIDITY(FALSE, "sport/+/player1"); ASSERT_TOPIC_VALIDITY(FALSE, "sport+"); + ASSERT_TOPIC_VALIDITY(FALSE, "\x41/\xED\xBF\xBF/\x41"); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_topic_filter_validation, s_mqtt_topic_filter_validation_fn) +static int s_mqtt_topic_filter_validation_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + +#define ASSERT_TOPIC_FILTER_VALIDITY(expected, topic_filter) \ + do { \ + struct aws_byte_cursor topic_filter_cursor; \ + topic_filter_cursor.ptr = (uint8_t *)(topic_filter); \ + topic_filter_cursor.len = strlen(topic_filter); \ + ASSERT_##expected(aws_mqtt_is_valid_topic_filter(&topic_filter_cursor)); \ + } while (false) + + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "#"); + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "sport/tennis/#"); + ASSERT_TOPIC_FILTER_VALIDITY(FALSE, "sport/tennis#"); + ASSERT_TOPIC_FILTER_VALIDITY(FALSE, "sport/tennis/#/ranking"); + ASSERT_TOPIC_FILTER_VALIDITY(FALSE, ""); + + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "+/"); + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "+"); + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "+/tennis/#"); + ASSERT_TOPIC_FILTER_VALIDITY(TRUE, "sport/+/player1"); + ASSERT_TOPIC_FILTER_VALIDITY(FALSE, "sport+"); + + ASSERT_TOPIC_FILTER_VALIDITY(FALSE, "\x41/\xED\xA0\x80/\x41"); + return AWS_OP_SUCCESS; } diff --git a/tests/v5/mqtt5_utils_tests.c b/tests/v5/mqtt5_utils_tests.c index fd14d027..d2908930 100644 --- a/tests/v5/mqtt5_utils_tests.c +++ b/tests/v5/mqtt5_utils_tests.c @@ -137,152 +137,3 @@ static int s_mqtt5_shared_subscription_validation_fn(struct aws_allocator *alloc } AWS_TEST_CASE(mqtt5_shared_subscription_validation, s_mqtt5_shared_subscription_validation_fn) - -struct utf8_example { - const char *name; - struct aws_byte_cursor text; -}; - -static struct utf8_example s_valid_mqtt5_utf8_examples[] = { - { - .name = "1 letter", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("a"), - }, - { - .name = "Several ascii letters", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("ascii word"), - }, - { - .name = "empty string", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(""), - }, - { - .name = "2 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\xA3"), - }, - { - .name = "3 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE2\x82\xAC"), - }, - { - .name = "4 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x8D\x88"), - }, - { - .name = "A variety of different length codepoints", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( - "\xF0\x90\x8D\x88\xE2\x82\xAC\xC2\xA3\x24\xC2\xA3\xE2\x82\xAC\xF0\x90\x8D\x88"), - }, - { - .name = "UTF8 BOM", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF"), - }, - { - .name = "UTF8 BOM plus extra", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBB\xBF\x24\xC2\xA3"), - }, - { - .name = "First possible 3 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xE0\xA0\x80"), - }, - { - .name = "First possible 4 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF0\x90\x80\x80"), - }, - { - .name = "Last possible 2 byte codepoint", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xDF\xBF"), - }, - { - .name = "Last valid codepoint before prohibited range U+D800 - U+DFFF", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xED\x9F\xBF"), - }, - { - .name = "Next valid codepoint after prohibited range U+D800 - U+DFFF", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEE\x80\x80"), - }, - { - .name = "Boundary condition", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBD"), - }, - { - .name = "Boundary condition", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF4\x90\x80\x80"), - }, -}; - -static struct utf8_example s_illegal_mqtt5_utf8_examples[] = { - { - .name = "non character U+0000", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x00"), - }, - { - .name = "Codepoint in prohibited range U+0001 - U+001F (in the middle)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x04"), - }, - { - .name = "Codepoint in prohibited range U+0001 - U+001F (boundary)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x1F"), - }, - { - .name = "Codepoint in prohibited range U+007F - U+009F (min: U+7F)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\x7F"), - }, - { - .name = "Codepoint in prohibited range U+007F - U+009F (in the middle u+8F)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x8F"), - }, - { - .name = "Codepoint in prohibited range U+007F - U+009F (boundary U+9F)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xC2\x9F"), - }, - { - .name = "non character end with U+FFFF", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xBF\xBF"), - }, - { - .name = "non character end with U+FFFE", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xF7\xBF\xBF\xBE"), - }, - { - .name = "non character in U+FDD0 - U+FDEF (lower bound)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\x90"), - }, - { - .name = "non character in U+FDD0 - U+FDEF (in middle)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xA1"), - }, - { - .name = "non character in U+FDD0 - U+FDEF (upper bound)", - .text = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\xEF\xB7\xAF"), - }}; - -static int s_mqtt5_utf8_encoded_string_test(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - /* Check the valid test cases */ - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt5_utf8_examples); ++i) { - struct utf8_example example = s_valid_mqtt5_utf8_examples[i]; - printf("valid example [%zu]: %s\n", i, example.name); - ASSERT_SUCCESS(aws_mqtt5_validate_utf8_text(example.text)); - } - - /* Glue all the valid test cases together, they ought to pass */ - struct aws_byte_buf all_good_text; - aws_byte_buf_init(&all_good_text, allocator, 1024); - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_valid_mqtt5_utf8_examples); ++i) { - aws_byte_buf_append_dynamic(&all_good_text, &s_valid_mqtt5_utf8_examples[i].text); - } - ASSERT_SUCCESS(aws_mqtt5_validate_utf8_text(aws_byte_cursor_from_buf(&all_good_text))); - aws_byte_buf_clean_up(&all_good_text); - - /* Check the illegal test cases */ - for (size_t i = 0; i < AWS_ARRAY_SIZE(s_illegal_mqtt5_utf8_examples); ++i) { - struct utf8_example example = s_illegal_mqtt5_utf8_examples[i]; - printf("illegal example [%zu]: %s\n", i, example.name); - ASSERT_FAILS(aws_mqtt5_validate_utf8_text(example.text)); - } - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt5_utf8_encoded_string_test, s_mqtt5_utf8_encoded_string_test) From 7c467e4f242920875cf68aed60deafc54bcb177e Mon Sep 17 00:00:00 2001 From: Alfred G <28123637+alfred2g@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:52:05 -0700 Subject: [PATCH 90/98] Fix: coredump, dereference null variable (#327) Co-authored-by: Zhihui Xia --- source/v5/mqtt5_to_mqtt3_adapter.c | 93 +++++++------- tests/CMakeLists.txt | 2 + tests/v5/mqtt5_to_mqtt3_adapter_tests.c | 161 ++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 44 deletions(-) diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index 9986de2e..cb1391d7 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -2184,10 +2184,10 @@ static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( const struct aws_mqtt5_packet_suback_view *suback, int error_code, void *complete_ctx) { - (void)suback; struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = complete_ctx; struct aws_mqtt_client_connection_5_impl *adapter = subscribe_op->base.adapter; + struct aws_mqtt_subscription_set_subscription_record *record = NULL; if (subscribe_op->on_suback != NULL) { AWS_LOGF_DEBUG( @@ -2197,20 +2197,22 @@ static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( struct aws_byte_cursor topic_filter; AWS_ZERO_STRUCT(topic_filter); + enum aws_mqtt_qos granted_qos = AWS_MQTT_QOS_AT_MOST_ONCE; size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); if (subscription_count > 0) { - struct aws_mqtt_subscription_set_subscription_record *record = NULL; aws_array_list_get_at(&subscribe_op->subscriptions, &record, 0); - topic_filter = record->subscription_view.topic_filter; } - if (suback->reason_code_count > 0) { - granted_qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[0]); + if (suback != NULL) { + if (suback->reason_code_count > 0) { + granted_qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[0]); + } + } else { + granted_qos = AWS_MQTT_QOS_FAILURE; } - (*subscribe_op->on_suback)( &adapter->base, subscribe_op->base.id, @@ -2226,47 +2228,50 @@ static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( "id=%p: mqtt3-to-5-adapter, completing multi-topic subscribe", (void *)adapter); - AWS_VARIABLE_LENGTH_ARRAY( - struct aws_mqtt_topic_subscription, multi_sub_subscription_buf, suback->reason_code_count); - AWS_VARIABLE_LENGTH_ARRAY( - struct aws_mqtt_topic_subscription *, multi_sub_subscription_ptr_buf, suback->reason_code_count); - struct aws_mqtt_topic_subscription *subscription_ptr = - (struct aws_mqtt_topic_subscription *)multi_sub_subscription_buf; - - struct aws_array_list multi_sub_list; - aws_array_list_init_static( - &multi_sub_list, - multi_sub_subscription_ptr_buf, - suback->reason_code_count, - sizeof(struct aws_mqtt_topic_subscription *)); - - size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); - - for (size_t i = 0; i < suback->reason_code_count; ++i) { - struct aws_mqtt_topic_subscription *subscription = subscription_ptr + i; - AWS_ZERO_STRUCT(*subscription); - - subscription->qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[i]); - - if (i < subscription_count) { - struct aws_mqtt_subscription_set_subscription_record *record = NULL; - aws_array_list_get_at(&subscribe_op->subscriptions, &record, i); + if (suback == NULL) { + (*subscribe_op->on_multi_suback)( + &adapter->base, subscribe_op->base.id, NULL, error_code, subscribe_op->on_multi_suback_user_data); + } else { + AWS_VARIABLE_LENGTH_ARRAY( + struct aws_mqtt_topic_subscription, multi_sub_subscription_buf, suback->reason_code_count); + AWS_VARIABLE_LENGTH_ARRAY( + struct aws_mqtt_topic_subscription *, multi_sub_subscription_ptr_buf, suback->reason_code_count); + struct aws_mqtt_topic_subscription *subscription_ptr = + (struct aws_mqtt_topic_subscription *)multi_sub_subscription_buf; + + struct aws_array_list multi_sub_list; + aws_array_list_init_static( + &multi_sub_list, + multi_sub_subscription_ptr_buf, + suback->reason_code_count, + sizeof(struct aws_mqtt_topic_subscription *)); + + size_t subscription_count = aws_array_list_length(&subscribe_op->subscriptions); + + for (size_t i = 0; i < suback->reason_code_count; ++i) { + struct aws_mqtt_topic_subscription *subscription = subscription_ptr + i; + AWS_ZERO_STRUCT(*subscription); + + subscription->qos = s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos(suback->reason_codes[i]); + + if (i < subscription_count) { + aws_array_list_get_at(&subscribe_op->subscriptions, &record, i); + + subscription->topic = record->subscription_view.topic_filter; + subscription->on_publish = record->subscription_view.on_publish_received; + subscription->on_publish_ud = record->subscription_view.callback_user_data; + subscription->on_cleanup = record->subscription_view.on_cleanup; + } - subscription->topic = record->subscription_view.topic_filter; - subscription->on_publish = record->subscription_view.on_publish_received; - subscription->on_publish_ud = record->subscription_view.callback_user_data; - subscription->on_cleanup = record->subscription_view.on_cleanup; + aws_array_list_push_back(&multi_sub_list, &subscription); } - - aws_array_list_push_back(&multi_sub_list, &subscription); + (*subscribe_op->on_multi_suback)( + &adapter->base, + subscribe_op->base.id, + &multi_sub_list, + error_code, + subscribe_op->on_multi_suback_user_data); } - - (*subscribe_op->on_multi_suback)( - &adapter->base, - subscribe_op->base.id, - &multi_sub_list, - error_code, - subscribe_op->on_multi_suback_user_data); } aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e6354aad..2315095d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -424,6 +424,8 @@ add_test_case(mqtt5to3_adapter_unsubscribe_overlapped) add_test_case(mqtt5to3_adapter_get_stats) add_test_case(mqtt5to3_adapter_resubscribe_nothing) add_test_case(mqtt5to3_adapter_resubscribe_something) +add_test_case(mqtt5to3_adapter_subscribe_single_null_suback) +add_test_case(mqtt5to3_adapter_subscribe_multi_null_suback) add_test_case(mqtt_subscription_set_add_empty_not_subbed) add_test_case(mqtt_subscription_set_add_single_path) diff --git a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c index 8c85e884..3b6c9dba 100644 --- a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c +++ b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c @@ -2686,6 +2686,88 @@ static int s_mqtt5to3_adapter_subscribe_single_success_fn(struct aws_allocator * AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_success, s_mqtt5to3_adapter_subscribe_single_success_fn) +/* + * This function tests receiving a subscribe acknowledge after disconnecting from + * the server. + * it expects a AWS_MQTT_QOS_FAILURE return + */ +static int s_mqtt5to3_adapter_subscribe_single_null_suback_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("derp"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + struct aws_mqtt_topic_subscription expected_subs[1] = { + { + .topic = topic, + .qos = AWS_MQTT_QOS_FAILURE, + }, + }; + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + }, + }; + aws_array_list_init_static_from_initialized( + &expected_events[0].granted_subscriptions, + (void *)expected_subs, + 1, + sizeof(struct aws_mqtt_topic_subscription)); + + aws_mqtt_client_connection_disconnect( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_single_null_suback, s_mqtt5to3_adapter_subscribe_single_null_suback_fn) + static void s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete( struct aws_mqtt_client_connection *connection, uint16_t packet_id, @@ -2814,6 +2896,85 @@ static int s_mqtt5to3_adapter_subscribe_multi_success_fn(struct aws_allocator *a AWS_TEST_CASE(mqtt5to3_adapter_subscribe_multi_success, s_mqtt5to3_adapter_subscribe_multi_success_fn) +/* + * This function tests receiving a subscribe acknowledge after disconnecting from + * the server. + * it expects a AWS_MQTT_QOS_FAILURE return + */ +static int s_mqtt5to3_adapter_subscribe_multi_null_suback_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_mqtt_topic_subscription subscriptions[] = { + { + .topic = aws_byte_cursor_from_c_str("topic/1"), + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + }, + { + .topic = aws_byte_cursor_from_c_str("topic/2"), + .qos = AWS_MQTT_QOS_AT_MOST_ONCE, + }, + }; + + struct aws_array_list subscription_list; + aws_array_list_init_static_from_initialized( + &subscription_list, subscriptions, 2, sizeof(struct aws_mqtt_topic_subscription)); + + aws_mqtt_client_connection_subscribe_multiple( + connection, + &subscription_list, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_multi_complete, + &fixture); + + struct aws_mqtt3_operation_event expected_events[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP, + }, + }; + + aws_mqtt_client_connection_disconnect( + connection, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_disconnection_complete, &fixture); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_DISCONNECTION_COMPLETE, 1); + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence( + &fixture, AWS_ARRAY_SIZE(expected_events), expected_events, AWS_ARRAY_SIZE(expected_events))); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5to3_adapter_subscribe_multi_null_suback, s_mqtt5to3_adapter_subscribe_multi_null_suback_fn) + static int s_mqtt5_mock_server_handle_subscribe_suback_failure( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, From 0cc50d1582719c9f3b55539139015fc86965bb6f Mon Sep 17 00:00:00 2001 From: Vera Xia Date: Thu, 12 Oct 2023 14:00:15 -0700 Subject: [PATCH 91/98] Restore negotiated settings copy (#330) --- include/aws/mqtt/v5/mqtt5_client.h | 14 ++++++++++++++ source/v5/mqtt5_utils.c | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 025c03ba..7cbb19a2 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -787,6 +787,20 @@ AWS_MQTT_API int aws_mqtt5_negotiated_settings_init( struct aws_mqtt5_negotiated_settings *negotiated_settings, const struct aws_byte_cursor *client_id); +/** + * Makes an owning copy of a negotiated settings structure. + * + * @param source settings to copy from + * @param dest settings to copy into. Must be in a zeroed or initialized state because it gets clean up + * called on it as the first step of the copy process. + * @return success/failure + * + * Used in downstream. + */ +AWS_MQTT_API int aws_mqtt5_negotiated_settings_copy( + const struct aws_mqtt5_negotiated_settings *source, + struct aws_mqtt5_negotiated_settings *dest); + /** * Clean up owned memory in negotiated_settings * diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index d4444203..dd483f69 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -123,6 +123,24 @@ int aws_mqtt5_negotiated_settings_init( return AWS_OP_SUCCESS; } +int aws_mqtt5_negotiated_settings_copy( + const struct aws_mqtt5_negotiated_settings *source, + struct aws_mqtt5_negotiated_settings *dest) { + aws_mqtt5_negotiated_settings_clean_up(dest); + + *dest = *source; + AWS_ZERO_STRUCT(dest->client_id_storage); + + if (source->client_id_storage.allocator != NULL) { + return aws_byte_buf_init_copy_from_cursor( + &dest->client_id_storage, + source->client_id_storage.allocator, + aws_byte_cursor_from_buf(&source->client_id_storage)); + } + + return AWS_OP_SUCCESS; +} + int aws_mqtt5_negotiated_settings_apply_client_id( struct aws_mqtt5_negotiated_settings *negotiated_settings, const struct aws_byte_cursor *client_id) { From c475ef1bfcc31f815e46558864161728a15a70ae Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 19 Oct 2023 10:18:51 -0700 Subject: [PATCH 92/98] Remove callback severance on 5-to-3 adapter (#329) Co-authored-by: Bret Ambrose Co-authored-by: Vera Xia --- .../private/v5/mqtt5_to_mqtt3_adapter_impl.h | 32 +- source/v5/mqtt5_to_mqtt3_adapter.c | 285 ++++++------------ tests/CMakeLists.txt | 1 + tests/v5/mqtt5_to_mqtt3_adapter_tests.c | 109 +++++++ 4 files changed, 210 insertions(+), 217 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h index c424db6a..d892d071 100644 --- a/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_to_mqtt3_adapter_impl.h @@ -62,9 +62,15 @@ enum aws_mqtt5_to_mqtt3_adapter_operation_type { AWS_MQTT5TO3_AOT_UNSUBSCRIBE, }; +struct aws_mqtt5_to_mqtt3_adapter_operation_vtable { + void (*fail_fn)(void *impl, int error_code); +}; + struct aws_mqtt5_to_mqtt3_adapter_operation_base { struct aws_allocator *allocator; struct aws_ref_count ref_count; + const struct aws_mqtt5_to_mqtt3_adapter_operation_vtable *vtable; + void *impl; /* @@ -224,15 +230,6 @@ struct aws_mqtt_client_connection_5_impl { struct aws_mqtt5_listener *listener; struct aws_event_loop *loop; - /* - * An event-loop-internal flag that we can read to check to see if we're in the scope of a callback - * that has already locked the adapter's lock. Can only be referenced from the event loop thread. - * - * We use the flag to avoid deadlock in a few cases where we can re-enter the adapter logic from within a callback. - * It also provides a nice solution for the fact that we cannot safely upgrade a read lock to a write lock. - */ - bool in_synchronous_callback; - /* * The current adapter state based on the sequence of connect(), disconnect(), and connection completion events. * This affects how the adapter reacts to incoming mqtt5 events. Under certain conditions, we may change @@ -260,23 +257,6 @@ struct aws_mqtt_client_connection_5_impl { */ struct aws_ref_count internal_refs; - /* - * We use the adapter lock to guarantee that we can synchronously sever all callbacks from the mqtt5 client even - * though adapter shutdown is an asynchronous process. This means the lock is held during callbacks which is a - * departure from our normal usage patterns. We prevent deadlock (due to logical re-entry) by using the - * in_synchronous_callback flag. - * - * We hold a read lock when invoking callbacks and a write lock when setting terminated from false to true. - */ - struct aws_rw_lock lock; - - /* - * Synchronized data protected by the adapter lock. - */ - struct { - bool terminated; - } synced_data; - struct aws_mqtt5_to_mqtt3_adapter_operation_table operational_state; struct aws_mqtt_subscription_set *subscriptions; diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index cb1391d7..ccad38d7 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -92,7 +92,6 @@ static void s_mqtt_adapter_final_destroy_task_fn(struct aws_task *task, void *ar aws_mqtt5_to_mqtt3_adapter_operation_table_clean_up(&adapter->operational_state); adapter->client = aws_mqtt5_client_release(adapter->client); - aws_rw_lock_clean_up(&adapter->lock); aws_mem_release(adapter->allocator, adapter); @@ -137,83 +136,6 @@ static void s_aws_mqtt_adapter_final_destroy(struct aws_mqtt_client_connection_5 aws_event_loop_schedule_task_now(adapter->loop, &task->task); } -typedef int (*adapter_callback_fn)(struct aws_mqtt_client_connection_5_impl *adapter, void *context); - -/* - * The state/ref-count lock is held during synchronous callbacks to prevent invoking into something that is in the - * process of destruction. In general this isn't a performance worry since callbacks are invoked from a single thread: - * the event loop that the client and adapter are seated on. - * - * But since we don't have recursive mutexes on all platforms, we need to be careful about the shutdown - * process since if we naively always locked, then an adapter release from within a callback would deadlock. - * - * We need a way to tell if locking will result in a deadlock. The specific case is invoking a synchronous - * callback from the event loop that re-enters the adapter logic via releasing the connection. We can recognize - * this scenario by setting/clearing an internal flag (in_synchronous_callback) and checking it only if we're - * in the event loop thread. If it's true, we know we've already locked at the beginning of the synchronous callback - * and we can safely skip locking, otherwise we must lock. - * - * This function gives us a helper for making these kinds of safe callbacks. We use it in: - * (1) Releasing the connection - * (2) Websocket handshake transform - * (3) Making lifecycle and operation callbacks on the mqtt311 interface - * - * It works by - * (1) Correctly determining if locking would deadlock and skipping lock only in that case, otherwise locking - * (2) Invoke the callback - * (3) Unlock if we locked in step (1) - * - * It also properly sets/clears the in_synchronous_callback flag if we're in the event loop and are not in - * a callback already. - */ -static int s_aws_mqtt5_adapter_perform_safe_callback( - struct aws_mqtt_client_connection_5_impl *adapter, - bool use_write_lock, - adapter_callback_fn callback_fn, - void *callback_user_data) { - - AWS_LOGF_DEBUG( - AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "id=%p: mqtt3-to-5 adapter performing safe user callback", (void *)adapter); - - /* Step (1) - conditionally lock and manipulate the in_synchronous_callback flag */ - bool should_unlock = true; - bool clear_synchronous_callback_flag = false; - if (aws_event_loop_thread_is_callers_thread(adapter->loop)) { - if (adapter->in_synchronous_callback) { - should_unlock = false; - } else { - adapter->in_synchronous_callback = true; - clear_synchronous_callback_flag = true; - } - } - - if (should_unlock) { - if (use_write_lock) { - aws_rw_lock_wlock(&adapter->lock); - } else { - aws_rw_lock_rlock(&adapter->lock); - } - } - - // Step (2) - perform the callback - int result = (*callback_fn)(adapter, callback_user_data); - - // Step (3) - undo anything we did in step (1) - if (should_unlock) { - if (use_write_lock) { - aws_rw_lock_wunlock(&adapter->lock); - } else { - aws_rw_lock_runlock(&adapter->lock); - } - } - - if (clear_synchronous_callback_flag) { - adapter->in_synchronous_callback = false; - } - - return result; -} - struct aws_mqtt_adapter_disconnect_task { struct aws_task task; struct aws_allocator *allocator; @@ -429,17 +351,8 @@ static int s_aws_mqtt_client_connection_5_connect( return AWS_OP_SUCCESS; } -static int s_aws_mqtt5_to_mqtt3_adapter_safe_lifecycle_handler( - struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - const struct aws_mqtt5_client_lifecycle_event *event = context; - - /* - * Never invoke a callback after termination - */ - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } +static void s_aws_mqtt5_to_mqtt3_adapter_lifecycle_handler(const struct aws_mqtt5_client_lifecycle_event *event) { + struct aws_mqtt_client_connection_5_impl *adapter = event->user_data; switch (event->event_type) { @@ -591,25 +504,11 @@ static int s_aws_mqtt5_to_mqtt3_adapter_safe_lifecycle_handler( default: break; } - - return AWS_OP_SUCCESS; } -static void s_aws_mqtt5_client_lifecycle_event_callback_adapter(const struct aws_mqtt5_client_lifecycle_event *event) { - struct aws_mqtt_client_connection_5_impl *adapter = event->user_data; - - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_lifecycle_handler, (void *)event); -} - -static int s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler( +static void s_aws_mqtt5_to_mqtt3_adapter_disconnect_handler( struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - struct aws_mqtt_adapter_disconnect_task *disconnect_task = context; - - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } + struct aws_mqtt_adapter_disconnect_task *disconnect_task) { AWS_LOGF_DEBUG( AWS_LS_MQTT5_TO_MQTT3_ADAPTER, @@ -625,7 +524,7 @@ static int s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler( (*disconnect_task->on_disconnect)(&adapter->base, disconnect_task->on_disconnect_user_data); } - return AWS_OP_SUCCESS; + return; } /* @@ -670,8 +569,6 @@ static int s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler( (*adapter->on_closed)(&adapter->base, NULL, adapter->on_closed_user_data); } } - - return AWS_OP_SUCCESS; } static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { @@ -683,8 +580,7 @@ static void s_adapter_disconnect_task_fn(struct aws_task *task, void *arg, enum goto done; } - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_disconnect_handler, disconnect_task); + s_aws_mqtt5_to_mqtt3_adapter_disconnect_handler(adapter, disconnect_task); done: @@ -741,14 +637,9 @@ static void s_aws_mqtt5_to_mqtt3_adapter_update_config_on_connect( } } -static int s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler( +static void s_aws_mqtt5_to_mqtt3_adapter_connect_handler( struct aws_mqtt_client_connection_5_impl *adapter, - void *context) { - struct aws_mqtt_adapter_connect_task *connect_task = context; - - if (adapter->synced_data.terminated) { - return AWS_OP_SUCCESS; - } + struct aws_mqtt_adapter_connect_task *connect_task) { AWS_LOGF_DEBUG( AWS_LS_MQTT5_TO_MQTT3_ADAPTER, @@ -765,7 +656,8 @@ static int s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler( false, connect_task->on_connection_complete_user_data); } - return AWS_OP_SUCCESS; + + return; } if (adapter->on_disconnect) { @@ -791,8 +683,6 @@ static int s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler( adapter->on_connection_complete = connect_task->on_connection_complete; adapter->on_connection_complete_user_data = connect_task->on_connection_complete_user_data; - - return AWS_OP_SUCCESS; } static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws_task_status status) { @@ -804,8 +694,7 @@ static void s_adapter_connect_task_fn(struct aws_task *task, void *arg, enum aws goto done; } - s_aws_mqtt5_adapter_perform_safe_callback( - adapter, false, s_aws_mqtt5_to_mqtt3_adapter_safe_connect_handler, connect_task); + s_aws_mqtt5_to_mqtt3_adapter_connect_handler(adapter, connect_task); done: @@ -1410,28 +1299,6 @@ static void s_aws_mqtt5_adapter_websocket_handshake_completion_fn( aws_ref_count_release(&adapter->internal_refs); } -struct aws_mqtt5_adapter_websocket_handshake_args { - bool chain_callback; - struct aws_http_message *input_request; - struct aws_http_message *output_request; - int completion_error_code; -}; - -static int s_safe_websocket_handshake_fn(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { - struct aws_mqtt5_adapter_websocket_handshake_args *args = context; - - if (adapter->synced_data.terminated) { - args->completion_error_code = AWS_ERROR_MQTT5_USER_REQUESTED_STOP; - } else if (adapter->websocket_handshake_transformer == NULL) { - args->output_request = args->input_request; - } else { - aws_ref_count_acquire(&adapter->internal_refs); - args->chain_callback = true; - } - - return AWS_OP_SUCCESS; -} - static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( struct aws_http_message *request, void *user_data, @@ -1440,13 +1307,10 @@ static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( struct aws_mqtt_client_connection_5_impl *adapter = user_data; - struct aws_mqtt5_adapter_websocket_handshake_args args = { - .input_request = request, - }; - - s_aws_mqtt5_adapter_perform_safe_callback(adapter, false, s_safe_websocket_handshake_fn, &args); - - if (args.chain_callback) { + if (adapter->websocket_handshake_transformer == NULL) { + (*complete_fn)(request, AWS_ERROR_SUCCESS, complete_ctx); + } else { + aws_ref_count_acquire(&adapter->internal_refs); adapter->mqtt5_websocket_handshake_completion_function = complete_fn; adapter->mqtt5_websocket_handshake_completion_user_data = complete_ctx; @@ -1455,8 +1319,6 @@ static void s_aws_mqtt5_adapter_transform_websocket_handshake_fn( adapter->websocket_handshake_transformer_user_data, s_aws_mqtt5_adapter_websocket_handshake_completion_fn, adapter); - } else { - (*complete_fn)(args.output_request, args.completion_error_code, complete_ctx); } } @@ -1872,36 +1734,23 @@ static struct aws_mqtt_client_connection *s_aws_mqtt_client_connection_5_acquire return &adapter->base; } -static int s_decref_for_shutdown(struct aws_mqtt_client_connection_5_impl *adapter, void *context) { - (void)context; - - adapter->synced_data.terminated = true; - - return AWS_OP_SUCCESS; -} - static void s_aws_mqtt5_to_mqtt3_adapter_on_zero_external_refs(void *impl) { struct aws_mqtt_client_connection_5_impl *adapter = impl; - s_aws_mqtt5_adapter_perform_safe_callback(adapter, true, s_decref_for_shutdown, NULL); - /* * When the adapter's exernal ref count goes to zero, here's what we want to do: * - * (1) Put the adapter into the terminated state, which tells it to stop processing callbacks from the mqtt5 - * client - * (2) Release the client listener, starting its asynchronous shutdown process (since we're the only user + * (1) Release the client listener, starting its asynchronous shutdown process (since we're the only user * of it) - * (3) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we + * (2) Wait for the client listener to notify us that asynchronous shutdown is over. At this point we * are guaranteed that no more callbacks from the mqtt5 client will reach us. - * (4) Release the single internal ref we started with when the adapter was created. - * (5) On last internal ref, we can safely release the mqtt5 client and synchronously clean up all other + * (3) Release the single internal ref we started with when the adapter was created. + * (4) On last internal ref, we can safely release the mqtt5 client and synchronously clean up all other * resources * - * Step (1) was done within the lock-guarded safe callback above. - * Step (2) is done here. - * Steps (3) and (4) are accomplished by s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached - * Step (5) is completed by s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs + * Step (1) is done here. + * Steps (2) and (3) are accomplished by s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached + * Step (4) is completed by s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs */ aws_mqtt5_listener_release(adapter->listener); } @@ -1991,6 +1840,22 @@ static void s_aws_mqtt5_to_mqtt3_adapter_publish_completion_fn( &publish_op->base.adapter->operational_state, publish_op->base.id); } +static void s_fail_publish(void *impl, int error_code) { + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *publish_op = impl; + + if (publish_op->on_publish_complete != NULL) { + (*publish_op->on_publish_complete)( + &publish_op->base.adapter->base, + publish_op->base.id, + error_code, + publish_op->on_publish_complete_user_data); + } +} + +static struct aws_mqtt5_to_mqtt3_adapter_operation_vtable s_publish_vtable = { + .fail_fn = s_fail_publish, +}; + struct aws_mqtt5_to_mqtt3_adapter_operation_publish *aws_mqtt5_to_mqtt3_adapter_operation_new_publish( struct aws_allocator *allocator, const struct aws_mqtt5_to_mqtt3_adapter_publish_options *options) { @@ -2000,6 +1865,7 @@ struct aws_mqtt5_to_mqtt3_adapter_operation_publish *aws_mqtt5_to_mqtt3_adapter_ publish_op->base.allocator = allocator; aws_ref_count_init(&publish_op->base.ref_count, publish_op, s_adapter_publish_operation_destroy); publish_op->base.impl = publish_op; + publish_op->base.vtable = &s_publish_vtable; publish_op->base.type = AWS_MQTT5TO3_AOT_PUBLISH; publish_op->base.adapter = options->adapter; publish_op->base.holding_adapter_ref = false; @@ -2180,12 +2046,11 @@ static enum aws_mqtt_qos s_convert_mqtt5_suback_reason_code_to_mqtt3_granted_qos } } -static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( +static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_helper( + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op, const struct aws_mqtt5_packet_suback_view *suback, - int error_code, - void *complete_ctx) { + int error_code) { - struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = complete_ctx; struct aws_mqtt_client_connection_5_impl *adapter = subscribe_op->base.adapter; struct aws_mqtt_subscription_set_subscription_record *record = NULL; @@ -2273,9 +2138,19 @@ static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( subscribe_op->on_multi_suback_user_data); } } +} - aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation( - &subscribe_op->base.adapter->operational_state, subscribe_op->base.id); +static void s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = complete_ctx; + struct aws_mqtt_client_connection_5_impl *adapter = subscribe_op->base.adapter; + + s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_helper(subscribe_op, suback, error_code); + + aws_mqtt5_to_mqtt3_adapter_operation_table_remove_operation(&adapter->operational_state, subscribe_op->base.id); } static int s_validate_adapter_subscribe_options( @@ -2366,6 +2241,16 @@ static int s_aws_mqtt5_to_mqtt3_adapter_build_subscribe( return AWS_OP_SUCCESS; } +static void s_fail_subscribe(void *impl, int error_code) { + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *subscribe_op = impl; + + s_aws_mqtt5_to_mqtt3_adapter_subscribe_completion_helper(subscribe_op, NULL, error_code); +} + +static struct aws_mqtt5_to_mqtt3_adapter_operation_vtable s_subscribe_vtable = { + .fail_fn = s_fail_subscribe, +}; + struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *aws_mqtt5_to_mqtt3_adapter_operation_new_subscribe( struct aws_allocator *allocator, const struct aws_mqtt5_to_mqtt3_adapter_subscribe_options *options, @@ -2381,6 +2266,7 @@ struct aws_mqtt5_to_mqtt3_adapter_operation_subscribe *aws_mqtt5_to_mqtt3_adapte subscribe_op->base.allocator = allocator; aws_ref_count_init(&subscribe_op->base.ref_count, subscribe_op, s_adapter_subscribe_operation_destroy); subscribe_op->base.impl = subscribe_op; + subscribe_op->base.vtable = &s_subscribe_vtable; subscribe_op->base.type = AWS_MQTT5TO3_AOT_SUBSCRIBE; subscribe_op->base.adapter = options->adapter; subscribe_op->base.holding_adapter_ref = false; @@ -2691,6 +2577,22 @@ static void s_aws_mqtt5_to_mqtt3_adapter_unsubscribe_completion_fn( &unsubscribe_op->base.adapter->operational_state, unsubscribe_op->base.id); } +static void s_fail_unsubscribe(void *impl, int error_code) { + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *unsubscribe_op = impl; + + if (unsubscribe_op->on_unsuback != NULL) { + (*unsubscribe_op->on_unsuback)( + &unsubscribe_op->base.adapter->base, + unsubscribe_op->base.id, + error_code, + unsubscribe_op->on_unsuback_user_data); + } +} + +static struct aws_mqtt5_to_mqtt3_adapter_operation_vtable s_unsubscribe_vtable = { + .fail_fn = s_fail_unsubscribe, +}; + struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *aws_mqtt5_to_mqtt3_adapter_operation_new_unsubscribe( struct aws_allocator *allocator, const struct aws_mqtt5_to_mqtt3_adapter_unsubscribe_options *options) { @@ -2701,6 +2603,7 @@ struct aws_mqtt5_to_mqtt3_adapter_operation_unsubscribe *aws_mqtt5_to_mqtt3_adap unsubscribe_op->base.allocator = allocator; aws_ref_count_init(&unsubscribe_op->base.ref_count, unsubscribe_op, s_adapter_unsubscribe_operation_destroy); unsubscribe_op->base.impl = unsubscribe_op; + unsubscribe_op->base.vtable = &s_unsubscribe_vtable; unsubscribe_op->base.type = AWS_MQTT5TO3_AOT_UNSUBSCRIBE; unsubscribe_op->base.adapter = options->adapter; unsubscribe_op->base.holding_adapter_ref = false; @@ -2996,28 +2899,17 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new_from_mqtt5_cli aws_ref_count_init(&adapter->external_refs, adapter, s_aws_mqtt5_to_mqtt3_adapter_on_zero_external_refs); aws_ref_count_init(&adapter->internal_refs, adapter, s_aws_mqtt5_to_mqtt3_adapter_on_zero_internal_refs); - aws_rw_lock_init(&adapter->lock); - aws_mqtt5_to_mqtt3_adapter_operation_table_init(&adapter->operational_state, allocator); adapter->subscriptions = aws_mqtt_subscription_set_new(allocator); - /* - * We start disabled to handle the case where someone passes in an mqtt5 client that is already "live." - * We'll enable the adapter as soon as they try to connect via the 311 interface. This - * also ties in to how we simulate the 311 implementation's don't-reconnect-if-initial-connect-fails logic. - * The 5 client will continue to try and reconnect, but the adapter will go disabled making it seem to the 311 - * user that it is offline. - */ - adapter->synced_data.terminated = false; - struct aws_mqtt5_listener_config listener_config = { .client = client, .listener_callbacks = { .listener_publish_received_handler = s_aws_mqtt5_listener_publish_received_adapter, .listener_publish_received_handler_user_data = adapter, - .lifecycle_event_handler = s_aws_mqtt5_client_lifecycle_event_callback_adapter, + .lifecycle_event_handler = s_aws_mqtt5_to_mqtt3_adapter_lifecycle_handler, .lifecycle_event_handler_user_data = adapter, }, .termination_callback = s_aws_mqtt5_to_mqtt3_adapter_on_listener_detached, @@ -3045,6 +2937,16 @@ void aws_mqtt5_to_mqtt3_adapter_operation_table_init( table->next_id = 1; } +static int s_adapter_operation_fail(void *context, struct aws_hash_element *operation_element) { + (void)context; + + struct aws_mqtt5_to_mqtt3_adapter_operation_base *operation = operation_element->value; + + (*operation->vtable->fail_fn)(operation->impl, AWS_ERROR_MQTT_CONNECTION_DESTROYED); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + static int s_adapter_operation_clean_up(void *context, struct aws_hash_element *operation_element) { (void)context; @@ -3056,6 +2958,7 @@ static int s_adapter_operation_clean_up(void *context, struct aws_hash_element * } void aws_mqtt5_to_mqtt3_adapter_operation_table_clean_up(struct aws_mqtt5_to_mqtt3_adapter_operation_table *table) { + aws_hash_table_foreach(&table->operations, s_adapter_operation_fail, table); aws_hash_table_foreach(&table->operations, s_adapter_operation_clean_up, table); aws_hash_table_clean_up(&table->operations); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2315095d..1b73ba70 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -426,6 +426,7 @@ add_test_case(mqtt5to3_adapter_resubscribe_nothing) add_test_case(mqtt5to3_adapter_resubscribe_something) add_test_case(mqtt5to3_adapter_subscribe_single_null_suback) add_test_case(mqtt5to3_adapter_subscribe_multi_null_suback) +add_test_case(mqtt5to3_adapter_operation_callbacks_after_shutdown) add_test_case(mqtt_subscription_set_add_empty_not_subbed) add_test_case(mqtt_subscription_set_add_single_path) diff --git a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c index 3b6c9dba..e3de0802 100644 --- a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c +++ b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c @@ -4221,3 +4221,112 @@ static int s_mqtt5to3_adapter_resubscribe_something_fn(struct aws_allocator *all } AWS_TEST_CASE(mqtt5to3_adapter_resubscribe_something, s_mqtt5to3_adapter_resubscribe_something_fn) + +static int s_mqtt5to3_adapter_operation_callbacks_after_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = NULL; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = NULL; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = NULL; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_mqtt5_to_mqtt3_adapter_test_fixture fixture; + ASSERT_SUCCESS(aws_mqtt5_to_mqtt3_adapter_test_fixture_init(&fixture, allocator, &test_fixture_options)); + + struct aws_mqtt_client_connection *connection = fixture.connection; + + struct aws_mqtt_connection_options connection_options; + s_init_adapter_connection_options_from_fixture(&connection_options, &fixture); + + connection_options.on_connection_complete = s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_connection_complete; + connection_options.user_data = &fixture; + + aws_mqtt_client_connection_connect(connection, &connection_options); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_CONNECTION_COMPLETE, 1); + + struct aws_byte_cursor topic1 = aws_byte_cursor_from_c_str("hello/world"); + struct aws_byte_cursor topic2 = aws_byte_cursor_from_c_str("hello/+"); + + aws_mqtt_client_connection_subscribe( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_topic_specific_publish, + &fixture, + NULL, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_subscribe_complete, + &fixture); + + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload 1!"); + + aws_mqtt_client_connection_publish( + connection, + &topic1, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload1, + s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_publish_complete, + &fixture); + + aws_mqtt_client_connection_unsubscribe( + connection, &topic2, s_aws_mqtt5_to_mqtt3_adapter_test_fixture_record_unsubscribe_complete, &fixture); + + aws_mqtt_client_connection_release(connection); + + s_wait_for_n_adapter_lifecycle_events(&fixture, AWS_MQTT3_LET_TERMINATION, 1); + fixture.connection = NULL; + + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_PUBLISH_COMPLETE, 1); + s_wait_for_n_adapter_operation_events(&fixture, AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, 1); + + struct aws_mqtt3_operation_event failed_ops[] = { + { + .type = AWS_MQTT3_OET_SUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_MQTT_CONNECTION_DESTROYED, + }, + { + .type = AWS_MQTT3_OET_PUBLISH_COMPLETE, + .error_code = AWS_ERROR_MQTT_CONNECTION_DESTROYED, + }, + { + .type = AWS_MQTT3_OET_UNSUBSCRIBE_COMPLETE, + .error_code = AWS_ERROR_MQTT_CONNECTION_DESTROYED, + }, + }; + + struct aws_mqtt_topic_subscription failed_subscriptions[] = { + { + .topic = topic1, + .qos = AWS_MQTT_QOS_FAILURE, + }, + }; + + aws_array_list_init_static_from_initialized( + &failed_ops[0].granted_subscriptions, + (void *)failed_subscriptions, + AWS_ARRAY_SIZE(failed_subscriptions), + sizeof(struct aws_mqtt_topic_subscription)); + + ASSERT_SUCCESS(s_aws_mqtt5_to_mqtt3_adapter_test_fixture_verify_operation_sequence_contains( + &fixture, AWS_ARRAY_SIZE(failed_ops), failed_ops)); + + aws_mqtt5_to_mqtt3_adapter_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt5to3_adapter_operation_callbacks_after_shutdown, + s_mqtt5to3_adapter_operation_callbacks_after_shutdown_fn) From ff4b23926fb7629b09c853c92d22f250a242c436 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 7 Nov 2023 11:26:47 -0800 Subject: [PATCH 93/98] Mqtt5 operation timeout (#333) Co-authored-by: Bret Ambrose --- .../aws/mqtt/private/v5/mqtt5_client_impl.h | 6 + .../mqtt/private/v5/mqtt5_options_storage.h | 7 +- include/aws/mqtt/v5/mqtt5_client.h | 14 +- source/v5/mqtt5_client.c | 172 +++++--- source/v5/mqtt5_options_storage.c | 37 +- source/v5/mqtt5_to_mqtt3_adapter.c | 2 +- tests/CMakeLists.txt | 2 + tests/v5/mqtt5_client_tests.c | 393 ++++++++++++++++++ 8 files changed, 570 insertions(+), 63 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_client_impl.h b/include/aws/mqtt/private/v5/mqtt5_client_impl.h index e251aee2..9a59de24 100644 --- a/include/aws/mqtt/private/v5/mqtt5_client_impl.h +++ b/include/aws/mqtt/private/v5/mqtt5_client_impl.h @@ -258,6 +258,12 @@ struct aws_mqtt5_client_operational_state { struct aws_linked_list unacked_operations; struct aws_linked_list write_completion_operations; + /* + * heap of operation pointers where the timeout is the sort value. Elements are added/removed from this + * data structure in exact synchronization with unacked_operations_table. + */ + struct aws_priority_queue operations_by_ack_timeout; + /* * Is there an io message in transit (to the socket) that has not invoked its write completion callback yet? * The client implementation only allows one in-transit message at a time, and so if this is true, we don't diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index 313a2fd6..9fe14817 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -42,6 +42,8 @@ struct aws_mqtt5_operation_vtable { int (*aws_mqtt5_operation_validate_vs_connection_settings_fn)( const void *operation_packet_view, const struct aws_mqtt5_client *client); + + uint32_t (*aws_mqtt5_operation_get_ack_timeout_override_fn)(const struct aws_mqtt5_operation *operation); }; /* Flags that indicate the way in which an operation is currently affecting the statistics of the client */ @@ -64,6 +66,7 @@ struct aws_mqtt5_operation { const struct aws_mqtt5_operation_vtable *vtable; struct aws_ref_count ref_count; uint64_t ack_timeout_timepoint_ns; + struct aws_priority_queue_node priority_queue_node; struct aws_linked_list_node node; enum aws_mqtt5_packet_type packet_type; @@ -163,7 +166,7 @@ struct aws_mqtt5_client_options_storage { uint64_t max_reconnect_delay_ms; uint64_t min_connected_time_to_reset_reconnect_delay_ms; - uint64_t ack_timeout_seconds; + uint32_t ack_timeout_seconds; uint32_t ping_timeout_ms; uint32_t connack_timeout_ms; @@ -208,6 +211,8 @@ AWS_MQTT_API int aws_mqtt5_operation_validate_vs_connection_settings( const struct aws_mqtt5_operation *operation, const struct aws_mqtt5_client *client); +AWS_MQTT_API uint32_t aws_mqtt5_operation_get_ack_timeout_override(const struct aws_mqtt5_operation *operation); + /* Connect */ AWS_MQTT_API struct aws_mqtt5_operation_connect *aws_mqtt5_operation_connect_new( diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 7cbb19a2..70206906 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -344,31 +344,37 @@ typedef void(aws_mqtt5_client_termination_completion_fn)(void *complete_ctx); /* operation completion options structures */ /** - * Completion callback options for the Publish operation + * Completion options for the Publish operation */ struct aws_mqtt5_publish_completion_options { aws_mqtt5_publish_completion_fn *completion_callback; void *completion_user_data; + + uint32_t ack_timeout_seconds_override; }; /** - * Completion callback options for the Subscribe operation + * Completion options for the Subscribe operation */ struct aws_mqtt5_subscribe_completion_options { aws_mqtt5_subscribe_completion_fn *completion_callback; void *completion_user_data; + + uint32_t ack_timeout_seconds_override; }; /** - * Completion callback options for the Unsubscribe operation + * Completion options for the Unsubscribe operation */ struct aws_mqtt5_unsubscribe_completion_options { aws_mqtt5_unsubscribe_completion_fn *completion_callback; void *completion_user_data; + + uint32_t ack_timeout_seconds_override; }; /** - * Public completion callback options for the a DISCONNECT operation + * Completion options for the a DISCONNECT operation */ struct aws_mqtt5_disconnect_completion_options { aws_mqtt5_disconnect_completion_fn *completion_callback; diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index e2c4e7e6..b5686155 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -172,6 +172,13 @@ static void s_complete_operation( const void *view) { if (client != NULL) { aws_mqtt5_client_statistics_change_operation_statistic_state(client, operation, AWS_MQTT5_OSS_NONE); + if (aws_priority_queue_node_is_in_queue(&operation->priority_queue_node)) { + struct aws_mqtt5_operation *queued_operation = NULL; + aws_priority_queue_remove( + &client->operational_state.operations_by_ack_timeout, + &queued_operation, + &operation->priority_queue_node); + } } aws_mqtt5_operation_complete(operation, error_code, packet_type, view); @@ -197,43 +204,47 @@ static void s_complete_operation_list( } static void s_check_timeouts(struct aws_mqtt5_client *client, uint64_t now) { - if (client->config->ack_timeout_seconds == 0) { - return; - } + struct aws_priority_queue *timeout_queue = &client->operational_state.operations_by_ack_timeout; + + bool done = aws_priority_queue_size(timeout_queue) == 0; + while (!done) { + struct aws_mqtt5_operation **next_operation_by_timeout_ptr = NULL; + aws_priority_queue_top(timeout_queue, (void **)&next_operation_by_timeout_ptr); + AWS_FATAL_ASSERT(next_operation_by_timeout_ptr != NULL); + struct aws_mqtt5_operation *next_operation_by_timeout = *next_operation_by_timeout_ptr; + AWS_FATAL_ASSERT(next_operation_by_timeout != NULL); + + // If the top of the heap hasn't timed out than nothing has + if (next_operation_by_timeout->ack_timeout_timepoint_ns > now) { + break; + } - struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); - while (node != aws_linked_list_end(&client->operational_state.unacked_operations)) { - struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); - node = aws_linked_list_next(node); - if (operation->ack_timeout_timepoint_ns < now) { - /* Timeout for this packet has been reached */ - aws_mqtt5_packet_id_t packet_id = aws_mqtt5_operation_get_packet_id(operation); - AWS_LOGF_INFO( - AWS_LS_MQTT5_CLIENT, - "id=%p: %s packet with id:%d has timed out", - (void *)client, - aws_mqtt5_packet_type_to_c_string(operation->packet_type), - (int)packet_id); - - struct aws_hash_element *elem = NULL; - aws_hash_table_find(&client->operational_state.unacked_operations_table, &packet_id, &elem); - - if (elem == NULL || elem->value == NULL) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CLIENT, - "id=%p: timeout for unknown operation with id %d", - (void *)client, - (int)packet_id); - return; - } + /* Ack timeout for this operation has been reached */ + aws_priority_queue_pop(timeout_queue, &next_operation_by_timeout); - aws_linked_list_remove(&operation->node); - aws_hash_table_remove(&client->operational_state.unacked_operations_table, &packet_id, NULL, NULL); + aws_mqtt5_packet_id_t packet_id = aws_mqtt5_operation_get_packet_id(next_operation_by_timeout); + AWS_LOGF_INFO( + AWS_LS_MQTT5_CLIENT, + "id=%p: %s packet with id:%d has timed out", + (void *)client, + aws_mqtt5_packet_type_to_c_string(next_operation_by_timeout->packet_type), + (int)packet_id); - s_complete_operation(client, operation, AWS_ERROR_MQTT_TIMEOUT, AWS_MQTT5_PT_NONE, NULL); - } else { - break; + struct aws_hash_element *elem = NULL; + aws_hash_table_find(&client->operational_state.unacked_operations_table, &packet_id, &elem); + + if (elem == NULL || elem->value == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CLIENT, "id=%p: timeout for unknown operation with id %d", (void *)client, (int)packet_id); + return; } + + aws_linked_list_remove(&next_operation_by_timeout->node); + aws_hash_table_remove(&client->operational_state.unacked_operations_table, &packet_id, NULL, NULL); + + s_complete_operation(client, next_operation_by_timeout, AWS_ERROR_MQTT_TIMEOUT, AWS_MQTT5_PT_NONE, NULL); + + done = aws_priority_queue_size(timeout_queue) == 0; } } @@ -412,7 +423,11 @@ static uint64_t s_compute_next_service_time_client_mqtt_connect(struct aws_mqtt5 return aws_min_u64(client->next_mqtt_connect_packet_timeout_time, operation_processing_time); } -static uint64_t s_min_non_0_64(uint64_t a, uint64_t b) { +/* + * Returns the minimum of two numbers, ignoring zero. Zero is returned only if both are zero. Useful when we're + * computing (next service) timepoints and zero means "no timepoint" + */ +static uint64_t s_min_non_zero_u64(uint64_t a, uint64_t b) { if (a == 0) { return b; } @@ -424,6 +439,19 @@ static uint64_t s_min_non_0_64(uint64_t a, uint64_t b) { return aws_min_u64(a, b); } +/* + * If there are unacked operations, returns the earliest point in time that one could timeout. + */ +static uint64_t s_get_unacked_operation_timeout_for_next_service_time(struct aws_mqtt5_client *client) { + if (aws_priority_queue_size(&client->operational_state.operations_by_ack_timeout) > 0) { + struct aws_mqtt5_operation **operation = NULL; + aws_priority_queue_top(&client->operational_state.operations_by_ack_timeout, (void **)&operation); + return (*operation)->ack_timeout_timepoint_ns; + } + + return 0; +} + static uint64_t s_compute_next_service_time_client_connected(struct aws_mqtt5_client *client, uint64_t now) { /* ping and ping timeout */ @@ -432,13 +460,8 @@ static uint64_t s_compute_next_service_time_client_connected(struct aws_mqtt5_cl next_service_time = aws_min_u64(next_service_time, client->next_ping_timeout_time); } - /* unacked operations timeout */ - if (client->config->ack_timeout_seconds != 0 && - !aws_linked_list_empty(&client->operational_state.unacked_operations)) { - struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); - struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); - next_service_time = aws_min_u64(next_service_time, operation->ack_timeout_timepoint_ns); - } + next_service_time = + s_min_non_zero_u64(next_service_time, s_get_unacked_operation_timeout_for_next_service_time(client)); if (client->desired_state != AWS_MCS_CONNECTED) { next_service_time = now; @@ -447,29 +470,21 @@ static uint64_t s_compute_next_service_time_client_connected(struct aws_mqtt5_cl uint64_t operation_processing_time = s_aws_mqtt5_client_compute_operational_state_service_time(&client->operational_state, now); - next_service_time = s_min_non_0_64(operation_processing_time, next_service_time); + next_service_time = s_min_non_zero_u64(operation_processing_time, next_service_time); /* reset reconnect delay interval */ - next_service_time = s_min_non_0_64(client->next_reconnect_delay_reset_time_ns, next_service_time); + next_service_time = s_min_non_zero_u64(client->next_reconnect_delay_reset_time_ns, next_service_time); return next_service_time; } static uint64_t s_compute_next_service_time_client_clean_disconnect(struct aws_mqtt5_client *client, uint64_t now) { - uint64_t ack_timeout_time = 0; - - /* unacked operations timeout */ - if (client->config->ack_timeout_seconds != 0 && - !aws_linked_list_empty(&client->operational_state.unacked_operations)) { - struct aws_linked_list_node *node = aws_linked_list_begin(&client->operational_state.unacked_operations); - struct aws_mqtt5_operation *operation = AWS_CONTAINER_OF(node, struct aws_mqtt5_operation, node); - ack_timeout_time = operation->ack_timeout_timepoint_ns; - } + uint64_t ack_timeout_time = s_get_unacked_operation_timeout_for_next_service_time(client); uint64_t operation_processing_time = s_aws_mqtt5_client_compute_operational_state_service_time(&client->operational_state, now); - return s_min_non_0_64(ack_timeout_time, operation_processing_time); + return s_min_non_zero_u64(ack_timeout_time, operation_processing_time); } static uint64_t s_compute_next_service_time_client_channel_shutdown(struct aws_mqtt5_client *client, uint64_t now) { @@ -587,8 +602,10 @@ static void s_aws_mqtt5_client_operational_state_reset( s_complete_operation_list(client, &client_operational_state->unacked_operations, completion_error_code); if (is_final) { + aws_priority_queue_clean_up(&client_operational_state->operations_by_ack_timeout); aws_hash_table_clean_up(&client_operational_state->unacked_operations_table); } else { + aws_priority_queue_clear(&client->operational_state.operations_by_ack_timeout); aws_hash_table_clear(&client_operational_state->unacked_operations_table); } } @@ -2497,6 +2514,25 @@ int aws_mqtt5_operation_bind_packet_id( return AWS_OP_ERR; } +/* + * Priority queue comparison function for ack timeout processing + */ +static int s_compare_operation_timeouts(const void *a, const void *b) { + const struct aws_mqtt5_operation **operation_a_ptr = (void *)a; + const struct aws_mqtt5_operation *operation_a = *operation_a_ptr; + + const struct aws_mqtt5_operation **operation_b_ptr = (void *)b; + const struct aws_mqtt5_operation *operation_b = *operation_b_ptr; + + if (operation_a->ack_timeout_timepoint_ns < operation_b->ack_timeout_timepoint_ns) { + return -1; + } else if (operation_a->ack_timeout_timepoint_ns > operation_b->ack_timeout_timepoint_ns) { + return 1; + } else { + return 0; + } +} + int aws_mqtt5_client_operational_state_init( struct aws_mqtt5_client_operational_state *client_operational_state, struct aws_allocator *allocator, @@ -2517,6 +2553,15 @@ int aws_mqtt5_client_operational_state_init( return AWS_OP_ERR; } + if (aws_priority_queue_init_dynamic( + &client_operational_state->operations_by_ack_timeout, + allocator, + 100, + sizeof(struct aws_mqtt5_operation *), + s_compare_operation_timeouts)) { + return AWS_OP_ERR; + } + client_operational_state->next_mqtt_packet_id = 1; client_operational_state->current_operation = NULL; client_operational_state->client = client; @@ -2631,6 +2676,7 @@ void aws_mqtt5_client_on_disconnection_update_operational_state(struct aws_mqtt5 client, &operations_to_fail, AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY); aws_hash_table_clear(&client->operational_state.unacked_operations_table); + aws_priority_queue_clear(&client->operational_state.operations_by_ack_timeout); /* * Prevents inbound resolution on the highly unlikely, illegal server behavior of sending a PUBLISH before @@ -3065,10 +3111,24 @@ int aws_mqtt5_client_service_operational_state(struct aws_mqtt5_client_operation break; } - if (client->config->ack_timeout_seconds != 0) { + uint32_t ack_timeout_seconds = aws_mqtt5_operation_get_ack_timeout_override(current_operation); + if (ack_timeout_seconds == 0) { + ack_timeout_seconds = client->config->ack_timeout_seconds; + } + + if (ack_timeout_seconds > 0) { current_operation->ack_timeout_timepoint_ns = - now + aws_timestamp_convert( - client->config->ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + now + aws_timestamp_convert(ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + } else { + current_operation->ack_timeout_timepoint_ns = UINT64_MAX; + } + + if (aws_priority_queue_push_ref( + &client_operational_state->operations_by_ack_timeout, + (void *)¤t_operation, + ¤t_operation->priority_queue_node)) { + operational_error_code = aws_last_error(); + break; } aws_linked_list_push_back(&client_operational_state->unacked_operations, ¤t_operation->node); diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 10625fa5..22888e77 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -309,11 +309,20 @@ int aws_mqtt5_operation_validate_vs_connection_settings( return AWS_OP_SUCCESS; } +uint32_t aws_mqtt5_operation_get_ack_timeout_override(const struct aws_mqtt5_operation *operation) { + if (operation->vtable->aws_mqtt5_operation_get_ack_timeout_override_fn != NULL) { + return (*operation->vtable->aws_mqtt5_operation_get_ack_timeout_override_fn)(operation); + } + + return 0; +} + static struct aws_mqtt5_operation_vtable s_empty_operation_vtable = { .aws_mqtt5_operation_completion_fn = NULL, .aws_mqtt5_operation_set_packet_id_fn = NULL, .aws_mqtt5_operation_get_packet_id_address_fn = NULL, .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, + .aws_mqtt5_operation_get_ack_timeout_override_fn = NULL, }; /********************************************************************************************************************* @@ -820,6 +829,7 @@ struct aws_mqtt5_operation_connect *aws_mqtt5_operation_connect_new( connect_op->base.vtable = &s_empty_operation_vtable; connect_op->base.packet_type = AWS_MQTT5_PT_CONNECT; aws_ref_count_init(&connect_op->base.ref_count, connect_op, s_destroy_operation_connect); + aws_priority_queue_node_init(&connect_op->base.priority_queue_node); connect_op->base.impl = connect_op; if (aws_mqtt5_packet_connect_storage_init(&connect_op->options_storage, allocator, connect_options)) { @@ -1502,6 +1512,7 @@ static struct aws_mqtt5_operation_vtable s_disconnect_operation_vtable = { .aws_mqtt5_operation_get_packet_id_address_fn = NULL, .aws_mqtt5_operation_validate_vs_connection_settings_fn = s_aws_mqtt5_packet_disconnect_view_validate_vs_connection_settings, + .aws_mqtt5_operation_get_ack_timeout_override_fn = NULL, }; struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_new( @@ -1525,6 +1536,7 @@ struct aws_mqtt5_operation_disconnect *aws_mqtt5_operation_disconnect_new( disconnect_op->base.vtable = &s_disconnect_operation_vtable; disconnect_op->base.packet_type = AWS_MQTT5_PT_DISCONNECT; aws_ref_count_init(&disconnect_op->base.ref_count, disconnect_op, s_destroy_operation_disconnect); + aws_priority_queue_node_init(&disconnect_op->base.priority_queue_node); disconnect_op->base.impl = disconnect_op; if (aws_mqtt5_packet_disconnect_storage_init(&disconnect_op->options_storage, allocator, disconnect_options)) { @@ -2063,13 +2075,18 @@ static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_publish_get_packet_id_addres return &publish_op->options_storage.storage_view.packet_id; } +static uint32_t s_aws_mqtt5_operation_publish_get_ack_timeout_override(const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_publish *publish_op = operation->impl; + return publish_op->completion_options.ack_timeout_seconds_override; +} + static struct aws_mqtt5_operation_vtable s_publish_operation_vtable = { .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_publish_complete, .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_publish_set_packet_id, .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_publish_get_packet_id_address, .aws_mqtt5_operation_validate_vs_connection_settings_fn = s_aws_mqtt5_packet_publish_view_validate_vs_connection_settings, -}; + .aws_mqtt5_operation_get_ack_timeout_override_fn = s_aws_mqtt5_operation_publish_get_ack_timeout_override}; static void s_destroy_operation_publish(void *object) { if (object == NULL) { @@ -2115,6 +2132,7 @@ struct aws_mqtt5_operation_publish *aws_mqtt5_operation_publish_new( publish_op->base.vtable = &s_publish_operation_vtable; publish_op->base.packet_type = AWS_MQTT5_PT_PUBLISH; aws_ref_count_init(&publish_op->base.ref_count, publish_op, s_destroy_operation_publish); + aws_priority_queue_node_init(&publish_op->base.priority_queue_node); publish_op->base.impl = publish_op; if (aws_mqtt5_packet_publish_storage_init(&publish_op->options_storage, allocator, publish_options)) { @@ -2285,6 +2303,7 @@ struct aws_mqtt5_operation_puback *aws_mqtt5_operation_puback_new( puback_op->base.vtable = &s_empty_operation_vtable; puback_op->base.packet_type = AWS_MQTT5_PT_PUBACK; aws_ref_count_init(&puback_op->base.ref_count, puback_op, s_destroy_operation_puback); + aws_priority_queue_node_init(&puback_op->base.priority_queue_node); puback_op->base.impl = puback_op; if (aws_mqtt5_packet_puback_storage_init(&puback_op->options_storage, allocator, puback_options)) { @@ -2523,11 +2542,18 @@ static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_unsubscribe_get_packet_id_ad return &unsubscribe_op->options_storage.storage_view.packet_id; } +static uint32_t s_aws_mqtt5_operation_unsubscribe_get_ack_timeout_override( + const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_unsubscribe *unsubscribe_op = operation->impl; + return unsubscribe_op->completion_options.ack_timeout_seconds_override; +} + static struct aws_mqtt5_operation_vtable s_unsubscribe_operation_vtable = { .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_unsubscribe_complete, .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_unsubscribe_set_packet_id, .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_unsubscribe_get_packet_id_address, .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, + .aws_mqtt5_operation_get_ack_timeout_override_fn = s_aws_mqtt5_operation_unsubscribe_get_ack_timeout_override, }; static void s_destroy_operation_unsubscribe(void *object) { @@ -2574,6 +2600,7 @@ struct aws_mqtt5_operation_unsubscribe *aws_mqtt5_operation_unsubscribe_new( unsubscribe_op->base.vtable = &s_unsubscribe_operation_vtable; unsubscribe_op->base.packet_type = AWS_MQTT5_PT_UNSUBSCRIBE; aws_ref_count_init(&unsubscribe_op->base.ref_count, unsubscribe_op, s_destroy_operation_unsubscribe); + aws_priority_queue_node_init(&unsubscribe_op->base.priority_queue_node); unsubscribe_op->base.impl = unsubscribe_op; if (aws_mqtt5_packet_unsubscribe_storage_init(&unsubscribe_op->options_storage, allocator, unsubscribe_options)) { @@ -2906,11 +2933,17 @@ static aws_mqtt5_packet_id_t *s_aws_mqtt5_operation_subscribe_get_packet_id_addr return &subscribe_op->options_storage.storage_view.packet_id; } +static uint32_t s_aws_mqtt5_operation_subscribe_get_ack_timeout_override(const struct aws_mqtt5_operation *operation) { + struct aws_mqtt5_operation_subscribe *subscribe_op = operation->impl; + return subscribe_op->completion_options.ack_timeout_seconds_override; +} + static struct aws_mqtt5_operation_vtable s_subscribe_operation_vtable = { .aws_mqtt5_operation_completion_fn = s_aws_mqtt5_operation_subscribe_complete, .aws_mqtt5_operation_set_packet_id_fn = s_aws_mqtt5_operation_subscribe_set_packet_id, .aws_mqtt5_operation_get_packet_id_address_fn = s_aws_mqtt5_operation_subscribe_get_packet_id_address, .aws_mqtt5_operation_validate_vs_connection_settings_fn = NULL, + .aws_mqtt5_operation_get_ack_timeout_override_fn = s_aws_mqtt5_operation_subscribe_get_ack_timeout_override, }; static void s_destroy_operation_subscribe(void *object) { @@ -2957,6 +2990,7 @@ struct aws_mqtt5_operation_subscribe *aws_mqtt5_operation_subscribe_new( subscribe_op->base.vtable = &s_subscribe_operation_vtable; subscribe_op->base.packet_type = AWS_MQTT5_PT_SUBSCRIBE; aws_ref_count_init(&subscribe_op->base.ref_count, subscribe_op, s_destroy_operation_subscribe); + aws_priority_queue_node_init(&subscribe_op->base.priority_queue_node); subscribe_op->base.impl = subscribe_op; if (aws_mqtt5_packet_subscribe_storage_init(&subscribe_op->options_storage, allocator, subscribe_options)) { @@ -3292,6 +3326,7 @@ struct aws_mqtt5_operation_pingreq *aws_mqtt5_operation_pingreq_new(struct aws_a pingreq_op->base.vtable = &s_empty_operation_vtable; pingreq_op->base.packet_type = AWS_MQTT5_PT_PINGREQ; aws_ref_count_init(&pingreq_op->base.ref_count, pingreq_op, s_destroy_operation_pingreq); + aws_priority_queue_node_init(&pingreq_op->base.priority_queue_node); pingreq_op->base.impl = pingreq_op; return pingreq_op; diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index ccad38d7..e48ca3d3 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -619,7 +619,7 @@ static void s_aws_mqtt5_to_mqtt3_adapter_update_config_on_connect( config->ping_timeout_ms = connect_task->ping_timeout_ms; /* Override timeout, rounding up as necessary */ - config->ack_timeout_seconds = aws_timestamp_convert( + config->ack_timeout_seconds = (uint32_t)aws_timestamp_convert( connect_task->protocol_operation_timeout_ms + AWS_TIMESTAMP_MILLIS - 1, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_SECS, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1b73ba70..70e6c76a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -336,6 +336,8 @@ add_test_case(mqtt5_client_subscribe_fail_packet_too_big) add_test_case(mqtt5_client_disconnect_fail_packet_too_big) add_test_case(mqtt5_client_flow_control_receive_maximum) add_test_case(mqtt5_client_publish_timeout) +add_test_case(mqtt5_client_dynamic_operation_timeout) +add_test_case(mqtt5_client_dynamic_operation_timeout_default) add_test_case(mqtt5_client_flow_control_iot_core_throughput) add_test_case(mqtt5_client_flow_control_iot_core_publish_tps) add_test_case(mqtt5_client_session_resumption_clean_start) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 78d9c935..911b1822 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -6076,3 +6076,396 @@ static int s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn(struct aws AWS_TEST_CASE( mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a, s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn) + +struct mqtt5_operation_timeout_completion_callback { + enum aws_mqtt5_packet_type type; + uint64_t timepoint_ns; + int error_code; +}; + +struct mqtt5_dynamic_operation_timeout_test_context { + struct aws_allocator *allocator; + struct aws_mqtt5_client_mock_test_fixture *fixture; + struct aws_array_list completion_callbacks; + + const struct mqtt5_operation_timeout_completion_callback *expected_callbacks; + size_t expected_callback_count; +}; + +static void s_mqtt5_dynamic_operation_timeout_test_context_init( + struct mqtt5_dynamic_operation_timeout_test_context *context, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mock_test_fixture *fixture) { + AWS_ZERO_STRUCT(*context); + + context->allocator = allocator; + context->fixture = fixture; + aws_array_list_init_dynamic( + &context->completion_callbacks, allocator, 5, sizeof(struct mqtt5_operation_timeout_completion_callback)); +} + +static void s_mqtt5_dynamic_operation_timeout_test_context_cleanup( + struct mqtt5_dynamic_operation_timeout_test_context *context) { + aws_array_list_clean_up(&context->completion_callbacks); +} + +static bool s_mqtt5_dynamic_operation_timeout_test_context_callback_sequence_equals_expected( + struct mqtt5_dynamic_operation_timeout_test_context *context) { + + if (context->expected_callback_count != aws_array_list_length(&context->completion_callbacks)) { + return false; + } + + for (size_t i = 0; i < context->expected_callback_count; ++i) { + const struct mqtt5_operation_timeout_completion_callback *expected_callback = &context->expected_callbacks[i]; + struct mqtt5_operation_timeout_completion_callback *callback = NULL; + aws_array_list_get_at_ptr(&context->completion_callbacks, (void **)&callback, i); + + if (callback->type != expected_callback->type) { + return false; + } + + if (callback->error_code != expected_callback->error_code) { + return false; + } + } + + return true; +} + +static void s_add_completion_callback( + struct mqtt5_dynamic_operation_timeout_test_context *context, + enum aws_mqtt5_packet_type type, + int error_code) { + aws_mutex_lock(&context->fixture->lock); + + struct mqtt5_operation_timeout_completion_callback callback_entry = { + .type = type, .error_code = error_code, .timepoint_ns = 0}; + + aws_high_res_clock_get_ticks(&callback_entry.timepoint_ns); + + aws_array_list_push_back(&context->completion_callbacks, &callback_entry); + + aws_mutex_unlock(&context->fixture->lock); + + aws_condition_variable_notify_all(&context->fixture->signal); +} + +static void s_timeout_test_publish_completion_fn( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + (void)packet_type; + (void)packet; + + s_add_completion_callback(complete_ctx, AWS_MQTT5_PT_PUBLISH, error_code); +} + +static void s_timeout_test_subscribe_completion_fn( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + (void)suback; + + s_add_completion_callback(complete_ctx, AWS_MQTT5_PT_SUBSCRIBE, error_code); +} + +static void s_timeout_test_unsubscribe_completion_fn( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + (void)unsuback; + + s_add_completion_callback(complete_ctx, AWS_MQTT5_PT_UNSUBSCRIBE, error_code); +} + +static bool s_all_timeout_operations_complete(void *arg) { + struct mqtt5_dynamic_operation_timeout_test_context *context = arg; + + return aws_array_list_length(&context->completion_callbacks) == context->expected_callback_count; +} + +static void s_wait_for_all_operation_timeouts(struct mqtt5_dynamic_operation_timeout_test_context *context) { + aws_mutex_lock(&context->fixture->lock); + aws_condition_variable_wait_pred( + &context->fixture->signal, &context->fixture->lock, s_all_timeout_operations_complete, context); + aws_mutex_unlock(&context->fixture->lock); +} + +/* + * Tests a mixture of qos 0 publish, qos 1 publish, subscribe, and unsubscribe with override ack timeouts. + * + * qos 1 publish with 3 second timeout + * subscribe with 2 second timeout + * qos 0 publish + * unsubscribe with 4 second timeout + * qos 1 publish with 1 second timeout + * + * We expect to see callbacks in sequence: + * + * qos 0 publish success + * qos 1 publish failure by timeout after 1 second + * subscribe failure by timeout after 2 seconds + * qos 1 publish failure by timeout after 3 seconds + * unsubscribe failure by timeout after 4 seconds + */ +static int s_mqtt5_client_dynamic_operation_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_timeout_publish; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct mqtt5_dynamic_operation_timeout_test_context context; + s_mqtt5_dynamic_operation_timeout_test_context_init(&context, allocator, &test_context); + + struct mqtt5_operation_timeout_completion_callback expected_callbacks[] = { + { + .type = AWS_MQTT5_PT_PUBLISH, + .error_code = AWS_ERROR_SUCCESS, + }, + { + .type = AWS_MQTT5_PT_PUBLISH, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }, + { + .type = AWS_MQTT5_PT_SUBSCRIBE, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }, + { + .type = AWS_MQTT5_PT_PUBLISH, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }, + { + .type = AWS_MQTT5_PT_UNSUBSCRIBE, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }, + }; + + context.expected_callbacks = expected_callbacks; + context.expected_callback_count = AWS_ARRAY_SIZE(expected_callbacks); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&test_context); + + uint64_t operation_start = 0; + aws_high_res_clock_get_ticks(&operation_start); + + struct aws_byte_cursor topic = { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }; + + // qos 1 publish - 3 second timeout + struct aws_mqtt5_packet_publish_view qos1_publish = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = topic, + }; + + struct aws_mqtt5_publish_completion_options qos1_publish_options = { + .completion_callback = s_timeout_test_publish_completion_fn, + .completion_user_data = &context, + .ack_timeout_seconds_override = 3, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish, &qos1_publish_options)); + + // subscribe - 2 seconds timeout + struct aws_mqtt5_subscription_view subscriptions[] = {{ + .topic_filter = topic, + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + }}; + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = subscriptions, + .subscription_count = AWS_ARRAY_SIZE(subscriptions), + }; + + struct aws_mqtt5_subscribe_completion_options subscribe_options = { + .completion_callback = s_timeout_test_subscribe_completion_fn, + .completion_user_data = &context, + .ack_timeout_seconds_override = 2, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_subscribe(client, &subscribe_view, &subscribe_options)); + + // qos 0 publish + struct aws_mqtt5_packet_publish_view qos0_publish = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .topic = topic, + }; + + struct aws_mqtt5_publish_completion_options qos0_publish_options = { + .completion_callback = s_timeout_test_publish_completion_fn, + .completion_user_data = &context, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos0_publish, &qos0_publish_options)); + + // unsubscribe - 4 second timeout + struct aws_byte_cursor topic_filters[] = { + topic, + }; + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe = { + .topic_filters = topic_filters, + .topic_filter_count = AWS_ARRAY_SIZE(topic_filters), + }; + + struct aws_mqtt5_unsubscribe_completion_options unsubscribe_options = { + .completion_callback = s_timeout_test_unsubscribe_completion_fn, + .completion_user_data = &context, + .ack_timeout_seconds_override = 4, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_unsubscribe(client, &unsubscribe, &unsubscribe_options)); + + // qos 1 publish - 1 second timeout + qos1_publish_options.ack_timeout_seconds_override = 1; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish, &qos1_publish_options)); + + s_wait_for_all_operation_timeouts(&context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_context); + + aws_mutex_lock(&test_context.lock); + s_mqtt5_dynamic_operation_timeout_test_context_callback_sequence_equals_expected(&context); + aws_mutex_unlock(&test_context.lock); + + /* + * Finally, do a minimum time elapsed check: + * each operation after the first (the qos 0 publish which did not time out) should have a completion + * timepoint N seconds or later after the start of the test (where N is the operation's index in the sequence) + */ + for (size_t i = 1; i < context.expected_callback_count; ++i) { + struct mqtt5_operation_timeout_completion_callback *callback = NULL; + aws_array_list_get_at_ptr(&context.completion_callbacks, (void **)&callback, i); + + uint64_t delta_ns = callback->timepoint_ns - operation_start; + ASSERT_TRUE(delta_ns >= aws_timestamp_convert(i, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + s_mqtt5_dynamic_operation_timeout_test_context_cleanup(&context); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_dynamic_operation_timeout, s_mqtt5_client_dynamic_operation_timeout_fn) + +#define DYNAMIC_TIMEOUT_DEFAULT_SECONDS 2 + +/* + * Checks that using a override operation timeout of zero results in using the client's default timeout + */ +static int s_mqtt5_client_dynamic_operation_timeout_default_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + test_options.client_options.ack_timeout_seconds = DYNAMIC_TIMEOUT_DEFAULT_SECONDS; + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_mock_server_handle_timeout_publish; + + struct aws_mqtt5_client_mock_test_fixture test_context; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_mock_test_fixture_init(&test_context, allocator, &test_fixture_options)); + + struct mqtt5_dynamic_operation_timeout_test_context context; + s_mqtt5_dynamic_operation_timeout_test_context_init(&context, allocator, &test_context); + + struct mqtt5_operation_timeout_completion_callback expected_callbacks[] = { + { + .type = AWS_MQTT5_PT_PUBLISH, + .error_code = AWS_ERROR_MQTT_TIMEOUT, + }, + }; + + context.expected_callbacks = expected_callbacks; + context.expected_callback_count = AWS_ARRAY_SIZE(expected_callbacks); + + struct aws_mqtt5_client *client = test_context.client; + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&test_context); + + uint64_t operation_start = 0; + aws_high_res_clock_get_ticks(&operation_start); + + struct aws_byte_cursor topic = { + .ptr = s_topic, + .len = AWS_ARRAY_SIZE(s_topic) - 1, + }; + + struct aws_mqtt5_packet_publish_view qos1_publish = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic = topic, + }; + + struct aws_mqtt5_publish_completion_options qos1_publish_options = { + .completion_callback = s_timeout_test_publish_completion_fn, + .completion_user_data = &context, + .ack_timeout_seconds_override = 0, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_publish(client, &qos1_publish, &qos1_publish_options)); + + s_wait_for_all_operation_timeouts(&context); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + aws_wait_for_stopped_lifecycle_event(&test_context); + + aws_mutex_lock(&test_context.lock); + s_mqtt5_dynamic_operation_timeout_test_context_callback_sequence_equals_expected(&context); + aws_mutex_unlock(&test_context.lock); + + /* + * Finally, do a minimum time elapsed check: + */ + for (size_t i = 0; i < context.expected_callback_count; ++i) { + struct mqtt5_operation_timeout_completion_callback *callback = NULL; + aws_array_list_get_at_ptr(&context.completion_callbacks, (void **)&callback, i); + + uint64_t delta_ns = callback->timepoint_ns - operation_start; + ASSERT_TRUE( + delta_ns >= + aws_timestamp_convert(DYNAMIC_TIMEOUT_DEFAULT_SECONDS, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + } + + s_mqtt5_dynamic_operation_timeout_test_context_cleanup(&context); + aws_mqtt5_client_mock_test_fixture_clean_up(&test_context); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt5_client_dynamic_operation_timeout_default, s_mqtt5_client_dynamic_operation_timeout_default_fn) \ No newline at end of file From 5d198cf2d09b19bb18bf03e4425831a246d0a391 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 9 Nov 2023 14:05:08 -0800 Subject: [PATCH 94/98] Relax validation on user-specified topic aliasing (#334) --- include/aws/mqtt/private/v5/mqtt5_utils.h | 18 +++++ include/aws/mqtt/v5/mqtt5_client.h | 9 ++- source/v5/mqtt5_encoder.c | 18 +++++ source/v5/mqtt5_options_storage.c | 38 ++++------- source/v5/mqtt5_topic_alias.c | 66 +++++++++---------- source/v5/mqtt5_utils.c | 16 ++++- tests/CMakeLists.txt | 15 ++--- tests/v5/mqtt5_client_tests.c | 40 +++-------- ...mqtt5_operation_validation_failure_tests.c | 33 ---------- tests/v5/mqtt5_topic_alias_tests.c | 30 +++++---- 10 files changed, 133 insertions(+), 150 deletions(-) diff --git a/include/aws/mqtt/private/v5/mqtt5_utils.h b/include/aws/mqtt/private/v5/mqtt5_utils.h index 9a05dedf..697a4330 100644 --- a/include/aws/mqtt/private/v5/mqtt5_utils.h +++ b/include/aws/mqtt/private/v5/mqtt5_utils.h @@ -210,6 +210,15 @@ AWS_MQTT_API enum aws_mqtt5_client_session_behavior_type aws_mqtt5_client_sessio AWS_MQTT_API const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior); +/** + * Checks an outbound aliasing behavior type value for validity + * + * @param outbound_aliasing_behavior value to check + * @return true if this is a valid value, false otherwise + */ +AWS_MQTT_API bool aws_mqtt5_outbound_topic_alias_behavior_type_validate( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior); + /** * Converts an outbound topic aliasing behavior type value to a final non-default value. * @@ -229,6 +238,15 @@ AWS_MQTT_API enum aws_mqtt5_client_outbound_topic_alias_behavior_type AWS_MQTT_API const char *aws_mqtt5_inbound_topic_alias_behavior_type_to_c_string( enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior); +/** + * Checks an inbound aliasing behavior type value for validity + * + * @param inbound_aliasing_behavior value to check + * @return true if this is a valid value, false otherwise + */ +AWS_MQTT_API bool aws_mqtt5_inbound_topic_alias_behavior_type_validate( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior); + /** * Converts an inbound topic aliasing behavior type value to a final non-default value. * diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 70206906..b14d7790 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -86,16 +86,15 @@ enum aws_mqtt5_client_outbound_topic_alias_behavior_type { * topic alias mappings unpredictably. The client will properly use the alias when the current connection * has seen the alias binding already. */ - AWS_MQTT5_COTABT_USER, + AWS_MQTT5_COTABT_MANUAL, /** - * Client fails any user-specified topic aliasing and acts on the outbound alias set as an LRU cache. + * Client ignores any user-specified topic aliasing and acts on the outbound alias set as an LRU cache. */ AWS_MQTT5_COTABT_LRU, /** - * Completely disable outbound topic aliasing. Attempting to set a topic alias on a PUBLISH results in - * an error. + * Completely disable outbound topic aliasing. */ AWS_MQTT5_COTABT_DISABLED }; @@ -162,7 +161,7 @@ struct aws_mqtt5_client_topic_alias_options { * disabled, this setting has no effect. * * Behaviorally, this value overrides anything present in the topic_alias_maximum field of - * the CONNECT packet options. We intentionally don't bind that field to managed clients to reduce + * the CONNECT packet options. */ uint16_t inbound_alias_cache_size; }; diff --git a/source/v5/mqtt5_encoder.c b/source/v5/mqtt5_encoder.c index b9a3ec56..fe1ab3ab 100644 --- a/source/v5/mqtt5_encoder.c +++ b/source/v5/mqtt5_encoder.c @@ -800,7 +800,25 @@ static int s_aws_mqtt5_encoder_begin_publish(struct aws_mqtt5_encoder *encoder, local_publish_view.topic = outbound_topic; if (outbound_topic_alias != 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - PUBLISH packet using topic alias value %" PRIu16, + (void *)encoder->config.client, + outbound_topic_alias); + if (outbound_topic.len == 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - PUBLISH packet dropping topic field for previously established alias", + (void *)encoder->config.client); + } local_publish_view.topic_alias = &outbound_topic_alias; + } else { + AWS_FATAL_ASSERT(local_publish_view.topic.len > 0); + AWS_LOGF_DEBUG( + AWS_LS_MQTT5_GENERAL, + "(%p) mqtt5 client encoder - PUBLISH packet not using a topic alias", + (void *)encoder->config.client); + local_publish_view.topic_alias = NULL; } } diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 22888e77..68a06a88 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -1743,30 +1743,6 @@ static int s_aws_mqtt5_packet_publish_view_validate_vs_connection_settings( return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); } - if (publish_view->topic_alias != NULL) { - const struct aws_mqtt5_client_options_storage *client_options = client->config; - if (client_options->topic_aliasing_options.outbound_topic_alias_behavior != AWS_MQTT5_COTABT_USER) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_publish_view - topic alias set but outbound topic alias behavior has not " - "been set to user controlled", - (void *)publish_view); - return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); - } - - if (*publish_view->topic_alias > settings->topic_alias_maximum_to_server) { - AWS_LOGF_ERROR( - AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_packet_publish_view - outbound topic alias (%d) exceeds server's topic alias " - "maximum " - "(%d)", - (void *)publish_view, - (int)(*publish_view->topic_alias), - (int)settings->topic_alias_maximum_to_server); - return aws_raise_error(AWS_ERROR_MQTT5_PUBLISH_OPTIONS_VALIDATION); - } - } - if (publish_view->retain && settings->retain_available == false) { AWS_LOGF_ERROR( AWS_LS_MQTT5_GENERAL, @@ -3416,6 +3392,20 @@ int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *opt return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); } + if (options->topic_aliasing_options != NULL) { + if (!aws_mqtt5_outbound_topic_alias_behavior_type_validate( + options->topic_aliasing_options->outbound_topic_alias_behavior)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid outbound topic alias behavior type value"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + + if (!aws_mqtt5_inbound_topic_alias_behavior_type_validate( + options->topic_aliasing_options->inbound_topic_alias_behavior)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid inbound topic alias behavior type value"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + } + return AWS_OP_SUCCESS; } diff --git a/source/v5/mqtt5_topic_alias.c b/source/v5/mqtt5_topic_alias.c index 91ec8c60..0b72f86a 100644 --- a/source/v5/mqtt5_topic_alias.c +++ b/source/v5/mqtt5_topic_alias.c @@ -132,7 +132,7 @@ static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topi struct aws_allocator *allocator); static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_lru_new( struct aws_allocator *allocator); -static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_user_new( +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_manual_new( struct aws_allocator *allocator); struct aws_mqtt5_outbound_topic_alias_resolver *aws_mqtt5_outbound_topic_alias_resolver_new( @@ -140,8 +140,8 @@ struct aws_mqtt5_outbound_topic_alias_resolver *aws_mqtt5_outbound_topic_alias_r enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_alias_behavior) { switch (aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default(outbound_alias_behavior)) { - case AWS_MQTT5_COTABT_USER: - return s_aws_mqtt5_outbound_topic_alias_resolver_user_new(allocator); + case AWS_MQTT5_COTABT_MANUAL: + return s_aws_mqtt5_outbound_topic_alias_resolver_manual_new(allocator); case AWS_MQTT5_COTABT_LRU: return s_aws_mqtt5_outbound_topic_alias_resolver_lru_new(allocator); @@ -243,60 +243,60 @@ static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topi } /* - * User resolver + * Manual resolver * - * User resolution implies the user is controlling the topic alias assignments, but we still want to validate their + * Manual resolution implies the user is controlling the topic alias assignments, but we still want to validate their * actions. In particular, we track the currently valid set of aliases (based on previous outbound publishes) * and only use an alias when the submitted publish is an exact match for the current assignment. */ -struct aws_mqtt5_outbound_topic_alias_resolver_user { +struct aws_mqtt5_outbound_topic_alias_resolver_manual { struct aws_mqtt5_outbound_topic_alias_resolver base; struct aws_array_list aliases; }; -static void s_cleanup_user_aliases(struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver) { - for (size_t i = 0; i < aws_array_list_length(&user_resolver->aliases); ++i) { +static void s_cleanup_manual_aliases(struct aws_mqtt5_outbound_topic_alias_resolver_manual *manual_resolver) { + for (size_t i = 0; i < aws_array_list_length(&manual_resolver->aliases); ++i) { struct aws_string *alias = NULL; - aws_array_list_get_at(&user_resolver->aliases, &alias, i); + aws_array_list_get_at(&manual_resolver->aliases, &alias, i); aws_string_destroy(alias); } - aws_array_list_clean_up(&user_resolver->aliases); - AWS_ZERO_STRUCT(user_resolver->aliases); + aws_array_list_clean_up(&manual_resolver->aliases); + AWS_ZERO_STRUCT(manual_resolver->aliases); } -static void s_aws_mqtt5_outbound_topic_alias_resolver_user_destroy( +static void s_aws_mqtt5_outbound_topic_alias_resolver_manual_destroy( struct aws_mqtt5_outbound_topic_alias_resolver *resolver) { if (resolver == NULL) { return; } - struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; - s_cleanup_user_aliases(user_resolver); + struct aws_mqtt5_outbound_topic_alias_resolver_manual *manual_resolver = resolver->impl; + s_cleanup_manual_aliases(manual_resolver); - aws_mem_release(resolver->allocator, user_resolver); + aws_mem_release(resolver->allocator, manual_resolver); } -static int s_aws_mqtt5_outbound_topic_alias_resolver_user_reset( +static int s_aws_mqtt5_outbound_topic_alias_resolver_manual_reset( struct aws_mqtt5_outbound_topic_alias_resolver *resolver, uint16_t topic_alias_maximum) { - struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; - s_cleanup_user_aliases(user_resolver); + struct aws_mqtt5_outbound_topic_alias_resolver_manual *manual_resolver = resolver->impl; + s_cleanup_manual_aliases(manual_resolver); aws_array_list_init_dynamic( - &user_resolver->aliases, resolver->allocator, topic_alias_maximum, sizeof(struct aws_string *)); + &manual_resolver->aliases, resolver->allocator, topic_alias_maximum, sizeof(struct aws_string *)); for (size_t i = 0; i < topic_alias_maximum; ++i) { struct aws_string *invalid_alias = NULL; - aws_array_list_push_back(&user_resolver->aliases, &invalid_alias); + aws_array_list_push_back(&manual_resolver->aliases, &invalid_alias); } return AWS_OP_SUCCESS; } -static int s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publish_fn( +static int s_aws_mqtt5_outbound_topic_alias_resolver_manual_resolve_outbound_publish_fn( struct aws_mqtt5_outbound_topic_alias_resolver *resolver, const struct aws_mqtt5_packet_publish_view *publish_view, uint16_t *topic_alias_out, @@ -316,15 +316,15 @@ static int s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publi return aws_raise_error(AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS); } - struct aws_mqtt5_outbound_topic_alias_resolver_user *user_resolver = resolver->impl; + struct aws_mqtt5_outbound_topic_alias_resolver_manual *manual_resolver = resolver->impl; uint16_t user_alias_index = user_alias - 1; - if (user_alias_index >= aws_array_list_length(&user_resolver->aliases)) { + if (user_alias_index >= aws_array_list_length(&manual_resolver->aliases)) { /* should have been caught by dynamic publish validation */ return aws_raise_error(AWS_ERROR_MQTT5_INVALID_OUTBOUND_TOPIC_ALIAS); } struct aws_string *current_assignment = NULL; - aws_array_list_get_at(&user_resolver->aliases, ¤t_assignment, user_alias_index); + aws_array_list_get_at(&manual_resolver->aliases, ¤t_assignment, user_alias_index); *topic_alias_out = user_alias; @@ -346,25 +346,25 @@ static int s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publi if (!can_use_alias) { aws_string_destroy(current_assignment); current_assignment = aws_string_new_from_cursor(resolver->allocator, &publish_view->topic); - aws_array_list_set_at(&user_resolver->aliases, ¤t_assignment, user_alias_index); + aws_array_list_set_at(&manual_resolver->aliases, ¤t_assignment, user_alias_index); } return AWS_OP_SUCCESS; } -static struct aws_mqtt5_outbound_topic_alias_resolver_vtable s_aws_mqtt5_outbound_topic_alias_resolver_user_vtable = { - .destroy_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_destroy, - .reset_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_reset, - .resolve_outbound_publish_fn = s_aws_mqtt5_outbound_topic_alias_resolver_user_resolve_outbound_publish_fn, +static struct aws_mqtt5_outbound_topic_alias_resolver_vtable s_aws_mqtt5_outbound_topic_alias_resolver_manual_vtable = { + .destroy_fn = s_aws_mqtt5_outbound_topic_alias_resolver_manual_destroy, + .reset_fn = s_aws_mqtt5_outbound_topic_alias_resolver_manual_reset, + .resolve_outbound_publish_fn = s_aws_mqtt5_outbound_topic_alias_resolver_manual_resolve_outbound_publish_fn, }; -static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_user_new( +static struct aws_mqtt5_outbound_topic_alias_resolver *s_aws_mqtt5_outbound_topic_alias_resolver_manual_new( struct aws_allocator *allocator) { - struct aws_mqtt5_outbound_topic_alias_resolver_user *resolver = - aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_outbound_topic_alias_resolver_user)); + struct aws_mqtt5_outbound_topic_alias_resolver_manual *resolver = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt5_outbound_topic_alias_resolver_manual)); resolver->base.allocator = allocator; - resolver->base.vtable = &s_aws_mqtt5_outbound_topic_alias_resolver_user_vtable; + resolver->base.vtable = &s_aws_mqtt5_outbound_topic_alias_resolver_manual_vtable; resolver->base.impl = resolver; aws_array_list_init_dynamic(&resolver->aliases, allocator, 0, sizeof(struct aws_string *)); diff --git a/source/v5/mqtt5_utils.c b/source/v5/mqtt5_utils.c index dd483f69..79dc686b 100644 --- a/source/v5/mqtt5_utils.c +++ b/source/v5/mqtt5_utils.c @@ -289,7 +289,7 @@ enum aws_mqtt5_client_session_behavior_type aws_mqtt5_client_session_behavior_ty const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { switch (aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default(outbound_aliasing_behavior)) { - case AWS_MQTT5_COTABT_USER: + case AWS_MQTT5_COTABT_MANUAL: return "User-controlled outbound topic aliasing behavior"; case AWS_MQTT5_COTABT_LRU: return "LRU caching outbound topic aliasing behavior"; @@ -301,6 +301,13 @@ const char *aws_mqtt5_outbound_topic_alias_behavior_type_to_c_string( } } +bool aws_mqtt5_outbound_topic_alias_behavior_type_validate( + enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { + + return outbound_aliasing_behavior >= AWS_MQTT5_COTABT_DEFAULT && + outbound_aliasing_behavior <= AWS_MQTT5_COTABT_DISABLED; +} + enum aws_mqtt5_client_outbound_topic_alias_behavior_type aws_mqtt5_outbound_topic_alias_behavior_type_to_non_default( enum aws_mqtt5_client_outbound_topic_alias_behavior_type outbound_aliasing_behavior) { if (outbound_aliasing_behavior == AWS_MQTT5_COTABT_DEFAULT) { @@ -322,6 +329,13 @@ const char *aws_mqtt5_inbound_topic_alias_behavior_type_to_c_string( } } +bool aws_mqtt5_inbound_topic_alias_behavior_type_validate( + enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior) { + + return inbound_aliasing_behavior >= AWS_MQTT5_CITABT_DEFAULT && + inbound_aliasing_behavior <= AWS_MQTT5_CITABT_DISABLED; +} + enum aws_mqtt5_client_inbound_topic_alias_behavior_type aws_mqtt5_inbound_topic_alias_behavior_type_to_non_default( enum aws_mqtt5_client_inbound_topic_alias_behavior_type inbound_aliasing_behavior) { if (inbound_aliasing_behavior == AWS_MQTT5_CITABT_DEFAULT) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 70e6c76a..3494aa2e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -131,10 +131,10 @@ add_test_case(mqtt5_inbound_topic_alias_resolve_failure) add_test_case(mqtt5_inbound_topic_alias_reset) add_test_case(mqtt5_outbound_topic_alias_disabled_resolve_success) add_test_case(mqtt5_outbound_topic_alias_disabled_resolve_failure) -add_test_case(mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias) -add_test_case(mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias) -add_test_case(mqtt5_outbound_topic_alias_user_resolve_success) -add_test_case(mqtt5_outbound_topic_alias_user_reset) +add_test_case(mqtt5_outbound_topic_alias_manual_resolve_failure_zero_alias) +add_test_case(mqtt5_outbound_topic_alias_manual_resolve_failure_too_big_alias) +add_test_case(mqtt5_outbound_topic_alias_manual_resolve_success) +add_test_case(mqtt5_outbound_topic_alias_manual_reset) add_test_case(mqtt5_outbound_topic_alias_lru_zero_size) # lru topic sequence tests @@ -247,7 +247,6 @@ add_test_case(mqtt5_client_options_validation_failure_invalid_keep_alive) add_test_case(mqtt5_operation_subscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_unsubscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_packet_size) -add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_topic_alias_maximum) add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_qos) add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_invalid_retain) add_test_case(mqtt5_operation_disconnect_connection_settings_validation_failure_exceeds_maximum_packet_size) @@ -365,12 +364,10 @@ add_test_case(mqtt5_client_inbound_alias_failure_disabled) add_test_case(mqtt5_client_inbound_alias_failure_zero_id) add_test_case(mqtt5_client_inbound_alias_failure_too_large_id) add_test_case(mqtt5_client_inbound_alias_failure_unbound_id) -add_test_case(mqtt5_client_outbound_alias_disabled_failure_alias_set) -add_test_case(mqtt5_client_outbound_alias_user_failure_empty_topic) -add_test_case(mqtt5_client_outbound_alias_lru_failure_alias_set) +add_test_case(mqtt5_client_outbound_alias_manual_failure_empty_topic) # a, b, c, r imply notation as the outbound resolver unit tests above -add_test_case(mqtt5_client_outbound_alias_user_success_a_b_ar_br) +add_test_case(mqtt5_client_outbound_alias_manual_success_a_b_ar_br) add_test_case(mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a) add_test_case(rate_limiter_token_bucket_init_invalid) diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index 911b1822..2d436f85 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -5818,7 +5818,7 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( .topic_alias = &topic_alias, }; - if (behavior_type == AWS_MQTT5_COTABT_USER) { + if (behavior_type == AWS_MQTT5_COTABT_MANUAL) { AWS_ZERO_STRUCT(packet_publish_view.topic); } @@ -5842,39 +5842,17 @@ static int s_do_mqtt5_client_outbound_alias_failure_test( return AWS_OP_SUCCESS; } -static int s_mqtt5_client_outbound_alias_disabled_failure_alias_set_fn(struct aws_allocator *allocator, void *ctx) { +static int s_mqtt5_client_outbound_alias_manual_failure_empty_topic_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; - ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_DISABLED)); + ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_MANUAL)); return AWS_OP_SUCCESS; } AWS_TEST_CASE( - mqtt5_client_outbound_alias_disabled_failure_alias_set, - s_mqtt5_client_outbound_alias_disabled_failure_alias_set_fn) - -static int s_mqtt5_client_outbound_alias_user_failure_empty_topic_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_USER)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE( - mqtt5_client_outbound_alias_user_failure_empty_topic, - s_mqtt5_client_outbound_alias_user_failure_empty_topic_fn) - -static int s_mqtt5_client_outbound_alias_lru_failure_alias_set_fn(struct aws_allocator *allocator, void *ctx) { - (void)ctx; - - ASSERT_SUCCESS(s_do_mqtt5_client_outbound_alias_failure_test(allocator, AWS_MQTT5_COTABT_LRU)); - - return AWS_OP_SUCCESS; -} - -AWS_TEST_CASE(mqtt5_client_outbound_alias_lru_failure_alias_set, s_mqtt5_client_outbound_alias_lru_failure_alias_set_fn) + mqtt5_client_outbound_alias_manual_failure_empty_topic, + s_mqtt5_client_outbound_alias_manual_failure_empty_topic_fn) struct outbound_alias_publish { struct aws_byte_cursor topic; @@ -6035,7 +6013,7 @@ AWS_STATIC_STRING_FROM_LITERAL(s_topic_a, "topic/a"); AWS_STATIC_STRING_FROM_LITERAL(s_topic_b, "b/topic"); AWS_STATIC_STRING_FROM_LITERAL(s_topic_c, "topic/c"); -static int s_mqtt5_client_outbound_alias_user_success_a_b_ar_br_fn(struct aws_allocator *allocator, void *ctx) { +static int s_mqtt5_client_outbound_alias_manual_success_a_b_ar_br_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; struct outbound_alias_publish test_publishes[] = { @@ -6046,14 +6024,14 @@ static int s_mqtt5_client_outbound_alias_user_success_a_b_ar_br_fn(struct aws_al }; ASSERT_SUCCESS(s_perform_outbound_alias_sequence_test( - allocator, AWS_MQTT5_COTABT_USER, test_publishes, AWS_ARRAY_SIZE(test_publishes))); + allocator, AWS_MQTT5_COTABT_MANUAL, test_publishes, AWS_ARRAY_SIZE(test_publishes))); return AWS_OP_SUCCESS; } AWS_TEST_CASE( - mqtt5_client_outbound_alias_user_success_a_b_ar_br, - s_mqtt5_client_outbound_alias_user_success_a_b_ar_br_fn) + mqtt5_client_outbound_alias_manual_success_a_b_ar_br, + s_mqtt5_client_outbound_alias_manual_success_a_b_ar_br_fn) static int s_mqtt5_client_outbound_alias_lru_success_a_b_c_br_cr_a_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c index 6ef34c9f..1bb2b814 100644 --- a/tests/v5/mqtt5_operation_validation_failure_tests.c +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -1271,39 +1271,6 @@ AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( s_packet_size_init_settings_success_fn, s_packet_size_init_settings_failure_fn) -static const uint16_t s_topic_alias = 5; - -static struct aws_mqtt5_packet_publish_view s_exceeds_topic_alias_maximum_publish_view = { - .topic = - { - .ptr = s_good_topic, - .len = AWS_ARRAY_SIZE(s_good_topic) - 1, - }, - .topic_alias = &s_topic_alias, -}; - -static struct aws_mqtt5_client_options_storage s_dummy_client_options; - -static void s_topic_alias_init_settings_success_fn(struct aws_mqtt5_client *dummy_client) { - AWS_ZERO_STRUCT(s_dummy_client_options); - s_dummy_client_options.topic_aliasing_options.outbound_topic_alias_behavior = AWS_MQTT5_COTABT_USER; - - dummy_client->config = &s_dummy_client_options; - dummy_client->negotiated_settings.maximum_packet_size_to_server = 100; - dummy_client->negotiated_settings.topic_alias_maximum_to_server = 10; -} - -static void s_topic_alias_init_settings_failure_fn(struct aws_mqtt5_client *dummy_client) { - dummy_client->negotiated_settings.topic_alias_maximum_to_server = 2; -} - -AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST3( - publish, - exceeds_topic_alias_maximum, - s_exceeds_topic_alias_maximum_publish_view, - s_topic_alias_init_settings_success_fn, - s_topic_alias_init_settings_failure_fn) - static struct aws_mqtt5_packet_publish_view s_exceeds_maximum_qos_publish_view = { .topic = { diff --git a/tests/v5/mqtt5_topic_alias_tests.c b/tests/v5/mqtt5_topic_alias_tests.c index 803fa708..52fe6dfd 100644 --- a/tests/v5/mqtt5_topic_alias_tests.c +++ b/tests/v5/mqtt5_topic_alias_tests.c @@ -177,11 +177,11 @@ AWS_TEST_CASE( mqtt5_outbound_topic_alias_disabled_resolve_failure, s_mqtt5_outbound_topic_alias_disabled_resolve_failure_fn) -static int s_mqtt5_outbound_topic_alias_user_resolve_success_fn(struct aws_allocator *allocator, void *ctx) { +static int s_mqtt5_outbound_topic_alias_manual_resolve_success_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; struct aws_mqtt5_outbound_topic_alias_resolver *resolver = - aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_MANUAL); ASSERT_NOT_NULL(resolver); aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); @@ -253,13 +253,15 @@ static int s_mqtt5_outbound_topic_alias_user_resolve_success_fn(struct aws_alloc return AWS_OP_SUCCESS; } -AWS_TEST_CASE(mqtt5_outbound_topic_alias_user_resolve_success, s_mqtt5_outbound_topic_alias_user_resolve_success_fn) +AWS_TEST_CASE(mqtt5_outbound_topic_alias_manual_resolve_success, s_mqtt5_outbound_topic_alias_manual_resolve_success_fn) -static int s_mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias_fn(struct aws_allocator *allocator, void *ctx) { +static int s_mqtt5_outbound_topic_alias_manual_resolve_failure_zero_alias_fn( + struct aws_allocator *allocator, + void *ctx) { (void)ctx; struct aws_mqtt5_outbound_topic_alias_resolver *resolver = - aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_MANUAL); ASSERT_NOT_NULL(resolver); aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); @@ -283,16 +285,16 @@ static int s_mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias_fn(struc } AWS_TEST_CASE( - mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias, - s_mqtt5_outbound_topic_alias_user_resolve_failure_zero_alias_fn) + mqtt5_outbound_topic_alias_manual_resolve_failure_zero_alias, + s_mqtt5_outbound_topic_alias_manual_resolve_failure_zero_alias_fn) -static int s_mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias_fn( +static int s_mqtt5_outbound_topic_alias_manual_resolve_failure_too_big_alias_fn( struct aws_allocator *allocator, void *ctx) { (void)ctx; struct aws_mqtt5_outbound_topic_alias_resolver *resolver = - aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_MANUAL); ASSERT_NOT_NULL(resolver); aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); @@ -316,14 +318,14 @@ static int s_mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias_fn( } AWS_TEST_CASE( - mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias, - s_mqtt5_outbound_topic_alias_user_resolve_failure_too_big_alias_fn) + mqtt5_outbound_topic_alias_manual_resolve_failure_too_big_alias, + s_mqtt5_outbound_topic_alias_manual_resolve_failure_too_big_alias_fn) -static int s_mqtt5_outbound_topic_alias_user_reset_fn(struct aws_allocator *allocator, void *ctx) { +static int s_mqtt5_outbound_topic_alias_manual_reset_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; struct aws_mqtt5_outbound_topic_alias_resolver *resolver = - aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_USER); + aws_mqtt5_outbound_topic_alias_resolver_new(allocator, AWS_MQTT5_COTABT_MANUAL); ASSERT_NOT_NULL(resolver); aws_mqtt5_outbound_topic_alias_resolver_reset(resolver, 5); @@ -367,7 +369,7 @@ static int s_mqtt5_outbound_topic_alias_user_reset_fn(struct aws_allocator *allo return AWS_OP_SUCCESS; } -AWS_TEST_CASE(mqtt5_outbound_topic_alias_user_reset, s_mqtt5_outbound_topic_alias_user_reset_fn) +AWS_TEST_CASE(mqtt5_outbound_topic_alias_manual_reset, s_mqtt5_outbound_topic_alias_manual_reset_fn) static int s_mqtt5_outbound_topic_alias_lru_zero_size_fn(struct aws_allocator *allocator, void *ctx) { (void)ctx; From 6d36cd3726233cb757468d0ea26f6cd8dad151ec Mon Sep 17 00:00:00 2001 From: Igor Abdrakhimov Date: Tue, 21 Nov 2023 14:27:12 -0800 Subject: [PATCH 95/98] Add mqtt error for subscribe failure (#335) --- include/aws/mqtt/mqtt.h | 1 + source/mqtt.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 9fc8001c..974c4576 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -80,6 +80,7 @@ enum aws_mqtt_error { AWS_ERROR_MQTT5_INVALID_UTF8_STRING, AWS_ERROR_MQTT_CONNECTION_RESET_FOR_ADAPTER_CONNECT, AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, + AWS_ERROR_MQTT_CONNECTION_SUBSCRIBE_FAILURE, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; diff --git a/source/mqtt.c b/source/mqtt.c index ac8c4529..c87ec0cc 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -230,6 +230,9 @@ bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter) AWS_DEFINE_ERROR_INFO_MQTT( AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, "Resubscribe was called when there were no subscriptions"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_CONNECTION_SUBSCRIBE_FAILURE, + "MQTT subscribe operation failed"), }; /* clang-format on */ #undef AWS_DEFINE_ERROR_INFO_MQTT From c91a118e522d336909ba7af2081ed3b2e5f2e461 Mon Sep 17 00:00:00 2001 From: Joseph Klix Date: Tue, 12 Dec 2023 10:20:55 -0800 Subject: [PATCH 96/98] Update README.md (#337) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 070aab53..2f99277e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## AWS C MQTT -C99 implementation of the MQTT 3.1.1 specification. +C99 implementation of the MQTT 3.1.1 and MQTT 5 specifications. ## License From eac4be396ec7499bd520724dcd91c4d5df3b729f Mon Sep 17 00:00:00 2001 From: Vera Xia Date: Wed, 13 Dec 2023 14:06:07 -0800 Subject: [PATCH 97/98] Mqtt5 General Availability (#339) --- include/aws/mqtt/v5/mqtt5_client.h | 8 -------- include/aws/mqtt/v5/mqtt5_packet_storage.h | 8 -------- include/aws/mqtt/v5/mqtt5_types.h | 8 -------- 3 files changed, 24 deletions(-) diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index b14d7790..2f92e124 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -6,14 +6,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -/** - * DEVELOPER PREVIEW DISCLAIMER - * - * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the - * preview window is especially valuable in shaping the final product. During the preview period we may make - * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. - */ - #include #include diff --git a/include/aws/mqtt/v5/mqtt5_packet_storage.h b/include/aws/mqtt/v5/mqtt5_packet_storage.h index 4b540926..354990c8 100644 --- a/include/aws/mqtt/v5/mqtt5_packet_storage.h +++ b/include/aws/mqtt/v5/mqtt5_packet_storage.h @@ -6,14 +6,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -/** - * DEVELOPER PREVIEW DISCLAIMER - * - * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the - * preview window is especially valuable in shaping the final product. During the preview period we may make - * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. - */ - #include #include diff --git a/include/aws/mqtt/v5/mqtt5_types.h b/include/aws/mqtt/v5/mqtt5_types.h index d374b9ba..b9ff8464 100644 --- a/include/aws/mqtt/v5/mqtt5_types.h +++ b/include/aws/mqtt/v5/mqtt5_types.h @@ -6,14 +6,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -/** - * DEVELOPER PREVIEW DISCLAIMER - * - * MQTT5 support is currently in **developer preview**. We encourage feedback at all times, but feedback during the - * preview window is especially valuable in shaping the final product. During the preview period we may make - * backwards-incompatible changes to the public API, but in general, this is something we will try our best to avoid. - */ - #include #include From 17ee24a2177fc64cf9773d430a24e6fa06a89dd0 Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Fri, 29 Dec 2023 16:49:29 -0800 Subject: [PATCH 98/98] Change `port` from uint16_t to uint32_t, to support VSOCK (#338) --- bin/elastipubsub/main.c | 2 +- bin/elastipubsub5/main.c | 2 +- bin/mqtt5canary/main.c | 4 ++-- include/aws/mqtt/client.h | 2 +- include/aws/mqtt/private/client_impl.h | 2 +- .../aws/mqtt/private/v5/mqtt5_options_storage.h | 2 +- include/aws/mqtt/v5/mqtt5_client.h | 2 +- source/v5/mqtt5_options_storage.c | 14 ++++++++++---- source/v5/mqtt5_to_mqtt3_adapter.c | 2 +- tests/CMakeLists.txt | 1 + .../v5/mqtt5_operation_validation_failure_tests.c | 7 +++++++ 11 files changed, 27 insertions(+), 13 deletions(-) diff --git a/bin/elastipubsub/main.c b/bin/elastipubsub/main.c index 04085169..1745fe9a 100644 --- a/bin/elastipubsub/main.c +++ b/bin/elastipubsub/main.c @@ -37,7 +37,7 @@ struct app_ctx { struct aws_mutex lock; struct aws_condition_variable signal; struct aws_uri uri; - uint16_t port; + uint32_t port; const char *cacert; const char *cert; const char *key; diff --git a/bin/elastipubsub5/main.c b/bin/elastipubsub5/main.c index f3ac686b..2c84e796 100644 --- a/bin/elastipubsub5/main.c +++ b/bin/elastipubsub5/main.c @@ -44,7 +44,7 @@ struct app_ctx { struct aws_mutex lock; struct aws_condition_variable signal; struct aws_uri uri; - uint16_t port; + uint32_t port; const char *cacert; const char *cert; const char *key; diff --git a/bin/mqtt5canary/main.c b/bin/mqtt5canary/main.c index cf7c50bc..4431d8f0 100644 --- a/bin/mqtt5canary/main.c +++ b/bin/mqtt5canary/main.c @@ -45,7 +45,7 @@ struct app_ctx { struct aws_mutex lock; struct aws_condition_variable signal; struct aws_uri uri; - uint16_t port; + uint32_t port; const char *cacert; const char *cert; const char *key; @@ -181,7 +181,7 @@ static void s_parse_options( ctx->use_websockets = true; break; case 'p': - ctx->port = (uint16_t)atoi(aws_cli_optarg); + ctx->port = (uint32_t)atoi(aws_cli_optarg); break; case 't': tester_options->elg_max_threads = (uint16_t)atoi(aws_cli_optarg); diff --git a/include/aws/mqtt/client.h b/include/aws/mqtt/client.h index de2934a7..8d85bfe8 100644 --- a/include/aws/mqtt/client.h +++ b/include/aws/mqtt/client.h @@ -254,7 +254,7 @@ struct aws_mqtt_topic_subscription { */ struct aws_mqtt_connection_options { struct aws_byte_cursor host_name; - uint16_t port; + uint32_t port; struct aws_socket_options *socket_options; struct aws_tls_connection_options *tls_options; struct aws_byte_cursor client_id; diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 4dc13cf4..1d0dd67a 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -199,7 +199,7 @@ struct aws_mqtt_client_connection_311_impl { /* The host information, changed by user when state is AWS_MQTT_CLIENT_STATE_DISCONNECTED */ struct aws_string *host_name; - uint16_t port; + uint32_t port; struct aws_tls_connection_options tls_options; struct aws_socket_options socket_options; struct aws_http_proxy_config *http_proxy_config; diff --git a/include/aws/mqtt/private/v5/mqtt5_options_storage.h b/include/aws/mqtt/private/v5/mqtt5_options_storage.h index 9fe14817..22e39e83 100644 --- a/include/aws/mqtt/private/v5/mqtt5_options_storage.h +++ b/include/aws/mqtt/private/v5/mqtt5_options_storage.h @@ -141,7 +141,7 @@ struct aws_mqtt5_client_options_storage { struct aws_allocator *allocator; struct aws_string *host_name; - uint16_t port; + uint32_t port; struct aws_client_bootstrap *bootstrap; struct aws_socket_options socket_options; diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index 2f92e124..d04d2981 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -526,7 +526,7 @@ struct aws_mqtt5_client_options { /** * Port to establish mqtt connections to */ - uint16_t port; + uint32_t port; /** * Client bootstrap to use whenever this client establishes a connection diff --git a/source/v5/mqtt5_options_storage.c b/source/v5/mqtt5_options_storage.c index 68a06a88..975e52e3 100644 --- a/source/v5/mqtt5_options_storage.c +++ b/source/v5/mqtt5_options_storage.c @@ -3356,14 +3356,20 @@ int aws_mqtt5_client_options_validate(const struct aws_mqtt5_client_options *opt } } + if (aws_socket_validate_port_for_connect( + options->port, options->socket_options ? options->socket_options->domain : AWS_SOCKET_IPV4)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid port in mqtt5 client configuration"); + return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); + } + if (options->http_proxy_options != NULL) { if (options->http_proxy_options->host.len == 0) { AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "proxy host name not set in mqtt5 client configuration"); return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); } - if (options->http_proxy_options->port == 0) { - AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "proxy port not set in mqtt5 client configuration"); + if (aws_socket_validate_port_for_connect(options->http_proxy_options->port, AWS_SOCKET_IPV4)) { + AWS_LOGF_ERROR(AWS_LS_MQTT5_GENERAL, "invalid proxy port in mqtt5 client configuration"); return aws_raise_error(AWS_ERROR_MQTT5_CLIENT_OPTIONS_VALIDATION); } } @@ -3542,7 +3548,7 @@ void aws_mqtt5_client_options_storage_log( log_handle, level, AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_client_options_storage port set to %" PRIu16, + "id=%p: aws_mqtt5_client_options_storage port set to %" PRIu32, (void *)options_storage, options_storage->port); @@ -3603,7 +3609,7 @@ void aws_mqtt5_client_options_storage_log( log_handle, level, AWS_LS_MQTT5_GENERAL, - "id=%p: aws_mqtt5_client_options_storage http proxy port set to %" PRIu16, + "id=%p: aws_mqtt5_client_options_storage http proxy port set to %" PRIu32, (void *)options_storage, options_storage->http_proxy_options.port); diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index e48ca3d3..129c0132 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -199,7 +199,7 @@ struct aws_mqtt_adapter_connect_task { struct aws_mqtt_client_connection_5_impl *adapter; struct aws_byte_buf host_name; - uint16_t port; + uint32_t port; struct aws_socket_options socket_options; struct aws_tls_connection_options *tls_options_ptr; struct aws_tls_connection_options tls_options; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3494aa2e..b8ef2587 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -244,6 +244,7 @@ add_test_case(mqtt5_client_options_validation_failure_no_publish_received) add_test_case(mqtt5_client_options_validation_failure_invalid_socket_options) add_test_case(mqtt5_client_options_validation_failure_invalid_connect) add_test_case(mqtt5_client_options_validation_failure_invalid_keep_alive) +add_test_case(mqtt5_client_options_validation_failure_invalid_port) add_test_case(mqtt5_operation_subscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_unsubscribe_connection_settings_validation_failure_exceeds_maximum_packet_size) add_test_case(mqtt5_operation_publish_connection_settings_validation_failure_exceeds_maximum_packet_size) diff --git a/tests/v5/mqtt5_operation_validation_failure_tests.c b/tests/v5/mqtt5_operation_validation_failure_tests.c index 1bb2b814..80c0f978 100644 --- a/tests/v5/mqtt5_operation_validation_failure_tests.c +++ b/tests/v5/mqtt5_operation_validation_failure_tests.c @@ -1113,6 +1113,7 @@ static struct aws_mqtt5_client_options s_good_client_options = { .ptr = s_server_reference, .len = AWS_ARRAY_SIZE(s_server_reference) - 1, }, + .port = 1883, .socket_options = &s_good_socket_options, .connect_options = &s_good_connect, .ping_timeout_ms = 5000, @@ -1183,6 +1184,12 @@ AWS_CLIENT_CREATION_VALIDATION_FAILURE( s_good_client_options, s_make_invalid_keep_alive_client_options) +static void s_make_invalid_port_client_options(struct aws_mqtt5_client_options *options) { + options->port = 0xFFFFFFFF; +} + +AWS_CLIENT_CREATION_VALIDATION_FAILURE(invalid_port, s_good_client_options, s_make_invalid_port_client_options) + #define AWS_CONNECTION_SETTINGS_VALIDATION_FAILURE_TEST_PREFIX(packet_type, failure_reason, init_success_settings_fn) \ static int s_mqtt5_operation_##packet_type##_connection_settings_validation_failure_##failure_reason##_fn( \ struct aws_allocator *allocator, void *ctx) { \