diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 3fe8e20a..cc8aefeb 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -88,6 +88,7 @@ path = /var/lib/lightnvr/data/database/lightnvr.db [web] port = 8080 +bind_ip = 0.0.0.0 root = /var/lib/lightnvr/www auth_enabled = true username = admin diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3afad13d..57f4b09e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -56,6 +56,7 @@ path = /var/lib/lightnvr/data/database/lightnvr.db [web] port = 8080 +bind_ip = 0.0.0.0 root = /var/lib/lightnvr/www auth_enabled = true username = admin @@ -220,6 +221,7 @@ path = /var/lib/lightnvr/data/database/lightnvr.db ```ini [web] port = 8080 +bind_ip = 0.0.0.0 root = /var/lib/lightnvr/www auth_enabled = true username = admin @@ -229,6 +231,7 @@ web_thread_pool_size = 8 ``` - `port`: Port for the web interface +- `bind_ip`: IP address for the web interface - `root`: Directory containing web interface files - `auth_enabled`: Whether to enable authentication for the web interface - `username`: Username for web interface authentication diff --git a/include/core/config.h b/include/core/config.h index 49e37cbd..7ab76025 100644 --- a/include/core/config.h +++ b/include/core/config.h @@ -153,6 +153,7 @@ typedef struct { // Web server settings int web_thread_pool_size; // libuv UV_THREADPOOL_SIZE (default: 2x CPU cores, requires restart) int web_port; + char web_bind_ip[32]; char web_root[MAX_PATH_LENGTH]; bool web_auth_enabled; char web_username[32]; diff --git a/include/web/http_server.h b/include/web/http_server.h index a9472506..8b8016e5 100644 --- a/include/web/http_server.h +++ b/include/web/http_server.h @@ -18,6 +18,7 @@ */ typedef struct { int port; // Server port + const char *bind_ip; // Server bind address const char *web_root; // Web root directory bool auth_enabled; // Authentication enabled char username[32]; // Authentication username diff --git a/src/core/config.c b/src/core/config.c index ab9a82c8..4a0eab3d 100644 --- a/src/core/config.c +++ b/src/core/config.c @@ -108,6 +108,7 @@ static const env_config_mapping_t env_config_mappings[] = { // Web server settings {"WEB_PORT", CONFIG_TYPE_INT, CONFIG_OFFSET(web_port), 0, NULL, 8080, false}, + {"WEB_BIND_IP", CONFIG_TYPE_STRING, CONFIG_OFFSET(web_bind_ip), 32, "0.0.0.0", 0, false}, {"WEB_AUTH_ENABLED", CONFIG_TYPE_BOOL, CONFIG_OFFSET(web_auth_enabled), 0, NULL, 0, true}, {"WEB_USERNAME", CONFIG_TYPE_STRING, CONFIG_OFFSET(web_username), 32, "admin", 0, false}, {"WEB_TRUSTED_PROXY_CIDRS", CONFIG_TYPE_STRING, CONFIG_OFFSET(trusted_proxy_cidrs), WEB_TRUSTED_PROXY_CIDRS_MAX, "", 0, false}, @@ -352,6 +353,7 @@ void load_default_config(config_t *config) { // Web server settings config->web_port = 8080; + snprintf(config->web_bind_ip, 32, "0.0.0.0"); snprintf(config->web_root, MAX_PATH_LENGTH, "/var/lib/lightnvr/www"); config->web_auth_enabled = true; snprintf(config->web_username, 32, "admin"); @@ -748,6 +750,9 @@ static int config_ini_handler(void* user, const char* section, const char* name, else if (strcmp(section, "web") == 0) { if (strcmp(name, "port") == 0) { config->web_port = safe_atoi(value, 0); + } else if (strcmp(name, "bind_ip") == 0) { + strncpy(config->web_bind_ip, value, sizeof(config->web_bind_ip) - 1); + config->web_bind_ip[sizeof(config->web_bind_ip) - 1] = '\0'; } else if (strcmp(name, "root") == 0) { strncpy(config->web_root, value, MAX_PATH_LENGTH - 1); } else if (strcmp(name, "auth_enabled") == 0) { @@ -1347,6 +1352,9 @@ int reload_config(config_t *config) { // Save copies of the current config fields needed for comparison int old_log_level = config->log_level; int old_web_port = config->web_port; + char old_web_bind_ip[32]; + strncpy(old_web_bind_ip, config->web_bind_ip, 31); + old_web_bind_ip[31] = '\0'; char old_storage_path[MAX_PATH_LENGTH]; strncpy(old_storage_path, config->storage_path, sizeof(old_storage_path) - 1); old_storage_path[sizeof(old_storage_path) - 1] = '\0'; @@ -1375,6 +1383,11 @@ int reload_config(config_t *config) { log_info("Web port changed: %d -> %d", old_web_port, config->web_port); log_warn("Web port change requires restart to take effect"); } + + if (strcmp(old_web_bind_ip, config->web_bind_ip) != 0) { + log_info("Web bind address changed: %s -> %s", old_web_bind_ip, config->web_bind_ip); + log_warn("Web bind address change requires restart to take effect"); + } if (strcmp(old_storage_path, config->storage_path) != 0) { log_info("Storage path changed: %s -> %s", old_storage_path, config->storage_path); @@ -1667,6 +1680,7 @@ int save_config(const config_t *config, const char *path) { fprintf(file, "[web]\n"); fprintf(file, "web_thread_pool_size = %d ; libuv UV_THREADPOOL_SIZE (default: 2x CPU cores; requires restart)\n", config->web_thread_pool_size); fprintf(file, "port = %d\n", config->web_port); + fprintf(file, "bind_ip = %s\n", config->web_bind_ip); fprintf(file, "root = %s\n", config->web_root); fprintf(file, "auth_enabled = %s\n", config->web_auth_enabled ? "true" : "false"); fprintf(file, "username = %s\n", config->web_username); @@ -1807,6 +1821,7 @@ void print_config(const config_t *config) { printf(" Web Server Settings:\n"); printf(" Web Port: %d\n", config->web_port); + printf(" Web Bind Address: %s\n", config->web_bind_ip); printf(" Web Root: %s\n", config->web_root); printf(" Web Auth Enabled: %s\n", config->web_auth_enabled ? "true" : "false"); printf(" Web Username: %s\n", config->web_username); diff --git a/src/core/main.c b/src/core/main.c index 80f98eb6..9cebb42e 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -912,6 +912,7 @@ int main(int argc, char *argv[]) { // Initialize web server with direct handlers http_server_config_t server_config = { .port = config.web_port, + .bind_ip = config.web_bind_ip, .web_root = config.web_root, .auth_enabled = config.web_auth_enabled, .cors_enabled = true, @@ -932,8 +933,8 @@ int main(int argc, char *argv[]) { } // Initialize HTTP server (libuv + llhttp) - log_info("Initializing web server on port %d (daemon_mode: %s)", - config.web_port, daemon_mode ? "true" : "false"); + log_info("Initializing web server on %s:%d (daemon_mode: %s)", + config.web_bind_ip, config.web_port, daemon_mode ? "true" : "false"); http_server = libuv_server_init(&server_config); if (!http_server) { @@ -962,13 +963,13 @@ int main(int argc, char *argv[]) { log_info("Starting web server..."); if (http_server_start(http_server) != 0) { - log_error("Failed to start libuv web server on port %d", config.web_port); + log_error("Failed to start libuv web server on %s:%d", config.web_bind_ip, config.web_port); http_server_destroy(http_server); http_server = NULL; // Prevent double-free in cleanup goto cleanup; } - log_info("libuv web server started successfully on port %d", config.web_port); + log_info("libuv web server started successfully on %s:%d", config.web_bind_ip, config.web_port); // Initialize and start health check system for web server self-healing init_health_check_system(); diff --git a/src/web/api_handlers_common_utils.c b/src/web/api_handlers_common_utils.c index c042bc69..9450198f 100644 --- a/src/web/api_handlers_common_utils.c +++ b/src/web/api_handlers_common_utils.c @@ -37,6 +37,7 @@ char* create_json_string(const config_t *config) { cJSON_AddNumberToObject(json, "retention", config->retention_days); cJSON_AddBoolToObject(json, "auto_delete", config->auto_delete_oldest); cJSON_AddNumberToObject(json, "web_port", config->web_port); + cJSON_AddStringToObject(json, "web_bind_ip", config->web_bind_ip); cJSON_AddBoolToObject(json, "auth_enabled", config->web_auth_enabled); cJSON_AddStringToObject(json, "username", config->web_username); cJSON_AddStringToObject(json, "password", "********"); // Don't include actual password diff --git a/src/web/api_handlers_settings.c b/src/web/api_handlers_settings.c index 00731dd4..93077f77 100644 --- a/src/web/api_handlers_settings.c +++ b/src/web/api_handlers_settings.c @@ -275,6 +275,7 @@ void handle_get_settings(const http_request_t *req, http_response_t *res) { // Add settings properties cJSON_AddNumberToObject(settings, "web_thread_pool_size", g_config.web_thread_pool_size); cJSON_AddNumberToObject(settings, "web_port", g_config.web_port); + cJSON_AddStringToObject(settings, "web_bind_ip", g_config.web_bind_ip); cJSON_AddStringToObject(settings, "web_root", g_config.web_root); cJSON_AddBoolToObject(settings, "web_auth_enabled", g_config.web_auth_enabled); cJSON_AddBoolToObject(settings, "demo_mode", g_config.demo_mode); @@ -482,6 +483,30 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { log_info("Updated web_port: %d", g_config.web_port); } + // Web bind address + cJSON *web_bind_ip = cJSON_GetObjectItem(settings, "web_bind_ip"); + if (web_bind_ip && cJSON_IsString(web_bind_ip)) { + const char *new_bind_ip = web_bind_ip->valuestring; + bool bind_ip_empty = (new_bind_ip == NULL); + + if (!bind_ip_empty) { + while (isspace((unsigned char)*new_bind_ip)) { + new_bind_ip++; + } + bind_ip_empty = (*new_bind_ip == '\0'); + } + + if (bind_ip_empty) { + log_warn("Rejected empty web_bind_ip update"); + } else if (strcmp(g_config.web_bind_ip, new_bind_ip) != 0) { + strncpy(g_config.web_bind_ip, new_bind_ip, sizeof(g_config.web_bind_ip) - 1); + g_config.web_bind_ip[sizeof(g_config.web_bind_ip) - 1] = '\0'; + settings_changed = true; + restart_required = true; + log_info("Updated web_bind_ip: %s (restart required)", g_config.web_bind_ip); + } + } + // Web root cJSON *web_root = cJSON_GetObjectItem(settings, "web_root"); if (web_root && cJSON_IsString(web_root)) { diff --git a/src/web/api_handlers_system.c b/src/web/api_handlers_system.c index 9441de19..a18ad859 100644 --- a/src/web/api_handlers_system.c +++ b/src/web/api_handlers_system.c @@ -1517,6 +1517,7 @@ void handle_post_system_backup(const http_request_t *req, http_response_t *res) // Add config properties cJSON_AddNumberToObject(config, "web_port", g_config.web_port); + cJSON_AddStringToObject(config, "web_bind_ip", g_config.web_bind_ip); cJSON_AddStringToObject(config, "web_root", g_config.web_root); cJSON_AddStringToObject(config, "log_file", g_config.log_file); cJSON_AddStringToObject(config, "pid_file", g_config.pid_file); diff --git a/src/web/libuv_server.c b/src/web/libuv_server.c index b7e247c5..8f881b4f 100644 --- a/src/web/libuv_server.c +++ b/src/web/libuv_server.c @@ -171,7 +171,7 @@ static http_server_handle_t libuv_server_init_internal(const http_server_config_ // Continue anyway - proxy requests will return 503 } - log_info("libuv_server_init: Server initialized on port %d", config->port); + log_info("libuv_server_init: Server initialized on %s:%d", config->bind_ip, config->port); // Cast to generic handle type (http_server_t* is compatible pointer) return (http_server_handle_t)server; @@ -293,9 +293,14 @@ int libuv_server_start(http_server_handle_t handle) { // Bind to address struct sockaddr_in addr; - uv_ip4_addr("0.0.0.0", server->config.port, &addr); + int r = uv_ip4_addr(server->config.bind_ip, server->config.port, &addr); + if (r != 0) { + log_error("libuv_server_start: IPv4 addr/port failed for %s:%d: %s", + server->config.bind_ip, server->config.port, uv_strerror(r)); + return -1; + } - int r = uv_tcp_bind(&server->listener, (const struct sockaddr *)&addr, 0); + r = uv_tcp_bind(&server->listener, (const struct sockaddr *)&addr, 0); if (r != 0) { log_error("libuv_server_start: Bind failed: %s", uv_strerror(r)); return -1; @@ -309,7 +314,7 @@ int libuv_server_start(http_server_handle_t handle) { } server->running = true; - log_info("libuv_server_start: Listening on port %d", server->config.port); + log_info("libuv_server_start: Listening on %s:%d", server->config.bind_ip, server->config.port); // Start event loop in separate thread if we own it if (server->owns_loop) { diff --git a/web/js/components/preact/SettingsView.jsx b/web/js/components/preact/SettingsView.jsx index eabd4e71..594af8ff 100644 --- a/web/js/components/preact/SettingsView.jsx +++ b/web/js/components/preact/SettingsView.jsx @@ -36,6 +36,7 @@ export function SettingsView() { dbBackupRetentionCount: '24', dbPostBackupScript: '', webPort: '8080', + webBindIp: '0.0.0.0', webThreadPoolSize: '', // populated from API; blank = use server default (2x cores) maxStreams: '32', authEnabled: true, @@ -258,6 +259,7 @@ export function SettingsView() { dbBackupRetentionCount: settingsData.db_backup_retention_count?.toString() || '24', dbPostBackupScript: settingsData.db_post_backup_script || '', webPort: settingsData.web_port?.toString() || '', + webBindIp: settingsData.web_bind_ip?.toString() || '0.0.0.0', webThreadPoolSize: settingsData.web_thread_pool_size?.toString() || '', maxStreams: settingsData.max_streams?.toString() || '32', authEnabled: settingsData.web_auth_enabled || false, @@ -353,6 +355,7 @@ export function SettingsView() { db_backup_retention_count: Number.isNaN(parsedDbBackupRetentionCount) ? 0 : parsedDbBackupRetentionCount, db_post_backup_script: settings.dbPostBackupScript, web_port: parseInt(settings.webPort, 10), + web_bind_ip: settings.webBindIp, web_thread_pool_size: Number.isNaN(webThreadPoolSize) ? undefined : webThreadPoolSize, max_streams: Number.isNaN(parsedMaxStreams) ? 32 : parsedMaxStreams, web_auth_enabled: settings.authEnabled, @@ -788,6 +791,19 @@ export function SettingsView() { disabled={!canModifySettings} /> +