diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index b1a2197c9c3..b4beb20dd13 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -16,6 +16,7 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -78,6 +79,7 @@ initializer that should be kept in sync .keep_alive_idle = 0, \ .keep_alive_interval = 0, \ .keep_alive_count = 0, \ + .if_name = NULL, \ .open_fn = NULL, \ .close_fn = NULL, \ .uri_match_fn = NULL \ @@ -239,6 +241,10 @@ typedef struct httpd_config { int keep_alive_idle; /*!< Keep-alive idle time. Default is 5 (second) */ int keep_alive_interval;/*!< Keep-alive interval time. Default is 5 (second) */ int keep_alive_count; /*!< Keep-alive packet retry send count. Default is 3 counts */ + struct ifreq *if_name; /*!< Bind server to a specific network interface. + If NULL, server listens on all interfaces (INADDR_ANY). + The pointer only needs to remain valid for the duration of + httpd_start() -- it is not referenced after that call returns. */ /** * Custom session opening callback. * diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c index 74331d92464..ae275766930 100644 --- a/components/esp_http_server/src/httpd_main.c +++ b/components/esp_http_server/src/httpd_main.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "esp_httpd_priv.h" @@ -386,6 +387,26 @@ static esp_err_t httpd_server_init(struct httpd_data *hd) ESP_LOGW(TAG, LOG_FMT("error in setsockopt SO_REUSEADDR (%d)"), errno); } + /* Bind to specific network interface if configured */ + if (hd->config.if_name && hd->config.if_name->ifr_name[0] != 0) { + ESP_LOGI(TAG, LOG_FMT("Binding server to interface: %s"), hd->config.if_name->ifr_name); +#ifndef __APPLE__ + if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, hd->config.if_name, sizeof(struct ifreq)) < 0) { +#else + int idx = if_nametoindex(hd->config.if_name->ifr_name); + if (idx == 0) { + ESP_LOGE(TAG, LOG_FMT("Invalid interface name %s"), hd->config.if_name->ifr_name); + close(fd); + return ESP_FAIL; + } + if (setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &idx, sizeof(idx)) < 0) { +#endif + ESP_LOGE(TAG, LOG_FMT("Failed to bind to interface %s (%d)"), hd->config.if_name->ifr_name, errno); + close(fd); + return ESP_FAIL; + } + } + int ret = bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if (ret < 0) { ESP_LOGE(TAG, LOG_FMT("error in bind (%d)"), errno); diff --git a/components/esp_http_server/test_apps/main/test_http_server.c b/components/esp_http_server/test_apps/main/test_http_server.c index 6b91505524f..3d8a7f03fac 100644 --- a/components/esp_http_server/test_apps/main/test_http_server.c +++ b/components/esp_http_server/test_apps/main/test_http_server.c @@ -9,6 +9,8 @@ #include #include #include +#include +#include "esp_log.h" #include "unity.h" #include "test_utils.h" @@ -16,6 +18,8 @@ int pre_start_mem, post_stop_mem, post_stop_min_mem; bool basic_sanity = true; +#define TAG "test_http_server" + esp_err_t null_func(httpd_req_t *req) { return ESP_OK; @@ -160,7 +164,7 @@ TEST_CASE("Leak Test", "[HTTP SERVER]") test_case_uses_tcpip(); task_count = uxTaskGetNumberOfTasks(); - printf("Initial task count: %d\n", task_count); + ESP_LOGI(TAG, "Initial task count: %d\n", task_count); pre_start_mem = esp_get_free_heap_size(); @@ -170,7 +174,7 @@ TEST_CASE("Leak Test", "[HTTP SERVER]") unsigned num_tasks = uxTaskGetNumberOfTasks(); task_count++; if (num_tasks != task_count) { - printf("Incorrect task count (starting): %d expected %d\n", + ESP_LOGE(TAG, "Incorrect task count (starting): %d expected %d\n", num_tasks, task_count); res = false; } @@ -178,14 +182,14 @@ TEST_CASE("Leak Test", "[HTTP SERVER]") for (int i = 0; i < SERVER_INSTANCES; i++) { if (httpd_stop(hd[i]) != ESP_OK) { - printf("Failed to stop httpd task %d\n", i); + ESP_LOGE(TAG, "Failed to stop httpd task %d\n", i); res = false; } vTaskDelay(10); unsigned num_tasks = uxTaskGetNumberOfTasks(); task_count--; if (num_tasks != task_count) { - printf("Incorrect task count (stopping): %d expected %d\n", + ESP_LOGE(TAG, "Incorrect task count (stopping): %d expected %d\n", num_tasks, task_count); res = false; } @@ -290,6 +294,79 @@ TEST_CASE("Max Allowed Sockets Test", "[HTTP SERVER]") TEST_ASSERT(httpd_start(&hd, &config) != ESP_OK); } +TEST_CASE("Interface Binding Test", "[HTTP SERVER]") +{ + test_case_uses_tcpip(); + + httpd_handle_t hd; + struct ifreq ifr; + + /* Test 1: Server starts successfully with NULL if_name (default behavior - INADDR_ANY) */ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 8080; + TEST_ASSERT(config.if_name == NULL); + TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK); + TEST_ASSERT(httpd_stop(hd) == ESP_OK); + + /* Test 2: Server starts successfully with loopback interface binding */ + memset(&ifr, 0, sizeof(ifr)); + strcpy(ifr.ifr_name, "lo0"); // Loopback interface + config.server_port = 8081; + config.if_name = 𝔦 + + /* On embedded systems, loopback may not exist, so we test both success and expected failure */ + esp_err_t ret = httpd_start(&hd, &config); + if (ret == ESP_OK) { + /* If loopback exists and binding succeeds, verify we can stop cleanly */ + TEST_ASSERT(httpd_stop(hd) == ESP_OK); + } else { + /* If loopback doesn't exist or binding fails, that's also acceptable */ + /* The important part is that the server doesn't crash */ + ESP_LOGI(TAG, "Loopback binding failed as expected on this platform\n"); + } + + /* Test 3: Server handles empty interface name (should bind to all interfaces) */ + memset(&ifr, 0, sizeof(ifr)); + ifr.ifr_name[0] = '\0'; // Empty interface name + config.server_port = 8082; + config.if_name = 𝔦 + + /* Empty interface name should be ignored and server should start normally */ + TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK); + TEST_ASSERT(httpd_stop(hd) == ESP_OK); + + /* Test 4: Server handles invalid interface name gracefully */ + memset(&ifr, 0, sizeof(ifr)); + strcpy(ifr.ifr_name, "nonex"); // Invalid interface + config.server_port = 8083; + config.if_name = 𝔦 + + /* Starting with invalid interface should fail gracefully */ + ret = httpd_start(&hd, &config); + TEST_ASSERT(ret != ESP_OK); + if (ret != ESP_OK) { + /* Expected failure - invalid interface */ + ESP_LOGI(TAG, "Invalid interface binding failed as expected\n"); + } else { + /* On some platforms, the check might not happen until actual use */ + httpd_stop(hd); + } + + /* Test 5: Verify backward compatibility - multiple servers without interface binding */ + httpd_handle_t hd1, hd2; + httpd_config_t config1 = HTTPD_DEFAULT_CONFIG(); + httpd_config_t config2 = HTTPD_DEFAULT_CONFIG(); + config1.server_port = 8084; + config1.ctrl_port = ESP_HTTPD_DEF_CTRL_PORT + 1; + config2.server_port = 8085; + config2.ctrl_port = ESP_HTTPD_DEF_CTRL_PORT + 2; + + TEST_ASSERT(httpd_start(&hd1, &config1) == ESP_OK); + TEST_ASSERT(httpd_start(&hd2, &config2) == ESP_OK); + TEST_ASSERT(httpd_stop(hd1) == ESP_OK); + TEST_ASSERT(httpd_stop(hd2) == ESP_OK); +} + void app_main(void) { unity_run_menu(); diff --git a/components/esp_https_server/include/esp_https_server.h b/components/esp_https_server/include/esp_https_server.h index 99ff8a95389..2a34eb7ebad 100644 --- a/components/esp_https_server/include/esp_https_server.h +++ b/components/esp_https_server/include/esp_https_server.h @@ -207,6 +207,7 @@ typedef struct httpd_ssl_config httpd_ssl_config_t; .keep_alive_idle = 0, \ .keep_alive_interval = 0, \ .keep_alive_count = 0, \ + .if_name = NULL, \ .open_fn = NULL, \ .close_fn = NULL, \ .uri_match_fn = NULL \ diff --git a/docs/en/api-reference/protocols/esp_http_server.rst b/docs/en/api-reference/protocols/esp_http_server.rst index f033c18ab84..22640d28d59 100644 --- a/docs/en/api-reference/protocols/esp_http_server.rst +++ b/docs/en/api-reference/protocols/esp_http_server.rst @@ -21,6 +21,33 @@ Application Examples - :example:`protocols/http_server/advanced_tests` demonstrates how to use the HTTP server for advanced testing. +Interface Binding +----------------- + +By default, the server listens on all available interfaces (``INADDR_ANY``). This is the behavior when ``httpd_config_t.if_name`` is ``NULL``. + +To bind the HTTP server to a specific network interface, set ``httpd_config_t.if_name`` to point to a ``struct ifreq`` with ``ifr_name`` populated (for example ``"eth0"``, ``"en0"``, or ``"lo"`` depending on platform). + +.. code-block:: c + + #include + + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + struct ifreq ifr = {0}; + strncpy(ifr.ifr_name, "eth0", sizeof(ifr.ifr_name) - 1); + ifr.ifr_name[sizeof(ifr.ifr_name) - 1] = '\0'; + + config.if_name = 𝔦 + config.server_port = 80; + + ESP_ERROR_CHECK(httpd_start(&server, &config)); + +Notes: + +- ``if_name`` is only used during ``httpd_start()``. The ``ifreq`` object only needs to stay valid for the duration of that call. + Persistent Connections ----------------------