From 331fb1a74601816896ef04d7462978f226c5ec94 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Tue, 21 Apr 2026 04:52:48 +0200 Subject: [PATCH] Add smart --- CMakeLists.txt | 88 +++++++ config.h.cmake | 23 ++ nwwebui.c | 618 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 729 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 config.h.cmake create mode 100644 nwwebui.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8d07388 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,88 @@ +################################# +# Project +############## + +################################# +# Dependencies +############## + +find_package(OpenSSL REQUIRED) + +find_library(PAM_LIB pam REQUIRED) +find_library(DL_LIB dl REQUIRED) + +################################# +# Generated files +############## + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/config.h" + IMMEDIATE @ONLY) + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/smart.conf.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/smart.conf" + IMMEDIATE @ONLY) + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/smart.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/smart" + IMMEDIATE @ONLY) + +################################# +# Compiler Switches +############## + +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include +) + +################################# +# Source Files +############## + +add_executable(nwwebui nwwebui.c) +add_executable(check_login check_login.c) + +################################# +# Linking +############## + +target_link_libraries(nwwebui + OpenSSL::SSL + OpenSSL::Crypto +) + +target_link_libraries(check_login + ${PAM_LIB} + ${DL_LIB} +) + +################################# +# Install Files +############## + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/smart.conf + DESTINATION ${MARS_NWE_INSTALL_FULL_CONFDIR}) + +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/smart + DESTINATION ${MARS_NWE_INSTALL_FULL_LIBEXEC}) + +install(PROGRAMS + apply.pl + readconfig.pl + settings.pl + static.pl + DESTINATION ${MARS_NWE_INSTALL_FULL_LIBEXEC}) + +install(DIRECTORY static/ + DESTINATION ${MARS_NWE_INSTALL_FULL_LIBEXEC}/static) + +install(TARGETS nwwebui + DESTINATION ${MARS_NWE_INSTALL_FULL_LIBEXEC}) + +install(TARGETS check_login + DESTINATION ${MARS_NWE_INSTALL_FULL_LIBEXEC}) diff --git a/config.h.cmake b/config.h.cmake new file mode 100644 index 0000000..a6f7144 --- /dev/null +++ b/config.h.cmake @@ -0,0 +1,23 @@ +#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_LIBEXEC@/smart" + +#define LOG_PATH_DEFAULT "@MARS_NWE_LOG_DIR@/nwwebui.log" + +#define LOG_LEVEL_ERROR 0 +#define LOG_LEVEL_INFO 1 +#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_CERT_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.crt" +#define DEFAULT_KEY_FILE "@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key" + +#define NW_BACKLOG 64 +#define NW_BUF_SZ 16384 + +#endif diff --git a/nwwebui.c b/nwwebui.c new file mode 100644 index 0000000..9900287 --- /dev/null +++ b/nwwebui.c @@ -0,0 +1,618 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +typedef struct { + char bind_ip[64]; + int tls_port; + + char cert_file[512]; + char key_file[512]; + + char smart_conf[512]; + char smart_perl_path[512]; +} nw_config_t; + +static FILE *g_log_fp = NULL; +static int g_log_level = LOG_LEVEL_DEFAULT; + +/* ------------------------------------------------------------ */ +/* Logging */ +/* ------------------------------------------------------------ */ + +static void log_open(void) { + if (!g_log_fp) { + g_log_fp = fopen(LOG_PATH_DEFAULT, "a"); + if (!g_log_fp) { + g_log_fp = stderr; + } + } +} + +static void log_msg(int level, const char *fmt, ...) { + if (level > g_log_level) { + return; + } + + log_open(); + + time_t now = time(NULL); + struct tm tm_now; + localtime_r(&now, &tm_now); + + char tbuf[64]; + 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) { + lvl = "DEBUG"; + } + + fprintf(g_log_fp, "[%s] [%s] ", tbuf, lvl); + + va_list ap; + va_start(ap, fmt); + vfprintf(g_log_fp, fmt, ap); + va_end(ap); + + fputc('\n', g_log_fp); + fflush(g_log_fp); +} + +static void die(const char *msg) { + log_msg(LOG_LEVEL_ERROR, "%s: %s", msg, strerror(errno)); + exit(EXIT_FAILURE); +} + +static void ssl_die(const char *msg) { + log_msg(LOG_LEVEL_ERROR, "%s", msg); + ERR_print_errors_fp(g_log_fp ? g_log_fp : stderr); + exit(EXIT_FAILURE); +} + +/* ------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------ */ + +static void trim(char *s) { + char *p = s; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (p != s) { + memmove(s, p, strlen(p) + 1); + } + + size_t len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) { + s[len - 1] = '\0'; + len--; + } +} + +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] == '"')) { + memmove(s, s + 1, len - 2); + s[len - 2] = '\0'; + } + } +} + +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; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '$') { + return 0; + } + p++; + + size_t ki = 0; + while (*p && (isalnum((unsigned char)*p) || *p == '_')) { + if (ki + 1 < ksz) { + key[ki++] = *p; + } + p++; + } + key[ki] = '\0'; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '=') { + return 0; + } + p++; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + size_t vi = 0; + while (*p && *p != ';' && *p != '\n' && *p != '\r') { + if (vi + 1 < vsz) { + val[vi++] = *p; + } + p++; + } + val[vi] = '\0'; + + trim(key); + trim(val); + strip_quotes(val); + + return key[0] != '\0'; +} + +static int file_exists_and_executable(const char *path) { + return access(path, X_OK) == 0; +} + +static int set_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + return -1; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + return -1; + } + return 0; +} + +/* ------------------------------------------------------------ */ +/* smart.conf loading */ +/* ------------------------------------------------------------ */ + +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; + + 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); +} + +static void load_smart_conf(nw_config_t *cfg) { + FILE *fp = fopen(cfg->smart_conf, "r"); + 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]; + + if (!parse_perl_assignment(line, key, sizeof(key), val, sizeof(val))) { + continue; + } + + 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_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; + } +} + +/* ------------------------------------------------------------ */ +/* Listener / TLS */ +/* ------------------------------------------------------------ */ + +static int create_listener(const char *bind_ip, int port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + 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); + + if (strcmp(bind_ip, "0.0.0.0") == 0) { + addr.sin_addr.s_addr = htonl(INADDR_ANY); + } else { + if (inet_pton(AF_INET, bind_ip, &addr.sin_addr) != 1) { + close(fd); + log_msg(LOG_LEVEL_ERROR, "Invalid bind IP: %s", bind_ip); + exit(EXIT_FAILURE); + } + } + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(fd); + die("bind"); + } + + if (listen(fd, NW_BACKLOG) < 0) { + close(fd); + die("listen"); + } + + return fd; +} + +static ssize_t write_all_fd(int fd, const unsigned char *buf, size_t len) { + size_t off = 0; + + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + return -1; + } + if (n == 0) { + break; + } + off += (size_t)n; + } + + return (ssize_t)off; +} + +static ssize_t send_all_ssl(SSL *ssl, const unsigned char *buf, size_t len) { + size_t off = 0; + + while (off < len) { + int n = SSL_write(ssl, buf + off, (int)(len - off)); + if (n <= 0) { + int err = SSL_get_error(ssl, n); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + continue; + } + return -1; + } + off += (size_t)n; + } + + return (ssize_t)off; +} + +/* ------------------------------------------------------------ */ +/* Perl launcher */ +/* ------------------------------------------------------------ */ + +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 }; + + if (pipe(inpipe) < 0) { + die("pipe stdin"); + } + + if (pipe(outpipe) < 0) { + close(inpipe[0]); + close(inpipe[1]); + die("pipe stdout"); + } + + pid_t pid = fork(); + if (pid < 0) { + close(inpipe[0]); + close(inpipe[1]); + close(outpipe[0]); + close(outpipe[1]); + die("fork"); + } + + 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); + } + if (dup2(outpipe[1], STDOUT_FILENO) < 0) { + perror("dup2 stdout"); + _exit(127); + } + if (dup2(outpipe[1], STDERR_FILENO) < 0) { + perror("dup2 stderr"); + _exit(127); + } + + close(inpipe[0]); + close(inpipe[1]); + close(outpipe[0]); + close(outpipe[1]); + + execl(cfg->smart_perl_path, cfg->smart_perl_path, (char *)NULL); + + perror("execl smart_perl_path"); + _exit(127); + } + + close(inpipe[0]); + close(outpipe[1]); + + *child_stdin_fd = inpipe[1]; + *child_stdout_fd = outpipe[0]; + + return pid; +} + +/* ------------------------------------------------------------ */ +/* Connection handler */ +/* ------------------------------------------------------------ */ + +static void handle_connection(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]; + + ssl = SSL_new(ctx); + if (!ssl) { + log_msg(LOG_LEVEL_ERROR, "SSL_new failed"); + goto cleanup; + } + + SSL_set_fd(ssl, client_fd); + + if (SSL_accept(ssl) <= 0) { + log_msg(LOG_LEVEL_ERROR, "SSL_accept failed"); + ERR_print_errors_fp(g_log_fp ? g_log_fp : stderr); + goto cleanup; + } + + log_msg(LOG_LEVEL_INFO, "Accepted new TLS connection"); + + 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; + + /* 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; + } + } 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; + } + } + + /* 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))) { + ssize_t rn = read(child_stdout_fd, buf, sizeof(buf)); + if (rn > 0) { + if (send_all_ssl(ssl, buf, (size_t)rn) < 0) { + log_msg(LOG_LEVEL_ERROR, "SSL_write failed"); + break; + } + } else if (rn == 0) { + break; + } else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + break; + } + } + + /* Exit if the child process is already gone */ + if (child_pid > 0) { + int status = 0; + pid_t w = waitpid(child_pid, &status, WNOHANG); + if (w == child_pid) { + break; + } + } + } + +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); + } + } + + if (ssl) { + SSL_shutdown(ssl); + SSL_free(ssl); + } + + close(client_fd); +} + +static void reap_children(int sig) { + (void)sig; + while (waitpid(-1, NULL, WNOHANG) > 0) { + } +} + +/* ------------------------------------------------------------ */ +/* Main */ +/* ------------------------------------------------------------ */ + +int main(int argc, char **argv) { + const char *smart_conf_path = DEFAULT_SMART_CONF; + + if (argc > 1) { + smart_conf_path = argv[1]; + } + + signal(SIGCHLD, reap_children); + signal(SIGPIPE, SIG_IGN); + + log_open(); + log_msg(LOG_LEVEL_INFO, "nwwebui starting"); + + 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; + } + + 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", + cfg.bind_ip, + cfg.tls_port, + cfg.cert_file, + cfg.key_file, + cfg.smart_conf); + + 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"); + } + + 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"); + } + + 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); + + int client_fd = accept(listen_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) { + 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); + return 0; +}