chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:04:25 +08:00
commit 548b49ebc0
20937 changed files with 5455372 additions and 0 deletions
@@ -0,0 +1,9 @@
# 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.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(wss_server)
@@ -0,0 +1,64 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- |
# HTTPS Websocket server
This example creates an HTTPS server with SSL/TLS support using ESP-TLS and employs a simple Websocket request handler. It demonstrates handling multiple clients from the server including:
* PING-PONG mechanism
* Sending asynchronous messages to all clients
For more information, refer to the [esp_https_server component documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_https_server.html).
### Websocket support in `http_server`
Please refer to the documentation of [Websocket server](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/protocols/esp_http_server.html#websocket-server) feature in the documentation,
or to the description of using websocket handlers in httpd in the [simple ws echo](../../http_server/ws_echo_server/README.md#how-to-use-example) example.
## How to use example
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`.
### Hardware Required
* A development board with ESP32/ESP32-S2/ESP32-C3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.)
* A USB cable for power supply and programming
### Configure the project
```
idf.py menuconfig
```
Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. 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
```
(Replace PORT with the name of the serial port to use.)
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
## Certificates
You will need to approve a security exception in your browser. This is because of a self signed
certificate; this will be always the case, unless you preload the CA root into your browser/system
as trusted.
You can generate a new certificate using the OpenSSL command line tool:
```
openssl req -newkey rsa:2048 -nodes -keyout prvtkey.pem -x509 -days 3650 -out servercert.pem -subj "/CN=ESP32 HTTPS server example" -addext "keyUsage=critical,digitalSignature,keyCertSign"
```
Expiry time and metadata fields can be adjusted in the invocation.
Please see the openssl man pages (man openssl-req) for more details.
It is **strongly recommended** to not reuse the example certificate in your application;
it is included only for demonstration.
@@ -0,0 +1,14 @@
set(requires esp_https_server nvs_flash esp_timer esp_netif)
idf_build_get_property(target IDF_TARGET)
if(${target} STREQUAL "linux")
list(APPEND requires esp_stubs protocol_examples_common)
else()
list(APPEND requires esp_wifi esp_eth)
endif()
idf_component_register(SRCS "wss_server_example.c" "keep_alive.c"
INCLUDE_DIRS "."
PRIV_REQUIRES ${requires}
EMBED_TXTFILES "certs/servercert.pem"
"certs/prvtkey.pem")
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH
JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw
h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT
aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al
3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg
0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB
vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui
f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9
Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y
JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX
49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc
+3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6
pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D
0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG
YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV
MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL
CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin
7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1
noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8
4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g
Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/
nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3
q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2
lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB
jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr
v/t+MeGJP/0Zw8v/X2CFll96
-----END PRIVATE KEY-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDOzCCAiOgAwIBAgIUGtx0JiogvT3DlTnZ3+tAT7Tr5JEwDQYJKoZIhvcNAQEL
BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMjUw
NDAyMDcwNzE1WhcNMzUwMzMxMDcwNzE1WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ
UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T
sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k
qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd
GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4
sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb
jAn4PCuR2akdF4G8WLUeDWECAwEAAaNjMGEwHQYDVR0OBBYEFMnmdJKOEepXrHI/
ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud
EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQAT
lpY3s1IAV369xl7cri72ErqFRBveKvJCaq/1l0FSH1w/u3SxABQw9SH29Lg0hbAa
jotcCMo4XZpmKSsQn0Zs4ZgKh1zk90JZVtssWuU3y8ftq9t4JjskRHTF19yp7CCC
zKIHPlCyYjCgw3tWhrsWa95dF3ebVrPkuUfd6CCxe8OB4EC74svI+uuxA7Ud9Jtx
Dno5yFu7NKpRLSwFqJnxbU0bZp434v2vcexkbvZP0Z2AW+C2L+x90xcP5rW2vL+a
S0bj0quNH43cGKbBFzE9cLy04xsGlOqzYSQcFbGeT9LyQBLFZtp6QlC97ZsQ78fG
A2PRPwcF0QCvQrT42b8y
-----END CERTIFICATE-----
@@ -0,0 +1,7 @@
dependencies:
protocol_examples_common:
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
esp_stubs:
path: ${IDF_PATH}/examples/protocols/linux_stubs/esp_stubs
rules:
- if: "target in [linux]"
@@ -0,0 +1,239 @@
/* Keep Alive engine for wss 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_log.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "keep_alive.h"
#if !CONFIG_IDF_TARGET_LINUX
#include <esp_system.h>
#include "esp_timer.h"
#else
#include <time.h>
#endif // !CONFIG_IDF_TARGET_LINUX
typedef enum {
NO_CLIENT = 0,
CLIENT_FD_ADD,
CLIENT_FD_REMOVE,
CLIENT_UPDATE,
CLIENT_ACTIVE,
STOP_TASK,
} client_fd_action_type_t;
typedef struct {
client_fd_action_type_t type;
int fd;
uint64_t last_seen;
} client_fd_action_t;
typedef struct wss_keep_alive_storage {
size_t max_clients;
wss_check_client_alive_cb_t check_client_alive_cb;
wss_check_client_alive_cb_t client_not_alive_cb;
size_t keep_alive_period_ms;
size_t not_alive_after_ms;
void * user_ctx;
QueueHandle_t q;
client_fd_action_t clients[];
} wss_keep_alive_storage_t;
typedef struct wss_keep_alive_storage* wss_keep_alive_t;
static const char *TAG = "wss_keep_alive";
static uint64_t _tick_get_ms(void)
{
#if CONFIG_IDF_TARGET_LINUX
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
#else
return esp_timer_get_time() / 1000;
#endif
}
// Goes over active clients to find out how long we could sleep before checking who's alive
static uint64_t get_max_delay(wss_keep_alive_t h)
{
int64_t check_after_ms = 30000; // max delay, no need to check anyone
for (int i=0; i<h->max_clients; ++i) {
if (h->clients[i].type == CLIENT_ACTIVE) {
uint64_t check_this_client_at = h->clients[i].last_seen + h->keep_alive_period_ms;
if (check_this_client_at < check_after_ms + _tick_get_ms()) {
check_after_ms = check_this_client_at - _tick_get_ms();
if (check_after_ms < 0) {
check_after_ms = 1000; // min delay, some client(s) not responding already
}
}
}
}
return check_after_ms;
}
static bool update_client(wss_keep_alive_t h, int sockfd, uint64_t timestamp)
{
for (int i=0; i<h->max_clients; ++i) {
if (h->clients[i].type == CLIENT_ACTIVE && h->clients[i].fd == sockfd) {
h->clients[i].last_seen = timestamp;
return true;
}
}
return false;
}
static bool remove_client(wss_keep_alive_t h, int sockfd)
{
for (int i=0; i<h->max_clients; ++i) {
if (h->clients[i].type == CLIENT_ACTIVE && h->clients[i].fd == sockfd) {
h->clients[i].type = NO_CLIENT;
h->clients[i].fd = -1;
return true;
}
}
return false;
}
static bool add_new_client(wss_keep_alive_t h,int sockfd)
{
for (int i=0; i<h->max_clients; ++i) {
if (h->clients[i].type == NO_CLIENT) {
h->clients[i].type = CLIENT_ACTIVE;
h->clients[i].fd = sockfd;
h->clients[i].last_seen = _tick_get_ms();
return true; // success
}
}
return false;
}
static void keep_alive_task(void* arg)
{
wss_keep_alive_storage_t *keep_alive_storage = arg;
bool run_task = true;
client_fd_action_t client_action;
while (run_task) {
if (xQueueReceive(keep_alive_storage->q, (void *) &client_action,
get_max_delay(keep_alive_storage) / portTICK_PERIOD_MS) == pdTRUE) {
switch (client_action.type) {
case CLIENT_FD_ADD:
if (!add_new_client(keep_alive_storage, client_action.fd)) {
ESP_LOGE(TAG, "Cannot add new client");
}
break;
case CLIENT_FD_REMOVE:
if (!remove_client(keep_alive_storage, client_action.fd)) {
ESP_LOGE(TAG, "Cannot remove client fd:%d", client_action.fd);
}
break;
case CLIENT_UPDATE:
if (!update_client(keep_alive_storage, client_action.fd, client_action.last_seen)) {
ESP_LOGE(TAG, "Cannot find client fd:%d", client_action.fd);
}
break;
case STOP_TASK:
run_task = false;
break;
default:
ESP_LOGE(TAG, "Unexpected client action");
break;
}
} else {
// timeout: check if PING message needed
for (int i=0; i<keep_alive_storage->max_clients; ++i) {
if (keep_alive_storage->clients[i].type == CLIENT_ACTIVE) {
if (keep_alive_storage->clients[i].last_seen + keep_alive_storage->keep_alive_period_ms <= _tick_get_ms()) {
ESP_LOGD(TAG, "Haven't seen the client (fd=%d) for a while", keep_alive_storage->clients[i].fd);
if (keep_alive_storage->clients[i].last_seen + keep_alive_storage->not_alive_after_ms <= _tick_get_ms()) {
ESP_LOGE(TAG, "Client (fd=%d) not alive!", keep_alive_storage->clients[i].fd);
keep_alive_storage->client_not_alive_cb(keep_alive_storage, keep_alive_storage->clients[i].fd);
} else {
keep_alive_storage->check_client_alive_cb(keep_alive_storage, keep_alive_storage->clients[i].fd);
}
}
}
}
}
}
vQueueDelete(keep_alive_storage->q);
free(keep_alive_storage);
vTaskDelete(NULL);
}
wss_keep_alive_t wss_keep_alive_start(wss_keep_alive_config_t *config)
{
size_t queue_size = config->max_clients/2;
size_t client_list_size = config->max_clients + queue_size;
wss_keep_alive_t keep_alive_storage = calloc(1,
sizeof(wss_keep_alive_storage_t) + client_list_size* sizeof(client_fd_action_t));
if (keep_alive_storage == NULL) {
return false;
}
keep_alive_storage->check_client_alive_cb = config->check_client_alive_cb;
keep_alive_storage->client_not_alive_cb = config->client_not_alive_cb;
keep_alive_storage->max_clients = config->max_clients;
keep_alive_storage->not_alive_after_ms = config->not_alive_after_ms;
keep_alive_storage->keep_alive_period_ms = config->keep_alive_period_ms;
keep_alive_storage->user_ctx = config->user_ctx;
keep_alive_storage->q = xQueueCreate(queue_size, sizeof(client_fd_action_t));
if (xTaskCreate(keep_alive_task, "keep_alive_task", config->task_stack_size,
keep_alive_storage, config->task_prio, NULL) != pdTRUE) {
wss_keep_alive_stop(keep_alive_storage);
return false;
}
return keep_alive_storage;
}
void wss_keep_alive_stop(wss_keep_alive_t h)
{
client_fd_action_t stop = { .type = STOP_TASK };
xQueueSendToBack(h->q, &stop, 0);
// internal structs will be de-allocated in the task
}
esp_err_t wss_keep_alive_add_client(wss_keep_alive_t h, int fd)
{
client_fd_action_t client_fd_action = { .fd = fd, .type = CLIENT_FD_ADD};
if (xQueueSendToBack(h->q, &client_fd_action, 0) == pdTRUE) {
return ESP_OK;
}
return ESP_FAIL;
}
esp_err_t wss_keep_alive_remove_client(wss_keep_alive_t h, int fd)
{
client_fd_action_t client_fd_action = { .fd = fd, .type = CLIENT_FD_REMOVE};
if (xQueueSendToBack(h->q, &client_fd_action, 0) == pdTRUE) {
return ESP_OK;
}
return ESP_FAIL;
}
esp_err_t wss_keep_alive_client_is_active(wss_keep_alive_t h, int fd)
{
client_fd_action_t client_fd_action = { .fd = fd, .type = CLIENT_UPDATE,
.last_seen = _tick_get_ms()};
if (xQueueSendToBack(h->q, &client_fd_action, 0) == pdTRUE) {
return ESP_OK;
}
return ESP_FAIL;
}
void wss_keep_alive_set_user_ctx(wss_keep_alive_t h, void *ctx)
{
h->user_ctx = ctx;
}
void* wss_keep_alive_get_user_ctx(wss_keep_alive_t h)
{
return h->user_ctx;
}
@@ -0,0 +1,96 @@
/* Keep Alive engine for wss 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.
*/
#pragma once
#define KEEP_ALIVE_CONFIG_DEFAULT() \
{ \
.max_clients = 10, \
.task_stack_size = 2048, \
.task_prio = tskIDLE_PRIORITY+1, \
.keep_alive_period_ms = 5000, \
.not_alive_after_ms = 10000, \
}
struct wss_keep_alive_storage;
typedef struct wss_keep_alive_storage* wss_keep_alive_t;
typedef bool (*wss_check_client_alive_cb_t)(wss_keep_alive_t h, int fd);
typedef bool (*wss_client_not_alive_cb_t)(wss_keep_alive_t h, int fd);
/**
* @brief Confiuration struct
*/
typedef struct {
size_t max_clients; /*!< max number of clients */
size_t task_stack_size; /*!< stack size of the created task */
size_t task_prio; /*!< priority of the created task */
size_t keep_alive_period_ms; /*!< check every client after this time */
size_t not_alive_after_ms; /*!< consider client not alive after this time */
wss_check_client_alive_cb_t check_client_alive_cb; /*!< callback function to check if client is alive */
wss_client_not_alive_cb_t client_not_alive_cb; /*!< callback function to notify that the client is not alive */
void *user_ctx; /*!< user context available in the keep-alive handle */
} wss_keep_alive_config_t;
/**
* @brief Adds a new client to internal set of clients to keep an eye on
*
* @param h keep-alive handle
* @param fd socket file descriptor for this client
* @return ESP_OK on success
*/
esp_err_t wss_keep_alive_add_client(wss_keep_alive_t h, int fd);
/**
* @brief Removes this client from the set
*
* @param h keep-alive handle
* @param fd socket file descriptor for this client
* @return ESP_OK on success
*/
esp_err_t wss_keep_alive_remove_client(wss_keep_alive_t h, int fd);
/**
* @brief Notify that this client is alive
*
* @param h keep-alive handle
* @param fd socket file descriptor for this client
* @return ESP_OK on success
*/
esp_err_t wss_keep_alive_client_is_active(wss_keep_alive_t h, int fd);
/**
* @brief Starts keep-alive engine
*
* @param config keep-alive configuration
* @return keep alive handle
*/
wss_keep_alive_t wss_keep_alive_start(wss_keep_alive_config_t *config);
/**
* @brief Stops keep-alive engine
*
* @param h keep-alive handle
*/
void wss_keep_alive_stop(wss_keep_alive_t h);
/**
* @brief Sets user defined context
*
* @param h keep-alive handle
* @param ctx user context
*/
void wss_keep_alive_set_user_ctx(wss_keep_alive_t h, void *ctx);
/**
* @brief Gets user defined context
*
* @param h keep-alive handle
* @return ctx user context
*/
void* wss_keep_alive_get_user_ctx(wss_keep_alive_t h);
@@ -0,0 +1,345 @@
/* Simple HTTP + SSL + WS 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_event.h>
#include <esp_log.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include "esp_netif.h"
#include "protocol_examples_common.h"
#if !CONFIG_IDF_TARGET_LINUX
#include <esp_system.h>
#include "esp_eth.h"
#include "esp_wifi.h"
#include "lwip/sockets.h"
#else
#include <unistd.h>
#endif // !CONFIG_IDF_TARGET_LINUX
#include <esp_https_server.h>
#include "esp_key_config.h"
#include "keep_alive.h"
#include "sdkconfig.h"
#if !CONFIG_HTTPD_WS_SUPPORT
#error This example cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration
#endif
struct async_resp_arg {
httpd_handle_t hd;
int fd;
};
static const char *TAG = "wss_echo_server";
static const size_t max_clients = 4;
static esp_err_t ws_handler(httpd_req_t *req)
{
if (req->method == HTTP_GET) {
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
return ESP_OK;
}
httpd_ws_frame_t ws_pkt;
uint8_t *buf = NULL;
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
// First receive the full ws message
/* Set max_len = 0 to get the frame len */
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret);
return ret;
}
ESP_LOGI(TAG, "frame len is %d", ws_pkt.len);
if (ws_pkt.len) {
/* ws_pkt.len + 1 is for NULL termination as we are expecting a string */
buf = calloc(1, ws_pkt.len + 1);
if (buf == NULL) {
ESP_LOGE(TAG, "Failed to calloc memory for buf");
return ESP_ERR_NO_MEM;
}
ws_pkt.payload = buf;
/* Set max_len = ws_pkt.len to get the frame payload */
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret);
free(buf);
return ret;
}
}
// If it was a PONG, update the keep-alive
if (ws_pkt.type == HTTPD_WS_TYPE_PONG) {
ESP_LOGD(TAG, "Received PONG message");
free(buf);
return wss_keep_alive_client_is_active(httpd_get_global_user_ctx(req->handle),
httpd_req_to_sockfd(req));
// If it was a TEXT message, just echo it back
} else if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_PING || ws_pkt.type == HTTPD_WS_TYPE_CLOSE) {
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) {
ESP_LOGI(TAG, "Received packet with message: %s", ws_pkt.payload);
} else if (ws_pkt.type == HTTPD_WS_TYPE_PING) {
// Response PONG packet to peer
ESP_LOGI(TAG, "Got a WS PING frame, Replying PONG");
ws_pkt.type = HTTPD_WS_TYPE_PONG;
} else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) {
// Response CLOSE packet with no payload to peer
ws_pkt.len = 0;
ws_pkt.payload = NULL;
}
ret = httpd_ws_send_frame(req, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
}
ESP_LOGI(TAG, "ws_handler: httpd_handle_t=%p, sockfd=%d, client_info:%d", req->handle,
httpd_req_to_sockfd(req), httpd_ws_get_fd_info(req->handle, httpd_req_to_sockfd(req)));
free(buf);
return ret;
}
free(buf);
return ESP_OK;
}
esp_err_t wss_open_fd(httpd_handle_t hd, int sockfd)
{
ESP_LOGI(TAG, "New client connected %d", sockfd);
wss_keep_alive_t h = httpd_get_global_user_ctx(hd);
return wss_keep_alive_add_client(h, sockfd);
}
void wss_close_fd(httpd_handle_t hd, int sockfd)
{
ESP_LOGI(TAG, "Client disconnected %d", sockfd);
wss_keep_alive_t h = httpd_get_global_user_ctx(hd);
wss_keep_alive_remove_client(h, sockfd);
close(sockfd);
}
static const httpd_uri_t ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = ws_handler,
.user_ctx = NULL,
.is_websocket = true,
.handle_ws_control_frames = true
};
static void send_hello(void *arg)
{
static const char * data = "Hello client";
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 void send_ping(void *arg)
{
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 = NULL;
ws_pkt.len = 0;
ws_pkt.type = HTTPD_WS_TYPE_PING;
httpd_ws_send_frame_async(hd, fd, &ws_pkt);
free(resp_arg);
}
bool client_not_alive_cb(wss_keep_alive_t h, int fd)
{
ESP_LOGE(TAG, "Client not alive, closing fd %d", fd);
httpd_sess_trigger_close(wss_keep_alive_get_user_ctx(h), fd);
return true;
}
bool check_client_alive_cb(wss_keep_alive_t h, int fd)
{
ESP_LOGD(TAG, "Checking if client (fd=%d) is alive", fd);
struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg));
assert(resp_arg != NULL);
resp_arg->hd = wss_keep_alive_get_user_ctx(h);
resp_arg->fd = fd;
if (httpd_queue_work(resp_arg->hd, send_ping, resp_arg) == ESP_OK) {
return true;
}
return false;
}
static httpd_handle_t start_wss_echo_server(void)
{
// Prepare keep-alive engine
wss_keep_alive_config_t keep_alive_config = KEEP_ALIVE_CONFIG_DEFAULT();
keep_alive_config.max_clients = max_clients;
keep_alive_config.client_not_alive_cb = client_not_alive_cb;
keep_alive_config.check_client_alive_cb = check_client_alive_cb;
wss_keep_alive_t keep_alive = wss_keep_alive_start(&keep_alive_config);
// Start the httpd server
httpd_handle_t server = NULL;
ESP_LOGI(TAG, "Starting server");
httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();
#if CONFIG_IDF_TARGET_LINUX
/* Use non-privileged port on Linux since port 443 requires root */
conf.port_secure = 8443;
#endif
conf.httpd.max_open_sockets = max_clients;
conf.httpd.global_user_ctx = keep_alive;
conf.httpd.open_fn = wss_open_fd;
conf.httpd.close_fn = wss_close_fd;
extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start");
extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end");
conf.servercert = servercert_start;
conf.servercert_len = servercert_end - servercert_start;
extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
extern const unsigned char prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
static esp_key_config_t server_key = {
.source = ESP_KEY_SOURCE_BUFFER,
.buffer = {
.data = prvtkey_pem_start,
}
};
server_key.buffer.len = prvtkey_pem_end - prvtkey_pem_start;
conf.server_key = &server_key;
esp_err_t ret = httpd_ssl_start(&server, &conf);
if (ESP_OK != ret) {
ESP_LOGI(TAG, "Error starting server!");
return NULL;
}
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &ws);
wss_keep_alive_set_user_ctx(keep_alive, server);
return server;
}
#if !CONFIG_IDF_TARGET_LINUX
static esp_err_t stop_wss_echo_server(httpd_handle_t server)
{
// Stop keep alive thread
wss_keep_alive_stop(httpd_get_global_user_ctx(server));
// Stop the httpd server
return httpd_ssl_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) {
if (stop_wss_echo_server(*server) == ESP_OK) {
*server = NULL;
} else {
ESP_LOGE(TAG, "Failed to stop https server");
}
}
}
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) {
*server = start_wss_echo_server();
}
}
#endif // !CONFIG_IDF_TARGET_LINUX
// Get all clients and send async message
static void wss_server_send_messages(httpd_handle_t* server)
{
bool send_messages = true;
// Send async message to all connected clients that use websocket protocol every 10 seconds
while (send_messages) {
vTaskDelay(10000 / portTICK_PERIOD_MS);
if (!*server) { // httpd might not have been created by now
continue;
}
size_t clients = max_clients;
int client_fds[max_clients];
if (httpd_get_client_list(*server, &clients, client_fds) == ESP_OK) {
for (size_t i=0; i < clients; ++i) {
int sock = client_fds[i];
if (httpd_ws_get_fd_info(*server, sock) == HTTPD_WS_CLIENT_WEBSOCKET) {
ESP_LOGI(TAG, "Active client (fd=%d) -> sending async message", sock);
struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg));
assert(resp_arg != NULL);
resp_arg->hd = *server;
resp_arg->fd = sock;
if (httpd_queue_work(resp_arg->hd, send_hello, resp_arg) != ESP_OK) {
ESP_LOGE(TAG, "httpd_queue_work failed!");
send_messages = false;
break;
}
}
}
} else {
ESP_LOGE(TAG, "httpd_get_client_list failed!");
return;
}
}
}
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());
/* Register event handlers to start server when Wi-Fi or Ethernet is connected,
* and stop server when disconnection happens.
*/
#if !CONFIG_IDF_TARGET_LINUX
#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
#endif // !CONFIG_IDF_TARGET_LINUX
/* 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());
#if CONFIG_IDF_TARGET_LINUX
/* On Linux, start the server directly since there are no WiFi/Ethernet events */
server = start_wss_echo_server();
#endif // CONFIG_IDF_TARGET_LINUX
/* This function demonstrates periodic sending Websocket messages
* to all connected clients to this server
*/
wss_server_send_messages(&server);
}
@@ -0,0 +1,188 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import threading
import time
from typing import Any
import pytest
import websocket
from common_test_methods import get_env_config_variable
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
OPCODE_TEXT = 0x1
OPCODE_BIN = 0x2
OPCODE_PING = 0x9
OPCODE_PONG = 0xA
CORRECT_ASYNC_DATA = 'Hello client'
class WsClient:
def __init__(self, ip, port, ca_file): # type: (str, int, str) -> None
self.port = port
self.ip = ip
sslopt = {'ca_certs': ca_file, 'check_hostname': False}
self.ws = websocket.WebSocket(sslopt=sslopt)
# Set timeout to 10 seconds to avoid connection failure at the time of handshake
self.ws.settimeout(10)
def __enter__(self): # type: ignore
self.ws.connect(f'wss://{self.ip}:{self.port}/ws')
return self
def __exit__(self, exc_type, exc_value, traceback): # type: ignore
self.ws.close()
def read(self) -> Any:
return self.ws.recv_data(control_frame=True)
def write(self, data: str, opcode: int = OPCODE_TEXT) -> Any:
if opcode == OPCODE_PING:
return self.ws.ping(data)
if opcode == OPCODE_PONG:
return self.ws.pong(data)
return self.ws.send(data)
class wss_client_thread(threading.Thread):
def __init__(self, ip, port, ca_file): # type: (str, int, str) -> None
threading.Thread.__init__(self)
self.ip = ip
self.port = port
self.ca_file = ca_file
self.start_time = time.time()
self.exc = None
self.data = 'Espressif'
self.async_response = False
def run(self) -> None:
with WsClient(self.ip, self.port, self.ca_file) as ws:
while True:
try:
opcode, data = ws.read()
data = data.decode('UTF-8')
if opcode == OPCODE_PING:
ws.write(data=self.data, opcode=OPCODE_PONG)
if opcode == OPCODE_TEXT:
if data == CORRECT_ASYNC_DATA:
self.async_response = True
logging.info(f'Thread {self.name} obtained correct async message')
# Keep sending pong to update the keepalive in the server
if (time.time() - self.start_time) > 20:
break
except Exception as e:
logging.info('Failed to connect to the client and read async data')
self.exc = e # type: ignore
if self.async_response is not True:
self.exc = RuntimeError('Failed to obtain correct async data') # type: ignore
def join(self, timeout: float | None = 0) -> None:
threading.Thread.join(self)
if self.exc:
raise self.exc
def test_multiple_client_keep_alive_and_async_response(ip, port, ca_file): # type: (str, int, str) -> None
threads = []
for _ in range(3):
try:
thread = wss_client_thread(ip, port, ca_file)
thread.start()
threads.append(thread)
except OSError:
logging.info('Error: unable to start thread')
# keep delay of 5 seconds between two connections to avoid handshake timeout
time.sleep(5)
for t in threads:
t.join()
@pytest.mark.wifi_router
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_examples_protocol_https_wss_server(dut: Dut) -> None:
# Get binary file
binary_file = os.path.join(dut.app.binary_path, 'wss_server.bin')
bin_size = os.path.getsize(binary_file)
logging.info(f'https_wss_server_bin_size : {bin_size // 1024}KB')
logging.info('Starting wss_server test app')
logging.info('Waiting to connect with AP')
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}')
# Parse IP address of STA
got_port = int(dut.expect(r'Server listening on port (\d+)', timeout=30)[1].decode())
got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=60)[1].decode()
logging.info(f'Got IP : {got_ip}')
logging.info(f'Got Port : {got_port}')
ca_file = os.path.join(os.path.dirname(__file__), 'main', 'certs', 'servercert.pem')
# Start ws server test
with WsClient(got_ip, int(got_port), ca_file) as ws:
# Check for echo
DATA = 'Espressif'
dut.expect('performing session handshake')
client_fd = int(dut.expect(r'New client connected (\d+)', timeout=30)[1].decode())
ws.write(data=DATA, opcode=OPCODE_TEXT)
dut.expect(rf'Received packet with message: {DATA}')
opcode, data = ws.read()
data = data.decode('UTF-8')
if data != DATA:
raise RuntimeError('Failed to receive the correct echo response.')
logging.info('Correct echo response obtained from the wss server')
# Test for PING
logging.info('Testing for send PING')
ws.write(data=DATA, opcode=OPCODE_PING)
dut.expect('Got a WS PING frame, Replying PONG')
opcode, data = ws.read()
data = data.decode('UTF-8')
if data != DATA or opcode != OPCODE_PONG:
raise RuntimeError('Failed to receive the PONG response')
logging.info('Passed the test for PING')
# Test for keepalive
logging.info('Testing for keep alive (approx time = 20s)')
start_time = time.time()
while True:
try:
opcode, data = ws.read()
if opcode == OPCODE_PING:
ws.write(data='Espressif', opcode=OPCODE_PONG)
logging.info('Received PING, replying PONG (to update the keepalive)')
# Keep sending pong to update the keepalive in the server
if (time.time() - start_time) > 20:
break
except Exception:
logging.info('Failed the test for keep alive,\nthe client got abruptly disconnected')
raise
# keepalive timeout is 10 seconds so do not respond for (10 + 1) seconds
logging.info(
'Testing if client is disconnected if it does not respond for 10s '
'i.e. keep_alive timeout (approx time = 11s)'
)
try:
dut.expect(f'Client not alive, closing fd {client_fd}', timeout=20)
dut.expect(f'Client disconnected {client_fd}')
except Exception:
logging.info('ENV_ERROR:Failed the test for keep alive,\nthe connection was not closed after timeout')
time.sleep(11)
logging.info('Passed the test for keep alive')
# Test keep alive and async response for multiple simultaneous client connections
logging.info('Testing for multiple simultaneous client connections (approx time = 30s)')
test_multiple_client_keep_alive_and_async_response(got_ip, int(got_port), ca_file)
logging.info('Passed the test for multiple simultaneous client connections')
@@ -0,0 +1 @@
CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y
@@ -0,0 +1,3 @@
CONFIG_ESP_HTTPS_SERVER_ENABLE=y
CONFIG_HTTPD_WS_SUPPORT=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y