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 -d $(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:
|
||||
rm -f $(DESTDIR)/usr/local/bin/$(TARGET)
|
||||
rm -f $(DESTDIR)/usr/local/share/man/man1/tnt.1
|
||||
|
||||
# Development targets
|
||||
debug: CFLAGS += -g -DDEBUG
|
||||
|
|
|
|||
|
|
@ -36,9 +36,6 @@ void room_remove_client(chat_room_t *room, struct client *client);
|
|||
/* Broadcast message to all clients */
|
||||
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) */
|
||||
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 */
|
||||
char username[MAX_USERNAME_LEN];
|
||||
char client_ip[INET6_ADDRSTRLEN];
|
||||
int width;
|
||||
int height;
|
||||
_Atomic int width;
|
||||
_Atomic int height;
|
||||
client_mode_t mode;
|
||||
help_lang_t help_lang;
|
||||
int scroll_pos;
|
||||
int help_scroll_pos;
|
||||
bool show_help;
|
||||
char command_input[256];
|
||||
char command_history[16][256];
|
||||
int command_history_count;
|
||||
int command_history_pos;
|
||||
char command_output[2048];
|
||||
char exec_command[MAX_EXEC_COMMAND_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);
|
||||
}
|
||||
|
||||
/* Broadcast message to all clients */
|
||||
void room_broadcast(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 */
|
||||
|
||||
/* Add message to room history (caller must hold write lock) */
|
||||
static void room_add_message(chat_room_t *room, const message_t *msg) {
|
||||
if (room->message_count >= MAX_MESSAGES) {
|
||||
/* Shift messages to make room */
|
||||
memmove(&room->messages[0], &room->messages[1],
|
||||
(MAX_MESSAGES - 1) * sizeof(message_t));
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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) */
|
||||
bool room_get_message(chat_room_t *room, int index, message_t *out) {
|
||||
if (!room || !out) return false;
|
||||
|
|
|
|||
191
src/ssh_server.c
191
src/ssh_server.c
|
|
@ -1083,7 +1083,10 @@ static int execute_exec_command(client_t *client) {
|
|||
|
||||
/* Execute a command */
|
||||
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};
|
||||
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 ||
|
||||
strcmp(cmd, "who") == 0) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
|
|
@ -1130,10 +1148,58 @@ static void execute_command(client_t *client) {
|
|||
" Available Commands\n"
|
||||
"========================================\n"
|
||||
"list, users, who - Show online users\n"
|
||||
"msg/w <user> <text> - Whisper to user\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");
|
||||
|
||||
} 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) {
|
||||
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;
|
||||
|
||||
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') {
|
||||
client->mode = MODE_INSERT;
|
||||
tui_render_screen(client);
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
} else if (key == ':') {
|
||||
client->mode = MODE_COMMAND;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return true; /* Key consumed - prevents double colon */
|
||||
return true;
|
||||
} else if (key == 'j') {
|
||||
/* Get message count atomically to prevent TOCTOU */
|
||||
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) {
|
||||
if (client->scroll_pos < nm_max_scroll) {
|
||||
client->scroll_pos++;
|
||||
tui_render_screen(client);
|
||||
}
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
} else if (key == 'k' && client->scroll_pos > 0) {
|
||||
client->scroll_pos--;
|
||||
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') {
|
||||
client->scroll_pos = 0;
|
||||
tui_render_screen(client);
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
} else if (key == 'G') {
|
||||
/* Get message count atomically to prevent TOCTOU */
|
||||
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;
|
||||
client->scroll_pos = nm_max_scroll;
|
||||
tui_render_screen(client);
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
} else if (key == '?') {
|
||||
client->show_help = true;
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return true; /* Key consumed */
|
||||
return true;
|
||||
} else if (key == '\r' || key == '\n') {
|
||||
execute_command(client);
|
||||
return true; /* Key consumed */
|
||||
|
|
@ -1353,6 +1468,8 @@ void* client_handle_session(void *arg) {
|
|||
client->mode = MODE_INSERT;
|
||||
client->help_lang = LANG_ZH;
|
||||
client->connected = true;
|
||||
client->command_history_count = 0;
|
||||
client->command_history_pos = 0;
|
||||
|
||||
/* Check for exec command */
|
||||
if (client->exec_command[0] != '\0') {
|
||||
|
|
@ -1381,6 +1498,7 @@ void* client_handle_session(void *arg) {
|
|||
join_msg.username[MAX_USERNAME_LEN - 1] = '\0';
|
||||
snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username);
|
||||
room_broadcast(g_room, &join_msg);
|
||||
message_save(&join_msg);
|
||||
|
||||
/* Render initial screen */
|
||||
tui_render_screen(client);
|
||||
|
|
@ -1526,6 +1644,7 @@ cleanup:
|
|||
client->connected = false;
|
||||
room_remove_client(g_room, client);
|
||||
room_broadcast(g_room, &leave_msg);
|
||||
message_save(&leave_msg);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&client->io_lock);
|
||||
client->width = width;
|
||||
client->height = height;
|
||||
sanitize_terminal_size(&client->width, &client->height);
|
||||
pthread_mutex_unlock(&client->io_lock);
|
||||
int w = width;
|
||||
int h = height;
|
||||
sanitize_terminal_size(&w, &h);
|
||||
client->width = w;
|
||||
client->height = h;
|
||||
client->redraw_pending = true;
|
||||
return SSH_OK;
|
||||
}
|
||||
|
|
@ -1974,9 +2093,11 @@ static void *bootstrap_client_session(void *arg) {
|
|||
|
||||
client->session = session;
|
||||
client->channel = channel;
|
||||
client->width = ctx->pty_width;
|
||||
client->height = ctx->pty_height;
|
||||
sanitize_terminal_size(&client->width, &client->height);
|
||||
int init_w = ctx->pty_width;
|
||||
int init_h = ctx->pty_height;
|
||||
sanitize_terminal_size(&init_w, &init_h);
|
||||
client->width = init_w;
|
||||
client->height = init_h;
|
||||
client->ref_count = 1;
|
||||
pthread_mutex_init(&client->ref_lock, NULL);
|
||||
pthread_mutex_init(&client->io_lock, NULL);
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ void tui_render_help(client_t *client) {
|
|||
|
||||
/* Help content */
|
||||
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);
|
||||
help_copy[sizeof(help_copy) - 1] = '\0';
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ TEST(room_add_message_single) {
|
|||
chat_room_t *room = room_create();
|
||||
message_t msg = make_msg("alice", "hello");
|
||||
|
||||
room_add_message(room, &msg);
|
||||
room_broadcast(room, &msg);
|
||||
assert(room->message_count == 1);
|
||||
assert(strcmp(room->messages[0].username, "alice") == 0);
|
||||
assert(strcmp(room->messages[0].content, "hello") == 0);
|
||||
|
|
@ -60,7 +60,7 @@ TEST(room_add_message_overflow) {
|
|||
char content[32];
|
||||
snprintf(content, sizeof(content), "msg %d", i);
|
||||
message_t msg = make_msg("user", content);
|
||||
room_add_message(room, &msg);
|
||||
room_broadcast(room, &msg);
|
||||
}
|
||||
|
||||
assert(room->message_count == MAX_MESSAGES);
|
||||
|
|
@ -94,7 +94,7 @@ TEST(room_broadcast_increments_seq) {
|
|||
TEST(room_get_message_valid) {
|
||||
chat_room_t *room = room_create();
|
||||
message_t msg = make_msg("carol", "test");
|
||||
room_add_message(room, &msg);
|
||||
room_broadcast(room, &msg);
|
||||
|
||||
message_t out;
|
||||
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