diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index b3937b918..2f8ed780c 100644 --- a/components/esp_http_server/Kconfig +++ b/components/esp_http_server/Kconfig @@ -41,7 +41,7 @@ menu "HTTP Server" config HTTPD_WS_SUPPORT bool "WebSocket server support" - default y + default n help This sets the WebSocket server support. diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 92cae494c..e1e5c288d 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -1514,6 +1514,23 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t ma */ 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 * @} diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index ec920dcba..30ce87d46 100644 --- a/components/esp_http_server/src/httpd_parse.c +++ b/components/esp_http_server/src/httpd_parse.c @@ -690,7 +690,9 @@ static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config) ra->first_chunk_sent = 0; ra->req_hdrs_count = 0; ra->resp_hdrs_count = 0; +#if CONFIG_HTTPD_WS_SUPPORT ra->ws_handshake_detect = false; +#endif memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr)); } @@ -703,11 +705,13 @@ static void httpd_req_cleanup(httpd_req_t *r) httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_ctx); } +#if CONFIG_HTTPD_WS_SUPPORT /* Close the socket when a WebSocket Close request is received */ if (ra->sd->ws_close) { ESP_LOGD(TAG, LOG_FMT("Try closing WS connection at FD: %d"), ra->sd->fd); httpd_sess_trigger_close(r->handle, ra->sd->fd); } +#endif /* Retrieve session info from the request into the socket database. */ ra->sd->ctx = r->sess_ctx; diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index 96182a223..c3d559ad2 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -279,7 +279,6 @@ esp_err_t httpd_uri(struct httpd_data *hd) { httpd_uri_t *uri = NULL; httpd_req_t *req = &hd->hd_req; - struct httpd_req_aux *aux = req->aux; struct http_parser_url *res = &hd->hd_req_aux.url_parse_res; /* For conveying URI not found/method not allowed */ @@ -313,6 +312,7 @@ esp_err_t httpd_uri(struct httpd_data *hd) /* 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); diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c index 56718ccb4..d9a1cc767 100644 --- a/components/esp_http_server/src/httpd_ws.c +++ b/components/esp_http_server/src/httpd_ws.c @@ -26,7 +26,7 @@ #ifdef CONFIG_HTTPD_WS_SUPPORT -#define TAG "httpd_ws" +static const char *TAG="httpd_ws"; /* * Bit masks for WebSocket frames. @@ -266,6 +266,11 @@ esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *frame) 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; @@ -299,15 +304,20 @@ esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *frame) /* 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 (httpd_send(req, (const char *)header_buf, tx_len) < 0) { + 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 (httpd_send(req, (const char *)frame->payload, frame->len) < 0) { + 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; } diff --git a/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c index 9809ec62f..e10b0ba60 100644 --- a/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c +++ b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c @@ -22,30 +22,79 @@ /* 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; +}; -static esp_err_t ws_ping_handler(httpd_req_t *req) +/* + * 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 = ret ? : httpd_ws_send_frame(req, &ws_pkt); + 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 = ws_ping_handler, + .handler = echo_handler, .user_ctx = NULL, .is_websocket = true }; @@ -59,7 +108,7 @@ static httpd_handle_t start_webserver(void) // Start the httpd server ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); if (httpd_start(&server, &config) == ESP_OK) { - // Set URI handlers + // Registering the ws handler ESP_LOGI(TAG, "Registering URI handlers"); httpd_register_uri_handler(server, &ws); return server; diff --git a/examples/protocols/http_server/ws_echo_server/sdkconfig.ci b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci index 2cc339d53..757c0adcc 100644 --- a/examples/protocols/http_server/ws_echo_server/sdkconfig.ci +++ b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci @@ -1,2 +1 @@ CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y - diff --git a/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults b/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults new file mode 100644 index 000000000..7ffc474ed --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_HTTPD_WS_SUPPORT=y diff --git a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py index 2d282d7a2..f7d8ad26f 100644 --- a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py +++ b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py @@ -17,7 +17,6 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from builtins import range import re from tiny_test_fw import Utility import ttfw_idf @@ -26,6 +25,7 @@ import six import socket import hashlib import base64 +import struct OPCODE_TEXT = 0x1 @@ -54,9 +54,9 @@ class WsClient: 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) + 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) @@ -66,14 +66,11 @@ class WsClient: raise("Unexpected Sec-WebSocket-Accept, handshake response: {}".format(response)) def _masked(self, data): - mask_key = os.urandom(4) - out = mask_key - for i in range(len(data)): - if six.PY3: - out += (data[i] ^ mask_key[i % 4]).to_bytes(1, byteorder="little") - else: - out += chr(ord(data[i]) ^ ord(mask_key[i % 4])) - return out + 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') @@ -109,7 +106,7 @@ def test_examples_protocol_http_ws_echo_server(env, extra_data): 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) + 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") @@ -117,19 +114,19 @@ def test_examples_protocol_http_ws_echo_server(env, extra_data): # 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=30)[0] - got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0] + 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, 80) as ws: + with WsClient(got_ip, int(got_port)) as ws: DATA = 'Espressif' for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]: - Utility.console_log("Testing opcode {}".format(expected_opcode)) 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: @@ -139,6 +136,11 @@ def test_examples_protocol_http_ws_echo_server(env, extra_data): 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__':