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:
commit
1d3dbb239a
18 changed files with 1110 additions and 8 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
384
components/esp_http_server/src/httpd_ws.c
Normal file
384
components/esp_http_server/src/httpd_ws.c
Normal 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 */
|
|
@ -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
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
10
examples/protocols/http_server/ws_echo_server/CMakeLists.txt
Normal file
10
examples/protocols/http_server/ws_echo_server/CMakeLists.txt
Normal 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)
|
11
examples/protocols/http_server/ws_echo_server/Makefile
Normal file
11
examples/protocols/http_server/ws_echo_server/Makefile
Normal 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
|
||||||
|
|
121
examples/protocols/http_server/ws_echo_server/README.md
Normal file
121
examples/protocols/http_server/ws_echo_server/README.md
Normal 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.
|
|
@ -0,0 +1,2 @@
|
||||||
|
idf_component_register(SRCS "ws_echo_server.c"
|
||||||
|
INCLUDE_DIRS ".")
|
|
@ -0,0 +1,5 @@
|
||||||
|
#
|
||||||
|
# "main" pseudo-component makefile.
|
||||||
|
#
|
||||||
|
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
|
|
@ -0,0 +1 @@
|
||||||
|
CONFIG_HTTPD_WS_SUPPORT=y
|
|
@ -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()
|
Loading…
Reference in a new issue