Merge branch 'bugfix/httpd_ws_sock_type' into 'master'

https_server: Added WSS server example and some http(s)+ws updates

Closes IDFGH-3822, IDFGH-3668, IDFGH-3766, and IDFGH-3444

See merge request espressif/esp-idf!10262
This commit is contained in:
David Čermák 2020-09-09 20:57:11 +08:00
commit e6f0087448
29 changed files with 841 additions and 70 deletions

View File

@ -406,6 +406,12 @@ typedef struct httpd_uri {
* If this flag is true, then method must be HTTP_GET. Otherwise the handshake will not be handled.
*/
bool is_websocket;
/**
* Flag indicating that control frames (PING, PONG, CLOSE) are also passed to the handler
* This is used if a custom processing of the control frames is needed
*/
bool handle_ws_control_frames;
#endif
} httpd_uri_t;
@ -1466,6 +1472,20 @@ esp_err_t httpd_sess_trigger_close(httpd_handle_t handle, int sockfd);
*/
esp_err_t httpd_sess_update_lru_counter(httpd_handle_t handle, int sockfd);
/**
* @brief Returns list of current socket descriptors of active sessions
*
* @param[in] handle Handle to server returned by httpd_start
* @param[in,out] fds In: Number of fds allocated in the supplied structure client_fds
* Out: Number of valid client fds returned in client_fds,
* @param[out] client_fds Array of client fds
*
* @return
* - ESP_OK : Successfully retrieved session list
* - ESP_ERR_INVALID_ARG : Wrong arguments or list is longer than allocated
*/
esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t *fds, int *client_fds);
/** End of Session
* @}
*/
@ -1526,6 +1546,15 @@ typedef enum {
HTTPD_WS_TYPE_PONG = 0xA
} httpd_ws_type_t;
/**
* @brief Enum for client info description
*/
typedef enum {
HTTPD_WS_CLIENT_INVALID = 0x0,
HTTPD_WS_CLIENT_HTTP = 0x1,
HTTPD_WS_CLIENT_WEBSOCKET = 0x2,
} httpd_ws_client_info_t;
/**
* @brief WebSocket frame format
*/
@ -1586,6 +1615,19 @@ esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *pkt);
*/
esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame);
/**
* @brief Checks the supplied socket descriptor if it belongs to any active client
* of this server instance and if the websoket protocol is active
*
* @param[in] hd Server instance data
* @param[in] fd Socket descriptor
* @return
* - HTTPD_WS_CLIENT_INVALID : This fd is not a client of this httpd
* - HTTPD_WS_CLIENT_HTTP : This fd is an active client, protocol is not WS
* - HTTPD_WS_CLIENT_WEBSOCKET : This fd is an active client, protocol is WS
*/
httpd_ws_client_info_t httpd_ws_get_fd_info(httpd_handle_t hd, int fd);
#endif /* CONFIG_HTTPD_WS_SUPPORT */
/** End of WebSocket related stuff
* @}

View File

@ -75,6 +75,7 @@ struct sock_db {
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 */
bool ws_control_frames; /*!< WebSocket flag indicating that control frames should be passed to user handlers */
#endif
};

View File

@ -104,6 +104,26 @@ esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *ar
return ESP_OK;
}
esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t *fds, int *client_fds)
{
struct httpd_data *hd = (struct httpd_data *) handle;
if (hd == NULL || fds == NULL || *fds == 0 || client_fds == NULL || *fds < hd->config.max_open_sockets) {
return ESP_ERR_INVALID_ARG;
}
size_t max_fds = *fds;
*fds = 0;
for (int i = 0; i < hd->config.max_open_sockets; ++i) {
if (hd->hd_sd[i].fd != -1) {
if (*fds < max_fds) {
client_fds[(*fds)++] = hd->hd_sd[i].fd;
} else {
return ESP_ERR_INVALID_ARG;
}
}
}
return ESP_OK;
}
void *httpd_get_global_user_ctx(httpd_handle_t handle)
{
return ((struct httpd_data *)handle)->config.global_user_ctx;

View File

@ -758,8 +758,8 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
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);
ESP_LOGD(TAG, LOG_FMT("New WS request from existing socket, ws_type=%d"), ra->ws_type);
/* Stop and return here immediately if it's a CLOSE frame */
if (ra->ws_type == HTTPD_WS_TYPE_CLOSE) {
@ -767,13 +767,14 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
return ret;
}
/* Ignore PONG frame, as this is a server */
if (ra->ws_type == HTTPD_WS_TYPE_PONG) {
return ret;
/* Pass the PONG frames to the handler as well, as user app might send PINGs */
ESP_LOGD(TAG, LOG_FMT("Received PONG frame"));
}
/* Call handler if it's a non-control frame */
if (ret == ESP_OK && ra->ws_type < HTTPD_WS_TYPE_CLOSE) {
/* Call handler if it's a non-control frame (or if handler requests control frames, as well) */
if (ret == ESP_OK &&
(ra->ws_type < HTTPD_WS_TYPE_CLOSE || sd->ws_control_frames)) {
ret = sd->ws_handler(r);
}

View File

@ -174,6 +174,7 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
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;
hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames;
#endif
ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri);
return ESP_OK;
@ -322,6 +323,7 @@ esp_err_t httpd_uri(struct httpd_data *hd)
aux->sd->ws_handshake_done = true;
aux->sd->ws_handler = uri->handler;
aux->sd->ws_control_frames = uri->handle_ws_control_frames;
/* Return immediately after handshake, no need to call handler here */
return ESP_OK;

View File

@ -383,4 +383,15 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
return ESP_OK;
}
httpd_ws_client_info_t httpd_ws_get_fd_info(httpd_handle_t hd, int fd)
{
struct sock_db *sess = httpd_sess_get(hd, fd);
if (sess == NULL) {
return HTTPD_WS_CLIENT_INVALID;
}
bool is_active_ws = sess->ws_handshake_done && (!sess->ws_close);
return is_active_ws ? HTTPD_WS_CLIENT_WEBSOCKET : HTTPD_WS_CLIENT_HTTP;
}
#endif /* CONFIG_HTTPD_WS_SUPPORT */

View File

@ -20,6 +20,11 @@
const static char *TAG = "esp_https_server";
typedef struct httpd_ssl_ctx {
esp_tls_cfg_server_t *tls_cfg;
httpd_open_func_t open_fn;
} httpd_ssl_ctx_t;
/**
* SSL socket close handler
*
@ -93,7 +98,7 @@ static esp_err_t httpd_ssl_open(httpd_handle_t server, int sockfd)
assert(server != NULL);
// Retrieve the SSL context from the global context field (set in config)
esp_tls_cfg_server_t *global_ctx = httpd_get_global_transport_ctx(server);
httpd_ssl_ctx_t *global_ctx = httpd_get_global_transport_ctx(server);
assert(global_ctx != NULL);
esp_tls_t *tls = (esp_tls_t *)calloc(1, sizeof(esp_tls_t));
@ -101,7 +106,7 @@ static esp_err_t httpd_ssl_open(httpd_handle_t server, int sockfd)
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "performing session handshake");
int ret = esp_tls_server_session_create(global_ctx, sockfd, tls);
int ret = esp_tls_server_session_create(global_ctx->tls_cfg, sockfd, tls);
if (ret != 0) {
ESP_LOGE(TAG, "esp_tls_create_server_session failed");
goto fail;
@ -119,6 +124,9 @@ static esp_err_t httpd_ssl_open(httpd_handle_t server, int sockfd)
ESP_LOGD(TAG, "Secure socket open");
if (global_ctx->open_fn) {
(global_ctx->open_fn)(server, sockfd);
}
return ESP_OK;
fail:
esp_tls_server_session_delete(tls);
@ -133,7 +141,8 @@ fail:
static void free_secure_context(void *ctx)
{
assert(ctx != NULL);
esp_tls_cfg_server_t *cfg = (esp_tls_cfg_server_t *)ctx;
httpd_ssl_ctx_t *ssl_ctx = ctx;
esp_tls_cfg_server_t *cfg = ssl_ctx->tls_cfg;
ESP_LOGI(TAG, "Server shuts down, releasing SSL context");
if (cfg->cacert_buf) {
free((void *)cfg->cacert_buf);
@ -145,14 +154,21 @@ static void free_secure_context(void *ctx)
free((void *)cfg->serverkey_buf);
}
free(cfg);
free(ssl_ctx);
}
static esp_tls_cfg_server_t *create_secure_context(const struct httpd_ssl_config *config)
static httpd_ssl_ctx_t *create_secure_context(const struct httpd_ssl_config *config)
{
esp_tls_cfg_server_t *cfg = (esp_tls_cfg_server_t *)calloc(1, sizeof(esp_tls_cfg_server_t));
if (!cfg) {
httpd_ssl_ctx_t *ssl_ctx = calloc(1, sizeof(httpd_ssl_ctx_t));
if (!ssl_ctx) {
return NULL;
}
esp_tls_cfg_server_t *cfg = (esp_tls_cfg_server_t *)calloc(1, sizeof(esp_tls_cfg_server_t));
if (!cfg) {
free(ssl_ctx);
return NULL;
}
ssl_ctx->tls_cfg = cfg;
/* cacert = CA which signs client cert, or client cert itself , which is mapped to client_verify_cert_pem */
if(config->client_verify_cert_pem != NULL) {
cfg->cacert_buf = (unsigned char *)malloc(config->client_verify_cert_len);
@ -186,7 +202,7 @@ static esp_tls_cfg_server_t *create_secure_context(const struct httpd_ssl_config
memcpy((char *)cfg->serverkey_buf, config->prvtkey_pem, config->prvtkey_len);
cfg->serverkey_bytes = config->prvtkey_len;
return cfg;
return ssl_ctx;
}
/** Start the server */
@ -199,16 +215,21 @@ esp_err_t httpd_ssl_start(httpd_handle_t *pHandle, struct httpd_ssl_config *conf
if (HTTPD_SSL_TRANSPORT_SECURE == config->transport_mode) {
esp_tls_cfg_server_t *esp_tls_cfg = create_secure_context(config);
if (!esp_tls_cfg) {
httpd_ssl_ctx_t *ssl_ctx = create_secure_context(config);
if (!ssl_ctx) {
return -1;
}
ESP_LOGD(TAG, "SSL context ready");
// set SSL specific config
config->httpd.global_transport_ctx = esp_tls_cfg;
config->httpd.global_transport_ctx = ssl_ctx;
config->httpd.global_transport_ctx_free_fn = free_secure_context;
if (config->httpd.open_fn) {
// since the httpd's open_fn is used for opening the SSL session, we save the configured
// user pointer and call it upon opening the ssl socket
ssl_ctx->open_fn = config->httpd.open_fn;
}
config->httpd.open_fn = httpd_ssl_open; // the open function configures the created SSL sessions
config->httpd.server_port = config->port_secure;

View File

@ -21,11 +21,7 @@ import re
from tiny_test_fw import Utility
import ttfw_idf
import os
import six
import socket
import hashlib
import base64
import struct
import websocket
OPCODE_TEXT = 0x1
@ -38,63 +34,24 @@ 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)
self.ws = websocket.WebSocket()
def __enter__(self):
self.socket.connect((self.ip, self.port))
self._handshake()
self.ws.connect("ws://{}:{}/ws".format(self.ip, self.port))
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)
self.ws.close()
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')
return self.ws.recv_data(control_frame=True)
def write(self, data="", opcode=OPCODE_TEXT, mask=1):
return self.socket.sendall(self._ws_encode(data=data, opcode=opcode, mask=mask))
def write(self, data="", opcode=OPCODE_TEXT):
if opcode == OPCODE_BIN:
return self.ws.send_binary(data.encode())
if opcode == OPCODE_PING:
return self.ws.ping(data)
return self.ws.send(data)
@ttfw_idf.idf_example_test(env_tag="Example_WIFI")

View File

@ -4,7 +4,7 @@ This example creates a SSL server that returns a simple HTML page when you visit
See the `esp_https_server` component documentation for details.
Before using the example, 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.
Before using the example, 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.
## Certificates

View 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(wss_server)

View 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 := wss_server
EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common
include $(IDF_PATH)/make/project.mk

View File

@ -0,0 +1,28 @@
# HTTP Websocket server with SSL support
This example creates a SSL server 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
See the `esp_https_server` component documentation for details.
Before using the example, 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.
## 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 cacert.pem -subj "/CN=ESP32 HTTPS server example"
```
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.

View File

@ -0,0 +1,4 @@
idf_component_register(SRCS "wss_server_example.c" "keep_alive.c"
INCLUDE_DIRS "."
EMBED_TXTFILES "certs/cacert.pem"
"certs/prvtkey.pem")

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL
BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx
MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ
UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T
sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k
qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd
GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4
sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb
jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/
ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3
emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY
W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx
bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN
ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl
hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo=
-----END CERTIFICATE-----

View File

@ -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-----

View File

@ -0,0 +1,7 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
COMPONENT_EMBED_TXTFILES := certs/cacert.pem
COMPONENT_EMBED_TXTFILES += certs/prvtkey.pem

View File

@ -0,0 +1,228 @@
/* 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 <esp_system.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "keep_alive.h"
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)
{
return esp_timer_get_time()/1000;
}
// 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;
}

View File

@ -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);

View File

@ -0,0 +1,282 @@
/* 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 <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include "esp_netif.h"
#include "esp_eth.h"
#include "protocol_examples_common.h"
#include <esp_https_server.h>
#include "keep_alive.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)
{
uint8_t buf[128] = { 0 };
httpd_ws_frame_t ws_pkt;
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
ws_pkt.payload = buf;
// First receive the full ws message
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;
}
// If it was a PONG, update the keep-alive
if (ws_pkt.type == HTTPD_WS_TYPE_PONG) {
ESP_LOGD(TAG, "Received PONG message");
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) {
ESP_LOGI(TAG, "Received packet with message: %s", ws_pkt.payload);
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)));
return ret;
}
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);
}
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));
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();
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 cacert_pem_start[] asm("_binary_cacert_pem_start");
extern const unsigned char cacert_pem_end[] asm("_binary_cacert_pem_end");
conf.cacert_pem = cacert_pem_start;
conf.cacert_len = cacert_pem_end - cacert_pem_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");
conf.prvtkey_pem = prvtkey_pem_start;
conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;
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;
}
static void 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
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) {
stop_wss_echo_server(*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) {
*server = start_wss_echo_server();
}
}
// 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));
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.
*/
#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
/* 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());
/* This function demonstrates periodic sending Websocket messages
* to all connected clients to this server
*/
wss_server_send_messages(&server);
}

View File

@ -0,0 +1,3 @@
CONFIG_ESP_HTTPS_SERVER_ENABLE=y
CONFIG_ESP_NETIF_TCPIP_ADAPTER_COMPATIBLE_LAYER=n
CONFIG_HTTPD_WS_SUPPORT=y