From d57b8c5b297c6223a285e390df281cf1eb20043f Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 11 Dec 2024 19:21:46 +0100 Subject: [PATCH 01/57] feat(mosq): Add example with two brokers synced on P2P Broker-less two chip example which virtual private IoT networks on MQTT protocol. --- .github/workflows/mosq__build.yml | 10 +- ci/check_copyright_ignore.txt | 1 + components/mosquitto/.build-test-rules.yml | 3 + .../examples/serverless_mqtt/CMakeLists.txt | 6 + .../examples/serverless_mqtt/README.md | 53 +++ .../components/libjuice/CMakeLists.txt | 44 +++ .../components/libjuice/include/ifaddrs.h | 13 + .../components/libjuice/port/juice_random.c | 40 ++ .../serverless_mqtt/main/CMakeLists.txt | 4 + .../serverless_mqtt/main/Kconfig.projbuild | 85 ++++ .../serverless_mqtt/main/idf_component.yml | 5 + .../serverless_mqtt/main/serverless_mqtt.c | 374 ++++++++++++++++++ .../serverless_mqtt/main/wifi_connect.c | 122 ++++++ .../serverless_mqtt/sdkconfig.defaults | 3 + .../examples/serverless_mqtt/serverless.png | Bin 0 -> 86433 bytes 15 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 components/mosquitto/.build-test-rules.yml create mode 100644 components/mosquitto/examples/serverless_mqtt/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/README.md create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h create mode 100644 components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c create mode 100644 components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt create mode 100644 components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild create mode 100644 components/mosquitto/examples/serverless_mqtt/main/idf_component.yml create mode 100644 components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c create mode 100644 components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c create mode 100644 components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults create mode 100644 components/mosquitto/examples/serverless_mqtt/serverless.png diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index abf9dbb4bc..27ff2581c1 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -17,7 +17,8 @@ jobs: runs-on: ubuntu-22.04 container: espressif/idf:${{ matrix.idf_ver }} env: - TEST_DIR: components/mosquitto/examples/broker + TEST_DIR: components/mosquitto/examples + TARGET_TEST: broker TARGET_TEST_DIR: build_esp32_default steps: - name: Checkout esp-protocols @@ -29,14 +30,15 @@ jobs: run: | . ${IDF_PATH}/export.sh pip install idf-component-manager idf-build-apps --upgrade - python ci/build_apps.py ${TEST_DIR} - cd ${TEST_DIR} + python ci/build_apps.py -c ${TEST_DIR} -m components/mosquitto/.build-test-rules.yml + # upload only the target test artifacts + cd ${TEST_DIR}/${TARGET_TEST} ${GITHUB_WORKSPACE}/ci/clean_build_artifacts.sh `pwd`/${TARGET_TEST_DIR} zip -qur artifacts.zip ${TARGET_TEST_DIR} - uses: actions/upload-artifact@v4 with: name: mosq_target_esp32_${{ matrix.idf_ver }} - path: ${{ env.TEST_DIR }}/artifacts.zip + path: ${{ env.TEST_DIR }}/${{ env.TARGET_TEST }}/artifacts.zip if-no-files-found: error test_mosq: diff --git a/ci/check_copyright_ignore.txt b/ci/check_copyright_ignore.txt index e69de29bb2..1cd8798f5b 100644 --- a/ci/check_copyright_ignore.txt +++ b/ci/check_copyright_ignore.txt @@ -0,0 +1 @@ +components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c diff --git a/components/mosquitto/.build-test-rules.yml b/components/mosquitto/.build-test-rules.yml new file mode 100644 index 0000000000..e1f5846485 --- /dev/null +++ b/components/mosquitto/.build-test-rules.yml @@ -0,0 +1,3 @@ +components/mosquitto/examples/serverless_mqtt: + disable: + - if: IDF_TARGET not in ["esp32", "esp32s3", "esp32c3"] diff --git a/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt new file mode 100644 index 0000000000..c9935c9567 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(serverless_mqtt) diff --git a/components/mosquitto/examples/serverless_mqtt/README.md b/components/mosquitto/examples/serverless_mqtt/README.md new file mode 100644 index 0000000000..5ee8369e67 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/README.md @@ -0,0 +1,53 @@ +# Brokerless MQTT Example + +MQTT served by (two) mosquitto's running on two ESP chips. + +* Leverages MQTT connectivity between two private networks without cloud premisses. +* Creates two local MQTT servers (on ESP32x's) which are being synchronized over peer to peer connection (established via ICE protocol, by [libjuice](https://github.com/paullouisageneau/libjuice)). + +## How it works + +This example needs two ESP32 chipsets, that will create two separate Wi-Fi networks (IoT networks) used for IoT devices. +Each IoT network is served by an MQTT server (using mosquitto component). +This example will also synchronize these two MQTT brokers, as if there was only one IoT network with one broker. +This example creates a peer to peer connection between two chipsets to keep them synchronize. This connection utilizes libjuice (which implements a simplified ICE-UDP) to traverse NATs, which enabling direct connection between two private networks behind NATs. + +* Diagram + +![demo](serverless.png) + +Here's a step-by-step procedure of establishing this remote connection: +1) Initialize and start Wi-Fi AP (for IoT networks) and Wi-Fi station (for internet connection) +2) Start mosquitto broker on IoT network +3) Start libjuice to gather connection candidates +4) Synchronize using a public MQTT broker and exchange ICE descriptors +5) Establish ICE UDP connection between the two ESP32 chipsets +6) Start forwarding mqtt messages + - Each remote datagram (received from ICE-UDP channel) is re-published to the local MQTT server + - Each local MQTT message (received from mosquitto on_message callback) is sent in ICE-UDP datagram + +## How to use this example + +You need two ESP32 devices that support Wi-Fi station and Wi-Fi software access point. + +* Configure Wi-Fi credentials for both devices on both interfaces + * These devices would be deployed in distinct Wi-Fi environments, so the Wi-Fi station credentials would likely be different. + * They also create their own IoT network (on the soft-AP interface) Wi-Fi, so the AP credentials would likely be the same, suggesting the IoT networks will be keep synchronized (even though these are two distict Wi-Fi networks). +* Choose `CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1` for one device and `CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER2` for another. It's not important which device is PEER1, since the code is symmetric, but these two devices need to have different role. +* Optionally: You can use `idf.py` `-D` and `-B` flag to keep separate build directories and sdkconfigs for these two roles +``` +idf.py -B build1 -DSDKCONFIG=build1/sdkconfig menuconfig build flash monitor +``` +* Flash and run the two devices and wait for them to connect and synchronize. +* Now you can test MQTT connectivity, for example: + * Join PEER1 device's AP and connect to the MQTT broker with one or more clients, subscribing to one or more topics. + * Join PEER2 device's AP and connect to the MQTT broker with one or more clients, subscribing to one or more topics. + * Whenever you publish to a topic, all subscribed clients should receive the message, no matter which Wi-Fi network they're connected to. + +## Warning + +This example uses libjuice as a dependency: + +* libjuice (UDP Interactive Connectivity Establishment): https://github.com/paullouisageneau/libjuice + +which is distributed under Mozilla Public License v2.0. diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt new file mode 100644 index 0000000000..f7cf012d1b --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/CMakeLists.txt @@ -0,0 +1,44 @@ +set(LIBJUICE_VERSION "73785387eafe15c02b6a210edb10f722474e8e14") +set(LIBJUICE_URL "https://github.com/paullouisageneau/libjuice/archive/${LIBJUICE_VERSION}.zip") + +set(libjuice_dir ${CMAKE_BINARY_DIR}/libjuice/libjuice-${LIBJUICE_VERSION}) + +# Fetch the library +if(NOT EXISTS ${libjuice_dir}) + message(STATUS "Downloading libjuice ${LIBJUICE_VERSION}...") + file(DOWNLOAD ${LIBJUICE_URL} ${CMAKE_BINARY_DIR}/libjuice.zip SHOW_PROGRESS) + execute_process(COMMAND unzip -o ${CMAKE_BINARY_DIR}/libjuice.zip -d ${CMAKE_BINARY_DIR}/libjuice + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +endif() + +set(JUICE_SOURCES ${libjuice_dir}/src/addr.c + ${libjuice_dir}/src/agent.c + ${libjuice_dir}/src/base64.c + ${libjuice_dir}/src/conn.c + ${libjuice_dir}/src/conn_mux.c + ${libjuice_dir}/src/conn_poll.c + ${libjuice_dir}/src/conn_thread.c + ${libjuice_dir}/src/const_time.c + ${libjuice_dir}/src/crc32.c + ${libjuice_dir}/src/hash.c + ${libjuice_dir}/src/ice.c + ${libjuice_dir}/src/juice.c + ${libjuice_dir}/src/log.c + ${libjuice_dir}/src/server.c + ${libjuice_dir}/src/stun.c + ${libjuice_dir}/src/timestamp.c + ${libjuice_dir}/src/turn.c + ${libjuice_dir}/src/udp.c +# Use hmac from mbedtls and random numbers from esp_random: +# ${libjuice_dir}/src/hmac.c +# ${libjuice_dir}/src/random.c + ) + +idf_component_register(SRCS port/juice_random.c + ${JUICE_SOURCES} + INCLUDE_DIRS "include" "${libjuice_dir}/include" "${libjuice_dir}/include/juice" + REQUIRES esp_netif + PRIV_REQUIRES sock_utils) + +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") +set_source_files_properties(${libjuice_dir}/src/udp.c PROPERTIES COMPILE_FLAGS -Wno-unused-variable) diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h b/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h new file mode 100644 index 0000000000..ba92bc72b8 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/include/ifaddrs.h @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +// Purpose of this header is to replace udp_sendto() to avoid name conflict with lwip +// added here since ifaddrs.h is included from juice_udp sources +#define udp_sendto juice_udp_sendto + +// other than that, let's just include the ifaddrs (from sock_utils) +#include_next "ifaddrs.h" diff --git a/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c b/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c new file mode 100644 index 0000000000..89c1c6bdaa --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/components/libjuice/port/juice_random.c @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +#include "esp_random.h" + +void juice_random(void *buf, size_t size) +{ + esp_fill_random(buf, size); +} + +void juice_random_str64(char *buf, size_t size) +{ + static const char chars64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i = 0; + for (i = 0; i + 1 < size; ++i) { + uint8_t byte = 0; + juice_random(&byte, 1); + buf[i] = chars64[byte & 0x3F]; + } + buf[i] = '\0'; +} + +uint32_t juice_rand32(void) +{ + uint32_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} + +uint64_t juice_rand64(void) +{ + uint64_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} diff --git a/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt b/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt new file mode 100644 index 0000000000..b757b72863 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "serverless_mqtt.c" + "wifi_connect.c" + INCLUDE_DIRS "." + REQUIRES libjuice nvs_flash mqtt json esp_wifi) diff --git a/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild b/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild new file mode 100644 index 0000000000..7e0e97de03 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/Kconfig.projbuild @@ -0,0 +1,85 @@ +menu "Example Configuration" + + menu "AP Configuration" + comment "AP Configuration" + + config EXAMPLE_AP_SSID + string "Wi-Fi SSID" + default "myssid" + help + Set the SSID of Wi-Fi ap interface. + + config EXAMPLE_AP_PASSWORD + string "Wi-Fi Password" + default "12345678" + help + Set the password of Wi-Fi ap interface. + + endmenu + + menu "STA Configuration" + comment "STA Configuration" + + config EXAMPLE_STA_SSID + string "WiFi Station SSID" + default "mystationssid" + help + SSID for the example's sta to connect to. + + config EXAMPLE_STA_PASSWORD + string "WiFi Station Password" + default "mystationpassword" + help + WiFi station password for the example to use. + endmenu + + config EXAMPLE_MQTT_BROKER_URI + string "MQTT Broker URL" + default "mqtt://mqtt.eclipseprojects.io" + help + URL of the mqtt broker use for synchronisation and exchanging + ICE connect info (description and candidates). + + config EXAMPLE_MQTT_SYNC_TOPIC + string "MQTT topic for synchronisation" + default "/topic/serverless_mqtt" + help + MQTT topic used fo synchronisation. + + config EXAMPLE_STUN_SERVER + string "Hostname of STUN server" + default "stun.l.google.com" + help + STUN server hostname. + + config EXAMPLE_MQTT_CLIENT_STACK_SIZE + int "Stack size for mqtt client" + default 16384 + help + Set stack size for the mqtt client. + Need more stack, since calling juice API from the handler. + + config EXAMPLE_MQTT_BROKER_PORT + int "port for the mosquitto to listen to" + default 1883 + help + This is a port which the local mosquitto uses. + + choice EXAMPLE_SERVERLESS_ROLE + prompt "Choose your role" + default EXAMPLE_SERVERLESS_ROLE_PEER1 + help + Choose either peer1 or peer2. + It's not very important which device is peer1 + (peer-1 sends sync messages, peer2 listens for them) + It is important that we have two peers, + one with peer1 config, another one with peer2 config + + config EXAMPLE_SERVERLESS_ROLE_PEER1 + bool "peer1" + + config EXAMPLE_SERVERLESS_ROLE_PEER2 + bool "peer2" + endchoice + +endmenu diff --git a/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml b/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml new file mode 100644 index 0000000000..e3297d5351 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mosquitto: + override_path: ../../.. + espressif/sock_utils: "*" diff --git a/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c b/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c new file mode 100644 index 0000000000..8dd0bee7c7 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/serverless_mqtt.c @@ -0,0 +1,374 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "mqtt_client.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_random.h" +#include "esp_check.h" +#include "esp_sleep.h" +#include "mosq_broker.h" +#include "juice/juice.h" +#include "cJSON.h" + +#if defined(CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1) +#define OUR_PEER "1" +#define THEIR_PEER "2" +#elif defined(CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER2) +#define OUR_PEER "2" +#define THEIR_PEER "1" +#endif + +#define PEER_SYNC0 BIT(0) +#define PEER_SYNC1 BIT(1) +#define PEER_SYNC2 BIT(2) +#define PEER_FAIL BIT(3) +#define PEER_GATHER_DONE BIT(4) +#define PEER_DESC_PUBLISHED BIT(5) +#define PEER_CONNECTED BIT(6) + +#define SYNC_BITS (PEER_SYNC1 | PEER_SYNC2 | PEER_FAIL) + +#define PUBLISH_SYNC_TOPIC CONFIG_EXAMPLE_MQTT_SYNC_TOPIC OUR_PEER +#define SUBSCRIBE_SYNC_TOPIC CONFIG_EXAMPLE_MQTT_SYNC_TOPIC THEIR_PEER +#define MAX_BUFFER_SIZE JUICE_MAX_SDP_STRING_LEN + +typedef struct message_wrap { + uint16_t topic_len; + uint16_t data_len; + char data[]; +} __attribute__((packed)) message_wrap_t; + +static const char *TAG = "serverless_mqtt" OUR_PEER; +static char s_buffer[MAX_BUFFER_SIZE]; +static EventGroupHandle_t s_state = NULL; +static juice_agent_t *s_agent = NULL; +static cJSON *s_peer_desc_json = NULL; +static char *s_peer_desc = NULL; +static esp_mqtt_client_handle_t s_local_mqtt = NULL; + +char *wifi_get_ipv4(wifi_interface_t interface); +esp_err_t wifi_connect(void); +static esp_err_t sync_peers(void); +static esp_err_t create_candidates(void); +static esp_err_t create_local_client(void); +static esp_err_t create_local_broker(void); + +void app_main(void) +{ + __attribute__((__unused__)) esp_err_t ret; + ESP_GOTO_ON_ERROR(wifi_connect(), err, TAG, "Failed to initialize WiFi"); + ESP_GOTO_ON_ERROR(create_local_broker(), err, TAG, "Failed to create local broker"); + ESP_GOTO_ON_ERROR(create_candidates(), err, TAG, "Failed to create juice candidates"); + ESP_GOTO_ON_ERROR(sync_peers(), err, TAG, "Failed to sync with the other peer"); + EventBits_t bits = xEventGroupWaitBits(s_state, PEER_FAIL | PEER_CONNECTED, pdFALSE, pdFALSE, pdMS_TO_TICKS(90000)); + if (bits & PEER_CONNECTED) { + ESP_LOGI(TAG, "Peer is connected!"); + ESP_GOTO_ON_ERROR(create_local_client(), err, TAG, "Failed to create forwarding mqtt client"); + ESP_LOGI(TAG, "Everything is ready, exiting main task"); + return; + } +err: + ESP_LOGE(TAG, "Non recoverable error, going to sleep for some time (random, max 20s)"); + esp_deep_sleep(1000000LL * (esp_random() % 20)); +} + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + if (esp_mqtt_client_subscribe(client, SUBSCRIBE_SYNC_TOPIC, 1) < 0) { + ESP_LOGE(TAG, "Failed to subscribe to the sync topic"); + } + xEventGroupSetBits(s_state, PEER_SYNC0); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + xEventGroupSetBits(s_state, PEER_FAIL); + break; + + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + if (s_state == NULL || memcmp(event->topic, SUBSCRIBE_SYNC_TOPIC, event->topic_len) != 0) { + break; + } + EventBits_t bits = xEventGroupGetBits(s_state); + if (event->data_len > 1 && s_agent) { + cJSON *root = cJSON_Parse(event->data); + if (root == NULL) { + break; + } + cJSON *desc = cJSON_GetObjectItem(root, "desc"); + if (desc == NULL) { + cJSON_Delete(root); + break; + } + printf("desc->valuestring:%s\n", desc->valuestring); + juice_set_remote_description(s_agent, desc->valuestring); + char cand_name[] = "cand0"; + while (true) { + cJSON *cand = cJSON_GetObjectItem(root, cand_name); + if (cand == NULL) { + break; + } + printf("%s: cand->valuestring:%s\n", cand_name, cand->valuestring); + juice_add_remote_candidate(s_agent, cand->valuestring); + cand_name[4]++; + } + cJSON_Delete(root); + xEventGroupSetBits(s_state, PEER_DESC_PUBLISHED); // this will complete the sync process + // and destroy the mqtt client + } +#ifdef CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1 + if (event->data_len == 1 && event->data[0] == '1' && (bits & PEER_SYNC2) == 0) { + if (esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "2", 1, 1, 0) >= 0) { + xEventGroupSetBits(s_state, PEER_SYNC2); + } else { + xEventGroupSetBits(s_state, PEER_FAIL); + } + } +#else + if (event->data_len == 1 && event->data[0] == '0' && (bits & PEER_SYNC1) == 0) { + if (esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "1", 1, 1, 0) >= 0) { + xEventGroupSetBits(s_state, PEER_SYNC1); + } else { + xEventGroupSetBits(s_state, PEER_FAIL); + } + } else if (event->data_len == 1 && event->data[0] == '2' && (bits & PEER_SYNC2) == 0) { + xEventGroupSetBits(s_state, PEER_SYNC2); + } +#endif + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + xEventGroupSetBits(s_state, PEER_FAIL); + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +static esp_err_t sync_peers(void) +{ + esp_err_t ret = ESP_OK; + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = CONFIG_EXAMPLE_MQTT_BROKER_URI, + .task.stack_size = CONFIG_EXAMPLE_MQTT_CLIENT_STACK_SIZE, + }; + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + ESP_GOTO_ON_FALSE(client, ESP_ERR_NO_MEM, err, TAG, "Failed to create mqtt client"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL), + err, TAG, "Failed to register mqtt event handler"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_start(client), err, TAG, "Failed to start mqtt client"); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_SYNC0, pdTRUE, pdTRUE, pdMS_TO_TICKS(10000)), + ESP_FAIL, err, TAG, "Failed to connect to the sync broker"); + ESP_LOGI(TAG, "Waiting for the other peer..."); + const int max_sync_retry = 60; + int retry = 0; + while (true) { + EventBits_t bits = xEventGroupWaitBits(s_state, SYNC_BITS, pdTRUE, pdFALSE, pdMS_TO_TICKS(1000)); + if (bits & PEER_SYNC2) { + break; + } + if (bits & PEER_SYNC1) { + continue; + } + ESP_GOTO_ON_FALSE((bits & PEER_FAIL) == 0, ESP_FAIL, err, TAG, "Failed to sync with the other peer"); + ESP_GOTO_ON_FALSE(retry++ < max_sync_retry, ESP_FAIL, err, TAG, "Failed to sync after %d seconds", retry); +#ifdef CONFIG_EXAMPLE_SERVERLESS_ROLE_PEER1 + ESP_RETURN_ON_FALSE(esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, "0", 1, 1, 0) >= 0, + ESP_FAIL, TAG, "Failed to publish mqtt message"); +#endif + } + ESP_LOGI(TAG, "Sync done"); + ESP_RETURN_ON_FALSE(esp_mqtt_client_publish(client, PUBLISH_SYNC_TOPIC, s_peer_desc, 0, 1, 0) >= 0, + ESP_FAIL, TAG, "Failed to publish peer's description"); + ESP_LOGI(TAG, "Waiting for the other peer description and candidates..."); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_DESC_PUBLISHED, pdTRUE, pdTRUE, pdMS_TO_TICKS(10000)), + ESP_FAIL, err, TAG, "Timeout in waiting for the other peer candidates"); +err: + free(s_peer_desc); + esp_mqtt_client_destroy(client); + return ret; +} + +static void juice_state(juice_agent_t *agent, juice_state_t state, void *user_ptr) +{ + ESP_LOGI(TAG, "JUICE state change: %s", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + xEventGroupSetBits(s_state, PEER_CONNECTED); + } else if (state == JUICE_STATE_FAILED || state == JUICE_STATE_DISCONNECTED) { + esp_restart(); + } +} + +static void juice_candidate(juice_agent_t *agent, const char *sdp, void *user_ptr) +{ + static uint8_t cand_nr = 0; + if (s_peer_desc_json && cand_nr < 10) { // supporting only 10 candidates + char cand_name[] = "cand0"; + cand_name[4] += cand_nr++; + cJSON_AddStringToObject(s_peer_desc_json, cand_name, sdp); + } +} + +static void juice_gathering_done(juice_agent_t *agent, void *user_ptr) +{ + ESP_LOGI(TAG, "Gathering done"); + if (s_state) { + xEventGroupSetBits(s_state, PEER_GATHER_DONE); + } +} + +#define ALIGN(size) (((size) + 3U) & ~(3U)) + +static void juice_recv(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) +{ + if (s_local_mqtt) { + message_wrap_t *message = (message_wrap_t *)data; + int topic_len = message->topic_len; + int payload_len = message->data_len; + int topic_len_aligned = ALIGN(topic_len); + char *topic = message->data; + char *payload = message->data + topic_len_aligned; + if (topic_len + topic_len_aligned + 4 > size) { + ESP_LOGE(TAG, "Received invalid message"); + return; + } + ESP_LOGI(TAG, "forwarding remote message: topic:%s", topic); + ESP_LOGI(TAG, "forwarding remote message: payload:%.*s", payload_len, payload); + esp_mqtt_client_publish(s_local_mqtt, topic, payload, payload_len, 0, 0); + } +} + +static esp_err_t create_candidates(void) +{ + ESP_RETURN_ON_FALSE(s_state = xEventGroupCreate(), ESP_ERR_NO_MEM, TAG, "Failed to create state event group"); + s_peer_desc_json = cJSON_CreateObject(); + esp_err_t ret = ESP_OK; + juice_set_log_level(JUICE_LOG_LEVEL_INFO); + juice_config_t config = { .stun_server_host = CONFIG_EXAMPLE_STUN_SERVER, + .bind_address = wifi_get_ipv4(WIFI_IF_STA), + .stun_server_port = 19302, + .cb_state_changed = juice_state, + .cb_candidate = juice_candidate, + .cb_gathering_done = juice_gathering_done, + .cb_recv = juice_recv, + }; + + s_agent = juice_create(&config); + ESP_RETURN_ON_FALSE(s_agent, ESP_FAIL, TAG, "Failed to create juice agent"); + ESP_GOTO_ON_FALSE(juice_get_local_description(s_agent, s_buffer, MAX_BUFFER_SIZE) == JUICE_ERR_SUCCESS, + ESP_FAIL, err, TAG, "Failed to get local description"); + ESP_LOGI(TAG, "desc: %s", s_buffer); + cJSON_AddStringToObject(s_peer_desc_json, "desc", s_buffer); + + ESP_GOTO_ON_FALSE(juice_gather_candidates(s_agent) == JUICE_ERR_SUCCESS, + ESP_FAIL, err, TAG, "Failed to start gathering candidates"); + ESP_GOTO_ON_FALSE(xEventGroupWaitBits(s_state, PEER_GATHER_DONE, pdTRUE, pdTRUE, pdMS_TO_TICKS(30000)), + ESP_FAIL, err, TAG, "Failed to connect to the sync broker"); + s_peer_desc = cJSON_Print(s_peer_desc_json); + ESP_LOGI(TAG, "desc: %s", s_peer_desc); + cJSON_Delete(s_peer_desc_json); + return ESP_OK; + +err: + juice_destroy(s_agent); + s_agent = NULL; + cJSON_Delete(s_peer_desc_json); + s_peer_desc_json = NULL; + return ret; +} + +static void local_handler(void *args, esp_event_base_t base, int32_t id, void *data) +{ + switch (id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "local client connected"); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "local client disconnected"); + break; + + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "local client error"); + break; + default: + ESP_LOGI(TAG, "local client event id:%d", (int)id); + break; + } +} + +static esp_err_t create_local_client(void) +{ + esp_err_t ret = ESP_OK; + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.transport = MQTT_TRANSPORT_OVER_TCP, + .broker.address.hostname = wifi_get_ipv4(WIFI_IF_AP), + .broker.address.port = CONFIG_EXAMPLE_MQTT_BROKER_PORT, + .task.stack_size = CONFIG_EXAMPLE_MQTT_CLIENT_STACK_SIZE, + .credentials.client_id = "local_mqtt" + }; + s_local_mqtt = esp_mqtt_client_init(&mqtt_cfg); + ESP_GOTO_ON_FALSE(s_local_mqtt, ESP_ERR_NO_MEM, err, TAG, "Failed to create mqtt client"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_register_event(s_local_mqtt, ESP_EVENT_ANY_ID, local_handler, NULL), + err, TAG, "Failed to register mqtt event handler"); + ESP_GOTO_ON_ERROR(esp_mqtt_client_start(s_local_mqtt), err, TAG, "Failed to start mqtt client"); + + return ESP_OK; +err: + esp_mqtt_client_destroy(s_local_mqtt); + s_local_mqtt = NULL; + return ret; +} + +static void handle_message(char *client, char *topic, char *payload, int len, int qos, int retain) +{ + if (client && strcmp(client, "local_mqtt") == 0 ) { + // This is our little local client -- do not forward + return; + } + ESP_LOGI(TAG, "handle_message topic:%s", topic); + ESP_LOGI(TAG, "handle_message data:%.*s", len, payload); + ESP_LOGI(TAG, "handle_message qos=%d, retain=%d", qos, retain); + if (s_local_mqtt && s_agent) { + int topic_len = strlen(topic) + 1; // null term + int topic_len_aligned = ALIGN(topic_len); + int total_msg_len = 2 + 2 /* msg_wrap header */ + topic_len_aligned + len; + if (total_msg_len > MAX_BUFFER_SIZE) { + ESP_LOGE(TAG, "Fail to forward, message too long"); + return; + } + message_wrap_t *message = (message_wrap_t *)s_buffer; + message->topic_len = topic_len; + message->data_len = len; + + memcpy(s_buffer + 4, topic, topic_len); + memcpy(s_buffer + 4 + topic_len_aligned, payload, len); + juice_send(s_agent, s_buffer, total_msg_len); + } +} + +static void broker_task(void *ctx) +{ + struct mosq_broker_config config = { .host = wifi_get_ipv4(WIFI_IF_AP), .port = CONFIG_EXAMPLE_MQTT_BROKER_PORT, .handle_message_cb = handle_message }; + mosq_broker_run(&config); + vTaskDelete(NULL); +} + +static esp_err_t create_local_broker(void) +{ + return xTaskCreate(broker_task, "mqtt_broker_task", 1024 * 32, NULL, 5, NULL) == pdTRUE ? + ESP_OK : ESP_FAIL; +} diff --git a/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c b/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c new file mode 100644 index 0000000000..aeb3af7599 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/main/wifi_connect.c @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_check.h" +#include "esp_wifi.h" +#include "esp_mac.h" + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static const char *TAG = "serverless_wifi"; +static EventGroupHandle_t s_wifi_events; +static int s_retry_num = 0; +static const int s_max_retry = 30; + +static void wifi_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *) event_data; + ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *) event_data; + ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < s_max_retry) { + esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "retry to connect to the AP"); + } else { + xEventGroupSetBits(s_wifi_events, WIFI_FAIL_BIT); + } + ESP_LOGI(TAG, "Connect to the AP fail"); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_events, WIFI_CONNECTED_BIT); + } +} + +esp_err_t wifi_connect(void) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(s_wifi_events = xEventGroupCreate(), ESP_ERR_NO_MEM, err, TAG, "Failed to create wifi_events"); + ESP_GOTO_ON_ERROR(nvs_flash_init(), err, TAG, "Failed to init nvs flash"); + ESP_GOTO_ON_ERROR(esp_netif_init(), err, TAG, "Failed to init esp_netif"); + ESP_GOTO_ON_ERROR(esp_event_loop_create_default(), err, TAG, "Failed to create default event loop"); + ESP_GOTO_ON_ERROR(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL), + err, TAG, "Failed to register WiFi event handler"); + ESP_GOTO_ON_ERROR(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL), + err, TAG, "Failed to register IP event handler"); + + // Initialize WiFi + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_GOTO_ON_ERROR(esp_wifi_init(&cfg), err, TAG, "Failed to initialize WiFi"); + ESP_GOTO_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_APSTA), err, TAG, "Failed to set STA+AP mode"); + + // Initialize AP + esp_netif_t *ap = esp_netif_create_default_wifi_ap(); + ESP_GOTO_ON_FALSE(ap, ESP_FAIL, err, TAG, "Failed to create AP network interface"); + wifi_config_t wifi_ap_config = { + .ap = { + .ssid = CONFIG_EXAMPLE_AP_SSID, + .password = CONFIG_EXAMPLE_AP_PASSWORD, + .authmode = WIFI_AUTH_WPA2_PSK, + .max_connection = 4, + }, + }; + ESP_GOTO_ON_ERROR(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config), err, TAG, "Failed to set AP config"); + + + // Initialize STA + esp_netif_t *sta = esp_netif_create_default_wifi_sta(); + ESP_GOTO_ON_FALSE(sta, ESP_FAIL, err, TAG, "Failed to create WiFi station network interface"); + wifi_config_t wifi_sta_config = { + .sta = { + .ssid = CONFIG_EXAMPLE_STA_SSID, + .password = CONFIG_EXAMPLE_STA_PASSWORD, + }, + }; + ESP_GOTO_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config), err, TAG, "Failed to set STA config"); + + // Start WiFi + ESP_GOTO_ON_ERROR(esp_wifi_start(), err, TAG, "Failed to start WiFi"); + + // Wait for connection + EventBits_t bits = xEventGroupWaitBits(s_wifi_events, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, pdMS_TO_TICKS(30000)); + ESP_GOTO_ON_FALSE((bits & WIFI_CONNECTED_BIT) == WIFI_CONNECTED_BIT, ESP_FAIL, err, + TAG, "Failed to obtain IP address from WiFi station"); + return ESP_OK; +err: + esp_wifi_stop(); + esp_wifi_deinit(); + nvs_flash_deinit(); + esp_netif_deinit(); + esp_event_loop_delete_default(); + return ret; + +} + +_Thread_local char s_ipv4_addr[4 * 4]; // 4 octets + '.'/term + +char *wifi_get_ipv4(wifi_interface_t interface) +{ + esp_netif_t *netif = esp_netif_get_handle_from_ifkey(interface == WIFI_IF_AP ? "WIFI_AP_DEF" : "WIFI_STA_DEF"); + ESP_RETURN_ON_FALSE(netif, NULL, TAG, "Failed to find default Wi-Fi netif"); + esp_netif_ip_info_t ip_info; + ESP_RETURN_ON_FALSE(esp_netif_get_ip_info(netif, &ip_info) == ESP_OK, NULL, TAG, "Failed to get IP from netif"); + ESP_RETURN_ON_FALSE(esp_ip4addr_ntoa(&ip_info.ip, s_ipv4_addr, sizeof(s_ipv4_addr)) != NULL, NULL, TAG, "Failed to convert IP"); + return s_ipv4_addr; +} diff --git a/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults b/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults new file mode 100644 index 0000000000..037b680153 --- /dev/null +++ b/components/mosquitto/examples/serverless_mqtt/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=32768 diff --git a/components/mosquitto/examples/serverless_mqtt/serverless.png b/components/mosquitto/examples/serverless_mqtt/serverless.png new file mode 100644 index 0000000000000000000000000000000000000000..6822526427e764d6b0d8377665240eae41e77cf8 GIT binary patch literal 86433 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 TDnScRfRB!*!L{P6R>A)QP(mnb literal 0 HcmV?d00001 From 6cce87e4652bb690ac230dfacd8111e6d4dd0029 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:33:48 +0100 Subject: [PATCH 02/57] fix(mosq): Fix dependency issues moving esp-tls to public deps Since esp-tls structs are using in public header files --- components/mosquitto/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/mosquitto/CMakeLists.txt b/components/mosquitto/CMakeLists.txt index 1db843ec19..47e5d551d9 100644 --- a/components/mosquitto/CMakeLists.txt +++ b/components/mosquitto/CMakeLists.txt @@ -81,7 +81,8 @@ idf_component_register(SRCS ${m_srcs} PRIV_INCLUDE_DIRS port/priv_include port/priv_include/sys ${m_dir} ${m_src_dir} ${m_incl_dir} ${m_lib_dir} ${m_deps_dir} INCLUDE_DIRS ${m_incl_dir} port/include - PRIV_REQUIRES newlib esp-tls + REQUIRES esp-tls + PRIV_REQUIRES newlib ) target_compile_definitions(${COMPONENT_LIB} PRIVATE "WITH_BROKER") From cdeab8f517923e2f6a2a9352d7fecbcb6d4af88f Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:36:51 +0100 Subject: [PATCH 03/57] feat(mosq): Add support for on-message callback --- components/mosquitto/port/broker.c | 5 +++++ components/mosquitto/port/callbacks.c | 5 +++++ components/mosquitto/port/include/mosq_broker.h | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/components/mosquitto/port/broker.c b/components/mosquitto/port/broker.c index 62cfb29bce..f78f651790 100644 --- a/components/mosquitto/port/broker.c +++ b/components/mosquitto/port/broker.c @@ -101,6 +101,8 @@ void mosq_broker_stop(void) run = 0; } +extern mosq_message_cb_t g_mosq_message_callback; + int mosq_broker_run(struct mosq_broker_config *broker_config) { @@ -125,6 +127,9 @@ int mosq_broker_run(struct mosq_broker_config *broker_config) if (broker_config->tls_cfg) { net__set_tls_config(broker_config->tls_cfg); } + if (broker_config->handle_message_cb) { + g_mosq_message_callback = broker_config->handle_message_cb; + } db.config = &config; diff --git a/components/mosquitto/port/callbacks.c b/components/mosquitto/port/callbacks.c index cf6e5f9a29..dc0f2ad1b5 100644 --- a/components/mosquitto/port/callbacks.c +++ b/components/mosquitto/port/callbacks.c @@ -13,7 +13,9 @@ #include "util_mosq.h" #include "utlist.h" #include "lib_load.h" +#include "mosq_broker.h" +mosq_message_cb_t g_mosq_message_callback = NULL; int mosquitto_callback_register( mosquitto_plugin_id_t *identifier, @@ -44,5 +46,8 @@ void plugin__handle_disconnect(struct mosquitto *context, int reason) int plugin__handle_message(struct mosquitto *context, struct mosquitto_msg_store *stored) { + if (g_mosq_message_callback) { + g_mosq_message_callback(context->id, stored->topic, stored->payload, stored->payloadlen, stored->qos, stored->retain); + } return MOSQ_ERR_SUCCESS; } diff --git a/components/mosquitto/port/include/mosq_broker.h b/components/mosquitto/port/include/mosq_broker.h index 362943557f..d9b858b8c4 100644 --- a/components/mosquitto/port/include/mosq_broker.h +++ b/components/mosquitto/port/include/mosq_broker.h @@ -9,6 +9,7 @@ struct mosquitto__config; +typedef void (*mosq_message_cb_t)(char *client, char *topic, char *data, int len, int qos, int retain); /** * @brief Mosquitto configuration structure * @@ -24,6 +25,10 @@ struct mosq_broker_config { * You can open the respective docs with this idf.py command: * `idf.py docs -sp api-reference/protocols/esp_tls.html` */ + void (*handle_message_cb)(char *client, char *topic, char *data, int len, int qos, int retain); /*!< + * On message callback. If configured, user function is called + * whenever mosquitto processes a message. + */ }; /** From 3b2c614d8669acace15ac67452589b0adba9c7c5 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:38:41 +0100 Subject: [PATCH 04/57] feat(mosq): Upgrade to mosquitto v2.0.20 Used tagged version v2.0.20 --- components/mosquitto/mosquitto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mosquitto/mosquitto b/components/mosquitto/mosquitto index 3923526c6b..a196c2b244 160000 --- a/components/mosquitto/mosquitto +++ b/components/mosquitto/mosquitto @@ -1 +1 @@ -Subproject commit 3923526c6b4c048bbecad2506c4c9963bc46cd36 +Subproject commit a196c2b244f248072a6b3ac8fb3f00ce0ff63dea From e6fb8aa078464b59c9fd38fec246df3911611630 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 19 Dec 2024 16:45:04 +0100 Subject: [PATCH 05/57] bump(mosq): 2.0.18~0 -> 2.0.20~0 2.0.20~0 Features - Upgrade to mosquitto v2.0.20 (3b2c614d) - Add support for on-message callback (cdeab8f5) - Add example with two brokers synced on P2P (d57b8c5b) Bug Fixes - Fix dependency issues moving esp-tls to public deps (6cce87e4) --- components/mosquitto/.cz.yaml | 2 +- components/mosquitto/CHANGELOG.md | 18 ++++++++++++++++++ components/mosquitto/idf_component.yml | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/components/mosquitto/.cz.yaml b/components/mosquitto/.cz.yaml index f9b741cd0b..0b6507a505 100644 --- a/components/mosquitto/.cz.yaml +++ b/components/mosquitto/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mosq): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mosquitto tag_format: mosq-v$version - version: 2.0.28~0 + version: 2.0.20 version_files: - idf_component.yml diff --git a/components/mosquitto/CHANGELOG.md b/components/mosquitto/CHANGELOG.md index bcc1c9a553..5c24047c45 100644 --- a/components/mosquitto/CHANGELOG.md +++ b/components/mosquitto/CHANGELOG.md @@ -1,5 +1,23 @@ +# Changelog + +## [2.0.20](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.20) + +### Features + +- Upgrade to mosquitto v2.0.20 ([3b2c614d](https://github.com/espressif/esp-protocols/commit/3b2c614d)) +- Add support for on-message callback ([cdeab8f5](https://github.com/espressif/esp-protocols/commit/cdeab8f5)) +- Add example with two brokers synced on P2P ([d57b8c5b](https://github.com/espressif/esp-protocols/commit/d57b8c5b)) + +### Bug Fixes + +- Fix dependency issues moving esp-tls to public deps ([6cce87e4](https://github.com/espressif/esp-protocols/commit/6cce87e4)) + ## [2.0.28~0](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.28_0) +### Warning + +Incorrect version number! This version published under `2.0.28~0` is based on upstream v2.0.18 + ### Features - Added support for TLS transport using ESP-TLS ([1af4bbe1](https://github.com/espressif/esp-protocols/commit/1af4bbe1)) diff --git a/components/mosquitto/idf_component.yml b/components/mosquitto/idf_component.yml index 5ef670047f..d8aee0e68a 100644 --- a/components/mosquitto/idf_component.yml +++ b/components/mosquitto/idf_component.yml @@ -1,4 +1,4 @@ -version: "2.0.28~0" +version: "2.0.20~0" url: https://github.com/espressif/esp-protocols/tree/master/components/mosquitto description: The component provides a simple ESP32 port of mosquitto broker dependencies: From 5dcc33300ffe1e64b0830f592020d010f4820772 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 12:09:48 +0100 Subject: [PATCH 06/57] fix(mosq): Update API docs adding on-message callback --- components/mosquitto/api.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/mosquitto/api.md b/components/mosquitto/api.md index f010cbb241..aea93fdfea 100644 --- a/components/mosquitto/api.md +++ b/components/mosquitto/api.md @@ -15,6 +15,7 @@ | Type | Name | | ---: | :--- | | struct | [**mosq\_broker\_config**](#struct-mosq_broker_config)
_Mosquitto configuration structure._ | +| typedef void(\* | [**mosq\_message\_cb\_t**](#typedef-mosq_message_cb_t)
| ## Functions @@ -34,12 +35,20 @@ ESP port of mosquittto supports only the options in this configuration structure Variables: +- void(\* handle_message_cb
On message callback. If configured, user function is called whenever mosquitto processes a message. + - char \* host
Address on which the broker is listening for connections - int port
Port number of the broker to listen to - esp\_tls\_cfg\_server\_t \* tls_cfg
ESP-TLS configuration (if TLS transport used) Please refer to the ESP-TLS official documentation for more details on configuring the TLS options. You can open the respective docs with this idf.py command: `idf.py docs -sp api-reference/protocols/esp_tls.html` +### typedef `mosq_message_cb_t` + +```c +typedef void(* mosq_message_cb_t) (char *client, char *topic, char *data, int len, int qos, int retain); +``` + ## Functions Documentation From 95294f5f89400827f4e336ccec8fff916e86f212 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 12:35:02 +0100 Subject: [PATCH 07/57] fix(mosq): Add consistency check for api docs and versions --- .github/workflows/mosq__build.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index 27ff2581c1..4534408b61 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -73,3 +73,30 @@ jobs: mv $dir build python -m pytest --log-cli-level DEBUG --junit-xml=./results_esp32_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=esp32 done + + check_consistency: + name: Checks that API docs and versions are consistent + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Checks API Docs and versions + run: | + sudo apt-get update + sudo apt-get -y install doxygen + pip install esp-doxybook + cd components/mosquitto + cp api.md api_orig.md + ./generate_api_docs.sh + diff -wB api.md api_orig.md + # check version consistency + CONFIG_VERSION=$(grep -Po '(?<=#define VERSION ")[^"]*' port/priv_include/config.h) + CZ_VERSION=$(grep -Po '(?<=version: )[^"]*' .cz.yaml) + COMP_VERSION=$(grep -Po '(?<=version: ")[^"]*' idf_component.yml) + if [ "$CONFIG_VERSION" != "v$CZ_VERSION" ] || [ "$CONFIG_VERSION" != "v$COMP_VERSION" ]; then + echo "Version mismatch detected:" + echo "config.h: $CONFIG_VERSION" + echo ".cz.yaml: $CZ_VERSION" + echo "idf_component.yml: $COMP_VERSION" + exit 1 + fi + echo "Versions are consistent: $CONFIG_VERSION" From 3cd0ed377b749735408045c5cd68e5550c576560 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 13:47:33 +0100 Subject: [PATCH 08/57] fix(mosq): Use sock_utils instead of func stubs --- components/mosquitto/CMakeLists.txt | 5 ++--- components/mosquitto/idf_component.yml | 2 ++ components/mosquitto/port/ifaddrs.c | 20 ------------------- .../mosquitto/port/priv_include/config.h | 13 ++---------- .../mosquitto/port/priv_include/ifaddrs.h | 17 ---------------- components/mosquitto/port/priv_include/poll.h | 8 -------- components/mosquitto/port/sysconf.c | 18 +++++++++++++++++ 7 files changed, 24 insertions(+), 59 deletions(-) delete mode 100644 components/mosquitto/port/ifaddrs.c delete mode 100644 components/mosquitto/port/priv_include/ifaddrs.h delete mode 100644 components/mosquitto/port/priv_include/poll.h create mode 100644 components/mosquitto/port/sysconf.c diff --git a/components/mosquitto/CMakeLists.txt b/components/mosquitto/CMakeLists.txt index 47e5d551d9..a7e337009e 100644 --- a/components/mosquitto/CMakeLists.txt +++ b/components/mosquitto/CMakeLists.txt @@ -74,16 +74,15 @@ idf_component_register(SRCS ${m_srcs} port/callbacks.c port/config.c port/signals.c - port/ifaddrs.c port/broker.c port/files.c port/net__esp_tls.c + port/sysconf.c PRIV_INCLUDE_DIRS port/priv_include port/priv_include/sys ${m_dir} ${m_src_dir} ${m_incl_dir} ${m_lib_dir} ${m_deps_dir} INCLUDE_DIRS ${m_incl_dir} port/include REQUIRES esp-tls - PRIV_REQUIRES newlib - ) + PRIV_REQUIRES newlib sock_utils) target_compile_definitions(${COMPONENT_LIB} PRIVATE "WITH_BROKER") target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") diff --git a/components/mosquitto/idf_component.yml b/components/mosquitto/idf_component.yml index d8aee0e68a..7514a882fe 100644 --- a/components/mosquitto/idf_component.yml +++ b/components/mosquitto/idf_component.yml @@ -3,3 +3,5 @@ url: https://github.com/espressif/esp-protocols/tree/master/components/mosquitto description: The component provides a simple ESP32 port of mosquitto broker dependencies: idf: '>=5.1' + espressif/sock_utils: + version: '^0.2' diff --git a/components/mosquitto/port/ifaddrs.c b/components/mosquitto/port/ifaddrs.c deleted file mode 100644 index 1e746416bd..0000000000 --- a/components/mosquitto/port/ifaddrs.c +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 Roger Light - * - * SPDX-License-Identifier: EPL-2.0 - * - * SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD - */ -#include "ifaddrs.h" - -// Dummy implementation of getifaddrs() -// TODO: Implement this if we need to use bind_interface option of listener's config -int getifaddrs(struct ifaddrs **ifap) -{ - return -1; -} - -void freeifaddrs(struct ifaddrs *ifa) -{ - -} diff --git a/components/mosquitto/port/priv_include/config.h b/components/mosquitto/port/priv_include/config.h index 8680e88f75..bf7cb619b9 100644 --- a/components/mosquitto/port/priv_include/config.h +++ b/components/mosquitto/port/priv_include/config.h @@ -5,20 +5,11 @@ */ #pragma once #include +#include "net/if.h" #undef isspace #define isspace(__c) (__ctype_lookup((int)__c)&_S) #include_next "config.h" -#define VERSION "v2.0.18~0" -#define _SC_OPEN_MAX_OVERRIDE 4 - -// used to override syscall for obtaining max open fds -static inline long sysconf(int arg) -{ - if (arg == _SC_OPEN_MAX_OVERRIDE) { - return 64; - } - return -1; -} +#define VERSION "v2.0.20~1" diff --git a/components/mosquitto/port/priv_include/ifaddrs.h b/components/mosquitto/port/priv_include/ifaddrs.h deleted file mode 100644 index 604628cbac..0000000000 --- a/components/mosquitto/port/priv_include/ifaddrs.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#pragma once - -#define gai_strerror(x) "gai_strerror() not supported" - -struct ifaddrs { - struct ifaddrs *ifa_next; /* Next item in list */ - char *ifa_name; /* Name of interface */ - struct sockaddr *ifa_addr; /* Address of interface */ -}; - -int getifaddrs(struct ifaddrs **ifap); -void freeifaddrs(struct ifaddrs *ifa); diff --git a/components/mosquitto/port/priv_include/poll.h b/components/mosquitto/port/priv_include/poll.h deleted file mode 100644 index 1ac76a5b4f..0000000000 --- a/components/mosquitto/port/priv_include/poll.h +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#pragma once - -#include_next "sys/poll.h" diff --git a/components/mosquitto/port/sysconf.c b/components/mosquitto/port/sysconf.c new file mode 100644 index 0000000000..80a1d92318 --- /dev/null +++ b/components/mosquitto/port/sysconf.c @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#define _SC_OPEN_MAX_OVERRIDE 4 + +// used to override syscall for obtaining max open fds +long sysconf(int arg) +{ + if (arg == _SC_OPEN_MAX_OVERRIDE) { + return 64; + } + return -1; +} From 73e523e736987e493d02564cc45e3d8a33fc53ed Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 15:08:18 +0100 Subject: [PATCH 09/57] bump(mosq): 2.0.20 -> 2.0.20~1 2.0.20~1 Bug Fixes - Use sock_utils instead of func stubs (3cd0ed37) - Update API docs adding on-message callback (5dcc3330) --- components/mosquitto/.cz.yaml | 2 +- components/mosquitto/CHANGELOG.md | 7 +++++++ components/mosquitto/idf_component.yml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/components/mosquitto/.cz.yaml b/components/mosquitto/.cz.yaml index 0b6507a505..b74ae209d7 100644 --- a/components/mosquitto/.cz.yaml +++ b/components/mosquitto/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mosq): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mosquitto tag_format: mosq-v$version - version: 2.0.20 + version: 2.0.20~1 version_files: - idf_component.yml diff --git a/components/mosquitto/CHANGELOG.md b/components/mosquitto/CHANGELOG.md index 5c24047c45..954ccc8ee5 100644 --- a/components/mosquitto/CHANGELOG.md +++ b/components/mosquitto/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.0.20~1](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.20_1) + +### Bug Fixes + +- Use sock_utils instead of func stubs ([3cd0ed37](https://github.com/espressif/esp-protocols/commit/3cd0ed37)) +- Update API docs adding on-message callback ([5dcc3330](https://github.com/espressif/esp-protocols/commit/5dcc3330)) + ## [2.0.20](https://github.com/espressif/esp-protocols/commits/mosq-v2.0.20) ### Features diff --git a/components/mosquitto/idf_component.yml b/components/mosquitto/idf_component.yml index 7514a882fe..6dda14b8f1 100644 --- a/components/mosquitto/idf_component.yml +++ b/components/mosquitto/idf_component.yml @@ -1,4 +1,4 @@ -version: "2.0.20~0" +version: "2.0.20~1" url: https://github.com/espressif/esp-protocols/tree/master/components/mosquitto description: The component provides a simple ESP32 port of mosquitto broker dependencies: From ade9448c01e15fb3e27a84bf5bed8395bddf8918 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 14:37:16 +0100 Subject: [PATCH 10/57] fix(sockutls): Fix potential macro colission including standard headers --- components/sock_utils/include/netdb_macros.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/sock_utils/include/netdb_macros.h b/components/sock_utils/include/netdb_macros.h index 6c518c9bbe..91987dc91f 100644 --- a/components/sock_utils/include/netdb_macros.h +++ b/components/sock_utils/include/netdb_macros.h @@ -5,6 +5,17 @@ */ #pragma once +#include "sdkconfig.h" +#ifndef CONFIG_IDF_TARGET_LINUX +#include // For network-related definitions +#include // For socket-related definitions +#include // For interface flags (e.g., IFF_UP) +#include // For NI_NUMERICHOST, NI_NUMERICSERV, etc. +#include // For EAI_BADFLAGS +#include // For AF_UNIX +#include // For PF_LOCAL +#endif + #ifndef NI_NUMERICHOST #define NI_NUMERICHOST 0x1 #endif From 0499ed93df7725d689c31d1d8ff62fcfbf01661c Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 15:41:10 +0100 Subject: [PATCH 11/57] bump(sockutls): 0.2.0 -> 0.2.1 0.2.1 Bug Fixes - Fix potential macro colission including standard headers (ade9448c) --- components/sock_utils/.cz.yaml | 2 +- components/sock_utils/CHANGELOG.md | 6 ++++++ components/sock_utils/idf_component.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/sock_utils/.cz.yaml b/components/sock_utils/.cz.yaml index c58b02998e..ed20caec5f 100644 --- a/components/sock_utils/.cz.yaml +++ b/components/sock_utils/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(sockutls): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py sock_utils tag_format: sock_utils-v$version - version: 0.2.0 + version: 0.2.1 version_files: - idf_component.yml diff --git a/components/sock_utils/CHANGELOG.md b/components/sock_utils/CHANGELOG.md index 7034928b30..fcaa12cf31 100644 --- a/components/sock_utils/CHANGELOG.md +++ b/components/sock_utils/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.2.1](https://github.com/espressif/esp-protocols/commits/sock_utils-v0.2.1) + +### Bug Fixes + +- Fix potential macro colission including standard headers ([ade9448c](https://github.com/espressif/esp-protocols/commit/ade9448c)) + ## [0.2.0](https://github.com/espressif/esp-protocols/commits/sock_utils-v0.2.0) ### Features diff --git a/components/sock_utils/idf_component.yml b/components/sock_utils/idf_component.yml index 081c2b9842..171a8c8b32 100644 --- a/components/sock_utils/idf_component.yml +++ b/components/sock_utils/idf_component.yml @@ -1,4 +1,4 @@ -version: 0.2.0 +version: 0.2.1 description: The component provides helper implementation of common system/socket utilities url: https://github.com/espressif/esp-protocols/tree/master/components/sock_utils dependencies: From 5bd82c01a59bffc2b12618fbccb4e800c772e4e2 Mon Sep 17 00:00:00 2001 From: zwx Date: Mon, 6 Jan 2025 20:00:50 +0800 Subject: [PATCH 12/57] feat(mdns): support zero item when update subtype --- components/mdns/include/mdns.h | 2 ++ components/mdns/mdns.c | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/components/mdns/include/mdns.h b/components/mdns/include/mdns.h index c15ecb6221..8676717fd3 100644 --- a/components/mdns/include/mdns.h +++ b/components/mdns/include/mdns.h @@ -615,6 +615,8 @@ esp_err_t mdns_service_subtype_add_multiple_items_for_host(const char *instance_ * @param subtype the pointer of subtype array to add. * @param num_items number of items in subtype array * + * @note If `num_items` is 0, then remove all subtypes. + * * @return * - ESP_OK success * - ESP_ERR_INVALID_ARG Parameter error diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 113a1e999b..0d75836e88 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -6429,8 +6429,8 @@ esp_err_t mdns_service_subtype_update_multiple_items_for_host(const char *instan MDNS_SERVICE_LOCK(); esp_err_t ret = ESP_OK; int cur_index = 0; - ESP_GOTO_ON_FALSE(_mdns_server && _mdns_server->services && !_str_null_or_empty(service_type) && !_str_null_or_empty(proto) && - (num_items > 0), ESP_ERR_INVALID_ARG, err, TAG, "Invalid state or arguments"); + ESP_GOTO_ON_FALSE(_mdns_server && _mdns_server->services && !_str_null_or_empty(service_type) && !_str_null_or_empty(proto), + ESP_ERR_INVALID_ARG, err, TAG, "Invalid state or arguments"); mdns_srv_item_t *s = _mdns_get_service_item_instance(instance_name, service_type, proto, hostname); ESP_GOTO_ON_FALSE(s, ESP_ERR_NOT_FOUND, err, TAG, "Service doesn't exist"); @@ -6450,8 +6450,9 @@ esp_err_t mdns_service_subtype_update_multiple_items_for_host(const char *instan goto exit; } } - - _mdns_announce_all_pcbs(&s, 1, false); + if (num_items) { + _mdns_announce_all_pcbs(&s, 1, false); + } err: if (ret == ESP_ERR_NO_MEM) { for (int idx = 0; idx < cur_index; idx++) { From f12a20565788974e289d5bdce3587dfff5d3c6de Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 7 Jan 2025 12:17:35 +0100 Subject: [PATCH 13/57] fix(sockutls): Fix gai_strerror() impl to return const string Previous gai_strerror() returned numeric representation of the code, but used TLS storage, which might cause issues with stack sizes on all tasks in the system. Alternatively we can leave the storage to static only (which wouldn't be thread-safe) or we could one-time allocate and never free (which is wrong). This option uses hardcoded strings for common error codes used in lwip. The disadvantage is that we might need to update the impl in the future when lwip adds more codes. --- components/sock_utils/include/gai_strerror.h | 11 +++---- components/sock_utils/src/gai_strerror.c | 31 ++++++++++++++++---- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/components/sock_utils/include/gai_strerror.h b/components/sock_utils/include/gai_strerror.h index ad872b982f..f65601d01b 100644 --- a/components/sock_utils/include/gai_strerror.h +++ b/components/sock_utils/include/gai_strerror.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -18,13 +18,14 @@ extern "C" { #endif /** -* @brief Returns a numeric string representing of `getaddrinfo()` error code. +* @brief Returns a string representing of `getaddrinfo()` error code. * -* @param[in] ecode Error code returned by `getaddrinfo()`. +* @param[in] errcode Error code returned by `getaddrinfo()`. * -* @return A pointer to a string describing the error. +* @return A pointer to a string containing the error code, for example "EAI_NONAME" +* for EAI_NONAME error type. */ -const char *gai_strerror(int ecode); +const char *gai_strerror(int errcode); #ifdef __cplusplus } diff --git a/components/sock_utils/src/gai_strerror.c b/components/sock_utils/src/gai_strerror.c index a4060cf2f0..a4ff737678 100644 --- a/components/sock_utils/src/gai_strerror.c +++ b/components/sock_utils/src/gai_strerror.c @@ -1,17 +1,36 @@ /* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include "gai_strerror.h" +#include "lwip/netdb.h" -_Thread_local char gai_strerror_string[32]; +#define HANDLE_GAI_ERROR(code) \ + case code: return #code; -const char *gai_strerror(int ecode) +const char *gai_strerror(int errcode) { - if (snprintf(gai_strerror_string, sizeof(gai_strerror_string), "EAI error:%d", ecode) < 0) { - return "gai_strerror() failed"; + switch (errcode) { + /* lwip defined DNS codes */ + HANDLE_GAI_ERROR(EAI_BADFLAGS) + HANDLE_GAI_ERROR(EAI_FAIL) + HANDLE_GAI_ERROR(EAI_FAMILY) + HANDLE_GAI_ERROR(EAI_MEMORY) + HANDLE_GAI_ERROR(EAI_NONAME) + HANDLE_GAI_ERROR(EAI_SERVICE) + /* other error codes optionally defined in platform/newlib or toolchain */ +#ifdef EAI_AGAIN + HANDLE_GAI_ERROR(EAI_AGAIN) +#endif +#ifdef EAI_SOCKTYPE + HANDLE_GAI_ERROR(EAI_SOCKTYPE) +#endif +#ifdef EAI_SYSTEM + HANDLE_GAI_ERROR(EAI_SYSTEM) +#endif + default: + return "Unknown error"; } - return gai_strerror_string; } From 9ed835ba3f8d2795332866bed95f49ff2b032b79 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 7 Jan 2025 14:06:42 +0100 Subject: [PATCH 14/57] bump(sockutls): 0.2.1 -> 0.2.2 0.2.2 Bug Fixes - Fix gai_strerror() impl to return const string (f12a2056) --- components/sock_utils/.cz.yaml | 2 +- components/sock_utils/CHANGELOG.md | 6 ++++++ components/sock_utils/idf_component.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/sock_utils/.cz.yaml b/components/sock_utils/.cz.yaml index ed20caec5f..f134fe03ec 100644 --- a/components/sock_utils/.cz.yaml +++ b/components/sock_utils/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(sockutls): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py sock_utils tag_format: sock_utils-v$version - version: 0.2.1 + version: 0.2.2 version_files: - idf_component.yml diff --git a/components/sock_utils/CHANGELOG.md b/components/sock_utils/CHANGELOG.md index fcaa12cf31..20adc55fd2 100644 --- a/components/sock_utils/CHANGELOG.md +++ b/components/sock_utils/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.2.2](https://github.com/espressif/esp-protocols/commits/sock_utils-v0.2.2) + +### Bug Fixes + +- Fix gai_strerror() impl to return const string ([f12a2056](https://github.com/espressif/esp-protocols/commit/f12a2056)) + ## [0.2.1](https://github.com/espressif/esp-protocols/commits/sock_utils-v0.2.1) ### Bug Fixes diff --git a/components/sock_utils/idf_component.yml b/components/sock_utils/idf_component.yml index 171a8c8b32..0956f71041 100644 --- a/components/sock_utils/idf_component.yml +++ b/components/sock_utils/idf_component.yml @@ -1,4 +1,4 @@ -version: 0.2.1 +version: 0.2.2 description: The component provides helper implementation of common system/socket utilities url: https://github.com/espressif/esp-protocols/tree/master/components/sock_utils dependencies: From 9b0ba6060f66b48c3de93d987c4fb4ebf4d4f461 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 8 Jan 2025 09:42:38 +0100 Subject: [PATCH 15/57] bump(mdns): 1.4.2 -> 1.4.3 1.4.3 Features - support zero item when update subtype (5bd82c01) --- components/mdns/.cz.yaml | 2 +- components/mdns/CHANGELOG.md | 6 ++++++ components/mdns/idf_component.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/mdns/.cz.yaml b/components/mdns/.cz.yaml index 1ab8bd7b80..a8390acf21 100644 --- a/components/mdns/.cz.yaml +++ b/components/mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.4.2 + version: 1.4.3 version_files: - idf_component.yml diff --git a/components/mdns/CHANGELOG.md b/components/mdns/CHANGELOG.md index d375d6e69e..2fae369918 100644 --- a/components/mdns/CHANGELOG.md +++ b/components/mdns/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.4.3](https://github.com/espressif/esp-protocols/commits/mdns-v1.4.3) + +### Features + +- support zero item when update subtype ([5bd82c01](https://github.com/espressif/esp-protocols/commit/5bd82c01)) + ## [1.4.2](https://github.com/espressif/esp-protocols/commits/mdns-v1.4.2) ### Features diff --git a/components/mdns/idf_component.yml b/components/mdns/idf_component.yml index c0fc4e545e..1e43ed704d 100644 --- a/components/mdns/idf_component.yml +++ b/components/mdns/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.4.2" +version: "1.4.3" description: "Multicast UDP service used to provide local network service and host discovery." url: "https://github.com/espressif/esp-protocols/tree/master/components/mdns" issues: "https://github.com/espressif/esp-protocols/issues" From 9537721600eb522a0be45bfebdeda2808ba90c47 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 10:20:06 +0100 Subject: [PATCH 16/57] fix(mdns): Fixed complier warning if MDNS_MAX_SERVICES==0 Closes https://github.com/espressif/esp-protocols/issues/611 --- components/mdns/mdns.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 0d75836e88..f5274782ad 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -334,6 +334,9 @@ static mdns_host_item_t *mdns_get_host_item(const char *hostname) static bool _mdns_can_add_more_services(void) { +#if MDNS_MAX_SERVICES == 0 + return false; +#else mdns_srv_item_t *s = _mdns_server->services; uint16_t service_num = 0; while (s) { @@ -343,8 +346,8 @@ static bool _mdns_can_add_more_services(void) return false; } } - return true; +#endif } esp_err_t _mdns_send_rx_action(mdns_rx_packet_t *packet) @@ -5901,7 +5904,8 @@ esp_err_t mdns_service_add_for_host(const char *instance, const char *service, c const char *hostname = host ? host : _mdns_server->hostname; mdns_service_t *s = NULL; - ESP_GOTO_ON_FALSE(_mdns_can_add_more_services(), ESP_ERR_NO_MEM, err, TAG, "Cannot add more services"); + ESP_GOTO_ON_FALSE(_mdns_can_add_more_services(), ESP_ERR_NO_MEM, err, TAG, + "Cannot add more services, please increase CONFIG_MDNS_MAX_SERVICES (%d)", CONFIG_MDNS_MAX_SERVICES); mdns_srv_item_t *item = _mdns_get_service_item_instance(instance, service, proto, hostname); ESP_GOTO_ON_FALSE(!item, ESP_ERR_INVALID_ARG, err, TAG, "Service already exists"); From 827ea65fd543397c988732065c2960b0052353fa Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 10:26:55 +0100 Subject: [PATCH 17/57] fix(mdns): Allow advertizing service with port==0 Closes https://github.com/espressif/esp-idf/issues/14335 --- components/mdns/mdns.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index f5274782ad..22950ffc99 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -5895,7 +5895,7 @@ esp_err_t mdns_instance_name_set(const char *instance) esp_err_t mdns_service_add_for_host(const char *instance, const char *service, const char *proto, const char *host, uint16_t port, mdns_txt_item_t txt[], size_t num_items) { - if (!_mdns_server || _str_null_or_empty(service) || _str_null_or_empty(proto) || !port || !_mdns_server->hostname) { + if (!_mdns_server || _str_null_or_empty(service) || _str_null_or_empty(proto) || !_mdns_server->hostname) { return ESP_ERR_INVALID_ARG; } From 68a9e14898ae70e8d1908644575361238088890b Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 10:29:04 +0100 Subject: [PATCH 18/57] fix(mdns): Cleanup includes in mdns.c Closes https://github.com/espressif/esp-protocols/issues/725 --- components/mdns/mdns.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 22950ffc99..6a7df9d334 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -5,19 +5,17 @@ */ #include -#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "freertos/semphr.h" #include "esp_log.h" #include "esp_event.h" +#include "esp_random.h" +#include "esp_check.h" #include "mdns.h" #include "mdns_private.h" #include "mdns_networking.h" -#include "esp_log.h" -#include "esp_random.h" -#include "esp_check.h" static void _mdns_browse_item_free(mdns_browse_t *browse); static esp_err_t _mdns_send_browse_action(mdns_action_type_t type, mdns_browse_t *browse); From 907087c09bb94ab06cf3de15d3b18d174e42f0ee Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 10:33:02 +0100 Subject: [PATCH 19/57] fix(mdns): Move MDNS_NAME_BUF_LEN to public headers Since it's used by public API as maximum length of user buffer Closes https://github.com/espressif/esp-protocols/issues/724 --- components/mdns/include/mdns.h | 10 +++++++++- components/mdns/private_include/mdns_private.h | 8 +------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/components/mdns/include/mdns.h b/components/mdns/include/mdns.h index 8676717fd3..cc9a39d157 100644 --- a/components/mdns/include/mdns.h +++ b/components/mdns/include/mdns.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -10,6 +10,7 @@ extern "C" { #endif +#include "sdkconfig.h" #include #define MDNS_TYPE_A 0x0001 @@ -21,6 +22,13 @@ extern "C" { #define MDNS_TYPE_NSEC 0x002F #define MDNS_TYPE_ANY 0x00FF +#if defined(CONFIG_LWIP_IPV6) && defined(CONFIG_MDNS_RESPOND_REVERSE_QUERIES) +#define MDNS_NAME_MAX_LEN (64+4) // Need to account for IPv6 reverse queries (64 char address + ".ip6" ) +#else +#define MDNS_NAME_MAX_LEN 64 // Maximum string length of hostname, instance, service and proto +#endif +#define MDNS_NAME_BUF_LEN (MDNS_NAME_MAX_LEN+1) // Maximum char buffer size to hold hostname, instance, service or proto + /** * @brief Asynchronous query handle */ diff --git a/components/mdns/private_include/mdns_private.h b/components/mdns/private_include/mdns_private.h index 381bd4be43..ce4c96b631 100644 --- a/components/mdns/private_include/mdns_private.h +++ b/components/mdns/private_include/mdns_private.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,12 +103,6 @@ #define MDNS_PACKET_QUEUE_LEN 16 // Maximum packets that can be queued for parsing #define MDNS_ACTION_QUEUE_LEN CONFIG_MDNS_ACTION_QUEUE_LEN // Maximum actions pending to the server #define MDNS_TXT_MAX_LEN 1024 // Maximum string length of text data in TXT record -#if defined(CONFIG_LWIP_IPV6) && defined(CONFIG_MDNS_RESPOND_REVERSE_QUERIES) -#define MDNS_NAME_MAX_LEN (64+4) // Need to account for IPv6 reverse queries (64 char address + ".ip6" ) -#else -#define MDNS_NAME_MAX_LEN 64 // Maximum string length of hostname, instance, service and proto -#endif -#define MDNS_NAME_BUF_LEN (MDNS_NAME_MAX_LEN+1) // Maximum char buffer size to hold hostname, instance, service or proto #define MDNS_MAX_PACKET_SIZE 1460 // Maximum size of mDNS outgoing packet #define MDNS_HEAD_LEN 12 From 75a8e8640a0bacc88b40cfb893fc947b56651473 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 11:22:46 +0100 Subject: [PATCH 20/57] fix(mdns): Fixed potential overflow when allocating txt data Closes coverity warning: 470092 Overflowed integer argument --- components/mdns/mdns.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 6a7df9d334..2cda7b2291 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -3486,8 +3486,9 @@ static void _mdns_result_txt_create(const uint8_t *data, size_t len, mdns_txt_it uint16_t i = 0, y; size_t partLen = 0; int num_items = _mdns_txt_items_count_get(data, len); - if (num_items < 0) { - return;//error + if (num_items < 0 || num_items > SIZE_MAX / sizeof(mdns_txt_item_t)) { + // Error: num_items is incorrect (or too large to allocate) + return; } if (!num_items) { From 8f8516cc3f81c0bc1e258794b1c07868f3aece6c Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 11:31:45 +0100 Subject: [PATCH 21/57] fix(mdns): Fixed incorrect error conversion Mixing esp_err_t (int) with err_t (uint8_t) from lwip. Closes coverity isssue: 470139 Overflowed return value --- components/mdns/mdns_networking_lwip.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mdns/mdns_networking_lwip.c b/components/mdns/mdns_networking_lwip.c index 635f7e352e..16604cdf14 100644 --- a/components/mdns/mdns_networking_lwip.c +++ b/components/mdns/mdns_networking_lwip.c @@ -288,7 +288,7 @@ typedef struct { static err_t _mdns_pcb_init_api(struct tcpip_api_call_data *api_call_msg) { mdns_api_call_t *msg = (mdns_api_call_t *)api_call_msg; - msg->err = _udp_pcb_init(msg->tcpip_if, msg->ip_protocol); + msg->err = _udp_pcb_init(msg->tcpip_if, msg->ip_protocol) == ESP_OK ? ERR_OK : ERR_IF; return msg->err; } From 24f55ce9b488a02b7e7efe962f99037dc07410a5 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 10 Jan 2025 11:41:02 +0100 Subject: [PATCH 22/57] fix(mdns): Fixed potential out-of-bound interface error invalid mdns_if was handled for enabling/announcing pcbs, but not for the consequent browsing Closes coverity isssue: 470162 Out-of-bounds access --- components/mdns/mdns.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 2cda7b2291..c1d41de0e8 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -4479,10 +4479,11 @@ void mdns_preset_if_handle_system_event(void *arg, esp_event_base_t event_base, case IP_EVENT_GOT_IP6: { ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; mdns_if_t mdns_if = _mdns_get_if_from_esp_netif(event->esp_netif); - if (mdns_if < MDNS_MAX_INTERFACES) { - post_mdns_enable_pcb(mdns_if, MDNS_IP_PROTOCOL_V6); - post_mdns_announce_pcb(mdns_if, MDNS_IP_PROTOCOL_V4); + if (mdns_if >= MDNS_MAX_INTERFACES) { + return; } + post_mdns_enable_pcb(mdns_if, MDNS_IP_PROTOCOL_V6); + post_mdns_announce_pcb(mdns_if, MDNS_IP_PROTOCOL_V4); mdns_browse_t *browse = _mdns_server->browse; while (browse) { _mdns_browse_send(browse, mdns_if); From 3d8835cfb90cb94cbbc2e4e73cf2302a7a99e823 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 15 Jan 2025 11:42:34 +0100 Subject: [PATCH 23/57] fix(mdns): Fix AFL test mock per espressif/esp-idf@a5bc08fb55c --- components/mdns/tests/test_afl_fuzz_host/esp32_mock.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.c b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.c index 14b9134118..5830c251e7 100644 --- a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.c +++ b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.c @@ -117,6 +117,10 @@ void esp_log_write(esp_log_level_t level, const char *tag, const char *format, . { } +void esp_log(esp_log_config_t config, const char *tag, const char *format, ...) +{ +} + uint32_t esp_log_timestamp(void) { return 0; From f5be2f4115a9da0e9aaab30bfd439f0043d8dbb4 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 13 Jan 2025 11:37:51 +0100 Subject: [PATCH 24/57] fix(mdns): Fix potential null derefernce in _mdns_execute_action() We did check for null-deref before checking 'a->type', but contol continues and passes potential null-ptr to the processing function _mdns_execute_action() Fixed by asserting action != NULL, since it is an invalid state which should never happen. --- components/mdns/mdns.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index c1d41de0e8..b4d18364ce 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -5346,7 +5346,8 @@ static void _mdns_service_task(void *pvParameters) for (;;) { if (_mdns_server && _mdns_server->action_queue) { if (xQueueReceive(_mdns_server->action_queue, &a, portMAX_DELAY) == pdTRUE) { - if (a && a->type == ACTION_TASK_STOP) { + assert(a); + if (a->type == ACTION_TASK_STOP) { break; } MDNS_SERVICE_LOCK(); From 99b54ac384340bf4c7b17ec86ade77acf450fc4c Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 13 Jan 2025 14:55:35 +0100 Subject: [PATCH 25/57] fix(mdns): Fix name mangling not to use strcpy() Since it was flagged by clang-tidy as insecture API --- components/mdns/mdns.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index b4d18364ce..5a097df0d6 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -253,12 +253,13 @@ static char *_mdns_mangle_name(char *in) } sprintf(ret, "%s-2", in); } else { - ret = malloc(strlen(in) + 2); //one extra byte in case 9-10 or 99-100 etc + size_t in_len = strlen(in); + ret = malloc(in_len + 2); //one extra byte in case 9-10 or 99-100 etc if (ret == NULL) { HOOK_MALLOC_FAILED; return NULL; } - strcpy(ret, in); + memcpy(ret, in, in_len); int baseLen = p - in; //length of 'bla' in 'bla-123' //overwrite suffix with new suffix sprintf(ret + baseLen, "-%d", suffix + 1); From e838bf03f488e9401fd315342863923064740523 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 13 Jan 2025 14:57:33 +0100 Subject: [PATCH 26/57] fix(mdns): Remove dead store to arg variable shared Fixing: warning: Value stored to 'shared' is never read [clang-analyzer-deadcode.DeadStores] --- components/mdns/mdns.c | 1 - 1 file changed, 1 deletion(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 5a097df0d6..1607aae3b2 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -1812,7 +1812,6 @@ static bool _mdns_create_answer_from_service(mdns_tx_packet_t *packet, mdns_serv return false; } } else if (question->type == MDNS_TYPE_SDPTR) { - shared = true; if (!_mdns_alloc_answer(&packet->answers, MDNS_TYPE_SDPTR, service, NULL, false, false)) { return false; } From 196198ecc9eb1cbe4aee4fe7d6b89b29406d5ca3 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 13 Jan 2025 16:15:48 +0100 Subject: [PATCH 27/57] fix(mdns): Fix zero-sized VLA clang-tidy warnings and some minor leaks in creation of browse results --- components/mdns/mdns.c | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 1607aae3b2..a72e7d7281 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -2355,6 +2355,11 @@ static void _mdns_restart_pcb(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol srv_count++; a = a->next; } + if (srv_count == 0) { + // proble only IP + _mdns_init_pcb_probe(tcpip_if, ip_protocol, NULL, 0, true); + return; + } mdns_srv_item_t *services[srv_count]; size_t i = 0; a = _mdns_server->services; @@ -2555,6 +2560,10 @@ static void _mdns_restart_all_pcbs(void) srv_count++; a = a->next; } + if (srv_count == 0) { + _mdns_probe_all_pcbs(NULL, 0, true, true); + return; + } mdns_srv_item_t *services[srv_count]; size_t l = 0; a = _mdns_server->services; @@ -2898,11 +2907,12 @@ static int _mdns_check_srv_collision(mdns_service_t *service, uint16_t priority, static int _mdns_check_txt_collision(mdns_service_t *service, const uint8_t *data, size_t len) { size_t data_len = 0; - if (len == 1 && service->txt) { + if (len <= 1 && service->txt) { // len==0 means incorrect packet (and handled by the packet parser) + // but handled here again to fix clang-tidy warning on VLA "uint8_t our[0];" return -1;//we win } else if (len > 1 && !service->txt) { return 1;//they win - } else if (len == 1 && !service->txt) { + } else if (len <= 1 && !service->txt) { return 0;//same } @@ -3788,7 +3798,7 @@ void mdns_parse_packet(mdns_rx_packet_t *packet) mdns_class &= 0x7FFF; content = data_ptr + data_len; - if (content > (data + len)) { + if (content > (data + len) || data_len == 0) { goto clear_rx_packet; } @@ -4271,15 +4281,10 @@ void mdns_parse_packet(mdns_rx_packet_t *packet) free(record); } free(parsed_packet); - if (browse_result_instance) { - free(browse_result_instance); - } - if (browse_result_service) { - free(browse_result_service); - } - if (browse_result_proto) { - free(browse_result_proto); - } + free(browse_result_instance); + free(browse_result_service); + free(browse_result_proto); + free(out_sync_browse); } /** From 4ad88e297f9729cc275ca367c8f3124e7e04dc2c Mon Sep 17 00:00:00 2001 From: Tan Yan Quan Date: Mon, 13 Jan 2025 19:44:07 +0800 Subject: [PATCH 28/57] feat(mdns): supported removal of subtype when updating service --- components/mdns/mdns.c | 139 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 9 deletions(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 0d75836e88..bf2c1771d5 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -2383,6 +2383,53 @@ static void _mdns_send_bye(mdns_srv_item_t **services, size_t len, bool include_ } } +/** + * @brief Send bye for particular subtypes + */ +static void _mdns_send_bye_subtype(mdns_srv_item_t *service, const char *instance_name, mdns_subtype_t *remove_subtypes) +{ + uint8_t i, j; + for (i = 0; i < MDNS_MAX_INTERFACES; i++) { + for (j = 0; j < MDNS_IP_PROTOCOL_MAX; j++) { + if (mdns_is_netif_ready(i, j)) { + mdns_tx_packet_t *packet = _mdns_alloc_packet_default((mdns_if_t)i, (mdns_ip_protocol_t)j); + packet->flags = MDNS_FLAGS_QR_AUTHORITATIVE; + if (!_mdns_alloc_answer(&packet->answers, MDNS_TYPE_PTR, service->service, NULL, true, true)) { + _mdns_free_tx_packet(packet); + return; + } + + static uint8_t pkt[MDNS_MAX_PACKET_SIZE]; + uint16_t index = MDNS_HEAD_LEN; + memset(pkt, 0, MDNS_HEAD_LEN); + mdns_out_answer_t *a; + uint8_t count; + + _mdns_set_u16(pkt, MDNS_HEAD_FLAGS_OFFSET, packet->flags); + _mdns_set_u16(pkt, MDNS_HEAD_ID_OFFSET, packet->id); + + count = 0; + a = packet->answers; + while (a) { + if (a->type == MDNS_TYPE_PTR && a->service) { + const mdns_subtype_t *current_subtype = remove_subtypes; + while (current_subtype) { + count += (_mdns_append_subtype_ptr_record(pkt, &index, instance_name, current_subtype->subtype, a->service->service, a->service->proto, a->flush, a->bye) > 0); + current_subtype = current_subtype->next; + } + } + a = a->next; + } + _mdns_set_u16(pkt, MDNS_HEAD_ANSWERS_OFFSET, count); + + _mdns_udp_pcb_write(packet->tcpip_if, packet->ip_protocol, &packet->dst, packet->port, pkt, index); + + _mdns_free_tx_packet(packet); + } + } + } +} + /** * @brief Send announcement on particular PCB */ @@ -2794,16 +2841,22 @@ static void _mdns_remove_scheduled_service_packets(mdns_service_t *service) } } -static void _mdns_free_service_subtype(mdns_service_t *service) +static void _mdns_free_subtype(mdns_subtype_t *subtype) { - while (service->subtype) { - mdns_subtype_t *next = service->subtype->next; - free((char *)service->subtype->subtype); - free(service->subtype); - service->subtype = next; + while (subtype) { + mdns_subtype_t *next = subtype->next; + free((char *)subtype->subtype); + free(subtype); + subtype = next; } } +static void _mdns_free_service_subtype(mdns_service_t *service) +{ + _mdns_free_subtype(service->subtype); + service->subtype = NULL; +} + /** * @brief free service memory * @@ -6347,11 +6400,23 @@ esp_err_t mdns_service_subtype_remove_for_host(const char *instance_name, const ret = _mdns_service_subtype_remove_for_host(s, subtype); ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to remove the subtype: %s", subtype); - // TODO: Need to transmit a sendbye message for the removed subtype. - // TODO: Need to remove this subtype answer from the scheduled answer list. + // Transmit a sendbye message for the removed subtype. + mdns_subtype_t *remove_subtypes = (mdns_subtype_t *)malloc(sizeof(mdns_subtype_t)); + ESP_GOTO_ON_FALSE(remove_subtypes, ESP_ERR_NO_MEM, out_of_mem, TAG, "Out of memory"); + remove_subtypes->subtype = strdup(subtype); + ESP_GOTO_ON_FALSE(remove_subtypes->subtype, ESP_ERR_NO_MEM, out_of_mem, TAG, "Out of memory"); + remove_subtypes->next = NULL; + + _mdns_send_bye_subtype(s, instance_name, remove_subtypes); + _mdns_free_subtype(remove_subtypes); err: MDNS_SERVICE_UNLOCK(); return ret; +out_of_mem: + HOOK_MALLOC_FAILED; + free(remove_subtypes); + MDNS_SERVICE_UNLOCK(); + return ret; } static esp_err_t _mdns_service_subtype_add_for_host(mdns_srv_item_t *service, const char *subtype) @@ -6423,6 +6488,56 @@ esp_err_t mdns_service_subtype_add_for_host(const char *instance_name, const cha return mdns_service_subtype_add_multiple_items_for_host(instance_name, service_type, proto, hostname, _subtype, 1); } +static mdns_subtype_t *_mdns_service_find_subtype_needed_sendbye(mdns_service_t *service, mdns_subtype_item_t subtype[], + uint8_t num_items) +{ + if (!service) { + return NULL; + } + + mdns_subtype_t *current = service->subtype; + mdns_subtype_t *prev = NULL; + mdns_subtype_t *prev_goodbye = NULL; + mdns_subtype_t *out_goodbye_subtype = NULL; + + while (current) { + bool subtype_in_update = false; + + for (int i = 0; i < num_items; i++) { + if (strcmp(subtype[i].subtype, current->subtype) == 0) { + subtype_in_update = true; + break; + } + } + + if (!subtype_in_update) { + // Remove from original list + if (prev) { + prev->next = current->next; + } else { + service->subtype = current->next; + } + + mdns_subtype_t *to_move = current; + current = current->next; + + // Add to goodbye list + to_move->next = NULL; + if (prev_goodbye) { + prev_goodbye->next = to_move; + } else { + out_goodbye_subtype = to_move; + } + prev_goodbye = to_move; + } else { + prev = current; + current = current->next; + } + } + + return out_goodbye_subtype; +} + esp_err_t mdns_service_subtype_update_multiple_items_for_host(const char *instance_name, const char *service_type, const char *proto, const char *hostname, mdns_subtype_item_t subtype[], uint8_t num_items) { @@ -6435,7 +6550,13 @@ esp_err_t mdns_service_subtype_update_multiple_items_for_host(const char *instan mdns_srv_item_t *s = _mdns_get_service_item_instance(instance_name, service_type, proto, hostname); ESP_GOTO_ON_FALSE(s, ESP_ERR_NOT_FOUND, err, TAG, "Service doesn't exist"); - // TODO: find subtype needs to say sendbye + mdns_subtype_t *goodbye_subtype = _mdns_service_find_subtype_needed_sendbye(s->service, subtype, num_items); + + if (goodbye_subtype) { + _mdns_send_bye_subtype(s, instance_name, goodbye_subtype); + } + + _mdns_free_subtype(goodbye_subtype); _mdns_free_service_subtype(s->service); for (; cur_index < num_items; cur_index++) { From 774bab22ea293a29b85c256ad6da430ea6e87593 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 20 Jan 2025 12:38:52 +0100 Subject: [PATCH 29/57] fix(common): Update esp-docs dependencies to fix docs-build job --- docs/requirements.txt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 1b66e56c3f..2ef7186598 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1 @@ -sphinxcontrib-applehelp==1.0.4 -sphinxcontrib_devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.1 -sphinxcontrib-serializinghtml==1.1.5 -sphinxcontrib-qthelp==1.0.3 -breathe==4.35 -recommonmark==0.7.1 -esp-docs==1.7.1 +esp-docs>=2.0.0 From ae5a8ceedacd3d8cfe965f5fdc68980ca428ceaa Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 20 Jan 2025 12:56:02 +0100 Subject: [PATCH 30/57] fix(mosq): Run mosquitto version check only on labeled job or push --- .github/workflows/mosq__build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index 4534408b61..b4dddb25ec 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -75,6 +75,7 @@ jobs: done check_consistency: + if: contains(github.event.pull_request.labels.*.name, 'mosquitto') || github.event_name == 'push' name: Checks that API docs and versions are consistent runs-on: ubuntu-latest steps: From 84caca465df6408696c79811d48fabb2bb60a8db Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 20 Jan 2025 17:57:07 +0100 Subject: [PATCH 31/57] bump(mdns): 1.4.3 -> 1.5.0 1.5.0 Features - supported removal of subtype when updating service (4ad88e29) Bug Fixes - Fix zero-sized VLA clang-tidy warnings (196198ec) - Remove dead store to arg variable shared (e838bf03) - Fix name mangling not to use strcpy() (99b54ac3) - Fix potential null derefernce in _mdns_execute_action() (f5be2f41) - Fix AFL test mock per espressif/esp-idf@a5bc08fb55c (3d8835cf) - Fixed potential out-of-bound interface error (24f55ce9) - Fixed incorrect error conversion (8f8516cc) - Fixed potential overflow when allocating txt data (75a8e864) - Move MDNS_NAME_BUF_LEN to public headers (907087c0, #724) - Cleanup includes in mdns.c (68a9e148, #725) - Allow advertizing service with port==0 (827ea65f) - Fixed complier warning if MDNS_MAX_SERVICES==0 (95377216, #611) --- components/mdns/.cz.yaml | 2 +- components/mdns/CHANGELOG.md | 21 +++++++++++++++++++++ components/mdns/idf_component.yml | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/components/mdns/.cz.yaml b/components/mdns/.cz.yaml index a8390acf21..33938973ca 100644 --- a/components/mdns/.cz.yaml +++ b/components/mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.4.3 + version: 1.5.0 version_files: - idf_component.yml diff --git a/components/mdns/CHANGELOG.md b/components/mdns/CHANGELOG.md index 2fae369918..1740ab9063 100644 --- a/components/mdns/CHANGELOG.md +++ b/components/mdns/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [1.5.0](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.0) + +### Features + +- supported removal of subtype when updating service ([4ad88e29](https://github.com/espressif/esp-protocols/commit/4ad88e29)) + +### Bug Fixes + +- Fix zero-sized VLA clang-tidy warnings ([196198ec](https://github.com/espressif/esp-protocols/commit/196198ec)) +- Remove dead store to arg variable shared ([e838bf03](https://github.com/espressif/esp-protocols/commit/e838bf03)) +- Fix name mangling not to use strcpy() ([99b54ac3](https://github.com/espressif/esp-protocols/commit/99b54ac3)) +- Fix potential null derefernce in _mdns_execute_action() ([f5be2f41](https://github.com/espressif/esp-protocols/commit/f5be2f41)) +- Fix AFL test mock per espressif/esp-idf@a5bc08fb55c ([3d8835cf](https://github.com/espressif/esp-protocols/commit/3d8835cf)) +- Fixed potential out-of-bound interface error ([24f55ce9](https://github.com/espressif/esp-protocols/commit/24f55ce9)) +- Fixed incorrect error conversion ([8f8516cc](https://github.com/espressif/esp-protocols/commit/8f8516cc)) +- Fixed potential overflow when allocating txt data ([75a8e864](https://github.com/espressif/esp-protocols/commit/75a8e864)) +- Move MDNS_NAME_BUF_LEN to public headers ([907087c0](https://github.com/espressif/esp-protocols/commit/907087c0), [#724](https://github.com/espressif/esp-protocols/issues/724)) +- Cleanup includes in mdns.c ([68a9e148](https://github.com/espressif/esp-protocols/commit/68a9e148), [#725](https://github.com/espressif/esp-protocols/issues/725)) +- Allow advertizing service with port==0 ([827ea65f](https://github.com/espressif/esp-protocols/commit/827ea65f)) +- Fixed complier warning if MDNS_MAX_SERVICES==0 ([95377216](https://github.com/espressif/esp-protocols/commit/95377216), [#611](https://github.com/espressif/esp-protocols/issues/611)) + ## [1.4.3](https://github.com/espressif/esp-protocols/commits/mdns-v1.4.3) ### Features diff --git a/components/mdns/idf_component.yml b/components/mdns/idf_component.yml index 1e43ed704d..091fcba041 100644 --- a/components/mdns/idf_component.yml +++ b/components/mdns/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.4.3" +version: "1.5.0" description: "Multicast UDP service used to provide local network service and host discovery." url: "https://github.com/espressif/esp-protocols/tree/master/components/mdns" issues: "https://github.com/espressif/esp-protocols/issues" From 90d663ad01677e9bb02f6820f023d901ed8c2b5a Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 20 Dec 2024 18:12:57 +0100 Subject: [PATCH 32/57] feat(mosq): Add IDF MQTT stress tests to mosquitto CI --- .github/workflows/mosq__build.yml | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index b4dddb25ec..9e69a1ce24 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -101,3 +101,83 @@ jobs: exit 1 fi echo "Versions are consistent: $CONFIG_VERSION" + + build_idf_tests_with_mosq: + name: Build IDF tests + strategy: + matrix: + idf_ver: ["latest"] + idf_target: ["esp32"] + test: [ { app: publish, path: "tools/test_apps/protocols/mqtt/publish_connect_test" }] + runs-on: ubuntu-20.04 + container: espressif/idf:${{ matrix.idf_ver }} + env: + TARGET_TEST_DIR: build_esp32_local_broker + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build ${{ matrix.test.app }} with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }} + shell: bash + run: | + . ${IDF_PATH}/export.sh + pip install idf-component-manager idf-build-apps --upgrade + export OVERRIDE_PATH=`pwd`/components/mosquitto + echo ${OVERRIDE_PATH} + sed -i '/espressif\/mosquitto:/a \ \ \ \ override_path: "${OVERRIDE_PATH}"' ${IDF_PATH}/${{matrix.test.path}}/main/idf_component.yml + cat ${IDF_PATH}/${{matrix.test.path}}/main/idf_component.yml + export PEDANTIC_FLAGS="-DIDF_CI_BUILD -Werror -Werror=deprecated-declarations -Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function" + export EXTRA_CFLAGS="${PEDANTIC_FLAGS} -Wstrict-prototypes" + export EXTRA_CXXFLAGS="${PEDANTIC_FLAGS}" + cd ${IDF_PATH}/${{matrix.test.path}} + sed -i 's/4096, /5\*1024, /' main/publish_connect_test.c + cat main/publish_connect_test.c + idf-build-apps find --config sdkconfig.ci.local_broker -vv --target ${{ matrix.idf_target }} --build-dir=${TARGET_TEST_DIR} + idf-build-apps build --config sdkconfig.ci.local_broker -vv --target ${{ matrix.idf_target }} --build-dir=${TARGET_TEST_DIR} + ${GITHUB_WORKSPACE}/ci/clean_build_artifacts.sh `pwd`/${TARGET_TEST_DIR} + sed '/@pytest.mark.parametrize.*config.*/{ + s/@pytest.mark.parametrize.*config.*local_broker.*/@pytest.mark.protocols/ + t + d + }' pytest_mqtt_publish_app.py > ${TARGET_TEST_DIR}/pytest_local_mosq.py + cat ${TARGET_TEST_DIR}/pytest_local_mosq.py + zip -qur ${GITHUB_WORKSPACE}/artifacts.zip ${TARGET_TEST_DIR} + - uses: actions/upload-artifact@v4 + with: + name: mosq_publish_esp32_${{ matrix.idf_ver }} + path: artifacts.zip + if-no-files-found: error + + test_idf_ci_with_mosq: + # Skip running on forks since it won't have access to secrets + if: | + github.repository == 'espressif/esp-protocols' && + ( contains(github.event.pull_request.labels.*.name, 'mosquitto') || github.event_name == 'push' ) + name: Mosquitto IDF target tests + needs: build_idf_tests_with_mosq + strategy: + matrix: + idf_ver: ["latest"] + runs-on: + - self-hosted + - ESP32-ETHERNET-KIT + env: + TEST_DIR: examples + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: mosq_publish_esp32_${{ matrix.idf_ver }} + path: ${{ env.TEST_DIR }}/ci/ + - name: Run Test + working-directory: ${{ env.TEST_DIR }} + run: | + python -m pip install pytest-embedded-serial-esp pytest-embedded-idf pytest-rerunfailures pytest-timeout pytest-ignore-test-results "paho-mqtt<2" + unzip ci/artifacts.zip -d ci + for dir in `ls -d ci/build_*`; do + rm -rf build sdkconfig.defaults + mv $dir build + mv build/*.py . + python -m pytest --log-cli-level DEBUG --junit-xml=./results_esp32_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=esp32 -m protocols + done From dbd164dd91d73f9e66d1fb16f3756e55a21c6703 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 7 Jan 2025 08:52:38 +0100 Subject: [PATCH 33/57] fix(mosq): Add a note about stack size --- components/mosquitto/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mosquitto/README.md b/components/mosquitto/README.md index a6408db29a..3f8041982b 100644 --- a/components/mosquitto/README.md +++ b/components/mosquitto/README.md @@ -20,7 +20,7 @@ mosq_broker_run(&config); ## Memory Footprint Considerations -The broker primarily uses the heap for internal data, with minimal use of static/BSS memory. It consumes approximately 60 kB of program memory. +The broker primarily uses the heap for internal data, with minimal use of static/BSS memory. It consumes approximately 60 kB of program memory and minimum 5kB of stack size. - **Initial Memory Usage**: ~2 kB of heap on startup - **Per Client Memory Usage**: ~4 kB of heap for each connected client From 9162de1150d1ed8966647c90d59dafbd5e2d7f3d Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 8 Jan 2025 11:29:48 +0100 Subject: [PATCH 34/57] fix(mosq): Remove temp modification of IDF files --- .github/workflows/mosq__build.yml | 20 ++++----- components/mosquitto/test/README.md | 5 +++ .../mosquitto/test/replace_decorators.py | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 components/mosquitto/test/README.md create mode 100644 components/mosquitto/test/replace_decorators.py diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index 9e69a1ce24..5a12bc30f5 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -123,25 +123,18 @@ jobs: run: | . ${IDF_PATH}/export.sh pip install idf-component-manager idf-build-apps --upgrade - export OVERRIDE_PATH=`pwd`/components/mosquitto - echo ${OVERRIDE_PATH} - sed -i '/espressif\/mosquitto:/a \ \ \ \ override_path: "${OVERRIDE_PATH}"' ${IDF_PATH}/${{matrix.test.path}}/main/idf_component.yml - cat ${IDF_PATH}/${{matrix.test.path}}/main/idf_component.yml + export MOSQUITTO_PATH=`pwd`/components/mosquitto + # to use the actual version of mosquitto + sed -i '/espressif\/mosquitto:/a \ \ \ \ override_path: "${MOSQUITTO_PATH}"' ${IDF_PATH}/${{matrix.test.path}}/main/idf_component.yml export PEDANTIC_FLAGS="-DIDF_CI_BUILD -Werror -Werror=deprecated-declarations -Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function" export EXTRA_CFLAGS="${PEDANTIC_FLAGS} -Wstrict-prototypes" export EXTRA_CXXFLAGS="${PEDANTIC_FLAGS}" cd ${IDF_PATH}/${{matrix.test.path}} - sed -i 's/4096, /5\*1024, /' main/publish_connect_test.c - cat main/publish_connect_test.c idf-build-apps find --config sdkconfig.ci.local_broker -vv --target ${{ matrix.idf_target }} --build-dir=${TARGET_TEST_DIR} idf-build-apps build --config sdkconfig.ci.local_broker -vv --target ${{ matrix.idf_target }} --build-dir=${TARGET_TEST_DIR} ${GITHUB_WORKSPACE}/ci/clean_build_artifacts.sh `pwd`/${TARGET_TEST_DIR} - sed '/@pytest.mark.parametrize.*config.*/{ - s/@pytest.mark.parametrize.*config.*local_broker.*/@pytest.mark.protocols/ - t - d - }' pytest_mqtt_publish_app.py > ${TARGET_TEST_DIR}/pytest_local_mosq.py - cat ${TARGET_TEST_DIR}/pytest_local_mosq.py + # to replace mqtt test configs with specific mosquitto markers + python ${MOSQUITTO_PATH}/test/replace_decorators.py pytest_mqtt_publish_app.py ${TARGET_TEST_DIR}/pytest_mosquitto.py zip -qur ${GITHUB_WORKSPACE}/artifacts.zip ${TARGET_TEST_DIR} - uses: actions/upload-artifact@v4 with: @@ -179,5 +172,6 @@ jobs: rm -rf build sdkconfig.defaults mv $dir build mv build/*.py . - python -m pytest --log-cli-level DEBUG --junit-xml=./results_esp32_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=esp32 -m protocols + # Run only "test_mosquitto" marked tests + python -m pytest --log-cli-level DEBUG --junit-xml=./results_esp32_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=esp32 -m test_mosquitto done diff --git a/components/mosquitto/test/README.md b/components/mosquitto/test/README.md new file mode 100644 index 0000000000..eef6b4ea51 --- /dev/null +++ b/components/mosquitto/test/README.md @@ -0,0 +1,5 @@ +# ESP32 mosquitto tests + +Mosquitto component doesn't have any tests yet, but we upcycle IDF mqtt tests and run them with the current version of mosquitto. +For that we need to update the IDF test project's `idf_component.yml` file to reference this actual version of mosquitto. +We also need to update some pytest decorators to run only relevant test cases. See the [replacement](./replace_decorators.py) script. diff --git a/components/mosquitto/test/replace_decorators.py b/components/mosquitto/test/replace_decorators.py new file mode 100644 index 0000000000..6c842bedd4 --- /dev/null +++ b/components/mosquitto/test/replace_decorators.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 + +# This script replaces the `@pytest` decorators in the test files +# based on the value of the `config` parameter in the `@pytest` decorator. +# to reuse mqtt test cases for mosquitto broker. + +import re +import sys + + +def replace_decorators(in_file: str, out_file: str) -> None: + with open(in_file, 'r') as file: + content = file.read() + + # we replace config decorators to differentiate between local mosquitto based tests + pattern = r"@pytest\.mark\.parametrize\(\s*'config'\s*,\s*\[\s*'(.*?)'\s*\]\s*,.*\)" + + def replacement(match): + config_value = match.group(1) + if config_value == 'local_broker': + return '@pytest.mark.test_mosquitto' + else: + return '@pytest.mark.test_mqtt' + + # Replace occurrences + updated_content = re.sub(pattern, replacement, content) + + with open(out_file, 'w') as file: + file.write(updated_content) + + +# Main function to handle arguments +if __name__ == '__main__': + if len(sys.argv) != 3: + print('Usage: python replace_decorators.py ') + sys.exit(1) + + in_file = sys.argv[1] + out_file = sys.argv[2] + replace_decorators(in_file, out_file) + print(f'Replacements completed') From 4451a8c5add0da540a7512a482ae105c3679cb50 Mon Sep 17 00:00:00 2001 From: Xu Si Yu Date: Wed, 22 Jan 2025 19:33:02 +0800 Subject: [PATCH 35/57] fix(mdns): Fix incorrect memory free for mdns browse --- components/mdns/mdns.c | 1 + 1 file changed, 1 insertion(+) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 49574dfbd7..903cc02b4c 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -4298,6 +4298,7 @@ void mdns_parse_packet(mdns_rx_packet_t *packet) } else { free(out_sync_browse); } + out_sync_browse = NULL; } clear_rx_packet: From 96eae250962f7ed2ac9eb896e28b26a241c9b667 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 23 Jan 2025 09:00:01 +0100 Subject: [PATCH 36/57] bump(mdns): 1.5.0 -> 1.5.1 1.5.1 Bug Fixes - Fix incorrect memory free for mdns browse (4451a8c5) --- components/mdns/.cz.yaml | 2 +- components/mdns/CHANGELOG.md | 6 ++++++ components/mdns/idf_component.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/mdns/.cz.yaml b/components/mdns/.cz.yaml index 33938973ca..3eb7ee67cb 100644 --- a/components/mdns/.cz.yaml +++ b/components/mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.5.0 + version: 1.5.1 version_files: - idf_component.yml diff --git a/components/mdns/CHANGELOG.md b/components/mdns/CHANGELOG.md index 1740ab9063..321e488ff9 100644 --- a/components/mdns/CHANGELOG.md +++ b/components/mdns/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.5.1](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.1) + +### Bug Fixes + +- Fix incorrect memory free for mdns browse ([4451a8c5](https://github.com/espressif/esp-protocols/commit/4451a8c5)) + ## [1.5.0](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.0) ### Features diff --git a/components/mdns/idf_component.yml b/components/mdns/idf_component.yml index 091fcba041..d4127e80d4 100644 --- a/components/mdns/idf_component.yml +++ b/components/mdns/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.5.0" +version: "1.5.1" description: "Multicast UDP service used to provide local network service and host discovery." url: "https://github.com/espressif/esp-protocols/tree/master/components/mdns" issues: "https://github.com/espressif/esp-protocols/issues" From 4eda7d472f8049130dba3b513f61e9836c79bd47 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 23 Jan 2025 18:10:08 +0100 Subject: [PATCH 37/57] fix(modem): Fix deprecated download action --- .github/workflows/modem__target-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/modem__target-test.yml b/.github/workflows/modem__target-test.yml index 33917430bb..e2a6639580 100644 --- a/.github/workflows/modem__target-test.yml +++ b/.github/workflows/modem__target-test.yml @@ -42,7 +42,7 @@ jobs: idf.py set-target ${{ matrix.idf_target }} idf.py build $GITHUB_WORKSPACE/ci/clean_build_artifacts.sh ${GITHUB_WORKSPACE}/${TEST_DIR}/build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: modem_target_bin_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${{ matrix.test.app }} path: ${{ env.TEST_DIR }}/build @@ -75,7 +75,7 @@ jobs: run: | sudo rm -fr $GITHUB_WORKSPACE && mkdir $GITHUB_WORKSPACE - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: modem_target_bin_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${{ matrix.test.app }} path: ${{ env.TEST_DIR }}/build From bd23c233a4e8a07c7737013e6f4ad75434633637 Mon Sep 17 00:00:00 2001 From: Andrew Chalmers Date: Fri, 24 Jan 2025 17:39:04 +1030 Subject: [PATCH 38/57] fix(mdns): Fix _mdns_append_fqdn excessive stack usage Move "mdns_name_t name" declaration inside loop to move it out of scope and reclain stack space before recursion call. --- components/mdns/mdns.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 903cc02b4c..f5f12cc136 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -751,12 +751,12 @@ static uint16_t _mdns_append_fqdn(uint8_t *packet, uint16_t *index, const char * //empty string so terminate return _mdns_append_u8(packet, index, 0); } - mdns_name_t name; static char buf[MDNS_NAME_BUF_LEN]; uint8_t len = strlen(strings[0]); //try to find first the string length in the packet (if it exists) uint8_t *len_location = (uint8_t *)memchr(packet, (char)len, *index); while (len_location) { + mdns_name_t name; //check if the string after len_location is the string that we are looking for if (memcmp(len_location + 1, strings[0], len)) { //not continuing with our string search_next: From 27435b7f3423aaf6beb43d3668270ad9ddaa5833 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 18 Dec 2024 11:20:10 +0100 Subject: [PATCH 39/57] feat(asio): Drop esp/asio patches in favor of sock-utils --- .gitmodules | 2 +- components/asio/CMakeLists.txt | 7 ++-- components/asio/asio | 2 +- components/asio/idf_component.yml | 2 ++ .../asio/port/include/asio/detail/config.hpp | 11 ++++++ .../ssl/detail/openssl_types.hpp} | 0 .../asio/port/include/esp_asio_config.h | 12 ------- .../asio/port/mbedtls/src/mbedtls_context.cpp | 2 +- .../asio/port/mbedtls/src/mbedtls_engine.cpp | 2 +- components/asio/port/src/asio_stub.cpp | 36 +++++++++++++++++++ 10 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 components/asio/port/include/asio/detail/config.hpp rename components/asio/port/include/{openssl_stub.hpp => asio/ssl/detail/openssl_types.hpp} (100%) delete mode 100644 components/asio/port/include/esp_asio_config.h create mode 100644 components/asio/port/src/asio_stub.cpp diff --git a/.gitmodules b/.gitmodules index 60b3730713..391f572334 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "components/asio/asio"] path = components/asio/asio - url = https://github.com/espressif/asio + url = https://github.com/chriskohlhoff/asio [submodule "components/mosquitto/mosquitto"] path = components/mosquitto/mosquitto url = https://github.com/eclipse/mosquitto diff --git a/components/asio/CMakeLists.txt b/components/asio/CMakeLists.txt index 3ec69bac48..8aebfc71bc 100644 --- a/components/asio/CMakeLists.txt +++ b/components/asio/CMakeLists.txt @@ -6,8 +6,8 @@ if(NOT CONFIG_LWIP_IPV6 AND NOT CMAKE_BUILD_EARLY_EXPANSION) return() endif() -set(asio_sources "asio/asio/src/asio.cpp") -set(asio_requires lwip) +set(asio_sources "asio/asio/src/asio.cpp" "port/src/asio_stub.cpp") +set(asio_requires lwip sock_utils) if(CONFIG_ASIO_SSL_SUPPORT) list(APPEND asio_sources @@ -18,7 +18,7 @@ if(CONFIG_ASIO_SSL_SUPPORT) endif() idf_component_register(SRCS ${asio_sources} - INCLUDE_DIRS "asio/asio/include" "port/include" + INCLUDE_DIRS "port/include" "asio/asio/include" PRIV_INCLUDE_DIRS ${asio_priv_includes} PRIV_REQUIRES ${asio_requires}) @@ -30,6 +30,7 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC SA_RESTART=0x01 ASIO_STANDALONE ASIO_HAS_PTHREADS OPENSSL_NO_ENGINE + ASIO_DETAIL_IMPL_POSIX_EVENT_IPP # this replaces asio's posix_event constructor ) if(NOT CONFIG_COMPILER_CXX_EXCEPTIONS) diff --git a/components/asio/asio b/components/asio/asio index a2e0f70d61..7609450f71 160000 --- a/components/asio/asio +++ b/components/asio/asio @@ -1 +1 @@ -Subproject commit a2e0f70d612309f4623bd43d8a26629bd716bb2c +Subproject commit 7609450f71434bdc9fbd9491a9505b423c2a8496 diff --git a/components/asio/idf_component.yml b/components/asio/idf_component.yml index a54601f0f9..3708c85107 100644 --- a/components/asio/idf_component.yml +++ b/components/asio/idf_component.yml @@ -7,3 +7,5 @@ repository: https://github.com/espressif/esp-protocols.git dependencies: idf: version: ">=5.0" + espressif/sock_utils: + version: "^0.1" diff --git a/components/asio/port/include/asio/detail/config.hpp b/components/asio/port/include/asio/detail/config.hpp new file mode 100644 index 0000000000..288fe08b8a --- /dev/null +++ b/components/asio/port/include/asio/detail/config.hpp @@ -0,0 +1,11 @@ +// +// SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "sys/socket.h" +#include "socketpair.h" + +#include_next "asio/detail/config.hpp" diff --git a/components/asio/port/include/openssl_stub.hpp b/components/asio/port/include/asio/ssl/detail/openssl_types.hpp similarity index 100% rename from components/asio/port/include/openssl_stub.hpp rename to components/asio/port/include/asio/ssl/detail/openssl_types.hpp diff --git a/components/asio/port/include/esp_asio_config.h b/components/asio/port/include/esp_asio_config.h deleted file mode 100644 index 2ad9c79c76..0000000000 --- a/components/asio/port/include/esp_asio_config.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef _ESP_ASIO_CONFIG_H_ -#define _ESP_ASIO_CONFIG_H_ - -#define ASIO_SSL_DETAIL_OPENSSL_TYPES_HPP -#include "openssl_stub.hpp" - -#endif // _ESP_ASIO_CONFIG_H_ diff --git a/components/asio/port/mbedtls/src/mbedtls_context.cpp b/components/asio/port/mbedtls/src/mbedtls_context.cpp index bf9220ad59..4b3e2f4189 100644 --- a/components/asio/port/mbedtls/src/mbedtls_context.cpp +++ b/components/asio/port/mbedtls/src/mbedtls_context.cpp @@ -8,7 +8,7 @@ // #include "asio/detail/config.hpp" -#include "openssl_stub.hpp" +#include "asio/ssl/detail/openssl_types.hpp" #include #include "asio/detail/throw_error.hpp" #include "asio/error.hpp" diff --git a/components/asio/port/mbedtls/src/mbedtls_engine.cpp b/components/asio/port/mbedtls/src/mbedtls_engine.cpp index 1d97b3a738..1c03a32841 100644 --- a/components/asio/port/mbedtls/src/mbedtls_engine.cpp +++ b/components/asio/port/mbedtls/src/mbedtls_engine.cpp @@ -7,7 +7,7 @@ // #include "asio/detail/config.hpp" -#include "openssl_stub.hpp" +#include "asio/ssl/detail/openssl_types.hpp" #include "asio/detail/throw_error.hpp" #include "asio/error.hpp" #include "asio/ssl/detail/engine.hpp" diff --git a/components/asio/port/src/asio_stub.cpp b/components/asio/port/src/asio_stub.cpp new file mode 100644 index 0000000000..ebc0c93f6c --- /dev/null +++ b/components/asio/port/src/asio_stub.cpp @@ -0,0 +1,36 @@ +// +// SPDX-FileCopyrightText: 2003-2023 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// SPDX-License-Identifier: BSL-1.0 +// +// SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD +// +#include "asio/detail/posix_event.hpp" +#include "asio/detail/throw_error.hpp" +#include "asio/error.hpp" +#include "asio/detail/push_options.hpp" +#include +#include + +namespace asio::detail { +// This replaces asio's posix_event constructor +// since the default POSIX version uses pthread_condattr_t operations (init, setclock, destroy), +// which are not available on all IDF versions (some are defined in compilers' headers, others in +// pthread library, but they typically return `ENOSYS` which causes trouble in the event wrapper) +// IMPORTANT: Check implementation of posix_event() when upgrading upstream asio in order not to +// miss any initialization step. +posix_event::posix_event() + : state_(0) +{ + int error = ::pthread_cond_init(&cond_, nullptr); + asio::error_code ec(error, asio::error::get_system_category()); + asio::detail::throw_error(ec, "event"); +} +} // namespace asio::detail + +extern "C" int pause (void) +{ + while (true) { + ::sleep(UINT_MAX); + } +} From 6f7c52cc3f1685cd65924a6612afaf76b58342d1 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 18 Dec 2024 16:07:38 +0100 Subject: [PATCH 40/57] fix(asio): Add tests for IDF-v5.4 --- .github/workflows/asio__build-target-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/asio__build-target-test.yml b/.github/workflows/asio__build-target-test.yml index a8d70d6566..b7d0e1556a 100644 --- a/.github/workflows/asio__build-target-test.yml +++ b/.github/workflows/asio__build-target-test.yml @@ -13,7 +13,7 @@ jobs: name: Build strategy: matrix: - idf_ver: ["latest", "release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3"] + idf_ver: ["latest", "release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4"] idf_target: ["esp32", "esp32s2"] example: ["asio_chat", "async_request", "socks4", "ssl_client_server", "tcp_echo_server", "udp_echo_server"] runs-on: ubuntu-22.04 @@ -64,7 +64,7 @@ jobs: name: Target tests strategy: matrix: - idf_ver: ["latest", "release-v5.0", "release-v5.2", "release-v5.3"] + idf_ver: ["latest", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4"] idf_target: ["esp32"] example: ["asio_chat", "tcp_echo_server", "udp_echo_server", "ssl_client_server"] needs: build_asio From 9bdd429c7ca74682db6c84beebb37e9eb8526b4e Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 18 Dec 2024 16:59:11 +0100 Subject: [PATCH 41/57] feat(asio): Upgrade asio to 1.32 --- components/asio/asio | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/asio/asio b/components/asio/asio index 7609450f71..03ae834edb 160000 --- a/components/asio/asio +++ b/components/asio/asio @@ -1 +1 @@ -Subproject commit 7609450f71434bdc9fbd9491a9505b423c2a8496 +Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9 From 5db32cce305d60560c9ef3fc4fa69227b2f330dd Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 18 Dec 2024 18:08:42 +0100 Subject: [PATCH 42/57] fix(asio): Make asio enable if_nametoindex to fix linking --- components/asio/Kconfig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/asio/Kconfig b/components/asio/Kconfig index 8b46ae0652..b8a30b7d3e 100644 --- a/components/asio/Kconfig +++ b/components/asio/Kconfig @@ -1,6 +1,15 @@ menu "ESP-ASIO" visible if LWIP_IPV6 + config ASIO_IS_ENABLED + # Invisible option that is enabled if ASIO is added to the IDF components. + # This is used to "select" LWIP_NETIF_API option + # which enables if_indextoname() and if_nametoindex() functions + # (these are optionally used in asio) + bool + default "y" + select LWIP_NETIF_API + config ASIO_SSL_SUPPORT bool "Enable SSL/TLS support of ASIO" default n From 76aaea08d280e9f16da928a9dcf818eb5a5ac0f7 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 24 Jan 2025 11:39:25 +0100 Subject: [PATCH 43/57] fix(asio): Fix chat example to print only the message body --- components/asio/examples/asio_chat/main/server.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/asio/examples/asio_chat/main/server.hpp b/components/asio/examples/asio_chat/main/server.hpp index 971c19d2f9..af2c4a47f2 100644 --- a/components/asio/examples/asio_chat/main/server.hpp +++ b/components/asio/examples/asio_chat/main/server.hpp @@ -120,7 +120,7 @@ class chat_session asio::buffer(read_msg_.body(), read_msg_.body_length()), [this, self](std::error_code ec, std::size_t /*length*/) { if (!ec) { - ESP_LOGD("asio-chat:", "%s", read_msg_.body()); + ESP_LOGD("asio-chat", "%.*s", read_msg_.body_length(), read_msg_.body()); room_.deliver(read_msg_); do_read_header(); } else { From ac6a388cdd7f31e113faa9cf132b8c57f1ff4df4 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 24 Jan 2025 12:40:45 +0100 Subject: [PATCH 44/57] bump(asio): 1.28.0 -> 1.32.0 1.32.0 Features - Upgrade asio to 1.32 (9bdd429c) - Drop esp/asio patches in favor of sock-utils (27435b7f) Bug Fixes - Fix chat example to print only the message body (76aaea08) - Make asio enable if_nametoindex to fix linking (5db32cce) - Re-applie refs to common comps idf_component.yml (9fe44a45) - Reference common component from IDF (74fc228c) - Revert referencing protocol_examples_common from IDF (f9e0281a) - reference protocol_examples_common from IDF (09abb18b) - specify override_path in example manifest files (1d8923cf) Updated - docs(asio): Updates asio docs (ce9337d3) --- components/asio/.cz.yaml | 3 ++- components/asio/CHANGELOG.md | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/components/asio/.cz.yaml b/components/asio/.cz.yaml index a993a13894..f48d4ec790 100644 --- a/components/asio/.cz.yaml +++ b/components/asio/.cz.yaml @@ -1,7 +1,8 @@ +--- commitizen: bump_message: 'bump(asio): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py asio tag_format: asio-v$version - version: 1.28.0~0 + version: 1.32.0 version_files: - idf_component.yml diff --git a/components/asio/CHANGELOG.md b/components/asio/CHANGELOG.md index 06d4dbae21..48688264ed 100644 --- a/components/asio/CHANGELOG.md +++ b/components/asio/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [1.32.0](https://github.com/espressif/esp-protocols/commits/asio-v1.32.0) + +### Features + +- Upgrade asio to 1.32 ([9bdd429c](https://github.com/espressif/esp-protocols/commit/9bdd429c)) +- Drop esp/asio patches in favor of sock-utils ([27435b7f](https://github.com/espressif/esp-protocols/commit/27435b7f)) + +### Bug Fixes + +- Fix chat example to print only the message body ([76aaea08](https://github.com/espressif/esp-protocols/commit/76aaea08)) +- Make asio enable if_nametoindex to fix linking ([5db32cce](https://github.com/espressif/esp-protocols/commit/5db32cce)) +- Re-applie refs to common comps idf_component.yml ([9fe44a45](https://github.com/espressif/esp-protocols/commit/9fe44a45)) +- Reference common component from IDF ([74fc228c](https://github.com/espressif/esp-protocols/commit/74fc228c)) +- Revert referencing protocol_examples_common from IDF ([f9e0281a](https://github.com/espressif/esp-protocols/commit/f9e0281a)) +- reference protocol_examples_common from IDF ([09abb18b](https://github.com/espressif/esp-protocols/commit/09abb18b)) +- specify override_path in example manifest files ([1d8923cf](https://github.com/espressif/esp-protocols/commit/1d8923cf)) + +### Updated + +- docs(asio): Updates asio docs ([ce9337d3](https://github.com/espressif/esp-protocols/commit/ce9337d3)) + ## [1.28.2~0](https://github.com/espressif/esp-protocols/commits/asio-1.28.2_0) ### Bug Fixes From 72ba24470de57347e6e3dae9ff8cd41fa09407ed Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 24 Jan 2025 14:32:01 +0100 Subject: [PATCH 45/57] bump(asio): Publish 1.32.0 to component manager Related to the bump-commit: ac6a388cdd7f31e113faa9cf132b8c57f1ff4df4 but missed to update idf_component.yml This publishes ASIO-1.32-0~0 --- components/asio/idf_component.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/asio/idf_component.yml b/components/asio/idf_component.yml index 3708c85107..63904337bd 100644 --- a/components/asio/idf_component.yml +++ b/components/asio/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.28.2~0" +version: "1.32.0" description: Cross-platform C++ library for network and I/O programming url: https://github.com/espressif/esp-protocols/tree/master/components/asio issues: https://github.com/espressif/esp-protocols/issues From eeeb9006ebc1487ea78b113907689e5482806f97 Mon Sep 17 00:00:00 2001 From: Johan Stokking Date: Tue, 12 Nov 2024 13:46:22 +0100 Subject: [PATCH 46/57] fix(websocket): propagate error type --- components/esp_websocket_client/esp_websocket_client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esp_websocket_client/esp_websocket_client.c b/components/esp_websocket_client/esp_websocket_client.c index 8f3eb106ed..c39fe47c5d 100644 --- a/components/esp_websocket_client/esp_websocket_client.c +++ b/components/esp_websocket_client/esp_websocket_client.c @@ -234,9 +234,9 @@ static esp_err_t esp_websocket_client_abort_connection(esp_websocket_client_hand } else { client->reconnect_tick_ms = _tick_get_ms(); ESP_LOGI(TAG, "Reconnect after %d ms", client->wait_timeout_ms); - client->error_handle.error_type = error_type; client->state = WEBSOCKET_STATE_WAIT_TIMEOUT; } + client->error_handle.error_type = error_type; esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_DISCONNECTED, NULL, 0); return ESP_OK; } From 9046af8f8d1a4a9262412b375a8175fd958e1b99 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 27 Jan 2025 15:31:10 +0100 Subject: [PATCH 47/57] fix(websocket): Fix pytest to verify client correctly --- .../esp_websocket_client/examples/target/pytest_websocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esp_websocket_client/examples/target/pytest_websocket.py b/components/esp_websocket_client/examples/target/pytest_websocket.py index 8e23b1b9d8..dd59384d44 100644 --- a/components/esp_websocket_client/examples/target/pytest_websocket.py +++ b/components/esp_websocket_client/examples/target/pytest_websocket.py @@ -55,7 +55,7 @@ def run(self): ssl_context.load_cert_chain(certfile='main/certs/server/server_cert.pem', keyfile='main/certs/server/server_key.pem') if self.client_verify is True: ssl_context.load_verify_locations(cafile='main/certs/ca_cert.pem') - ssl_context.verify = ssl.CERT_REQUIRED + ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = False self.server = SimpleSSLWebSocketServer('', self.port, WebsocketTestEcho, ssl_context=ssl_context) else: From e7273c46ecbce8874ce2590040f31be3300ffe38 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 24 Jan 2025 15:20:11 +0100 Subject: [PATCH 48/57] fix(mdns): Fix potential NULL deref when sending sub-buy Closes coverity reported issue: 473829 Dereference null return value --- components/mdns/mdns.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index f5f12cc136..12539a4352 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -2399,6 +2399,9 @@ static void _mdns_send_bye_subtype(mdns_srv_item_t *service, const char *instanc for (j = 0; j < MDNS_IP_PROTOCOL_MAX; j++) { if (mdns_is_netif_ready(i, j)) { mdns_tx_packet_t *packet = _mdns_alloc_packet_default((mdns_if_t)i, (mdns_ip_protocol_t)j); + if (packet == NULL) { + return; + } packet->flags = MDNS_FLAGS_QR_AUTHORITATIVE; if (!_mdns_alloc_answer(&packet->answers, MDNS_TYPE_PTR, service->service, NULL, true, true)) { _mdns_free_tx_packet(packet); From 84c47c37f15bf0b2b92eef99d8359979e2c44edf Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 28 Jan 2025 09:54:54 +0100 Subject: [PATCH 49/57] bump(mdns): 1.5.1 -> 1.5.2 1.5.2 Bug Fixes - Fix potential NULL deref when sending sub-buy (e7273c46) - Fix _mdns_append_fqdn excessive stack usage (bd23c233) --- components/mdns/.cz.yaml | 2 +- components/mdns/CHANGELOG.md | 7 +++++++ components/mdns/idf_component.yml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/components/mdns/.cz.yaml b/components/mdns/.cz.yaml index 3eb7ee67cb..3ec98354e1 100644 --- a/components/mdns/.cz.yaml +++ b/components/mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.5.1 + version: 1.5.2 version_files: - idf_component.yml diff --git a/components/mdns/CHANGELOG.md b/components/mdns/CHANGELOG.md index 321e488ff9..1522ca3725 100644 --- a/components/mdns/CHANGELOG.md +++ b/components/mdns/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.5.2](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.2) + +### Bug Fixes + +- Fix potential NULL deref when sending sub-buy ([e7273c46](https://github.com/espressif/esp-protocols/commit/e7273c46)) +- Fix _mdns_append_fqdn excessive stack usage ([bd23c233](https://github.com/espressif/esp-protocols/commit/bd23c233)) + ## [1.5.1](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.1) ### Bug Fixes diff --git a/components/mdns/idf_component.yml b/components/mdns/idf_component.yml index d4127e80d4..ea4e511a90 100644 --- a/components/mdns/idf_component.yml +++ b/components/mdns/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.5.1" +version: "1.5.2" description: "Multicast UDP service used to provide local network service and host discovery." url: "https://github.com/espressif/esp-protocols/tree/master/components/mdns" issues: "https://github.com/espressif/esp-protocols/issues" From 55385ec312984d3ccd26995259b0b8297ff63365 Mon Sep 17 00:00:00 2001 From: Johan Stokking Date: Tue, 28 Jan 2025 14:57:56 +0100 Subject: [PATCH 50/57] feat(websocket): Support DS peripheral for mutual TLS --- .../esp_websocket_client.c | 10 +++++++ .../include/esp_websocket_client.h | 7 +++-- docs/esp_websocket_client/en/index.rst | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/components/esp_websocket_client/esp_websocket_client.c b/components/esp_websocket_client/esp_websocket_client.c index 8f3eb106ed..b76a6aa4c5 100644 --- a/components/esp_websocket_client/esp_websocket_client.c +++ b/components/esp_websocket_client/esp_websocket_client.c @@ -93,6 +93,9 @@ typedef struct { size_t client_cert_len; const char *client_key; size_t client_key_len; +#if CONFIG_ESP_TLS_USE_DS_PERIPHERAL + void *client_ds_data; +#endif bool use_global_ca_store; bool skip_cert_common_name_check; const char *cert_common_name; @@ -531,6 +534,10 @@ static esp_err_t esp_websocket_client_create_transport(esp_websocket_client_hand } else { esp_transport_ssl_set_client_key_data_der(ssl, client->config->client_key, client->config->client_key_len); } +#if CONFIG_ESP_TLS_USE_DS_PERIPHERAL + } else if (client->config->client_ds_data) { + esp_transport_ssl_set_ds_data(ssl, client->config->client_ds_data); +#endif } if (client->config->crt_bundle_attach) { #ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE @@ -696,6 +703,9 @@ esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_clie client->config->client_cert_len = config->client_cert_len; client->config->client_key = config->client_key; client->config->client_key_len = config->client_key_len; +#if CONFIG_ESP_TLS_USE_DS_PERIPHERAL + client->config->client_ds_data = config->client_ds_data; +#endif client->config->skip_cert_common_name_check = config->skip_cert_common_name_check; client->config->cert_common_name = config->cert_common_name; client->config->crt_bundle_attach = config->crt_bundle_attach; diff --git a/components/esp_websocket_client/include/esp_websocket_client.h b/components/esp_websocket_client/include/esp_websocket_client.h index 15fb63b7ca..07093119dd 100644 --- a/components/esp_websocket_client/include/esp_websocket_client.h +++ b/components/esp_websocket_client/include/esp_websocket_client.h @@ -108,10 +108,13 @@ typedef struct { int buffer_size; /*!< Websocket buffer size */ const char *cert_pem; /*!< Pointer to certificate data in PEM or DER format for server verify (with SSL), default is NULL, not required to verify the server. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in cert_len. */ size_t cert_len; /*!< Length of the buffer pointed to by cert_pem. May be 0 for null-terminated pem */ - const char *client_cert; /*!< Pointer to certificate data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_key` has to be provided. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_cert_len. */ + const char *client_cert; /*!< Pointer to certificate data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_key` or `client_ds_data` (if supported) has to be provided. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_cert_len. */ size_t client_cert_len; /*!< Length of the buffer pointed to by client_cert. May be 0 for null-terminated pem */ - const char *client_key; /*!< Pointer to private key data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_cert` has to be provided. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_key_len */ + const char *client_key; /*!< Pointer to private key data in PEM or DER format for SSL mutual authentication, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_cert` has to be provided and `client_ds_data` (if supported) gets ignored. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in client_key_len */ size_t client_key_len; /*!< Length of the buffer pointed to by client_key_pem. May be 0 for null-terminated pem */ +#if CONFIG_ESP_TLS_USE_DS_PERIPHERAL + void *client_ds_data; /*!< Pointer to the encrypted private key data for SSL mutual authentication using the DS peripheral, default is NULL, not required if mutual authentication is not needed. If it is not NULL, also `client_cert` has to be provided. It is ignored if `client_key` is provided */ +#endif esp_websocket_transport_t transport; /*!< Websocket transport type, see `esp_websocket_transport_t */ const char *subprotocol; /*!< Websocket subprotocol */ const char *user_agent; /*!< Websocket user-agent */ diff --git a/docs/esp_websocket_client/en/index.rst b/docs/esp_websocket_client/en/index.rst index c4444d43c5..e73ff394a4 100644 --- a/docs/esp_websocket_client/en/index.rst +++ b/docs/esp_websocket_client/en/index.rst @@ -73,6 +73,34 @@ echo "" | openssl s_client -showcerts -connect websocket.org:443 | sed -n "1,/Ro This command will extract the second certificate in the chain and save it as a pem-file. +Mutual TLS with DS Peripheral +""""""""""""""""""""""""""""" + +To leverage the Digital Signature (DS) peripheral on supported targets, use `esp_secure_cert_mgr `_ to flash an encrypted client certificate. In your project, add the dependency: :: + + idf.py add-dependency esp_secure_cert_mgr + +Set ``client_cert`` and ``client_ds_data`` in the config struct: + +.. code:: c + + char *client_cert = NULL; + uint32_t client_cert_len = 0; + esp_err_t err = esp_secure_cert_get_device_cert(&client_cert, &client_cert_len); + assert(err == ESP_OK); + + esp_ds_data_ctx_t *ds_data = esp_secure_cert_get_ds_ctx(); + assert(ds_data != NULL); + + esp_websocket_client_config_t config = { + .uri = "wss://echo.websocket.org", + .cert_pem = (const char *)websocket_org_pem_start, + .client_cert = client_cert, + .client_ds_data = ds_data, + }; + +.. note:: ``client_cert`` provided by `esp_secure_cert_mgr` is a null-terminated PEM; so ``client_cert_len`` (DER format) should not be set. + Subprotocol ^^^^^^^^^^^ From 44d476fc50a9a4afc8eea9d619a0c12930d130d3 Mon Sep 17 00:00:00 2001 From: Johan Stokking Date: Tue, 28 Jan 2025 14:59:24 +0100 Subject: [PATCH 51/57] docs(websocket): fix minor readability issues --- docs/esp_websocket_client/en/index.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/esp_websocket_client/en/index.rst b/docs/esp_websocket_client/en/index.rst index e73ff394a4..e904256a08 100644 --- a/docs/esp_websocket_client/en/index.rst +++ b/docs/esp_websocket_client/en/index.rst @@ -66,10 +66,12 @@ Configuration: .. note:: If you want to verify the server, then you need to provide a certificate in PEM format, and provide to ``cert_pem`` in :cpp:type:`websocket_client_config_t`. If no certficate is provided then the TLS connection will default to not requiring verification. PEM certificate for this example could be extracted from an openssl `s_client` command connecting to websocket.org. -In case a host operating system has `openssl` and `sed` packages installed, one could execute the following command to download and save the root or intermediate root certificate to a file (Note for Windows users: Both Linux like environment or Windows native packages may be used). -``` -echo "" | openssl s_client -showcerts -connect websocket.org:443 | sed -n "1,/Root/d; /BEGIN/,/END/p" | openssl x509 -outform PEM >websocket_org.pem -``` +In case a host operating system has `openssl` and `sed` packages installed, one could execute the following command to download and save the root or intermediate root certificate to a file (Note for Windows users: Both Linux like environment or Windows native packages may be used). :: + + echo "" | openssl s_client -showcerts -connect websocket.org:443 \ + | sed -n "1,/Root/d; /BEGIN/,/END/p" \ + | openssl x509 -outform PEM \ + > websocket_org.pem This command will extract the second certificate in the chain and save it as a pem-file. @@ -119,14 +121,14 @@ For more options on :cpp:type:`esp_websocket_client_config_t`, please refer to A Events ------ -* `WEBSOCKET_EVENT_BEGIN': The client thread is running. +* `WEBSOCKET_EVENT_BEGIN`: The client thread is running. * `WEBSOCKET_EVENT_BEFORE_CONNECT`: The client is about to connect. * `WEBSOCKET_EVENT_CONNECTED`: The client has successfully established a connection to the server. The client is now ready to send and receive data. Contains no event data. * `WEBSOCKET_EVENT_DATA`: The client has successfully received and parsed a WebSocket frame. The event data contains a pointer to the payload data, the length of the payload data as well as the opcode of the received frame. A message may be fragmented into multiple events if the length exceeds the buffer size. This event will also be posted for non-payload frames, e.g. pong or connection close frames. * `WEBSOCKET_EVENT_ERROR`: The client has experienced an error. Examples include transport write or read failures. * `WEBSOCKET_EVENT_DISCONNECTED`: The client has aborted the connection due to the transport layer failing to read data, e.g. because the server is unavailable. Contains no event data. * `WEBSOCKET_EVENT_CLOSED`: The connection has been closed cleanly. -* `WEBSOCKET_EVENT_FINISH': The client thread is about to exit. +* `WEBSOCKET_EVENT_FINISH`: The client thread is about to exit. If the client handle is needed in the event handler it can be accessed through the pointer passed to the event handler: From 42674b49f9d8028f199a10705a6cbf5b3f913499 Mon Sep 17 00:00:00 2001 From: Johan Stokking Date: Tue, 28 Jan 2025 17:19:30 +0100 Subject: [PATCH 52/57] fix(websocket): wait for task on destroy --- .../esp_websocket_client.c | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/components/esp_websocket_client/esp_websocket_client.c b/components/esp_websocket_client/esp_websocket_client.c index 80c397dec4..6ef0818365 100644 --- a/components/esp_websocket_client/esp_websocket_client.c +++ b/components/esp_websocket_client/esp_websocket_client.c @@ -449,6 +449,21 @@ static void destroy_and_free_resources(esp_websocket_client_handle_t client) client = NULL; } +static esp_err_t stop_wait_task(esp_websocket_client_handle_t client) +{ + /* A running client cannot be stopped from the websocket task/event handler */ + TaskHandle_t running_task = xTaskGetCurrentTaskHandle(); + if (running_task == client->task_handle) { + ESP_LOGE(TAG, "Client cannot be stopped from websocket task"); + return ESP_FAIL; + } + + client->run = false; + xEventGroupWaitBits(client->status_bits, STOPPED_BIT, false, true, portMAX_DELAY); + client->state = WEBSOCKET_STATE_UNKNOW; + return ESP_OK; +} + static esp_err_t set_websocket_transport_optional_settings(esp_websocket_client_handle_t client, const char *scheme) { esp_transport_handle_t trans = esp_transport_list_get_transport(client->transport_list, scheme); @@ -754,6 +769,7 @@ esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_clie ESP_WS_CLIENT_MEM_CHECK(TAG, client->status_bits, { goto _websocket_init_fail; }); + xEventGroupSetBits(client->status_bits, STOPPED_BIT); client->buffer_size = buffer_size; return client; @@ -768,9 +784,11 @@ esp_err_t esp_websocket_client_destroy(esp_websocket_client_handle_t client) if (client == NULL) { return ESP_ERR_INVALID_ARG; } - if (client->run) { - esp_websocket_client_stop(client); + + if (client->status_bits && (STOPPED_BIT & xEventGroupGetBits(client->status_bits)) == 0) { + stop_wait_task(client); } + destroy_and_free_resources(client); return ESP_OK; } @@ -1159,23 +1177,13 @@ esp_err_t esp_websocket_client_stop(esp_websocket_client_handle_t client) if (client == NULL) { return ESP_ERR_INVALID_ARG; } - if (!client->run) { - ESP_LOGW(TAG, "Client was not started"); - return ESP_FAIL; - } - /* A running client cannot be stopped from the websocket task/event handler */ - TaskHandle_t running_task = xTaskGetCurrentTaskHandle(); - if (running_task == client->task_handle) { - ESP_LOGE(TAG, "Client cannot be stopped from websocket task"); + if (xEventGroupGetBits(client->status_bits) & STOPPED_BIT) { + ESP_LOGW(TAG, "Client was not started"); return ESP_FAIL; } - - client->run = false; - xEventGroupWaitBits(client->status_bits, STOPPED_BIT, false, true, portMAX_DELAY); - client->state = WEBSOCKET_STATE_UNKNOW; - return ESP_OK; + return stop_wait_task(client); } static int esp_websocket_client_send_close(esp_websocket_client_handle_t client, int code, const char *additional_data, int total_len, TickType_t timeout) From 39866116f55c55ce90d148d9361771e0d73f64f9 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 30 Jan 2025 07:16:24 +0100 Subject: [PATCH 53/57] bump(websocket): 1.3.0 -> 1.4.0 1.4.0 Features - Support DS peripheral for mutual TLS (55385ec3) Bug Fixes - wait for task on destroy (42674b49) - Fix pytest to verify client correctly (9046af8f) - propagate error type (eeeb9006) - fix example buffer leak (5219c39d) Updated - chore(websocket): align structure members (beb6e57e) - chore(websocket): remove unused client variable (15d3a01e) --- components/esp_websocket_client/.cz.yaml | 2 +- components/esp_websocket_client/CHANGELOG.md | 18 ++++++++++++++++++ .../esp_websocket_client/idf_component.yml | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/components/esp_websocket_client/.cz.yaml b/components/esp_websocket_client/.cz.yaml index 9d5acf605c..9778733c39 100644 --- a/components/esp_websocket_client/.cz.yaml +++ b/components/esp_websocket_client/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(websocket): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py esp_websocket_client tag_format: websocket-v$version - version: 1.3.0 + version: 1.4.0 version_files: - idf_component.yml diff --git a/components/esp_websocket_client/CHANGELOG.md b/components/esp_websocket_client/CHANGELOG.md index f36b2f5483..14fd337f4d 100644 --- a/components/esp_websocket_client/CHANGELOG.md +++ b/components/esp_websocket_client/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.4.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.4.0) + +### Features + +- Support DS peripheral for mutual TLS ([55385ec3](https://github.com/espressif/esp-protocols/commit/55385ec3)) + +### Bug Fixes + +- wait for task on destroy ([42674b49](https://github.com/espressif/esp-protocols/commit/42674b49)) +- Fix pytest to verify client correctly ([9046af8f](https://github.com/espressif/esp-protocols/commit/9046af8f)) +- propagate error type ([eeeb9006](https://github.com/espressif/esp-protocols/commit/eeeb9006)) +- fix example buffer leak ([5219c39d](https://github.com/espressif/esp-protocols/commit/5219c39d)) + +### Updated + +- chore(websocket): align structure members ([beb6e57e](https://github.com/espressif/esp-protocols/commit/beb6e57e)) +- chore(websocket): remove unused client variable ([15d3a01e](https://github.com/espressif/esp-protocols/commit/15d3a01e)) + ## [1.3.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.3.0) ### Features diff --git a/components/esp_websocket_client/idf_component.yml b/components/esp_websocket_client/idf_component.yml index 9ba85c058f..f2812e5fb5 100644 --- a/components/esp_websocket_client/idf_component.yml +++ b/components/esp_websocket_client/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.3.0" +version: "1.4.0" description: WebSocket protocol client for ESP-IDF url: https://github.com/espressif/esp-protocols/tree/master/components/esp_websocket_client dependencies: From 87f835af0f1d890dbe69e174a75a0e07433305b2 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 31 Jan 2025 11:19:00 +0100 Subject: [PATCH 54/57] fix(mosq): Run IDF build test only on master or labeled PRs --- .github/workflows/mosq__build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index 5a12bc30f5..f016a80e58 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -103,6 +103,9 @@ jobs: echo "Versions are consistent: $CONFIG_VERSION" build_idf_tests_with_mosq: + if: | + github.repository == 'espressif/esp-protocols' && + ( contains(github.event.pull_request.labels.*.name, 'mosquitto') || github.event_name == 'push' ) name: Build IDF tests strategy: matrix: From cd07228f81603103e5b56f2d2334743a9db2f36c Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 4 Feb 2025 16:02:04 +0100 Subject: [PATCH 55/57] fix(mdns): Fix responder to ignore only invalid queries not the entire packet, so we can still reply to next questions Closes https://github.com/espressif/esp-protocols/issues/754 --- components/mdns/mdns.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 12539a4352..967519461d 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -1879,7 +1879,11 @@ static void _mdns_create_answer_from_parsed_packet(mdns_parsed_packet_t *parsed_ shared = q->type == MDNS_TYPE_PTR || q->type == MDNS_TYPE_SDPTR || !parsed_packet->probe; if (q->type == MDNS_TYPE_SRV || q->type == MDNS_TYPE_TXT) { mdns_srv_item_t *service = _mdns_get_service_item_instance(q->host, q->service, q->proto, NULL); - if (service == NULL || !_mdns_create_answer_from_service(packet, service->service, q, shared, send_flush)) { + if (service == NULL) { // Service not found, but we continue to the next question + q = q->next; + continue; + } + if (!_mdns_create_answer_from_service(packet, service->service, q, shared, send_flush)) { _mdns_free_tx_packet(packet); return; } else { From 64d818b2d32495a911395640340735acca3d1c4a Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 7 Feb 2025 14:31:18 +0100 Subject: [PATCH 56/57] bump(mdns): 1.5.2 -> 1.5.3 1.5.3 Bug Fixes - Fix responder to ignore only invalid queries (cd07228f, #754) --- components/mdns/.cz.yaml | 2 +- components/mdns/CHANGELOG.md | 6 ++++++ components/mdns/idf_component.yml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/mdns/.cz.yaml b/components/mdns/.cz.yaml index 3ec98354e1..be067e4d92 100644 --- a/components/mdns/.cz.yaml +++ b/components/mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.5.2 + version: 1.5.3 version_files: - idf_component.yml diff --git a/components/mdns/CHANGELOG.md b/components/mdns/CHANGELOG.md index 1522ca3725..419d952b1c 100644 --- a/components/mdns/CHANGELOG.md +++ b/components/mdns/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.5.3](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.3) + +### Bug Fixes + +- Fix responder to ignore only invalid queries ([cd07228f](https://github.com/espressif/esp-protocols/commit/cd07228f), [#754](https://github.com/espressif/esp-protocols/issues/754)) + ## [1.5.2](https://github.com/espressif/esp-protocols/commits/mdns-v1.5.2) ### Bug Fixes diff --git a/components/mdns/idf_component.yml b/components/mdns/idf_component.yml index ea4e511a90..9d8792f9fe 100644 --- a/components/mdns/idf_component.yml +++ b/components/mdns/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.5.2" +version: "1.5.3" description: "Multicast UDP service used to provide local network service and host discovery." url: "https://github.com/espressif/esp-protocols/tree/master/components/mdns" issues: "https://github.com/espressif/esp-protocols/issues" From 443c344fa4a4f1d308ecf77ab150875f96d7e79c Mon Sep 17 00:00:00 2001 From: glmfe Date: Fri, 7 Feb 2025 17:57:56 -0300 Subject: [PATCH 57/57] feat(lws): Add initial support libwebsockets component --- .github/workflows/lws_build.yml | 86 +++++ .github/workflows/publish-docs-component.yml | 1 + .gitmodules | 3 + .pre-commit-config.yaml | 4 +- README.md | 4 + components/libwebsockets/CMakeLists.txt | 21 ++ components/libwebsockets/README.md | 24 ++ .../examples/client/CMakeLists.txt | 6 + .../libwebsockets/examples/client/LICENSE | 311 +++++++++++++++++ .../libwebsockets/examples/client/README.md | 87 +++++ .../examples/client/main/CMakeLists.txt | 12 + .../examples/client/main/Kconfig.projbuild | 45 +++ .../examples/client/main/certs/ca_cert.pem | 21 ++ .../client/main/certs/client_cert.pem | 20 ++ .../examples/client/main/certs/client_key.pem | 28 ++ .../client/main/certs/server/server_cert.pem | 20 ++ .../client/main/certs/server/server_key.pem | 28 ++ .../examples/client/main/idf_component.yml | 6 + .../examples/client/main/lws-client.c | 323 ++++++++++++++++++ .../examples/client/pytest_websocket.py | 237 +++++++++++++ .../examples/client/sdkconfig.ci | 13 + .../examples/client/sdkconfig.ci.mutual_auth | 18 + .../examples/client/sdkconfig.ci.plain_tcp | 18 + components/libwebsockets/idf_component.yml | 5 + components/libwebsockets/libwebsockets | 1 + components/libwebsockets/port/lws_port.c | 57 ++++ test_app/CMakeLists.txt | 1 + 27 files changed, 1398 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/lws_build.yml create mode 100644 components/libwebsockets/CMakeLists.txt create mode 100644 components/libwebsockets/README.md create mode 100644 components/libwebsockets/examples/client/CMakeLists.txt create mode 100644 components/libwebsockets/examples/client/LICENSE create mode 100644 components/libwebsockets/examples/client/README.md create mode 100644 components/libwebsockets/examples/client/main/CMakeLists.txt create mode 100644 components/libwebsockets/examples/client/main/Kconfig.projbuild create mode 100644 components/libwebsockets/examples/client/main/certs/ca_cert.pem create mode 100644 components/libwebsockets/examples/client/main/certs/client_cert.pem create mode 100644 components/libwebsockets/examples/client/main/certs/client_key.pem create mode 100644 components/libwebsockets/examples/client/main/certs/server/server_cert.pem create mode 100644 components/libwebsockets/examples/client/main/certs/server/server_key.pem create mode 100644 components/libwebsockets/examples/client/main/idf_component.yml create mode 100644 components/libwebsockets/examples/client/main/lws-client.c create mode 100644 components/libwebsockets/examples/client/pytest_websocket.py create mode 100644 components/libwebsockets/examples/client/sdkconfig.ci create mode 100644 components/libwebsockets/examples/client/sdkconfig.ci.mutual_auth create mode 100644 components/libwebsockets/examples/client/sdkconfig.ci.plain_tcp create mode 100644 components/libwebsockets/idf_component.yml create mode 160000 components/libwebsockets/libwebsockets create mode 100644 components/libwebsockets/port/lws_port.c diff --git a/.github/workflows/lws_build.yml b/.github/workflows/lws_build.yml new file mode 100644 index 0000000000..59343adf54 --- /dev/null +++ b/.github/workflows/lws_build.yml @@ -0,0 +1,86 @@ +name: "lws: build-tests" + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + build_lws: + if: contains(github.event.pull_request.labels.*.name, 'lws') || github.event_name == 'push' + name: Libwebsockets build + strategy: + matrix: + idf_ver: ["latest", "release-v5.4", "release-v5.3", "release-v5.2"] + test: [ { app: example, path: "examples/client" }] + runs-on: ubuntu-22.04 + container: espressif/idf:${{ matrix.idf_ver }} + env: + TEST_DIR: components/libwebsockets/${{ matrix.test.path }} + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }} + shell: bash + run: | + . ${IDF_PATH}/export.sh + python -m pip install idf-build-apps + python ./ci/build_apps.py ${TEST_DIR} + cd ${TEST_DIR} + for dir in `ls -d build_esp32_*`; do + $GITHUB_WORKSPACE/ci/clean_build_artifacts.sh `pwd`/$dir + zip -qur artifacts.zip $dir + done + - uses: actions/upload-artifact@v4 + with: + name: lws_target_esp32_${{ matrix.idf_ver }}_${{ matrix.test.app }} + path: ${{ env.TEST_DIR }}/artifacts.zip + if-no-files-found: error + + run-target-lws: + # Skip running on forks since it won't have access to secrets + if: | + github.repository == 'espressif/esp-protocols' && + ( contains(github.event.pull_request.labels.*.name, 'lws') || github.event_name == 'push' ) + name: Target test + needs: build_lws + strategy: + fail-fast: false + matrix: + idf_ver: ["latest", "release-v5.4", "release-v5.3", "release-v5.2"] + idf_target: ["esp32"] + test: [ { app: example, path: "examples/client" }] + runs-on: + - self-hosted + - ESP32-ETHERNET-KIT + env: + TEST_DIR: components/libwebsockets/${{ matrix.test.path }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: lws_target_esp32_${{ matrix.idf_ver }}_${{ matrix.test.app }} + path: ${{ env.TEST_DIR }}/ci/ + - name: Install Python packages + env: + PIP_EXTRA_INDEX_URL: "https://www.piwheels.org/simple" + run: | + pip install --only-binary cryptography --extra-index-url https://dl.espressif.com/pypi/ -r $GITHUB_WORKSPACE/ci/requirements.txt + - name: Run Example Test on target + working-directory: ${{ env.TEST_DIR }} + run: | + unzip ci/artifacts.zip -d ci + for dir in `ls -d ci/build_*`; do + rm -rf build sdkconfig.defaults + mv $dir build + python -m pytest --log-cli-level DEBUG --junit-xml=./results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=${{ matrix.idf_target }} + done + - uses: actions/upload-artifact@v4 + if: always() + with: + name: results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}.xml + path: components/libwebsockets/${{ matrix.test.path }}/*.xml diff --git a/.github/workflows/publish-docs-component.yml b/.github/workflows/publish-docs-component.yml index c364a0bf0d..601c3700d5 100644 --- a/.github/workflows/publish-docs-component.yml +++ b/.github/workflows/publish-docs-component.yml @@ -102,5 +102,6 @@ jobs: components/mbedtls_cxx; components/mosquitto; components/sock_utils; + components/libwebsockets; namespace: "espressif" api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/.gitmodules b/.gitmodules index 391f572334..d1d5c9c874 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "components/mosquitto/mosquitto"] path = components/mosquitto/mosquitto url = https://github.com/eclipse/mosquitto +[submodule "components/libwebsockets/libwebsockets"] + path = components/libwebsockets/libwebsockets + url = https://github.com/warmcat/libwebsockets.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b7af80bff..cd1b0ceb7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,8 +61,8 @@ repos: - repo: local hooks: - id: commit message scopes - name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common, eppp, tls_cxx, mosq, sockutls" - entry: '\A(?!(feat|fix|ci|bump|test|docs|chore)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|tls_cxx|mosq|sockutls)\)\:)' + name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common, eppp, tls_cxx, mosq, sockutls, lws" + entry: '\A(?!(feat|fix|ci|bump|test|docs|chore)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|tls_cxx|mosq|sockutls|lws)\)\:)' language: pygrep args: [--multiline] stages: [commit-msg] diff --git a/README.md b/README.md index 3962546f0b..e8e6ec398d 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,7 @@ Please refer to instructions in [ESP-IDF](https://github.com/espressif/esp-idf) ### Socket helpers (sock-utils) * Brief introduction [README](components/sock_utils/README.md) + +### libwebsockets + +* Brief introduction [README](components/libwebsockets/README.md) diff --git a/components/libwebsockets/CMakeLists.txt b/components/libwebsockets/CMakeLists.txt new file mode 100644 index 0000000000..f1b980759a --- /dev/null +++ b/components/libwebsockets/CMakeLists.txt @@ -0,0 +1,21 @@ +idf_component_register(REQUIRES mbedtls driver) + +option(LWS_WITH_EXPORT_LWSTARGETS "Export libwebsockets CMake targets. Disable if they conflict with an outer cmake project." OFF) +set(LWS_WITH_EXPORT_LWSTARGETS OFF) + +option(LWS_WITH_MBEDTLS "Use mbedTLS (>=2.0) replacement for OpenSSL." ON) +set(LWS_WITH_MBEDTLS ON) + +set(WRAP_FUNCTIONS mbedtls_ssl_handshake_step + lws_adopt_descriptor_vhost) + +foreach(wrap ${WRAP_FUNCTIONS}) + target_link_libraries(${COMPONENT_LIB} INTERFACE "-Wl,--wrap=${wrap}") +endforeach() + +target_link_libraries(${COMPONENT_LIB} INTERFACE websockets) + +target_sources(${COMPONENT_LIB} INTERFACE "port/lws_port.c") + + +add_subdirectory(libwebsockets) diff --git a/components/libwebsockets/README.md b/components/libwebsockets/README.md new file mode 100644 index 0000000000..daabf537c5 --- /dev/null +++ b/components/libwebsockets/README.md @@ -0,0 +1,24 @@ +# ESP32 libwebsockets Port + +This is a lightweight port of the libwebsockets library designed to run on the ESP32. It provides WebSocket client functionalities. + +## Supported Options + +The ESP-IDF port of libwebsockets supports a set of common WebSocket configurations for clients. These options can be configured through a structure passed to the `lws_create_context()` function. + +Key features supported: +- WebSocket with optional SSL/TLS +- HTTP/1.1 client support + +## Memory Footprint Considerations + +The memory consumption primarily depends on the number of concurrent connections and the selected options for WebSocket frames, protocol handling, and SSL/TLS features. It consumes approximately 300 kB of program memory. + +### Client: +The values bellow were extracted from the client example (./examples/client/main/lws-client.c) +- **Initial Memory Usage**: ~8 kB of heap on startup +- **Connected Memory Usage Over TLS**: ~35.3 kB of heap after connected to a server +- **Connected Memory Usage Over TCP (Plain)**: ~4.5 kB of heap after connected to a server + +#### When configuring a WebSocket client, ensure that you have enough heap space to handle the desired number of concurrent client connections. +#### SSL/TLS configurations may require additional memory overhead, depending on the certificate size and cryptographic settings. diff --git a/components/libwebsockets/examples/client/CMakeLists.txt b/components/libwebsockets/examples/client/CMakeLists.txt new file mode 100644 index 0000000000..74859695e0 --- /dev/null +++ b/components/libwebsockets/examples/client/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(client_example) diff --git a/components/libwebsockets/examples/client/LICENSE b/components/libwebsockets/examples/client/LICENSE new file mode 100644 index 0000000000..8f82a43bc3 --- /dev/null +++ b/components/libwebsockets/examples/client/LICENSE @@ -0,0 +1,311 @@ +Libwebsockets and included programs are provided under the terms of the +MIT license shown below, with the exception that some sources are under +a similar permissive license like BSD, or are explicitly CC0 / public +domain to remove any obstacles from basing differently-licensed code on +them. + +Original liberal license retained: + + - lib/misc/sha-1.c - 3-clause BSD license retained, link to original [BSD3] + - win32port/zlib + - lib/drivers/display/upng.* - ZLIB license (see zlib.h) [ZLIB] + - lib/tls/mbedtls/wrapper - Apache 2.0 (only built if linked against mbedtls) [APACHE2] + - lib/tls/mbedtls/mbedtls-extensions.c + - lib/misc/base64-decode.c - already MIT + - contrib/mcufont/encoder + - lib/misc/ieeehalfprecision.c - 2-clause BSD license retained [BSD2] + - contrib/mcufont/fonts - Open Font License [OFL] + +Relicensed to MIT: + + - lib/misc/daemonize.c - relicensed from Public Domain to MIT, + link to original Public Domain version + - lib/plat/windows/windows-resolv.c - relicensed from "Beerware v42" to MIT + +Public Domain (CC-zero) to simplify reuse: + + - test-apps/*.c + - test-apps/*.h + - minimal-examples/* + - lwsws/* + +Although libwebsockets is available under a permissive license, it does not +change the reality of dealing with large lumps of external code... if your +copy diverges it is guaranteed to contain security problems after a while +and can be very painful to pick backports (especially since historically, +we are very hot on cleaning and refactoring the codebase). The least +painful and lowest risk way remains sending your changes and fixes upstream +to us so you can easily use later releases and fixes. + +## MIT License applied to libwebsockets + +https://opensource.org/licenses/MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + +## BSD2 + +``` + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. +``` + +## BSD3 + +For convenience, a copy of the license on `./lib/misc/sha-1.c`. In binary +distribution, this applies to builds with ws support enabled, and without +`LWS_WITHOUT_BUILTIN_SHA1` at cmake. + +``` +/* + * Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the project nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH +``` + +## ZLIB + +For convenience, a copy of the license on zlib. In binary distribution, +this applies for win32 builds with internal zlib only. You can avoid +building any zlib usage or copy at all with `-DLWS_WITH_ZLIB=0` (the +default), and so avoid needing to observe the license for binary +distribution that doesn't include the related code. + +``` + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu +``` + + +## APACHE2 + +For convenience, a copy of the license on the mbedtls wrapper part. In binary +distribution, this applies only when building lws against mbedtls. + +The canonical license application to source files uses the URL reference, so the +whole is not reproduced here. + +``` +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +``` + +## CC0 + +For convenience,the full text of CC0 dedication found on the lws examples. +The intention of this is to dedicate the examples to the public domain, so +users can build off and modify them without any constraint. + +``` +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + moral rights retained by the original author(s) and/or performer(s); + publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + rights protecting the extraction, dissemination, use and reuse of data in a Work; + database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. +``` + +## OFL: Open Font License + +``` +Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. +``` diff --git a/components/libwebsockets/examples/client/README.md b/components/libwebsockets/examples/client/README.md new file mode 100644 index 0000000000..eda69c38ef --- /dev/null +++ b/components/libwebsockets/examples/client/README.md @@ -0,0 +1,87 @@ +# Websocket LWS client example + +This example will shows how to set up and communicate over a websocket. + +## How to Use Example + +### Hardware Required + +This example can be executed on any ESP32 board, the only required interface is WiFi and connection to internet or a local server. + +### Configure the project + +* Open the project configuration menu (`idf.py menuconfig`) +* Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. +* Configure the websocket endpoint URI under "Example Configuration" + +### Server Certificate Verification + +* Mutual Authentication: When `CONFIG_WS_OVER_TLS_MUTUAL_AUTH=y` is enabled, it's essential to provide valid certificates for both the server and client. + This ensures a secure two-way verification process. +* Server-Only Authentication: To perform verification of the server's certificate only (without requiring a client certificate), set `CONFIG_WS_OVER_TLS_SERVER_AUTH=y`. + This method skips client certificate verification. +* Example below demonstrates how to generate a new self signed certificates for the server and client using the OpenSSL command line tool + +Please note: This example represents an extremely simplified approach to generating self-signed certificates/keys with a single common CA, devoid of CN checks, lacking password protection, and featuring hardcoded key sizes and types. It is intended solely for testing purposes. +In the outlined steps, we are omitting the configuration of the CN (Common Name) field due to the context of a testing environment. However, it's important to recognize that the CN field is a critical element of SSL/TLS certificates, significantly influencing the security and efficacy of HTTPS communications. This field facilitates the verification of a website's identity, enhancing trust and security in web interactions. In practical deployments beyond testing scenarios, ensuring the CN field is accurately set is paramount for maintaining the integrity and reliability of secure communications + +It is **strongly recommended** to not reuse the example certificate in your application; +it is included only for demonstration. + + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (18208) lws-client: LWS minimal ws client echo + +219868: lws_create_context: LWS: 4.3.99-v4.3.0-424-ga74362ff, MbedTLS-3.6.2 NET CLI SRV H1 H2 WS SS-JSON-POL ConMon IPv6-absent +219576: mem: platform fd map: 20 bytes +217880: __lws_lc_tag: ++ [wsi|0|pipe] (1) +216516: __lws_lc_tag: ++ [vh|0|default||-1] (1) +I (18248) lws-client: connect_cb: connecting + +210112: __lws_lc_tag: ++ [wsicli|0|WS/h1/default/echo.websocket.events] (1) +204800: [wsicli|0|WS/h1/default/echo.websocket.events]: lws_client_connect_3_connect: trying 13.248.241.119 +180776: lws_ssl_client_bio_create: allowing selfsigned +I (19998) wifi:idx:0 (ifx:0, b4:89:01:63:9d:08), tid:0, ssn:321, winSize:64 +I (20768) lws-client: WEBSOCKET_EVENT_CONNECTED +I (20768) lws-client: Sending hello 0000 +I (20778) lws-client: WEBSOCKET_EVENT_DATA +W (20778) lws-client: Received=echo.websocket.events sponsored by Lob.com + + +I (20968) lws-client: WEBSOCKET_EVENT_DATA +W (20968) lws-client: Received=hello 0000 + + +I (22978) lws-client: Sending hello 0001 +I (23118) lws-client: WEBSOCKET_EVENT_DATA +W (23118) lws-client: Received=hello 0001 + + +I (23778) lws-client: Sending hello 0002 +I (23938) lws-client: WEBSOCKET_EVENT_DATA +W (23938) lws-client: Received=hello 0002 + + +I (25948) lws-client: Sending hello 0003 +I (26088) lws-client: WEBSOCKET_EVENT_DATA +W (26088) lws-client: Received=hello 0003 + + +I (26948) lws-client: Sending hello 0004 +I (27118) lws-client: WEBSOCKET_EVENT_DATA +W (27118) lws-client: Received=hello 0004 +``` diff --git a/components/libwebsockets/examples/client/main/CMakeLists.txt b/components/libwebsockets/examples/client/main/CMakeLists.txt new file mode 100644 index 0000000000..55f6388549 --- /dev/null +++ b/components/libwebsockets/examples/client/main/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SRC_FILES "lws-client.c") # Define source files +set(INCLUDE_DIRS ".") # Define include directories +set(EMBED_FILES "") # Initialize an empty list for files to embed + +list(APPEND EMBED_FILES + "certs/client_cert.pem" + "certs/ca_cert.pem" + "certs/client_key.pem") + +idf_component_register(SRCS "${SRC_FILES}" + INCLUDE_DIRS "${INCLUDE_DIRS}" + EMBED_TXTFILES "${EMBED_FILES}") diff --git a/components/libwebsockets/examples/client/main/Kconfig.projbuild b/components/libwebsockets/examples/client/main/Kconfig.projbuild new file mode 100644 index 0000000000..82aa549974 --- /dev/null +++ b/components/libwebsockets/examples/client/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Example Configuration" + choice WEBSOCKET_URI_SOURCE + prompt "Websocket URI source" + default WEBSOCKET_URI_FROM_STRING + help + Selects the source of the URI used in the example. + + config WEBSOCKET_URI_FROM_STRING + bool "From string" + + config WEBSOCKET_URI_FROM_STDIN + bool "From stdin" + endchoice + + config WEBSOCKET_URI + string "Websocket endpoint URI" + default "echo.websocket.events" + help + URL or IP of websocket endpoint this example connects to and sends echo + config WEBSOCKET_PORT + int "Websocket endpoint PORT" + default 443 + help + Port of websocket endpoint this example connects to and sends echo + + config WS_OVER_TLS_SERVER_AUTH + bool "Enable WebSocket over TLS with Server Certificate Verification Only" + default n + help + Enables WebSocket connections over TLS (WSS) with server certificate verification. + This setting mandates the client to verify the servers certificate, while the server + does not require client certificate verification. + + config WS_OVER_TLS_MUTUAL_AUTH + bool "Enable WebSocket over TLS with Server Client Mutual Authentification" + default y + help + Enables WebSocket connections over TLS (WSS) with server and client mutual certificate verification. + + config WS_OVER_TLS_SKIP_COMMON_NAME_CHECK + bool "Skip common name(CN) check during TLS authentification" + default n + help + Skipping Common Name(CN) check during TLS(WSS) authentification +endmenu diff --git a/components/libwebsockets/examples/client/main/certs/ca_cert.pem b/components/libwebsockets/examples/client/main/certs/ca_cert.pem new file mode 100644 index 0000000000..e9a27099b9 --- /dev/null +++ b/components/libwebsockets/examples/client/main/certs/ca_cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUL04QhbSEt5oNbV4f7CeLLqTCw2gwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA2MjVaFw0zNDAy +MjAwODA2MjVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDjc78SuXAmJeBc0el2/m+2lwtk3J/VrNxHYkhjHa8K +/ybU89VvKGuv9+L3IP67WMguFTaMgivJYUePjfMchtNJLJ+4cR9BkBKH4JnyXDae +s0a5181LxRo8rqcaOw9hmJTgt9R4dIRTR3GN2/VLhlR+L9OTYA54RUtMyMMpyk5M +YIJbcOwiwkVLsIYnexXDfgz9vQGl/2vBQ/RBtDBvbSyBiWox9SuzOrya1HUBzJkM +Iu5L0bSa0LAeXHT3i3P1Y4WPt9ub70OhUNfJtHC+XbGFSEkkQG+lfbXU75XLoMWa +iATMREOcb3Mq+pn1G8o1ZHVc6lBHUkfrNfxs5P/GQcSvAgMBAAGjUzBRMB0GA1Ud +DgQWBBQGkdK2gR2HrQTnZnbuWO7I1+wdxDAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn +ZnbuWO7I1+wdxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx +G0hFtMwV/agIwC3ZaYC36ZWiijFzWkJSZG+fqAy32mSoVL2uQvOT8vEfF0ZnAcPc +JI4oI059dBhAVlwqv6uLHyD4Gf2bF4oSLljdTz3X23llF+/wrTC2LLqMrm09aUC0 +ac74Q0FVwVJJcqH1HgemCMVjna5MkwNA6B+q7uR3eQ692VqXk6vjd4fRLBg1bBO1 +hXjasfNxA8A9quORF5+rjYrwyUZHuzcs0FfSClckIt4tHKtt4moLufOW6/PM4fRe +AgdDfiTupxYLJFz4hFPhfgCh4TjQ+f9+uP4IAjW42dJmTVZjLEku/hm5lxCFObAq +RgfaNwH8Ug1r1xswjSZG +-----END CERTIFICATE----- diff --git a/components/libwebsockets/examples/client/main/certs/client_cert.pem b/components/libwebsockets/examples/client/main/certs/client_cert.pem new file mode 100644 index 0000000000..e99921a3c4 --- /dev/null +++ b/components/libwebsockets/examples/client/main/certs/client_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUUPCOgMA2v09E29fCkogx3RUBRtEwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA3MzFaFw0zNDAy +MjAwODA3MzFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCrNeomxI2aoP+4iUy5SiA+41oHUDZDFeJOBjv5JCsK +mlvFqxE9zynmPOVpuABErOJwzerPTa4NYKvuvs5CxVJUV5CXtWANuu9majioZNzj +f877MDNX/GnZHK2gnkxVrZCPaDmx9yiMsFMXgmfdrDhwoUpXbdgSyeU/al9Ds2kF +0hrHOH2LBWt/mVeLbONU5CC1HOdVVw+uRlhVlxnfhTPd/Nru3rJx7R0sN7qXcZpJ +PL87WvrszLVOux24DeaOz9oiD2b7egFyUuq1BM25iCwi8s/Ths8xd0Ca1d8mEcHW +FVd4w2+nUMXFE+IbP+wo6FXuiSaOBNri3rztpvCCMaWjAgMBAAGjQjBAMB0GA1Ud +DgQWBBSOlA+9Vfbcfy8iS4HSd4V0KPtm4jAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn +ZnbuWO7I1+wdxDANBgkqhkiG9w0BAQsFAAOCAQEAOmzm/MwowKTrSpMSrmfA3MmW +ULzsfa25WyAoTl90ATlg4653Y7pRaNfdvVvyi2V2LlPcmc7E0rfD53t1NxjDH1uM +LgFMTNEaZ9nMRSW0kMiwaRpvmXS8Eb9PXfvIM/Mw0co/aMOtAQnfTGIqsgkQwKyk +1GG7QKQq3p4QGu5ZaTnjnaoa79hODt+0xQDD1wp6C9xwBY0M4gndAi3wkOeFkGv+ +OmGPtaCBu5V9tJCZ9dfZvjkaK44NGwDw0urAcYRK2h7asnlflu7cnlGMBB0qY4kQ +BX5WI8UjN6rECBHbtNRvEh06ogDdHbxYV+TibrqkkeDRw6HX1qqiEJ+iCgWEDQ== +-----END CERTIFICATE----- diff --git a/components/libwebsockets/examples/client/main/certs/client_key.pem b/components/libwebsockets/examples/client/main/certs/client_key.pem new file mode 100644 index 0000000000..68dcc7af61 --- /dev/null +++ b/components/libwebsockets/examples/client/main/certs/client_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrNeomxI2aoP+4 +iUy5SiA+41oHUDZDFeJOBjv5JCsKmlvFqxE9zynmPOVpuABErOJwzerPTa4NYKvu +vs5CxVJUV5CXtWANuu9majioZNzjf877MDNX/GnZHK2gnkxVrZCPaDmx9yiMsFMX +gmfdrDhwoUpXbdgSyeU/al9Ds2kF0hrHOH2LBWt/mVeLbONU5CC1HOdVVw+uRlhV +lxnfhTPd/Nru3rJx7R0sN7qXcZpJPL87WvrszLVOux24DeaOz9oiD2b7egFyUuq1 +BM25iCwi8s/Ths8xd0Ca1d8mEcHWFVd4w2+nUMXFE+IbP+wo6FXuiSaOBNri3rzt +pvCCMaWjAgMBAAECggEAOTWjz16AXroLmRMv8v5E9h6sN6Ni7lnCrAXDRoYCZ+Ga +Ztu5wCiYPJn+oqvcUxZd+Ammu6yeS1QRP468h20+DHbSFw+BUDU1x8gYtJQ3h0Fu +3VqG3ZC3odfGYNRkd4CuvGy8Uq5e+1vz9/gYUuc4WNJccAiBWg3ir6UQviOWJV46 +LGfdEd9hVvIGl5pmArMBVYdpj9+JHunDtG4uQxiWla5pdLjlkC2mGexD18T9d718 +6I+o3YHv1Y9RPT1d4rNhYQWx6YdTTD2rmS7nTrzroj/4fXsblpXzR+/l7crlNERY +67RMPwgDR1NiAbCAJKsSbMS66lRCNlhTM4YffGAN6QKBgQDkIdcNm9j49SK5Wbl5 +j8U6UbcVYPzPG+2ea+fDfUIafA0VQHIuX6FgA17Kp7BDX9ldKtSBpr0Z8vetVswr +agmXVMR/7QdvnZ9NpL66YA/BRs67CvsryVu4AVAzThFGySmlcXGlPq47doWDQ3B9 +0BOEnVoeDXR3SabaNsEbhDYn1wKBgQDAIAUyhJcgz+LcgaAtBwdnEN57y66JlRVZ +bsb6cEG/MNmnLjQYsplJjNbz4yrB5ukTChPTGRF/JQRqHoXh6DGQFHvobukwwA6x +RAIIq0NLJ5HUipfOi+VpCbWUHdoUNhwjAB2qVtD4LXE2Lyn46C8ET5eRtRjUKpzV +lpsq63KHFQKBgFB+cDbpCoGtXPcxZXQy+lA9jPAKLKmXHRyMzlX32F8n7iXVe3RJ +YdNS3Rt8V4EuTK/G8PxeLNL/G80ZlyiqXX/79Ol+ZOVJJHBs9K8mPejgZwkwMrec +cLRYIkg3/3iOehdaE9NOboOkqi9KmGKMDJb6PlXkQXflkO3l6/UdjU45AoGAen0v +sxiTncjMU1eVfn+nuY8ouXaPbYoOFXmqBItDb5i+e3baohBj6F+Rv+ZKIVuNp6Ta +JNErtYstOFcDdpbp2nkk0ni71WftNhkszsgZ3DV7JS3DQV0xwvj8ulUZ757b63is +cShujHu0XR5OvTGSoEX6VVxHWyVb3lTp0sBPwU0CgYBe2Ieuya0X8mAbputFN64S +Kv++dqktTUT8i+tp07sIrpDeYwO3D89x9kVSJj4ImlmhiBVGkxFWPkpGyBLotdse +Ai/E6f5I7CDSZZC0ZucgcItNd4Yy459QY+dFwFtT3kIaD9ml8fnqQ83J9W8DWtv9 +6mY9FnUUufbJcpHxN58RTw== +-----END PRIVATE KEY----- diff --git a/components/libwebsockets/examples/client/main/certs/server/server_cert.pem b/components/libwebsockets/examples/client/main/certs/server/server_cert.pem new file mode 100644 index 0000000000..cb1e9dfe74 --- /dev/null +++ b/components/libwebsockets/examples/client/main/certs/server/server_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUUPCOgMA2v09E29fCkogx3RUBRtAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA2NTlaFw0zNDAy +MjAwODA2NTlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC8WWbDxnLzTSfuQaO+kQnnzbwjhUHWn58s+BIEaO8M +GG6bX+8r/SH9XjMfFS36qAN3qxgRun3YoRTaHc2QByiGjf5IL4EAPDnLN+NzUIL5 +7Gi2QPQP/GksAsOGKWk/nMRPk1vcMptkFVIWSp474SQ0A92Z9z0dUIqBpjRa34kr +HsAIcT59/EG7YBBadMk0fQIxQVLh3Vosky85q+0waFihe47Ef5U2UftexoUx4Vcz +6EtP60Wx+4qN+FLsr+n2B7Oz2ITqfwgqLzjNLZwm9bMjcLZ0fWm1A/W1C989MXwI +w6DAPEZv7pbgp8r9phyrNieSDuuRaCvFsaXh6troAjLxAgMBAAGjQjBAMB0GA1Ud +DgQWBBRJCYAQG2+1FN5P/wyAR1AsrAyb4DAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn +ZnbuWO7I1+wdxDANBgkqhkiG9w0BAQsFAAOCAQEAmllul/GIH7RVq85mM/SxP47J +M7Z7T032KuR3n/Psyv2iq/uEV2CUje3XrKNwR2PaJL4Q6CtoWy7xgIP+9CBbjddR +M7sdNQab8P2crAUtBKnkNOl/na/5KnXnjwi/PmWJJ9i2Cqt0PPkaykTWp/MLfYIw +RPkY2Yo8f8gEiqXQd+0qTuMgumbgkPq3V8Lk1ocy62F5/qUhXxH+ifAXEoUQS6EG +8DlgwdZlfUY+jeM6N56WzYmxD1syjNW7faPio+qXINfpYatROhqphaMQ5SA6TRj6 +jcnLa31TdDdWmWYDcYgZntAv6yGi3rh0MdYqeNS0FKlMKmaH81VHs7V1UUXwUQ== +-----END CERTIFICATE----- diff --git a/components/libwebsockets/examples/client/main/certs/server/server_key.pem b/components/libwebsockets/examples/client/main/certs/server/server_key.pem new file mode 100644 index 0000000000..cf2fdadb7a --- /dev/null +++ b/components/libwebsockets/examples/client/main/certs/server/server_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8WWbDxnLzTSfu +QaO+kQnnzbwjhUHWn58s+BIEaO8MGG6bX+8r/SH9XjMfFS36qAN3qxgRun3YoRTa +Hc2QByiGjf5IL4EAPDnLN+NzUIL57Gi2QPQP/GksAsOGKWk/nMRPk1vcMptkFVIW +Sp474SQ0A92Z9z0dUIqBpjRa34krHsAIcT59/EG7YBBadMk0fQIxQVLh3Vosky85 +q+0waFihe47Ef5U2UftexoUx4Vcz6EtP60Wx+4qN+FLsr+n2B7Oz2ITqfwgqLzjN +LZwm9bMjcLZ0fWm1A/W1C989MXwIw6DAPEZv7pbgp8r9phyrNieSDuuRaCvFsaXh +6troAjLxAgMBAAECggEACNVCggTxCCMCr+RJKxs/NS1LWPkbZNbYjrHVmnpXV6Bf +s460t0HoUasUx6zlGp+9heOyvcYat8maIj6KkOodBu5q0fTUXm/0n+ivlI1ejxz8 +ritupr9GKWe5xrVzd6XA+SBmivWenvt2/Y+jSxica4oQ3vMe3RyVWk4yn15jXu+9 +7B9lNyNeZtOBr6OozHGLYw4dwWcBNv2S6wevRKfHPwn/Ch5yTH1uAskgoMxUuyK2 +ynNVHWUhyS4pFU7Tex5ENDel15VYdbxV/2lQ2W6fHMLtC5GWKJXXbigCX7pfOpzC +BFJEfZl7ze/qptE9AR7DkLFYyMtrS7OlebYbLDOM9wKBgQD+rTdwULZibpKwlI3a +9Y22d4N/EDFvuu8LnuEiVQnXgwg9M+tlaa2liP18j1a7y/FCfoXf5sjUWCsdYR6d +C0TuiOGI59hYGI94NvVLAmOutR+vJ/3jhbv5wyqEQLhJ42Yz9kWBrDCI+V3q3TdO +H7wcH6suUIZpeLEJF4qHzY/1dwKBgQC9U/Pvswiww8sfysmd5shUNo4ofAZnTM1A +ak6pWE3lSyiOkSm+3B2GqxYWLRoo1v+pTyhhXDtRRmxGtMNrKCsmlHef/o3c6kkG +cuC2h/DiSmoITHy3BYKJoDeE54E8ubXUUKqHo41LYUs+D7M/IGxeiO13MUoIrEtF +AwzVWPBU1wKBgH8barD2x6Bm+XWCHy6qIZlxGsMfDN1r2gTdvhWJhcj3D/Sj5heO +X+lfbsxtKee+yOHcDesK3y8D9jjKkSHmTvgSfyX6OML3NxvTqidOwPugUHj2J8QX +qhLk8mJhftj50reacWRf0TV76ADhecnXEuaic6hA7mTTpOAZzL0svm3PAoGBALWF +r6VLX3KzVqZVtLb7FWmAoQ35093pCgXPpznAW3cTd4Axd/fxbTG4CUYb2i/760X2 +ij3Gw2yqe5fTKmYsLisgQA2bb4K28msHa6I2dmNQe5cXVp/X3Y98mJ6JpCSH3ekB +qm7ABfGXCCApx28n9B8zY5JbJKNqJgS15vELA+ojAoGAAkaV2w46+3iQ6gJtQepr +zGNybiYBx/Wo5fDdTS5u0xN+ZdC9fl2Zs0n7sMmUT8bWdDLcMnntHHO+oDIKyRHs +TQh1n68vQ4JoegQv3Z9Z/TLEKqr9gyJC1Ao6M4bZpPhUWQwupfHColtsr2TskcnJ +Nf2FpJZ7z6fQEShGlK1yTXM= +-----END PRIVATE KEY----- diff --git a/components/libwebsockets/examples/client/main/idf_component.yml b/components/libwebsockets/examples/client/main/idf_component.yml new file mode 100644 index 0000000000..959a1dfb33 --- /dev/null +++ b/components/libwebsockets/examples/client/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + espressif/libwebsockets: + version: "*" + override_path: "../../../" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/components/libwebsockets/examples/client/main/lws-client.c b/components/libwebsockets/examples/client/main/lws-client.c new file mode 100644 index 0000000000..457137ea93 --- /dev/null +++ b/components/libwebsockets/examples/client/main/lws-client.c @@ -0,0 +1,323 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* ESP libwebsockets client example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include + +#include "esp_wifi.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "protocol_examples_common.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "esp_log.h" +#include "esp_task_wdt.h" + +#include + +static uint32_t disconnect_timeout = 5000000; // microseconds +static uint8_t message_count = 0; +static const char *TAG = "lws-client"; + +static struct lws_context *context; +static struct lws *client_wsi; + +static lws_sorted_usec_list_t sul; +static unsigned char msg[LWS_PRE + 128]; + +static const lws_retry_bo_t retry = { + .secs_since_valid_ping = 3, + .secs_since_valid_hangup = 10, +}; + +#if CONFIG_WEBSOCKET_URI_FROM_STDIN +static void get_string(char *line, size_t size) +{ + int count = 0; + while (count < size) { + int c = fgetc(stdin); + if (c == '\n') { + line[count] = '\0'; + break; + } else if (c > 0 && c < 127) { + line[count] = c; + ++count; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + esp_task_wdt_reset(); + } +} + +#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */ + +static void send_data(struct lws *wsi, const char *data, size_t len, enum lws_write_protocol type) +{ + unsigned char buf[LWS_PRE + len]; + unsigned char *p = &buf[LWS_PRE]; + memcpy(p, data, len); + + int n = lws_write(wsi, p, len, type); + if (n < len) { + ESP_LOGE(TAG, "ERROR %d writing ws\n", n); + } +} + +static void send_large_text_data(struct lws *wsi) +{ + const int size = 2000; + char *long_data = malloc(size); + memset(long_data, 'a', size); + send_data(wsi, long_data, size, LWS_WRITE_TEXT); + free(long_data); +} + +static void send_fragmented_text_data(struct lws *wsi) +{ + char data[32]; + memset(data, 'a', sizeof(data)); + send_data(wsi, data, sizeof(data), LWS_WRITE_TEXT | LWS_WRITE_NO_FIN); + memset(data, 'b', sizeof(data)); + send_data(wsi, data, sizeof(data), LWS_WRITE_CONTINUATION); +} + +static void send_fragmented_binary_data(struct lws *wsi) +{ + char binary_data[5]; + memset(binary_data, 0, sizeof(binary_data)); + send_data(wsi, binary_data, sizeof(binary_data), LWS_WRITE_BINARY | LWS_WRITE_NO_FIN); + memset(binary_data, 1, sizeof(binary_data)); + send_data(wsi, binary_data, sizeof(binary_data), LWS_WRITE_CONTINUATION); +} + +static void connect_cb(lws_sorted_usec_list_t *_sul) +{ + struct lws_client_connect_info connect_info; + + ESP_LOGI(TAG, "%s: connecting\n", __func__); + + memset(&connect_info, 0, sizeof(connect_info)); +#if CONFIG_WEBSOCKET_URI_FROM_STDIN + char line[128]; + + ESP_LOGI(TAG, "Please enter uri of websocket endpoint"); + get_string(line, sizeof(line)); + + connect_info.address = line; + ESP_LOGI(TAG, "Endpoint uri: %s\n", line); + +#else + connect_info.address = CONFIG_WEBSOCKET_URI; +#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */ + + + connect_info.context = context; + connect_info.port = CONFIG_WEBSOCKET_PORT; + connect_info.host = connect_info.address; + connect_info.origin = connect_info.address; + connect_info.local_protocol_name = "lws-echo"; + connect_info.pwsi = &client_wsi; + connect_info.retry_and_idle_policy = &retry; + +#if defined(CONFIG_WS_OVER_TLS_MUTUAL_AUTH) || defined(CONFIG_WS_OVER_TLS_SERVER_AUTH) + connect_info.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED; + +#if defined(CONFIG_WS_OVER_TLS_SKIP_COMMON_NAME_CHECK) && defined(CONFIG_WS_OVER_TLS_SERVER_AUTH) + connect_info.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; +#endif +#else + connect_info.ssl_connection = LCCSCF_ALLOW_INSECURE; +#endif + + if (!lws_client_connect_via_info(&connect_info)) { + lws_sul_schedule(context, 0, _sul, connect_cb, 5 * LWS_USEC_PER_SEC); + } +} + +static int callback_minimal_echo(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + ESP_LOGE(TAG, "CLIENT_CONNECTION_ERROR: %s\n", + in ? (char *)in : "(null)"); + lws_sul_schedule(context, 0, &sul, connect_cb, 5 * LWS_USEC_PER_SEC); + break; + + case LWS_CALLBACK_CLIENT_ESTABLISHED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); + lws_callback_on_writable(wsi); + + break; + + case LWS_CALLBACK_CLIENT_WRITEABLE: + if (message_count < 5) { + char text_data[32]; + sprintf(text_data, "hello %04d", message_count++); + ESP_LOGI(TAG, "Sending text: %s", text_data); + send_data(wsi, text_data, strlen(text_data), LWS_WRITE_TEXT); + } else if (message_count == 5) { + ESP_LOGI(TAG, "Sending fragmented text message"); + send_fragmented_text_data(wsi); + ESP_LOGI(TAG, "Sending fragmented binary message"); + send_fragmented_binary_data(wsi); + ESP_LOGI(TAG, "Sending text longer than ws buffer (1024)"); + send_large_text_data(wsi); + message_count++; + } + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA"); + + if (lws_frame_is_binary(wsi)) { + ESP_LOGI(TAG, "Received binary data"); + ESP_LOG_BUFFER_HEX("Received binary data", in, len); + } else { + ESP_LOGW(TAG, "Received=%.*s\n\n", len, (char *)in); + } + + size_t remain = lws_remaining_packet_payload(wsi); + + // If received data is larger than the ws buffer + if (remain > 0) { + ESP_LOGW(TAG, "Total payload length=%u, data_len=%u\n\n", remain + len, len); + } + + // If received data contains json structure it succeed to parse + cJSON *root = cJSON_Parse(in); + if (root) { + for (int i = 0 ; i < cJSON_GetArraySize(root) ; i++) { + cJSON *elem = cJSON_GetArrayItem(root, i); + cJSON *id = cJSON_GetObjectItem(elem, "id"); + cJSON *name = cJSON_GetObjectItem(elem, "name"); + ESP_LOGW(TAG, "Json={'id': '%s', 'name': '%s'}", id->valuestring, name->valuestring); + } + cJSON_Delete(root); + } + + /* Reset the timeout*/ + lws_set_timer_usecs(wsi, disconnect_timeout); + break; + case LWS_CALLBACK_TIMER: + ESP_LOGW(TAG, "Closing connection"); + lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL, (unsigned char *)"bye", 3); + /* Return non-null to close the connection. */ + return -1; + break; + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static const struct lws_protocols protocols[] = { + { + .name = "lws-echo", + .callback = callback_minimal_echo, + .per_session_data_size = 1024, + .rx_buffer_size = 1024, + .id = 0, + .user = NULL, + .tx_packet_size = 0 + }, + LWS_PROTOCOL_LIST_TERM +}; + +void app_main(void) +{ + ESP_LOGI(TAG, "[APP] Startup.."); + ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size()); + ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version()); + esp_log_level_set("*", ESP_LOG_INFO); + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); + + /* Configure WDT. */ + TaskHandle_t handle = xTaskGetCurrentTaskHandle(); + esp_task_wdt_add(handle); + + /* Create LWS Context - Client. */ + struct lws_context_creation_info info; + int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + + memset(msg, 'x', sizeof(msg)); + + lws_set_log_level(logs, NULL); + ESP_LOGI(TAG, "LWS minimal ws client echo\n"); + + memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */ + info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */ + info.protocols = protocols; + info.fd_limit_per_thread = 1 + 1 + 1; + +#if CONFIG_WS_OVER_TLS_MUTUAL_AUTH + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + + /* Configuring client certificates for mutual authentification */ + extern const char cert_start[] asm("_binary_client_cert_pem_start"); // Client certificate + extern const char cert_end[] asm("_binary_client_cert_pem_end"); + extern const char key_start[] asm("_binary_client_key_pem_start"); // Client private key + extern const char key_end[] asm("_binary_client_key_pem_end"); + + info.client_ssl_cert_mem = cert_start; + info.client_ssl_cert_mem_len = cert_end - cert_start; + info.client_ssl_key_mem = key_start; + info.client_ssl_key_mem_len = key_end - key_start; +#elif CONFIG_WS_OVER_TLS_SERVER_AUTH + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + + extern const char cacert_start[] asm("_binary_ca_cert_pem_start"); // CA certificate + extern const char cacert_end[] asm("_binary_ca_cert_pem_end"); + + info.client_ssl_ca_mem = cacert_start; + info.client_ssl_ca_mem_len = cacert_end - cacert_start; +#endif + + context = lws_create_context(&info); + if (!context) { + ESP_LOGE(TAG, "lws init failed"); + } else { + lws_sul_schedule(context, 0, &sul, connect_cb, 100); + /* + * Holds the result of the lws_service call: + * = 0 -> service succeeded and events were processed, + * < 0 -> an error occurred or the event loop should stop + */ + int service_result = 0; + while (service_result >= 0) { + service_result = lws_service(context, 0); + } + + lws_context_destroy(context); + } + + while (1) { + //Should not get here, Spin undefinitely. + vTaskDelay(10); + taskYIELD(); + } +} diff --git a/components/libwebsockets/examples/client/pytest_websocket.py b/components/libwebsockets/examples/client/pytest_websocket.py new file mode 100644 index 0000000000..452a043550 --- /dev/null +++ b/components/libwebsockets/examples/client/pytest_websocket.py @@ -0,0 +1,237 @@ +# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import json +import random +import re +import socket +import ssl +import string +import sys +from threading import Event, Thread + +from SimpleWebSocketServer import (SimpleSSLWebSocketServer, + SimpleWebSocketServer, WebSocket) + + +def get_my_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + s.connect(('8.8.8.8', 1)) + IP = s.getsockname()[0] + except Exception: + IP = '127.0.0.1' + finally: + s.close() + return IP + + +class WebsocketTestEcho(WebSocket): + def handleMessage(self): + if isinstance(self.data, bytes): + print(f'\n Server received binary data: {self.data.hex()}\n') + self.sendMessage(self.data, binary=True) + else: + print(f'\n Server received: {self.data}\n') + self.sendMessage(self.data) + + def handleConnected(self): + print('Connection from: {}'.format(self.address)) + + def handleClose(self): + print('{} closed the connection'.format(self.address)) + + +# Simple Websocket server for testing purposes +class Websocket(object): + + def send_data(self, data): + for nr, conn in self.server.connections.items(): + conn.sendMessage(data) + + def run(self): + if self.use_tls is True: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(certfile='main/certs/server/server_cert.pem', keyfile='main/certs/server/server_key.pem') + if self.client_verify is True: + ssl_context.load_verify_locations(cafile='main/certs/ca_cert.pem') + ssl_context.verify = ssl.CERT_REQUIRED + ssl_context.check_hostname = False + self.server = SimpleSSLWebSocketServer('', self.port, WebsocketTestEcho, ssl_context=ssl_context) + else: + self.server = SimpleWebSocketServer('', self.port, WebsocketTestEcho) + while not self.exit_event.is_set(): + self.server.serveonce() + + def __init__(self, port, use_tls, verify): + self.port = port + self.use_tls = use_tls + self.client_verify = verify + self.exit_event = Event() + self.thread = Thread(target=self.run) + self.thread.start() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.exit_event.set() + self.thread.join(10) + if self.thread.is_alive(): + print('Thread cannot be joined', 'orange') + + +def test_examples_protocol_websocket(dut): + """ + steps: + 1. obtain IP address + 2. connect to uri specified in the config + 3. send and receive data + """ + + # Test for echo functionality: + # Sends a series of simple "hello" messages to the WebSocket server and verifies that each one is echoed back correctly. + # This tests the basic responsiveness and correctness of the WebSocket connection. + def test_echo(dut): + dut.expect('WEBSOCKET_EVENT_CONNECTED') + for i in range(0, 5): + dut.expect(re.compile(b'Received=hello (\\d)')) + print('All echos received') + sys.stdout.flush() + + # Test for clean closure of the WebSocket connection: + # Ensures that the WebSocket can correctly receive a close frame and terminate the connection without issues. + def test_close(dut): + dut.expect('__lws_lc_untag') + + # Test for JSON message handling: + # Sends a JSON formatted string and verifies that the received message matches the expected JSON structure. + def test_json(dut, websocket): + json_string = """ + [ + { + "id":"1", + "name":"user1" + }, + { + "id":"2", + "name":"user2" + } + ] + """ + websocket.send_data(json_string) + data = json.loads(json_string) + + match = dut.expect( + re.compile(b'Json=({[a-zA-Z0-9]*).*}')).group(0).decode()[5:] + if match == str(data[0]): + print('\n Sent message and received message are equal \n') + sys.stdout.flush() + else: + raise ValueError( + 'DUT received string do not match sent string, \nexpected: {}\nwith length {}\ + \nreceived: {}\nwith length {}'.format( + data[0], len(data[0]), match, len(match))) + + # Test for receiving long messages: + # This sends a message with a specified length (2000 characters) to ensure the WebSocket can handle large data payloads. Repeated 3 times for reliability. + def test_recv_long_msg(dut, websocket, msg_len, repeats): + + send_msg = ''.join( + random.choice(string.ascii_uppercase + string.ascii_lowercase + + string.digits) for _ in range(msg_len)) + + for _ in range(repeats): + websocket.send_data(send_msg) + + recv_msg = '' + while len(recv_msg) < msg_len: + match = dut.expect(re.compile( + b'Received=([a-zA-Z0-9]*).*\n')).group(1).decode() + recv_msg += match + + if recv_msg == send_msg: + print('\n Sent message and received message are equal \n') + sys.stdout.flush() + else: + raise ValueError( + 'DUT received string do not match sent string, \nexpected: {}\nwith length {}\ + \nreceived: {}\nwith length {}'.format( + send_msg, len(send_msg), recv_msg, len(recv_msg))) + + # Test for receiving the first fragment of a large message: + # Verifies the WebSocket's ability to correctly process the initial segment of a fragmented message. + def test_recv_fragmented_msg1(dut): + dut.expect('Total payload length=2000, data_len=1024') + + # Test for receiving fragmented text messages: + # Checks if the WebSocket can accurately reconstruct a message sent in several smaller parts. + def test_fragmented_txt_msg(dut): + dut.expect('Received=' + 32 * 'a' + 32 * 'b') + print('\nFragmented data received\n') + + # Extract the hexdump portion of the log line + def parse_hexdump(line): + match = re.search(r'\(.*\) Received binary data: ([0-9A-Fa-f ]+)', line) + if match: + hexdump = match.group(1).strip().replace(' ', '') + # Convert the hexdump string to a bytearray + return bytearray.fromhex(hexdump) + return bytearray() + + # Capture the binary log output from the DUT + def test_fragmented_binary_msg(dut): + match = dut.expect(r'\(.*\) Received binary data: .*') + if match: + line = match.group(0).strip() + if isinstance(line, bytes): + line = line.decode('utf-8') + + # Parse the hexdump from the log line + received_data = parse_hexdump(line) + + # Create the expected bytearray with the specified pattern + expected_data = bytearray([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + + # Validate the received data + assert received_data == expected_data, f'Received data does not match expected data. Received: {received_data}, Expected: {expected_data}' + print('\nFragmented data received\n') + else: + assert False, 'Log line with binary data not found' + + # Starting of the test + try: + if dut.app.sdkconfig.get('WEBSOCKET_URI_FROM_STDIN') is True: + uri_from_stdin = True + else: + uri = dut.app.sdkconfig['WEBSOCKET_URI'] + uri_from_stdin = False + + if dut.app.sdkconfig.get('WS_OVER_TLS_MUTUAL_AUTH') is True: + use_tls = True + client_verify = True + else: + use_tls = False + client_verify = False + + except Exception: + print('ENV_TEST_FAILURE: Cannot find uri settings in sdkconfig') + raise + + if uri_from_stdin: + server_port = 8080 + with Websocket(server_port, use_tls, client_verify) as ws: + uri = '{}'.format(get_my_ip()) + print('DUT connecting to {}'.format(uri)) + dut.expect('Please enter uri of websocket endpoint', timeout=30) + dut.write(uri) + test_echo(dut) + test_recv_long_msg(dut, ws, 2000, 3) + test_json(dut, ws) + test_fragmented_txt_msg(dut) + test_fragmented_binary_msg(dut) + test_recv_fragmented_msg1(dut) + test_close(dut) + else: + print('DUT connecting to {}'.format(uri)) + test_echo(dut) diff --git a/components/libwebsockets/examples/client/sdkconfig.ci b/components/libwebsockets/examples/client/sdkconfig.ci new file mode 100644 index 0000000000..2c3b8ccaa3 --- /dev/null +++ b/components/libwebsockets/examples/client/sdkconfig.ci @@ -0,0 +1,13 @@ +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_TARGET_LINUX=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_CONNECT_IPV6=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120 diff --git a/components/libwebsockets/examples/client/sdkconfig.ci.mutual_auth b/components/libwebsockets/examples/client/sdkconfig.ci.mutual_auth new file mode 100644 index 0000000000..a4be691edd --- /dev/null +++ b/components/libwebsockets/examples/client/sdkconfig.ci.mutual_auth @@ -0,0 +1,18 @@ +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_TARGET_LINUX=n +CONFIG_WEBSOCKET_URI_FROM_STDIN=y +CONFIG_WEBSOCKET_URI_FROM_STRING=n +CONFIG_WEBSOCKET_PORT=8080 +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_CONNECT_IPV6=y +CONFIG_WS_OVER_TLS_MUTUAL_AUTH=y +CONFIG_WS_OVER_TLS_SKIP_COMMON_NAME_CHECK=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120 diff --git a/components/libwebsockets/examples/client/sdkconfig.ci.plain_tcp b/components/libwebsockets/examples/client/sdkconfig.ci.plain_tcp new file mode 100644 index 0000000000..eb6c3e4de7 --- /dev/null +++ b/components/libwebsockets/examples/client/sdkconfig.ci.plain_tcp @@ -0,0 +1,18 @@ +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_TARGET_LINUX=n +CONFIG_WEBSOCKET_URI_FROM_STDIN=y +CONFIG_WEBSOCKET_URI_FROM_STRING=n +CONFIG_WEBSOCKET_PORT=8080 +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_CONNECT_IPV6=y +CONFIG_WS_OVER_TLS_MUTUAL_AUTH=n +CONFIG_WS_OVER_TLS_SERVER_AUTH=n +CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120 diff --git a/components/libwebsockets/idf_component.yml b/components/libwebsockets/idf_component.yml new file mode 100644 index 0000000000..744c9857bb --- /dev/null +++ b/components/libwebsockets/idf_component.yml @@ -0,0 +1,5 @@ +version: "0.1.0" +url: https://github.com/espressif/esp-protocols/tree/master/components/libwebsockets +description: The component provides a simple ESP-IDF port of libwebsockets client. +dependencies: + idf: '>=5.2' diff --git a/components/libwebsockets/libwebsockets b/components/libwebsockets/libwebsockets new file mode 160000 index 0000000000..a74362ffdd --- /dev/null +++ b/components/libwebsockets/libwebsockets @@ -0,0 +1 @@ +Subproject commit a74362ffdd17b7f6293f675edef6d602096a1e29 diff --git a/components/libwebsockets/port/lws_port.c b/components/libwebsockets/port/lws_port.c new file mode 100644 index 0000000000..f5991effef --- /dev/null +++ b/components/libwebsockets/port/lws_port.c @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "stdio.h" +#include + +/* + * External function prototype for the wrapped 'mbedtls_ssl_handshake_step'. + * The "real" function is not being called, this prototype is just to improve + * the code readability. + */ +extern int __real_mbedtls_ssl_handshake_step(mbedtls_ssl_context *ssl); + +int __wrap_mbedtls_ssl_handshake_step( mbedtls_ssl_context *ssl ) +{ + int ret = 0; + + while (ssl->MBEDTLS_PRIVATE(state) != MBEDTLS_SSL_HANDSHAKE_OVER) { + ret = __real_mbedtls_ssl_handshake_step(ssl); + + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + continue; + } + + if (ret != 0) { + break; + } + } + + return ret; +} + +/* + * External function prototype for the wrapped 'lws_adopt_descriptor_vhost'. + * The "real" function is not being called, this prototype is just to improve + * the code readability. + */ +extern struct lws *__real_lws_adopt_descriptor_vhost(struct lws_vhost *vh, lws_adoption_type type, lws_sock_file_fd_type fd, const char *vh_prot_name, struct lws *parent); + +struct lws *__wrap_lws_adopt_descriptor_vhost(struct lws_vhost *vh, lws_adoption_type type, lws_sock_file_fd_type fd, const char *vh_prot_name, struct lws *parent) +{ + lws_adopt_desc_t info; + char nullstr[] = "(null)"; + memset(&info, 0, sizeof(info)); + + info.vh = vh; + info.type = type; + info.fd = fd; + info.vh_prot_name = vh_prot_name; + info.parent = parent; + info.fi_wsi_name = nullstr; + + return lws_adopt_descriptor_vhost_via_info(&info); +} diff --git a/test_app/CMakeLists.txt b/test_app/CMakeLists.txt index bda7a057a9..f2cf76d4be 100644 --- a/test_app/CMakeLists.txt +++ b/test_app/CMakeLists.txt @@ -15,6 +15,7 @@ set(EXTRA_COMPONENT_DIRS ../components/console_simple_init ../components/mbedtls_cxx ../components/sock_utils + ../components/libwebsockets ../components/mdns)