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