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.
This commit is contained in:
m1ngsama 2026-05-16 22:44:41 +08:00
parent eead27544c
commit d9382882d1
9 changed files with 87 additions and 267 deletions

View file

@ -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

12
TODO.md
View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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 */

View file

@ -1,5 +1,6 @@
#include "common.h"
#include <errno.h>
#include <stdarg.h>
#include <sys/stat.h>
#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;
}
}

View file

@ -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;
}

View file

@ -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. */

View file

@ -2,7 +2,6 @@
#include "ssh_server.h"
#include "chat_room.h"
#include "utf8.h"
#include <stdarg.h>
#include <unistd.h>
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;