diff --git a/components/esp-tls/esp_tls.c b/components/esp-tls/esp_tls.c index 8278aaf02f..fe5ffccc01 100644 --- a/components/esp-tls/esp_tls.c +++ b/components/esp-tls/esp_tls.c @@ -267,13 +267,13 @@ static esp_err_t esp_tls_set_socket_non_blocking(int fd, bool non_blocking) return ESP_OK; } -static esp_err_t esp_tcp_connect(const char *host, int hostlen, int port, int *sockfd, const esp_tls_t *tls, const esp_tls_cfg_t *cfg) +static inline esp_err_t tcp_connect(const char *host, int hostlen, int port, const esp_tls_cfg_t *cfg, esp_tls_error_handle_t error_handle, int *sockfd) { struct sockaddr_storage address; int fd; esp_err_t ret = esp_tls_hostname_to_fd(host, hostlen, port, &address, &fd); if (ret != ESP_OK) { - ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ESP_TLS_ERR_TYPE_SYSTEM, errno); + ESP_INT_EVENT_TRACKER_CAPTURE(error_handle, ESP_TLS_ERR_TYPE_SYSTEM, errno); return ret; } @@ -311,7 +311,7 @@ static esp_err_t esp_tcp_connect(const char *host, int hostlen, int port, int *s int res = select(fd+1, NULL, &fdset, NULL, &tv); if (res < 0) { ESP_LOGE(TAG, "[sock=%d] select() error: %s", fd, strerror(errno)); - ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ESP_TLS_ERR_TYPE_SYSTEM, errno); + ESP_INT_EVENT_TRACKER_CAPTURE(error_handle, ESP_TLS_ERR_TYPE_SYSTEM, errno); goto err; } else if (res == 0) { @@ -328,7 +328,7 @@ static esp_err_t esp_tcp_connect(const char *host, int hostlen, int port, int *s goto err; } else if (sockerr) { - ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ESP_TLS_ERR_TYPE_SYSTEM, sockerr); + ESP_INT_EVENT_TRACKER_CAPTURE(error_handle, ESP_TLS_ERR_TYPE_SYSTEM, sockerr); ESP_LOGE(TAG, "[sock=%d] delayed connect error: %s", fd, strerror(sockerr)); goto err; } @@ -371,7 +371,7 @@ static int esp_tls_low_level_conn(const char *hostname, int hostlen, int port, c _esp_tls_net_init(tls); tls->is_tls = true; } - if ((esp_ret = esp_tcp_connect(hostname, hostlen, port, &tls->sockfd, tls, cfg)) != ESP_OK) { + if ((esp_ret = tcp_connect(hostname, hostlen, port, cfg, tls->error_handle, &tls->sockfd)) != ESP_OK) { ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ESP_TLS_ERR_TYPE_ESP, esp_ret); return -1; } @@ -440,6 +440,17 @@ static int esp_tls_low_level_conn(const char *hostname, int hostlen, int port, c return -1; } +/** + * @brief Create a new plain TCP connection + */ +esp_err_t esp_tls_plain_tcp_connect(const char *host, int hostlen, int port, const esp_tls_cfg_t *cfg, esp_tls_error_handle_t error_handle, int *sockfd) +{ + if (sockfd == NULL || error_handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + return tcp_connect(host, hostlen, port, cfg, error_handle, sockfd); +} + /** * @brief Create a new TLS/SSL connection */ diff --git a/components/esp-tls/esp_tls.h b/components/esp-tls/esp_tls.h index 72a78c5e7d..3cdedc0998 100644 --- a/components/esp-tls/esp_tls.h +++ b/components/esp-tls/esp_tls.h @@ -171,7 +171,10 @@ typedef struct esp_tls_cfg { void *ds_data; /*!< Pointer for digital signature peripheral context */ bool is_plain_tcp; /*!< Use non-TLS connection: When set to true, the esp-tls uses - plain TCP transport rather then TLS/SSL connection */ + plain TCP transport rather then TLS/SSL connection. + Note, that it is possible to connect using a plain tcp transport + directly with esp_tls_plain_tcp_connect() API */ + struct ifreq *if_name; /*!< The name of interface for data to go through. Use the default interface without setting */ } esp_tls_cfg_t; @@ -599,6 +602,21 @@ int esp_tls_server_session_create(esp_tls_cfg_server_t *cfg, int sockfd, esp_tls void esp_tls_server_session_delete(esp_tls_t *tls); #endif /* ! CONFIG_ESP_TLS_SERVER */ +/** + * @brief Creates a plain TCP connection, returning a valid socket fd on success or an error handle + * + * @param[in] host Hostname of the host. + * @param[in] hostlen Length of hostname. + * @param[in] port Port number of the host. + * @param[in] cfg ESP-TLS configuration as esp_tls_cfg_t. + * @param[out] error_handle ESP-TLS error handle holding potential errors occurred during connection + * @param[out] sockfd Socket descriptor if successfully connected on TCP layer + * @return ESP_OK on success + * ESP_ERR_INVALID_ARG if invalid output parameters + * ESP-TLS based error codes on failure + */ +esp_err_t esp_tls_plain_tcp_connect(const char *host, int hostlen, int port, const esp_tls_cfg_t *cfg, esp_tls_error_handle_t error_handle, int *sockfd); + #ifdef __cplusplus } #endif diff --git a/components/tcp_transport/test/tcp_transport_fixtures.h b/components/tcp_transport/test/tcp_transport_fixtures.h new file mode 100644 index 0000000000..dc93f89b92 --- /dev/null +++ b/components/tcp_transport/test/tcp_transport_fixtures.h @@ -0,0 +1,41 @@ +#ifndef _TCP_TRANSPORT_FIXTURES_H_ +#define _TCP_TRANSPORT_FIXTURES_H_ + +/** + * @brief 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; +}; + +/** + * @brief Helper test functions for timeout connection tests + * + * 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 + */ +void tcp_transport_test_connection_timeout(esp_transport_handle_t transport_under_test); + + +/** + * @brief Helper test function to check socket options configured separately by transports + * + * This sets up the connection test to start two tasks, but unlike tcp_transport_test_connection_timeout, + * here we just let the connection to happen or at least open on TCP layer so we get the internal socket + * descriptor. While the connection is in progress or connected, we can check the socket options configured + * by the tcp_transport API. + */ +void tcp_transport_test_socket_options(esp_transport_handle_t transport_under_test, bool async, + const struct expected_sock_option *expected_opts, size_t sock_options_len); + +#endif //_TCP_TRANSPORT_FIXTURES_H_ diff --git a/components/tcp_transport/test/test_transport.c b/components/tcp_transport/test/test_transport.c deleted file mode 100644 index 51d48f7188..0000000000 --- a/components/tcp_transport/test/test_transport.c +++ /dev/null @@ -1,454 +0,0 @@ -#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 -#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; -}; - -#define TEST_TRANSPORT_BIND_IFNAME() \ - struct ifreq ifr; \ - ifr.ifr_name[0] = 'l'; \ - ifr.ifr_name[1] = 'o'; \ - ifr.ifr_name[2] = '\0'; - -/** - * @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; ikeep_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); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_tcp_set_interface_name(tcp, &ifr); - - 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); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - 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); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - 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); - - // Bind device interface to loopback - TEST_TRANSPORT_BIND_IFNAME(); - esp_transport_ssl_set_interface_name(ssl, &ifr); - - tcp_transport_keepalive_test(ssl, &keep_alive_cfg); - - // Cleanup - esp_transport_close(ssl); - esp_transport_destroy(ssl); -} diff --git a/components/tcp_transport/test/test_transport_basic.c b/components/tcp_transport/test/test_transport_basic.c new file mode 100644 index 0000000000..9e7f92a661 --- /dev/null +++ b/components/tcp_transport/test/test_transport_basic.c @@ -0,0 +1,44 @@ +#include "unity.h" + +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "esp_transport_ssl.h" +#include "esp_transport_ws.h" +#include "esp_log.h" + + +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)); +} + +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)); +} diff --git a/components/tcp_transport/test/test_transport_connect.c b/components/tcp_transport/test/test_transport_connect.c new file mode 100644 index 0000000000..514c8cfdf8 --- /dev/null +++ b/components/tcp_transport/test/test_transport_connect.c @@ -0,0 +1,178 @@ +#include "unity.h" +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "esp_transport_ssl.h" +#include "esp_transport_ws.h" +#include "esp_log.h" +#include "lwip/sockets.h" +#include "tcp_transport_fixtures.h" + + +#define TEST_TRANSPORT_BIND_IFNAME() \ + struct ifreq ifr; \ + ifr.ifr_name[0] = 'l'; \ + ifr.ifr_name[1] = 'o'; \ + ifr.ifr_name[2] = '\0'; + + +static void tcp_transport_keepalive_test(esp_transport_handle_t transport_under_test, bool async, 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; + + tcp_transport_test_socket_options(transport_under_test, async, expected_opts, + sizeof(expected_opts) / sizeof(struct expected_sock_option)); +} + +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"); + + tcp_transport_test_connection_timeout(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"); + + tcp_transport_test_connection_timeout(ssl); + esp_transport_close(tcp); + esp_transport_close(ssl); + esp_transport_list_destroy(transport_list); +} + +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); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_tcp_set_interface_name(tcp, &ifr); + + // Run the test for both sync and async_connect + tcp_transport_keepalive_test(tcp, true, &keep_alive_cfg); + tcp_transport_keepalive_test(tcp, false, &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); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(ssl, &ifr); + + // Run the test for async_connect only + // - TLS connection would connect on socket level only, returning tls-handshake in progress + tcp_transport_keepalive_test(ssl, true, &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 tcp = esp_transport_tcp_init(); + esp_transport_list_add(transport_list, tcp, "tcp"); + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + esp_transport_list_add(transport_list, ws, "ws"); + + // Perform the test + esp_transport_keep_alive_t keep_alive_cfg = { + .keep_alive_interval = 11, + .keep_alive_idle = 22, + .keep_alive_enable = true, + .keep_alive_count = 33 }; + esp_transport_tcp_set_keep_alive(tcp, &keep_alive_cfg); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(tcp, &ifr); + + // Run the test for sync_connect only (ws doesn't support async) + tcp_transport_keepalive_test(ws, false, &keep_alive_cfg); + + // Cleanup + esp_transport_close(tcp); + 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); + + // Bind device interface to loopback + TEST_TRANSPORT_BIND_IFNAME(); + esp_transport_ssl_set_interface_name(ssl, &ifr); + + tcp_transport_keepalive_test(ssl, true, &keep_alive_cfg); + + // Cleanup + esp_transport_close(ssl); + esp_transport_destroy(ssl); +} diff --git a/components/tcp_transport/test/test_transport_fixtures.c b/components/tcp_transport/test/test_transport_fixtures.c new file mode 100644 index 0000000000..1ad70ddbd7 --- /dev/null +++ b/components/tcp_transport/test/test_transport_fixtures.c @@ -0,0 +1,311 @@ +#include "unity.h" + +#include "esp_transport.h" +#include "esp_transport_tcp.h" +#include "test_utils.h" +#include "esp_log.h" +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "freertos/event_groups.h" +#include "tcp_transport_fixtures.h" + +// 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); + +/** + * @brief Event flags for synchronization between the listener task, the connection task and the test task + */ +enum { + TCP_CONNECT_DONE = 1 << 0, /*!< Indicates that the connection task has finished, so the transport_connect() exited */ + TCP_LISTENER_DONE = 1 << 1, /*!< Indicates that the listener task has finished either with success for failure */ + TCP_TEST_DONE = 1 << 2, /*!< Indicates that the test case finished, test tear-down() called */ + TCP_LISTENER_READY = 1 << 3, /*!< Indicates that the listener task is ready to accept connections */ + TCP_LISTENER_ACCEPTED = 1 << 4, /*!< Indicates that the listener task has accepted a connection (from transport_connect()) */ +}; + +/** + * @brief Connection test configuration parameters + */ +struct tcp_connect_test_config { + esp_transport_handle_t transport_under_test; + bool accept_connection; + bool consume_sock_backlog; + bool connect_async; + int timeout_ms; + int port; + bool listener_task_prio_higher; +}; + +/** + * @brief Test setup structure containing all the info needed for the connection tests + */ +struct tcp_connect_test_storage { + struct tcp_connect_test_config config; + TickType_t max_wait; + EventGroupHandle_t tcp_connect_done; + int connect_return_value; + int listen_sock; + int accepted_sock; + int last_connect_sock; + bool tcp_listener_failed; + TaskHandle_t listener_task; + TaskHandle_t tcp_connect_task; +}; + +typedef struct tcp_connect_test_storage *tcp_connect_test_t; + +/** + * @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_test_storage *storage) +{ + struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + .sin_family = AF_INET, + .sin_port = htons(storage->config.port) }; + int connect_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + if (connect_sock < 0) { + storage->tcp_listener_failed = true; + return; + } + storage->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(storage); + 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_test_storage *storage = pvParameters; + struct sockaddr_in dest_addr_ip4 = { .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_family = AF_INET, + .sin_port = htons(storage->config.port) }; + // Create listener socket and bind it to ANY address + storage->listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + int opt = 1; + setsockopt(storage->listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + if (storage->listen_sock < 0) { + ESP_LOGE(TAG, "Unable to create socket"); + storage->tcp_listener_failed = true; + goto failed; + } + int err = bind(storage->listen_sock, (struct sockaddr *)&dest_addr_ip4, sizeof(dest_addr_ip4)); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + storage->tcp_listener_failed = true; + goto failed; + } + + // Listen with backlog set to a low number + err = listen(storage->listen_sock, 4); + if (err != 0) { + ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); + storage->tcp_listener_failed = true; + goto failed; + } + + // Listener is ready at this point + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_READY); + + if (storage->config.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(storage); + } else if (storage->config.accept_connection) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + storage->accepted_sock = accept(storage->listen_sock, (struct sockaddr *)&source_addr, &addr_len); + if (storage->accepted_sock < 0) { + ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); + goto failed; + } + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_ACCEPTED); // Mark the socket as accepted + // ...and wait for the "acceptor" tests to finish + xEventGroupWaitBits(storage->tcp_connect_done, TCP_TEST_DONE, true, true, storage->config.timeout_ms * 10); + } + +failed: + xEventGroupSetBits(storage->tcp_connect_done, TCP_LISTENER_DONE); + vTaskSuspend(NULL); +} + +/** + * @brief This task simply tries to connect to localhost (server provided by listner's task) using tcp_transport + */ +static void tcp_connect_task(void *pvParameters) +{ + struct tcp_connect_test_storage *storage = pvParameters; + + int (*connect_fn)(esp_transport_handle_t, const char *, int, int) = + storage->config.connect_async ? esp_transport_connect_async : esp_transport_connect; + + storage->connect_return_value = connect_fn(storage->config.transport_under_test, "localhost", storage->config.port, storage->config.timeout_ms); + + if (storage->config.accept_connection) { + // If we test the accepted connection, need to wait until the test completes + xEventGroupWaitBits(storage->tcp_connect_done, TCP_TEST_DONE, true, true, storage->config.timeout_ms * 10); + } + xEventGroupSetBits(storage->tcp_connect_done, TCP_CONNECT_DONE); + vTaskSuspend(NULL); +} + +static inline void close_if_valid(int *s) +{ + if (*s >= 0) { + close(*s); + *s = -1; + } +} + +/** + * @brief Connect test setup function + * + * Creates the Test storage, configures it accordingly and starts two tasks + * * localhost_listener -- to provide a simple server endpoint for the transport layers to connect to + * * tcp_connect_task -- to perform the connection + */ +static tcp_connect_test_t connect_test_setup(struct tcp_connect_test_config *config) +{ + tcp_connect_test_t t = calloc(1, sizeof(struct tcp_connect_test_storage)); + if (!t) { + return NULL; + } + memcpy(&t->config, config, sizeof(struct tcp_connect_test_config)); + t->tcp_connect_done = xEventGroupCreate(); + if (!t->tcp_connect_done) { + return NULL; + } + t->max_wait = pdMS_TO_TICKS(config->timeout_ms * 10); + + t->listen_sock = t->last_connect_sock = t->accepted_sock = -1; // mark all sockets invalid + + test_case_uses_tcpip(); + + // Create listener task + xTaskCreatePinnedToCore(localhost_listener, "localhost_listener", 4096, t, 5, &t->listener_task, 0); + xEventGroupWaitBits(t->tcp_connect_done, TCP_LISTENER_READY, true, true, t->max_wait); + + // Perform tcp-connect in a separate task to check asynchronously for the timeout or to connect (depends on the test config) + xTaskCreatePinnedToCore(tcp_connect_task, "tcp_connect_task", 4096, t, + config->listener_task_prio_higher? 4 : 6, &t->tcp_connect_task, 0); + + return t; +} + +/** + * @brief Destroys and cleans out the test environment + */ +static void connect_test_teardown(tcp_connect_test_t t) +{ + // Mark the test done and wait for the listener to check if finished with no issues + xEventGroupSetBits(t->tcp_connect_done, TCP_TEST_DONE); + xEventGroupWaitBits(t->tcp_connect_done, TCP_LISTENER_DONE, true, true, t->max_wait); + TEST_ASSERT_EQUAL(false, t->tcp_listener_failed); + + // Closing both parties of the last "blocking" connection to unwind localhost_listener() and let other connected sockets closed + close_if_valid(&t->listen_sock); + close_if_valid(&t->last_connect_sock); + close_if_valid(&t->accepted_sock); + + // Cleanup + vTaskSuspend(t->tcp_connect_task); + vTaskSuspend(t->listener_task); + vEventGroupDelete(t->tcp_connect_done); + test_utils_task_delete(t->tcp_connect_task); + test_utils_task_delete(t->listener_task); + free(t); +} + +/** + * @brief Utility function for testing timeouts for different transports + */ +void tcp_transport_test_connection_timeout(esp_transport_handle_t transport_under_test) +{ + + struct tcp_connect_test_config params = { + .timeout_ms = 200, + .port = 80, + .consume_sock_backlog = true, + .connect_async = false, + .transport_under_test = transport_under_test, + .listener_task_prio_higher = true + }; + + tcp_connect_test_t test = connect_test_setup(¶ms); + TEST_ASSERT_NOT_NULL(test); + + // Roughly measure tick-time spent while trying to connect + TickType_t start = xTaskGetTickCount(); + EventBits_t bits = xEventGroupWaitBits(test->tcp_connect_done, TCP_CONNECT_DONE, true, true, test->max_wait); + TickType_t end = xTaskGetTickCount(); + + TEST_ASSERT_EQUAL(TCP_CONNECT_DONE, TCP_CONNECT_DONE & bits); // Connection has finished + TEST_ASSERT_EQUAL(-1, test->connect_return_value); // 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); + + // Close the last bound connection, to recursively unwind the consumed backlog + close_if_valid(&test->last_connect_sock); + + connect_test_teardown(test); +} + +/** + * @brief Utility function for testing timeouts for different transports, options and both sync and async connection + */ +void tcp_transport_test_socket_options(esp_transport_handle_t transport_under_test, bool async, + const struct expected_sock_option *expected_opts, size_t sock_options_len) +{ + struct tcp_connect_test_config params = { + .timeout_ms = 200, + .port = 80, + .accept_connection = true, + .consume_sock_backlog = false, + .transport_under_test = transport_under_test, + .connect_async = async, + .listener_task_prio_higher = false + }; + + tcp_connect_test_t test = connect_test_setup(¶ms); + TEST_ASSERT_NOT_NULL(test); + + // Wait till the connection gets accepted to get the client's socket + xEventGroupWaitBits(test->tcp_connect_done, TCP_LISTENER_ACCEPTED, true, true, test->max_wait); + int sock = esp_transport_get_socket(params.transport_under_test); + for (int i=0; iconn_state = TRANS_SSL_CONNECTING; + ssl->sockfd = INVALID_SOCKET; } if (ssl->conn_state == TRANS_SSL_CONNECTING) { - return esp_tls_conn_new_async(host, strlen(host), port, &ssl->cfg, ssl->tls); + int progress = esp_tls_conn_new_async(host, strlen(host), port, &ssl->cfg, ssl->tls); + if (progress >= 0) { + ssl->sockfd = ssl->tls->sockfd; + } + return progress; + } return 0; } @@ -95,12 +102,12 @@ static inline int tcp_connect_async(esp_transport_handle_t t, const char *host, return esp_tls_connect_async(t, host, port, timeout_ms, true); } -static int esp_tls_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms, bool is_plain_tcp) +static int ssl_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) { transport_esp_tls_t *ssl = ssl_get_context_data(t); ssl->cfg.timeout_ms = timeout_ms; - ssl->cfg.is_plain_tcp = is_plain_tcp; + ssl->cfg.is_plain_tcp = false; ssl->ssl_initialized = true; ssl->tls = esp_tls_init(); @@ -114,22 +121,30 @@ static int esp_tls_connect(esp_transport_handle_t t, const char *host, int port, esp_transport_set_errors(t, ssl->tls->error_handle); esp_tls_conn_destroy(ssl->tls); ssl->tls = NULL; + ssl->sockfd = INVALID_SOCKET; + return -1; + } + ssl->sockfd = ssl->tls->sockfd; + return 0; +} + +static int tcp_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) +{ + transport_esp_tls_t *ssl = ssl_get_context_data(t); + esp_tls_last_error_t *err_handle = esp_transport_get_error_handle(t); + + ssl->cfg.timeout_ms = timeout_ms; + esp_err_t err = esp_tls_plain_tcp_connect(host, strlen(host), port, &ssl->cfg, err_handle, &ssl->sockfd); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open a new connection: %d", err); + err_handle->last_error = err; + ssl->sockfd = INVALID_SOCKET; return -1; } return 0; } -static inline int ssl_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) -{ - return esp_tls_connect(t, host, port, timeout_ms, false); -} - -static inline int tcp_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) -{ - return esp_tls_connect(t, host, port, timeout_ms, true); -} - -static int ssl_poll_read(esp_transport_handle_t t, int timeout_ms) +static int base_poll_read(esp_transport_handle_t t, int timeout_ms) { transport_esp_tls_t *ssl = ssl_get_context_data(t); int ret = -1; @@ -139,26 +154,26 @@ static int ssl_poll_read(esp_transport_handle_t t, int timeout_ms) fd_set errset; FD_ZERO(&readset); FD_ZERO(&errset); - FD_SET(ssl->tls->sockfd, &readset); - FD_SET(ssl->tls->sockfd, &errset); + FD_SET(ssl->sockfd, &readset); + FD_SET(ssl->sockfd, &errset); - if ((remain = esp_tls_get_bytes_avail(ssl->tls)) > 0) { + if (ssl->tls && (remain = esp_tls_get_bytes_avail(ssl->tls)) > 0) { ESP_LOGD(TAG, "remain data in cache, need to read again"); return remain; } - ret = select(ssl->tls->sockfd + 1, &readset, NULL, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); - if (ret > 0 && FD_ISSET(ssl->tls->sockfd, &errset)) { + ret = select(ssl->sockfd + 1, &readset, NULL, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); + if (ret > 0 && FD_ISSET(ssl->sockfd, &errset)) { int sock_errno = 0; uint32_t optlen = sizeof(sock_errno); - getsockopt(ssl->tls->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); + getsockopt(ssl->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); esp_transport_capture_errno(t, sock_errno); - ESP_LOGE(TAG, "ssl_poll_read select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->tls->sockfd); + ESP_LOGE(TAG, "poll_read select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->sockfd); ret = -1; } return ret; } -static int ssl_poll_write(esp_transport_handle_t t, int timeout_ms) +static int base_poll_write(esp_transport_handle_t t, int timeout_ms) { transport_esp_tls_t *ssl = ssl_get_context_data(t); int ret = -1; @@ -167,15 +182,15 @@ static int ssl_poll_write(esp_transport_handle_t t, int timeout_ms) fd_set errset; FD_ZERO(&writeset); FD_ZERO(&errset); - FD_SET(ssl->tls->sockfd, &writeset); - FD_SET(ssl->tls->sockfd, &errset); - ret = select(ssl->tls->sockfd + 1, NULL, &writeset, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); - if (ret > 0 && FD_ISSET(ssl->tls->sockfd, &errset)) { + FD_SET(ssl->sockfd, &writeset); + FD_SET(ssl->sockfd, &errset); + ret = select(ssl->sockfd + 1, NULL, &writeset, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); + if (ret > 0 && FD_ISSET(ssl->sockfd, &errset)) { int sock_errno = 0; uint32_t optlen = sizeof(sock_errno); - getsockopt(ssl->tls->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); + getsockopt(ssl->sockfd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); esp_transport_capture_errno(t, sock_errno); - ESP_LOGE(TAG, "ssl_poll_write select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->tls->sockfd); + ESP_LOGE(TAG, "poll_write select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), ssl->sockfd); ret = -1; } return ret; @@ -183,14 +198,14 @@ static int ssl_poll_write(esp_transport_handle_t t, int timeout_ms) static int ssl_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms) { - int poll, ret; + int poll; transport_esp_tls_t *ssl = ssl_get_context_data(t); if ((poll = esp_transport_poll_write(t, timeout_ms)) <= 0) { - ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->tls->sockfd, timeout_ms); + ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->sockfd, timeout_ms); return poll; } - ret = esp_tls_conn_write(ssl->tls, (const unsigned char *) buffer, len); + int ret = esp_tls_conn_write(ssl->tls, (const unsigned char *) buffer, len); if (ret < 0) { ESP_LOGE(TAG, "esp_tls_conn_write error, errno=%s", strerror(errno)); esp_transport_set_errors(t, ssl->tls->error_handle); @@ -198,15 +213,32 @@ static int ssl_write(esp_transport_handle_t t, const char *buffer, int len, int return ret; } +static int tcp_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms) +{ + int poll; + transport_esp_tls_t *ssl = ssl_get_context_data(t); + + if ((poll = esp_transport_poll_write(t, timeout_ms)) <= 0) { + ESP_LOGW(TAG, "Poll timeout or error, errno=%s, fd=%d, timeout_ms=%d", strerror(errno), ssl->sockfd, timeout_ms); + return poll; + } + int ret = send(ssl->sockfd,(const unsigned char *) buffer, len, 0); + if (ret < 0) { + ESP_LOGE(TAG, "tcp_write error, errno=%s", strerror(errno)); + esp_transport_capture_errno(t, errno); + } + return ret; +} + static int ssl_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms) { - int poll, ret; + int poll; transport_esp_tls_t *ssl = ssl_get_context_data(t); if ((poll = esp_transport_poll_read(t, timeout_ms)) <= 0) { return poll; } - ret = esp_tls_conn_read(ssl->tls, (unsigned char *)buffer, len); + int ret = esp_tls_conn_read(ssl->tls, (unsigned char *)buffer, len); if (ret < 0) { ESP_LOGE(TAG, "esp_tls_conn_read error, errno=%s", strerror(errno)); esp_transport_set_errors(t, ssl->tls->error_handle); @@ -221,7 +253,30 @@ static int ssl_read(esp_transport_handle_t t, char *buffer, int len, int timeout return ret; } -static int ssl_close(esp_transport_handle_t t) +static int tcp_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms) +{ + int poll; + transport_esp_tls_t *ssl = ssl_get_context_data(t); + + if ((poll = esp_transport_poll_read(t, timeout_ms)) <= 0) { + return poll; + } + int ret = recv(ssl->sockfd, (unsigned char *)buffer, len, 0); + if (ret < 0) { + ESP_LOGE(TAG, "tcp_read error, errno=%s", strerror(errno)); + esp_transport_capture_errno(t, errno); + } + if (ret == 0) { + if (poll > 0) { + // no error, socket reads 0 while previously detected as readable -> connection has been closed cleanly + capture_tcp_transport_error(t, ERR_TCP_TRANSPORT_CONNECTION_CLOSED_BY_FIN); + } + ret = -1; + } + return ret; +} + +static int base_close(esp_transport_handle_t t) { int ret = -1; transport_esp_tls_t *ssl = ssl_get_context_data(t); @@ -229,11 +284,15 @@ static int ssl_close(esp_transport_handle_t t) ret = esp_tls_conn_destroy(ssl->tls); ssl->conn_state = TRANS_SSL_INIT; ssl->ssl_initialized = false; + ssl->sockfd = INVALID_SOCKET; + } else if (ssl && ssl->sockfd >= 0) { + close(ssl->sockfd); + ssl->sockfd = INVALID_SOCKET; } return ret; } -static int ssl_destroy(esp_transport_handle_t t) +static int base_destroy(esp_transport_handle_t t) { transport_esp_tls_t *ssl = ssl_get_context_data(t); if (ssl) { @@ -335,13 +394,13 @@ void esp_transport_ssl_crt_bundle_attach(esp_transport_handle_t t, esp_err_t ((* ssl->cfg.crt_bundle_attach = crt_bundle_attach; } -static int ssl_get_socket(esp_transport_handle_t t) +static int base_get_socket(esp_transport_handle_t t) { - transport_esp_tls_t *ssl = ssl_get_context_data(t); - if (ssl && ssl->tls) { - return ssl->tls->sockfd; + transport_esp_tls_t *ctx = ssl_get_context_data(t); + if (ctx) { + return ctx->sockfd; } - return -1; + return INVALID_SOCKET; } void esp_transport_ssl_set_ds_data(esp_transport_handle_t t, void *ds_data) @@ -368,16 +427,16 @@ esp_transport_handle_t esp_transport_ssl_init(void) if (t == NULL) { return NULL; } - - esp_transport_set_func(t, ssl_connect, ssl_read, ssl_write, ssl_close, ssl_poll_read, ssl_poll_write, ssl_destroy); + esp_transport_set_func(t, ssl_connect, ssl_read, ssl_write, base_close, base_poll_read, base_poll_write, base_destroy); esp_transport_set_async_connect_func(t, ssl_connect_async); - t->_get_socket = ssl_get_socket; + t->_get_socket = base_get_socket; return t; } struct transport_esp_tls* esp_transport_esp_tls_create(void) { transport_esp_tls_t *transport_esp_tls = calloc(1, sizeof(transport_esp_tls_t)); + transport_esp_tls->sockfd = INVALID_SOCKET; return transport_esp_tls; } @@ -392,9 +451,9 @@ esp_transport_handle_t esp_transport_tcp_init(void) if (t == NULL) { return NULL; } - esp_transport_set_func(t, tcp_connect, ssl_read, ssl_write, ssl_close, ssl_poll_read, ssl_poll_write, ssl_destroy); + esp_transport_set_func(t, tcp_connect, tcp_read, tcp_write, base_close, base_poll_read, base_poll_write, base_destroy); esp_transport_set_async_connect_func(t, tcp_connect_async); - t->_get_socket = ssl_get_socket; + t->_get_socket = base_get_socket; return t; } diff --git a/components/tcp_transport/transport_ws.c b/components/tcp_transport/transport_ws.c index 75d984739b..4906a44a64 100644 --- a/components/tcp_transport/transport_ws.c +++ b/components/tcp_transport/transport_ws.c @@ -602,7 +602,7 @@ esp_transport_handle_t esp_transport_ws_init(esp_transport_handle_t parent_handl }); esp_transport_set_func(t, ws_connect, ws_read, ws_write, ws_close, ws_poll_read, ws_poll_write, ws_destroy); - // webocket underlying transfer is the payload transfer handle + // websocket underlying transfer is the payload transfer handle esp_transport_set_parent_transport_func(t, ws_get_payload_transport_handle); esp_transport_set_context_data(t, ws);