From d3801be6d9fa4f72f68b79696cc952d72b75c12d Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 27 Aug 2020 16:50:19 +0200 Subject: [PATCH 1/7] esp_http_server: Add httpd_ws_get_fd_info() API to check active WS clients Added a new API to WebSocket server to test provided socket descriptor if it belongs to active clients for this server and if websocket handshake has been performed Closes https://github.com/espressif/esp-idf/issues/5602 --- .../esp_http_server/include/esp_http_server.h | 13 +++++++++++++ components/esp_http_server/src/httpd_ws.c | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 0d936de88a..2eafca1839 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -1586,6 +1586,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 + * - -1 : This fd is not a client of this httpd + * - 0 : This fd is an active client, protocol is not WS + * - 1 : This fd is an active client, protocol is WS + */ +int httpd_ws_get_fd_info(httpd_handle_t hd, int fd) + #endif /* CONFIG_HTTPD_WS_SUPPORT */ /** End of WebSocket related stuff * @} diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c index a6af5c5d9e..07b9824a26 100644 --- a/components/esp_http_server/src/httpd_ws.c +++ b/components/esp_http_server/src/httpd_ws.c @@ -383,4 +383,15 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req) return ESP_OK; } +int httpd_ws_get_fd_info(httpd_handle_t hd, int fd) +{ + struct sock_db *sess = httpd_sess_get(hd, fd); + + if (sess == NULL) { + return -1; + } + bool is_active_ws = sess->ws_handshake_done && (!sess->ws_close); + return is_active_ws ? 1 : 0; +} + #endif /* CONFIG_HTTPD_WS_SUPPORT */ From fbf26804273d134e401b33c6cc2b429d19e8787c Mon Sep 17 00:00:00 2001 From: David Cermak Date: Fri, 28 Aug 2020 17:12:11 +0200 Subject: [PATCH 2/7] esp_http_server: Add Websocket API to return list of active clients Closes https://github.com/espressif/esp-idf/issues/5406 --- .../esp_http_server/include/esp_http_server.h | 38 +++++++++++++++++-- components/esp_http_server/src/httpd_main.c | 19 ++++++++++ components/esp_http_server/src/httpd_parse.c | 8 ++-- components/esp_http_server/src/httpd_ws.c | 6 +-- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 2eafca1839..6765e7d19d 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -409,6 +409,14 @@ typedef struct httpd_uri { #endif } httpd_uri_t; +/** + * @brief Structure for holding list of clients + */ +typedef struct httpd_client_list { + size_t active_clients; /*!< number of active clients in this struct */ + int client_fds[]; /*!< array of file descriptors of all active clients */ +} httpd_client_list_t; + /** * @brief Registers a URI handler * @@ -1466,6 +1474,19 @@ 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] max_fds Maximum number of socket fds the supplied list could hold + * @param[out] fd_list Structure holding socket descriptors + * + * @return + * - ESP_OK : Successfully retrieved session list + * - ESP_ERR_INVALID_ARG : Wrong arguments or list is longer than maximum + */ +esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t max_fds, httpd_client_list_t *fd_list); + /** End of Session * @} */ @@ -1526,6 +1547,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 */ @@ -1593,11 +1623,11 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t * @param[in] hd Server instance data * @param[in] fd Socket descriptor * @return - * - -1 : This fd is not a client of this httpd - * - 0 : This fd is an active client, protocol is not WS - * - 1 : This fd is an active client, protocol is WS + * - 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 */ -int httpd_ws_get_fd_info(httpd_handle_t hd, int fd) +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 diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c index daa52c3122..af536a9d87 100644 --- a/components/esp_http_server/src/httpd_main.c +++ b/components/esp_http_server/src/httpd_main.c @@ -104,6 +104,25 @@ 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 max_fds, httpd_client_list_t *fd_list) +{ + struct httpd_data *hd = (struct httpd_data *) handle; + if (hd == NULL || max_fds == 0 || fd_list == NULL || max_fds < hd->config.max_open_sockets) { + return ESP_ERR_INVALID_ARG; + } + fd_list->active_clients = 0; + for (int i = 0; i < hd->config.max_open_sockets; ++i) { + if (hd->hd_sd[i].fd != -1) { + if (fd_list->active_clients < max_fds) { + fd_list->client_fds[fd_list->active_clients++] = 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; diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index 30ce87d468..e6aaa3c4c6 100644 --- a/components/esp_http_server/src/httpd_parse.c +++ b/components/esp_http_server/src/httpd_parse.c @@ -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,13 @@ 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) { + if (ret == ESP_OK && ra->ws_type <= HTTPD_WS_TYPE_PONG) { ret = sd->ws_handler(r); } diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c index 07b9824a26..7675f263c3 100644 --- a/components/esp_http_server/src/httpd_ws.c +++ b/components/esp_http_server/src/httpd_ws.c @@ -383,15 +383,15 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req) return ESP_OK; } -int httpd_ws_get_fd_info(httpd_handle_t hd, int fd) +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 -1; + return HTTPD_WS_CLIENT_INVALID; } bool is_active_ws = sess->ws_handshake_done && (!sess->ws_close); - return is_active_ws ? 1 : 0; + return is_active_ws ? HTTPD_WS_CLIENT_WEBSOCKET : HTTPD_WS_CLIENT_HTTP; } #endif /* CONFIG_HTTPD_WS_SUPPORT */ From fe862f413feed007451e932c3196c30511ce8d14 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 3 Sep 2020 20:28:47 +0200 Subject: [PATCH 3/7] https_server: Fix use of open_fn from application level https-server uses the open_fn() callback and potentially overwrites user defined callback. This commit adds the user defined open_fn() to https-server's context and calls it upon openning a session --- .../esp_https_server/src/https_server.c | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/components/esp_https_server/src/https_server.c b/components/esp_https_server/src/https_server.c index a5befcf22f..31d2eefc5e 100644 --- a/components/esp_https_server/src/https_server.c +++ b/components/esp_https_server/src/https_server.c @@ -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; From 76ca826758fd1341f552ba02a92f57ab1cc4f89f Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 3 Sep 2020 09:57:23 +0200 Subject: [PATCH 4/7] https_server example: Add new WSS server example Added a new https server example with WS support which runs * keep-alive thread to send PINGs to clients * async message to all active WS clients Moved the existing https-server example to a subfolder Closes https://github.com/espressif/esp-idf/issues/5733 Closes https://github.com/espressif/esp-idf/issues/5686 --- .../https_server/{ => simple}/CMakeLists.txt | 0 .../https_server/{ => simple}/Makefile | 0 .../https_server/{ => simple}/README.md | 2 +- .../{ => simple}/main/CMakeLists.txt | 0 .../{ => simple}/main/certs/cacert.pem | 0 .../{ => simple}/main/certs/prvtkey.pem | 0 .../{ => simple}/main/component.mk | 0 .../https_server/{ => simple}/main/main.c | 0 .../https_server/{ => simple}/sdkconfig.ci | 0 .../{ => simple}/sdkconfig.defaults | 0 .../https_server/wss_server/CMakeLists.txt | 10 + .../https_server/wss_server/Makefile | 11 + .../https_server/wss_server/README.md | 28 ++ .../wss_server/main/CMakeLists.txt | 4 + .../wss_server/main/certs/cacert.pem | 19 ++ .../wss_server/main/certs/prvtkey.pem | 28 ++ .../https_server/wss_server/main/component.mk | 7 + .../https_server/wss_server/main/keep_alive.c | 228 ++++++++++++++ .../https_server/wss_server/main/keep_alive.h | 96 ++++++ .../wss_server/main/wss_server_example.c | 285 ++++++++++++++++++ .../wss_server/sdkconfig.defaults | 3 + 21 files changed, 720 insertions(+), 1 deletion(-) rename examples/protocols/https_server/{ => simple}/CMakeLists.txt (100%) rename examples/protocols/https_server/{ => simple}/Makefile (100%) rename examples/protocols/https_server/{ => simple}/README.md (95%) rename examples/protocols/https_server/{ => simple}/main/CMakeLists.txt (100%) rename examples/protocols/https_server/{ => simple}/main/certs/cacert.pem (100%) rename examples/protocols/https_server/{ => simple}/main/certs/prvtkey.pem (100%) rename examples/protocols/https_server/{ => simple}/main/component.mk (100%) rename examples/protocols/https_server/{ => simple}/main/main.c (100%) rename examples/protocols/https_server/{ => simple}/sdkconfig.ci (100%) rename examples/protocols/https_server/{ => simple}/sdkconfig.defaults (100%) create mode 100644 examples/protocols/https_server/wss_server/CMakeLists.txt create mode 100644 examples/protocols/https_server/wss_server/Makefile create mode 100644 examples/protocols/https_server/wss_server/README.md create mode 100644 examples/protocols/https_server/wss_server/main/CMakeLists.txt create mode 100644 examples/protocols/https_server/wss_server/main/certs/cacert.pem create mode 100644 examples/protocols/https_server/wss_server/main/certs/prvtkey.pem create mode 100644 examples/protocols/https_server/wss_server/main/component.mk create mode 100644 examples/protocols/https_server/wss_server/main/keep_alive.c create mode 100644 examples/protocols/https_server/wss_server/main/keep_alive.h create mode 100644 examples/protocols/https_server/wss_server/main/wss_server_example.c create mode 100644 examples/protocols/https_server/wss_server/sdkconfig.defaults diff --git a/examples/protocols/https_server/CMakeLists.txt b/examples/protocols/https_server/simple/CMakeLists.txt similarity index 100% rename from examples/protocols/https_server/CMakeLists.txt rename to examples/protocols/https_server/simple/CMakeLists.txt diff --git a/examples/protocols/https_server/Makefile b/examples/protocols/https_server/simple/Makefile similarity index 100% rename from examples/protocols/https_server/Makefile rename to examples/protocols/https_server/simple/Makefile diff --git a/examples/protocols/https_server/README.md b/examples/protocols/https_server/simple/README.md similarity index 95% rename from examples/protocols/https_server/README.md rename to examples/protocols/https_server/simple/README.md index 088819d2c6..24c6d6a9eb 100644 --- a/examples/protocols/https_server/README.md +++ b/examples/protocols/https_server/simple/README.md @@ -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 diff --git a/examples/protocols/https_server/main/CMakeLists.txt b/examples/protocols/https_server/simple/main/CMakeLists.txt similarity index 100% rename from examples/protocols/https_server/main/CMakeLists.txt rename to examples/protocols/https_server/simple/main/CMakeLists.txt diff --git a/examples/protocols/https_server/main/certs/cacert.pem b/examples/protocols/https_server/simple/main/certs/cacert.pem similarity index 100% rename from examples/protocols/https_server/main/certs/cacert.pem rename to examples/protocols/https_server/simple/main/certs/cacert.pem diff --git a/examples/protocols/https_server/main/certs/prvtkey.pem b/examples/protocols/https_server/simple/main/certs/prvtkey.pem similarity index 100% rename from examples/protocols/https_server/main/certs/prvtkey.pem rename to examples/protocols/https_server/simple/main/certs/prvtkey.pem diff --git a/examples/protocols/https_server/main/component.mk b/examples/protocols/https_server/simple/main/component.mk similarity index 100% rename from examples/protocols/https_server/main/component.mk rename to examples/protocols/https_server/simple/main/component.mk diff --git a/examples/protocols/https_server/main/main.c b/examples/protocols/https_server/simple/main/main.c similarity index 100% rename from examples/protocols/https_server/main/main.c rename to examples/protocols/https_server/simple/main/main.c diff --git a/examples/protocols/https_server/sdkconfig.ci b/examples/protocols/https_server/simple/sdkconfig.ci similarity index 100% rename from examples/protocols/https_server/sdkconfig.ci rename to examples/protocols/https_server/simple/sdkconfig.ci diff --git a/examples/protocols/https_server/sdkconfig.defaults b/examples/protocols/https_server/simple/sdkconfig.defaults similarity index 100% rename from examples/protocols/https_server/sdkconfig.defaults rename to examples/protocols/https_server/simple/sdkconfig.defaults diff --git a/examples/protocols/https_server/wss_server/CMakeLists.txt b/examples/protocols/https_server/wss_server/CMakeLists.txt new file mode 100644 index 0000000000..a61ada774e --- /dev/null +++ b/examples/protocols/https_server/wss_server/CMakeLists.txt @@ -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) diff --git a/examples/protocols/https_server/wss_server/Makefile b/examples/protocols/https_server/wss_server/Makefile new file mode 100644 index 0000000000..77c7ae0295 --- /dev/null +++ b/examples/protocols/https_server/wss_server/Makefile @@ -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 + diff --git a/examples/protocols/https_server/wss_server/README.md b/examples/protocols/https_server/wss_server/README.md new file mode 100644 index 0000000000..ee2fd7305e --- /dev/null +++ b/examples/protocols/https_server/wss_server/README.md @@ -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. diff --git a/examples/protocols/https_server/wss_server/main/CMakeLists.txt b/examples/protocols/https_server/wss_server/main/CMakeLists.txt new file mode 100644 index 0000000000..0093301a39 --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/CMakeLists.txt @@ -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") diff --git a/examples/protocols/https_server/wss_server/main/certs/cacert.pem b/examples/protocols/https_server/wss_server/main/certs/cacert.pem new file mode 100644 index 0000000000..cd2b80c824 --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/certs/cacert.pem @@ -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----- diff --git a/examples/protocols/https_server/wss_server/main/certs/prvtkey.pem b/examples/protocols/https_server/wss_server/main/certs/prvtkey.pem new file mode 100644 index 0000000000..70d29078c4 --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/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----- diff --git a/examples/protocols/https_server/wss_server/main/component.mk b/examples/protocols/https_server/wss_server/main/component.mk new file mode 100644 index 0000000000..b120d66e5e --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/component.mk @@ -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 diff --git a/examples/protocols/https_server/wss_server/main/keep_alive.c b/examples/protocols/https_server/wss_server/main/keep_alive.c new file mode 100644 index 0000000000..8f720d8e2f --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/keep_alive.c @@ -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 +#include +#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; imax_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; imax_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; imax_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; imax_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; imax_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; +} diff --git a/examples/protocols/https_server/wss_server/main/keep_alive.h b/examples/protocols/https_server/wss_server/main/keep_alive.h new file mode 100644 index 0000000000..add3b68f15 --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/keep_alive.h @@ -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); diff --git a/examples/protocols/https_server/wss_server/main/wss_server_example.c b/examples/protocols/https_server/wss_server/main/wss_server_example.c new file mode 100644 index 0000000000..5f1d2978c2 --- /dev/null +++ b/examples/protocols/https_server/wss_server/main/wss_server_example.c @@ -0,0 +1,285 @@ +/* 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 +#include +#include +#include +#include +#include "esp_netif.h" +#include "esp_eth.h" +#include "protocol_examples_common.h" + +#include +#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 +}; + + +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(); + } +} + +static void wss_server_send_messages(httpd_handle_t* server) +{ + // Get all clients and send async message + struct { + size_t active_clients; + int client_fds[max_clients]; + } client_list; + + 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; + } + + if (httpd_get_client_list(*server, max_clients, (httpd_client_list_t*)&client_list) == ESP_OK) { + for (size_t i=0; i < client_list.active_clients; ++i) { + int sock = client_list.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); +} + + diff --git a/examples/protocols/https_server/wss_server/sdkconfig.defaults b/examples/protocols/https_server/wss_server/sdkconfig.defaults new file mode 100644 index 0000000000..584ee7580f --- /dev/null +++ b/examples/protocols/https_server/wss_server/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_ESP_HTTPS_SERVER_ENABLE=y +CONFIG_ESP_NETIF_TCPIP_ADAPTER_COMPATIBLE_LAYER=n +CONFIG_HTTPD_WS_SUPPORT=y From 5e1e5f8be90f23201d6884f53b5031a8dfd80165 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 7 Sep 2020 16:24:26 +0200 Subject: [PATCH 5/7] http_server: Add a flag to enable using control frames in user handlers --- components/esp_http_server/include/esp_http_server.h | 6 ++++++ components/esp_http_server/src/esp_httpd_priv.h | 1 + components/esp_http_server/src/httpd_parse.c | 5 +++-- components/esp_http_server/src/httpd_uri.c | 2 ++ .../https_server/wss_server/main/wss_server_example.c | 3 ++- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 6765e7d19d..cfbe0efee2 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -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; diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h index 27a3051ac2..554f0e69bf 100644 --- a/components/esp_http_server/src/esp_httpd_priv.h +++ b/components/esp_http_server/src/esp_httpd_priv.h @@ -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 }; diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index e6aaa3c4c6..cc328a9e66 100644 --- a/components/esp_http_server/src/httpd_parse.c +++ b/components/esp_http_server/src/httpd_parse.c @@ -772,8 +772,9 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd) 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_PONG) { + /* 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); } diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index c3d559ad2a..cdc93df506 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -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; diff --git a/examples/protocols/https_server/wss_server/main/wss_server_example.c b/examples/protocols/https_server/wss_server/main/wss_server_example.c index 5f1d2978c2..fe2954640f 100644 --- a/examples/protocols/https_server/wss_server/main/wss_server_example.c +++ b/examples/protocols/https_server/wss_server/main/wss_server_example.c @@ -84,7 +84,8 @@ static const httpd_uri_t ws = { .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, - .is_websocket = true + .is_websocket = true, + .handle_ws_control_frames = true }; From 2f22a43a5df7263eaad225d86e1e49c1326266c1 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 7 Sep 2020 16:29:50 +0200 Subject: [PATCH 6/7] http_server example test: Simplify client code in the test to use ws package --- .../ws_echo_server/ws_server_example_test.py | 65 ++++--------------- 1 file changed, 11 insertions(+), 54 deletions(-) diff --git a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py index 3dd77b4697..2d53b698f0 100644 --- a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py +++ b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py @@ -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") From 6f3fa81863ff7a2796587e965f9f5d860d1d309a Mon Sep 17 00:00:00 2001 From: David Cermak Date: Tue, 8 Sep 2020 09:40:01 +0200 Subject: [PATCH 7/7] http_server: Simplified httpd_get_client_list() to return clients in int array --- .../esp_http_server/include/esp_http_server.h | 17 +++++------------ components/esp_http_server/src/httpd_main.c | 11 ++++++----- .../wss_server/main/wss_server_example.c | 16 ++++++---------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index cfbe0efee2..7372003df3 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -415,14 +415,6 @@ typedef struct httpd_uri { #endif } httpd_uri_t; -/** - * @brief Structure for holding list of clients - */ -typedef struct httpd_client_list { - size_t active_clients; /*!< number of active clients in this struct */ - int client_fds[]; /*!< array of file descriptors of all active clients */ -} httpd_client_list_t; - /** * @brief Registers a URI handler * @@ -1484,14 +1476,15 @@ 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] max_fds Maximum number of socket fds the supplied list could hold - * @param[out] fd_list Structure holding socket descriptors + * @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 maximum + * - 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 max_fds, httpd_client_list_t *fd_list); +esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t *fds, int *client_fds); /** End of Session * @} diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c index af536a9d87..67b86e1cd9 100644 --- a/components/esp_http_server/src/httpd_main.c +++ b/components/esp_http_server/src/httpd_main.c @@ -104,17 +104,18 @@ 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 max_fds, httpd_client_list_t *fd_list) +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 || max_fds == 0 || fd_list == NULL || max_fds < hd->config.max_open_sockets) { + if (hd == NULL || fds == NULL || *fds == 0 || client_fds == NULL || *fds < hd->config.max_open_sockets) { return ESP_ERR_INVALID_ARG; } - fd_list->active_clients = 0; + 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 (fd_list->active_clients < max_fds) { - fd_list->client_fds[fd_list->active_clients++] = hd->hd_sd[i].fd; + if (*fds < max_fds) { + client_fds[(*fds)++] = hd->hd_sd[i].fd; } else { return ESP_ERR_INVALID_ARG; } diff --git a/examples/protocols/https_server/wss_server/main/wss_server_example.c b/examples/protocols/https_server/wss_server/main/wss_server_example.c index fe2954640f..9bd4c8e804 100644 --- a/examples/protocols/https_server/wss_server/main/wss_server_example.c +++ b/examples/protocols/https_server/wss_server/main/wss_server_example.c @@ -210,14 +210,9 @@ static void connect_handler(void* arg, esp_event_base_t event_base, } } +// Get all clients and send async message static void wss_server_send_messages(httpd_handle_t* server) { - // Get all clients and send async message - struct { - size_t active_clients; - int client_fds[max_clients]; - } client_list; - bool send_messages = true; // Send async message to all connected clients that use websocket protocol every 10 seconds @@ -227,10 +222,11 @@ static void wss_server_send_messages(httpd_handle_t* server) if (!*server) { // httpd might not have been created by now continue; } - - if (httpd_get_client_list(*server, max_clients, (httpd_client_list_t*)&client_list) == ESP_OK) { - for (size_t i=0; i < client_list.active_clients; ++i) { - int sock = client_list.client_fds[i]; + 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));