diff --git a/CMakeLists.txt b/CMakeLists.txt index cfb56d3c..812212ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ else() set(CMAKE_C_STANDARD 99) endif() -set(SOURCE_FILES src/utils.c src/pty.c src/protocol.c src/http.c src/server.c) +set(SOURCE_FILES src/utils.c src/pty.c src/protocol.c src/http.c src/server.c src/audit.c) include(FindPackageHandleStandardArgs) diff --git a/src/audit.c b/src/audit.c new file mode 100644 index 00000000..5fd6a534 --- /dev/null +++ b/src/audit.c @@ -0,0 +1,322 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "audit.h" +#include "utils.h" + +static audit_config_t config = {0}; +static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; + +static int ensure_log_dir(const char *log_file) { + char *log_dir = strdup(log_file); + char *last_slash = strrchr(log_dir, '/'); + if (last_slash) { + *last_slash = '\0'; + lwsl_notice("Creating log directory: %s\n", log_dir); + if (mkdir(log_dir, 0755) != 0 && errno != EEXIST) { + lwsl_err("Failed to create log directory: %s, error: %s\n", log_dir, strerror(errno)); + free(log_dir); + return -1; + } + lwsl_notice("Log directory created or already exists: %s\n", log_dir); + } + free(log_dir); + return 0; +} + +int audit_init(const char *log_file) { + lwsl_notice("Initializing audit system with log file: %s\n", log_file); + + if (config.enabled) { + lwsl_notice("Audit system already initialized\n"); + return 0; + } + + // Ensure log directory exists + if (ensure_log_dir(log_file) != 0) { + lwsl_err("Failed to ensure log directory exists\n"); + return -1; + } + + config.log_file = strdup(log_file); + config.enabled = true; + config.max_size = 10 * 1024 * 1024; // Default 10MB + + lwsl_notice("Opening log file: %s\n", log_file); + // Create log file + FILE *fp = fopen(log_file, "a"); + if (fp == NULL) { + lwsl_err("Failed to open audit log file: %s, error: %s\n", log_file, strerror(errno)); + return -1; + } + fclose(fp); + lwsl_notice("Log file opened successfully\n"); + + lwsl_notice("Audit system initialized successfully\n"); + return 0; +} + +// Add custom fields +int audit_add_custom_field(const char *key, const char *value) { + if (!key || !value) { + lwsl_err("Invalid custom field key or value\n"); + return -1; + } + + pthread_mutex_lock(&log_mutex); + + // Reallocate memory + audit_custom_field_t *new_fields = xrealloc(config.custom_fields, + (config.custom_fields_count + 1) * sizeof(audit_custom_field_t)); + + if (!new_fields) { + lwsl_err("Failed to allocate memory for custom field\n"); + pthread_mutex_unlock(&log_mutex); + return -1; + } + + config.custom_fields = new_fields; + + // Add new field + size_t key_len = strlen(key); + size_t value_len = strlen(value); + + config.custom_fields[config.custom_fields_count].key = xmalloc(key_len + 1); + config.custom_fields[config.custom_fields_count].value = xmalloc(value_len + 1); + + if (!config.custom_fields[config.custom_fields_count].key || + !config.custom_fields[config.custom_fields_count].value) { + lwsl_err("Failed to allocate memory for custom field strings\n"); + if (config.custom_fields[config.custom_fields_count].key) { + free(config.custom_fields[config.custom_fields_count].key); + } + if (config.custom_fields[config.custom_fields_count].value) { + free(config.custom_fields[config.custom_fields_count].value); + } + pthread_mutex_unlock(&log_mutex); + return -1; + } + + strncpy(config.custom_fields[config.custom_fields_count].key, key, key_len); + config.custom_fields[config.custom_fields_count].key[key_len] = '\0'; + + strncpy(config.custom_fields[config.custom_fields_count].value, value, value_len); + config.custom_fields[config.custom_fields_count].value[value_len] = '\0'; + + config.custom_fields_count++; + pthread_mutex_unlock(&log_mutex); + + lwsl_notice("Added custom field: %s=%s\n", key, value); + return 0; +} + +// Clear all custom fields +void audit_clear_custom_fields(void) { + pthread_mutex_lock(&log_mutex); + + for (int i = 0; i < config.custom_fields_count; i++) { + free(config.custom_fields[i].key); + free(config.custom_fields[i].value); + } + + free(config.custom_fields); + config.custom_fields = NULL; + config.custom_fields_count = 0; + + pthread_mutex_unlock(&log_mutex); + lwsl_notice("Cleared all custom fields\n"); +} + +// Rotate log file +static int rotate_log(void) { + char new_name[256]; + snprintf(new_name, sizeof(new_name), "%s.1", config.log_file); + + // Rename current log file + if (rename(config.log_file, new_name) != 0) { + lwsl_err("Failed to rename log file: %s\n", strerror(errno)); + return -1; + } + + // Create new log file + FILE *fp = fopen(config.log_file, "a"); + if (fp == NULL) { + lwsl_err("Failed to create new log file: %s\n", strerror(errno)); + // If creating new file fails, try to restore original file + if (rename(new_name, config.log_file) != 0) { + lwsl_err("Failed to restore original log file: %s\n", strerror(errno)); + } + return -1; + } + fclose(fp); + + lwsl_notice("Log rotated: %s -> %s\n", config.log_file, new_name); + return 0; +} + +static void write_log_entry(const audit_entry_t *entry) { + lwsl_notice("Writing log entry to file: %s\n", config.log_file); + pthread_mutex_lock(&log_mutex); + + // Check file size + struct stat st; + if (stat(config.log_file, &st) == 0 && st.st_size >= config.max_size) { + lwsl_notice("Need to rotate log file, current file size: %d, config.max_size: %d\n",st.st_size,config.max_size); + int ret = rotate_log(); + if (ret != 0) { + lwsl_err("Failed to rotate log file\n"); + pthread_mutex_unlock(&log_mutex); + return; + } + } + + FILE *fp = fopen(config.log_file, "a"); + if (fp == NULL) { + lwsl_err("Failed to open audit log file for writing: %s\n", strerror(errno)); + pthread_mutex_unlock(&log_mutex); + return; + } + + char timestamp[32]; + struct tm *tm_info = localtime(&entry->timestamp); + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); + + // Write timestamp and address + fprintf(fp, "[%s] Address: %s", + timestamp, + entry->address ? entry->address : "unknown"); + + // Write custom fields + for (int i = 0; i < entry->custom_fields_count; i++) { + fprintf(fp, ", %s: %s", + entry->custom_fields[i].key, + entry->custom_fields[i].value); + } + + if (entry->command) { + fprintf(fp, ", Command: %s", entry->command); + } + fprintf(fp, "\n"); + fflush(fp); + fclose(fp); + pthread_mutex_unlock(&log_mutex); + lwsl_notice("Log entry written successfully\n"); +} + +// Clean command string, remove control characters +static char *clean_command_string(const char *cmd) { + if (!cmd) return NULL; + + size_t len = strlen(cmd); + char *clean = xmalloc(len + 1); + size_t j = 0; + + for (size_t i = 0; i < len; i++) { + // Skip control characters (ASCII 0-31, except newline and carriage return) + if (cmd[i] >= 32 || cmd[i] == '\n' || cmd[i] == '\r') { + clean[j++] = cmd[i]; + } + } + clean[j] = '\0'; + + // Remove trailing whitespace characters + while (j > 0 && (clean[j-1] == ' ' || clean[j-1] == '\t')) { + clean[--j] = '\0'; + } + + return clean; +} + +// Modify audit_log_command function +void audit_log_command(const char *address, const char *command) { + if (!config.enabled) { + lwsl_notice("Audit system is not enabled, skipping log entry\n"); + return; + } + + char *clean_cmd = clean_command_string(command); + lwsl_notice("Logging command: address='%s', command='%s'\n", + address, clean_cmd); + + audit_entry_t entry = { + .timestamp = time(NULL), + .address = strdup(address), + .command = clean_cmd, + .custom_fields = NULL, + .custom_fields_count = 0 + }; + + // Copy custom fields + if (config.custom_fields_count > 0) { + entry.custom_fields = xmalloc((config.custom_fields_count + 1) * sizeof(audit_custom_field_t)); + entry.custom_fields_count = config.custom_fields_count; + + for (int i = 0; i < config.custom_fields_count; i++) { + entry.custom_fields[i].key = strdup(config.custom_fields[i].key); + entry.custom_fields[i].value = strdup(config.custom_fields[i].value); + } + } else { + entry.custom_fields = xmalloc(sizeof(audit_custom_field_t)); + entry.custom_fields_count = 0; + } + + write_log_entry(&entry); + + // Clean memory + free(entry.address); + free(entry.command); + for (int i = 0; i < entry.custom_fields_count; i++) { + free(entry.custom_fields[i].key); + free(entry.custom_fields[i].value); + } + free(entry.custom_fields); +} + +// Validate audit field format +int validate_audit_field(const char *field) { + if (!field) { + lwsl_err("Invalid audit field: NULL\n"); + return -1; + } + + // Check if it contains an equal sign + char *value = strchr(field, '='); + if (!value) { + lwsl_err("Invalid audit field format: %s (should be key=value)\n", field); + return -1; + } + + // Validate key name is not empty + if (value == field) { + lwsl_err("Empty key in audit field: %s\n", field); + return -1; + } + + // Validate value is not empty + if (*(value + 1) == '\0') { + lwsl_err("Empty value in audit field: %s\n", field); + return -1; + } + + return 0; +} + +// Modify audit_cleanup function to clean up custom fields +void audit_cleanup(void) { + lwsl_notice("Cleaning up audit system\n"); + if (!config.enabled) { + lwsl_notice("Audit system is not enabled, nothing to clean up\n"); + return; + } + + free(config.log_file); + audit_clear_custom_fields(); // Clean up custom fields + config.enabled = false; + pthread_mutex_destroy(&log_mutex); + lwsl_notice("Audit system cleaned up successfully\n"); +} \ No newline at end of file diff --git a/src/audit.h b/src/audit.h new file mode 100644 index 00000000..1d88a903 --- /dev/null +++ b/src/audit.h @@ -0,0 +1,47 @@ +#ifndef AUDIT_H +#define AUDIT_H + +#include +#include + +// Custom field structure +typedef struct { + char *key; + char *value; +} audit_custom_field_t; + +// Audit configuration structure +typedef struct { + bool enabled; + char *log_file; + audit_custom_field_t *custom_fields; // Custom fields array + int custom_fields_count; // Custom fields count + size_t max_size; // Maximum size of single log file (bytes) +} audit_config_t; + +// Audit log entry structure +typedef struct { + time_t timestamp; + char *address; + char *command; + audit_custom_field_t *custom_fields; // Custom fields array + int custom_fields_count; // Custom fields count +} audit_entry_t; + +// Initialize audit system +int audit_init(const char *log_file); + +// Log command +void audit_log_command(const char *address, const char *command); + +// 关闭审计系统 +void audit_cleanup(void); + +// 新增函数声明 +int audit_add_custom_field(const char *key, const char *value); +void audit_clear_custom_fields(void); + +// Validate audit field format +int validate_audit_field(const char *field); + +#endif // AUDIT_H \ No newline at end of file diff --git a/src/protocol.c b/src/protocol.c index 53e65d4d..bea0a6e3 100644 --- a/src/protocol.c +++ b/src/protocol.c @@ -5,10 +5,13 @@ #include #include #include +#include +#include #include "pty.h" #include "server.h" #include "utils.h" +#include "audit.h" // initial message list static char initial_cmds[] = {SET_WINDOW_TITLE, SET_PREFERENCES}; @@ -73,10 +76,13 @@ static pty_ctx_t *pty_ctx_init(struct pss_tty *pss) { pty_ctx_t *ctx = xmalloc(sizeof(pty_ctx_t)); ctx->pss = pss; ctx->ws_closed = false; + ctx->last_audit_line = 0; // Initialize line number to 0 return ctx; } -static void pty_ctx_free(pty_ctx_t *ctx) { free(ctx); } +static void pty_ctx_free(pty_ctx_t *ctx) { + free(ctx); +} static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) { pty_ctx_t *ctx = (pty_ctx_t *)process->ctx; @@ -85,6 +91,75 @@ static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) { return; } + if (buf->len > 0) { + char *output = xmalloc(buf->len + 1); + memcpy(output, buf->base, buf->len); + output[buf->len] = '\0'; + + // Check if it contains AUDIT_CMD: string + char *audit_cmd = strstr(output, "AUDIT_CMD:"); + if (audit_cmd) { + // Extract line number and command content + char *content = audit_cmd + strlen("AUDIT_CMD:"); + // Remove newline + char *newline = strchr(content, '\n'); + if (newline) *newline = '\0'; + + // Parse line number and command + int line_num; + char cmd[1024] = {0}; + if (sscanf(content, "%d %[^\n]", &line_num, cmd) == 2) { + // When opening console, we receive an AUDIT_CMD output that needs to be filtered out + if (ctx->last_audit_line == 0) { + ctx->last_audit_line = line_num; + lwsl_notice("Skipping init command: %s, at line %d\n", cmd, line_num); + } else { + // Check if it's a duplicate AUDIT_CMD command, pressing Enter will also trigger PROMPT_COMMAND, this case needs to be filtered + if (line_num != ctx->last_audit_line) { + audit_log_command(ctx->pss->address, cmd); + ctx->last_audit_line = line_num; + } else { + lwsl_notice("Skipping duplicate command at line %d\n", line_num); + } + } + + } + + // Remove the AUDIT_CMD line + char *line_start = audit_cmd; + // Find the start of this line + while (line_start > output && *(line_start - 1) != '\n') { + line_start--; + } + // Find the end of this line + char *line_end = strchr(audit_cmd, '\n'); + if (!line_end) line_end = output + buf->len; + + // Calculate the length of content to keep + size_t before_len = line_start - output; + size_t after_len = buf->len - (line_end - output); + + // Create new buffer containing content before and after the AUDIT_CMD line + pty_buf_t *new_buf = pty_buf_init(output, before_len); + if (after_len > 0) { + pty_buf_t *after_buf = pty_buf_init(line_end + 1, after_len); + // Merge two buffers + char *combined = xmalloc(before_len + after_len); + memcpy(combined, output, before_len); + memcpy(combined + before_len, line_end + 1, after_len); + pty_buf_free(new_buf); + pty_buf_free(after_buf); + new_buf = pty_buf_init(combined, before_len + after_len); + free(combined); + } + + pty_buf_free(buf); + buf = new_buf; + } + free(output); + } + + // Normal output if (eof && !process_running(process)) ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006; else @@ -125,26 +200,35 @@ static char **build_args(struct pss_tty *pss) { return argv; } +// Add audit command function +static const char *get_audit_command(void) { + return "echo \"AUDIT_CMD:$(history 1)\""; +} + static char **build_env(struct pss_tty *pss) { - int i = 0, n = 2; - char **envp = xmalloc(n * sizeof(char *)); - - // TERM - envp[i] = xmalloc(36); - snprintf(envp[i], 36, "TERM=%s", server->terminal_type); - i++; - - // TTYD_USER - if (strlen(pss->user) > 0) { - envp = xrealloc(envp, (++n) * sizeof(char *)); - envp[i] = xmalloc(40); - snprintf(envp[i], 40, "TTYD_USER=%s", pss->user); + int i = 0, n = 3; + char **envp = xmalloc(n * sizeof(char *)); + + // TERM + envp[i] = xmalloc(36); + snprintf(envp[i], 36, "TERM=%s", server->terminal_type); i++; - } - envp[i] = NULL; + // TTYD_USER + if (strlen(pss->user) > 0) { + envp = xrealloc(envp, (++n) * sizeof(char *)); + envp[i] = xmalloc(40); + snprintf(envp[i], 40, "TTYD_USER=%s", pss->user); + i++; + } + + // Add audit command + envp[i] = xmalloc(200); + snprintf(envp[i], 200, "PROMPT_COMMAND=%s", get_audit_command()); + i++; - return envp; + envp[i] = NULL; + return envp; } static bool spawn_process(struct pss_tty *pss, uint16_t columns, uint16_t rows) { @@ -197,7 +281,9 @@ static bool check_auth(struct lws *wsi, struct pss_tty *pss) { int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { struct pss_tty *pss = (struct pss_tty *)user; char buf[256]; - size_t n = 0; + static char current_cmd[1024] = {0}; // Used to store current command + static size_t cmd_len = 0; // Current command length + int n = 0; // Used to store lws_hdr_copy return value switch (reason) { case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: @@ -233,7 +319,7 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, pss->authenticated = false; pss->wsi = wsi; pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS; - + if (server->url_arg) { while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) { if (strncmp(buf, "arg=", 4) == 0) { @@ -247,7 +333,7 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, server->client_count++; lws_get_peer_simple(lws_get_network_wsi(wsi), pss->address, sizeof(pss->address)); - lwsl_notice("WS %s - %s, clients: %d\n", pss->path, pss->address, server->client_count); + lwsl_notice("WS %s - %s, clients: %d, user: %s\n", pss->path, pss->address, server->client_count, pss->user); break; case LWS_CALLBACK_SERVER_WRITEABLE: @@ -307,11 +393,20 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, switch (command) { case INPUT: if (!server->writable) break; + + // Get input content + char *input = xmalloc(pss->len); + memcpy(input, pss->buffer + 1, pss->len - 1); + input[pss->len - 1] = '\0'; + + // Continue processing command int err = pty_write(pss->process, pty_buf_init(pss->buffer + 1, pss->len - 1)); if (err) { lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err)); + free(input); return -1; } + free(input); break; case RESIZE_TERMINAL: if (pss->process == NULL) break; @@ -371,6 +466,7 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, } if (pss->process != NULL) { + ((pty_ctx_t *)pss->process->ctx)->ws_closed = true; if (process_running(pss->process)) { pty_pause(pss->process); @@ -379,8 +475,8 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, } } - if ((server->once || server->exit_no_conn) && server->client_count == 0) { - lwsl_notice("exiting due to the --once/--exit-no-conn option.\n"); + if (server->once && server->client_count == 0) { + lwsl_notice("exiting due to the --once option.\n"); force_exit = true; lws_cancel_service(context); exit(0); diff --git a/src/server.c b/src/server.c index c9e2fa96..4525129e 100644 --- a/src/server.c +++ b/src/server.c @@ -12,6 +12,7 @@ #include #include "utils.h" +#include "audit.h" #ifndef TTYD_VERSION #define TTYD_VERSION "unknown" @@ -56,6 +57,7 @@ static const struct option options[] = {{"port", required_argument, NULL, 'p'}, {"socket-owner", required_argument, NULL, 'U'}, {"credential", required_argument, NULL, 'c'}, {"auth-header", required_argument, NULL, 'H'}, + {"username", required_argument, NULL, 'n'}, {"uid", required_argument, NULL, 'u'}, {"gid", required_argument, NULL, 'g'}, {"signal", required_argument, NULL, 's'}, @@ -77,14 +79,15 @@ static const struct option options[] = {{"port", required_argument, NULL, 'p'}, {"check-origin", no_argument, NULL, 'O'}, {"max-clients", required_argument, NULL, 'm'}, {"once", no_argument, NULL, 'o'}, - {"exit-no-conn", no_argument, NULL, 'q'}, {"browser", no_argument, NULL, 'B'}, {"debug", required_argument, NULL, 'd'}, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, - {"serv_buffer_size", required_argument, NULL, 'f'}, + {"audit-enable", no_argument, NULL, 0}, + {"audit-log-file", required_argument, NULL, 0}, + {"audit-field", required_argument, NULL, 0}, {NULL, 0, 0, 0}}; -static const char *opt_string = "p:i:U:c:H:u:g:s:w:I:b:f:P:6aSC:K:A:Wt:T:Om:oqBd:vh"; +static const char *opt_string = "p:i:U:c:H:n:u:g:s:w:I:b:P:6aSC:K:A:Wt:T:Om:oBd:vh"; static void print_help() { // clang-format off @@ -99,6 +102,7 @@ static void print_help() { " -U, --socket-owner User owner of the UNIX domain socket file, when enabled (eg: user:group)\n" " -c, --credential Credential for basic authentication (format: username:password)\n" " -H, --auth-header HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication\n" + " -n, --username Username for audit logging, will be used in audit logs instead of auth header value\n" " -u, --uid User id to run with\n" " -g, --gid Group id to run with\n" " -s, --signal Signal to send to the command when exit it (default: 1, SIGHUP)\n" @@ -110,11 +114,9 @@ static void print_help() { " -O, --check-origin Do not allow websocket connection from different origin\n" " -m, --max-clients Maximum clients to support (default: 0, no limit)\n" " -o, --once Accept only one client and exit on disconnection\n" - " -q, --exit-no-conn Exit on all clients disconnection\n" " -B, --browser Open terminal with the default system browser\n" " -I, --index Custom index.html path\n" " -b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)\n" - " -f, --serv_buffer_size Maximum chunk of file that can be sent at once (eg: --service_buffer_size 4096 indicates 4KB)\n" #if LWS_LIBRARY_VERSION_NUMBER >= 4000000 " -P, --ping-interval Websocket ping interval(sec) (default: 5)\n" #endif @@ -129,6 +131,9 @@ static void print_help() { #endif " -d, --debug Set log level (default: 7)\n" " -v, --version Print the version and exit\n" + " --audit-enable Enable command audit logging\n" + " --audit-log-file Audit log file path (default: /var/log/ttyd/audit.log)\n" + " --audit-field Add custom field to audit log (format: key=value), can be used multiple times\n" " -h, --help Print this text and exit\n\n" "Visit https://github.com/tsl0922/ttyd to get more information and report bugs.\n", TTYD_VERSION @@ -154,10 +159,8 @@ static void print_config() { if (server->url_arg) lwsl_notice(" allow url arg: true\n"); if (server->max_clients > 0) lwsl_notice(" max clients: %d\n", server->max_clients); if (server->once) lwsl_notice(" once: true\n"); - if (server->exit_no_conn) lwsl_notice(" exit_no_conn: true\n"); if (server->index != NULL) lwsl_notice(" custom index.html: %s\n", server->index); if (server->cwd != NULL) lwsl_notice(" working directory: %s\n", server->cwd); - if (server->serv_buffer_size != 0) lwsl_notice(" Service buffer size: %d bytes\n", server->serv_buffer_size); if (!server->writable) lwsl_notice("The --writable option is not set, will start in readonly mode"); } @@ -301,6 +304,59 @@ static int calc_command_start(int argc, char **argv) { return start; } +static int init_audit_fields(void) { + if (!server->audit_enabled) { + return 0; + } + lwsl_notice("Initializing audit system\n"); + if (!server->audit_log_file) { + server->audit_log_file = strdup("/var/log/ttyd/audit.log"); + lwsl_notice("Using default audit log file: %s\n", server->audit_log_file); + } + + // verify audit fields. + for (int i = 0; i < server->audit_fields_count; i++) { + char *field = server->audit_fields[i]; + lwsl_notice("Processing audit field[%d]: %s\n", i, field); + + // Create a copy of the field to avoid modifying the original string + char *field_copy = strdup(field); + if (!field_copy) { + lwsl_err("Failed to duplicate audit field: %s\n", field); + return -1; + } + + char *value = strchr(field_copy, '='); + if (!value) { + lwsl_err("Invalid audit field format: %s (should be key=value)\n", field); + free(field_copy); + return -1; + } + + // split key-values. + *value = '\0'; + value++; + + lwsl_notice("Adding audit field: key='%s', value='%s'\n", field_copy, value); + if (audit_add_custom_field(field_copy, value) != 0) { + lwsl_err("Failed to add audit field: %s=%s\n", field_copy, value); + free(field_copy); + return -1; + } + + free(field_copy); + lwsl_notice("Audit field[%d] added successfully\n", i); + } + + // Initialize audit system + if (audit_init(server->audit_log_file) != 0) { + lwsl_err("Failed to initialize audit system\n"); + return -1; + } + + return 0; +} + int main(int argc, char **argv) { if (argc == 1) { print_help(); @@ -314,7 +370,10 @@ int main(int argc, char **argv) { #endif int start = calc_command_start(argc, argv); + if (start < 0) return 1; + server = server_new(argc, argv, start); + if (server == NULL) return 1; struct lws_context_creation_info info; memset(&info, 0, sizeof(info)); @@ -323,6 +382,7 @@ int main(int argc, char **argv) { info.protocols = protocols; info.gid = -1; info.uid = -1; + info.pt_serv_buf_size = 262144; info.max_http_header_pool = 16; info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6; #ifndef LWS_WITHOUT_EXTENSIONS @@ -330,7 +390,6 @@ int main(int argc, char **argv) { #endif info.max_http_header_data = 65535; - int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE; char iface[128] = ""; char socket_owner[128] = ""; @@ -348,7 +407,9 @@ int main(int argc, char **argv) { // parse command line options int c; - while ((c = getopt_long(start, argv, opt_string, options, NULL)) != -1) { + int option_index = 0; + while ((c = getopt_long(start, argv, opt_string, options, &option_index)) != -1) { + lwsl_notice("Processing option: c=%d, option_index=%d\n", c, option_index); switch (c) { case 'h': print_help(); @@ -374,9 +435,6 @@ int main(int argc, char **argv) { case 'o': server->once = true; break; - case 'q': - server->exit_no_conn = true; - break; case 'B': browser = true; break; @@ -387,14 +445,6 @@ int main(int argc, char **argv) { return -1; } break; - case 'f': - info.pt_serv_buf_size = parse_int("serv_buffer_size", optarg); - if (info.pt_serv_buf_size < 0) { - fprintf(stderr, "ttyd: invalid service buffer size: %s\n", optarg); - return -1; - } - server->serv_buffer_size = info.pt_serv_buf_size; - break; case 'i': strncpy(iface, optarg, sizeof(iface) - 1); iface[sizeof(iface) - 1] = '\0'; @@ -415,6 +465,10 @@ int main(int argc, char **argv) { case 'H': server->auth_header = strdup(optarg); break; + case 'n': + server->username = strdup(optarg); + lwsl_notice("Username set to: %s\n", server->username); + break; case 'u': info.uid = parse_int("uid", optarg); break; @@ -519,11 +573,37 @@ int main(int argc, char **argv) { json_object_object_add(client_prefs, key, obj != NULL ? obj : json_object_new_string(value)); } break; + case 0: + lwsl_notice("Case 0: option name=%s, optarg=%s\n", + options[option_index].name, + optarg ? optarg : "NULL"); + if (strcmp(options[option_index].name, "audit-enable") == 0) { + server->audit_enabled = true; + } else if (strcmp(options[option_index].name, "audit-log-file") == 0) { + server->audit_log_file = strdup(optarg); + } else if (strcmp(options[option_index].name, "audit-field") == 0) { + if (validate_audit_field(optarg) != 0) { + lwsl_err("Invalid audit field format: %s\n", optarg); + cleanup(); + return -1; + } + server->audit_fields = xrealloc(server->audit_fields, (server->audit_fields_count + 1) * sizeof(char *)); + server->audit_fields[server->audit_fields_count++] = strdup(optarg); + } + break; default: print_help(); return -1; } } + + // Initialize audit fields + if (init_audit_fields() != 0) { + lwsl_err("Failed to initialize audit fields\n"); + cleanup(); + return -1; + } + server->prefs_json = strdup(json_object_to_json_string(client_prefs)); json_object_put(client_prefs); @@ -632,5 +712,20 @@ int main(int argc, char **argv) { // cleanup server_free(server); + cleanup(); return 0; } + +void cleanup(void) { + if (server->audit_enabled) { + audit_cleanup(); + } + + if (server->audit_fields) { + for (char **field = server->audit_fields; *field; field++) { + free(*field); + } + free(server->audit_fields); + } + +} diff --git a/src/server.h b/src/server.h index fcd82ccb..00194b62 100644 --- a/src/server.h +++ b/src/server.h @@ -3,6 +3,7 @@ #include #include "pty.h" +#include "audit.h" // client message #define INPUT '0' @@ -59,14 +60,15 @@ struct pss_tty { typedef struct { struct pss_tty *pss; bool ws_closed; + int last_audit_line; } pty_ctx_t; struct server { int client_count; // client count - int serv_buffer_size; // service buffer size char *prefs_json; // client preferences char *credential; // encoded basic auth credential char *auth_header; // header name used for auth proxy + char *username; // username for audit logging char *index; // custom index.html char *command; // full command line char **argv; // command with arguments @@ -79,9 +81,16 @@ struct server { bool check_origin; // whether allow websocket connection from different origin int max_clients; // maximum clients to support bool once; // whether accept only one client and exit on disconnection - bool exit_no_conn; // whether exit on all clients disconnection char socket_path[255]; // UNIX domain socket path char terminal_type[30]; // terminal type to report + char *audit_log; // audit log file path + uv_loop_t *loop; // the libuv event loop + + // Audit log configuration + bool audit_enabled; + char *audit_log_file; + char **audit_fields; // NULL-terminated array of custom fields + int audit_fields_count; // number of audit fields };