From d9382882d1847517715b1cdd714f09a518676e0d Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sat, 16 May 2026 22:44:41 +0800 Subject: [PATCH] chore: bug fixes and code cleanup 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. --- README.md | 2 + TODO.md | 12 ------ docs/CHANGELOG.md | 87 ++++++++--------------------------------- docs/README.old | 98 ----------------------------------------------- include/common.h | 8 ++++ src/common.c | 42 ++++++++++++++++++++ src/message.c | 10 ++++- src/ssh_server.c | 54 +++++--------------------- src/tui.c | 41 -------------------- 9 files changed, 87 insertions(+), 267 deletions(-) delete mode 100644 TODO.md delete mode 100644 docs/README.old diff --git a/README.md b/README.md index d319420..0f538d9 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,8 @@ ssh -p 2222 operator@chat.m1ng.space post "service notice" ssh -p 2222 chat.m1ng.space post "/me deploys v2.0" ``` +**`post` identity**: the message is attributed to the SSH login name (the `user@` part of the URL, falling back to `anonymous`). In the default anonymous-access configuration there is no identity check, so any client can post as any name. Set `TNT_ACCESS_TOKEN` if you need authenticated posting. + ## Development ### Building diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 07705d5..0000000 --- a/TODO.md +++ /dev/null @@ -1,12 +0,0 @@ -# TODO - -## Maintenance -- [x] Replace deprecated `libssh` functions in `src/ssh_server.c`: - - ~~`ssh_message_auth_password`~~ → `auth_password_function` callback (✓ completed) - - ~~`ssh_message_channel_request_pty_width/height`~~ → `channel_pty_request_function` callback (✓ completed) - - Migrated to callback-based server API as of libssh 0.9+ - -## Future Features -- [x] Implement robust command handling for non-interactive SSH exec requests. - - Basic exec support completed (handles `exit` command) - - All tests passing diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5689bdb..007961b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 2026-05-16 - Internal cleanup + +### Fixed +- `message_load()` now holds `g_message_file_lock` for the duration of the read. + Previously `:last [N]` could race with `message_save()` and observe a + half-written line. +- `constant_time_strcmp()` accumulates the length difference in `size_t` instead + of `unsigned char`. The old code lost the length-mismatch signal when the + two lengths differed by a multiple of 256. + +### Changed +- `buffer_appendf()` and `buffer_append_bytes()` moved to `common.c`; the two + identical copies in `ssh_server.c` and `tui.c` have been removed. +- Removed `TODO.md` (both items completed) and `docs/README.old` (superseded by + the root `README.md`). +- Trimmed the auto-generated 2025 entry block from this changelog. + ## 2026-04-23 - Chat UX Commands and MOTD ### Added @@ -108,73 +125,3 @@ All changes maintain backward compatibility. Server remains open by default. ### Changed - Improved error handling throughout - Better memory management in message loading - -## 2025 -- Ongoing development and improvements -- Bug fixes and optimizations -- Feature enhancements -- Optimize performance (2025-01-10) -- Code cleanup (2025-01-15) -- Code cleanup (2025-01-17) -- Add minor improvements (2025-01-22) -- Code cleanup (2025-01-28) -- Fix edge cases (2025-02-03) -- Update documentation (2025-02-06) -- Fix edge cases (2025-02-07) -- Add minor improvements (2025-02-26) -- Update dependencies (2025-02-27) -- Fix edge cases (2025-03-01) -- Fix bugs and improve stability (2025-03-06) -- Fix bugs and improve stability (2025-03-12) -- Minor fixes (2025-03-17) -- Add minor improvements (2025-03-18) -- Refactor code structure (2025-03-24) -- Update dependencies (2025-03-27) -- Improve error handling (2025-03-28) -- Improve error handling (2025-04-03) -- Update documentation (2025-04-07) -- Update documentation (2025-04-13) -- Code cleanup (2025-04-15) -- Fix bugs and improve stability (2025-04-16) -- Add minor improvements (2025-04-17) -- Minor fixes (2025-04-23) -- Code cleanup (2025-04-24) -- Fix edge cases (2025-04-25) -- Refactor code structure (2025-05-13) -- Fix edge cases (2025-05-14) -- Minor fixes (2025-06-03) -- Code cleanup (2025-06-05) -- Add minor improvements (2025-06-10) -- Fix bugs and improve stability (2025-06-18) -- Update dependencies (2025-06-24) -- Optimize performance (2025-06-30) -- Update documentation (2025-07-07) -- Refactor code structure (2025-07-17) -- Fix bugs and improve stability (2025-07-19) -- Refactor code structure (2025-07-21) -- Code cleanup (2025-07-27) -- Code cleanup (2025-08-04) -- Minor fixes (2025-08-28) -- Improve error handling (2025-09-05) -- Update documentation (2025-09-09) -- Code cleanup (2025-09-15) -- Fix bugs and improve stability (2025-09-19) -- Update documentation (2025-09-25) -- Fix bugs and improve stability (2025-10-06) -- Fix bugs and improve stability (2025-10-13) -- Fix bugs and improve stability (2025-10-16) -- Optimize performance (2025-10-17) -- Add minor improvements (2025-10-22) -- Code cleanup (2025-10-26) -- Add minor improvements (2025-10-28) -- Fix edge cases (2025-10-29) -- Fix bugs and improve stability (2025-10-30) -- Optimize performance (2025-11-04) -- Improve error handling (2025-11-07) -- Update documentation (2025-11-12) -- Fix bugs and improve stability (2025-11-14) -- Update documentation (2025-11-17) -- Add minor improvements (2025-11-18) -- Refactor code structure (2025-11-19) -- Fix bugs and improve stability (2025-11-20) -- Minor fixes (2025-11-24) diff --git a/docs/README.old b/docs/README.old deleted file mode 100644 index 514afdd..0000000 --- a/docs/README.old +++ /dev/null @@ -1,98 +0,0 @@ -TNT(1) User Commands TNT(1) - -NAME - tnt - terminal chat server with vim-style interface - -SYNOPSIS - tnt [-p port] [-h] - -DESCRIPTION - TNT (TNT's Not Tunnel) is a lightweight SSH chat server. - Supports vim-style navigation and message history browsing. - -INSTALLATION - curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh - - Or download binary from: - https://github.com/m1ngsama/TNT/releases - -USAGE - Start server: - tnt # Listen on port 2222 - tnt -p 3333 # Listen on port 3333 - PORT=3333 tnt # Same as above - - Connect as client: - ssh -p 2222 localhost - -OPTIONS - -p port Listen on specified port (default: 2222) - -h Display help message - -MODES - INSERT Type and send messages (default mode) - ESC Switch to NORMAL mode - Enter Send message - Backspace Delete character - - NORMAL Browse message history - i Return to INSERT mode - : Enter COMMAND mode - j/k Scroll down/up - g/G Jump to top/bottom - ? Show help - - COMMAND Execute commands - :list List online users - :help Show available commands - ESC Return to NORMAL mode - -ENVIRONMENT - PORT Server port (default: 2222) - -FILES - messages.log Message history (append-only) - host_key SSH host key (auto-generated) - -EXAMPLES - Start server on port 3000: - PORT=3000 tnt - - Connect and set username: - ssh -p 2222 localhost - # Enter username when prompted - - Production deployment with systemd: - sudo cp tnt.service /etc/systemd/system/ - sudo systemctl enable --now tnt - -DIAGNOSTICS - Build with debug symbols: - make debug - - Check for memory leaks: - make asan - ASAN_OPTIONS=detect_leaks=1 ./tnt - - Run tests: - ./test_basic.sh - ./test_stress.sh 50 120 - -BUGS - Report bugs at: https://github.com/m1ngsama/TNT/issues - -SEE ALSO - ssh(1), sshd(8) - - Project documentation: - HACKING Developer guide - DEPLOYMENT.md Production setup - CICD.md CI/CD workflow - -AUTHOR - Written by m1ng. - -LICENSE - MIT License. See LICENSE file for details. - -TNT 1.0 December 2025 TNT(1) diff --git a/include/common.h b/include/common.h index 36d8769..e284781 100644 --- a/include/common.h +++ b/include/common.h @@ -54,4 +54,12 @@ const char* tnt_state_dir(void); int tnt_ensure_state_dir(void); int tnt_state_path(char *buffer, size_t buf_size, const char *filename); +/* Bounded string buffer builders. Both append to `buffer[*pos..]`, advance + * `*pos`, and always keep the buffer NUL-terminated. They never write past + * `buf_size - 1` and become no-ops once the buffer is full. */ +void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos, + const char *data, size_t len); +void buffer_appendf(char *buffer, size_t buf_size, size_t *pos, + const char *fmt, ...); + #endif /* COMMON_H */ diff --git a/src/common.c b/src/common.c index b466eba..2cc8b5f 100644 --- a/src/common.c +++ b/src/common.c @@ -1,5 +1,6 @@ #include "common.h" #include +#include #include #ifndef PATH_MAX @@ -84,3 +85,44 @@ int tnt_state_path(char *buffer, size_t buf_size, const char *filename) { return 0; } + +void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos, + const char *data, size_t len) { + size_t available; + size_t to_copy; + + if (!buffer || !pos || !data || len == 0 || buf_size == 0 || + *pos >= buf_size - 1) { + return; + } + + available = (buf_size - 1) - *pos; + to_copy = (len < available) ? len : available; + memcpy(buffer + *pos, data, to_copy); + *pos += to_copy; + buffer[*pos] = '\0'; +} + +void buffer_appendf(char *buffer, size_t buf_size, size_t *pos, + const char *fmt, ...) { + va_list args; + int written; + + if (!buffer || !pos || !fmt || buf_size == 0 || *pos >= buf_size - 1) { + return; + } + + va_start(args, fmt); + written = vsnprintf(buffer + *pos, buf_size - *pos, fmt, args); + va_end(args); + + if (written < 0) { + return; + } + + if ((size_t)written >= buf_size - *pos) { + *pos = buf_size - 1; + } else { + *pos += (size_t)written; + } +} diff --git a/src/message.c b/src/message.c index 123cbbb..e48b5cd 100644 --- a/src/message.c +++ b/src/message.c @@ -31,7 +31,9 @@ void message_init(void) { /* Nothing to initialize for now */ } -/* Load messages from log file - Optimized for large files */ +/* 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]; @@ -46,9 +48,12 @@ int message_load(message_t **messages, int max_messages) { 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; } @@ -56,6 +61,7 @@ int message_load(message_t **messages, int max_messages) { /* Seek to end */ if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); + pthread_mutex_unlock(&g_message_file_lock); *messages = msg_array; return 0; } @@ -63,6 +69,7 @@ int message_load(message_t **messages, int max_messages) { long file_size = ftell(fp); if (file_size <= 0) { fclose(fp); + pthread_mutex_unlock(&g_message_file_lock); *messages = msg_array; return 0; } @@ -175,6 +182,7 @@ read_messages:; } fclose(fp); + pthread_mutex_unlock(&g_message_file_lock); *messages = msg_array; return count; } diff --git a/src/ssh_server.c b/src/ssh_server.c index 7ce775d..b070288 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -68,60 +68,24 @@ static int g_rate_limit_enabled = 1; static char g_access_token[256] = ""; static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT; -static void buffer_appendf(char *buffer, size_t buf_size, size_t *pos, - const char *fmt, ...) { - va_list args; - int written; - - if (!buffer || !pos || !fmt || buf_size == 0 || *pos >= buf_size - 1) { - return; - } - - va_start(args, fmt); - written = vsnprintf(buffer + *pos, buf_size - *pos, fmt, args); - va_end(args); - - if (written < 0) { - return; - } - - if ((size_t)written >= buf_size - *pos) { - *pos = buf_size - 1; - } else { - *pos += (size_t)written; - } -} - -static void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos, - const char *data, size_t len) { - size_t available; - size_t to_copy; - - if (!buffer || !pos || !data || len == 0 || buf_size == 0 || - *pos >= buf_size - 1) { - return; - } - - available = (buf_size - 1) - *pos; - to_copy = (len < available) ? len : available; - memcpy(buffer + *pos, data, to_copy); - *pos += to_copy; - buffer[*pos] = '\0'; -} - /* Constant-time string comparison to prevent timing side-channel attacks. * Always iterates over the full length of the secret (b) to avoid leaking * its length. When the input (a) is shorter, compares against zero bytes; - * the length mismatch is folded into the result separately. */ + * the length mismatch is folded into the result separately. + * + * Note: the length-diff is accumulated in size_t to avoid the bug where a + * narrower type (e.g. unsigned char) would collapse pairs like (300, 44) to + * 0 because 300 ^ 44 == 256 ^ (44 ^ 44) == 256 which truncates to 0. */ static bool constant_time_strcmp(const char *a, const char *b) { size_t len_a = strlen(a); size_t len_b = strlen(b); - volatile unsigned char result = (unsigned char)(len_a ^ len_b); + volatile size_t length_diff = len_a ^ len_b; + volatile unsigned char byte_diff = 0; for (size_t i = 0; i < len_b; i++) { unsigned char ca = (i < len_a) ? (unsigned char)a[i] : 0; - result |= ca ^ (unsigned char)b[i]; + byte_diff |= ca ^ (unsigned char)b[i]; } - return result == 0; + return length_diff == 0 && byte_diff == 0; } /* Safe integer parse from environment variable; returns fallback on error. */ diff --git a/src/tui.c b/src/tui.c index 38a0eaa..c26e3fc 100644 --- a/src/tui.c +++ b/src/tui.c @@ -2,7 +2,6 @@ #include "ssh_server.h" #include "chat_room.h" #include "utf8.h" -#include #include static bool is_join_leave_msg(const message_t *msg) { @@ -111,46 +110,6 @@ static void format_message_colored(const message_t *msg, char *buffer, } } -static void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos, - const char *data, size_t len) { - size_t available; - size_t to_copy; - - if (!buffer || !pos || !data || len == 0 || buf_size == 0 || *pos >= buf_size - 1) { - return; - } - - available = (buf_size - 1) - *pos; - to_copy = (len < available) ? len : available; - memcpy(buffer + *pos, data, to_copy); - *pos += to_copy; - buffer[*pos] = '\0'; -} - -static void buffer_appendf(char *buffer, size_t buf_size, size_t *pos, - const char *fmt, ...) { - va_list args; - int written; - - if (!buffer || !pos || !fmt || buf_size == 0 || *pos >= buf_size - 1) { - return; - } - - va_start(args, fmt); - written = vsnprintf(buffer + *pos, buf_size - *pos, fmt, args); - va_end(args); - - if (written < 0) { - return; - } - - if ((size_t)written >= buf_size - *pos) { - *pos = buf_size - 1; - } else { - *pos += (size_t)written; - } -} - /* Clear the screen */ void tui_clear_screen(client_t *client) { if (!client || !client->connected) return;