diff --git a/config.h.cmake b/config.h.cmake index 406e65d..24e7266 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -11,8 +11,10 @@ #define LOG_LEVEL_DEBUG 2 #define LOG_LEVEL_DEFAULT LOG_LEVEL_INFO -#define DEFAULT_BIND_IP "0.0.0.0" -#define DEFAULT_TLS_PORT 9443 +#define DEFAULT_BIND_IP "0.0.0.0" +#define DEFAULT_SSL_ENABLE 0 +#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" @@ -20,4 +22,4 @@ #define NW_BACKLOG 64 #define NW_BUF_SZ 16384 -#endif +#endif \ No newline at end of file diff --git a/nwwebui.c b/nwwebui.c index 9900287..7b7389d 100644 --- a/nwwebui.c +++ b/nwwebui.c @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -25,7 +24,10 @@ typedef struct { char bind_ip[64]; - int tls_port; + + int ssl_enable; + int http_port; + int https_port; char cert_file[512]; char key_file[512]; @@ -200,14 +202,15 @@ 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; snprintf(cfg->cert_file, sizeof(cfg->cert_file), "%s", DEFAULT_CERT_FILE); snprintf(cfg->key_file, sizeof(cfg->key_file), "%s", DEFAULT_KEY_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); } @@ -229,27 +232,42 @@ 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_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) { @@ -400,10 +418,100 @@ 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); + } + *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(child_stdout_fd) < 0) { + log_msg(LOG_LEVEL_ERROR, "Could not set child stdout non-blocking"); + goto cleanup; + } + + for (;;) { + struct pollfd pfd; + pfd.fd = child_stdout_fd; + pfd.events = POLLIN | POLLHUP | POLLERR; + pfd.revents = 0; + + 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) { + break; + } + + int pr = poll(&pfd, 1, 10); + if (pr > 0 && (pfd.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) { + 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; @@ -483,21 +591,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); @@ -519,6 +613,9 @@ static void reap_children(int sig) { int main(int argc, char **argv) { const char *smart_conf_path = DEFAULT_SMART_CONF; + int http_fd = -1; + int https_fd = -1; + SSL_CTX *ctx = NULL; if (argc > 1) { smart_conf_path = argv[1]; @@ -542,77 +639,149 @@ int main(int argc, char **argv) { } log_msg(LOG_LEVEL_INFO, "Using SMArT Perl path: %s", cfg.smart_perl_path); - log_msg(LOG_LEVEL_INFO, - "Config loaded: bind=%s:%d cert=%s key=%s smart.conf=%s", + "Config loaded: bind=%s log_level=%d ssl_enable=%d http_port=%d https_port=%d cert=%s key=%s smart.conf=%s", cfg.bind_ip, - cfg.tls_port, + g_log_level, + cfg.ssl_enable, + cfg.http_port, + cfg.https_port, cfg.cert_file, cfg.key_file, cfg.smart_conf); - SSL_library_init(); - SSL_load_error_strings(); - OpenSSL_add_ssl_algorithms(); + if (cfg.ssl_enable) { + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); - SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); - if (!ctx) { - ssl_die("SSL_CTX_new failed"); + ctx = SSL_CTX_new(TLS_server_method()); + if (!ctx) { + ssl_die("SSL_CTX_new failed"); + } + + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + + if (SSL_CTX_use_certificate_file(ctx, cfg.cert_file, SSL_FILETYPE_PEM) <= 0) { + ssl_die("Could not load certificate file"); + } + + if (SSL_CTX_use_PrivateKey_file(ctx, cfg.key_file, SSL_FILETYPE_PEM) <= 0) { + ssl_die("Could not load private key file"); + } + + if (!SSL_CTX_check_private_key(ctx)) { + ssl_die("Private key does not match certificate"); + } } - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); - - if (SSL_CTX_use_certificate_file(ctx, cfg.cert_file, SSL_FILETYPE_PEM) <= 0) { - ssl_die("Could not load certificate file"); + 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 (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 && 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 (!SSL_CTX_check_private_key(ctx)) { - ssl_die("Private key does not match certificate"); + 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; } - int listen_fd = create_listener(cfg.bind_ip, cfg.tls_port); - - log_msg(LOG_LEVEL_INFO, - "TLS wrapper listening on %s:%d and launching %s", - cfg.bind_ip, - cfg.tls_port, - cfg.smart_perl_path); - for (;;) { - struct sockaddr_in peer; - socklen_t peerlen = sizeof(peer); + struct pollfd pfds[2]; + int nfds = 0; - int client_fd = accept(listen_fd, (struct sockaddr *)&peer, &peerlen); - if (client_fd < 0) { + 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++; + } + + int pr = poll(pfds, nfds, -1); + if (pr < 0) { if (errno == EINTR) { continue; } - log_msg(LOG_LEVEL_ERROR, "accept failed: %s", strerror(errno)); + log_msg(LOG_LEVEL_ERROR, "poll failed: %s", strerror(errno)); continue; } - pid_t pid = fork(); - if (pid < 0) { - log_msg(LOG_LEVEL_ERROR, "fork failed: %s", strerror(errno)); + for (int i = 0; i < nfds; i++) { + if (!(pfds[i].revents & POLLIN)) { + continue; + } + + struct sockaddr_in peer; + socklen_t peerlen = sizeof(peer); + int 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_t 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); - continue; } - - if (pid == 0) { - close(listen_fd); - handle_connection(ctx, client_fd, &cfg); - 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); + } + return 0; } diff --git a/smart.conf.cmake b/smart.conf.cmake index 494f061..de39f20 100644 --- a/smart.conf.cmake +++ b/smart.conf.cmake @@ -1,24 +1,105 @@ +# 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'; +# ------------------------------------------------------------ +# 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; + +# 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 = 1; + +# 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';