mirror of
https://github.com/espressif/esp-idf.git
synced 2025-09-02 06:38:47 +00:00
Merge branch 'feat/support_authentication_feature_for_ws' into 'master'
Added pre handshake callback for websocket Closes IDF-13605 See merge request espressif/esp-idf!40706
This commit is contained in:
@@ -63,4 +63,13 @@ menu "HTTP Server"
|
|||||||
help
|
help
|
||||||
This config option helps in setting the time in millisecond to wait for event to be posted to the
|
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.
|
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
|
endmenu
|
||||||
|
@@ -458,6 +458,14 @@ typedef struct httpd_uri {
|
|||||||
* Pointer to subprotocol supported by URI
|
* Pointer to subprotocol supported by URI
|
||||||
*/
|
*/
|
||||||
const char *supported_subprotocol;
|
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
|
#endif
|
||||||
} httpd_uri_t;
|
} httpd_uri_t;
|
||||||
|
|
||||||
|
@@ -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]->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_PRE_HANDSHAKE_CB_SUPPORT
|
||||||
|
hd->hd_calls[i]->ws_pre_handshake_cb = uri_handler->ws_pre_handshake_cb;
|
||||||
|
#endif
|
||||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||||
hd->hd_calls[i]->is_websocket = uri_handler->is_websocket;
|
hd->hd_calls[i]->is_websocket = uri_handler->is_websocket;
|
||||||
hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames;
|
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
|
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||||
struct httpd_req_aux *aux = req->aux;
|
struct httpd_req_aux *aux = req->aux;
|
||||||
if (uri->is_websocket && aux->ws_handshake_detect && uri->method == HTTP_GET) {
|
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_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);
|
esp_err_t ret = httpd_ws_respond_server_handshake(&hd->hd_req, uri->supported_subprotocol);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
|
@@ -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.
|
: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
|
Event Handling
|
||||||
--------------
|
--------------
|
||||||
|
@@ -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
|
@@ -67,6 +67,26 @@ static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req)
|
|||||||
return ret;
|
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
|
* This handler echos back the received ws data
|
||||||
* and triggers an async send if certain message received
|
* 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);
|
ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type);
|
||||||
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT &&
|
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT &&
|
||||||
|
ws_pkt.payload != NULL &&
|
||||||
strcmp((char*)ws_pkt.payload,"Trigger async") == 0) {
|
strcmp((char*)ws_pkt.payload,"Trigger async") == 0) {
|
||||||
free(buf);
|
free(buf);
|
||||||
return trigger_async_send(req->handle, req);
|
return trigger_async_send(req->handle, req);
|
||||||
@@ -128,6 +149,17 @@ static const httpd_uri_t ws = {
|
|||||||
.is_websocket = true
|
.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)
|
static httpd_handle_t start_webserver(void)
|
||||||
{
|
{
|
||||||
@@ -140,6 +172,7 @@ static httpd_handle_t start_webserver(void)
|
|||||||
// Registering the ws handler
|
// Registering the ws handler
|
||||||
ESP_LOGI(TAG, "Registering URI handlers");
|
ESP_LOGI(TAG, "Registering URI handlers");
|
||||||
httpd_register_uri_handler(server, &ws);
|
httpd_register_uri_handler(server, &ws);
|
||||||
|
httpd_register_uri_handler(server, &ws_auth);
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,13 +23,14 @@ OPCODE_PONG = 0xA
|
|||||||
|
|
||||||
|
|
||||||
class WsClient:
|
class WsClient:
|
||||||
def __init__(self, ip: str, port: int) -> None:
|
def __init__(self, ip: str, port: int, uri: str = '') -> None:
|
||||||
self.port = port
|
self.port = port
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.ws = websocket.WebSocket()
|
self.ws = websocket.WebSocket()
|
||||||
|
self.uri = uri
|
||||||
|
|
||||||
def __enter__(self): # type: ignore
|
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
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback): # type: ignore
|
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))
|
logging.info('Got Port : {}'.format(got_port))
|
||||||
|
|
||||||
# Start ws server test
|
# 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'
|
DATA = 'Espressif'
|
||||||
for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]:
|
for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]:
|
||||||
ws.write(data=DATA, opcode=expected_opcode)
|
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()
|
data = data.decode()
|
||||||
if opcode != OPCODE_TEXT or data != 'Async data':
|
if opcode != OPCODE_TEXT or data != 'Async data':
|
||||||
raise RuntimeError('Failed to receive correct opcode:{} or data:{}'.format(opcode, 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')
|
||||||
|
Reference in New Issue
Block a user