diff --git a/.github/workflows/mosq__build.yml b/.github/workflows/mosq__build.yml index 73cfb2fed9..f9d0bcdc8f 100644 --- a/.github/workflows/mosq__build.yml +++ b/.github/workflows/mosq__build.yml @@ -77,6 +77,20 @@ jobs: - name: Run Test working-directory: ${{ env.TEST_DIR }} run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + if ! pyenv versions --bare | grep -q '^3\.12\.6$'; then + echo "Installing Python 3.12.6..." + pyenv install -s 3.12.6 + fi + if ! pyenv virtualenvs --bare | grep -q '^myenv$'; then + echo "Creating pyenv virtualenv 'myenv'..." + pyenv virtualenv 3.12.6 myenv + fi + pyenv activate myenv + python --version python -m pip install pytest-embedded-serial-esp pytest-embedded-idf pytest-rerunfailures pytest-timeout pytest-ignore-test-results unzip ci/artifacts.zip -d ci for dir in `ls -d ci/build_*`; do @@ -180,6 +194,20 @@ jobs: - name: Run Test working-directory: ${{ env.TEST_DIR }} run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + if ! pyenv versions --bare | grep -q '^3\.12\.6$'; then + echo "Installing Python 3.12.6..." + pyenv install -s 3.12.6 + fi + if ! pyenv virtualenvs --bare | grep -q '^myenv$'; then + echo "Creating pyenv virtualenv 'myenv'..." + pyenv virtualenv 3.12.6 myenv + fi + pyenv activate myenv + python --version python -m pip install pytest-embedded-serial-esp pytest-embedded-idf pytest-rerunfailures pytest-timeout pytest-ignore-test-results "paho-mqtt<2" --upgrade unzip ci/artifacts.zip -d ci for dir in `ls -d ci/build_*`; do diff --git a/components/mosquitto/CMakeLists.txt b/components/mosquitto/CMakeLists.txt index ccef7144fc..5e71918d4d 100644 --- a/components/mosquitto/CMakeLists.txt +++ b/components/mosquitto/CMakeLists.txt @@ -90,6 +90,10 @@ if (CONFIG_MOSQ_ENABLE_SYS) endif() target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") +# Enable linker wrapping for mosquitto_unpwd_check to allow connection callback interception +# without modifying upstream code +target_link_options(${COMPONENT_LIB} INTERFACE "-Wl,--wrap=mosquitto_unpwd_check") + # Some mosquitto source unconditionally define `_GNU_SOURCE` which collides with IDF build system # producing warning: "_GNU_SOURCE" redefined # This workarounds this issue by undefining the macro for the selected files diff --git a/components/mosquitto/api.md b/components/mosquitto/api.md index 1e12195591..fb42c2a21d 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 int(\* | [**mosq\_connect\_cb\_t**](#typedef-mosq_connect_cb_t)
| | typedef void(\* | [**mosq\_message\_cb\_t**](#typedef-mosq_message_cb_t)
| ## Functions @@ -35,6 +36,8 @@ ESP port of mosquittto supports only the options in this configuration structure Variables: +- mosq\_connect\_cb\_t handle_connect_cb
On connect callback. If configured, user function is called whenever a client attempts to connect. The callback receives client\_id, username, password, and password length. Return 0 to accept the connection, non-zero to reject it. + - void(\* handle_message_cb
On message callback. If configured, user function is called whenever mosquitto processes a message. - const char \* host
Address on which the broker is listening for connections @@ -43,6 +46,12 @@ Variables: - 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_connect_cb_t` + +```c +typedef int(* mosq_connect_cb_t) (const char *client_id, const char *username, const char *password, int password_len); +``` + ### typedef `mosq_message_cb_t` ```c diff --git a/components/mosquitto/examples/broker/main/Kconfig.projbuild b/components/mosquitto/examples/broker/main/Kconfig.projbuild index 44c9e8f2d0..3a8bf66ee9 100644 --- a/components/mosquitto/examples/broker/main/Kconfig.projbuild +++ b/components/mosquitto/examples/broker/main/Kconfig.projbuild @@ -19,6 +19,15 @@ menu "Example Configuration" If enabled, it runs a local mqtt client connecting to the same endpoint ans the broker listens to + config EXAMPLE_BROKER_USE_BASIC_AUTH + bool "Use basic authentication (username/password)" + default n + help + If enabled, the broker will require username and password + authentication. The example uses "testuser" / "testpass" as + credentials. The client will also use these credentials when + connecting to the broker. + config EXAMPLE_BROKER_WITH_TLS bool "Use TLS" default y diff --git a/components/mosquitto/examples/broker/main/example_broker.c b/components/mosquitto/examples/broker/main/example_broker.c index 02632bb5f6..d54073f629 100644 --- a/components/mosquitto/examples/broker/main/example_broker.c +++ b/components/mosquitto/examples/broker/main/example_broker.c @@ -1,9 +1,10 @@ /* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include +#include #include "nvs_flash.h" #include "esp_event.h" #include "esp_netif.h" @@ -14,6 +15,45 @@ const static char *TAG = "mqtt_broker"; +/* Basic auth credentials for the example */ +#define EXAMPLE_USERNAME "testuser" +#define EXAMPLE_PASSWORD "testpass" + +#if CONFIG_EXAMPLE_BROKER_USE_BASIC_AUTH +/* Connection callback to validate username/password */ +static int example_connect_callback(const char *client_id, const char *username, const char *password, int password_len) +{ + ESP_LOGI(TAG, "Connection attempt from client_id='%s', username='%s'", client_id, username ? username : "(none)"); + + /* Check if username is provided */ + if (!username) { + ESP_LOGW(TAG, "Connection rejected: no username provided"); + return 1; /* Reject connection */ + } + + /* Check if password is provided */ + if (!password) { + ESP_LOGW(TAG, "Connection rejected: no password provided"); + return 1; /* Reject connection */ + } + + /* Validate username */ + if (strcmp(username, EXAMPLE_USERNAME) != 0) { + ESP_LOGW(TAG, "Connection rejected: invalid username '%s'", username); + return 1; /* Reject connection */ + } + + /* Validate password */ + if (strcmp(password, EXAMPLE_PASSWORD) != 0) { + ESP_LOGW(TAG, "Connection rejected: invalid password"); + return 1; /* Reject connection */ + } + + ESP_LOGI(TAG, "Connection accepted for client_id='%s', username='%s'", client_id, username); + return 0; /* Accept connection */ +} +#endif /* CONFIG_EXAMPLE_BROKER_USE_BASIC_AUTH */ + #if CONFIG_EXAMPLE_BROKER_WITH_TLS extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start"); extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end"); @@ -81,6 +121,10 @@ static void mqtt_app_start(struct mosq_broker_config *config) .broker.address.transport = MQTT_TRANSPORT_OVER_TCP, #endif .broker.address.port = config->port, +#if CONFIG_EXAMPLE_BROKER_USE_BASIC_AUTH + .credentials.username = "EXAMPLE_USERNAME", + .credentials.authentication.password = EXAMPLE_PASSWORD, +#endif }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); @@ -95,7 +139,16 @@ void app_main(void) ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(example_connect()); - struct mosq_broker_config config = { .host = CONFIG_EXAMPLE_BROKER_HOST, .port = CONFIG_EXAMPLE_BROKER_PORT, .tls_cfg = NULL }; + struct mosq_broker_config config = { + .host = CONFIG_EXAMPLE_BROKER_HOST, + .port = CONFIG_EXAMPLE_BROKER_PORT, + .tls_cfg = NULL, +#if CONFIG_EXAMPLE_BROKER_USE_BASIC_AUTH + .handle_connect_cb = example_connect_callback, +#else + .handle_connect_cb = NULL, +#endif + }; #if CONFIG_EXAMPLE_BROKER_RUN_LOCAL_MQTT_CLIENT mqtt_app_start(&config); diff --git a/components/mosquitto/port/broker.c b/components/mosquitto/port/broker.c index f78f651790..d169e221d7 100644 --- a/components/mosquitto/port/broker.c +++ b/components/mosquitto/port/broker.c @@ -102,6 +102,7 @@ void mosq_broker_stop(void) } extern mosq_message_cb_t g_mosq_message_callback; +extern mosq_connect_cb_t g_mosq_connect_callback; int mosq_broker_run(struct mosq_broker_config *broker_config) { @@ -130,6 +131,9 @@ int mosq_broker_run(struct mosq_broker_config *broker_config) if (broker_config->handle_message_cb) { g_mosq_message_callback = broker_config->handle_message_cb; } + if (broker_config->handle_connect_cb) { + g_mosq_connect_callback = broker_config->handle_connect_cb; + } db.config = &config; diff --git a/components/mosquitto/port/callbacks.c b/components/mosquitto/port/callbacks.c index dc0f2ad1b5..8b9306cc03 100644 --- a/components/mosquitto/port/callbacks.c +++ b/components/mosquitto/port/callbacks.c @@ -3,8 +3,9 @@ * * SPDX-License-Identifier: EPL-2.0 * - * SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2024-2025 Espressif Systems (Shanghai) CO LTD */ +#include #include "mosquitto_internal.h" #include "mosquitto_broker.h" #include "memory_mosq.h" @@ -16,6 +17,7 @@ #include "mosq_broker.h" mosq_message_cb_t g_mosq_message_callback = NULL; +mosq_connect_cb_t g_mosq_connect_callback = NULL; int mosquitto_callback_register( mosquitto_plugin_id_t *identifier, @@ -51,3 +53,39 @@ int plugin__handle_message(struct mosquitto *context, struct mosquitto_msg_store } return MOSQ_ERR_SUCCESS; } + +int __real_mosquitto_unpwd_check(struct mosquitto *context); + +/* Wrapper function to intercept mosquitto_unpwd_check calls via linker wrapping */ +int __wrap_mosquitto_unpwd_check(struct mosquitto *context) +{ + int rc; + int password_len = 0; + + /* Call user's connect callback if set */ + if (g_mosq_connect_callback) { + /* Extract password length if password is present. + * Note: MQTT passwords are binary data, but mosquitto stores them as null-terminated strings. + * If password contains null bytes, strlen() will not return the full length. + * This matches how mosquitto itself handles passwords in some security functions. */ + if (context->password) { + password_len = (int)strlen(context->password); + } + + /* Call user callback */ + rc = g_mosq_connect_callback( + context->id ? context->id : "", + context->username ? context->username : NULL, + context->password ? context->password : NULL, + password_len + ); + + /* If callback rejects (returns non-zero), return AUTH error immediately */ + if (rc != 0) { + return MOSQ_ERR_AUTH; + } + } + + /* Call the original function */ + return __real_mosquitto_unpwd_check(context); +} diff --git a/components/mosquitto/port/include/mosq_broker.h b/components/mosquitto/port/include/mosq_broker.h index 62e7c28290..2e836fcd2a 100644 --- a/components/mosquitto/port/include/mosq_broker.h +++ b/components/mosquitto/port/include/mosq_broker.h @@ -14,6 +14,8 @@ extern "C" { struct mosquitto__config; typedef void (*mosq_message_cb_t)(char *client, char *topic, char *data, int len, int qos, int retain); + +typedef int (*mosq_connect_cb_t)(const char *client_id, const char *username, const char *password, int password_len); /** * @brief Mosquitto configuration structure * @@ -33,6 +35,11 @@ struct mosq_broker_config { * On message callback. If configured, user function is called * whenever mosquitto processes a message. */ + mosq_connect_cb_t handle_connect_cb; /*!< On connect callback. If configured, user function is called + * whenever a client attempts to connect. The callback receives + * client_id, username, password, and password length. Return 0 to + * accept the connection, non-zero to reject it. + */ }; /**