mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 09:14:38 +08:00
Fixes: - message_load() now holds g_message_file_lock for the read, so :last [N] can no longer observe a half-written line while message_save() is flushing. - constant_time_strcmp() accumulates the length difference in size_t. The old code truncated to unsigned char, which collapsed pairs whose lengths differed by a multiple of 256 down to 0 and lost the signal. Refactor: - buffer_appendf() / buffer_append_bytes() moved to common.c; the two identical copies in ssh_server.c and tui.c have been removed. Docs / cleanup: - README clarifies that exec 'post' uses the SSH login name as the author and that anonymous mode performs no identity check. - Removed TODO.md (both items completed) and docs/README.old. - Trimmed the auto-generated 2025 entry block from docs/CHANGELOG.md and added a 2026-05-16 entry summarising this change.
353 lines
11 KiB
C
353 lines
11 KiB
C
#ifndef _DEFAULT_SOURCE
|
|
#define _DEFAULT_SOURCE /* for timegm() on glibc */
|
|
#endif
|
|
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
|
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
|
|
#endif
|
|
#include "message.h"
|
|
#include "utf8.h"
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static time_t parse_rfc3339_utc(const char *timestamp_str) {
|
|
struct tm tm = {0};
|
|
|
|
if (!timestamp_str) {
|
|
return (time_t)-1;
|
|
}
|
|
|
|
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
|
if (!result || *result != '\0') {
|
|
return (time_t)-1;
|
|
}
|
|
|
|
return timegm(&tm);
|
|
}
|
|
|
|
/* Initialize message subsystem */
|
|
void message_init(void) {
|
|
/* Nothing to initialize for now */
|
|
}
|
|
|
|
/* Load messages from log file - Optimized for large files.
|
|
* Holds g_message_file_lock for the duration of the read so concurrent
|
|
* message_save() calls from chat threads cannot interleave a partial line. */
|
|
int message_load(message_t **messages, int max_messages) {
|
|
char log_path[PATH_MAX];
|
|
|
|
/* Always allocate the message array */
|
|
message_t *msg_array = calloc(max_messages, sizeof(message_t));
|
|
if (!msg_array) {
|
|
return 0;
|
|
}
|
|
|
|
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
|
*messages = msg_array;
|
|
return 0;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_message_file_lock);
|
|
|
|
FILE *fp = fopen(log_path, "r");
|
|
if (!fp) {
|
|
/* File doesn't exist yet, no messages */
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*messages = msg_array;
|
|
return 0;
|
|
}
|
|
|
|
/* Seek to end */
|
|
if (fseek(fp, 0, SEEK_END) != 0) {
|
|
fclose(fp);
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*messages = msg_array;
|
|
return 0;
|
|
}
|
|
|
|
long file_size = ftell(fp);
|
|
if (file_size <= 0) {
|
|
fclose(fp);
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*messages = msg_array;
|
|
return 0;
|
|
}
|
|
|
|
/* Scan backwards to find the start position */
|
|
int newlines_found = 0;
|
|
long pos = file_size - 1;
|
|
/* Skip the very last byte if it's a newline */
|
|
if (pos >= 0) {
|
|
/* Read last char */
|
|
fseek(fp, pos, SEEK_SET);
|
|
if (fgetc(fp) == '\n') {
|
|
pos--;
|
|
}
|
|
}
|
|
|
|
/* Read backwards in chunks for performance */
|
|
#define CHUNK_SIZE 4096
|
|
char chunk[CHUNK_SIZE];
|
|
|
|
while (pos >= 0 && newlines_found < max_messages) {
|
|
long read_size = (pos >= CHUNK_SIZE) ? CHUNK_SIZE : (pos + 1);
|
|
long read_pos = pos - read_size + 1;
|
|
|
|
fseek(fp, read_pos, SEEK_SET);
|
|
if (fread(chunk, 1, read_size, fp) != (size_t)read_size) {
|
|
break;
|
|
}
|
|
|
|
/* Scan chunk backwards */
|
|
for (int i = read_size - 1; i >= 0; i--) {
|
|
if (chunk[i] == '\n') {
|
|
newlines_found++;
|
|
if (newlines_found >= max_messages) {
|
|
/* Found our start point: one char after this newline */
|
|
fseek(fp, read_pos + i + 1, SEEK_SET);
|
|
goto read_messages;
|
|
}
|
|
}
|
|
}
|
|
|
|
pos -= read_size;
|
|
}
|
|
|
|
/* If we got here, we reached start of file or didn't find enough newlines */
|
|
fseek(fp, 0, SEEK_SET);
|
|
|
|
read_messages:;
|
|
char line[2048];
|
|
int count = 0;
|
|
|
|
/* Now read forward */
|
|
while (fgets(line, sizeof(line), fp) && count < max_messages) {
|
|
/* Check for oversized lines */
|
|
size_t line_len = strlen(line);
|
|
if (line_len >= sizeof(line) - 1) {
|
|
/* Skip remainder of line */
|
|
int c;
|
|
while ((c = fgetc(fp)) != '\n' && c != EOF);
|
|
continue;
|
|
}
|
|
|
|
/* Format: RFC3339_timestamp|username|content */
|
|
char line_copy[2048];
|
|
strncpy(line_copy, line, sizeof(line_copy) - 1);
|
|
line_copy[sizeof(line_copy) - 1] = '\0';
|
|
|
|
char *timestamp_str = strtok(line_copy, "|");
|
|
char *username = strtok(NULL, "|");
|
|
char *content = strtok(NULL, "\n");
|
|
|
|
/* Validate all fields exist and are non-empty */
|
|
if (!timestamp_str || !username || !content) {
|
|
continue;
|
|
}
|
|
if (username[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
/* Validate field lengths */
|
|
if (strlen(username) >= MAX_USERNAME_LEN) {
|
|
continue;
|
|
}
|
|
if (strlen(content) >= MAX_MESSAGE_LEN) {
|
|
continue;
|
|
}
|
|
|
|
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) {
|
|
continue;
|
|
}
|
|
|
|
/* Parse strict UTC RFC3339 timestamp */
|
|
time_t msg_time = parse_rfc3339_utc(timestamp_str);
|
|
if (msg_time == (time_t)-1) {
|
|
continue;
|
|
}
|
|
|
|
/* Validate timestamp is reasonable (not in far future or past) */
|
|
time_t now = time(NULL);
|
|
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
|
|
continue;
|
|
}
|
|
|
|
msg_array[count].timestamp = msg_time;
|
|
strncpy(msg_array[count].username, username, MAX_USERNAME_LEN - 1);
|
|
msg_array[count].username[MAX_USERNAME_LEN - 1] = '\0';
|
|
strncpy(msg_array[count].content, content, MAX_MESSAGE_LEN - 1);
|
|
msg_array[count].content[MAX_MESSAGE_LEN - 1] = '\0';
|
|
count++;
|
|
}
|
|
|
|
fclose(fp);
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*messages = msg_array;
|
|
return count;
|
|
}
|
|
|
|
/* Save a message to log file */
|
|
int message_save(const message_t *msg) {
|
|
char log_path[PATH_MAX];
|
|
int rc = 0;
|
|
|
|
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_message_file_lock);
|
|
|
|
FILE *fp = fopen(log_path, "a");
|
|
if (!fp) {
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
return -1;
|
|
}
|
|
|
|
/* Format timestamp as RFC3339 */
|
|
char timestamp[64];
|
|
struct tm tm_info;
|
|
gmtime_r(&msg->timestamp, &tm_info);
|
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
|
|
|
/* Sanitize username and content to prevent log injection */
|
|
char safe_username[MAX_USERNAME_LEN];
|
|
char safe_content[MAX_MESSAGE_LEN];
|
|
|
|
strncpy(safe_username, msg->username, sizeof(safe_username) - 1);
|
|
safe_username[sizeof(safe_username) - 1] = '\0';
|
|
|
|
strncpy(safe_content, msg->content, sizeof(safe_content) - 1);
|
|
safe_content[sizeof(safe_content) - 1] = '\0';
|
|
|
|
/* Replace pipe characters and newlines to prevent log format corruption */
|
|
for (char *p = safe_username; *p; p++) {
|
|
if (*p == '|' || *p == '\n' || *p == '\r') {
|
|
*p = '_';
|
|
}
|
|
}
|
|
for (char *p = safe_content; *p; p++) {
|
|
if (*p == '|' || *p == '\n' || *p == '\r') {
|
|
*p = ' ';
|
|
}
|
|
}
|
|
|
|
/* Write to file: timestamp|username|content */
|
|
if (fprintf(fp, "%s|%s|%s\n", timestamp, safe_username, safe_content) < 0 ||
|
|
fflush(fp) != 0) {
|
|
rc = -1;
|
|
}
|
|
|
|
/* Rotate if the log exceeds MAX_LOG_SIZE */
|
|
long file_size = ftell(fp);
|
|
fclose(fp);
|
|
|
|
if (file_size > MAX_LOG_SIZE) {
|
|
char backup_path[PATH_MAX + 4];
|
|
snprintf(backup_path, sizeof(backup_path), "%s.1", log_path);
|
|
rename(log_path, backup_path);
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
return rc;
|
|
}
|
|
|
|
/* Search log file for messages whose username or content contains query.
|
|
* Case-insensitive. Returns the last max_results matches (most recent); caller frees *results. */
|
|
int message_search(const char *query, message_t **results, int max_results) {
|
|
char log_path[PATH_MAX];
|
|
|
|
message_t *res = calloc(max_results, sizeof(message_t));
|
|
if (!res) return 0;
|
|
|
|
if (!query || query[0] == '\0' ||
|
|
tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
|
*results = res;
|
|
return 0;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_message_file_lock);
|
|
FILE *fp = fopen(log_path, "r");
|
|
if (!fp) {
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*results = res;
|
|
return 0;
|
|
}
|
|
|
|
char line[2048];
|
|
int count = 0;
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
size_t line_len = strlen(line);
|
|
if (line_len >= sizeof(line) - 1) {
|
|
int c;
|
|
while ((c = fgetc(fp)) != '\n' && c != EOF);
|
|
continue;
|
|
}
|
|
|
|
char line_copy[2048];
|
|
strncpy(line_copy, line, sizeof(line_copy) - 1);
|
|
line_copy[sizeof(line_copy) - 1] = '\0';
|
|
|
|
char *timestamp_str = strtok(line_copy, "|");
|
|
char *username = strtok(NULL, "|");
|
|
char *content = strtok(NULL, "\n");
|
|
|
|
if (!timestamp_str || !username || !content || username[0] == '\0') continue;
|
|
if (strlen(username) >= MAX_USERNAME_LEN || strlen(content) >= MAX_MESSAGE_LEN) continue;
|
|
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) continue;
|
|
|
|
if (strcasestr(username, query) == NULL && strcasestr(content, query) == NULL) continue;
|
|
|
|
time_t msg_time = parse_rfc3339_utc(timestamp_str);
|
|
if (msg_time == (time_t)-1) continue;
|
|
|
|
message_t m;
|
|
m.timestamp = msg_time;
|
|
strncpy(m.username, username, MAX_USERNAME_LEN - 1);
|
|
m.username[MAX_USERNAME_LEN - 1] = '\0';
|
|
strncpy(m.content, content, MAX_MESSAGE_LEN - 1);
|
|
m.content[MAX_MESSAGE_LEN - 1] = '\0';
|
|
|
|
if (count < max_results) {
|
|
res[count++] = m;
|
|
} else {
|
|
memmove(&res[0], &res[1], (max_results - 1) * sizeof(message_t));
|
|
res[max_results - 1] = m;
|
|
/* count stays at max_results */
|
|
}
|
|
}
|
|
|
|
fclose(fp);
|
|
pthread_mutex_unlock(&g_message_file_lock);
|
|
*results = res;
|
|
return (count < max_results) ? count : max_results;
|
|
}
|
|
|
|
/* Format a message for display */
|
|
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
|
struct tm tm_info;
|
|
localtime_r(&msg->timestamp, &tm_info);
|
|
char time_str[64];
|
|
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M %Z", &tm_info);
|
|
|
|
int written = snprintf(buffer, buf_size, "[%s] %s: %s", time_str, msg->username, msg->content);
|
|
|
|
/* If snprintf truncated, the last UTF-8 character may be incomplete.
|
|
* Re-validate and trim any trailing partial sequence. */
|
|
if (written >= (int)buf_size) {
|
|
size_t len = strlen(buffer);
|
|
while (len > 0 && (buffer[len - 1] & 0xC0) == 0x80) {
|
|
len--; /* walk back continuation bytes */
|
|
}
|
|
if (len > 0 && (unsigned char)buffer[len - 1] >= 0xC0) {
|
|
/* This is a start byte whose sequence was truncated */
|
|
buffer[len - 1] = '\0';
|
|
}
|
|
}
|
|
|
|
/* Truncate to terminal width */
|
|
if (utf8_string_width(buffer) > width) {
|
|
utf8_truncate(buffer, width);
|
|
}
|
|
}
|