mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-05-10 19:00:57 +08:00
feat: consolidated improvements, manpage, and deployment prep
Bug fixes: - Fix data race on client->width/height (now _Atomic int) - Persist join/leave system messages via message_save() - Make room_add_message static to enforce lock contract - Fix execute_command mutating command_input directly - Increase help_copy buffer from 4096 to 8192 for CJK safety New features: - Add :msg/:w whisper command for private messaging - Add command history with UP/DOWN arrows in command mode - Add Ctrl+D/U/F/B page scrolling in normal mode - Add :q/:quit/:exit Vim-style disconnect Unix community: - Add tnt.1 manpage (roff format) with full documentation - Add manpage install/uninstall to Makefile
This commit is contained in:
parent
200e5a2f28
commit
e10b43074c
8 changed files with 393 additions and 63 deletions
3
Makefile
3
Makefile
|
|
@ -45,9 +45,12 @@ clean:
|
||||||
install: $(TARGET)
|
install: $(TARGET)
|
||||||
install -d $(DESTDIR)/usr/local/bin
|
install -d $(DESTDIR)/usr/local/bin
|
||||||
install -m 755 $(TARGET) $(DESTDIR)/usr/local/bin/
|
install -m 755 $(TARGET) $(DESTDIR)/usr/local/bin/
|
||||||
|
install -d $(DESTDIR)/usr/local/share/man/man1
|
||||||
|
install -m 644 tnt.1 $(DESTDIR)/usr/local/share/man/man1/
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm -f $(DESTDIR)/usr/local/bin/$(TARGET)
|
rm -f $(DESTDIR)/usr/local/bin/$(TARGET)
|
||||||
|
rm -f $(DESTDIR)/usr/local/share/man/man1/tnt.1
|
||||||
|
|
||||||
# Development targets
|
# Development targets
|
||||||
debug: CFLAGS += -g -DDEBUG
|
debug: CFLAGS += -g -DDEBUG
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,6 @@ void room_remove_client(chat_room_t *room, struct client *client);
|
||||||
/* Broadcast message to all clients */
|
/* Broadcast message to all clients */
|
||||||
void room_broadcast(chat_room_t *room, const message_t *msg);
|
void room_broadcast(chat_room_t *room, const message_t *msg);
|
||||||
|
|
||||||
/* Add message to room history */
|
|
||||||
void room_add_message(chat_room_t *room, const message_t *msg);
|
|
||||||
|
|
||||||
/* Get message by index (thread-safe value copy) */
|
/* Get message by index (thread-safe value copy) */
|
||||||
bool room_get_message(chat_room_t *room, int index, message_t *out);
|
bool room_get_message(chat_room_t *room, int index, message_t *out);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,17 @@ typedef struct client {
|
||||||
ssh_channel channel; /* SSH channel */
|
ssh_channel channel; /* SSH channel */
|
||||||
char username[MAX_USERNAME_LEN];
|
char username[MAX_USERNAME_LEN];
|
||||||
char client_ip[INET6_ADDRSTRLEN];
|
char client_ip[INET6_ADDRSTRLEN];
|
||||||
int width;
|
_Atomic int width;
|
||||||
int height;
|
_Atomic int height;
|
||||||
client_mode_t mode;
|
client_mode_t mode;
|
||||||
help_lang_t help_lang;
|
help_lang_t help_lang;
|
||||||
int scroll_pos;
|
int scroll_pos;
|
||||||
int help_scroll_pos;
|
int help_scroll_pos;
|
||||||
bool show_help;
|
bool show_help;
|
||||||
char command_input[256];
|
char command_input[256];
|
||||||
|
char command_history[16][256];
|
||||||
|
int command_history_count;
|
||||||
|
int command_history_pos;
|
||||||
char command_output[2048];
|
char command_output[2048];
|
||||||
char exec_command[MAX_EXEC_COMMAND_LEN];
|
char exec_command[MAX_EXEC_COMMAND_LEN];
|
||||||
char ssh_login[MAX_USERNAME_LEN];
|
char ssh_login[MAX_USERNAME_LEN];
|
||||||
|
|
|
||||||
|
|
@ -87,23 +87,9 @@ void room_remove_client(chat_room_t *room, struct client *client) {
|
||||||
pthread_rwlock_unlock(&room->lock);
|
pthread_rwlock_unlock(&room->lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Broadcast message to all clients */
|
/* Add message to room history (caller must hold write lock) */
|
||||||
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
static void room_add_message(chat_room_t *room, const message_t *msg) {
|
||||||
pthread_rwlock_wrlock(&room->lock);
|
|
||||||
|
|
||||||
/* Add to history */
|
|
||||||
room_add_message(room, msg);
|
|
||||||
room->update_seq++;
|
|
||||||
|
|
||||||
pthread_rwlock_unlock(&room->lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add message to room history */
|
|
||||||
void room_add_message(chat_room_t *room, const message_t *msg) {
|
|
||||||
/* Caller should hold write lock */
|
|
||||||
|
|
||||||
if (room->message_count >= MAX_MESSAGES) {
|
if (room->message_count >= MAX_MESSAGES) {
|
||||||
/* Shift messages to make room */
|
|
||||||
memmove(&room->messages[0], &room->messages[1],
|
memmove(&room->messages[0], &room->messages[1],
|
||||||
(MAX_MESSAGES - 1) * sizeof(message_t));
|
(MAX_MESSAGES - 1) * sizeof(message_t));
|
||||||
room->message_count = MAX_MESSAGES - 1;
|
room->message_count = MAX_MESSAGES - 1;
|
||||||
|
|
@ -112,6 +98,16 @@ void room_add_message(chat_room_t *room, const message_t *msg) {
|
||||||
room->messages[room->message_count++] = *msg;
|
room->messages[room->message_count++] = *msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Broadcast message to all clients */
|
||||||
|
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
||||||
|
pthread_rwlock_wrlock(&room->lock);
|
||||||
|
|
||||||
|
room_add_message(room, msg);
|
||||||
|
room->update_seq++;
|
||||||
|
|
||||||
|
pthread_rwlock_unlock(&room->lock);
|
||||||
|
}
|
||||||
|
|
||||||
/* Get message by index (thread-safe value copy) */
|
/* Get message by index (thread-safe value copy) */
|
||||||
bool room_get_message(chat_room_t *room, int index, message_t *out) {
|
bool room_get_message(chat_room_t *room, int index, message_t *out) {
|
||||||
if (!room || !out) return false;
|
if (!room || !out) return false;
|
||||||
|
|
|
||||||
197
src/ssh_server.c
197
src/ssh_server.c
|
|
@ -1083,7 +1083,10 @@ static int execute_exec_command(client_t *client) {
|
||||||
|
|
||||||
/* Execute a command */
|
/* Execute a command */
|
||||||
static void execute_command(client_t *client) {
|
static void execute_command(client_t *client) {
|
||||||
char *cmd = client->command_input;
|
char cmd_buf[256];
|
||||||
|
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
||||||
|
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
|
||||||
|
char *cmd = cmd_buf;
|
||||||
char output[2048] = {0};
|
char output[2048] = {0};
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|
||||||
|
|
@ -1098,6 +1101,21 @@ static void execute_command(client_t *client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Save to command history */
|
||||||
|
if (cmd[0] != '\0') {
|
||||||
|
int max_hist = 16;
|
||||||
|
if (client->command_history_count >= max_hist) {
|
||||||
|
memmove(&client->command_history[0], &client->command_history[1],
|
||||||
|
(max_hist - 1) * sizeof(client->command_history[0]));
|
||||||
|
client->command_history_count = max_hist - 1;
|
||||||
|
}
|
||||||
|
strncpy(client->command_history[client->command_history_count],
|
||||||
|
cmd, sizeof(client->command_history[0]) - 1);
|
||||||
|
client->command_history[client->command_history_count][sizeof(client->command_history[0]) - 1] = '\0';
|
||||||
|
client->command_history_count++;
|
||||||
|
client->command_history_pos = client->command_history_count;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(cmd, "list") == 0 || strcmp(cmd, "users") == 0 ||
|
if (strcmp(cmd, "list") == 0 || strcmp(cmd, "users") == 0 ||
|
||||||
strcmp(cmd, "who") == 0) {
|
strcmp(cmd, "who") == 0) {
|
||||||
buffer_appendf(output, sizeof(output), &pos,
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
|
@ -1129,11 +1147,59 @@ static void execute_command(client_t *client) {
|
||||||
"========================================\n"
|
"========================================\n"
|
||||||
" Available Commands\n"
|
" Available Commands\n"
|
||||||
"========================================\n"
|
"========================================\n"
|
||||||
"list, users, who - Show online users\n"
|
"list, users, who - Show online users\n"
|
||||||
"help, commands - Show this help\n"
|
"msg/w <user> <text> - Whisper to user\n"
|
||||||
"clear, cls - Clear command output\n"
|
"help, commands - Show this help\n"
|
||||||
|
"clear, cls - Clear command output\n"
|
||||||
|
"q, quit, exit - Disconnect\n"
|
||||||
|
"Up/Down arrows - Command history\n"
|
||||||
"========================================\n");
|
"========================================\n");
|
||||||
|
|
||||||
|
} else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
||||||
|
char *rest = (cmd[0] == 'w') ? cmd + 2 : cmd + 4;
|
||||||
|
while (*rest == ' ') rest++;
|
||||||
|
char target_name[MAX_USERNAME_LEN] = {0};
|
||||||
|
int ti = 0;
|
||||||
|
while (*rest && *rest != ' ' && ti < MAX_USERNAME_LEN - 1) {
|
||||||
|
target_name[ti++] = *rest++;
|
||||||
|
}
|
||||||
|
while (*rest == ' ') rest++;
|
||||||
|
|
||||||
|
if (target_name[0] == '\0' || rest[0] == '\0') {
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
"Usage: msg <username> <message>\n"
|
||||||
|
" w <username> <message>\n");
|
||||||
|
} else {
|
||||||
|
bool found = false;
|
||||||
|
pthread_rwlock_rdlock(&g_room->lock);
|
||||||
|
for (int i = 0; i < g_room->client_count; i++) {
|
||||||
|
if (strcmp(g_room->clients[i]->username, target_name) == 0) {
|
||||||
|
char whisper[MAX_MESSAGE_LEN];
|
||||||
|
snprintf(whisper, sizeof(whisper),
|
||||||
|
"\r\n\033[35m[whisper from %s]: %s\033[0m\r\n",
|
||||||
|
client->username, rest);
|
||||||
|
client_send(g_room->clients[i], whisper, strlen(whisper));
|
||||||
|
g_room->clients[i]->redraw_pending = true;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_rwlock_unlock(&g_room->lock);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
"Whisper sent to %s\n", target_name);
|
||||||
|
} else {
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
"User '%s' not found\n", target_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0 ||
|
||||||
|
strcmp(cmd, "exit") == 0) {
|
||||||
|
client->connected = false;
|
||||||
|
return;
|
||||||
|
|
||||||
} else if (strcmp(cmd, "clear") == 0 || strcmp(cmd, "cls") == 0) {
|
} else if (strcmp(cmd, "clear") == 0 || strcmp(cmd, "cls") == 0) {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "Command output cleared\n");
|
buffer_appendf(output, sizeof(output), &pos, "Command output cleared\n");
|
||||||
|
|
||||||
|
|
@ -1253,62 +1319,111 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_NORMAL:
|
case MODE_NORMAL: {
|
||||||
|
int nm_msg_count = room_get_message_count(g_room);
|
||||||
|
int nm_msg_height = client->height - 3;
|
||||||
|
if (nm_msg_height < 1) nm_msg_height = 1;
|
||||||
|
int nm_max_scroll = nm_msg_count - nm_msg_height;
|
||||||
|
if (nm_max_scroll < 0) nm_max_scroll = 0;
|
||||||
|
|
||||||
if (key == 'i') {
|
if (key == 'i') {
|
||||||
client->mode = MODE_INSERT;
|
client->mode = MODE_INSERT;
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
} else if (key == ':') {
|
} else if (key == ':') {
|
||||||
client->mode = MODE_COMMAND;
|
client->mode = MODE_COMMAND;
|
||||||
client->command_input[0] = '\0';
|
client->command_input[0] = '\0';
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed - prevents double colon */
|
return true;
|
||||||
} else if (key == 'j') {
|
} else if (key == 'j') {
|
||||||
/* Get message count atomically to prevent TOCTOU */
|
if (client->scroll_pos < nm_max_scroll) {
|
||||||
int max_scroll = room_get_message_count(g_room);
|
|
||||||
int msg_height = client->height - 3;
|
|
||||||
if (msg_height < 1) msg_height = 1;
|
|
||||||
max_scroll = max_scroll - msg_height;
|
|
||||||
if (max_scroll < 0) max_scroll = 0;
|
|
||||||
|
|
||||||
if (client->scroll_pos < max_scroll) {
|
|
||||||
client->scroll_pos++;
|
client->scroll_pos++;
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
}
|
}
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
} else if (key == 'k' && client->scroll_pos > 0) {
|
} else if (key == 'k' && client->scroll_pos > 0) {
|
||||||
client->scroll_pos--;
|
client->scroll_pos--;
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
|
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||||
|
int half = nm_msg_height / 2;
|
||||||
|
if (half < 1) half = 1;
|
||||||
|
client->scroll_pos += half;
|
||||||
|
if (client->scroll_pos > nm_max_scroll) client->scroll_pos = nm_max_scroll;
|
||||||
|
tui_render_screen(client);
|
||||||
|
return true;
|
||||||
|
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||||
|
int half = nm_msg_height / 2;
|
||||||
|
if (half < 1) half = 1;
|
||||||
|
client->scroll_pos -= half;
|
||||||
|
if (client->scroll_pos < 0) client->scroll_pos = 0;
|
||||||
|
tui_render_screen(client);
|
||||||
|
return true;
|
||||||
|
} else if (key == 6) { /* Ctrl+F: full page down */
|
||||||
|
client->scroll_pos += nm_msg_height;
|
||||||
|
if (client->scroll_pos > nm_max_scroll) client->scroll_pos = nm_max_scroll;
|
||||||
|
tui_render_screen(client);
|
||||||
|
return true;
|
||||||
|
} else if (key == 2) { /* Ctrl+B: full page up */
|
||||||
|
client->scroll_pos -= nm_msg_height;
|
||||||
|
if (client->scroll_pos < 0) client->scroll_pos = 0;
|
||||||
|
tui_render_screen(client);
|
||||||
|
return true;
|
||||||
} else if (key == 'g') {
|
} else if (key == 'g') {
|
||||||
client->scroll_pos = 0;
|
client->scroll_pos = 0;
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
} else if (key == 'G') {
|
} else if (key == 'G') {
|
||||||
/* Get message count atomically to prevent TOCTOU */
|
client->scroll_pos = nm_max_scroll;
|
||||||
int max_scroll = room_get_message_count(g_room);
|
|
||||||
int msg_height = client->height - 3;
|
|
||||||
if (msg_height < 1) msg_height = 1;
|
|
||||||
max_scroll = max_scroll - msg_height;
|
|
||||||
if (max_scroll < 0) max_scroll = 0;
|
|
||||||
|
|
||||||
client->scroll_pos = max_scroll;
|
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
} else if (key == '?') {
|
} else if (key == '?') {
|
||||||
client->show_help = true;
|
client->show_help = true;
|
||||||
client->help_scroll_pos = 0;
|
client->help_scroll_pos = 0;
|
||||||
tui_render_help(client);
|
tui_render_help(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case MODE_COMMAND:
|
case MODE_COMMAND:
|
||||||
if (key == 27) { /* ESC */
|
if (key == 27) { /* ESC - check for arrow key sequences */
|
||||||
|
char seq[2];
|
||||||
|
int n = ssh_channel_read_timeout(client->channel, seq, 1, 0, 50);
|
||||||
|
if (n == 1 && seq[0] == '[') {
|
||||||
|
n = ssh_channel_read_timeout(client->channel, &seq[1], 1, 0, 50);
|
||||||
|
if (n == 1) {
|
||||||
|
if (seq[1] == 'A') { /* Up arrow */
|
||||||
|
if (client->command_history_count > 0 &&
|
||||||
|
client->command_history_pos > 0) {
|
||||||
|
client->command_history_pos--;
|
||||||
|
strncpy(client->command_input,
|
||||||
|
client->command_history[client->command_history_pos],
|
||||||
|
sizeof(client->command_input) - 1);
|
||||||
|
client->command_input[sizeof(client->command_input) - 1] = '\0';
|
||||||
|
tui_render_screen(client);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (seq[1] == 'B') { /* Down arrow */
|
||||||
|
if (client->command_history_pos < client->command_history_count - 1) {
|
||||||
|
client->command_history_pos++;
|
||||||
|
strncpy(client->command_input,
|
||||||
|
client->command_history[client->command_history_pos],
|
||||||
|
sizeof(client->command_input) - 1);
|
||||||
|
client->command_input[sizeof(client->command_input) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
client->command_history_pos = client->command_history_count;
|
||||||
|
client->command_input[0] = '\0';
|
||||||
|
}
|
||||||
|
tui_render_screen(client);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
client->mode = MODE_NORMAL;
|
client->mode = MODE_NORMAL;
|
||||||
client->command_input[0] = '\0';
|
client->command_input[0] = '\0';
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true; /* Key consumed */
|
return true;
|
||||||
} else if (key == '\r' || key == '\n') {
|
} else if (key == '\r' || key == '\n') {
|
||||||
execute_command(client);
|
execute_command(client);
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
|
|
@ -1353,6 +1468,8 @@ void* client_handle_session(void *arg) {
|
||||||
client->mode = MODE_INSERT;
|
client->mode = MODE_INSERT;
|
||||||
client->help_lang = LANG_ZH;
|
client->help_lang = LANG_ZH;
|
||||||
client->connected = true;
|
client->connected = true;
|
||||||
|
client->command_history_count = 0;
|
||||||
|
client->command_history_pos = 0;
|
||||||
|
|
||||||
/* Check for exec command */
|
/* Check for exec command */
|
||||||
if (client->exec_command[0] != '\0') {
|
if (client->exec_command[0] != '\0') {
|
||||||
|
|
@ -1381,6 +1498,7 @@ void* client_handle_session(void *arg) {
|
||||||
join_msg.username[MAX_USERNAME_LEN - 1] = '\0';
|
join_msg.username[MAX_USERNAME_LEN - 1] = '\0';
|
||||||
snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username);
|
snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username);
|
||||||
room_broadcast(g_room, &join_msg);
|
room_broadcast(g_room, &join_msg);
|
||||||
|
message_save(&join_msg);
|
||||||
|
|
||||||
/* Render initial screen */
|
/* Render initial screen */
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
|
|
@ -1526,6 +1644,7 @@ cleanup:
|
||||||
client->connected = false;
|
client->connected = false;
|
||||||
room_remove_client(g_room, client);
|
room_remove_client(g_room, client);
|
||||||
room_broadcast(g_room, &leave_msg);
|
room_broadcast(g_room, &leave_msg);
|
||||||
|
message_save(&leave_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
release_ip_connection(client->client_ip);
|
release_ip_connection(client->client_ip);
|
||||||
|
|
@ -1798,11 +1917,11 @@ static int client_channel_window_change(ssh_session session, ssh_channel channel
|
||||||
return SSH_ERROR;
|
return SSH_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_lock(&client->io_lock);
|
int w = width;
|
||||||
client->width = width;
|
int h = height;
|
||||||
client->height = height;
|
sanitize_terminal_size(&w, &h);
|
||||||
sanitize_terminal_size(&client->width, &client->height);
|
client->width = w;
|
||||||
pthread_mutex_unlock(&client->io_lock);
|
client->height = h;
|
||||||
client->redraw_pending = true;
|
client->redraw_pending = true;
|
||||||
return SSH_OK;
|
return SSH_OK;
|
||||||
}
|
}
|
||||||
|
|
@ -1974,9 +2093,11 @@ static void *bootstrap_client_session(void *arg) {
|
||||||
|
|
||||||
client->session = session;
|
client->session = session;
|
||||||
client->channel = channel;
|
client->channel = channel;
|
||||||
client->width = ctx->pty_width;
|
int init_w = ctx->pty_width;
|
||||||
client->height = ctx->pty_height;
|
int init_h = ctx->pty_height;
|
||||||
sanitize_terminal_size(&client->width, &client->height);
|
sanitize_terminal_size(&init_w, &init_h);
|
||||||
|
client->width = init_w;
|
||||||
|
client->height = init_h;
|
||||||
client->ref_count = 1;
|
client->ref_count = 1;
|
||||||
pthread_mutex_init(&client->ref_lock, NULL);
|
pthread_mutex_init(&client->ref_lock, NULL);
|
||||||
pthread_mutex_init(&client->io_lock, NULL);
|
pthread_mutex_init(&client->io_lock, NULL);
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,7 @@ void tui_render_help(client_t *client) {
|
||||||
|
|
||||||
/* Help content */
|
/* Help content */
|
||||||
const char *help_text = tui_get_help_text(client->help_lang);
|
const char *help_text = tui_get_help_text(client->help_lang);
|
||||||
char help_copy[4096];
|
char help_copy[8192];
|
||||||
strncpy(help_copy, help_text, sizeof(help_copy) - 1);
|
strncpy(help_copy, help_text, sizeof(help_copy) - 1);
|
||||||
help_copy[sizeof(help_copy) - 1] = '\0';
|
help_copy[sizeof(help_copy) - 1] = '\0';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ TEST(room_add_message_single) {
|
||||||
chat_room_t *room = room_create();
|
chat_room_t *room = room_create();
|
||||||
message_t msg = make_msg("alice", "hello");
|
message_t msg = make_msg("alice", "hello");
|
||||||
|
|
||||||
room_add_message(room, &msg);
|
room_broadcast(room, &msg);
|
||||||
assert(room->message_count == 1);
|
assert(room->message_count == 1);
|
||||||
assert(strcmp(room->messages[0].username, "alice") == 0);
|
assert(strcmp(room->messages[0].username, "alice") == 0);
|
||||||
assert(strcmp(room->messages[0].content, "hello") == 0);
|
assert(strcmp(room->messages[0].content, "hello") == 0);
|
||||||
|
|
@ -60,7 +60,7 @@ TEST(room_add_message_overflow) {
|
||||||
char content[32];
|
char content[32];
|
||||||
snprintf(content, sizeof(content), "msg %d", i);
|
snprintf(content, sizeof(content), "msg %d", i);
|
||||||
message_t msg = make_msg("user", content);
|
message_t msg = make_msg("user", content);
|
||||||
room_add_message(room, &msg);
|
room_broadcast(room, &msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(room->message_count == MAX_MESSAGES);
|
assert(room->message_count == MAX_MESSAGES);
|
||||||
|
|
@ -94,7 +94,7 @@ TEST(room_broadcast_increments_seq) {
|
||||||
TEST(room_get_message_valid) {
|
TEST(room_get_message_valid) {
|
||||||
chat_room_t *room = room_create();
|
chat_room_t *room = room_create();
|
||||||
message_t msg = make_msg("carol", "test");
|
message_t msg = make_msg("carol", "test");
|
||||||
room_add_message(room, &msg);
|
room_broadcast(room, &msg);
|
||||||
|
|
||||||
message_t out;
|
message_t out;
|
||||||
assert(room_get_message(room, 0, &out) == true);
|
assert(room_get_message(room, 0, &out) == true);
|
||||||
|
|
|
||||||
210
tnt.1
Normal file
210
tnt.1
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
.\" tnt(1) - Terminal Network Talk
|
||||||
|
.TH TNT 1 "April 2026" "TNT 1.0.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
tnt \- anonymous SSH chat server with Vim\-style TUI
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B tnt
|
||||||
|
.RB [ \-p
|
||||||
|
.IR port ]
|
||||||
|
.RB [ \-d
|
||||||
|
.IR dir ]
|
||||||
|
.RB [ \-h ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B tnt
|
||||||
|
is a multi\-user anonymous chat server accessed over SSH.
|
||||||
|
It provides a Vim\-style terminal user interface with INSERT, NORMAL, and
|
||||||
|
COMMAND modes.
|
||||||
|
Users connect with any standard SSH client; no account or registration is needed.
|
||||||
|
.PP
|
||||||
|
Messages are persisted to a log file and restored on server restart.
|
||||||
|
The server supports CJK and emoji input, rate limiting, access tokens, and
|
||||||
|
a non\-interactive exec interface for scripting.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-p " port"
|
||||||
|
Listen on
|
||||||
|
.I port
|
||||||
|
instead of the default 2222.
|
||||||
|
Overrides the
|
||||||
|
.B PORT
|
||||||
|
environment variable.
|
||||||
|
.TP
|
||||||
|
.BI \-d " dir"
|
||||||
|
Store the host key and message log in
|
||||||
|
.IR dir .
|
||||||
|
Overrides the
|
||||||
|
.B TNT_STATE_DIR
|
||||||
|
environment variable.
|
||||||
|
Defaults to the current working directory.
|
||||||
|
.TP
|
||||||
|
.B \-h
|
||||||
|
Print a short usage summary and exit.
|
||||||
|
.SH CONNECTING
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
ssh any\-username@hostname \-p 2222
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
If an access token is configured, supply it as the SSH password.
|
||||||
|
The username entered in the SSH handshake is ignored; a chat\-room
|
||||||
|
nickname is chosen interactively after login.
|
||||||
|
.SH MODES
|
||||||
|
.TP
|
||||||
|
.B INSERT
|
||||||
|
Type and send messages.
|
||||||
|
Press
|
||||||
|
.B Enter
|
||||||
|
to send,
|
||||||
|
.B ESC
|
||||||
|
to switch to NORMAL mode.
|
||||||
|
.TP
|
||||||
|
.B NORMAL
|
||||||
|
Scroll through chat history with Vim keybindings.
|
||||||
|
Press
|
||||||
|
.B i
|
||||||
|
to return to INSERT,
|
||||||
|
.B :
|
||||||
|
to enter COMMAND mode,
|
||||||
|
.B ?
|
||||||
|
to open the help screen.
|
||||||
|
.TP
|
||||||
|
.B COMMAND
|
||||||
|
Execute commands prefixed with
|
||||||
|
.BR : .
|
||||||
|
.SH KEYBINDINGS
|
||||||
|
.SS INSERT mode
|
||||||
|
.TS
|
||||||
|
l l.
|
||||||
|
Enter Send message
|
||||||
|
ESC Switch to NORMAL
|
||||||
|
Ctrl+W Delete last word
|
||||||
|
Ctrl+U Clear input line
|
||||||
|
Ctrl+C Switch to NORMAL
|
||||||
|
.TE
|
||||||
|
.SS NORMAL mode
|
||||||
|
.TS
|
||||||
|
l l.
|
||||||
|
j/k Scroll down/up one line
|
||||||
|
Ctrl+D/Ctrl+U Scroll half page down/up
|
||||||
|
Ctrl+F/Ctrl+B Scroll full page down/up
|
||||||
|
g/G Jump to top/bottom
|
||||||
|
i Switch to INSERT
|
||||||
|
: Enter COMMAND mode
|
||||||
|
? Open help screen
|
||||||
|
Ctrl+C Disconnect
|
||||||
|
.TE
|
||||||
|
.SS COMMAND mode
|
||||||
|
.TS
|
||||||
|
l l.
|
||||||
|
:list Show online users
|
||||||
|
:msg \fIuser text\fR Send private whisper
|
||||||
|
:w \fIuser text\fR Short alias for :msg
|
||||||
|
:help Show available commands
|
||||||
|
:clear Clear command output
|
||||||
|
:q, :quit, :exit Disconnect
|
||||||
|
Up/Down Browse command history
|
||||||
|
ESC Cancel and return to NORMAL
|
||||||
|
.TE
|
||||||
|
.SH EXEC INTERFACE
|
||||||
|
Commands can be run non\-interactively for scripting:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
ssh host \-p 2222 help
|
||||||
|
ssh host \-p 2222 users \-\-json
|
||||||
|
ssh host \-p 2222 stats \-\-json
|
||||||
|
ssh host \-p 2222 tail 20
|
||||||
|
ssh host \-p 2222 post "Hello from a script"
|
||||||
|
ssh host \-p 2222 health
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Exit codes follow
|
||||||
|
.BR sysexits (3)
|
||||||
|
conventions.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B PORT
|
||||||
|
Default listening port (default: 2222).
|
||||||
|
.TP
|
||||||
|
.B TNT_STATE_DIR
|
||||||
|
Directory for host key and message log (default: current directory).
|
||||||
|
.TP
|
||||||
|
.B TNT_ACCESS_TOKEN
|
||||||
|
If set, clients must supply this string as their SSH password.
|
||||||
|
Compared in constant time.
|
||||||
|
.TP
|
||||||
|
.B TNT_MAX_CONNECTIONS
|
||||||
|
Global connection limit (default: 64, max: 1024).
|
||||||
|
.TP
|
||||||
|
.B TNT_MAX_CONN_PER_IP
|
||||||
|
Max concurrent sessions from one IP (default: 5).
|
||||||
|
.TP
|
||||||
|
.B TNT_MAX_CONN_RATE_PER_IP
|
||||||
|
Max new connections per IP per 60\-second window (default: 10).
|
||||||
|
.TP
|
||||||
|
.B TNT_RATE_LIMIT
|
||||||
|
Set to 0 to disable rate\-based blocking and auth\-failure IP blocking.
|
||||||
|
Explicit capacity limits still apply (default: 1).
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I messages.log
|
||||||
|
Chat history in RFC\ 3339 pipe\-delimited format
|
||||||
|
.RI ( timestamp | username | content ).
|
||||||
|
Stored in the state directory.
|
||||||
|
.TP
|
||||||
|
.I host_key
|
||||||
|
RSA 4096\-bit host key, auto\-generated on first run.
|
||||||
|
Stored in the state directory with mode 0600.
|
||||||
|
.SH SYSTEMD
|
||||||
|
A unit file
|
||||||
|
.I tnt.service
|
||||||
|
is provided.
|
||||||
|
Typical setup:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
sudo useradd \-r \-s /bin/false tnt
|
||||||
|
sudo cp tnt.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon\-reload
|
||||||
|
sudo systemctl enable \-\-now tnt
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Runtime overrides can be placed in
|
||||||
|
.IR /etc/default/tnt .
|
||||||
|
.SH SECURITY
|
||||||
|
.IP \(bu 2
|
||||||
|
Reference\-counted client lifecycle prevents use\-after\-free.
|
||||||
|
.IP \(bu 2
|
||||||
|
Per\-IP rate limiting with auth\-failure blocking (5 failures = 5\-minute ban).
|
||||||
|
.IP \(bu 2
|
||||||
|
Access\-token comparison uses constant\-time algorithm.
|
||||||
|
.IP \(bu 2
|
||||||
|
Host key created with restrictive permissions (0600).
|
||||||
|
.IP \(bu 2
|
||||||
|
systemd hardening: NoNewPrivileges, PrivateTmp, ProtectSystem=strict.
|
||||||
|
.SH EXAMPLES
|
||||||
|
Start on port 3000 with state in /var/lib/tnt:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
tnt \-p 3000 \-d /var/lib/tnt
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Start with an access token:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
TNT_ACCESS_TOKEN=s3cret tnt
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Connect from another machine:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
ssh user@chat.example.com \-p 2222
|
||||||
|
.fi
|
||||||
|
.SH AUTHORS
|
||||||
|
m1ngsama <contact@m1ng.space>
|
||||||
|
.SH BUGS
|
||||||
|
Report bugs at
|
||||||
|
.UR https://github.com/m1ngsama/TNT/issues
|
||||||
|
.UE .
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR ssh (1),
|
||||||
|
.BR sshd (8),
|
||||||
|
.BR systemctl (1)
|
||||||
Loading…
Reference in a new issue