mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
1fa0db8d44
For compatibility reasons, support also transports separately if the transport is used before attaching to parent list. In this case we create an internal context which is independent on the foundation transport and used preferably
433 lines
18 KiB
C
433 lines
18 KiB
C
#include "unity.h"
|
|
|
|
#include "esp_transport.h"
|
|
#include "esp_transport_tcp.h"
|
|
#include "esp_transport_ssl.h"
|
|
#include "esp_transport_ws.h"
|
|
#include "test_utils.h"
|
|
#include "esp_log.h"
|
|
#include "lwip/err.h"
|
|
#include "lwip/sockets.h"
|
|
#include "lwip/sys.h"
|
|
#include <lwip/netdb.h>
|
|
#include "freertos/event_groups.h"
|
|
|
|
#define TCP_CONNECT_DONE (1)
|
|
#define TCP_LISTENER_DONE (2)
|
|
#define TCP_ACCEPTOR_DONE (4)
|
|
#define TCP_LISTENER_ACCEPTED (8)
|
|
#define TCP_LISTENER_READY (16)
|
|
|
|
struct tcp_connect_task_params {
|
|
int timeout_ms;
|
|
int port;
|
|
EventGroupHandle_t tcp_connect_done;
|
|
int ret;
|
|
int listen_sock;
|
|
int accepted_sock;
|
|
int last_connect_sock;
|
|
bool tcp_listener_failed;
|
|
esp_transport_handle_t transport_under_test;
|
|
bool accept_connection;
|
|
bool consume_sock_backlog;
|
|
};
|
|
|
|
/**
|
|
* @brief Recursively connects with a new socket to loopback interface until the last one blocks.
|
|
* The last socket is closed upon test teardown, that initiates recursive cleanup (close) for all
|
|
* active/connected sockets.
|
|
*/
|
|
static void connect_once(struct tcp_connect_task_params *params)
|
|
{
|
|
struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
|
.sin_family = AF_INET,
|
|
.sin_port = htons(params->port) };
|
|
int connect_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
if (connect_sock < 0) {
|
|
params->tcp_listener_failed = true;
|
|
return;
|
|
}
|
|
params->last_connect_sock = connect_sock;
|
|
int err = connect(connect_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4));
|
|
if (err != 0) {
|
|
// The last connection is expected to fail here, since the both sockets get closed on test cleanup
|
|
return;
|
|
}
|
|
connect_once(params);
|
|
close(connect_sock);
|
|
}
|
|
|
|
/**
|
|
* @brief creates a listener (and an acceptor if configured)
|
|
*
|
|
* if consume_sock_backlog set: connect as many times as possible to prepare an endpoint which
|
|
* would make the client block but not complete TCP handshake
|
|
*
|
|
* if accept_connection set: waiting normally for connection creating an acceptor to mimic tcp-transport endpoint
|
|
*/
|
|
static void localhost_listener(void *pvParameters)
|
|
{
|
|
const char* TAG = "tcp_transport_test";
|
|
struct tcp_connect_task_params *params = pvParameters;
|
|
struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_ANY),
|
|
.sin_family = AF_INET,
|
|
.sin_port = htons(params->port) };
|
|
// Create listener socket and bind it to ANY address
|
|
params->listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
int opt = 1;
|
|
setsockopt(params->listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
|
|
|
if (params->listen_sock < 0) {
|
|
ESP_LOGE(TAG, "Unable to create socket");
|
|
params->tcp_listener_failed = true;
|
|
goto failed;
|
|
}
|
|
int err = bind(params->listen_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4));
|
|
if (err != 0) {
|
|
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
|
|
params->tcp_listener_failed = true;
|
|
goto failed;
|
|
}
|
|
|
|
// Listen with backlog set to a low number
|
|
err = listen(params->listen_sock, 4);
|
|
if (err != 0) {
|
|
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
|
|
params->tcp_listener_failed = true;
|
|
goto failed;
|
|
}
|
|
|
|
// Listener is ready at this point
|
|
xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_READY);
|
|
|
|
if (params->consume_sock_backlog) {
|
|
// Ideally we would set backlog to 0, but since this is an implementation specific recommendation parameter,
|
|
// we recursively create sockets and try to connect to this listener in order to consume the backlog. After
|
|
// the backlog is consumed, the last connection blocks (waiting for accept), but at that point we are sure
|
|
// that any other connection would also block
|
|
connect_once(params);
|
|
} else if (params->accept_connection) {
|
|
struct sockaddr_storage source_addr;
|
|
socklen_t addr_len = sizeof(source_addr);
|
|
params->accepted_sock = accept(params->listen_sock, (struct sockaddr *)&source_addr, &addr_len);
|
|
if (params->accepted_sock < 0) {
|
|
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
|
|
goto failed;
|
|
}
|
|
xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_ACCEPTED); // Mark the socket as accepted
|
|
// ...and wait for the "acceptor" tests to finish
|
|
xEventGroupWaitBits(params->tcp_connect_done, TCP_ACCEPTOR_DONE, true, true, params->timeout_ms * 10);
|
|
}
|
|
|
|
failed:
|
|
xEventGroupSetBits(params->tcp_connect_done, TCP_LISTENER_DONE);
|
|
vTaskSuspend(NULL);
|
|
}
|
|
|
|
static void tcp_connect_task(void *pvParameters)
|
|
{
|
|
struct tcp_connect_task_params *params = pvParameters;
|
|
|
|
params->ret = esp_transport_connect(params->transport_under_test, "localhost", params->port, params->timeout_ms);
|
|
if (params->accept_connection) {
|
|
// If we test the accepted connection, need to wait until the test completes
|
|
xEventGroupWaitBits(params->tcp_connect_done, TCP_ACCEPTOR_DONE, true, true, params->timeout_ms * 10);
|
|
}
|
|
xEventGroupSetBits(params->tcp_connect_done, TCP_CONNECT_DONE);
|
|
vTaskSuspend(NULL);
|
|
}
|
|
|
|
|
|
TEST_CASE("tcp_transport: init and deinit transport list", "[tcp_transport][leaks=0]")
|
|
{
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_list_add(transport_list, tcp, "tcp");
|
|
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list));
|
|
}
|
|
|
|
TEST_CASE("tcp_transport: using ssl transport separately", "[tcp_transport][leaks=0]")
|
|
{
|
|
esp_transport_handle_t h = esp_transport_ssl_init();
|
|
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(h));
|
|
}
|
|
|
|
TEST_CASE("tcp_transport: using ws transport separately", "[tcp_transport][leaks=0]")
|
|
{
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_handle_t ws = esp_transport_ws_init(tcp);
|
|
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(ws));
|
|
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_destroy(tcp));
|
|
}
|
|
|
|
static void transport_connection_timeout_test(esp_transport_handle_t transport_under_test)
|
|
{
|
|
// This case simulates connection timeout running tcp connect asynchronously with other socket connection
|
|
// consuming entire socket listener backlog.
|
|
// Important: Both tasks must run on the same core, with listener's prio higher to make sure that
|
|
// 1) first the localhost_listener() creates and connects all sockets until the last one blocks
|
|
// 2) before the tcp_connect_task() attempts to connect and thus fails with connection timeout
|
|
|
|
struct tcp_connect_task_params params = { .tcp_connect_done = xEventGroupCreate(),
|
|
.timeout_ms = 200,
|
|
.port = 80,
|
|
.consume_sock_backlog = true,
|
|
.transport_under_test = transport_under_test };
|
|
TickType_t max_wait = pdMS_TO_TICKS(params.timeout_ms * 10);
|
|
TaskHandle_t localhost_listener_task_handle = NULL;
|
|
TaskHandle_t tcp_connect_task_handle = NULL;
|
|
|
|
test_case_uses_tcpip();
|
|
|
|
// Create listener and connect it with as many sockets until the last one blocks
|
|
xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, (void*)¶ms, 5, &localhost_listener_task_handle, 0);
|
|
|
|
// Perform tcp-connect in a separate task to check asynchronously for the timeout
|
|
xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, (void*)¶ms, 4, &tcp_connect_task_handle, 0);
|
|
|
|
// Roughly measure tick-time spent while trying to connect
|
|
TickType_t start = xTaskGetTickCount();
|
|
EventBits_t bits = xEventGroupWaitBits(params.tcp_connect_done, TCP_CONNECT_DONE, true, true, max_wait);
|
|
TickType_t end = xTaskGetTickCount();
|
|
|
|
TEST_ASSERT_EQUAL(TCP_CONNECT_DONE, TCP_CONNECT_DONE & bits); // Connection has finished
|
|
TEST_ASSERT_EQUAL(-1, params.ret); // Connection failed with -1
|
|
|
|
// Test connection attempt took expected timeout value
|
|
TEST_ASSERT_INT_WITHIN(pdMS_TO_TICKS(params.timeout_ms/5), pdMS_TO_TICKS(params.timeout_ms), end-start);
|
|
|
|
// Closing both parties of the last "blocking" connection to unwind localhost_listener() and let other connected sockets closed
|
|
close(params.listen_sock);
|
|
close(params.last_connect_sock);
|
|
|
|
// Cleanup
|
|
xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_DONE, true, true, max_wait);
|
|
TEST_ASSERT_EQUAL(false, params.tcp_listener_failed);
|
|
vEventGroupDelete(params.tcp_connect_done);
|
|
test_utils_task_delete(localhost_listener_task_handle);
|
|
test_utils_task_delete(tcp_connect_task_handle);
|
|
}
|
|
|
|
TEST_CASE("tcp_transport: connect timeout", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_list_add(transport_list, tcp, "tcp");
|
|
|
|
transport_connection_timeout_test(tcp);
|
|
esp_transport_close(tcp);
|
|
esp_transport_list_destroy(transport_list);
|
|
}
|
|
|
|
TEST_CASE("ssl_transport: connect timeout", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_list_add(transport_list, tcp, "tcp");
|
|
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
|
esp_transport_list_add(transport_list, ssl, "ssl");
|
|
|
|
transport_connection_timeout_test(ssl);
|
|
esp_transport_close(tcp);
|
|
esp_transport_close(ssl);
|
|
esp_transport_list_destroy(transport_list);
|
|
}
|
|
|
|
TEST_CASE("transport: init and deinit multiple transport items", "[tcp_transport][leaks=0]")
|
|
{
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_list_add(transport_list, tcp, "tcp");
|
|
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
|
esp_transport_list_add(transport_list, ssl, "ssl");
|
|
esp_transport_handle_t ws = esp_transport_ws_init(tcp);
|
|
esp_transport_list_add(transport_list, ws, "ws");
|
|
esp_transport_handle_t wss = esp_transport_ws_init(ssl);
|
|
esp_transport_list_add(transport_list, wss, "wss");
|
|
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_list_destroy(transport_list));
|
|
}
|
|
|
|
// This is a private API of the tcp transport, but needed for socket operation tests
|
|
int esp_transport_get_socket(esp_transport_handle_t t);
|
|
|
|
// Structures and types for passing socket options
|
|
enum expected_sock_option_types {
|
|
SOCK_OPT_TYPE_BOOL,
|
|
SOCK_OPT_TYPE_INT,
|
|
};
|
|
|
|
struct expected_sock_option {
|
|
int level;
|
|
int optname;
|
|
int optval;
|
|
enum expected_sock_option_types opttype;
|
|
};
|
|
|
|
static void socket_operation_test(esp_transport_handle_t transport_under_test,
|
|
const struct expected_sock_option expected_opts[], size_t sock_options_len)
|
|
{
|
|
struct tcp_connect_task_params params = { .tcp_connect_done = xEventGroupCreate(),
|
|
.timeout_ms = 200,
|
|
.port = 80,
|
|
.accept_connection = true,
|
|
.transport_under_test = transport_under_test };
|
|
TickType_t max_wait = pdMS_TO_TICKS(params.timeout_ms * 10);
|
|
TaskHandle_t localhost_listener_task_handle = NULL;
|
|
TaskHandle_t tcp_connect_task_handle = NULL;
|
|
|
|
test_case_uses_tcpip();
|
|
|
|
// Create a listener and wait for it to be ready
|
|
xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, (void*)¶ms, 5, &localhost_listener_task_handle, 0);
|
|
xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_READY, true, true, max_wait);
|
|
// Perform tcp-connect in a separate task
|
|
xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, (void*)¶ms, 6, &tcp_connect_task_handle, 0);
|
|
|
|
// Wait till the connection gets accepted to get the client's socket
|
|
xEventGroupWaitBits(params.tcp_connect_done, TCP_LISTENER_ACCEPTED, true, true, max_wait);
|
|
int sock = esp_transport_get_socket(params.transport_under_test);
|
|
for (int i=0; i<sock_options_len; ++i) {
|
|
int value = -1;
|
|
socklen_t optlen = (socklen_t)sizeof(value);
|
|
TEST_ASSERT_EQUAL(getsockopt(sock, expected_opts[i].level, expected_opts[i].optname,
|
|
(void*)&value, &optlen), 0);
|
|
if (expected_opts[i].opttype == SOCK_OPT_TYPE_BOOL) {
|
|
TEST_ASSERT_EQUAL((bool)value, (bool) expected_opts[i].optval);
|
|
} else if (expected_opts[i].opttype == SOCK_OPT_TYPE_INT) {
|
|
TEST_ASSERT_EQUAL(value, expected_opts[i].optval);
|
|
} else {
|
|
TEST_FAIL_MESSAGE("Unsupported socket option type");
|
|
}
|
|
}
|
|
|
|
close(sock); // close the tcp_transport's socket so we don't have to wait for connection timeout
|
|
xEventGroupSetBits(params.tcp_connect_done, TCP_ACCEPTOR_DONE);
|
|
xEventGroupWaitBits(params.tcp_connect_done, TCP_CONNECT_DONE, true, true, max_wait);
|
|
// Closing the listener and acceptor sockets
|
|
close(params.listen_sock);
|
|
close(params.accepted_sock);
|
|
|
|
// Cleanup
|
|
TEST_ASSERT_EQUAL(false, params.tcp_listener_failed);
|
|
vEventGroupDelete(params.tcp_connect_done);
|
|
test_utils_task_delete(localhost_listener_task_handle);
|
|
test_utils_task_delete(tcp_connect_task_handle);
|
|
}
|
|
|
|
static void tcp_transport_keepalive_test(esp_transport_handle_t transport_under_test, esp_transport_keep_alive_t *config)
|
|
{
|
|
static struct expected_sock_option expected_opts[4] = {
|
|
{ .level = SOL_SOCKET, .optname = SO_KEEPALIVE, .optval = 1, .opttype = SOCK_OPT_TYPE_BOOL },
|
|
{ .level = IPPROTO_TCP },
|
|
{ .level = IPPROTO_TCP },
|
|
{ .level = IPPROTO_TCP }
|
|
};
|
|
|
|
expected_opts[1].optname = TCP_KEEPIDLE;
|
|
expected_opts[1].optval = config->keep_alive_idle;
|
|
expected_opts[2].optname = TCP_KEEPINTVL;
|
|
expected_opts[2].optval = config->keep_alive_interval;
|
|
expected_opts[3].optname = TCP_KEEPCNT;
|
|
expected_opts[3].optval = config->keep_alive_count;
|
|
|
|
socket_operation_test(transport_under_test, expected_opts, sizeof(expected_opts)/sizeof(struct expected_sock_option));
|
|
}
|
|
|
|
TEST_CASE("tcp_transport: Keep alive test", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
|
esp_transport_list_add(transport_list, tcp, "tcp");
|
|
|
|
// Perform the test
|
|
esp_transport_keep_alive_t keep_alive_cfg = {
|
|
.keep_alive_interval = 5,
|
|
.keep_alive_idle = 4,
|
|
.keep_alive_enable = true,
|
|
.keep_alive_count = 3 };
|
|
esp_transport_tcp_set_keep_alive(tcp, &keep_alive_cfg);
|
|
|
|
tcp_transport_keepalive_test(tcp, &keep_alive_cfg);
|
|
|
|
// Cleanup
|
|
esp_transport_close(tcp);
|
|
esp_transport_list_destroy(transport_list);
|
|
}
|
|
|
|
TEST_CASE("ssl_transport: Keep alive test", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
|
esp_transport_list_add(transport_list, ssl, "ssl");
|
|
esp_tls_init_global_ca_store();
|
|
esp_transport_ssl_enable_global_ca_store(ssl);
|
|
|
|
// Perform the test
|
|
esp_transport_keep_alive_t keep_alive_cfg = {
|
|
.keep_alive_interval = 2,
|
|
.keep_alive_idle = 3,
|
|
.keep_alive_enable = true,
|
|
.keep_alive_count = 4 };
|
|
esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg);
|
|
|
|
tcp_transport_keepalive_test(ssl, &keep_alive_cfg);
|
|
|
|
// Cleanup
|
|
esp_transport_close(ssl);
|
|
esp_transport_list_destroy(transport_list);
|
|
}
|
|
|
|
TEST_CASE("ws_transport: Keep alive test", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_list_handle_t transport_list = esp_transport_list_init();
|
|
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
|
esp_transport_list_add(transport_list, ssl, "ssl");
|
|
esp_tls_init_global_ca_store();
|
|
esp_transport_ssl_enable_global_ca_store(ssl);
|
|
esp_transport_handle_t ws = esp_transport_ws_init(ssl);
|
|
esp_transport_list_add(transport_list, ws, "wss");
|
|
|
|
// Perform the test
|
|
esp_transport_keep_alive_t keep_alive_cfg = {
|
|
.keep_alive_interval = 1,
|
|
.keep_alive_idle = 2,
|
|
.keep_alive_enable = true,
|
|
.keep_alive_count = 3 };
|
|
esp_transport_tcp_set_keep_alive(ssl, &keep_alive_cfg);
|
|
|
|
tcp_transport_keepalive_test(ws, &keep_alive_cfg);
|
|
|
|
// Cleanup
|
|
esp_transport_close(ssl);
|
|
esp_transport_list_destroy(transport_list);
|
|
}
|
|
|
|
// Note: This functionality is tested and kept only for compatibility reasons with IDF <= 4.x
|
|
// It is strongly encouraged to use transport within lists only
|
|
TEST_CASE("ssl_transport: Check that parameters (keepalive) are set independently on the list", "[tcp_transport]")
|
|
{
|
|
// Init the transport under test
|
|
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
|
esp_tls_init_global_ca_store();
|
|
esp_transport_ssl_enable_global_ca_store(ssl);
|
|
|
|
// Perform the test
|
|
esp_transport_keep_alive_t keep_alive_cfg = {
|
|
.keep_alive_interval = 2,
|
|
.keep_alive_idle = 4,
|
|
.keep_alive_enable = true,
|
|
.keep_alive_count = 3 };
|
|
esp_transport_ssl_set_keep_alive(ssl, &keep_alive_cfg);
|
|
|
|
tcp_transport_keepalive_test(ssl, &keep_alive_cfg);
|
|
|
|
// Cleanup
|
|
esp_transport_close(ssl);
|
|
esp_transport_destroy(ssl);
|
|
}
|