From 696b32c942f7f27632ec5065303f999a46f5ab4d Mon Sep 17 00:00:00 2001 From: "hrushikesh.bhosale" Date: Wed, 4 Dec 2024 14:17:31 +0530 Subject: [PATCH] feat(https_server): Added Server Sent Events demo This commit adds the demo of Server Sent Events functionality in the https_server/simple example Closes https://github.com/espressif/esp-idf/issues/13603 Co-authored-by: default avatarAditya Patwardhan --- .../protocols/http_server/simple/README.md | 9 +++ .../http_server/simple/main/Kconfig.projbuild | 7 +++ .../protocols/http_server/simple/main/main.c | 44 +++++++++++++- .../simple/pytest_http_server_simple.py | 58 ++++++++++++++++++- .../http_server/simple/sdkconfig.ci.sse | 1 + 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 examples/protocols/http_server/simple/sdkconfig.ci.sse diff --git a/examples/protocols/http_server/simple/README.md b/examples/protocols/http_server/simple/README.md index 9a82716de6..a744aac35f 100644 --- a/examples/protocols/http_server/simple/README.md +++ b/examples/protocols/http_server/simple/README.md @@ -6,6 +6,15 @@ The Example consists of HTTPD server demo with demonstration of URI handling : 1. URI \hello for GET command returns "Hello World!" message 2. URI \echo for POST command echoes back the POSTed message + 3. URI \sse for GET command sends a message to client every second + +## User Callback + +The example includes a simple user callback that can be used to get the SSL context (connection information) when the server is being initialized. To enable the user callback, set `CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK` to `y` in the project configuration menu. + +## Server-Sent Events (SSE) + +The example also includes a simple SSE handler (having endpoint \sse), which sends a message to the client every second. To enable SSE, set `CONFIG_EXAMPLE_ENABLE_SSE_HANDLER` to `y` in the project configuration menu. ## How to use example diff --git a/examples/protocols/http_server/simple/main/Kconfig.projbuild b/examples/protocols/http_server/simple/main/Kconfig.projbuild index 2611ab44ac..87f7a449bc 100644 --- a/examples/protocols/http_server/simple/main/Kconfig.projbuild +++ b/examples/protocols/http_server/simple/main/Kconfig.projbuild @@ -26,4 +26,11 @@ menu "Example Configuration" help The client's password which used for basic authenticate. + config EXAMPLE_ENABLE_SSE_HANDLER + bool "Enable Server-Sent Events (SSE) handler" + default n + help + Enable this option to use Server-Sent Events (SSE) functionality. + This will allow the server to push real-time updates to the client over an HTTP connection. + endmenu diff --git a/examples/protocols/http_server/simple/main/main.c b/examples/protocols/http_server/simple/main/main.c index e13810a4e1..1a237a2c46 100644 --- a/examples/protocols/http_server/simple/main/main.c +++ b/examples/protocols/http_server/simple/main/main.c @@ -22,7 +22,8 @@ #include "esp_netif.h" #include "esp_tls.h" #include "esp_check.h" - +#include +#include #if !CONFIG_IDF_TARGET_LINUX #include #include @@ -392,6 +393,40 @@ static const httpd_uri_t ctrl = { .user_ctx = NULL }; +#if CONFIG_EXAMPLE_ENABLE_SSE_HANDLER +/* An HTTP GET handler for SSE */ +static esp_err_t sse_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "text/event-stream"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + httpd_resp_set_hdr(req, "Connection", "keep-alive"); + + char sse_data[64]; + while (1) { + struct timeval tv; + gettimeofday(&tv, NULL); // Get the current time + int64_t time_since_boot = tv.tv_sec; // Time since boot in seconds + esp_err_t err; + int len = snprintf(sse_data, sizeof(sse_data), "data: Time since boot: %lld seconds\n\n", time_since_boot); + if ((err = httpd_resp_send_chunk(req, sse_data, len)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to send sse data (returned %02X)", err); + break; + } + vTaskDelay(pdMS_TO_TICKS(1000)); // Send data every second + } + + httpd_resp_send_chunk(req, NULL, 0); // End response + return ESP_OK; +} + +static const httpd_uri_t sse = { + .uri = "/sse", + .method = HTTP_GET, + .handler = sse_handler, + .user_ctx = NULL +}; +#endif // CONFIG_EXAMPLE_ENABLE_SSE_HANDLER + static httpd_handle_t start_webserver(void) { httpd_handle_t server = NULL; @@ -414,9 +449,12 @@ static httpd_handle_t start_webserver(void) httpd_register_uri_handler(server, &echo); httpd_register_uri_handler(server, &ctrl); httpd_register_uri_handler(server, &any); - #if CONFIG_EXAMPLE_BASIC_AUTH +#if CONFIG_EXAMPLE_ENABLE_SSE_HANDLER + httpd_register_uri_handler(server, &sse); // Register SSE handler +#endif +#if CONFIG_EXAMPLE_BASIC_AUTH httpd_register_basic_auth(server); - #endif +#endif return server; } diff --git a/examples/protocols/http_server/simple/pytest_http_server_simple.py b/examples/protocols/http_server/simple/pytest_http_server_simple.py index 0827bdb60e..1894b755d8 100644 --- a/examples/protocols/http_server/simple/pytest_http_server_simple.py +++ b/examples/protocols/http_server/simple/pytest_http_server_simple.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import logging import os @@ -14,6 +14,7 @@ import time import pytest try: + import http.client from idf_http_server_test import client except ModuleNotFoundError: sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'tools', 'ci', 'python_packages')) @@ -171,3 +172,58 @@ def test_examples_protocol_http_server_lru_purge_enable(dut: Dut) -> None: for t in threads: t.join() + + +@pytest.mark.esp32 +@pytest.mark.wifi_router +@pytest.mark.parametrize('config', ['sse',], indirect=True) +def test_examples_protocol_http_server_sse(dut: Dut) -> None: + # Get binary file + binary_file = os.path.join(dut.app.binary_path, 'simple.bin') + bin_size = os.path.getsize(binary_file) + logging.info('http_server_bin_size : {}KB'.format(bin_size // 1024)) + + # Upload binary and start testing + logging.info('Starting http_server simple test app') + + # Parse IP address of STA + logging.info('Waiting to connect with AP') + if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True: + dut.expect('Please input ssid password:') + env_name = 'wifi_router' + ap_ssid = get_env_config_variable(env_name, 'ap_ssid') + ap_password = get_env_config_variable(env_name, 'ap_password') + dut.write(f'{ap_ssid} {ap_password}') + got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode() + got_port = int(dut.expect(r"(?:[\s\S]*)Starting server on port: '(\d+)'", timeout=30)[1].decode()) + + logging.info('Got IP : {}'.format(got_ip)) + logging.info('Got Port : {}'.format(got_port)) + + # Expected Logs + dut.expect('Registering URI handlers', timeout=30) + + logging.info('Test /sse GET handler') + try: + logging.info(f'Connecting to {got_ip}:{got_port}') + conn = http.client.HTTPConnection(got_ip, got_port, timeout=15) + conn.request('GET', url='/sse') # Ensure the URL path is correct + response = conn.getresponse() + + # Process and verify only the first 5 lines of the response as the response is continuous + response_data = '' + for i, line in enumerate(response): + if i >= 5: + break + decoded_line = line.decode('utf-8').strip() + response_data += decoded_line + + conn.close() + + # Verify the format of the line + if 'data: Time since boot:' not in response_data: + raise RuntimeError(f'Unexpected line format: {response_data}') + + except Exception as e: + logging.error(f'Error during SSE GET request: {e}') + raise RuntimeError('SSE handler test failed due to connection error') diff --git a/examples/protocols/http_server/simple/sdkconfig.ci.sse b/examples/protocols/http_server/simple/sdkconfig.ci.sse new file mode 100644 index 0000000000..e34f367149 --- /dev/null +++ b/examples/protocols/http_server/simple/sdkconfig.ci.sse @@ -0,0 +1 @@ +CONFIG_EXAMPLE_ENABLE_SSE_HANDLER=y