Merge branch 'feature/ws_server' into 'master'

http_server: adds WebSocket support

Closes IDFGH-2151 and IDFGH-2752

See merge request espressif/esp-idf!7893
This commit is contained in:
David Čermák 2020-03-19 17:20:56 +08:00
commit 1d3dbb239a
18 changed files with 1110 additions and 8 deletions

View file

@ -3,8 +3,9 @@ idf_component_register(SRCS "src/httpd_main.c"
"src/httpd_sess.c" "src/httpd_sess.c"
"src/httpd_txrx.c" "src/httpd_txrx.c"
"src/httpd_uri.c" "src/httpd_uri.c"
"src/httpd_ws.c"
"src/util/ctrl_sock.c" "src/util/ctrl_sock.c"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src/port/esp32" "src/util" PRIV_INCLUDE_DIRS "src/port/esp32" "src/util"
REQUIRES nghttp # for http_parser.h REQUIRES nghttp # for http_parser.h
PRIV_REQUIRES lwip esp_timer) PRIV_REQUIRES lwip mbedtls esp_timer)

View file

@ -39,4 +39,10 @@ menu "HTTP Server"
Enabling this will log discarded binary HTTP request data at Debug level. 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. 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 endmenu

View file

@ -399,6 +399,14 @@ typedef struct httpd_uri {
* Pointer to user context data which will be available to handler * Pointer to user context data which will be available to handler
*/ */
void *user_ctx; 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; } 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 #ifdef __cplusplus
} }
#endif #endif

View file

@ -71,6 +71,11 @@ struct sock_db {
uint64_t lru_counter; /*!< LRU Counter indicating when the socket was last used */ 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 */ char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */
size_t pending_len; /*!< Length of 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; const char *value;
} *resp_hdrs; /*!< Additional headers in response packet */ } *resp_hdrs; /*!< Additional headers in response packet */
struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */ 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 #ifdef __cplusplus
} }
#endif #endif

View file

@ -374,15 +374,38 @@ 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("bytes read = %d"), parser->nread);
ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len); ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len);
/* Handle upgrade requests - only WebSocket is supported for now */
if (parser->upgrade) { if (parser->upgrade) {
ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported")); #ifdef CONFIG_HTTPD_WS_SUPPORT
/* There is no specific HTTP error code to notify the client that ESP_LOGD(TAG, LOG_FMT("Got an upgrade request"));
* upgrade is not supported, thus sending 400 Bad 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->error = HTTPD_400_BAD_REQUEST;
parser_data->status = PARSING_FAILED; parser_data->status = PARSING_FAILED;
return ESP_FAIL; 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; parser_data->status = PARSING_BODY;
ra->remaining_len = r->content_len; ra->remaining_len = r->content_len;
return ESP_OK; return ESP_OK;
@ -667,6 +690,9 @@ static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config)
ra->first_chunk_sent = 0; ra->first_chunk_sent = 0;
ra->req_hdrs_count = 0; ra->req_hdrs_count = 0;
ra->resp_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)); 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)) { if ((r->ignore_sess_ctx_changes == false) && (ra->sd->ctx != r->sess_ctx)) {
httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_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. */ /* Retrieve session info from the request into the socket database. */
ra->sd->ctx = r->sess_ctx; ra->sd->ctx = r->sess_ctx;
ra->sd->free_ctx = r->free_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); init_req_aux(&hd->hd_req_aux, &hd->config);
r->handle = hd; r->handle = hd;
r->aux = &hd->hd_req_aux; r->aux = &hd->hd_req_aux;
/* Associate the request to the socket */ /* Associate the request to the socket */
struct httpd_req_aux *ra = r->aux; struct httpd_req_aux *ra = r->aux;
ra->sd = sd; ra->sd = sd;
/* Set defaults */ /* Set defaults */
ra->status = (char *)HTTPD_200; ra->status = (char *)HTTPD_200;
ra->content_type = (char *)HTTPD_TYPE_TEXT; ra->content_type = (char *)HTTPD_TYPE_TEXT;
ra->first_chunk_sent = false; ra->first_chunk_sent = false;
/* Copy session info to the request */ /* Copy session info to the request */
r->sess_ctx = sd->ctx; r->sess_ctx = sd->ctx;
r->free_ctx = sd->free_ctx; r->free_ctx = sd->free_ctx;
r->ignore_sess_ctx_changes = sd->ignore_sess_ctx_changes; r->ignore_sess_ctx_changes = sd->ignore_sess_ctx_changes;
/* Parse request */
esp_err_t err = httpd_parse_req(hd); esp_err_t ret;
if (err != ESP_OK) {
#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); httpd_req_cleanup(r);
} }
return err; return ret;
}
#endif
/* Parse request */
ret = httpd_parse_req(hd);
if (ret != ESP_OK) {
httpd_req_cleanup(r);
}
return ret;
} }
/* Function that resets the http request data /* Function that resets the http request data

View file

@ -281,7 +281,9 @@ bool httpd_sess_pending(struct httpd_data *hd, int fd)
if (sd->pending_fn) { 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) // 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 // 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); return (sd->pending_len != 0);

View file

@ -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]->method = uri_handler->method;
hd->hd_calls[i]->handler = uri_handler->handler; hd->hd_calls[i]->handler = uri_handler->handler;
hd->hd_calls[i]->user_ctx = uri_handler->user_ctx; 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); ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri);
return ESP_OK; 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 */ /* Attach user context data (passed during URI registration) into request */
req->user_ctx = uri->user_ctx; 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 */ /* Invoke handler */
if (uri->handler(req) != ESP_OK) { if (uri->handler(req) != ESP_OK) {
/* Handler returns error, this socket should be closed */ /* Handler returns error, this socket should be closed */

View file

@ -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 <stdlib.h>
#include <sys/random.h>
#include <esp_log.h>
#include <esp_err.h>
#include <mbedtls/sha1.h>
#include <mbedtls/base64.h>
#include <esp_http_server.h>
#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 */

View file

@ -152,6 +152,13 @@ Persistent Connections Example
Check the example under :example:`protocols/http_server/persistent_sockets`. 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 API Reference
------------- -------------

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -0,0 +1,2 @@
idf_component_register(SRCS "ws_echo_server.c"
INCLUDE_DIRS ".")

View file

@ -0,0 +1,5 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View file

@ -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 <esp_wifi.h>
#include <esp_event.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_eth.h"
#include "protocol_examples_common.h"
#include <esp_http_server.h>
/* 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();
}

View file

@ -0,0 +1 @@
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y

View file

@ -0,0 +1 @@
CONFIG_HTTPD_WS_SUPPORT=y

View file

@ -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()