diff --git a/components/esp-tls/esp_tls.c b/components/esp-tls/esp_tls.c index 33f3a0e1a..3ead8ef2e 100644 --- a/components/esp-tls/esp_tls.c +++ b/components/esp-tls/esp_tls.c @@ -22,6 +22,7 @@ #include #include "esp_tls.h" +#include static const char *TAG = "esp-tls"; @@ -32,8 +33,6 @@ static const char *TAG = "esp-tls"; #define ESP_LOGE(TAG, ...) printf(__VA_ARGS__); #endif -#define DEFAULT_TIMEOUT_MS -1 - static struct addrinfo *resolve_host_name(const char *host, size_t hostlen) { struct addrinfo hints; @@ -82,19 +81,20 @@ static void ms_to_timeval(int timeout_ms, struct timeval *tv) tv->tv_usec = (timeout_ms % 1000) * 1000; } -static int esp_tcp_connect(const char *host, int hostlen, int port, int timeout_ms) +static int esp_tcp_connect(const char *host, int hostlen, int port, int *sockfd, const esp_tls_cfg_t *cfg) { + int ret = -1; struct addrinfo *res = resolve_host_name(host, hostlen); if (!res) { - return -1; + return ret; } - int ret = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (ret < 0) { + int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (fd < 0) { ESP_LOGE(TAG, "Failed to create socket (family %d socktype %d protocol %d)", res->ai_family, res->ai_socktype, res->ai_protocol); goto err_freeaddr; } - int fd = ret; + *sockfd = fd; void *addr_ptr; if (res->ai_family == AF_INET) { @@ -111,32 +111,39 @@ static int esp_tcp_connect(const char *host, int hostlen, int port, int timeout_ goto err_freesocket; } - if (timeout_ms >= 0) { - struct timeval tv; - ms_to_timeval(timeout_ms, &tv); - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + if (cfg) { + if (cfg->timeout_ms >= 0) { + struct timeval tv; + ms_to_timeval(cfg->timeout_ms, &tv); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + if (cfg->non_block) { + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } } ret = connect(fd, addr_ptr, res->ai_addrlen); - if (ret < 0) { + if (ret < 0 && !(errno == EINPROGRESS && cfg->non_block)) { + ESP_LOGE(TAG, "Failed to connnect to host (errno %d)", errno); goto err_freesocket; } freeaddrinfo(res); - return fd; + return 0; err_freesocket: close(fd); err_freeaddr: freeaddrinfo(res); - return -1; + return ret; } static void verify_certificate(esp_tls_t *tls) { int flags; - char buf[100]; + char buf[100]; if ((flags = mbedtls_ssl_get_verify_result(&tls->ssl)) != 0) { ESP_LOGI(TAG, "Failed to verify peer certificate!"); bzero(buf, sizeof(buf)); @@ -225,20 +232,8 @@ static int create_ssl_handle(esp_tls_t *tls, const char *hostname, size_t hostle ESP_LOGE(TAG, "mbedtls_ssl_setup returned -0x%x\n\n", -ret); goto exit; } - mbedtls_ssl_set_bio(&tls->ssl, &tls->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); - while ((ret = mbedtls_ssl_handshake(&tls->ssl)) != 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - ESP_LOGE(TAG, "mbedtls_ssl_handshake returned -0x%x", -ret); - if (cfg->cacert_pem_buf != NULL) { - /* This is to check whether handshake failed due to invalid certificate*/ - verify_certificate(tls); - } - goto exit; - } - } - return 0; exit: mbedtls_cleanup(tls); @@ -275,45 +270,132 @@ static ssize_t tls_write(esp_tls_t *tls, const char *data, size_t datalen) return ret; } +static int esp_tls_low_level_conn(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg, esp_tls_t *tls) +{ + if (!tls) { + ESP_LOGE(TAG, "empty esp_tls parameter"); + return -1; + } + /* These states are used to keep a tab on connection progress in case of non-blocking connect, + and in case of blocking connect these cases will get executed one after the other */ + switch (tls->conn_state) { + case ESP_TLS_INIT: + ; + int sockfd; + int ret = esp_tcp_connect(hostname, hostlen, port, &sockfd, cfg); + if (ret < 0) { + return -1; + } + tls->sockfd = sockfd; + if (!cfg) { + tls->read = tcp_read; + tls->write = tcp_write; + ESP_LOGD(TAG, "non-tls connection established"); + return 1; + } + if (cfg->non_block) { + FD_ZERO(&tls->rset); + FD_SET(tls->sockfd, &tls->rset); + tls->wset = tls->rset; + } + tls->conn_state = ESP_TLS_CONNECTING; + /* falls through */ + case ESP_TLS_CONNECTING: + if (cfg->non_block) { + ESP_LOGD(TAG, "connecting..."); + struct timeval tv; + ms_to_timeval(cfg->timeout_ms, &tv); + + /* In case of non-blocking I/O, we use the select() API to check whether + connection has been estbalished or not*/ + if (select(tls->sockfd + 1, &tls->rset, &tls->wset, NULL, + cfg->timeout_ms ? &tv : NULL) == 0) { + ESP_LOGD(TAG, "select() timed out"); + return 0; + } + if (FD_ISSET(tls->sockfd, &tls->rset) || FD_ISSET(tls->sockfd, &tls->wset)) { + int error; + unsigned int len = sizeof(error); + /* pending error check */ + if (getsockopt(tls->sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { + ESP_LOGD(TAG, "Non blocking connect failed"); + tls->conn_state = ESP_TLS_FAIL; + return -1; + } + } + } + /* By now, the connection has been established */ + ret = create_ssl_handle(tls, hostname, hostlen, cfg); + if (ret != 0) { + ESP_LOGD(TAG, "create_ssl_handshake failed"); + tls->conn_state = ESP_TLS_FAIL; + return -1; + } + tls->read = tls_read; + tls->write = tls_write; + tls->conn_state = ESP_TLS_HANDSHAKE; + /* falls through */ + case ESP_TLS_HANDSHAKE: + ESP_LOGD(TAG, "handshake in progress..."); + ret = mbedtls_ssl_handshake(&tls->ssl); + if (ret == 0) { + tls->conn_state = ESP_TLS_DONE; + return 1; + } else { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + ESP_LOGE(TAG, "mbedtls_ssl_handshake returned -0x%x", -ret); + if (cfg->cacert_pem_buf != NULL) { + /* This is to check whether handshake failed due to invalid certificate*/ + verify_certificate(tls); + } + tls->conn_state = ESP_TLS_FAIL; + return -1; + } + /* Irrespective of blocking or non-blocking I/O, we return on getting MBEDTLS_ERR_SSL_WANT_READ + or MBEDTLS_ERR_SSL_WANT_WRITE during handshake */ + return 0; + } + break; + case ESP_TLS_FAIL: + ESP_LOGE(TAG, "failed to open a new connection");; + break; + default: + ESP_LOGE(TAG, "invalid esp-tls state"); + break; + } + return -1; +} + /** * @brief Create a new TLS/SSL connection */ esp_tls_t *esp_tls_conn_new(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg) { - int sockfd; - if (cfg) { - sockfd = esp_tcp_connect(hostname, hostlen, port, cfg->timeout_ms); - } else { - sockfd = esp_tcp_connect(hostname, hostlen, port, DEFAULT_TIMEOUT_MS); - } - - if (sockfd < 0) { - return NULL; - } - esp_tls_t *tls = (esp_tls_t *)calloc(1, sizeof(esp_tls_t)); if (!tls) { - close(sockfd); return NULL; } - tls->sockfd = sockfd; - tls->read = tcp_read; - tls->write = tcp_write; - - if (cfg) { - if (create_ssl_handle(tls, hostname, hostlen, cfg) != 0) { + /* esp_tls_conn_new() API establishes connection in a blocking manner thus this loop ensures that esp_tls_conn_new() + API returns only after connection is established unless there is an error*/ + while (1) { + int ret = esp_tls_low_level_conn(hostname, hostlen, port, cfg, tls); + if (ret == 1) { + return tls; + } else if (ret == -1) { esp_tls_conn_delete(tls); + ESP_LOGE(TAG, "Failed to open new connection"); return NULL; } - tls->read = tls_read; - tls->write = tls_write; - if (cfg->non_block == true) { - int flags = fcntl(tls->sockfd, F_GETFL, 0); - fcntl(tls->sockfd, F_SETFL, flags | O_NONBLOCK); - } } + return NULL; +} - return tls; +/* + * @brief Create a new TLS/SSL non-blocking connection + */ +int esp_tls_conn_new_async(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg , esp_tls_t *tls) +{ + return esp_tls_low_level_conn(hostname, hostlen, port, cfg, tls); } static int get_port(const char *url, struct http_parser_url *u) @@ -352,4 +434,19 @@ size_t esp_tls_get_bytes_avail(esp_tls_t *tls) return ESP_FAIL; } return mbedtls_ssl_get_bytes_avail(&tls->ssl); +} + +/** + * @brief Create a new non-blocking TLS/SSL connection with a given "HTTP" url + */ +int esp_tls_conn_http_new_async(const char *url, const esp_tls_cfg_t *cfg, esp_tls_t *tls) +{ + /* Parse URI */ + struct http_parser_url u; + http_parser_url_init(&u); + http_parser_parse_url(url, strlen(url), 0, &u); + + /* Connect to host */ + return esp_tls_conn_new_async(&url[u.field_data[UF_HOST].off], u.field_data[UF_HOST].len, + get_port(url, &u), cfg, tls); } \ No newline at end of file diff --git a/components/esp-tls/esp_tls.h b/components/esp-tls/esp_tls.h index d5975dc19..f6a4a7c50 100644 --- a/components/esp-tls/esp_tls.h +++ b/components/esp-tls/esp_tls.h @@ -31,6 +31,17 @@ extern "C" { #endif +/** + * @brief ESP-TLS Connection State + */ +typedef enum esp_tls_conn_state { + ESP_TLS_INIT = 0, + ESP_TLS_CONNECTING, + ESP_TLS_HANDSHAKE, + ESP_TLS_FAIL, + ESP_TLS_DONE, +} esp_tls_conn_state_t; + /** * @brief ESP-TLS configuration parameters */ @@ -84,12 +95,18 @@ typedef struct esp_tls { ssize_t (*write)(struct esp_tls *tls, const char *data, size_t datalen); /*!< Callback function for writing data to TLS/SSL connection. */ + + esp_tls_conn_state_t conn_state; /*!< ESP-TLS Connection state */ + + fd_set rset; /*!< read file descriptors */ + + fd_set wset; /*!< write file descriptors */ } esp_tls_t; /** - * @brief Create a new TLS/SSL connection + * @brief Create a new blocking TLS/SSL connection * - * This function establishes a TLS/SSL connection with the specified host. + * This function establishes a TLS/SSL connection with the specified host in blocking manner. * * @param[in] hostname Hostname of the host. * @param[in] hostlen Length of hostname. @@ -103,7 +120,7 @@ typedef struct esp_tls { esp_tls_t *esp_tls_conn_new(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg); /** - * @brief Create a new TLS/SSL connection with a given "HTTP" url + * @brief Create a new blocking TLS/SSL connection with a given "HTTP" url * * The behaviour is same as esp_tls_conn_new() API. However this API accepts host's url. * @@ -116,6 +133,40 @@ esp_tls_t *esp_tls_conn_new(const char *hostname, int hostlen, int port, const e */ esp_tls_t *esp_tls_conn_http_new(const char *url, const esp_tls_cfg_t *cfg); +/* + * @brief Create a new non-blocking TLS/SSL connection + * + * This function initiates a non-blocking TLS/SSL connection with the specified host, but due to + * its non-blocking nature, it doesn't wait for the connection to get established. + * + * @param[in] hostname Hostname of the host. + * @param[in] hostlen Length of hostname. + * @param[in] port Port number of the host. + * @param[in] cfg TLS configuration as esp_tls_cfg_t. `non_block` member of + * this structure should be set to be true. + * @param[in] tls pointer to esp-tls as esp-tls handle. + * + * @return - 1 If connection establishment fails. + * - 0 If connection establishment is in progress. + * - 1 If connection establishment is successful. + */ +int esp_tls_conn_new_async(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg, esp_tls_t *tls); + +/** + * @brief Create a new non-blocking TLS/SSL connection with a given "HTTP" url + * + * The behaviour is same as esp_tls_conn_new() API. However this API accepts host's url. + * + * @param[in] url url of host. + * @param[in] tls pointer to esp-tls as esp-tls handle. + * @param[in] cfg TLS configuration as esp_tls_cfg_t. + * + * @return - 1 If connection establishment fails. + * - 0 If connection establishment is in progress. + * - 1 If connection establishment is successful. + */ +int esp_tls_conn_http_new_async(const char *url, const esp_tls_cfg_t *cfg, esp_tls_t *tls); + /** * @brief Write from buffer 'data' into specified tls connection. * @@ -144,13 +195,13 @@ static inline ssize_t esp_tls_conn_write(esp_tls_t *tls, const void *data, size_ * @param[in] datalen Length of data buffer. * * @return -* - >0 if read operation was successful, the return value is the number -* of bytes actually read from the TLS/SSL connection. -* - 0 if read operation was not successful. The underlying -* connection was closed. -* - <0 if read operation was not successful, because either an -* error occured or an action must be taken by the calling process. -*/ + * - >0 if read operation was successful, the return value is the number + * of bytes actually read from the TLS/SSL connection. + * - 0 if read operation was not successful. The underlying + * connection was closed. + * - <0 if read operation was not successful, because either an + * error occured or an action must be taken by the calling process. + */ static inline ssize_t esp_tls_conn_read(esp_tls_t *tls, void *data, size_t datalen) { return tls->read(tls, (char *)data, datalen); @@ -181,6 +232,7 @@ void esp_tls_conn_delete(esp_tls_t *tls); */ size_t esp_tls_get_bytes_avail(esp_tls_t *tls); + #ifdef __cplusplus } #endif diff --git a/components/esp32/esp_err_to_name.c b/components/esp32/esp_err_to_name.c index dbc5c48e0..2f157d67f 100644 --- a/components/esp32/esp_err_to_name.c +++ b/components/esp32/esp_err_to_name.c @@ -449,6 +449,12 @@ static const esp_err_msg_t esp_err_msg_table[] = { # ifdef ESP_ERR_HTTP_INVALID_TRANSPORT ERR_TBL_IT(ESP_ERR_HTTP_INVALID_TRANSPORT), /* 28677 0x7005 There are no transport support for the input scheme */ +# endif +# ifdef ESP_ERR_HTTP_CONNECTING + ERR_TBL_IT(ESP_ERR_HTTP_CONNECTING), /* 28678 0x7006 HTTP connection hasn't been established yet */ +# endif +# ifdef ESP_ERR_HTTP_EAGAIN + ERR_TBL_IT(ESP_ERR_HTTP_EAGAIN), /* 28679 0x7007 Mapping of errno EAGAIN to esp_err_t */ # endif // components/http_server/include/http_server.h # ifdef ESP_ERR_HTTPD_BASE diff --git a/components/esp_http_client/esp_http_client.c b/components/esp_http_client/esp_http_client.c index 6dc7e4499..ae174f137 100644 --- a/components/esp_http_client/esp_http_client.c +++ b/components/esp_http_client/esp_http_client.c @@ -26,6 +26,7 @@ #include "sdkconfig.h" #include "transport.h" #include "esp_http_client.h" +#include "errno.h" #ifdef CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS #include "transport_ssl.h" @@ -113,6 +114,11 @@ struct esp_http_client { int buffer_size; bool disable_auto_redirect; esp_http_client_event_t event; + int data_written_index; + int data_write_left; + bool first_line_prepared; + int header_index; + bool is_async; }; typedef struct esp_http_client esp_http_client_t; @@ -124,6 +130,10 @@ static esp_err_t _clear_connection_info(esp_http_client_handle_t client); #define DEFAULT_HTTP_PORT (80) #define DEFAULT_HTTPS_PORT (443) +#define ASYNC_TRANS_CONNECT_FAIL -1 +#define ASYNC_TRANS_CONNECTING 0 +#define ASYNC_TRANS_CONNECT_PASS 1 + static const char *DEFAULT_HTTP_USER_AGENT = "ESP32 HTTP Client/1.0"; static const char *DEFAULT_HTTP_PROTOCOL = "HTTP/1.1"; static const char *DEFAULT_HTTP_PATH = "/"; @@ -156,6 +166,11 @@ enum HttpStatus_Code HttpStatus_Unauthorized = 401 }; + +static esp_err_t esp_http_client_request_send(esp_http_client_handle_t client); +static esp_err_t esp_http_client_connect(esp_http_client_handle_t client); +static esp_err_t esp_http_client_send_post_data(esp_http_client_handle_t client); + static esp_err_t http_dispatch_event(esp_http_client_t *client, esp_http_client_event_id_t event_id, void *data, int len) { esp_http_client_event_t *event = &client->event; @@ -359,6 +374,9 @@ static esp_err_t _set_config(esp_http_client_handle_t client, const esp_http_cli if (client->timeout_ms == 0) { client->timeout_ms = DEFAULT_TIMEOUT_MS; } + if (config->is_async) { + client->is_async = true; + } return ESP_OK; } @@ -399,6 +417,7 @@ static esp_err_t esp_http_client_prepare(esp_http_client_handle_t client) { client->process_again = 0; client->response->data_process = 0; + client->first_line_prepared = false; http_parser_init(client->parser, HTTP_RESPONSE); if (client->connection_info.username) { char *auth_response = NULL; @@ -781,55 +800,89 @@ int esp_http_client_read(esp_http_client_handle_t client, char *buffer, int len) esp_err_t esp_http_client_perform(esp_http_client_handle_t client) { esp_err_t err; - if (client == NULL) { - return ESP_ERR_INVALID_ARG; - } do { - if ((err = esp_http_client_open(client, client->post_len)) != ESP_OK) { - return err; - } - if (client->post_data && client->post_len) { - if (esp_http_client_write(client, client->post_data, client->post_len) <= 0) { - ESP_LOGE(TAG, "Error upload data"); - return ESP_ERR_HTTP_WRITE_DATA; - } - } - if (esp_http_client_fetch_headers(client) < 0) { - return ESP_ERR_HTTP_FETCH_HEADER; + if (client->process_again) { + esp_http_client_prepare(client); } + switch (client->state) { + /* In case of blocking esp_http_client_perform(), the following states will fall through one after the after; + in case of non-blocking esp_http_client_perform(), if there is an error condition, like EINPROGRESS or EAGAIN, + then the esp_http_client_perform() API will return ESP_ERR_HTTP_EAGAIN error. The user may call + esp_http_client_perform API again, and for this reason, we maintain the states */ + case HTTP_STATE_INIT: + if ((err = esp_http_client_connect(client)) != ESP_OK) { + if (client->is_async && err == ESP_ERR_HTTP_CONNECTING) { + return ESP_ERR_HTTP_EAGAIN; + } + return err; + } + /* falls through */ + case HTTP_STATE_CONNECTED: + if ((err = esp_http_client_request_send(client)) != ESP_OK) { + if (client->is_async && errno == EAGAIN) { + return ESP_ERR_HTTP_EAGAIN; + } + return err; + } + /* falls through */ + case HTTP_STATE_REQ_COMPLETE_HEADER: + if ((err = esp_http_client_send_post_data(client)) != ESP_OK) { + if (client->is_async && errno == EAGAIN) { + return ESP_ERR_HTTP_EAGAIN; + } + return err; + } + /* falls through */ + case HTTP_STATE_REQ_COMPLETE_DATA: + if (esp_http_client_fetch_headers(client) < 0) { + if (client->is_async && errno == EAGAIN) { + return ESP_ERR_HTTP_EAGAIN; + } + return ESP_ERR_HTTP_FETCH_HEADER; + } + /* falls through */ + case HTTP_STATE_RES_COMPLETE_HEADER: + if ((err = esp_http_check_response(client)) != ESP_OK) { + ESP_LOGE(TAG, "Error response"); + return err; + } + while (client->response->is_chunked && !client->is_chunk_complete) { + if (esp_http_client_get_data(client) <= 0) { + if (client->is_async && errno == EAGAIN) { + return ESP_ERR_HTTP_EAGAIN; + } + ESP_LOGD(TAG, "Read finish or server requests close"); + break; + } + } + while (client->response->data_process < client->response->content_length) { + if (esp_http_client_get_data(client) <= 0) { + if (client->is_async && errno == EAGAIN) { + return ESP_ERR_HTTP_EAGAIN; + } + ESP_LOGD(TAG, "Read finish or server requests close"); + break; + } + } + http_dispatch_event(client, HTTP_EVENT_ON_FINISH, NULL, 0); - if ((err = esp_http_check_response(client)) != ESP_OK) { - ESP_LOGE(TAG, "Error response"); - return err; - } - while (client->response->is_chunked && !client->is_chunk_complete) { - if (esp_http_client_get_data(client) <= 0) { - ESP_LOGD(TAG, "Read finish or server requests close"); + if (!http_should_keep_alive(client->parser)) { + ESP_LOGD(TAG, "Close connection"); + esp_http_client_close(client); + } else { + if (client->state > HTTP_STATE_CONNECTED) { + client->state = HTTP_STATE_CONNECTED; + client->first_line_prepared = false; + } + } break; - } - } - while (client->response->data_process < client->response->content_length) { - if (esp_http_client_get_data(client) <= 0) { - ESP_LOGD(TAG, "Read finish or server requests close"); + default: break; - } - } - - http_dispatch_event(client, HTTP_EVENT_ON_FINISH, NULL, 0); - - if (!http_should_keep_alive(client->parser)) { - ESP_LOGD(TAG, "Close connection"); - esp_http_client_close(client); - } else { - if (client->state > HTTP_STATE_CONNECTED) { - client->state = HTTP_STATE_CONNECTED; - } } } while (client->process_again); return ESP_OK; } - int esp_http_client_fetch_headers(esp_http_client_handle_t client) { if (client->state < HTTP_STATE_REQ_COMPLETE_HEADER) { @@ -842,7 +895,7 @@ int esp_http_client_fetch_headers(esp_http_client_handle_t client) while (client->state < HTTP_STATE_RES_COMPLETE_HEADER) { buffer->len = transport_read(client->transport, buffer->data, client->buffer_size, client->timeout_ms); - if (buffer->len <= 0) { + if (buffer->len < 0) { return ESP_FAIL; } http_parser_execute(client->parser, client->parser_settings, buffer->data, buffer->len); @@ -855,9 +908,10 @@ int esp_http_client_fetch_headers(esp_http_client_handle_t client) return client->response->content_length; } -esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len) +static esp_err_t esp_http_client_connect(esp_http_client_handle_t client) { esp_err_t err; + if (client->state == HTTP_STATE_UNINIT) { ESP_LOGE(TAG, "Client has not been initialized"); return ESP_ERR_INVALID_STATE; @@ -881,79 +935,165 @@ esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len) #endif return ESP_ERR_HTTP_INVALID_TRANSPORT; } - if (transport_connect(client->transport, client->connection_info.host, client->connection_info.port, client->timeout_ms) < 0) { - ESP_LOGE(TAG, "Connection failed"); - return ESP_ERR_HTTP_CONNECT; + if (!client->is_async) { + if (transport_connect(client->transport, client->connection_info.host, client->connection_info.port, client->timeout_ms) < 0) { + ESP_LOGE(TAG, "Connection failed, sock < 0"); + return ESP_ERR_HTTP_CONNECT; + } + } else { + int ret = transport_connect_async(client->transport, client->connection_info.host, client->connection_info.port, client->timeout_ms); + if (ret == ASYNC_TRANS_CONNECT_FAIL) { + ESP_LOGE(TAG, "Connection failed"); + return ESP_ERR_HTTP_CONNECT; + } else if (ret == ASYNC_TRANS_CONNECTING) { + ESP_LOGD(TAG, "Connection not yet established"); + return ESP_ERR_HTTP_CONNECTING; + } } - http_dispatch_event(client, HTTP_EVENT_ON_CONNECTED, NULL, 0); client->state = HTTP_STATE_CONNECTED; + http_dispatch_event(client, HTTP_EVENT_ON_CONNECTED, NULL, 0); } + return ESP_OK; +} +static int http_client_prepare_first_line(esp_http_client_handle_t client, int write_len) +{ if (write_len >= 0) { http_header_set_format(client->request->headers, "Content-Length", "%d", write_len); - } else if (write_len < 0) { + } else { esp_http_client_set_header(client, "Transfer-Encoding", "chunked"); esp_http_client_set_method(client, HTTP_METHOD_POST); } - int header_index = 0; - int wlen = client->buffer_size; - const char *method = HTTP_METHOD_MAPPING[client->connection_info.method]; - int first_line = snprintf(client->request->buffer->data, - client->buffer_size, "%s %s", - method, - client->connection_info.path); - if (first_line > client->buffer_size) { + int first_line_len = snprintf(client->request->buffer->data, + client->buffer_size, "%s %s", + method, + client->connection_info.path); + if (first_line_len >= client->buffer_size) { ESP_LOGE(TAG, "Out of buffer"); - return ESP_ERR_HTTP_CONNECT; + return -1; } if (client->connection_info.query) { - first_line += snprintf(client->request->buffer->data + first_line, - client->buffer_size - first_line, "?%s", client->connection_info.query); - if (first_line > client->buffer_size) { + first_line_len += snprintf(client->request->buffer->data + first_line_len, + client->buffer_size - first_line_len, "?%s", client->connection_info.query); + if (first_line_len >= client->buffer_size) { ESP_LOGE(TAG, "Out of buffer"); - return ESP_ERR_HTTP_CONNECT; + return -1; + } } - first_line += snprintf(client->request->buffer->data + first_line, - client->buffer_size - first_line, " %s\r\n", DEFAULT_HTTP_PROTOCOL); - if (first_line > client->buffer_size) { + first_line_len += snprintf(client->request->buffer->data + first_line_len, + client->buffer_size - first_line_len, " %s\r\n", DEFAULT_HTTP_PROTOCOL); + if (first_line_len >= client->buffer_size) { ESP_LOGE(TAG, "Out of buffer"); - return ESP_ERR_HTTP_CONNECT; + return -1; } - wlen -= first_line; + return first_line_len; +} - while ((header_index = http_header_generate_string(client->request->headers, header_index, client->request->buffer->data + first_line, &wlen))) { +static esp_err_t esp_http_client_request_send(esp_http_client_handle_t client) +{ + int first_line_len = 0; + if (!client->first_line_prepared) { + if ((first_line_len = http_client_prepare_first_line(client, client->post_len)) < 0) { + return first_line_len; + } + client->first_line_prepared = true; + client->header_index = 0; + client->data_written_index = 0; + client->data_write_left = 0; + } + + if (client->data_write_left > 0) { + /* sending leftover data from previous call to esp_http_client_request_send() API */ + int wret = 0; + if (((wret = esp_http_client_write(client, client->request->buffer->data + client->data_written_index, client->data_write_left)) < 0)) { + ESP_LOGE(TAG, "Error write request"); + return ESP_ERR_HTTP_WRITE_DATA; + } + client->data_write_left -= wret; + client->data_written_index += wret; + if (client->is_async && client->data_write_left > 0) { + return ESP_ERR_HTTP_WRITE_DATA; /* In case of EAGAIN error, we return ESP_ERR_HTTP_WRITE_DATA, + and the handling of EAGAIN should be done in the higher level APIs. */ + } + } + + int wlen = client->buffer_size - first_line_len; + while ((client->header_index = http_header_generate_string(client->request->headers, client->header_index, client->request->buffer->data + first_line_len, &wlen))) { if (wlen <= 0) { break; } - if (first_line) { - wlen += first_line; - first_line = 0; + if (first_line_len) { + wlen += first_line_len; + first_line_len = 0; } client->request->buffer->data[wlen] = 0; - ESP_LOGD(TAG, "Write header[%d]: %s", header_index, client->request->buffer->data); + ESP_LOGD(TAG, "Write header[%d]: %s", client->header_index, client->request->buffer->data); - int widx = 0, wret = 0; - while (wlen > 0) { - wret = transport_write(client->transport, client->request->buffer->data + widx, wlen, client->timeout_ms); + client->data_write_left = wlen; + client->data_written_index = 0; + while (client->data_write_left > 0) { + int wret = transport_write(client->transport, client->request->buffer->data + client->data_written_index, client->data_write_left, client->timeout_ms); if (wret <= 0) { ESP_LOGE(TAG, "Error write request"); esp_http_client_close(client); return ESP_ERR_HTTP_WRITE_DATA; } - widx += wret; - wlen -= wret; + client->data_write_left -= wret; + client->data_written_index += wret; } wlen = client->buffer_size; } + + client->data_written_index = 0; + client->data_write_left = client->post_len; client->state = HTTP_STATE_REQ_COMPLETE_HEADER; return ESP_OK; } +static esp_err_t esp_http_client_send_post_data(esp_http_client_handle_t client) +{ + if (client->state != HTTP_STATE_REQ_COMPLETE_HEADER) { + ESP_LOGE(TAG, "Invalid state"); + return ESP_ERR_INVALID_STATE; + } + if (!(client->post_data && client->post_len)) { + goto success; + } + + int wret = esp_http_client_write(client, client->post_data + client->data_written_index, client->data_write_left); + if (wret < 0) { + return wret; + } + client->data_write_left -= wret; + client->data_written_index += wret; + + if (client->data_write_left <= 0) { + goto success; + } else { + return ESP_ERR_HTTP_WRITE_DATA; + } + +success: + client->state = HTTP_STATE_REQ_COMPLETE_DATA; + return ESP_OK; +} + +esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len) +{ + esp_err_t err; + if ((err = esp_http_client_connect(client)) != ESP_OK) { + return err; + } + if ((err = esp_http_client_request_send(client)) != ESP_OK) { + return err; + } + return ESP_OK; +} int esp_http_client_write(esp_http_client_handle_t client, const char *buffer, int len) { @@ -964,7 +1104,9 @@ int esp_http_client_write(esp_http_client_handle_t client, const char *buffer, i int wlen = 0, widx = 0; while (len > 0) { wlen = transport_write(client->transport, buffer + widx, len, client->timeout_ms); - if (wlen <= 0) { + /* client->async_block is initialised in case of non-blocking IO, and in this case we return how + much ever data was written by the transport_write() API. */ + if (client->is_async || wlen <= 0) { return wlen; } widx += wlen; diff --git a/components/esp_http_client/include/esp_http_client.h b/components/esp_http_client/include/esp_http_client.h index 9043a48fe..b3b40ca0d 100644 --- a/components/esp_http_client/include/esp_http_client.h +++ b/components/esp_http_client/include/esp_http_client.h @@ -114,6 +114,7 @@ typedef struct { esp_http_client_transport_t transport_type; /*!< HTTP transport type, see `esp_http_client_transport_t` */ int buffer_size; /*!< HTTP buffer size (both send and receive) */ void *user_data; /*!< HTTP user_data context */ + bool is_async; /*!< Set asynchronous mode */ } esp_http_client_config_t; @@ -123,6 +124,8 @@ typedef struct { #define ESP_ERR_HTTP_WRITE_DATA (ESP_ERR_HTTP_BASE + 3) /*!< Error write HTTP data */ #define ESP_ERR_HTTP_FETCH_HEADER (ESP_ERR_HTTP_BASE + 4) /*!< Error read HTTP header from server */ #define ESP_ERR_HTTP_INVALID_TRANSPORT (ESP_ERR_HTTP_BASE + 5) /*!< There are no transport support for the input scheme */ +#define ESP_ERR_HTTP_CONNECTING (ESP_ERR_HTTP_BASE + 6) /*!< HTTP connection hasn't been established yet */ +#define ESP_ERR_HTTP_EAGAIN (ESP_ERR_HTTP_BASE + 7) /*!< Mapping of errno EAGAIN to esp_err_t */ /** * @brief Start a HTTP session @@ -141,7 +144,10 @@ esp_http_client_handle_t esp_http_client_init(const esp_http_client_config_t *co /** * @brief Invoke this function after `esp_http_client_init` and all the options calls are made, and will perform the * transfer as described in the options. It must be called with the same esp_http_client_handle_t as input as the esp_http_client_init call returned. - * esp_http_client_perform performs the entire request in a blocking manner and returns when done, or if it failed. + * esp_http_client_perform performs the entire request in either blocking or non-blocking manner. By default, the API performs request in a blocking manner and returns when done, + * or if it failed, and in non-blocking manner, it returns if EAGAIN/EWOULDBLOCK or EINPROGRESS is encountered, or if it failed. And in case of non-blocking request, + * the user may call this API multiple times unless request & response is complete or there is a failure. To enable non-blocking esp_http_client_perform(), `is_async` member of esp_http_client_config_t + * must be set while making a call to esp_http_client_init() API. * You can do any amount of calls to esp_http_client_perform while using the same esp_http_client_handle_t. The underlying connection may be kept open if the server allows it. * If you intend to transfer more than one file, you are even encouraged to do so. * esp_http_client will then attempt to re-use the same connection for the following transfers, thus making the operations faster, less CPU intense and using less network resources. diff --git a/components/tcp_transport/include/transport.h b/components/tcp_transport/include/transport.h index a54cb83c5..f10a8f8d6 100644 --- a/components/tcp_transport/include/transport.h +++ b/components/tcp_transport/include/transport.h @@ -30,6 +30,7 @@ typedef int (*io_func)(transport_handle_t t, const char *buffer, int len, int ti typedef int (*io_read_func)(transport_handle_t t, char *buffer, int len, int timeout_ms); typedef int (*trans_func)(transport_handle_t t); typedef int (*poll_func)(transport_handle_t t, int timeout_ms); +typedef int (*connect_async_func)(transport_handle_t t, const char *host, int port, int timeout_ms); typedef transport_handle_t (*payload_transfer_func)(transport_handle_t); /** @@ -138,6 +139,20 @@ esp_err_t transport_set_default_port(transport_handle_t t, int port); */ int transport_connect(transport_handle_t t, const char *host, int port, int timeout_ms); +/** + * @brief Non-blocking transport connection function, to make a connection to server + * + * @param t The transport handle + * @param[in] host Hostname + * @param[in] port Port + * @param[in] timeout_ms The timeout milliseconds + * + * @return + * - socket for will use by this transport + * - (-1) if there are any errors, should check errno + */ +int transport_connect_async(transport_handle_t t, const char *host, int port, int timeout_ms); + /** * @brief Transport read function * @@ -259,6 +274,20 @@ esp_err_t transport_set_func(transport_handle_t t, poll_func _poll_write, trans_func _destroy, payload_transfer_func _parrent_transport); + + +/** + * @brief Set transport functions for the transport handle + * + * @param[in] t The transport handle + * @param[in] _connect_async_func The connect_async function pointer + * + * @return + * - ESP_OK + * - ESP_FAIL + */ +esp_err_t transport_set_async_connect_func(transport_handle_t t, connect_async_func _connect_async_func); + #ifdef __cplusplus } #endif diff --git a/components/tcp_transport/transport.c b/components/tcp_transport/transport.c index abff9670f..c23ed8d34 100644 --- a/components/tcp_transport/transport.c +++ b/components/tcp_transport/transport.c @@ -40,6 +40,7 @@ struct transport_item_t { poll_func _poll_read; /*!< Poll and read */ poll_func _poll_write; /*!< Poll and write */ trans_func _destroy; /*!< Destroy and free transport */ + connect_async_func _connect_async; /*!< non-blocking connect function of this transport */ payload_transfer_func _parrent_transfer; /*!< Function returning underlying transport layer */ STAILQ_ENTRY(transport_item_t) next; @@ -145,6 +146,15 @@ int transport_connect(transport_handle_t t, const char *host, int port, int time return ret; } +int transport_connect_async(transport_handle_t t, const char *host, int port, int timeout_ms) +{ + int ret = -1; + if (t && t->_connect) { + return t->_connect_async(t, host, port, timeout_ms); + } + return ret; +} + int transport_read(transport_handle_t t, char *buffer, int len, int timeout_ms) { if (t && t->_read) { @@ -222,6 +232,7 @@ esp_err_t transport_set_func(transport_handle_t t, t->_poll_read = _poll_read; t->_poll_write = _poll_write; t->_destroy = _destroy; + t->_connect_async = NULL; t->_parrent_transfer = _parrent_transport; return ESP_OK; } @@ -246,4 +257,13 @@ esp_err_t transport_set_default_port(transport_handle_t t, int port) transport_handle_t transport_get_handle(transport_handle_t t) { return t; -} \ No newline at end of file +} + +esp_err_t transport_set_async_connect_func(transport_handle_t t, connect_async_func _connect_async_func) +{ + if (t == NULL) { + return ESP_FAIL; + } + t->_connect_async = _connect_async_func; + return ESP_OK; +} diff --git a/components/tcp_transport/transport_ssl.c b/components/tcp_transport/transport_ssl.c index 9ccaf4028..8e995764e 100644 --- a/components/tcp_transport/transport_ssl.c +++ b/components/tcp_transport/transport_ssl.c @@ -26,33 +26,58 @@ #include "transport_utils.h" static const char *TAG = "TRANS_SSL"; + +typedef enum { + TRANS_SSL_INIT = 0, + TRANS_SSL_CONNECTING, +} transport_ssl_conn_state_t; + /** * mbedtls specific transport data */ typedef struct { esp_tls_t *tls; - void *cert_pem_data; - int cert_pem_len; + esp_tls_cfg_t cfg; bool ssl_initialized; bool verify_server; + transport_ssl_conn_state_t conn_state; } transport_ssl_t; transport_handle_t transport_get_handle(transport_handle_t t); static int ssl_close(transport_handle_t t); +static int ssl_connect_async(transport_handle_t t, const char *host, int port, int timeout_ms) +{ + transport_ssl_t *ssl = transport_get_context_data(t); + if (ssl->conn_state == TRANS_SSL_INIT) { + if (ssl->cfg.cacert_pem_buf) { + ssl->verify_server = true; + } + ssl->cfg.timeout_ms = timeout_ms; + ssl->cfg.non_block = true; + ssl->ssl_initialized = true; + ssl->tls = calloc(1, sizeof(esp_tls_t)); + if (!ssl->tls) { + return -1; + } + ssl->conn_state = TRANS_SSL_CONNECTING; + } + if (ssl->conn_state == TRANS_SSL_CONNECTING) { + return esp_tls_conn_new_async(host, strlen(host), port, &ssl->cfg, ssl->tls); + } + return 0; +} + static int ssl_connect(transport_handle_t t, const char *host, int port, int timeout_ms) { transport_ssl_t *ssl = transport_get_context_data(t); - esp_tls_cfg_t cfg = { 0 }; - if (ssl->cert_pem_data) { + if (ssl->cfg.cacert_pem_buf) { ssl->verify_server = true; - cfg.cacert_pem_buf = ssl->cert_pem_data; - cfg.cacert_pem_bytes = ssl->cert_pem_len + 1; } - cfg.timeout_ms = timeout_ms; + ssl->cfg.timeout_ms = timeout_ms; ssl->ssl_initialized = true; - ssl->tls = esp_tls_conn_new(host, strlen(host), port, &cfg); + ssl->tls = esp_tls_conn_new(host, strlen(host), port, &ssl->cfg); if (!ssl->tls) { ESP_LOGE(TAG, "Failed to open a new connection"); return -1; @@ -94,7 +119,7 @@ static int ssl_write(transport_handle_t t, const char *buffer, int len, int time } ret = esp_tls_conn_write(ssl->tls, (const unsigned char *) buffer, len); if (ret <= 0) { - ESP_LOGE(TAG, "mbedtls_ssl_write error, errno=%s", strerror(errno)); + ESP_LOGE(TAG, "esp_tls_conn_write error, errno=%s", strerror(errno)); } return ret; } @@ -111,7 +136,7 @@ static int ssl_read(transport_handle_t t, char *buffer, int len, int timeout_ms) } ret = esp_tls_conn_read(ssl->tls, (unsigned char *)buffer, len); if (ret <= 0) { - ESP_LOGE(TAG, "mbedtls_ssl_read error, errno=%s", strerror(errno)); + ESP_LOGE(TAG, "esp_tls_conn_read error, errno=%s", strerror(errno)); } return ret; } @@ -140,8 +165,8 @@ void transport_ssl_set_cert_data(transport_handle_t t, const char *data, int len { transport_ssl_t *ssl = transport_get_context_data(t); if (t && ssl) { - ssl->cert_pem_data = (void *)data; - ssl->cert_pem_len = len; + ssl->cfg.cacert_pem_buf = (void *)data; + ssl->cfg.cacert_pem_bytes = len + 1; } } @@ -152,6 +177,7 @@ transport_handle_t transport_ssl_init() TRANSPORT_MEM_CHECK(TAG, ssl, return NULL); transport_set_context_data(t, ssl); transport_set_func(t, ssl_connect, ssl_read, ssl_write, ssl_close, ssl_poll_read, ssl_poll_write, ssl_destroy, transport_get_handle); + transport_set_async_connect_func(t, ssl_connect_async); return t; } diff --git a/examples/protocols/esp_http_client/main/esp_http_client_example.c b/examples/protocols/esp_http_client/main/esp_http_client_example.c index f862ddc70..ac5e1942c 100644 --- a/examples/protocols/esp_http_client/main/esp_http_client_example.c +++ b/examples/protocols/esp_http_client/main/esp_http_client_example.c @@ -342,6 +342,38 @@ static void http_perform_as_stream_reader() free(buffer); } +static void https_async() +{ + esp_http_client_config_t config = { + .url = "https://postman-echo.com/post", + .event_handler = _http_event_handler, + .is_async = true, + .timeout_ms = 5000, + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + esp_err_t err; + const char *post_data = "Using a Palantír requires a person with great strength of will and wisdom. The Palantíri were meant to " + "be used by the Dúnedain to communicate throughout the Realms in Exile. During the War of the Ring, " + "the Palantíri were used by many individuals. Sauron used the Ithil-stone to take advantage of the users " + "of the other two stones, the Orthanc-stone and Anor-stone, but was also susceptible to deception himself."; + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_http_client_set_post_field(client, post_data, strlen(post_data)); + while (1) { + err = esp_http_client_perform(client); + if (err != ESP_ERR_HTTP_EAGAIN) { + break; + } + } + if (err == ESP_OK) { + ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d", + esp_http_client_get_status_code(client), + esp_http_client_get_content_length(client)); + } else { + ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err)); + } + esp_http_client_cleanup(client); +} + static void http_test_task(void *pvParameters) { app_wifi_wait_connected(); @@ -356,6 +388,7 @@ static void http_test_task(void *pvParameters) http_redirect_to_https(); http_download_chunk(); http_perform_as_stream_reader(); + https_async(); ESP_LOGI(TAG, "Finish http example"); vTaskDelete(NULL); }