#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 "config.h" typedef struct { char bind_ip[64]; int ssl_enable; int http_port; int https_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->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); 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_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) { snprintf(cfg->smart_perl_path, sizeof(cfg->smart_perl_path), "%s", val); } } fclose(fp); 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 / socket helpers */ /* ------------------------------------------------------------ */ 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; } /* ------------------------------------------------------------ */ /* Shared child cleanup */ /* ------------------------------------------------------------ */ 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]; 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; int 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; } /* Client -> Perl */ 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; } } /* Perl -> Client */ 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"); 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 pfds[2]; 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; int pr = poll(pfds, 2, -1); if (pr < 0) { if (errno == EINTR) { continue; } log_msg(LOG_LEVEL_ERROR, "poll failed in TLS handler: %s", strerror(errno)); break; } /* TLS client -> Perl */ 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; } } } /* Perl -> TLS client */ 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) { log_msg(LOG_LEVEL_ERROR, "SSL_write 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); if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); } close(client_fd); } /* ------------------------------------------------------------ */ /* SIGCHLD handler */ /* ------------------------------------------------------------ */ static void reap_children(int sig) { (void)sig; for (;;) { pid_t pid = waitpid(-1, NULL, WNOHANG); if (pid <= 0) { break; } } } /* ------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------ */ 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]; } { 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)"); } } 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 log_level=%d ssl_enable=%d http_port=%d https_port=%d cert=%s key=%s smart.conf=%s", cfg.bind_ip, g_log_level, cfg.ssl_enable, cfg.http_port, cfg.https_port, cfg.cert_file, cfg.key_file, cfg.smart_conf); if (cfg.ssl_enable) { SSL_library_init(); SSL_load_error_strings(); OpenSSL_add_ssl_algorithms(); 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"); } } 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; } for (;;) { struct pollfd pfds[2]; int nfds = 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, "poll failed: %s", strerror(errno)); continue; } 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); } } if (http_fd >= 0) { close(http_fd); } if (https_fd >= 0) { close(https_fd); } if (ctx) { SSL_CTX_free(ctx); } return 0; }