From 2696ff557ce8e4b658df03d3ae4057240a48c228 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Tue, 21 Apr 2026 09:48:15 +0200 Subject: [PATCH] feat: improve nwwebui configuration, logging and daemon support --- config.h.cmake | 30 +- nwwebui.c | 801 +++++++++++++++++++++++++++++++++++++++-------- readconfig.pl | 335 ++++++++++++++++---- smart.conf.cmake | 108 ++++++- 4 files changed, 1058 insertions(+), 216 deletions(-) diff --git a/config.h.cmake b/config.h.cmake index 406e65d..0f0a011 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -1,21 +1,29 @@ #ifndef NWWEBUI_CONFIG_H #define NWWEBUI_CONFIG_H -#define DEFAULT_SMART_CONF "@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf" -#define DEFAULT_SMART_PERL "@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/smart" +#define NWWEBUI_NAME "nwwebui" +#define NWWEBUI_VERSION "@MARS_NWE_VERSION@" -#define LOG_PATH_DEFAULT "@MARS_NWE_LOG_DIR@/nwwebui.log" +#define DEFAULT_SMART_CONF "@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf" +#define DEFAULT_SMART_PERL "@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/smart" -#define LOG_LEVEL_ERROR 0 -#define LOG_LEVEL_INFO 1 -#define LOG_LEVEL_DEBUG 2 -#define LOG_LEVEL_DEFAULT LOG_LEVEL_INFO +#define LOG_PATH_DEFAULT "@MARS_NWE_LOG_DIR@/nwwebui.log" -#define DEFAULT_BIND_IP "0.0.0.0" -#define DEFAULT_TLS_PORT 9443 +#define LOG_LEVEL_ERROR 0 +#define LOG_LEVEL_INFO 1 +#define LOG_LEVEL_DEBUG 2 +#define LOG_LEVEL_DEFAULT LOG_LEVEL_INFO -#define DEFAULT_CERT_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.crt" -#define DEFAULT_KEY_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key" +#define DEFAULT_BIND_IP "0.0.0.0" +#define DEFAULT_SSL_ENABLE 1 +#define DEFAULT_HTTP_PORT 9080 +#define DEFAULT_HTTPS_PORT 9443 + +#define DEFAULT_CERT_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.crt" +#define DEFAULT_KEY_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key" + +#define DEFAULT_PID_FILE "@MARS_NWE_PID_DIR@/nwwebui.pid" +#define DEFAULT_DAEMONIZE 0 #define NW_BACKLOG 64 #define NW_BUF_SZ 16384 diff --git a/nwwebui.c b/nwwebui.c index 9900287..0fd8921 100644 --- a/nwwebui.c +++ b/nwwebui.c @@ -25,10 +25,15 @@ typedef struct { char bind_ip[64]; - int tls_port; + + int ssl_enable; + int http_port; + int https_port; + int daemonize; char cert_file[512]; char key_file[512]; + char pid_file[512]; char smart_conf[512]; char smart_perl_path[512]; @@ -37,6 +42,27 @@ typedef struct { static FILE *g_log_fp = NULL; static int g_log_level = LOG_LEVEL_DEFAULT; +static volatile sig_atomic_t g_terminate = 0; +static volatile sig_atomic_t g_got_signal = 0; + +static int g_pid_file_written = 0; +static char g_pid_file_path[512] = { 0 }; + +/* ------------------------------------------------------------ */ +/* Console lifecycle output */ +/* ------------------------------------------------------------ */ + +static void console_msg(const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fputc('\n', stderr); + fflush(stderr); +} + /* ------------------------------------------------------------ */ /* Logging */ /* ------------------------------------------------------------ */ @@ -59,12 +85,13 @@ static void log_msg(int level, const char *fmt, ...) { time_t now = time(NULL); struct tm tm_now; - localtime_r(&now, &tm_now); - char tbuf[64]; + const char *lvl = "INFO"; + va_list ap; + + localtime_r(&now, &tm_now); strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm_now); - const char *lvl = "INFO"; if (level == LOG_LEVEL_ERROR) { lvl = "ERROR"; } else if (level == LOG_LEVEL_DEBUG) { @@ -73,7 +100,6 @@ static void log_msg(int level, const char *fmt, ...) { fprintf(g_log_fp, "[%s] [%s] ", tbuf, lvl); - va_list ap; va_start(ap, fmt); vfprintf(g_log_fp, fmt, ap); va_end(ap); @@ -99,14 +125,17 @@ static void ssl_die(const char *msg) { static void trim(char *s) { char *p = s; + size_t len; + while (*p && isspace((unsigned char)*p)) { p++; } + if (p != s) { memmove(s, p, strlen(p) + 1); } - size_t len = strlen(s); + len = strlen(s); while (len > 0 && isspace((unsigned char)s[len - 1])) { s[len - 1] = '\0'; len--; @@ -115,6 +144,7 @@ static void trim(char *s) { static void strip_quotes(char *s) { size_t len = strlen(s); + if (len >= 2) { if ((s[0] == '\'' && s[len - 1] == '\'') || (s[0] == '"' && s[len - 1] == '"')) { @@ -125,12 +155,9 @@ static void strip_quotes(char *s) { } static int parse_perl_assignment(const char *line, char *key, size_t ksz, char *val, size_t vsz) { - /* Expected format: - $variable = 'value'; - $variable = 1234; - */ - const char *p = line; + size_t ki = 0; + size_t vi = 0; while (*p && isspace((unsigned char)*p)) { p++; @@ -140,7 +167,6 @@ static int parse_perl_assignment(const char *line, char *key, size_t ksz, char * } p++; - size_t ki = 0; while (*p && (isalnum((unsigned char)*p) || *p == '_')) { if (ki + 1 < ksz) { key[ki++] = *p; @@ -161,7 +187,6 @@ static int parse_perl_assignment(const char *line, char *key, size_t ksz, char * p++; } - size_t vi = 0; while (*p && *p != ';' && *p != '\n' && *p != '\r') { if (vi + 1 < vsz) { val[vi++] = *p; @@ -192,6 +217,180 @@ static int set_nonblocking(int fd) { return 0; } +static void print_usage(const char *progname) { + fprintf(stdout, + "%s %s\n" + "\n" + "Usage: %s [-h] [-d] [-s] [-c ] [-p ]\n" + "\n" + "Options:\n" + " -h, --help Show this help text and exit\n" + " -d, --daemon Run in daemon mode\n" + " -s, --stop Stop the running nwwebui instance\n" + " -c, --config Use an alternate smart.conf path\n" + " -p, --pidfile Override PID file path\n", + NWWEBUI_NAME, NWWEBUI_VERSION, progname); +} + +/* ------------------------------------------------------------ */ +/* PID file handling */ +/* ------------------------------------------------------------ */ + +static void remove_pid_file(void) { + if (g_pid_file_written) { + unlink(g_pid_file_path); + g_pid_file_written = 0; + } +} + +static void write_pid_file(const char *path) { + FILE *fp = fopen(path, "w"); + if (!fp) { + die("fopen pid file"); + } + + fprintf(fp, "%ld\n", (long)getpid()); + fclose(fp); + + snprintf(g_pid_file_path, sizeof(g_pid_file_path), "%s", path); + g_pid_file_written = 1; +} + +static pid_t read_pid_file(const char *path) { + FILE *fp; + long pid = -1; + + fp = fopen(path, "r"); + if (!fp) { + return -1; + } + + if (fscanf(fp, "%ld", &pid) != 1) { + fclose(fp); + return -1; + } + + fclose(fp); + + if (pid <= 0) { + return -1; + } + + return (pid_t)pid; +} + +static int process_exists(pid_t pid) { + if (pid <= 0) { + return 0; + } + + if (kill(pid, 0) == 0) { + return 1; + } + + if (errno == EPERM) { + return 1; + } + + return 0; +} + +static int pid_is_nwwebui(pid_t pid) { + char path[128]; + char name[256]; + FILE *fp; + + snprintf(path, sizeof(path), "/proc/%ld/comm", (long)pid); + + fp = fopen(path, "r"); + if (!fp) { + return 0; + } + + if (!fgets(name, sizeof(name), fp)) { + fclose(fp); + return 0; + } + + fclose(fp); + + name[strcspn(name, "\r\n")] = '\0'; + + return strcmp(name, "nwwebui") == 0; +} + +static int stop_running_instance(const char *pid_file) { + pid_t pid = read_pid_file(pid_file); + + if (pid <= 0) { + fprintf(stderr, "Could not read PID file: %s\n", pid_file); + return EXIT_FAILURE; + } + + if (!process_exists(pid)) { + fprintf(stderr, "No running process found for PID %ld\n", (long)pid); + unlink(pid_file); + return EXIT_FAILURE; + } + + if (!pid_is_nwwebui(pid)) { + fprintf(stderr, "PID %ld is not a nwwebui process\n", (long)pid); + return EXIT_FAILURE; + } + + if (kill(pid, SIGTERM) < 0) { + fprintf(stderr, "Failed to stop nwwebui with PID %ld: %s\n", + (long)pid, strerror(errno)); + return EXIT_FAILURE; + } + + fprintf(stderr, "Sent SIGTERM to %s %s with PID %ld\n", + NWWEBUI_NAME, NWWEBUI_VERSION, (long)pid); + return EXIT_SUCCESS; +} + +/* ------------------------------------------------------------ */ +/* Daemonization */ +/* ------------------------------------------------------------ */ + +static void daemonize_process(void) { + pid_t pid; + + pid = fork(); + if (pid < 0) { + die("fork"); + } + if (pid > 0) { + exit(EXIT_SUCCESS); + } + + if (setsid() < 0) { + die("setsid"); + } + + pid = fork(); + if (pid < 0) { + die("fork"); + } + if (pid > 0) { + exit(EXIT_SUCCESS); + } + + if (chdir("/") < 0) { + die("chdir"); + } + + umask(0); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + open("/dev/null", O_RDONLY); + open("/dev/null", O_WRONLY); + open("/dev/null", O_WRONLY); +} + /* ------------------------------------------------------------ */ /* smart.conf loading */ /* ------------------------------------------------------------ */ @@ -200,25 +399,29 @@ static void init_defaults(nw_config_t *cfg, const char *smart_conf_path) { memset(cfg, 0, sizeof(*cfg)); snprintf(cfg->bind_ip, sizeof(cfg->bind_ip), "%s", DEFAULT_BIND_IP); - cfg->tls_port = DEFAULT_TLS_PORT; + + cfg->ssl_enable = DEFAULT_SSL_ENABLE; + cfg->http_port = DEFAULT_HTTP_PORT; + cfg->https_port = DEFAULT_HTTPS_PORT; + cfg->daemonize = DEFAULT_DAEMONIZE; snprintf(cfg->cert_file, sizeof(cfg->cert_file), "%s", DEFAULT_CERT_FILE); snprintf(cfg->key_file, sizeof(cfg->key_file), "%s", DEFAULT_KEY_FILE); + snprintf(cfg->pid_file, sizeof(cfg->pid_file), "%s", DEFAULT_PID_FILE); - /* Use the built-in default path unless smart.conf overrides it */ snprintf(cfg->smart_perl_path, sizeof(cfg->smart_perl_path), "%s", DEFAULT_SMART_PERL); - snprintf(cfg->smart_conf, sizeof(cfg->smart_conf), "%s", smart_conf_path); } static void load_smart_conf(nw_config_t *cfg) { FILE *fp = fopen(cfg->smart_conf, "r"); + char line[2048]; + if (!fp) { log_msg(LOG_LEVEL_ERROR, "Could not open smart.conf: %s", cfg->smart_conf); die("fopen smart.conf"); } - char line[2048]; while (fgets(line, sizeof(line), fp)) { char key[256]; char val[1024]; @@ -229,42 +432,62 @@ static void load_smart_conf(nw_config_t *cfg) { if (strcmp(key, "nw_bind_ip") == 0) { snprintf(cfg->bind_ip, sizeof(cfg->bind_ip), "%s", val); - } else if (strcmp(key, "nw_tls_port") == 0) { - cfg->tls_port = atoi(val); + } else if (strcmp(key, "nw_log_level") == 0) { + g_log_level = atoi(val); + } else if (strcmp(key, "nw_ssl_enable") == 0) { + cfg->ssl_enable = atoi(val); + } else if (strcmp(key, "nw_http_port") == 0) { + cfg->http_port = atoi(val); + } else if (strcmp(key, "nw_https_port") == 0) { + cfg->https_port = atoi(val); + } else if (strcmp(key, "nw_daemonize") == 0) { + cfg->daemonize = atoi(val); + } else if (strcmp(key, "nw_pid_file") == 0) { + snprintf(cfg->pid_file, sizeof(cfg->pid_file), "%s", val); } else if (strcmp(key, "nw_cert_file") == 0) { snprintf(cfg->cert_file, sizeof(cfg->cert_file), "%s", val); } else if (strcmp(key, "nw_key_file") == 0) { snprintf(cfg->key_file, sizeof(cfg->key_file), "%s", val); } else if (strcmp(key, "smart_perl_path") == 0) { - /* Override the default path only if smart.conf defines it */ snprintf(cfg->smart_perl_path, sizeof(cfg->smart_perl_path), "%s", val); } } fclose(fp); - if (cfg->tls_port <= 0) { - cfg->tls_port = DEFAULT_TLS_PORT; + if (cfg->http_port < 0) { + cfg->http_port = DEFAULT_HTTP_PORT; + } + if (cfg->https_port < 0) { + cfg->https_port = DEFAULT_HTTPS_PORT; + } + + if (g_log_level < LOG_LEVEL_ERROR) { + g_log_level = LOG_LEVEL_DEFAULT; + } + if (g_log_level > LOG_LEVEL_DEBUG) { + g_log_level = LOG_LEVEL_DEBUG; } } /* ------------------------------------------------------------ */ -/* Listener / TLS */ +/* Listener / socket helpers */ /* ------------------------------------------------------------ */ static int create_listener(const char *bind_ip, int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); + int yes = 1; + struct sockaddr_in addr; + if (fd < 0) { die("socket"); } - int yes = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { close(fd); die("setsockopt(SO_REUSEADDR)"); } - struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port); @@ -340,6 +563,7 @@ static ssize_t send_all_ssl(SSL *ssl, const unsigned char *buf, size_t len) { static pid_t spawn_smart_perl(const nw_config_t *cfg, int *child_stdin_fd, int *child_stdout_fd) { int inpipe[2] = { -1, -1 }; int outpipe[2] = { -1, -1 }; + pid_t pid; if (pipe(inpipe) < 0) { die("pipe stdin"); @@ -351,7 +575,7 @@ static pid_t spawn_smart_perl(const nw_config_t *cfg, int *child_stdin_fd, int * die("pipe stdout"); } - pid_t pid = fork(); + pid = fork(); if (pid < 0) { close(inpipe[0]); close(inpipe[1]); @@ -361,11 +585,6 @@ static pid_t spawn_smart_perl(const nw_config_t *cfg, int *child_stdin_fd, int * } if (pid == 0) { - /* Child process: - stdin <- parent writes client request data here - stdout -> parent reads response data here - stderr -> merged into stdout for logging/debugging - */ if (dup2(inpipe[0], STDIN_FILENO) < 0) { perror("dup2 stdin"); _exit(127); @@ -400,16 +619,138 @@ static pid_t spawn_smart_perl(const nw_config_t *cfg, int *child_stdin_fd, int * } /* ------------------------------------------------------------ */ -/* Connection handler */ +/* Shared child cleanup */ /* ------------------------------------------------------------ */ -static void handle_connection(SSL_CTX *ctx, int client_fd, const nw_config_t *cfg) { +static void cleanup_child_process(int *child_stdin_fd, int *child_stdout_fd, pid_t *child_pid) { + if (*child_stdin_fd >= 0) { + close(*child_stdin_fd); + *child_stdin_fd = -1; + } + + if (*child_stdout_fd >= 0) { + close(*child_stdout_fd); + *child_stdout_fd = -1; + } + + if (*child_pid > 0) { + int status = 0; + pid_t w = waitpid(*child_pid, &status, WNOHANG); + + if (w == 0) { + kill(*child_pid, SIGTERM); + waitpid(*child_pid, &status, 0); + } else if (w < 0 && errno != ECHILD) { + log_msg(LOG_LEVEL_ERROR, "waitpid failed for smart child: %s", strerror(errno)); + } + + *child_pid = -1; + } +} + +/* ------------------------------------------------------------ */ +/* Plain HTTP handler */ +/* ------------------------------------------------------------ */ + +static void handle_connection_plain(int client_fd, const nw_config_t *cfg) { + int child_stdin_fd = -1; + int child_stdout_fd = -1; + pid_t child_pid = -1; + unsigned char buf[NW_BUF_SZ]; + + child_pid = spawn_smart_perl(cfg, &child_stdin_fd, &child_stdout_fd); + + if (set_nonblocking(client_fd) < 0) { + log_msg(LOG_LEVEL_ERROR, "Could not set client socket non-blocking"); + goto cleanup; + } + + if (set_nonblocking(child_stdout_fd) < 0) { + log_msg(LOG_LEVEL_ERROR, "Could not set child stdout non-blocking"); + goto cleanup; + } + + for (;;) { + struct pollfd pfds[2]; + int pr; + + pfds[0].fd = client_fd; + pfds[0].events = POLLIN | POLLHUP | POLLERR; + pfds[0].revents = 0; + + pfds[1].fd = child_stdout_fd; + pfds[1].events = POLLIN | POLLHUP | POLLERR; + pfds[1].revents = 0; + + pr = poll(pfds, 2, -1); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + log_msg(LOG_LEVEL_ERROR, "poll failed in plain handler: %s", strerror(errno)); + break; + } + + if (pfds[0].revents & (POLLIN | POLLHUP | POLLERR)) { + ssize_t n = read(client_fd, buf, sizeof(buf)); + if (n > 0) { + if (write_all_fd(child_stdin_fd, buf, (size_t)n) < 0) { + log_msg(LOG_LEVEL_ERROR, "Write to Perl stdin failed"); + break; + } + } else if (n == 0) { + break; + } else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + log_msg(LOG_LEVEL_ERROR, "Read from HTTP client failed: %s", strerror(errno)); + break; + } + } + + if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) { + ssize_t rn = read(child_stdout_fd, buf, sizeof(buf)); + if (rn > 0) { + if (write_all_fd(client_fd, buf, (size_t)rn) < 0) { + log_msg(LOG_LEVEL_ERROR, "Write to HTTP client failed"); + break; + } + } else if (rn == 0) { + break; + } else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + log_msg(LOG_LEVEL_ERROR, "Read from Perl stdout failed: %s", strerror(errno)); + break; + } + } + + if (child_pid > 0) { + int status = 0; + pid_t w = waitpid(child_pid, &status, WNOHANG); + if (w == child_pid) { + break; + } + } + } + +cleanup: + cleanup_child_process(&child_stdin_fd, &child_stdout_fd, &child_pid); + close(client_fd); +} + +/* ------------------------------------------------------------ */ +/* TLS handler */ +/* ------------------------------------------------------------ */ + +static void handle_connection_tls(SSL_CTX *ctx, int client_fd, const nw_config_t *cfg) { SSL *ssl = NULL; int child_stdin_fd = -1; int child_stdout_fd = -1; pid_t child_pid = -1; unsigned char buf[NW_BUF_SZ]; + if (set_nonblocking(client_fd) < 0) { + log_msg(LOG_LEVEL_ERROR, "Could not set client socket non-blocking"); + goto cleanup; + } + ssl = SSL_new(ctx); if (!ssl) { log_msg(LOG_LEVEL_ERROR, "SSL_new failed"); @@ -434,31 +775,46 @@ static void handle_connection(SSL_CTX *ctx, int client_fd, const nw_config_t *cf } for (;;) { - struct pollfd pfd; - pfd.fd = child_stdout_fd; - pfd.events = POLLIN | POLLHUP | POLLERR; - pfd.revents = 0; + struct pollfd pfds[2]; + int pr; - /* Read from TLS client, decrypt data, and forward it to Perl stdin */ - int n = SSL_read(ssl, buf, sizeof(buf)); - if (n > 0) { - if (write_all_fd(child_stdin_fd, buf, (size_t)n) < 0) { - log_msg(LOG_LEVEL_ERROR, "Write to Perl stdin failed"); - break; + pfds[0].fd = client_fd; + pfds[0].events = POLLIN | POLLHUP | POLLERR; + pfds[0].revents = 0; + + pfds[1].fd = child_stdout_fd; + pfds[1].events = POLLIN | POLLHUP | POLLERR; + pfds[1].revents = 0; + + pr = poll(pfds, 2, -1); + if (pr < 0) { + if (errno == EINTR) { + continue; } - } else { - int err = SSL_get_error(ssl, n); - if (err == SSL_ERROR_ZERO_RETURN) { - break; - } - if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { - break; + log_msg(LOG_LEVEL_ERROR, "poll failed in TLS handler: %s", strerror(errno)); + break; + } + + if ((pfds[0].revents & (POLLIN | POLLHUP | POLLERR)) || SSL_pending(ssl) > 0) { + int n = SSL_read(ssl, buf, sizeof(buf)); + if (n > 0) { + if (write_all_fd(child_stdin_fd, buf, (size_t)n) < 0) { + log_msg(LOG_LEVEL_ERROR, "Write to Perl stdin failed"); + break; + } + } else { + int err = SSL_get_error(ssl, n); + if (err == SSL_ERROR_ZERO_RETURN) { + break; + } + if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { + log_msg(LOG_LEVEL_ERROR, "SSL_read failed"); + break; + } } } - /* Read any output from the Perl process and send it back through TLS */ - int pr = poll(&pfd, 1, 10); - if (pr > 0 && (pfd.revents & (POLLIN | POLLHUP | POLLERR))) { + if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) { ssize_t rn = read(child_stdout_fd, buf, sizeof(buf)); if (rn > 0) { if (send_all_ssl(ssl, buf, (size_t)rn) < 0) { @@ -468,11 +824,11 @@ static void handle_connection(SSL_CTX *ctx, int client_fd, const nw_config_t *cf } else if (rn == 0) { break; } else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + log_msg(LOG_LEVEL_ERROR, "Read from Perl stdout failed: %s", strerror(errno)); break; } } - /* Exit if the child process is already gone */ if (child_pid > 0) { int status = 0; pid_t w = waitpid(child_pid, &status, WNOHANG); @@ -483,21 +839,7 @@ static void handle_connection(SSL_CTX *ctx, int client_fd, const nw_config_t *cf } cleanup: - if (child_stdin_fd >= 0) { - close(child_stdin_fd); - } - if (child_stdout_fd >= 0) { - close(child_stdout_fd); - } - - if (child_pid > 0) { - int status = 0; - pid_t w = waitpid(child_pid, &status, WNOHANG); - if (w == 0) { - kill(child_pid, SIGTERM); - waitpid(child_pid, &status, 0); - } - } + cleanup_child_process(&child_stdin_fd, &child_stdout_fd, &child_pid); if (ssl) { SSL_shutdown(ssl); @@ -507,112 +849,293 @@ cleanup: close(client_fd); } +/* ------------------------------------------------------------ */ +/* Signal handlers */ +/* ------------------------------------------------------------ */ + static void reap_children(int sig) { (void)sig; - while (waitpid(-1, NULL, WNOHANG) > 0) { + + for (;;) { + pid_t pid = waitpid(-1, NULL, WNOHANG); + if (pid <= 0) { + break; + } } } +static void term_handler(int sig) { + g_got_signal = sig; + g_terminate = 1; +} + /* ------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------ */ int main(int argc, char **argv) { const char *smart_conf_path = DEFAULT_SMART_CONF; + const char *pidfile_override = NULL; + int http_fd = -1; + int https_fd = -1; + SSL_CTX *ctx = NULL; + int force_daemon = 0; + int stop_mode = 0; + int i; - if (argc > 1) { - smart_conf_path = argv[1]; + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return EXIT_SUCCESS; + } else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--daemon") == 0) { + force_daemon = 1; + } else if (strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--stop") == 0) { + stop_mode = 1; + } else if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) && i + 1 < argc) { + smart_conf_path = argv[++i]; + } else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--pidfile") == 0) && i + 1 < argc) { + pidfile_override = argv[++i]; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + + { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = reap_children; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; + + if (sigaction(SIGCHLD, &sa, NULL) < 0) { + die("sigaction(SIGCHLD)"); + } + } + + { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = term_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + if (sigaction(SIGTERM, &sa, NULL) < 0) { + die("sigaction(SIGTERM)"); + } + if (sigaction(SIGINT, &sa, NULL) < 0) { + die("sigaction(SIGINT)"); + } + if (sigaction(SIGHUP, &sa, NULL) < 0) { + die("sigaction(SIGHUP)"); + } } - signal(SIGCHLD, reap_children); signal(SIGPIPE, SIG_IGN); - log_open(); - log_msg(LOG_LEVEL_INFO, "nwwebui starting"); + console_msg("%s %s starting", NWWEBUI_NAME, NWWEBUI_VERSION); - nw_config_t cfg; - init_defaults(&cfg, smart_conf_path); - load_smart_conf(&cfg); + { + nw_config_t cfg; + init_defaults(&cfg, smart_conf_path); + load_smart_conf(&cfg); - if (!file_exists_and_executable(cfg.smart_perl_path)) { - log_msg(LOG_LEVEL_ERROR, - "SMArT Perl program is missing or not executable: %s", - cfg.smart_perl_path); - return EXIT_FAILURE; - } + if (force_daemon) { + cfg.daemonize = 1; + } - log_msg(LOG_LEVEL_INFO, "Using SMArT Perl path: %s", cfg.smart_perl_path); + if (pidfile_override) { + snprintf(cfg.pid_file, sizeof(cfg.pid_file), "%s", pidfile_override); + } - log_msg(LOG_LEVEL_INFO, - "Config loaded: bind=%s:%d cert=%s key=%s smart.conf=%s", - cfg.bind_ip, - cfg.tls_port, - cfg.cert_file, - cfg.key_file, - cfg.smart_conf); + if (stop_mode) { + return stop_running_instance(cfg.pid_file); + } - SSL_library_init(); - SSL_load_error_strings(); - OpenSSL_add_ssl_algorithms(); + if (!file_exists_and_executable(cfg.smart_perl_path)) { + log_msg(LOG_LEVEL_ERROR, + "SMArT Perl program is missing or not executable: %s", + cfg.smart_perl_path); + return EXIT_FAILURE; + } - SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); - if (!ctx) { - ssl_die("SSL_CTX_new failed"); - } + log_msg(LOG_LEVEL_INFO, "%s version %s starting", NWWEBUI_NAME, NWWEBUI_VERSION); + log_msg(LOG_LEVEL_INFO, "Using SMArT Perl path: %s", cfg.smart_perl_path); + log_msg(LOG_LEVEL_INFO, + "%s version %s config loaded: bind=%s log_level=%d ssl_enable=%d http_port=%d https_port=%d daemonize=%d cert=%s key=%s pid_file=%s smart.conf=%s", + NWWEBUI_NAME, + NWWEBUI_VERSION, + cfg.bind_ip, + g_log_level, + cfg.ssl_enable, + cfg.http_port, + cfg.https_port, + cfg.daemonize, + cfg.cert_file, + cfg.key_file, + cfg.pid_file, + cfg.smart_conf); - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + if (cfg.daemonize) { + console_msg("%s %s switching to daemon mode", NWWEBUI_NAME, NWWEBUI_VERSION); + daemonize_process(); + } - if (SSL_CTX_use_certificate_file(ctx, cfg.cert_file, SSL_FILETYPE_PEM) <= 0) { - ssl_die("Could not load certificate file"); - } + write_pid_file(cfg.pid_file); + atexit(remove_pid_file); + log_msg(LOG_LEVEL_INFO, "Wrote PID file: %s", cfg.pid_file); - if (SSL_CTX_use_PrivateKey_file(ctx, cfg.key_file, SSL_FILETYPE_PEM) <= 0) { - ssl_die("Could not load private key file"); - } + if (cfg.ssl_enable) { + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); - if (!SSL_CTX_check_private_key(ctx)) { - ssl_die("Private key does not match certificate"); - } + ctx = SSL_CTX_new(TLS_server_method()); + if (!ctx) { + ssl_die("SSL_CTX_new failed"); + } - int listen_fd = create_listener(cfg.bind_ip, cfg.tls_port); + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); - log_msg(LOG_LEVEL_INFO, - "TLS wrapper listening on %s:%d and launching %s", - cfg.bind_ip, - cfg.tls_port, - cfg.smart_perl_path); + if (SSL_CTX_use_certificate_file(ctx, cfg.cert_file, SSL_FILETYPE_PEM) <= 0) { + ssl_die("Could not load certificate file"); + } - for (;;) { - struct sockaddr_in peer; - socklen_t peerlen = sizeof(peer); + if (SSL_CTX_use_PrivateKey_file(ctx, cfg.key_file, SSL_FILETYPE_PEM) <= 0) { + ssl_die("Could not load private key file"); + } - int client_fd = accept(listen_fd, (struct sockaddr *)&peer, &peerlen); - if (client_fd < 0) { - if (errno == EINTR) { + if (!SSL_CTX_check_private_key(ctx)) { + ssl_die("Private key does not match certificate"); + } + } + + if (cfg.http_port > 0) { + http_fd = create_listener(cfg.bind_ip, cfg.http_port); + log_msg(LOG_LEVEL_INFO, + "HTTP listener active on %s:%d", + cfg.bind_ip, + cfg.http_port); + } + + if (cfg.ssl_enable && cfg.https_port > 0) { + https_fd = create_listener(cfg.bind_ip, cfg.https_port); + log_msg(LOG_LEVEL_INFO, + "HTTPS listener active on %s:%d", + cfg.bind_ip, + cfg.https_port); + } + + if (http_fd < 0 && https_fd < 0) { + log_msg(LOG_LEVEL_ERROR, "No listener is active"); + if (ctx) { + SSL_CTX_free(ctx); + } + return EXIT_FAILURE; + } + + console_msg("%s %s started", NWWEBUI_NAME, NWWEBUI_VERSION); + + for (;;) { + struct pollfd pfds[2]; + int nfds = 0; + int pr; + + if (g_terminate) { + console_msg("%s received signal %d, shutting down", NWWEBUI_NAME, g_got_signal); + log_msg(LOG_LEVEL_INFO, "Termination requested by signal %d", g_got_signal); + break; + } + + if (http_fd >= 0) { + pfds[nfds].fd = http_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + nfds++; + } + + if (https_fd >= 0) { + pfds[nfds].fd = https_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + nfds++; + } + + pr = poll(pfds, nfds, 1000); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + log_msg(LOG_LEVEL_ERROR, "poll failed: %s", strerror(errno)); continue; } - log_msg(LOG_LEVEL_ERROR, "accept failed: %s", strerror(errno)); - continue; - } - pid_t pid = fork(); - if (pid < 0) { - log_msg(LOG_LEVEL_ERROR, "fork failed: %s", strerror(errno)); - close(client_fd); - continue; - } + if (pr == 0) { + continue; + } - if (pid == 0) { - close(listen_fd); - handle_connection(ctx, client_fd, &cfg); - SSL_CTX_free(ctx); - _exit(0); - } + for (i = 0; i < nfds; i++) { + struct sockaddr_in peer; + socklen_t peerlen = sizeof(peer); + int client_fd; + pid_t pid; - close(client_fd); + if (!(pfds[i].revents & POLLIN)) { + continue; + } + + client_fd = accept(pfds[i].fd, (struct sockaddr *)&peer, &peerlen); + if (client_fd < 0) { + if (errno == EINTR) { + continue; + } + log_msg(LOG_LEVEL_ERROR, "accept failed: %s", strerror(errno)); + continue; + } + + pid = fork(); + if (pid < 0) { + log_msg(LOG_LEVEL_ERROR, "fork failed: %s", strerror(errno)); + close(client_fd); + continue; + } + + if (pid == 0) { + if (http_fd >= 0) { + close(http_fd); + } + if (https_fd >= 0) { + close(https_fd); + } + + if (pfds[i].fd == https_fd) { + handle_connection_tls(ctx, client_fd, &cfg); + } else { + handle_connection_plain(client_fd, &cfg); + } + + if (ctx) { + SSL_CTX_free(ctx); + } + _exit(0); + } + + close(client_fd); + } + } } - close(listen_fd); - SSL_CTX_free(ctx); + if (http_fd >= 0) { + close(http_fd); + } + if (https_fd >= 0) { + close(https_fd); + } + if (ctx) { + SSL_CTX_free(ctx); + } + + console_msg("%s stopped", NWWEBUI_NAME); return 0; } diff --git a/readconfig.pl b/readconfig.pl index 406b6de..fe8e0db 100644 --- a/readconfig.pl +++ b/readconfig.pl @@ -1,28 +1,12 @@ -# # SMArT -# +# # Configuration file code -# +# # Copyright 2001 Wilmer van der Gaast (lintux@lintux.cx) -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# # +# Updated for marker-aware config writing. -my( @info, @conf, $l ); +my( @info, @conf, @rawconf, $l ); $info[1] = 'Volume'; $info[2] = 'Server name'; @@ -53,6 +37,7 @@ $info[47] = 'Trustee files'; $info[50] = 'Conversion tables'; $info[60] = 'MAX_CONNECTIONS'; $info[61] = 'MAX_NW_VOLS'; +$info[62] = 'Reserved'; $info[63] = 'MAX_DIR_BASE_ENTRIES'; $info[68] = 'USE_MMAP'; $info[69] = 'HANDLE_ALL_SAP_TYPS'; @@ -79,19 +64,25 @@ $info[400] = 'nwserv.stations file'; $info[401] = 'Reply to nearest server requests'; $info[402] = 'Reply to connect requests'; -open( CONF, '<' . $mars_config ); +open( CONF, '<' . $mars_config ) or die "Could not open $mars_config: $!"; @conf = (); +@rawconf = (); + while( $l = ) { - $l =~ s/[\r\n]//g; - $l =~ s/[\t ]+/ /g; - $l =~ s/#.*//; - $l =~ s/^[\t ]*//; - $l =~ s/[\t ]*$//; - if( $l ne '' ) - { - unshift( @conf, $l ); - } + push( @rawconf, $l ); + + my $x = $l; + $x =~ s/[\r\n]//g; + $x =~ s/[\t ]+/ /g; + $x =~ s/#.*//; + $x =~ s/^[\t ]*//; + $x =~ s/[\t ]*$//; + + if( $x ne '' ) + { + push( @conf, $x ); + } } close( CONF ); @@ -99,53 +90,279 @@ sortconfig(); sub sortconfig() { - @conf = sort( { $a cmp $b } @conf ); - @conf = sort( { ( split( ' ', $a ) )[0] <=> ( split( ' ', $b ) )[0] } @conf ); + @conf = sort( { $a cmp $b } @conf ); + @conf = sort( { ( split( ' ', $a ) )[0] <=> ( split( ' ', $b ) )[0] } @conf ); } sub getconfigline( $ ) { - my( @c, $c ); - - @c = getconfig( @_ ); - $c = $c[0]; - $c =~ s/^[0-9]* //; - return( $c ); + my( @c, $c ); + + @c = getconfig( @_ ); + $c = $c[0]; + $c =~ s/^[0-9]* //; + return( $c ); } sub getconfig( $ ) { - my( @c ); - - @c = grep( /^$_[0] /i, @conf ); - return( @c ); + my( @c ); + + @c = grep( /^$_[0] /i, @conf ); + return( @c ); } sub addconfigline( $ ) { - unshift( @conf, $_[0] ); + unshift( @conf, $_[0] ); } sub delconfigline( $ ) { - @conf = grep( !/^$_[0] /i, grep( !/^$_[0]$/i, @conf ) ); + @conf = grep( !/^$_[0] /i, grep( !/^$_[0]$/i, @conf ) ); +} + +sub normalize_line( $ ) +{ + my $x = $_[0]; + + $x =~ s/[\r\n]//g; + $x =~ s/[\t ]+/ /g; + $x =~ s/#.*//; + $x =~ s/^[\t ]*//; + $x =~ s/[\t ]*$//; + + return( $x ); +} + +sub section_of_line( $ ) +{ + my $x = normalize_line( $_[0] ); + + if( $x =~ /^([0-9]+)\b/ ) + { + return( $1 ); + } + + return( '' ); +} + +sub grouped_section_key( $ ) +{ + my $sec = $_[0]; + + if( $sec >= 100 && $sec <= 106 ) { return '100-106'; } + if( $sec >= 200 && $sec <= 202 ) { return '200-202'; } + if( $sec >= 210 && $sec <= 211 ) { return '210-211'; } + if( $sec >= 300 && $sec <= 302 ) { return '300-302'; } + + return $sec; +} + +sub build_marker_map() +{ + my( %map, $line, $sec, $key ); + + foreach $line ( @conf ) + { + $sec = section_of_line( $line ); + next if $sec eq ''; + + $key = grouped_section_key( $sec ); + push( @{ $map{$key} }, $line ); + } + + # Keep SYS as first entry in section 1 + if( defined( $map{'1'} ) ) + { + my @sys = grep( /^1 SYS /, @{ $map{'1'} } ); + my @rest = grep( !/^1 SYS /, @{ $map{'1'} } ); + $map{'1'} = [ @sys, @rest ]; + } + + return %map; +} + +sub writeconfig_compact() +{ + my( $i, $l ); + + sortconfig(); + + open( CONF, '>' . $mars_config ) or die "Could not write $mars_config: $!"; + + $l = ( getconfig( '1 SYS' ) )[0]; + delconfigline( '1 SYS' ); + addconfigline( $l ); + + foreach $i ( @conf ) + { + $l = $i; + $l =~ s/ .*//; + printf( CONF "%-50s # %s\n", $i, $info[$l] ); + } + + close( CONF ); +} + +sub writeconfig_markers() +{ + my( %secmap, %emitted ); + my( $line, $active_key, $inside_active ); + + %secmap = build_marker_map(); + $inside_active = ''; + + open( CONF, '>' . $mars_config ) or die "Could not write $mars_config: $!"; + + foreach $line ( @rawconf ) + { + if( $line =~ /^\s*#\s*>>>\s*SMARTHOOK\s+SECTION\s+([0-9]+(?:-[0-9]+)?)\s+ACTIVE\s+BEGIN/i ) + { + $active_key = $1; + $inside_active = $active_key; + + print CONF $line; + + if( defined( $secmap{$active_key} ) ) + { + foreach my $entry ( @{ $secmap{$active_key} } ) + { + print CONF $entry . "\n"; + } + } + + $emitted{$active_key} = 1; + next; + } + + if( $line =~ /^\s*#\s*<<<\s*SMARTHOOK\s+SECTION\s+([0-9]+(?:-[0-9]+)?)\s+ACTIVE\s+END/i ) + { + $inside_active = ''; + print CONF $line; + next; + } + + if( $inside_active ne '' ) + { + # Skip old content inside ACTIVE blocks completely. + next; + } + + print CONF $line; + } + + close( CONF ); +} + +sub writeconfig_preserve_layout() +{ + my( %secmap, %written, @sections, $sec, $line, $sysline ); + my( $heading_sec ); + + sortconfig(); + + $sysline = ( grep( /^1 SYS /, @conf ) )[0]; + if( defined( $sysline ) ) + { + @conf = grep( $_ ne $sysline, @conf ); + unshift( @conf, $sysline ); + } + + foreach $line ( @conf ) + { + $sec = section_of_line( $line ); + if( $sec ne '' ) + { + push( @{ $secmap{$sec} }, $line ); + } + } + + open( CONF, '>' . $mars_config ) or die "Could not write $mars_config: $!"; + + foreach $line ( @rawconf ) + { + if( $line =~ /^\s*#.*Section\s+([0-9]+)\b/i ) + { + $heading_sec = $1; + + foreach $sec ( sort { $a <=> $b } keys( %secmap ) ) + { + next if $written{$sec}; + next if $sec > $heading_sec; + + if( defined( $secmap{$sec} ) ) + { + foreach my $entry ( @{ $secmap{$sec} } ) + { + print CONF $entry . "\n"; + } + } + $written{$sec} = 1; + } + } + + $sec = section_of_line( $line ); + + if( $sec eq '' ) + { + print CONF $line; + next; + } + + if( ! $written{$sec} ) + { + if( defined( $secmap{$sec} ) ) + { + foreach my $entry ( @{ $secmap{$sec} } ) + { + print CONF $entry . "\n"; + } + } + $written{$sec} = 1; + } + } + + @sections = sort { $a <=> $b } keys( %secmap ); + foreach $sec ( @sections ) + { + next if $written{$sec}; + + print CONF "\n"; + foreach my $entry ( @{ $secmap{$sec} } ) + { + print CONF $entry . "\n"; + } + $written{$sec} = 1; + } + + close( CONF ); +} + +sub config_has_smart_markers() +{ + foreach my $line ( @rawconf ) + { + if( $line =~ /SMARTHOOK\s+SECTION/i ) + { + return 1; + } + } + return 0; } sub writeconfig() { - my( $i, $l ); - - sortconfig(); - - open( CONF, '>' . $mars_config ); - $l = ( getconfig( '1 SYS' ) )[0]; - delconfigline( '1 SYS' ); - addconfigline( $l ); - foreach $i ( @conf ) - { - $l = $i; - $l =~ s/ .*//; - printf( CONF "%-50s # %s\n", $i, $info[$l] ); - } - close( CONF ); + if( defined( $smart_compact_nwservconf ) && $smart_compact_nwservconf ) + { + writeconfig_compact(); + } + elsif( config_has_smart_markers() ) + { + writeconfig_markers(); + } + else + { + writeconfig_preserve_layout(); + } } diff --git a/smart.conf.cmake b/smart.conf.cmake index 494f061..1d716c4 100644 --- a/smart.conf.cmake +++ b/smart.conf.cmake @@ -1,24 +1,118 @@ +# SMArT / nwwebui configuration file + +# ------------------------------------------------------------ +# UI colors +# ------------------------------------------------------------ + +# Background color used for the main page body. $COLOR_BACK = "#F0F0FF"; + +# Background color used for section headers. $COLOR_HEAD_BACK = "#C0C0FF"; + +# Text color used for section headers. $COLOR_HEAD_FORE = "#000000"; + +# Background color used for subsection headers. $COLOR_SUBH_BACK = "#D0D0FF"; + +# Text color used for subsection headers. $COLOR_SUBH_FORE = "#000000"; + +# Background color used for normal content rows. $COLOR_TEXT_BACK = "#E0E0FF"; + +# Text color used for normal content rows. $COLOR_TEXT_FORE = "#000000"; +# ------------------------------------------------------------ +# Main MARS NWE configuration +# ------------------------------------------------------------ + +# Path to the main mars_nwe server configuration file. +# This file is read and modified by the SMArT configuration pages. $mars_config = '@MARS_NWE_INSTALL_FULL_CONFDIR@/nwserv.conf'; +# User name used when SMArT drops privileges for non-root operations. +# Keep this set to an unprivileged local account. $nonroot_user = 'nobody'; -$smart_conf_path = '@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf'; +# Write the mars_nwe configuration file in compact form. +# 0 = preserve comments, blank lines and the original section layout +# 1 = write a compact configuration file without the original long comments +$smart_compact_nwservconf = 0; + +# ------------------------------------------------------------ +# SMArT internal file layout +# ------------------------------------------------------------ + +# Absolute path to the SMArT configuration file itself. +# Used when SMArT needs to append updated settings. +$smart_conf_path = '@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf'; + +# File used to store bindery login information for SMArT helper tools. +# This file should remain readable only by the service user or root. $smart_nwclient_path = '@MARS_NWE_INSTALL_FULL_CONFDIR@/.nwclient'; -$smart_static_dir = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/static'; -$smart_log_path = '@MARS_NWE_LOG_DIR@/smart.log'; -$smart_check_login = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/check_login'; +# Directory containing static HTML and image files served by SMArT. +$smart_static_dir = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/static'; -$nw_bind_ip = '0.0.0.0'; -$nw_tls_port = 9443; +# Log file used by the Perl SMArT frontend. +# Keep this separate from the nwwebui log file. +$smart_log_path = '@MARS_NWE_LOG_DIR@/smart.log'; +# Path to the PAM-based login helper used for root authentication. +$smart_check_login = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/check_login'; + +# Optional explicit path to the main SMArT Perl program. +# This is normally not required, because nwwebui already has a built-in default. +# Uncomment and adjust only if a non-standard location must be used. +# $smart_perl_path = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/smart'; + +# ------------------------------------------------------------ +# nwwebui listener settings +# ------------------------------------------------------------ + +# IP address used for the HTTP and HTTPS listeners. +# Use 0.0.0.0 to listen on all IPv4 interfaces. +# Use 127.0.0.1 for local-only testing. +$nw_bind_ip = '0.0.0.0'; + +# Log level used by nwwebui. +# 0 = errors only +# 1 = informational messages +# 2 = debug messages +$nw_log_level = 1; + +# Run nwwebui in daemon mode by default. +# 0 = stay in foreground +# 1 = detach into background +$nw_daemonize = 0; + +# PID file written by nwwebui. +$nw_pid_file = '@MARS_NWE_PID_DIR@/nwwebui.pid'; + +# Enable or disable TLS/SSL support. +# 1 = enable HTTPS listener +# 0 = disable HTTPS listener +# +# When disabled, nwwebui can still serve plain HTTP if nw_http_port > 0. +$nw_ssl_enable = 0; + +# Plain HTTP listener port. +# Set to 0 to disable plain HTTP completely. +# This is useful for local or isolated-network testing. +$nw_http_port = 9080; + +# HTTPS listener port. +# Used only when $nw_ssl_enable is set to 1. +# Set to 0 to disable HTTPS listening. +$nw_https_port = 9443; + +# TLS certificate file in PEM format. +# Required only when HTTPS is enabled. $nw_cert_file = '@MARS_NWE_INSTALL_FULL_CONFDIR@/server.crt'; -$nw_key_file = '@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key'; + +# TLS private key file in PEM format. +# Required only when HTTPS is enabled. +$nw_key_file = '@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key';