feat: Add support for percent-encoding in password and it's corresponding test

Add support of percent-encoded characters when passing the username and password in URI

Closes https://github.com/espressif/esp-mqtt/issues/294
This commit is contained in:
Bogdan Kolendovskyy
2026-04-23 16:05:24 +02:00
parent f192911f6b
commit 32df7e27fc
18 changed files with 427 additions and 27 deletions
+3
View File
@@ -2,6 +2,9 @@
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/RapidCheck.cmake)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
list(APPEND EXTRA_COMPONENT_DIRS
+1 -1
View File
@@ -4,7 +4,7 @@ idf_component_register(SRCS "test_mqtt_client.cpp" "test_log_intercept.cpp" "te
target_compile_options(${COMPONENT_LIB} PUBLIC -fsanitize=address -Wno-missing-field-initializers)
target_link_options(${COMPONENT_LIB} PUBLIC -fsanitize=address)
target_link_libraries(${COMPONENT_LIB} PUBLIC Catch2::Catch2WithMain)
target_link_libraries(${COMPONENT_LIB} PUBLIC Catch2::Catch2WithMain rapidcheck rapidcheck_catch)
idf_component_get_property(mqtt mqtt COMPONENT_LIB)
target_compile_definitions(${mqtt} PRIVATE SOC_WIFI_SUPPORTED=1)
+37 -1
View File
@@ -4,14 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <algorithm>
#include <exception>
#include <memory>
#include <net/if.h>
#include <random>
#include <string>
#include <string_view>
#include <type_traits>
#include "esp_transport.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <rapidcheck.h>
#include "mqtt_client.h"
#include "test_log_intercept.hpp"
@@ -31,7 +34,6 @@ extern "C" {
#include "Mockidf_additions.h"
#endif
#include "Mockesp_timer.h"
/*
* The following functions are not directly called but the generation of them
* from cmock is broken, so we need to define them here.
@@ -99,6 +101,40 @@ SCENARIO("MQTT Client Operation")
REQUIRE(res == ESP_FAIL);
}
}
SECTION("Any well-formed URI is accepted") {
static constexpr std::array<const char *, 4> schemes = {
"mqtt", "mqtts", "ws", "wss"
};
auto host_char = rc::gen::element('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
rc::check("esp_mqtt_client_set_uri accepts well-formed URIs",
[&] {
auto scheme = *rc::gen::elementOf(schemes);
auto host = *rc::gen::container<std::string>(
*rc::gen::inRange<int>(1, 33), host_char).as("host");
auto path = *rc::gen::container<std::string>(
*rc::gen::inRange<int>(0, 17), host_char).as("path");
auto port = *rc::gen::maybe(rc::gen::inRange<uint16_t>(1, 65535)).as("port");
std::string uri = scheme;
uri += "://";
uri += host;
if (port) {
uri += ":";
uri += std::to_string(*port);
}
if (!path.empty())
{
uri += "/";
uri += path;
}
RC_ASSERT(esp_mqtt_client_set_uri(client.get(), uri.c_str()) == ESP_OK);
});
}
SECTION("User set interface to use") {
struct ifreq if_name = {};
strncpy(if_name.ifr_name, "custom", IFNAMSIZ - 1);
+9
View File
@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.16)
include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/RapidCheck.cmake)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
project(mqtt_utils_host_test)
+38
View File
@@ -0,0 +1,38 @@
# Host test for utility functions of MQTT client
This is a host test which tests utility functions from `mqtt_utils` subcomponent.
## Usage
To run the test suite you will need to set target to `Linux` and build the application. You will also need to enable `(Top) → Compiler options → Enable C++ run-time type info (RTTI)` (`CONFIG_COMPILER_CXX_RTTI`)
The default sdkconfig should set those options automatically, so usually just the command below will be enough to run it.
```bash
idf.py build monitor
```
## Example structure
- [main/idf_component.yml](main/idf_component.yml) adds a dependency on `espressif/catch2` component.
- [main/CMakeLists.txt](main/CMakeLists.txt) specifies the source files and registers the `main` component with `WHOLE_ARCHIVE` option enabled.
- [CMakeLists.txt](CMakeLists.txt) includes CPM package manager and adds rapidcheck package
- [main/test_main.cpp](main/test_main.cpp) implements the application entry point which calls the test runner.
- [main/test_cases.cpp](main/test_cases.cpp) implements test cases.
- [sdkconfig.defaults](sdkconfig.defaults) sets the options required to run the example: enables C++ exceptions, increases the size of the `main` task stack, and enables C++ runtime type info.
## Expected output
```
Randomness seeded to: 2196951535
Using configuration: seed=951423191499285688
- Testing the decoding of random string
OK, passed 100 tests
- Testing the decoding of random URI
OK, passed 100 tests
===============================================================================
All tests passed (403 assertions in 1 test case)
Test passed.
```
@@ -0,0 +1,8 @@
idf_component_register(SRCS "test_main.cpp"
"test_cases.cpp"
INCLUDE_DIRS "."
WHOLE_ARCHIVE)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../../lib/mqtt_utils ${CMAKE_CURRENT_BINARY_DIR}/mqtt_utils)
target_link_libraries(${COMPONENT_LIB} PUBLIC idf::mqtt::utils Catch2 rapidcheck)
@@ -0,0 +1,3 @@
dependencies:
espressif/catch2:
version: "*"
@@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <catch2/catch_test_macros.hpp>
#include <cstring>
#include <iostream>
#include <iomanip>
#include <format>
#include "rapidcheck.h"
#include "mqtt_utils.h"
struct percent_encoded_string {
std::string value;
};
struct uri_username {
std::string value;
};
struct uri_password {
std::string value;
};
struct uri_host {
std::string value;
};
struct URI {
uri_username username;
uri_password password;
uri_host host;
};
namespace rc
{
template<typename T>
struct base {
static Gen<T> arbitrary()
{
return gen::build<T>(gen::set(&T::value));
}
};
template<>
struct Arbitrary<percent_encoded_string> : base<percent_encoded_string> { };
template<>
struct Arbitrary<uri_username> : base<uri_username> { };
template<>
struct Arbitrary<uri_password> : base<uri_password> { };
template<>
struct Arbitrary<uri_host> : base<uri_host> { };
template<>
struct Arbitrary<URI> {
static Gen<URI> arbitrary()
{
return gen::build<URI>(gen::set(&URI::username), gen::set(&URI::password), gen::set(&URI::host));
}
};
}
std::string percent_encode(std::string input, std::string filter = "`~!@#$%^&*(){}[]:\";'<>?,./\\-+=|")
{
std::string encoded_str;
for (char c : input) {
if (filter.find(c) == std::string::npos && std::isprint(c)) {
encoded_str.push_back(c);
} else {
encoded_str += std::format("%{:02x}", c);
}
}
return encoded_str;
}
TEST_CASE("Parse percent-encoded data")
{
SECTION("Zero-length string as an input") {
char c_style_zero_length_string[1] = {0};
REQUIRE(esp_mqtt_decode_percent_encoded_string(c_style_zero_length_string) == 0);
}
SECTION("Constant string as an input") {
std::string known_value_string = "p%40ssw0rd";
std::vector<char> known_value_tmp_vector{std::begin(known_value_string), std::end(known_value_string)};
known_value_tmp_vector.push_back('\0');
esp_mqtt_decode_percent_encoded_string(known_value_tmp_vector.data());
std::string result{known_value_tmp_vector.data()};
REQUIRE(result == "p@ssw0rd");
}
SECTION("Decoding random string") {
rc::check("Testing the decoding of random string",
[](const std::string & str) {
RC_PRE(str.length() > 0);
std::string encoded_str = percent_encode(str);
RC_PRE([encoded_str]() -> bool {
std::string filter = "`~!@#$^&*(){}[]:\";'<>?,./\\-+=|";
for (char c : encoded_str) {
if (filter.find(c) != std::string::npos) {
return false;
}
}
return true;
}());
char *buffer = (char *) malloc(encoded_str.length() + 1);
strcpy(buffer, encoded_str.c_str());
std::vector<char> encoded_str_tmp_vector{std::begin(encoded_str), std::end(encoded_str)};
encoded_str_tmp_vector.push_back('\0');
int len = esp_mqtt_decode_percent_encoded_string(encoded_str_tmp_vector.data());
REQUIRE(len == str.length());
std::string result{encoded_str_tmp_vector.data()};
REQUIRE(result == str);
});
}
SECTION("Decoding percent-encoding in URIs") {
rc::check("Testing the decoding of random URI",
[](const URI & uri) {
RC_PRE(uri.host.value.length() > 0);
RC_PRE(uri.username.value.length() > 0);
RC_PRE(uri.password.value.length() > 0);
std::string complete_uri_raw = uri.username.value + ":" + uri.password.value + "@" + uri.host.value;
std::string complete_uri_enc = percent_encode(uri.username.value) + ":" + percent_encode(uri.password.value) + "@" + percent_encode(uri.host.value);
// Verify that there are no prohibited characters in the encoded username
RC_PRE([complete_uri_enc]() -> bool {
// I have removed /, :, and @ as they are permitted symbols in URI
std::string filter = "`~!#$^&*(){}[]\";'<>?,.\\-+=|";
for (char c : complete_uri_enc) {
if (filter.find(c) != std::string::npos) {
std::cout << "Found '" << c << "' in \"" << complete_uri_enc << "\"" << std::endl;
return false;
}
}
return true;
}());
std::vector<char> complete_uri_tmp_vector{std::begin(complete_uri_enc), std::end(complete_uri_enc)};
complete_uri_tmp_vector.push_back('\0');
int len = esp_mqtt_decode_percent_encoded_string(complete_uri_tmp_vector.data());
REQUIRE(len == complete_uri_raw.length());
std::string result{complete_uri_tmp_vector.data()};
REQUIRE(result == complete_uri_raw);
});
}
}
@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <catch2/catch_session.hpp>
extern "C" void app_main(void)
{
int argc = 1;
const char *argv[2] = {
"target_test_main",
NULL
};
auto result = Catch::Session().run(argc, argv);
if (result != 0) {
printf("Test failed with result %d\n", result);
exit(1);
} else {
printf("Test passed.\n");
exit(0);
}
}
@@ -0,0 +1,5 @@
CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=10000
CONFIG_IDF_TARGET="linux"
CONFIG_COMPILER_CXX_RTTI=y
CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y