diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index ce1b71c57b..5078d70da6 100644 --- a/components/esp_http_server/Kconfig +++ b/components/esp_http_server/Kconfig @@ -63,4 +63,13 @@ menu "HTTP Server" help This config option helps in setting the time in millisecond to wait for event to be posted to the system default event loop. Set it to -1 if you need to set timeout to portMAX_DELAY. + + config HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT + bool "WebSocket pre-handshake callback support" + default n + depends on HTTPD_WS_SUPPORT + help + Enable this option to use WebSocket pre-handshake callback. This will allow the server to register + a callback function that will be called before the WebSocket handshake is processed i.e. before switching + to the WebSocket protocol. endmenu diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 6bd69f4b5c..a531f4ccb8 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -458,6 +458,14 @@ typedef struct httpd_uri { * Pointer to subprotocol supported by URI */ const char *supported_subprotocol; + +#if CONFIG_HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT || __DOXYGEN__ + /** + * Pointer to WebSocket pre-handshake callback. This will be called before the WebSocket handshake is processed, + * i.e. before the server responds with the WebSocket handshake response or before switching to the WebSocket handler. + */ + esp_err_t (*ws_pre_handshake_cb)(httpd_req_t *req); +#endif #endif } httpd_uri_t; diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index 64befe83e0..e16b7756e4 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -166,6 +166,9 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle, hd->hd_calls[i]->method = uri_handler->method; hd->hd_calls[i]->handler = uri_handler->handler; hd->hd_calls[i]->user_ctx = uri_handler->user_ctx; +#ifdef CONFIG_HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT + hd->hd_calls[i]->ws_pre_handshake_cb = uri_handler->ws_pre_handshake_cb; +#endif #ifdef CONFIG_HTTPD_WS_SUPPORT hd->hd_calls[i]->is_websocket = uri_handler->is_websocket; hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames; @@ -320,6 +323,13 @@ esp_err_t httpd_uri(struct httpd_data *hd) #ifdef CONFIG_HTTPD_WS_SUPPORT struct httpd_req_aux *aux = req->aux; if (uri->is_websocket && aux->ws_handshake_detect && uri->method == HTTP_GET) { +#ifdef CONFIG_HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT + if (uri->ws_pre_handshake_cb && uri->ws_pre_handshake_cb(req) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("ws_pre_handshake_cb failed")); + return ESP_FAIL; + } +#endif + 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, uri->supported_subprotocol); if (ret != ESP_OK) { diff --git a/docs/en/api-reference/protocols/esp_http_server.rst b/docs/en/api-reference/protocols/esp_http_server.rst index 88078305da..08bc3177f4 100644 --- a/docs/en/api-reference/protocols/esp_http_server.rst +++ b/docs/en/api-reference/protocols/esp_http_server.rst @@ -68,6 +68,37 @@ The HTTP server component provides WebSocket support. The WebSocket feature can :example:`protocols/http_server/ws_echo_server` demonstrates how to create a WebSocket echo server using the HTTP server, which starts on a local network and requires a WebSocket client for interaction, echoing back received WebSocket frames. +WebSocket Pre-Handshake Callback +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The HTTP server component provides a pre-handshake callback for WebSocket endpoints. This callback is invoked before the WebSocket handshake is processed — at this point, the connection is still an HTTP connection and has not yet been upgraded to WebSocket. + +The pre-handshake callback can be used for authentication, authorization, or other checks. If the callback returns :c:macro:`ESP_OK`, the WebSocket handshake will proceed. If the callback returns any other value, the handshake will be aborted and the connection will be closed. + +To use the WebSocket pre-handshake callback, you must enable :ref:`CONFIG_HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT` in your project configuration. + +.. code-block:: c + + static esp_err_t ws_auth_handler(httpd_req_t *req) + { + // Your authentication logic here + // return ESP_OK to allow the handshake, or another value to reject. + return ESP_OK; + } + + // Registering a WebSocket URI handler with pre-handshake authentication + static const httpd_uri_t ws = { + .uri = "/ws", + .method = HTTP_GET, + .handler = handler, // Your WebSocket data handler + .user_ctx = NULL, + .is_websocket = true, + .ws_pre_handshake_cb = ws_auth_handler // Set the pre-handshake callback + }; + + // Register the handler after starting the server: + httpd_register_uri_handler(server, &ws); + Event Handling -------------- diff --git a/examples/protocols/http_server/ws_echo_server/main/Kconfig.projbuild b/examples/protocols/http_server/ws_echo_server/main/Kconfig.projbuild new file mode 100644 index 0000000000..2883b67ed2 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/Kconfig.projbuild @@ -0,0 +1,12 @@ +menu "Example Configuration" + + config EXAMPLE_ENABLE_WS_PRE_HANDSHAKE_CB + bool "Enable WebSocket pre-handshake callback" + select HTTPD_WS_PRE_HANDSHAKE_CB_SUPPORT + default y + help + Enable this option to use WebSocket pre-handshake callback. + This will allow the server to register a callback function that will be + called before the WebSocket handshake is processed. + +endmenu 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 a9823a4a30..f79cfcceed 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 @@ -67,6 +67,26 @@ static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) return ret; } +#ifdef CONFIG_EXAMPLE_ENABLE_WS_PRE_HANDSHAKE_CB +static esp_err_t ws_pre_handshake_cb(httpd_req_t *req) +{ + ESP_LOGI(TAG, "=== ws_pre_handshake_cb called ==="); + + // Get the URI with query string + const char *uri = req->uri; + ESP_LOGI(TAG, "Requested URI: %s", uri ? uri : "NULL"); + + // Check if the query string contains token=valid + if (uri && strstr(uri, "token=valid") != NULL) { + ESP_LOGI(TAG, "Valid token found, accepting handshake"); + return ESP_OK; + } else { + ESP_LOGI(TAG, "No valid token found, rejecting handshake"); + return ESP_FAIL; + } +} +#endif + /* * This handler echos back the received ws data * and triggers an async send if certain message received @@ -107,6 +127,7 @@ static esp_err_t echo_handler(httpd_req_t *req) } ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type); if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && + ws_pkt.payload != NULL && strcmp((char*)ws_pkt.payload,"Trigger async") == 0) { free(buf); return trigger_async_send(req->handle, req); @@ -128,6 +149,17 @@ static const httpd_uri_t ws = { .is_websocket = true }; +static const httpd_uri_t ws_auth = { + .uri = "/auth", + .method = HTTP_GET, + .handler = echo_handler, + .user_ctx = NULL, + .is_websocket = true, +#ifdef CONFIG_EXAMPLE_ENABLE_WS_PRE_HANDSHAKE_CB + .ws_pre_handshake_cb = ws_pre_handshake_cb +#endif +}; + static httpd_handle_t start_webserver(void) { @@ -140,6 +172,7 @@ static httpd_handle_t start_webserver(void) // Registering the ws handler ESP_LOGI(TAG, "Registering URI handlers"); httpd_register_uri_handler(server, &ws); + httpd_register_uri_handler(server, &ws_auth); return server; } diff --git a/examples/protocols/http_server/ws_echo_server/pytest_ws_server_example.py b/examples/protocols/http_server/ws_echo_server/pytest_ws_server_example.py index 4a3b4c1443..000d61987b 100644 --- a/examples/protocols/http_server/ws_echo_server/pytest_ws_server_example.py +++ b/examples/protocols/http_server/ws_echo_server/pytest_ws_server_example.py @@ -23,13 +23,14 @@ OPCODE_PONG = 0xA class WsClient: - def __init__(self, ip: str, port: int) -> None: + def __init__(self, ip: str, port: int, uri: str = '') -> None: self.port = port self.ip = ip self.ws = websocket.WebSocket() + self.uri = uri def __enter__(self): # type: ignore - self.ws.connect('ws://{}:{}/ws'.format(self.ip, self.port)) + self.ws.connect('ws://{}:{}/{}'.format(self.ip, self.port, self.uri)) return self def __exit__(self, exc_type, exc_value, traceback): # type: ignore @@ -71,7 +72,7 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None: logging.info('Got Port : {}'.format(got_port)) # Start ws server test - with WsClient(got_ip, int(got_port)) as ws: + with WsClient(got_ip, int(got_port), uri='ws') as ws: DATA = 'Espressif' for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]: ws.write(data=DATA, opcode=expected_opcode) @@ -94,3 +95,35 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None: data = data.decode() if opcode != OPCODE_TEXT or data != 'Async data': raise RuntimeError('Failed to receive correct opcode:{} or data:{}'.format(opcode, data)) + + +@pytest.mark.wifi_router +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_ws_auth_handshake(dut: Dut) -> None: + """ + Test that connecting to /ws does NOT print the handshake success log. + This is used to verify ws_pre_handshake_cb can reject the handshake. + """ + # Wait for device to connect and start server + if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True: + dut.expect('Please input ssid password:') + env_name = 'wifi_router' + ap_ssid = get_env_config_variable(env_name, 'ap_ssid') + ap_password = get_env_config_variable(env_name, 'ap_password') + dut.write(f'{ap_ssid} {ap_password}') + got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode() + got_port = dut.expect(r"Starting server on port: '(\d+)'", timeout=30)[1].decode() + # Prepare a minimal WebSocket handshake request + # Use WSClient to attempt the handshake, expecting it to fail (handshake rejected) + + handshake_success = False + try: + # Attempt to use WSClient, expecting it to fail handshake + with WsClient(got_ip, int(got_port), uri='auth?token=valid') as ws: # type: ignore # noqa: F841 + handshake_success = True + except Exception as e: + logging.info(f'WebSocket handshake failed: {e}') + handshake_success = False + + if handshake_success is False: + raise RuntimeError('WebSocket handshake succeeded, but it should have been rejected by ws_pre_handshake_cb')