Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
1 change: 1 addition & 0 deletions include/web/http_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/core/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Comment on lines +753 to 756
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strncpy(config->web_bind_ip, value, 31); does not guarantee NUL-termination when value is length >= 31. This can leave web_bind_ip unterminated and later %s logging / uv_ip4_addr reads past the buffer. Ensure the destination is always NUL-terminated (or use a bounded copy helper that always terminates).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strncpy here follows the same pattern as other lines in the same file, though the review comment is absolutely correct. I'd like to follow up in a separate PR and make sure that 1) the safe_strcpy function in strings.c is correct and safe, and 2) replace nearly all instances of strncpy with safe_strcpy, since 3-4 different methods are used throughout the codebase today for copying strings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

strncpy(config->web_root, value, MAX_PATH_LENGTH - 1);
} else if (strcmp(name, "auth_enabled") == 0) {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions src/core/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/web/api_handlers_common_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions src/web/api_handlers_settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down
1 change: 1 addition & 0 deletions src/web/api_handlers_system.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 9 additions & 4 deletions src/web/libuv_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions web/js/components/preact/SettingsView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -788,6 +791,19 @@ export function SettingsView() {
disabled={!canModifySettings}
/>
</div>
<div class="setting grid grid-cols-1 md:grid-cols-3 gap-4 items-center mb-4">
<label for="setting-web-bind-ip" class="font-medium">{t('settings.webBindIp')}</label>
<input
type="text"
id="setting-web-bind-ip"
name="webBindIp"
class="col-span-2 p-2 border border-input rounded bg-background text-foreground disabled:opacity-60 disabled:cursor-not-allowed"
value={settings.webBindIp}
onChange={handleInputChange}
disabled={!canModifySettings}
placeholder="0.0.0.0"
/>
</div>
<div class="setting grid grid-cols-1 md:grid-cols-3 gap-4 items-center mb-4">
<label for="setting-thread-pool" class="font-medium">
{t('settings.threadPoolSize')}
Expand Down
1 change: 1 addition & 0 deletions web/js/utils/settings-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ function getDefaultSettings() {
go2rtc_api_port: 1984,
webrtc_disabled: false,
web_port: 8080,
web_bind_ip: "0.0.0.0",
web_auth_enabled: true // Default to auth enabled for safety
};
}
Expand Down
1 change: 1 addition & 0 deletions web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,7 @@
"settings.webInterface": "Web Interface Settings",
"settings.pendingRestartLabel": "Pending restart:",
"settings.webPort": "Web Port",
"settings.webBindIp": "Web Bind Address",
"settings.threadPoolSize": "Thread Pool Size",
"settings.requiresRestart": "requires restart",
"settings.threadPoolPlaceholder": "Default: 2× CPU cores",
Expand Down
Loading