diff --git a/components/esp_http_server/CMakeLists.txt b/components/esp_http_server/CMakeLists.txt index 0755d8f811..80ea7af2ab 100644 --- a/components/esp_http_server/CMakeLists.txt +++ b/components/esp_http_server/CMakeLists.txt @@ -3,8 +3,9 @@ idf_component_register(SRCS "src/httpd_main.c" "src/httpd_sess.c" "src/httpd_txrx.c" "src/httpd_uri.c" + "src/httpd_ws.c" "src/util/ctrl_sock.c" INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS "src/port/esp32" "src/util" REQUIRES nghttp # for http_parser.h - PRIV_REQUIRES lwip esp_timer) + PRIV_REQUIRES lwip mbedtls esp_timer) diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index 6112cbade0..2f8ed780c9 100644 --- a/components/esp_http_server/Kconfig +++ b/components/esp_http_server/Kconfig @@ -39,4 +39,10 @@ menu "HTTP Server" Enabling this will log discarded binary HTTP request data at Debug level. For large content data this may not be desirable as it will clutter the log. + config HTTPD_WS_SUPPORT + bool "WebSocket server support" + default n + help + This sets the WebSocket server support. + endmenu diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 8f6ef5f9e6..e1e5c288d9 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -399,6 +399,14 @@ typedef struct httpd_uri { * Pointer to user context data which will be available to handler */ void *user_ctx; + +#ifdef CONFIG_HTTPD_WS_SUPPORT + /** + * Flag for indicating a WebSocket endpoint. + * If this flag is true, then method must be HTTP_GET. Otherwise the handshake will not be handled. + */ + bool is_websocket; +#endif } httpd_uri_t; /** @@ -1452,6 +1460,82 @@ esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *ar * @} */ +/* ************** Group: WebSocket ************** */ +/** @name WebSocket + * Functions and structs for WebSocket server + * @{ + */ +#ifdef CONFIG_HTTPD_WS_SUPPORT +/** + * @brief Enum for WebSocket packet types (Opcode in the header) + * @note Please refer to RFC6455 Section 5.4 for more details + */ +typedef enum { + HTTPD_WS_TYPE_CONTINUE = 0x0, + HTTPD_WS_TYPE_TEXT = 0x1, + HTTPD_WS_TYPE_BINARY = 0x2, + HTTPD_WS_TYPE_CLOSE = 0x8, + HTTPD_WS_TYPE_PING = 0x9, + HTTPD_WS_TYPE_PONG = 0xA +} httpd_ws_type_t; + +/** + * @brief WebSocket frame format + */ +typedef struct httpd_ws_frame { + bool final; /*!< Final frame */ + httpd_ws_type_t type; /*!< WebSocket frame type */ + uint8_t *payload; /*!< Pre-allocated data buffer */ + size_t len; /*!< Length of the WebSocket data */ +} httpd_ws_frame_t; + +/** + * @brief Receive and parse a WebSocket frame + * @param[in] req Current request + * @param[out] pkt WebSocket packet + * @param[in] max_len Maximum length for receive + * @return + * - ESP_OK : On successful + * - ESP_FAIL : Socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t max_len); + +/** + * @brief Construct and send a WebSocket frame + * @param[in] req Current request + * @param[in] pkt WebSocket frame + * @return + * - ESP_OK : On successful + * - ESP_FAIL : When socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *pkt); + +/** + * @brief Low level send of a WebSocket frame out of the scope of current request + * using internally configured httpd send function + * + * This API should rarely be called directly, with an exception of asynchronous send using httpd_queue_work. + * + * @param[in] hd Server instance data + * @param[in] fd Socket descriptor for sending data + * @param[in] frame WebSocket frame + * @return + * - ESP_OK : On successful + * - ESP_FAIL : When socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame); + +#endif /* CONFIG_HTTPD_WS_SUPPORT */ +/** End of WebSocket related stuff + * @} + */ + #ifdef __cplusplus } #endif diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h index d0cdfd0fda..27a3051ac2 100644 --- a/components/esp_http_server/src/esp_httpd_priv.h +++ b/components/esp_http_server/src/esp_httpd_priv.h @@ -71,6 +71,11 @@ struct sock_db { uint64_t lru_counter; /*!< LRU Counter indicating when the socket was last used */ char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */ size_t pending_len; /*!< Length of pending data to be received */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + bool ws_handshake_done; /*!< True if it has done WebSocket handshake (if this socket is a valid WS) */ + bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */ + esp_err_t (*ws_handler)(httpd_req_t *r); /*!< WebSocket handler, leave to null if it's not WebSocket */ +#endif }; /** @@ -91,6 +96,11 @@ struct httpd_req_aux { const char *value; } *resp_hdrs; /*!< Additional headers in response packet */ struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + bool ws_handshake_detect; /*!< WebSocket handshake detection flag */ + httpd_ws_type_t ws_type; /*!< WebSocket frame type */ + bool ws_final; /*!< WebSocket FIN bit (final frame or not) */ +#endif }; /** @@ -471,6 +481,44 @@ int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, * @} */ +/* ************** Group: WebSocket ************** */ +/** @name WebSocket + * Functions for WebSocket header parsing + * @{ + */ + + +/** + * @brief This function is for responding a WebSocket handshake + * + * @param[in] req Pointer to handshake request that will be handled + * @return + * - ESP_OK : When handshake is sucessful + * - ESP_ERR_NOT_FOUND : When some headers (Sec-WebSocket-*) are not found + * - ESP_ERR_INVALID_VERSION : The WebSocket version is not "13" + * - ESP_ERR_INVALID_STATE : Handshake was done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + * - ESP_FAIL : Socket failures + */ +esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req); + +/** + * @brief This function is for getting a frame type + * and responding a WebSocket control frame automatically + * + * @param[in] req Pointer to handshake request that will be handled + * @return + * - ESP_OK : When handshake is sucessful + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + * - ESP_ERR_INVALID_STATE : Received only some parts of a control frame + * - ESP_FAIL : Socket failures + */ +esp_err_t httpd_ws_get_frame_type(httpd_req_t *req); + +/** End of WebSocket related functions + * @} + */ + #ifdef __cplusplus } #endif diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index ec73bbb5f5..30ce87d468 100644 --- a/components/esp_http_server/src/httpd_parse.c +++ b/components/esp_http_server/src/httpd_parse.c @@ -374,13 +374,36 @@ static esp_err_t cb_headers_complete(http_parser *parser) ESP_LOGD(TAG, LOG_FMT("bytes read = %d"), parser->nread); ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len); + /* Handle upgrade requests - only WebSocket is supported for now */ if (parser->upgrade) { - ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported")); - /* There is no specific HTTP error code to notify the client that - * upgrade is not supported, thus sending 400 Bad Request */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + ESP_LOGD(TAG, LOG_FMT("Got an upgrade request")); + + /* If there's no "Upgrade" header field, then it's not WebSocket. */ + char ws_upgrade_hdr_val[] = "websocket"; + if (httpd_req_get_hdr_value_str(r, "Upgrade", ws_upgrade_hdr_val, sizeof(ws_upgrade_hdr_val)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("Upgrade header does not match the length of \"websocket\"")); + parser_data->error = HTTPD_400_BAD_REQUEST; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* If "Upgrade" field's key is not "websocket", then we should also forget about it. */ + if (strcasecmp("websocket", ws_upgrade_hdr_val) != 0) { + ESP_LOGW(TAG, LOG_FMT("Upgrade header found but it's %s"), ws_upgrade_hdr_val); + parser_data->error = HTTPD_400_BAD_REQUEST; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Now set handshake flag to true */ + ra->ws_handshake_detect = true; +#else + ESP_LOGD(TAG, LOG_FMT("WS functions has been disabled, Upgrade request is not supported.")); parser_data->error = HTTPD_400_BAD_REQUEST; parser_data->status = PARSING_FAILED; return ESP_FAIL; +#endif } parser_data->status = PARSING_BODY; @@ -667,6 +690,9 @@ static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config) ra->first_chunk_sent = 0; ra->req_hdrs_count = 0; ra->resp_hdrs_count = 0; +#if CONFIG_HTTPD_WS_SUPPORT + ra->ws_handshake_detect = false; +#endif memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr)); } @@ -678,6 +704,15 @@ static void httpd_req_cleanup(httpd_req_t *r) if ((r->ignore_sess_ctx_changes == false) && (ra->sd->ctx != r->sess_ctx)) { httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_ctx); } + +#if CONFIG_HTTPD_WS_SUPPORT + /* Close the socket when a WebSocket Close request is received */ + if (ra->sd->ws_close) { + ESP_LOGD(TAG, LOG_FMT("Try closing WS connection at FD: %d"), ra->sd->fd); + httpd_sess_trigger_close(r->handle, ra->sd->fd); + } +#endif + /* Retrieve session info from the request into the socket database. */ ra->sd->ctx = r->sess_ctx; ra->sd->free_ctx = r->free_ctx; @@ -699,23 +734,62 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd) init_req_aux(&hd->hd_req_aux, &hd->config); r->handle = hd; r->aux = &hd->hd_req_aux; + /* Associate the request to the socket */ struct httpd_req_aux *ra = r->aux; ra->sd = sd; + /* Set defaults */ ra->status = (char *)HTTPD_200; ra->content_type = (char *)HTTPD_TYPE_TEXT; ra->first_chunk_sent = false; + /* Copy session info to the request */ r->sess_ctx = sd->ctx; r->free_ctx = sd->free_ctx; r->ignore_sess_ctx_changes = sd->ignore_sess_ctx_changes; + + esp_err_t ret; + +#ifdef CONFIG_HTTPD_WS_SUPPORT + /* Handle WebSocket */ + ESP_LOGD(TAG, LOG_FMT("New request, has WS? %s, sd->ws_handler valid? %s, sd->ws_close? %s"), + sd->ws_handshake_done ? "Yes" : "No", + sd->ws_handler != NULL ? "Yes" : "No", + sd->ws_close ? "Yes" : "No"); + if (sd->ws_handshake_done && sd->ws_handler != NULL) { + ESP_LOGD(TAG, LOG_FMT("New WS request from existing socket")); + ret = httpd_ws_get_frame_type(r); + + /* Stop and return here immediately if it's a CLOSE frame */ + if (ra->ws_type == HTTPD_WS_TYPE_CLOSE) { + sd->ws_close = true; + return ret; + } + + /* Ignore PONG frame, as this is a server */ + if (ra->ws_type == HTTPD_WS_TYPE_PONG) { + return ret; + } + + /* Call handler if it's a non-control frame */ + if (ret == ESP_OK && ra->ws_type < HTTPD_WS_TYPE_CLOSE) { + ret = sd->ws_handler(r); + } + + if (ret != ESP_OK) { + httpd_req_cleanup(r); + } + return ret; + } +#endif + /* Parse request */ - esp_err_t err = httpd_parse_req(hd); - if (err != ESP_OK) { + ret = httpd_parse_req(hd); + if (ret != ESP_OK) { httpd_req_cleanup(r); } - return err; + return ret; } /* Function that resets the http request data diff --git a/components/esp_http_server/src/httpd_sess.c b/components/esp_http_server/src/httpd_sess.c index 9deb6df2fe..5f044afa13 100644 --- a/components/esp_http_server/src/httpd_sess.c +++ b/components/esp_http_server/src/httpd_sess.c @@ -281,7 +281,9 @@ bool httpd_sess_pending(struct httpd_data *hd, int fd) if (sd->pending_fn) { // test if there's any data to be read (besides read() function, which is handled by select() in the main httpd loop) // this should check e.g. for the SSL data buffer - if (sd->pending_fn(hd, fd) > 0) return true; + if (sd->pending_fn(hd, fd) > 0) { + return true; + } } return (sd->pending_len != 0); diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index 9ffd3f9147..c3d559ad2a 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -172,6 +172,9 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle, hd->hd_calls[i]->method = uri_handler->method; hd->hd_calls[i]->handler = uri_handler->handler; hd->hd_calls[i]->user_ctx = uri_handler->user_ctx; +#ifdef CONFIG_HTTPD_WS_SUPPORT + hd->hd_calls[i]->is_websocket = uri_handler->is_websocket; +#endif ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri); return ESP_OK; } @@ -307,6 +310,24 @@ esp_err_t httpd_uri(struct httpd_data *hd) /* Attach user context data (passed during URI registration) into request */ req->user_ctx = uri->user_ctx; + /* Final step for a WebSocket handshake verification */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + struct httpd_req_aux *aux = req->aux; + if (uri->is_websocket && aux->ws_handshake_detect && uri->method == HTTP_GET) { + ESP_LOGD(TAG, LOG_FMT("Responding WS handshake to sock %d"), aux->sd->fd); + esp_err_t ret = httpd_ws_respond_server_handshake(&hd->hd_req); + if (ret != ESP_OK) { + return ret; + } + + aux->sd->ws_handshake_done = true; + aux->sd->ws_handler = uri->handler; + + /* Return immediately after handshake, no need to call handler here */ + return ESP_OK; + } +#endif + /* Invoke handler */ if (uri->handler(req) != ESP_OK) { /* Handler returns error, this socket should be closed */ diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c new file mode 100644 index 0000000000..d9a1cc7676 --- /dev/null +++ b/components/esp_http_server/src/httpd_ws.c @@ -0,0 +1,384 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + + +#include +#include +#include +#include +#include +#include + +#include +#include "esp_httpd_priv.h" + +#ifdef CONFIG_HTTPD_WS_SUPPORT + +static const char *TAG="httpd_ws"; + +/* + * Bit masks for WebSocket frames. + * Please refer to RFC6455 Section 5.2 for more details. + */ +#define HTTPD_WS_FIN_BIT 0x80U +#define HTTPD_WS_OPCODE_BITS 0x0fU +#define HTTPD_WS_MASK_BIT 0x80U +#define HTTPD_WS_LENGTH_BITS 0x7fU + +/* + * The magic GUID string used for handshake + * Please refer to RFC6455 Section 1.3 for more details. + */ +static const char ws_magic_uuid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req) +{ + /* Probe if input parameters are valid or not */ + if (!req || !req->aux) { + ESP_LOGW(TAG, LOG_FMT("Argument is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Detect handshake - reject if handshake was ALREADY performed */ + struct httpd_req_aux *req_aux = req->aux; + if (req_aux->sd->ws_handshake_done) { + ESP_LOGW(TAG, LOG_FMT("State is invalid - Handshake has been performed")); + return ESP_ERR_INVALID_STATE; + } + + /* Detect WS version existence */ + char version_val[3] = { '\0' }; + if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Version", version_val, sizeof(version_val)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not found")); + return ESP_ERR_NOT_FOUND; + } + + /* Detect if WS version is "13" or not. + * WS version must be 13 for now. Please refer to RFC6455 Section 4.1, Page 18 for more details. */ + if (strcasecmp(version_val, "13") != 0) { + ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not \"13\", it is: %s"), version_val); + return ESP_ERR_INVALID_VERSION; + } + + /* Grab Sec-WebSocket-Key (client key) from the header */ + /* Size of base64 coded string is equal '((input_size * 4) / 3) + (input_size / 96) + 6' including Z-term */ + char sec_key_encoded[28] = { '\0' }; + if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Key", sec_key_encoded, sizeof(sec_key_encoded)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("Cannot find client key")); + return ESP_ERR_NOT_FOUND; + } + + /* Prepare server key (Sec-WebSocket-Accept), concat the string */ + char server_key_encoded[33] = { '\0' }; + uint8_t server_key_hash[20] = { 0 }; + char server_raw_text[sizeof(sec_key_encoded) + sizeof(ws_magic_uuid) + 1] = { '\0' }; + + strcpy(server_raw_text, sec_key_encoded); + strcat(server_raw_text, ws_magic_uuid); + + ESP_LOGD(TAG, LOG_FMT("Server key before encoding: %s"), server_raw_text); + + /* Generate SHA-1 first and then encode to Base64 */ + size_t key_len = strlen(server_raw_text); + mbedtls_sha1_ret((uint8_t *)server_raw_text, key_len, server_key_hash); + + size_t encoded_len = 0; + mbedtls_base64_encode((uint8_t *)server_key_encoded, sizeof(server_key_encoded), &encoded_len, + server_key_hash, sizeof(server_key_hash)); + + ESP_LOGD(TAG, LOG_FMT("Generated server key: %s"), server_key_encoded); + + /* Prepare the Switching Protocol response */ + char tx_buf[192] = { '\0' }; + int fmt_len = snprintf(tx_buf, sizeof(tx_buf), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n\r\n", server_key_encoded); + if (fmt_len < 0 || fmt_len > sizeof(tx_buf)) { + ESP_LOGW(TAG, LOG_FMT("Failed to prepare Tx buffer")); + return ESP_FAIL; + } + + /* Send off the response */ + if (httpd_send(req, tx_buf, fmt_len) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send the response")); + return ESP_FAIL; + } + + return ESP_OK; +} + +static esp_err_t httpd_ws_check_req(httpd_req_t *req) +{ + /* Probe if input parameters are valid or not */ + if (!req || !req->aux) { + ESP_LOGW(TAG, LOG_FMT("Argument is null")); + return ESP_ERR_INVALID_ARG; + } + + /* Detect handshake - reject if handshake was NOT YET performed */ + struct httpd_req_aux *req_aux = req->aux; + if (!req_aux->sd->ws_handshake_done) { + ESP_LOGW(TAG, LOG_FMT("State is invalid - No handshake performed")); + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} + +static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uint8_t *mask_key) +{ + if (len < 1 || !payload) { + ESP_LOGW(TAG, LOG_FMT("Invalid payload provided")); + return ESP_ERR_INVALID_ARG; + } + + for (size_t idx = 0; idx < len; idx++) { + payload[idx] = (payload[idx] ^ mask_key[idx % 4]); + } + + return ESP_OK; +} + +esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t max_len) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + + struct httpd_req_aux *aux = req->aux; + if (aux == NULL) { + ESP_LOGW(TAG, LOG_FMT("Invalid Aux pointer")); + return ESP_ERR_INVALID_ARG; + } + + if (!frame) { + ESP_LOGW(TAG, LOG_FMT("Frame pointer is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Assign the frame info from the previous reading */ + frame->type = aux->ws_type; + frame->final = aux->ws_final; + + /* Grab the second byte */ + uint8_t second_byte = 0; + if (httpd_recv_with_opt(req, (char *)&second_byte, sizeof(second_byte), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive the second byte")); + return ESP_FAIL; + } + + /* Parse the second byte */ + /* Please refer to RFC6455 Section 5.2 for more details */ + bool masked = (second_byte & HTTPD_WS_MASK_BIT) != 0; + + /* Interpret length */ + uint8_t init_len = second_byte & HTTPD_WS_LENGTH_BITS; + if (init_len < 126) { + /* Case 1: If length is 0-125, then this length bit is 7 bits */ + frame->len = init_len; + } else if (init_len == 126) { + /* Case 2: If length byte is 126, then this frame's length bit is 16 bits */ + uint8_t length_bytes[2] = { 0 }; + if (httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive 2 bytes length")); + return ESP_FAIL; + } + + frame->len = ((uint32_t)(length_bytes[0] << 8U) | (length_bytes[1])); + } else if (init_len == 127) { + /* Case 3: If length is byte 127, then this frame's length bit is 64 bits */ + uint8_t length_bytes[8] = { 0 }; + if (httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive 2 bytes length")); + return ESP_FAIL; + } + + frame->len = (((uint64_t)length_bytes[0] << 56U) | + ((uint64_t)length_bytes[1] << 48U) | + ((uint64_t)length_bytes[2] << 40U) | + ((uint64_t)length_bytes[3] << 32U) | + ((uint64_t)length_bytes[4] << 24U) | + ((uint64_t)length_bytes[5] << 16U) | + ((uint64_t)length_bytes[6] << 8U) | + ((uint64_t)length_bytes[7])); + } + + /* We only accept the incoming packet length that is smaller than the max_len (or it will overflow the buffer!) */ + if (frame->len > max_len) { + ESP_LOGW(TAG, LOG_FMT("WS Message too long")); + return ESP_ERR_INVALID_SIZE; + } + + /* If this frame is masked, dump the mask as well */ + uint8_t mask_key[4] = { 0 }; + if (masked) { + if (httpd_recv_with_opt(req, (char *)mask_key, sizeof(mask_key), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive mask key")); + return ESP_FAIL; + } + } else { + /* If the WS frame from client to server is not masked, it should be rejected. + * Please refer to RFC6455 Section 5.2 for more details. */ + ESP_LOGW(TAG, LOG_FMT("WS frame is not properly masked.")); + return ESP_ERR_INVALID_STATE; + } + + /* Receive buffer */ + /* If there's nothing to receive, return and stop here. */ + if (frame->len == 0) { + return ESP_OK; + } + + if (frame->payload == NULL) { + ESP_LOGW(TAG, LOG_FMT("Payload buffer is null")); + return ESP_FAIL; + } + + if (httpd_recv_with_opt(req, (char *)frame->payload, frame->len, false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive payload")); + return ESP_FAIL; + } + + /* Unmask payload */ + httpd_ws_unmask_payload(frame->payload, frame->len, mask_key); + + return ESP_OK; +} + +esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *frame) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + return httpd_ws_send_frame_async(req->handle, httpd_req_to_sockfd(req), frame); +} + +esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame) +{ + if (!frame) { + ESP_LOGW(TAG, LOG_FMT("Argument is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Prepare Tx buffer - maximum length is 14, which includes 2 bytes header, 8 bytes length, 4 bytes mask key */ + uint8_t tx_len = 0; + uint8_t header_buf[10] = {0 }; + header_buf[0] |= frame->final ? HTTPD_WS_FIN_BIT : 0; /* Final (FIN) bit */ + header_buf[0] |= frame->type; /* Type (opcode): 4 bits */ + + if (frame->len <= 125) { + header_buf[1] = frame->len & 0x7fU; /* Length for 7 bits */ + tx_len = 2; + } else if (frame->len > 125 && frame->len < UINT16_MAX) { + header_buf[1] = 126; /* Length for 16 bits */ + header_buf[2] = (frame->len >> 8U) & 0xffU; + header_buf[3] = frame->len & 0xffU; + tx_len = 4; + } else { + header_buf[1] = 127; /* Length for 64 bits */ + uint8_t shift_idx = sizeof(uint64_t) - 1; /* Shift index starts at 7 */ + for (int8_t idx = 2; idx > 9; idx--) { + /* Now do shifting (be careful of endianess, i.e. when buffer index is 2, frame length shift index is 7) */ + header_buf[idx] = (frame->len >> (uint8_t)(shift_idx * 8)) & 0xffU; + shift_idx--; + } + tx_len = 10; + } + + /* WebSocket server does not required to mask response payload, so leave the MASK bit as 0. */ + header_buf[1] &= (~HTTPD_WS_MASK_BIT); + + struct sock_db *sess = httpd_sess_get(hd, fd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + + /* Send off header */ + if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send WS header")); + return ESP_FAIL; + } + + /* Send off payload */ + if(frame->len > 0 && frame->payload != NULL) { + if (sess->send_fn(hd, fd, (const char *)frame->payload, frame->len, 0) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send WS payload")); + return ESP_FAIL; + } + } + + return ESP_OK; +} + +esp_err_t httpd_ws_get_frame_type(httpd_req_t *req) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + + struct httpd_req_aux *aux = req->aux; + if (aux == NULL) { + ESP_LOGW(TAG, LOG_FMT("Invalid Aux pointer")); + return ESP_ERR_INVALID_ARG; + } + + /* Read the first byte from the frame to get the FIN flag and Opcode */ + /* Please refer to RFC6455 Section 5.2 for more details */ + uint8_t first_byte = 0; + if (httpd_recv_with_opt(req, (char *)&first_byte, sizeof(first_byte), false) <= 0) { + /* If the recv() return code is <= 0, then this socket FD is invalid (i.e. a broken connection) */ + /* Here we mark it as a Close message and close it later. */ + ESP_LOGW(TAG, LOG_FMT("Failed to read header byte (socket FD invalid), closing socket now")); + aux->ws_final = true; + aux->ws_type = HTTPD_WS_TYPE_CLOSE; + return ESP_OK; + } + + ESP_LOGD(TAG, LOG_FMT("First byte received: 0x%02X"), first_byte); + + /* Decode the FIN flag and Opcode from the byte */ + aux->ws_final = (first_byte & HTTPD_WS_FIN_BIT) != 0; + aux->ws_type = (first_byte & HTTPD_WS_OPCODE_BITS); + + /* Reply to PING. For PONG and CLOSE, it will be handled elsewhere. */ + if(aux->ws_type == HTTPD_WS_TYPE_PING) { + ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG...")); + + /* Read the rest of the PING frame, for PONG to reply back. */ + /* Please refer to RFC6455 Section 5.5.2 for more details */ + httpd_ws_frame_t frame; + uint8_t frame_buf[128] = { 0 }; + memset(&frame, 0, sizeof(httpd_ws_frame_t)); + frame.payload = frame_buf; + + if(httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) { + ESP_LOGD(TAG, LOG_FMT("Cannot receive the full PING frame")); + return ESP_ERR_INVALID_STATE; + } + + /* Now turn the frame to PONG */ + frame.type = HTTPD_WS_TYPE_PONG; + return httpd_ws_send_frame(req, &frame); + } + + return ESP_OK; +} + +#endif /* CONFIG_HTTPD_WS_SUPPORT */ \ No newline at end of file diff --git a/docs/en/api-reference/protocols/esp_http_server.rst b/docs/en/api-reference/protocols/esp_http_server.rst index ed414c2e60..50cc2278fe 100644 --- a/docs/en/api-reference/protocols/esp_http_server.rst +++ b/docs/en/api-reference/protocols/esp_http_server.rst @@ -152,6 +152,13 @@ Persistent Connections Example Check the example under :example:`protocols/http_server/persistent_sockets`. +Websocket server +---------------- + +HTTP server provides a simple websocket support if the feature is enabled in menuconfig, please see :ref:`CONFIG_HTTPD_WS_SUPPORT`. +Please check the example under :example:`protocols/http_server/ws_echo_server` + + API Reference ------------- diff --git a/examples/protocols/http_server/ws_echo_server/CMakeLists.txt b/examples/protocols/http_server/ws_echo_server/CMakeLists.txt new file mode 100644 index 0000000000..6ee7dd4fcd --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +# (Not part of the boilerplate) +# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ws_echo_server) diff --git a/examples/protocols/http_server/ws_echo_server/Makefile b/examples/protocols/http_server/ws_echo_server/Makefile new file mode 100644 index 0000000000..394c5e53a5 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := ws_echo_server + +EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/protocols/http_server/ws_echo_server/README.md b/examples/protocols/http_server/ws_echo_server/README.md new file mode 100644 index 0000000000..61bcc0fbbd --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/README.md @@ -0,0 +1,121 @@ +# Websocket echo server + +(See the README.md file in the upper level 'examples' directory for more information about examples.) +This example demonstrates the HTTPD server using the WebSocket feature. + +## How to Use Example + +The example starts a WS server on a local network, so a WS client is needed to interact with the server (an example test +ws_server_example_test.py could be used as a simple WS client). + +The server registers WebSocket handler which echoes back the received WebSocket frame. It also demonstrates +use of asynchronous send, which is triggered on reception of a certain message. + +### Hardware Required + +This example can be executed on any common development board, the only required interface is WiFi or Ethernet connection to a local network. + +### Configure the project + +* Open the project configuration menu (`idf.py menuconfig`) +* Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output +``` +I (4932) example_connect: Got IPv6 event! +I (4942) example_connect: Connected to Espressif +I (4942) example_connect: IPv4 address: 192.168.4.2 +I (4952) example_connect: IPv6 address: fe80:xxxx +I (4962) ws_echo_server: Starting server on port: '80' +I (4962) ws_echo_server: Registering URI handlers +D (4962) httpd: httpd_thread: web server started +D (4972) httpd: httpd_server: doing select maxfd+1 = 56 +D (4982) httpd_uri: httpd_register_uri_handler: [0] installed /ws +D (17552) httpd: httpd_server: processing listen socket 54 +D (17552) httpd: httpd_accept_conn: newfd = 57 +D (17552) httpd_sess: httpd_sess_new: fd = 57 +D (17562) httpd: httpd_accept_conn: complete +D (17562) httpd: httpd_server: doing select maxfd+1 = 58 +D (17572) httpd: httpd_server: processing socket 57 +D (17572) httpd_sess: httpd_sess_process: httpd_req_new +D (17582) httpd_parse: httpd_req_new: New request, has WS? No, sd->ws_handler valid? No, sd->ws_close? No +D (17592) httpd_txrx: httpd_recv_with_opt: requested length = 128 +D (17592) httpd_txrx: httpd_recv_with_opt: received length = 128 +D (17602) httpd_parse: read_block: received HTTP request block size = 128 +D (17612) httpd_parse: cb_url: message begin +D (17612) httpd_parse: cb_url: processing url = /ws +D (17622) httpd_parse: verify_url: received URI = /ws +D (17622) httpd_parse: cb_header_field: headers begin +D (17632) httpd_txrx: httpd_unrecv: length = 110 +D (17632) httpd_parse: pause_parsing: paused +D (17632) httpd_parse: cb_header_field: processing field = Host +D (17642) httpd_txrx: httpd_recv_with_opt: requested length = 128 +D (17652) httpd_txrx: httpd_recv_with_opt: pending length = 110 +D (17652) httpd_parse: read_block: received HTTP request block size = 110 +D (17662) httpd_parse: continue_parsing: skip pre-parsed data of size = 5 +D (17672) httpd_parse: continue_parsing: un-paused +D (17682) httpd_parse: cb_header_field: processing field = Upgrade +D (17682) httpd_parse: cb_header_value: processing value = websocket +D (17692) httpd_parse: cb_header_field: processing field = Connection +D (17702) httpd_parse: cb_header_value: processing value = Upgrade +D (17702) httpd_parse: cb_header_field: processing field = Sec-WebSocket-Key +D (17712) httpd_parse: cb_header_value: processing value = gfhjgfhjfj +D (17722) httpd_parse: cb_header_field: processing field = Sec-WebSocket-Proto +D (17722) httpd_parse: parse_block: parsed block size = 110 +D (17732) httpd_txrx: httpd_recv_with_opt: requested length = 128 +D (17742) httpd_txrx: httpd_recv_with_opt: received length = 40 +D (17742) httpd_parse: read_block: received HTTP request block size = 40 +D (17752) httpd_parse: cb_header_field: processing field = col +D (17752) httpd_parse: cb_header_value: processing value = echo +D (17762) httpd_parse: cb_header_field: processing field = Sec-WebSocket-Version +D (17772) httpd_parse: cb_header_value: processing value = 13 +D (17772) httpd_parse: cb_headers_complete: bytes read = 169 +D (17782) httpd_parse: cb_headers_complete: content length = 0 +D (17792) httpd_parse: cb_headers_complete: Got an upgrade request +D (17792) httpd_parse: pause_parsing: paused +D (17802) httpd_parse: cb_no_body: message complete +D (17802) httpd_parse: httpd_parse_req: parsing complete +D (17812) httpd_uri: httpd_uri: request for /ws with type 1 +D (17812) httpd_uri: httpd_find_uri_handler: [0] = /ws +D (17822) httpd_uri: httpd_uri: Responding WS handshake to sock 57 +D (17822) httpd_ws: httpd_ws_respond_server_handshake: Server key before encoding: gfhjgfhjfj258EAFA5-E914-47DA-95CA-C5AB0DC85B11 +D (17842) httpd_ws: httpd_ws_respond_server_handshake: Generated server key: Jg/fQVRsgwdDzYeG8yNBHRajUxw= +D (17852) httpd_sess: httpd_sess_process: httpd_req_delete +D (17852) httpd_sess: httpd_sess_process: success +D (17862) httpd: httpd_server: doing select maxfd+1 = 58 +D (17892) httpd: httpd_server: processing socket 57 +D (17892) httpd_sess: httpd_sess_process: httpd_req_new +D (17892) httpd_parse: httpd_req_new: New request, has WS? Yes, sd->ws_handler valid? Yes, sd->ws_close? No +D (17902) httpd_parse: httpd_req_new: New WS request from existing socket +D (17902) httpd_txrx: httpd_recv_with_opt: requested length = 1 +D (17912) httpd_txrx: httpd_recv_with_opt: received length = 1 +D (17912) httpd_ws: httpd_ws_get_frame_type: First byte received: 0x81 +D (17922) httpd_txrx: httpd_recv_with_opt: requested length = 1 +D (17932) httpd_txrx: httpd_recv_with_opt: received length = 1 +D (17932) httpd_txrx: httpd_recv_with_opt: requested length = 4 +D (17942) httpd_txrx: httpd_recv_with_opt: received length = 4 +D (17942) httpd_txrx: httpd_recv_with_opt: requested length = 13 +D (17952) httpd_txrx: httpd_recv_with_opt: received length = 13 +I (17962) ws_echo_server: Got packet with message: Trigger async +I (17962) ws_echo_server: Packet type: 1 +D (17972) httpd_sess: httpd_sess_process: httpd_req_delete +D (17972) httpd_sess: httpd_sess_process: success +D (17982) httpd: httpd_server: doing select maxfd+1 = 58 +D (17982) httpd: httpd_server: processing ctrl message +D (17992) httpd: httpd_process_ctrl_msg: work +D (18002) httpd: httpd_server: doing select maxfd+1 = 58 +``` + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt b/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt new file mode 100644 index 0000000000..addcda4538 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "ws_echo_server.c" + INCLUDE_DIRS ".") \ No newline at end of file diff --git a/examples/protocols/http_server/ws_echo_server/main/component.mk b/examples/protocols/http_server/ws_echo_server/main/component.mk new file mode 100644 index 0000000000..0b9d7585e7 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c new file mode 100644 index 0000000000..e10b0ba606 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c @@ -0,0 +1,177 @@ +/* WebSocket Echo Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include +#include +#include +#include "nvs_flash.h" +#include "esp_netif.h" +#include "esp_eth.h" +#include "protocol_examples_common.h" + +#include + +/* A simple example that demonstrates using websocket echo server + */ +static const char *TAG = "ws_echo_server"; + +/* + * Structure holding server handle + * and internal socket fd in order + * to use out of request send + */ +struct async_resp_arg { + httpd_handle_t hd; + int fd; +}; + +/* + * async send function, which we put into the httpd work queue + */ +static void ws_async_send(void *arg) +{ + static const char * data = "Async data"; + struct async_resp_arg *resp_arg = arg; + httpd_handle_t hd = resp_arg->hd; + int fd = resp_arg->fd; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = (uint8_t*)data; + ws_pkt.len = strlen(data); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + httpd_ws_send_frame_async(hd, fd, &ws_pkt); + free(resp_arg); +} + +static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) +{ + struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); + resp_arg->hd = req->handle; + resp_arg->fd = httpd_req_to_sockfd(req); + return httpd_queue_work(handle, ws_async_send, resp_arg); +} + +/* + * This handler echos back the received ws data + * and triggers an async send if certain message received + */ +static esp_err_t echo_handler(httpd_req_t *req) +{ + uint8_t buf[128] = { 0 }; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = buf; + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 128); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + return ret; + } + ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); + ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type); + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && + strcmp((char*)ws_pkt.payload,"Trigger async") == 0) { + return trigger_async_send(req->handle, req); + } + + ret = httpd_ws_send_frame(req, &ws_pkt); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret); + } + return ret; +} + +static const httpd_uri_t ws = { + .uri = "/ws", + .method = HTTP_GET, + .handler = echo_handler, + .user_ctx = NULL, + .is_websocket = true +}; + + +static httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) { + // Registering the ws handler + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &ws); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} + +static void stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + httpd_stop(server); +} + +static void disconnect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server) { + ESP_LOGI(TAG, "Stopping webserver"); + stop_webserver(*server); + *server = NULL; + } +} + +static void connect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server == NULL) { + ESP_LOGI(TAG, "Starting webserver"); + *server = start_webserver(); + } +} + + +void app_main(void) +{ + static httpd_handle_t server = NULL; + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); + + /* Register event handlers to stop the server when Wi-Fi or Ethernet is disconnected, + * and re-start it upon connection. + */ +#ifdef CONFIG_EXAMPLE_CONNECT_WIFI + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI +#ifdef CONFIG_EXAMPLE_CONNECT_ETHERNET + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_ETHERNET + + /* Start the server for the first time */ + server = start_webserver(); +} diff --git a/examples/protocols/http_server/ws_echo_server/sdkconfig.ci b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci new file mode 100644 index 0000000000..757c0adcca --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci @@ -0,0 +1 @@ +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y diff --git a/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults b/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults new file mode 100644 index 0000000000..7ffc474ed2 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_HTTPD_WS_SUPPORT=y diff --git a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py new file mode 100644 index 0000000000..f7d8ad26f7 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import re +from tiny_test_fw import Utility +import ttfw_idf +import os +import six +import socket +import hashlib +import base64 +import struct + + +OPCODE_TEXT = 0x1 +OPCODE_BIN = 0x2 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa + + +class WsClient: + def __init__(self, ip, port): + self.port = port + self.ip = ip + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_key = "abcdefghjk" + self.socket.settimeout(10.0) + + def __enter__(self): + self.socket.connect((self.ip, self.port)) + self._handshake() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.socket.close() + + def _handshake(self): + MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + client_key = self.client_key + MAGIC_STRING + expected_accept = base64.standard_b64encode(hashlib.sha1(client_key.encode()).digest()) + request = ('GET /ws HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: ' + 'Upgrade\r\nSec-WebSocket-Key: {}\r\n' + 'Sec-WebSocket-Version: 13\r\n\r\n'.format(self.client_key)) + self.socket.send(request.encode('utf-8')) + response = self.socket.recv(1024) + ws_accept = re.search(b'Sec-WebSocket-Accept: (.*)\r\n', response, re.IGNORECASE) + if ws_accept and ws_accept.group(1) is not None and ws_accept.group(1) == expected_accept: + pass + else: + raise("Unexpected Sec-WebSocket-Accept, handshake response: {}".format(response)) + + def _masked(self, data): + mask = struct.unpack('B' * 4, os.urandom(4)) + out = list(mask) + for i, d in enumerate(struct.unpack('B' * len(data), data)): + out.append(d ^ mask[i % 4]) + return struct.pack('B' * len(out), *out) + + def _ws_encode(self, data="", opcode=OPCODE_TEXT, mask=1): + data = data.encode('utf-8') + length = len(data) + if length >= 126: + raise("Packet length of {} not supported!".format(length)) + frame_header = chr(1 << 7 | opcode) + frame_header += chr(mask << 7 | length) + frame_header = six.b(frame_header) + if not mask: + return frame_header + data + return frame_header + self._masked(data) + + def read(self): + header = self.socket.recv(2) + if not six.PY3: + header = [ord(character) for character in header] + opcode = header[0] & 15 + length = header[1] & 127 + payload = self.socket.recv(length) + return opcode, payload.decode('utf-8') + + def write(self, data="", opcode=OPCODE_TEXT, mask=1): + return self.socket.sendall(self._ws_encode(data=data, opcode=opcode, mask=mask)) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_http_ws_echo_server(env, extra_data): + # Acquire DUT + dut1 = env.get_dut("http_server", "examples/protocols/http_server/ws_echo_server", dut_class=ttfw_idf.ESP32DUT) + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, "ws_echo_server.bin") + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("http_ws_server_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("http_ws_server_bin_size", bin_size // 1024, dut1.TARGET) + + # Upload binary and start testing + Utility.console_log("Starting ws-echo-server test app based on http_server") + dut1.start_app() + + # Parse IP address of STA + Utility.console_log("Waiting to connect with AP") + got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)IPv4 address: (\d+.\d+.\d+.\d+)"), timeout=60)[0] + got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=60)[0] + + Utility.console_log("Got IP : " + got_ip) + Utility.console_log("Got Port : " + got_port) + + # Start ws server test + with WsClient(got_ip, int(got_port)) as ws: + DATA = 'Espressif' + for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]: + ws.write(data=DATA, opcode=expected_opcode) + opcode, data = ws.read() + Utility.console_log("Testing opcode {}: Received opcode:{}, data:{}".format(expected_opcode, opcode, data)) + if expected_opcode == OPCODE_PING: + dut1.expect("Got a WS PING frame, Replying PONG") + if opcode != OPCODE_PONG or data != DATA: + raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data)) + continue + dut_data = dut1.expect(re.compile(r"Got packet with message: ([A-Za-z0-9_]*)"))[0] + dut_opcode = int(dut1.expect(re.compile(r"Packet type: ([0-9]*)"))[0]) + if opcode != expected_opcode or data != DATA or opcode != dut_opcode or data != dut_data: + raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data)) + ws.write(data="Trigger async", opcode=OPCODE_TEXT) + opcode, data = ws.read() + Utility.console_log("Testing async send: Received opcode:{}, data:{}".format(opcode, data)) + if opcode != OPCODE_TEXT or data != "Async data": + raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data)) + + +if __name__ == '__main__': + test_examples_protocol_http_ws_echo_server()