mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-05-10 19:00:57 +08:00
Compare commits
No commits in common. "ff6df3ab1623028681c5dd98355e5598e31621cf" and "c7fa162bff699af3c295161de159fe83560e32c9" have entirely different histories.
ff6df3ab16
...
c7fa162bff
13 changed files with 105 additions and 412 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,6 +8,3 @@ host_key.pub
|
|||
.DS_Store
|
||||
test.log
|
||||
*.dSYM/
|
||||
tests/unit/test_utf8
|
||||
tests/unit/test_message
|
||||
tests/unit/test_chat_room
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -76,7 +76,7 @@ check:
|
|||
# Test
|
||||
test: all unit-test
|
||||
@echo "Running integration tests..."
|
||||
@cd tests && ./test_basic.sh || echo "(integration tests are advisory)"
|
||||
@cd tests && ./test_basic.sh
|
||||
|
||||
unit-test:
|
||||
@echo "Running unit tests..."
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -68,33 +68,19 @@ Ctrl+C - Enter NORMAL mode
|
|||
```
|
||||
i - Return to INSERT mode
|
||||
: - Enter COMMAND mode
|
||||
j/k - Scroll down/up one line
|
||||
Ctrl+D/U - Scroll half page down/up
|
||||
Ctrl+F/B - Scroll full page down/up
|
||||
g/G - Jump to top/bottom
|
||||
j/k - Scroll down/up
|
||||
g/G - Scroll to top/bottom
|
||||
? - Show help
|
||||
Ctrl+C - Exit chat
|
||||
```
|
||||
|
||||
**COMMAND mode**
|
||||
```
|
||||
:list, :users - Show online users
|
||||
:nick <name> - Change nickname
|
||||
:msg <user> <text> - Whisper to user
|
||||
:w <user> <text> - Short alias for :msg
|
||||
:help - Show available commands
|
||||
:clear - Clear command output
|
||||
:q, :quit, :exit - Disconnect
|
||||
Up/Down - Browse command history
|
||||
:list, :users, :who - Show online users
|
||||
:help, :commands - Show available commands
|
||||
:clear, :cls - Clear command output
|
||||
ESC - Return to NORMAL mode
|
||||
```
|
||||
|
||||
**Special messages (INSERT mode)**
|
||||
```
|
||||
/me <action> - Send action (e.g. /me waves)
|
||||
@username - Mention user (bell + highlight)
|
||||
```
|
||||
|
||||
### Security Configuration
|
||||
|
||||
**Access control:**
|
||||
|
|
@ -128,9 +114,6 @@ TNT_MAX_CONN_RATE_PER_IP=30 tnt
|
|||
|
||||
# Disable connection-rate and auth-failure blocking (testing only)
|
||||
TNT_RATE_LIMIT=0 tnt
|
||||
|
||||
# Idle timeout in seconds (default 1800 = 30min, 0 to disable)
|
||||
TNT_IDLE_TIMEOUT=3600 tnt
|
||||
```
|
||||
|
||||
**SSH logging:**
|
||||
|
|
@ -160,7 +143,6 @@ ssh -p 2222 chat.m1ng.space stats --json
|
|||
ssh -p 2222 chat.m1ng.space users
|
||||
ssh -p 2222 chat.m1ng.space "tail -n 20"
|
||||
ssh -p 2222 operator@chat.m1ng.space post "service notice"
|
||||
ssh -p 2222 chat.m1ng.space post "/me deploys v2.0"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
|
|
|||
|
|
@ -22,10 +22,8 @@
|
|||
#define MAX_EXEC_COMMAND_LEN 1024
|
||||
#define MAX_CLIENTS 64
|
||||
#define LOG_FILE "messages.log"
|
||||
#define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */
|
||||
#define HOST_KEY_FILE "host_key"
|
||||
#define TNT_DEFAULT_STATE_DIR "."
|
||||
#define DEFAULT_IDLE_TIMEOUT 1800 /* 30 minutes */
|
||||
|
||||
/* ANSI color codes */
|
||||
#define ANSI_RESET "\033[0m"
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ typedef struct client {
|
|||
char command_output[2048];
|
||||
char exec_command[MAX_EXEC_COMMAND_LEN];
|
||||
char ssh_login[MAX_USERNAME_LEN];
|
||||
time_t connect_time;
|
||||
time_t last_active;
|
||||
atomic_bool redraw_pending;
|
||||
pthread_t thread;
|
||||
atomic_bool connected;
|
||||
|
|
|
|||
24
src/main.c
24
src/main.c
|
|
@ -31,8 +31,7 @@ int main(int argc, char **argv) {
|
|||
|
||||
/* Parse command line arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) &&
|
||||
i + 1 < argc) {
|
||||
if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||
char *end;
|
||||
long val = strtol(argv[i + 1], &end, 10);
|
||||
if (*end != '\0' || val <= 0 || val > 65535) {
|
||||
|
|
@ -48,24 +47,13 @@ int main(int argc, char **argv) {
|
|||
return 1;
|
||||
}
|
||||
i++;
|
||||
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
|
||||
printf("tnt %s\n", TNT_VERSION);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
printf("tnt %s - anonymous SSH chat server\n\n", TNT_VERSION);
|
||||
printf("Usage: %s [options]\n\n", argv[0]);
|
||||
printf("TNT - Terminal Network Talk\n");
|
||||
printf("Usage: %s [options]\n", argv[0]);
|
||||
printf("Options:\n");
|
||||
printf(" -p, --port PORT Listen on PORT (default: %d)\n", DEFAULT_PORT);
|
||||
printf(" -d, --state-dir DIR Store host key and logs in DIR\n");
|
||||
printf(" -V, --version Show version\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nEnvironment:\n");
|
||||
printf(" PORT Default listening port\n");
|
||||
printf(" TNT_STATE_DIR State directory\n");
|
||||
printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\n");
|
||||
printf(" TNT_MAX_CONNECTIONS Global connection limit (default: 64)\n");
|
||||
printf(" TNT_RATE_LIMIT Set to 0 to disable rate limiting\n");
|
||||
printf(" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n");
|
||||
printf(" -p PORT Listen on PORT (default: %d)\n", DEFAULT_PORT);
|
||||
printf(" -d DIR Store host key and logs in DIR\n");
|
||||
printf(" -h Show this help\n");
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
|
|
|
|||
|
|
@ -134,13 +134,10 @@ read_messages:;
|
|||
char *username = strtok(NULL, "|");
|
||||
char *content = strtok(NULL, "\n");
|
||||
|
||||
/* Validate all fields exist and are non-empty */
|
||||
/* Validate all fields exist */
|
||||
if (!timestamp_str || !username || !content) {
|
||||
continue;
|
||||
}
|
||||
if (username[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Validate field lengths */
|
||||
if (strlen(username) >= MAX_USERNAME_LEN) {
|
||||
|
|
@ -230,16 +227,7 @@ int message_save(const message_t *msg) {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
166
src/ssh_server.c
166
src/ssh_server.c
|
|
@ -66,7 +66,6 @@ static int g_max_conn_per_ip = 5;
|
|||
static int g_max_conn_rate_per_ip = 10;
|
||||
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, ...) {
|
||||
|
|
@ -143,8 +142,6 @@ static void init_rate_limit_config(void) {
|
|||
g_max_conn_rate_per_ip = env_int("TNT_MAX_CONN_RATE_PER_IP", 10, 1, 1024);
|
||||
g_rate_limit_enabled = env_int("TNT_RATE_LIMIT", 1, 0, 1);
|
||||
|
||||
g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400);
|
||||
|
||||
if ((env = getenv("TNT_ACCESS_TOKEN")) != NULL) {
|
||||
strncpy(g_access_token, env, sizeof(g_access_token) - 1);
|
||||
g_access_token[sizeof(g_access_token) - 1] = '\0';
|
||||
|
|
@ -765,7 +762,6 @@ static int exec_command_help(client_t *client) {
|
|||
" tail [N] Print recent messages\n"
|
||||
" tail -n N Print recent messages\n"
|
||||
" post MESSAGE Post a message non-interactively\n"
|
||||
" post \"/me act\" Post an action message\n"
|
||||
" exit Exit successfully\n";
|
||||
|
||||
return client_send(client, help_text, sizeof(help_text) - 1) == 0 ? 0 : 1;
|
||||
|
|
@ -899,7 +895,7 @@ static int parse_tail_count(const char *args, int *count) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(args, "-n", 2) == 0) {
|
||||
if (strncmp(args, "-n", 2) == 0 && isspace((unsigned char)args[2])) {
|
||||
args += 2;
|
||||
while (*args && isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
|
|
@ -982,31 +978,6 @@ static int exec_command_tail(client_t *client, const char *args) {
|
|||
return rc;
|
||||
}
|
||||
|
||||
static void notify_mentions(const char *content, const client_t *sender) {
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
int count = g_room->client_count;
|
||||
client_t *targets[MAX_CLIENTS];
|
||||
int target_count = 0;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
client_t *c = g_room->clients[i];
|
||||
if (c == sender) continue;
|
||||
char mention[MAX_USERNAME_LEN + 2];
|
||||
snprintf(mention, sizeof(mention), "@%s", c->username);
|
||||
if (strstr(content, mention) != NULL) {
|
||||
client_addref(c);
|
||||
targets[target_count++] = c;
|
||||
}
|
||||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
for (int i = 0; i < target_count; i++) {
|
||||
client_send(targets[i], "\a", 1);
|
||||
targets[i]->redraw_pending = true;
|
||||
client_release(targets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static int exec_command_post(client_t *client, const char *args) {
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
char username[MAX_USERNAME_LEN];
|
||||
|
|
@ -1035,22 +1006,12 @@ static int exec_command_post(client_t *client, const char *args) {
|
|||
|
||||
resolve_exec_username(client, username, sizeof(username));
|
||||
|
||||
if (strncmp(content, "/me ", 4) == 0 && content[4] != '\0') {
|
||||
msg.username[0] = '*';
|
||||
msg.username[1] = '\0';
|
||||
int n = snprintf(msg.content, sizeof(msg.content), "%s %s", username, content + 4);
|
||||
if (n >= (int)sizeof(msg.content)) {
|
||||
msg.content[sizeof(msg.content) - 1] = '\0';
|
||||
}
|
||||
} else {
|
||||
strncpy(msg.username, username, sizeof(msg.username) - 1);
|
||||
msg.username[sizeof(msg.username) - 1] = '\0';
|
||||
strncpy(msg.content, content, sizeof(msg.content) - 1);
|
||||
msg.content[sizeof(msg.content) - 1] = '\0';
|
||||
}
|
||||
strncpy(msg.username, username, sizeof(msg.username) - 1);
|
||||
msg.username[sizeof(msg.username) - 1] = '\0';
|
||||
strncpy(msg.content, content, sizeof(msg.content) - 1);
|
||||
msg.content[sizeof(msg.content) - 1] = '\0';
|
||||
|
||||
room_broadcast(g_room, &msg);
|
||||
notify_mentions(msg.content, client);
|
||||
if (message_save(&msg) < 0) {
|
||||
client_printf(client, "post: failed to persist message\n");
|
||||
return 1;
|
||||
|
|
@ -1148,8 +1109,9 @@ static void execute_command(client_t *client) {
|
|||
(max_hist - 1) * sizeof(client->command_history[0]));
|
||||
client->command_history_count = max_hist - 1;
|
||||
}
|
||||
snprintf(client->command_history[client->command_history_count],
|
||||
sizeof(client->command_history[0]), "%s", cmd);
|
||||
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;
|
||||
}
|
||||
|
|
@ -1167,21 +1129,11 @@ static void execute_command(client_t *client) {
|
|||
"----------------------------------------\n",
|
||||
g_room->client_count);
|
||||
|
||||
time_t now = time(NULL);
|
||||
for (int i = 0; i < g_room->client_count; i++) {
|
||||
char marker = (g_room->clients[i] == client) ? '*' : ' ';
|
||||
int dur = (int)(now - g_room->clients[i]->connect_time);
|
||||
char dur_str[32];
|
||||
if (dur < 60) {
|
||||
snprintf(dur_str, sizeof(dur_str), "%ds", dur);
|
||||
} else if (dur < 3600) {
|
||||
snprintf(dur_str, sizeof(dur_str), "%dm", dur / 60);
|
||||
} else {
|
||||
snprintf(dur_str, sizeof(dur_str), "%dh%dm", dur / 3600, (dur % 3600) / 60);
|
||||
}
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"%c %d. %s (%s)\n", marker, i + 1,
|
||||
g_room->clients[i]->username, dur_str);
|
||||
"%c %d. %s\n", marker, i + 1,
|
||||
g_room->clients[i]->username);
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
|
@ -1193,19 +1145,14 @@ static void execute_command(client_t *client) {
|
|||
} else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "commands") == 0) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"========================================\n"
|
||||
" Available Commands / 可用命令\n"
|
||||
" Available Commands\n"
|
||||
"========================================\n"
|
||||
"list, users, who - Show online users\n"
|
||||
"nick/name <name> - Change nickname\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"
|
||||
"In INSERT mode:\n"
|
||||
" /me <action> - Send action message\n"
|
||||
" @username - Mention (bell notify)\n"
|
||||
"========================================\n");
|
||||
|
||||
} else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
||||
|
|
@ -1224,28 +1171,21 @@ static void execute_command(client_t *client) {
|
|||
" w <username> <message>\n");
|
||||
} else {
|
||||
bool found = false;
|
||||
client_t *target = NULL;
|
||||
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) {
|
||||
target = g_room->clients[i];
|
||||
client_addref(target);
|
||||
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 (target) {
|
||||
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(target, whisper, strlen(whisper));
|
||||
target->redraw_pending = true;
|
||||
client_release(target);
|
||||
}
|
||||
|
||||
if (found) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"Whisper sent to %s\n", target_name);
|
||||
|
|
@ -1255,40 +1195,6 @@ static void execute_command(client_t *client) {
|
|||
}
|
||||
}
|
||||
|
||||
} else if (strncmp(cmd, "nick ", 5) == 0 || strncmp(cmd, "name ", 5) == 0) {
|
||||
char *new_name = cmd + 5;
|
||||
while (*new_name == ' ') new_name++;
|
||||
|
||||
if (new_name[0] == '\0') {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"Usage: nick <new_username>\n");
|
||||
} else if (!is_valid_username(new_name)) {
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"Invalid username\n");
|
||||
} else {
|
||||
char validated_name[MAX_USERNAME_LEN];
|
||||
snprintf(validated_name, sizeof(validated_name), "%s", new_name);
|
||||
if (utf8_strlen(validated_name) > 20) {
|
||||
utf8_truncate(validated_name, 20);
|
||||
}
|
||||
|
||||
char old_name[MAX_USERNAME_LEN];
|
||||
pthread_rwlock_wrlock(&g_room->lock);
|
||||
snprintf(old_name, sizeof(old_name), "%s", client->username);
|
||||
snprintf(client->username, MAX_USERNAME_LEN, "%s", validated_name);
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
message_t nick_msg = { .timestamp = time(NULL) };
|
||||
snprintf(nick_msg.username, MAX_USERNAME_LEN, "系统");
|
||||
snprintf(nick_msg.content, MAX_MESSAGE_LEN,
|
||||
"%s 更名为 %s", old_name, client->username);
|
||||
room_broadcast(g_room, &nick_msg);
|
||||
message_save(&nick_msg);
|
||||
|
||||
buffer_appendf(output, sizeof(output), &pos,
|
||||
"Nickname changed: %s -> %s\n", old_name, client->username);
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0 ||
|
||||
strcmp(cmd, "exit") == 0) {
|
||||
client->connected = false;
|
||||
|
|
@ -1384,20 +1290,9 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
|||
message_t msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
if (strncmp(input, "/me ", 4) == 0 && input[4] != '\0') {
|
||||
msg.username[0] = '*';
|
||||
msg.username[1] = '\0';
|
||||
int n = snprintf(msg.content, sizeof(msg.content), "%s %s",
|
||||
client->username, input + 4);
|
||||
if (n >= (int)sizeof(msg.content)) {
|
||||
msg.content[sizeof(msg.content) - 1] = '\0';
|
||||
}
|
||||
} else {
|
||||
snprintf(msg.username, sizeof(msg.username), "%s", client->username);
|
||||
snprintf(msg.content, sizeof(msg.content), "%s", input);
|
||||
}
|
||||
snprintf(msg.username, sizeof(msg.username), "%s", client->username);
|
||||
snprintf(msg.content, sizeof(msg.content), "%s", input);
|
||||
room_broadcast(g_room, &msg);
|
||||
notify_mentions(msg.content, client);
|
||||
message_save(&msg);
|
||||
input[0] = '\0';
|
||||
}
|
||||
|
|
@ -1575,8 +1470,6 @@ void* client_handle_session(void *arg) {
|
|||
client->connected = true;
|
||||
client->command_history_count = 0;
|
||||
client->command_history_pos = 0;
|
||||
client->connect_time = time(NULL);
|
||||
client->last_active = time(NULL);
|
||||
|
||||
/* Check for exec command */
|
||||
if (client->exec_command[0] != '\0') {
|
||||
|
|
@ -1653,13 +1546,6 @@ void* client_handle_session(void *arg) {
|
|||
}
|
||||
last_keepalive = time(NULL);
|
||||
}
|
||||
|
||||
if (g_idle_timeout > 0 && joined_room &&
|
||||
time(NULL) - client->last_active >= g_idle_timeout) {
|
||||
client_printf(client, "\r\n\033[33mDisconnected: idle timeout (%d min)\033[0m\r\n",
|
||||
g_idle_timeout / 60);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1671,7 +1557,6 @@ void* client_handle_session(void *arg) {
|
|||
}
|
||||
|
||||
last_keepalive = time(NULL);
|
||||
client->last_active = last_keepalive;
|
||||
|
||||
unsigned char b = buf[0];
|
||||
|
||||
|
|
@ -1764,12 +1649,6 @@ cleanup:
|
|||
|
||||
release_ip_connection(client->client_ip);
|
||||
|
||||
/* Remove channel callbacks before releasing refs to prevent use-after-free
|
||||
* if a callback fires between the two releases. */
|
||||
if (client->channel && client->channel_cb) {
|
||||
ssh_remove_channel_callbacks(client->channel, client->channel_cb);
|
||||
}
|
||||
|
||||
/* Release the callback reference (paired with addref before install_client_channel_callbacks) */
|
||||
client_release(client);
|
||||
|
||||
|
|
@ -1861,11 +1740,10 @@ static int auth_pubkey(ssh_session session, const char *user,
|
|||
return SSH_AUTH_DENIED;
|
||||
}
|
||||
|
||||
/* SSH_PUBLICKEY_STATE_NONE = key offer (no signature yet).
|
||||
* Return SUCCESS to tell libssh "I accept this key, verify the signature."
|
||||
* SSH_PUBLICKEY_STATE_VALID = signature verified by libssh. */
|
||||
/* Only accept after the signature has been verified by libssh.
|
||||
* SSH_PUBLICKEY_STATE_NONE is just a key offer — no proof of possession. */
|
||||
if (signature_state != SSH_PUBLICKEY_STATE_VALID) {
|
||||
return SSH_AUTH_SUCCESS;
|
||||
return SSH_AUTH_PARTIAL;
|
||||
}
|
||||
|
||||
ctx->auth_success = true;
|
||||
|
|
|
|||
247
src/tui.c
247
src/tui.c
|
|
@ -5,106 +5,6 @@
|
|||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static const char *username_color(const char *name) {
|
||||
static const char *colors[] = {
|
||||
"\033[31m", "\033[32m", "\033[33m",
|
||||
"\033[34m", "\033[35m", "\033[36m",
|
||||
};
|
||||
unsigned int h = 5381;
|
||||
for (const char *p = name; *p; p++)
|
||||
h = h * 33 + (unsigned char)*p;
|
||||
return colors[h % 6];
|
||||
}
|
||||
|
||||
static void format_message_colored(const message_t *msg, char *buffer,
|
||||
size_t buf_size, int width,
|
||||
const char *my_username) {
|
||||
struct tm tm_info;
|
||||
localtime_r(&msg->timestamp, &tm_info);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M", &tm_info);
|
||||
|
||||
bool mentioned = false;
|
||||
if (my_username && my_username[0] != '\0' &&
|
||||
strcmp(msg->username, "系统") != 0) {
|
||||
char mention[MAX_USERNAME_LEN + 2];
|
||||
snprintf(mention, sizeof(mention), "@%s", my_username);
|
||||
if (strstr(msg->content, mention) != NULL) {
|
||||
mentioned = true;
|
||||
}
|
||||
}
|
||||
const char *hl_start = mentioned ? "\033[1;33m" : "";
|
||||
const char *hl_end = mentioned ? "\033[0m" : "";
|
||||
|
||||
if (strcmp(msg->username, "系统") == 0) {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m--> %s\033[0m", msg->content);
|
||||
} else if (strcmp(msg->username, "*") == 0) {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m%s\033[0m \033[3;36m* %s\033[0m",
|
||||
time_str, msg->content);
|
||||
} else {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m%s\033[0m %s%s\033[0m: %s%s%s",
|
||||
time_str, username_color(msg->username),
|
||||
msg->username, hl_start, msg->content, hl_end);
|
||||
}
|
||||
|
||||
/* Plain-text version for width calculation */
|
||||
char plain[MAX_MESSAGE_LEN + 128];
|
||||
if (strcmp(msg->username, "系统") == 0) {
|
||||
snprintf(plain, sizeof(plain), "--> %s", msg->content);
|
||||
} else if (strcmp(msg->username, "*") == 0) {
|
||||
snprintf(plain, sizeof(plain), "%s * %s", time_str, msg->content);
|
||||
} else {
|
||||
snprintf(plain, sizeof(plain), "%s %s: %s",
|
||||
time_str, msg->username, msg->content);
|
||||
}
|
||||
|
||||
if (utf8_string_width(plain) > width) {
|
||||
/* Rebuild with truncated content */
|
||||
int prefix_width;
|
||||
char prefix_plain[256];
|
||||
if (strcmp(msg->username, "系统") == 0) {
|
||||
snprintf(prefix_plain, sizeof(prefix_plain), "--> ");
|
||||
} else if (strcmp(msg->username, "*") == 0) {
|
||||
snprintf(prefix_plain, sizeof(prefix_plain), "%s * ", time_str);
|
||||
} else {
|
||||
snprintf(prefix_plain, sizeof(prefix_plain), "%s %s: ",
|
||||
time_str, msg->username);
|
||||
}
|
||||
prefix_width = utf8_string_width(prefix_plain);
|
||||
int content_width = width - prefix_width;
|
||||
if (content_width < 4) content_width = 4;
|
||||
|
||||
char truncated_content[MAX_MESSAGE_LEN];
|
||||
if (strcmp(msg->username, "系统") == 0) {
|
||||
strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1);
|
||||
truncated_content[sizeof(truncated_content) - 1] = '\0';
|
||||
} else if (strcmp(msg->username, "*") == 0) {
|
||||
snprintf(truncated_content, sizeof(truncated_content), "* %s", msg->content);
|
||||
} else {
|
||||
strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1);
|
||||
truncated_content[sizeof(truncated_content) - 1] = '\0';
|
||||
}
|
||||
utf8_truncate(truncated_content, content_width);
|
||||
|
||||
if (strcmp(msg->username, "系统") == 0) {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m--> %s\033[0m", truncated_content);
|
||||
} else if (strcmp(msg->username, "*") == 0) {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m%s\033[0m \033[3;36m%s\033[0m",
|
||||
time_str, truncated_content);
|
||||
} else {
|
||||
snprintf(buffer, buf_size,
|
||||
"\033[90m%s\033[0m %s%s\033[0m: %s%s%s",
|
||||
time_str, username_color(msg->username),
|
||||
msg->username, hl_start, truncated_content, hl_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos,
|
||||
const char *data, size_t len) {
|
||||
size_t available;
|
||||
|
|
@ -156,12 +56,9 @@ void tui_clear_screen(client_t *client) {
|
|||
void tui_render_screen(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
int render_width = client->width;
|
||||
int render_height = client->height;
|
||||
if (render_width < 10) render_width = 10;
|
||||
if (render_height < 4) render_height = 4;
|
||||
|
||||
const size_t buf_size = (size_t)(render_height + 10) * (MAX_MESSAGE_LEN + 64) + 2048;
|
||||
/* Heap-allocated: worst case is ~200 messages * ~1100 bytes + separator + status.
|
||||
* 64 KiB covers all real terminal sizes without stack risk. */
|
||||
const size_t buf_size = 65536;
|
||||
char *buffer = malloc(buf_size);
|
||||
if (!buffer) return;
|
||||
size_t pos = 0;
|
||||
|
|
@ -174,7 +71,7 @@ void tui_render_screen(client_t *client) {
|
|||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
/* Calculate which messages to show */
|
||||
int msg_height = render_height - 3;
|
||||
int msg_height = client->height - 3;
|
||||
if (msg_height < 1) msg_height = 1;
|
||||
|
||||
int start = 0;
|
||||
|
|
@ -232,11 +129,11 @@ void tui_render_screen(client_t *client) {
|
|||
|
||||
char title[256];
|
||||
snprintf(title, sizeof(title),
|
||||
" %s | 在线: %d | 模式: %s | ? 帮助 ",
|
||||
client->username, online, mode_str);
|
||||
" 聊天室 | 在线: %d | 模式: %s | Ctrl+C 退出 | ? 帮助 ",
|
||||
online, mode_str);
|
||||
|
||||
int title_width = utf8_string_width(title);
|
||||
int padding = render_width - title_width;
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
buffer_appendf(buffer, buf_size, &pos, ANSI_REVERSE "%s", title);
|
||||
|
|
@ -248,9 +145,8 @@ void tui_render_screen(client_t *client) {
|
|||
/* Render messages from snapshot */
|
||||
if (msg_snapshot) {
|
||||
for (int i = 0; i < snapshot_count; i++) {
|
||||
char msg_line[2048];
|
||||
format_message_colored(&msg_snapshot[i], msg_line, sizeof(msg_line),
|
||||
render_width, client->username);
|
||||
char msg_line[1024];
|
||||
message_format(&msg_snapshot[i], msg_line, sizeof(msg_line), client->width);
|
||||
buffer_appendf(buffer, buf_size, &pos, "%s\033[K\r\n", msg_line);
|
||||
}
|
||||
free(msg_snapshot);
|
||||
|
|
@ -262,7 +158,7 @@ void tui_render_screen(client_t *client) {
|
|||
}
|
||||
|
||||
/* Separator - use box drawing character */
|
||||
for (int i = 0; i < render_width; i++) {
|
||||
for (int i = 0; i < client->width; i++) {
|
||||
buffer_append_bytes(buffer, buf_size, &pos, "─", strlen("─"));
|
||||
}
|
||||
buffer_appendf(buffer, buf_size, &pos, "\033[K\r\n");
|
||||
|
|
@ -274,15 +170,8 @@ void tui_render_screen(client_t *client) {
|
|||
int total = msg_count;
|
||||
int scroll_pos = client->scroll_pos + 1;
|
||||
if (total == 0) scroll_pos = 0;
|
||||
int unseen = msg_count - end;
|
||||
if (unseen > 0) {
|
||||
buffer_appendf(buffer, buf_size, &pos,
|
||||
"-- NORMAL -- (%d/%d) \033[33m↓ %d new\033[0m\033[K",
|
||||
scroll_pos, total, unseen);
|
||||
} else {
|
||||
buffer_appendf(buffer, buf_size, &pos,
|
||||
"-- NORMAL -- (%d/%d)\033[K", scroll_pos, total);
|
||||
}
|
||||
buffer_appendf(buffer, buf_size, &pos,
|
||||
"-- NORMAL -- (%d/%d)\033[K", scroll_pos, total);
|
||||
} else if (client->mode == MODE_COMMAND) {
|
||||
buffer_appendf(buffer, buf_size, &pos, ":%s\033[K", client->command_input);
|
||||
}
|
||||
|
|
@ -295,23 +184,17 @@ void tui_render_screen(client_t *client) {
|
|||
void tui_render_input(client_t *client, const char *input) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
int rw = client->width;
|
||||
int rh = client->height;
|
||||
if (rw < 10) rw = 10;
|
||||
if (rh < 4) rh = 4;
|
||||
|
||||
char buffer[2048];
|
||||
int input_width = utf8_string_width(input);
|
||||
int avail = rw - 3;
|
||||
if (avail < 1) avail = 1;
|
||||
|
||||
/* Truncate from start if too long */
|
||||
char display[MAX_MESSAGE_LEN];
|
||||
strncpy(display, input, sizeof(display) - 1);
|
||||
display[sizeof(display) - 1] = '\0';
|
||||
|
||||
if (input_width > avail) {
|
||||
int excess = input_width - avail;
|
||||
if (input_width > client->width - 3) {
|
||||
/* Find where to start displaying */
|
||||
int excess = input_width - (client->width - 3);
|
||||
int skip_width = 0;
|
||||
const char *p = input;
|
||||
int bytes_read;
|
||||
|
|
@ -327,7 +210,7 @@ void tui_render_input(client_t *client, const char *input) {
|
|||
|
||||
/* Move to input line and clear it, then write input */
|
||||
snprintf(buffer, sizeof(buffer), "\033[%d;1H" ANSI_CLEAR_LINE "> %s",
|
||||
rh, display);
|
||||
client->height, display);
|
||||
|
||||
client_send(client, buffer, strlen(buffer));
|
||||
}
|
||||
|
|
@ -336,11 +219,6 @@ void tui_render_input(client_t *client, const char *input) {
|
|||
void tui_render_command_output(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
int rw = client->width;
|
||||
int rh = client->height;
|
||||
if (rw < 10) rw = 10;
|
||||
if (rh < 4) rh = 4;
|
||||
|
||||
char buffer[4096];
|
||||
size_t pos = 0;
|
||||
buffer[0] = '\0';
|
||||
|
|
@ -351,7 +229,7 @@ void tui_render_command_output(client_t *client) {
|
|||
/* Title */
|
||||
const char *title = " COMMAND OUTPUT ";
|
||||
int title_width = strlen(title);
|
||||
int padding = rw - title_width;
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title);
|
||||
|
|
@ -367,15 +245,15 @@ void tui_render_command_output(client_t *client) {
|
|||
|
||||
char *line = strtok(output_copy, "\n");
|
||||
int line_count = 0;
|
||||
int max_lines = rh - 2;
|
||||
int max_lines = client->height - 2;
|
||||
|
||||
while (line && line_count < max_lines) {
|
||||
char truncated[1024];
|
||||
strncpy(truncated, line, sizeof(truncated) - 1);
|
||||
truncated[sizeof(truncated) - 1] = '\0';
|
||||
|
||||
if (utf8_string_width(truncated) > rw) {
|
||||
utf8_truncate(truncated, rw);
|
||||
if (utf8_string_width(truncated) > client->width) {
|
||||
utf8_truncate(truncated, client->width);
|
||||
}
|
||||
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, "%s\r\n", truncated);
|
||||
|
|
@ -407,31 +285,33 @@ const char* tui_get_help_text(help_lang_t lang) {
|
|||
"NORMAL MODE KEYS:\n"
|
||||
" i - Return to INSERT mode\n"
|
||||
" : - Enter COMMAND mode\n"
|
||||
" j/k - Scroll down/up one line\n"
|
||||
" Ctrl+D/U - Scroll half page down/up\n"
|
||||
" Ctrl+F/B - Scroll full page down/up\n"
|
||||
" g/G - Jump to top/bottom\n"
|
||||
" j - Scroll down (older messages)\n"
|
||||
" k - Scroll up (newer messages)\n"
|
||||
" g - Jump to top (oldest)\n"
|
||||
" G - Jump to bottom (newest)\n"
|
||||
" ? - Show this help\n"
|
||||
" Ctrl+C - Exit chat\n"
|
||||
"\n"
|
||||
"AVAILABLE COMMANDS:\n"
|
||||
" :list, :users - Show online users\n"
|
||||
" :nick <name> - Change nickname\n"
|
||||
" :msg <user> <text> - Whisper to user\n"
|
||||
" :w <user> <text> - Short alias for :msg\n"
|
||||
" :help - Show available commands\n"
|
||||
" :clear - Clear command output\n"
|
||||
" :q, :quit, :exit - Disconnect\n"
|
||||
"COMMAND MODE KEYS:\n"
|
||||
" Enter - Execute command\n"
|
||||
" ESC - Cancel, return to NORMAL\n"
|
||||
" Backspace - Delete character\n"
|
||||
" Ctrl+W - Delete last word\n"
|
||||
" Ctrl+U - Delete line\n"
|
||||
"\n"
|
||||
"SPECIAL MESSAGES:\n"
|
||||
" /me <action> - Send action (e.g. /me waves)\n"
|
||||
" @username - Mention user (bell + highlight)\n"
|
||||
"AVAILABLE COMMANDS:\n"
|
||||
" list, users, who - Show online users\n"
|
||||
" help, commands - Show available commands\n"
|
||||
" clear, cls - Clear command output\n"
|
||||
"\n"
|
||||
"HELP SCREEN KEYS:\n"
|
||||
" q, ESC - Close help\n"
|
||||
" j/k - Scroll down/up\n"
|
||||
" g/G - Jump to top/bottom\n"
|
||||
" e/z - Switch English/Chinese\n";
|
||||
" j - Scroll down\n"
|
||||
" k - Scroll up\n"
|
||||
" g - Jump to top\n"
|
||||
" G - Jump to bottom\n"
|
||||
" e, E - Switch to English\n"
|
||||
" z, Z - Switch to Chinese\n";
|
||||
} else {
|
||||
return "终端聊天室 - 帮助\n"
|
||||
"\n"
|
||||
|
|
@ -451,31 +331,33 @@ const char* tui_get_help_text(help_lang_t lang) {
|
|||
"NORMAL 模式按键:\n"
|
||||
" i - 返回 INSERT 模式\n"
|
||||
" : - 进入 COMMAND 模式\n"
|
||||
" j/k - 向下/上滚动一行\n"
|
||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" j - 向下滚动(更早的消息)\n"
|
||||
" k - 向上滚动(更新的消息)\n"
|
||||
" g - 跳到顶部(最早)\n"
|
||||
" G - 跳到底部(最新)\n"
|
||||
" ? - 显示此帮助\n"
|
||||
" Ctrl+C - 退出聊天\n"
|
||||
"\n"
|
||||
"可用命令:\n"
|
||||
" :list, :users - 显示在线用户\n"
|
||||
" :nick <名字> - 更改昵称\n"
|
||||
" :msg <用户> <文本> - 私聊\n"
|
||||
" :w <用户> <文本> - :msg 的简写\n"
|
||||
" :help - 显示可用命令\n"
|
||||
" :clear - 清空命令输出\n"
|
||||
" :q, :quit, :exit - 断开连接\n"
|
||||
"COMMAND 模式按键:\n"
|
||||
" Enter - 执行命令\n"
|
||||
" ESC - 取消,返回 NORMAL 模式\n"
|
||||
" Backspace - 删除字符\n"
|
||||
" Ctrl+W - 删除上个单词\n"
|
||||
" Ctrl+U - 删除整行\n"
|
||||
"\n"
|
||||
"特殊消息:\n"
|
||||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||||
" @用户名 - 提及用户 (响铃+高亮)\n"
|
||||
"可用命令:\n"
|
||||
" list, users, who - 显示在线用户\n"
|
||||
" help, commands - 显示可用命令\n"
|
||||
" clear, cls - 清空命令输出\n"
|
||||
"\n"
|
||||
"帮助界面按键:\n"
|
||||
" q, ESC - 关闭帮助\n"
|
||||
" j/k - 向下/上滚动\n"
|
||||
" g/G - 跳到顶部/底部\n"
|
||||
" e/z - 切换英文/中文\n";
|
||||
" j - 向下滚动\n"
|
||||
" k - 向上滚动\n"
|
||||
" g - 跳到顶部\n"
|
||||
" G - 跳到底部\n"
|
||||
" e, E - 切换到英文\n"
|
||||
" z, Z - 切换到中文\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -483,11 +365,6 @@ const char* tui_get_help_text(help_lang_t lang) {
|
|||
void tui_render_help(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
int rw = client->width;
|
||||
int rh = client->height;
|
||||
if (rw < 10) rw = 10;
|
||||
if (rh < 4) rh = 4;
|
||||
|
||||
char buffer[8192];
|
||||
size_t pos = 0;
|
||||
buffer[0] = '\0';
|
||||
|
|
@ -498,7 +375,7 @@ void tui_render_help(client_t *client) {
|
|||
/* Title */
|
||||
const char *title = " HELP ";
|
||||
int title_width = strlen(title);
|
||||
int padding = rw - title_width;
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
buffer_appendf(buffer, sizeof(buffer), &pos, ANSI_REVERSE "%s", title);
|
||||
|
|
@ -522,7 +399,7 @@ void tui_render_help(client_t *client) {
|
|||
line = strtok(NULL, "\n");
|
||||
}
|
||||
|
||||
int content_height = rh - 2;
|
||||
int content_height = client->height - 2;
|
||||
if (content_height < 1) content_height = 1;
|
||||
int max_scroll = line_count - content_height + 1;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
|
|
|
|||
|
|
@ -193,9 +193,9 @@ bool utf8_is_valid_sequence(const char *bytes, int len) {
|
|||
uint32_t codepoint = 0;
|
||||
switch (len) {
|
||||
case 1:
|
||||
/* 0xxxxxxx - valid range: 0x01-0x7F (reject NUL) */
|
||||
/* 0xxxxxxx - valid range: 0x00-0x7F */
|
||||
codepoint = b[0];
|
||||
if (codepoint == 0 || codepoint > 0x7F) return false;
|
||||
if (codepoint > 0x7F) return false;
|
||||
break;
|
||||
case 2:
|
||||
/* 110xxxxx 10xxxxxx - valid range: 0x80-0x7FF */
|
||||
|
|
|
|||
BIN
tests/unit/test_message
Executable file
BIN
tests/unit/test_message
Executable file
Binary file not shown.
BIN
tests/unit/test_utf8
Executable file
BIN
tests/unit/test_utf8
Executable file
Binary file not shown.
25
tnt.1
25
tnt.1
|
|
@ -4,12 +4,11 @@
|
|||
tnt \- anonymous SSH chat server with Vim\-style TUI
|
||||
.SH SYNOPSIS
|
||||
.B tnt
|
||||
.RB [ \-p | \-\-port
|
||||
.RB [ \-p
|
||||
.IR port ]
|
||||
.RB [ \-d | \-\-state\-dir
|
||||
.RB [ \-d
|
||||
.IR dir ]
|
||||
.RB [ \-V | \-\-version ]
|
||||
.RB [ \-h | \-\-help ]
|
||||
.RB [ \-h ]
|
||||
.SH DESCRIPTION
|
||||
.B tnt
|
||||
is a multi\-user anonymous chat server accessed over SSH.
|
||||
|
|
@ -22,7 +21,7 @@ The server supports CJK and emoji input, rate limiting, access tokens, and
|
|||
a non\-interactive exec interface for scripting.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BR \-p ", " \-\-port " " \fIport\fR
|
||||
.BI \-p " port"
|
||||
Listen on
|
||||
.I port
|
||||
instead of the default 2222.
|
||||
|
|
@ -30,7 +29,7 @@ Overrides the
|
|||
.B PORT
|
||||
environment variable.
|
||||
.TP
|
||||
.BR \-d ", " \-\-state\-dir " " \fIdir\fR
|
||||
.BI \-d " dir"
|
||||
Store the host key and message log in
|
||||
.IR dir .
|
||||
Overrides the
|
||||
|
|
@ -38,10 +37,7 @@ Overrides the
|
|||
environment variable.
|
||||
Defaults to the current working directory.
|
||||
.TP
|
||||
.BR \-V ", " \-\-version
|
||||
Print version and exit.
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
.B \-h
|
||||
Print a short usage summary and exit.
|
||||
.SH CONNECTING
|
||||
.PP
|
||||
|
|
@ -84,8 +80,6 @@ ESC Switch to NORMAL
|
|||
Ctrl+W Delete last word
|
||||
Ctrl+U Clear input line
|
||||
Ctrl+C Switch to NORMAL
|
||||
/me \fIaction\fR Send action message (e.g. /me waves)
|
||||
@\fIusername\fR Mention user (bell notification + highlight)
|
||||
.TE
|
||||
.SS NORMAL mode
|
||||
.TS
|
||||
|
|
@ -103,8 +97,6 @@ Ctrl+C Disconnect
|
|||
.TS
|
||||
l l.
|
||||
:list Show online users
|
||||
:nick \fIname\fR Change nickname
|
||||
:name \fIname\fR Alias for :nick
|
||||
:msg \fIuser text\fR Send private whisper
|
||||
:w \fIuser text\fR Short alias for :msg
|
||||
:help Show available commands
|
||||
|
|
@ -122,7 +114,6 @@ 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 post "/me deploys v2.0"
|
||||
ssh host \-p 2222 health
|
||||
.fi
|
||||
.PP
|
||||
|
|
@ -153,10 +144,6 @@ Max new connections per IP per 60\-second window (default: 10).
|
|||
.B TNT_RATE_LIMIT
|
||||
Set to 0 to disable rate\-based blocking and auth\-failure IP blocking.
|
||||
Explicit capacity limits still apply (default: 1).
|
||||
.TP
|
||||
.B TNT_IDLE_TIMEOUT
|
||||
Disconnect clients after this many seconds of inactivity.
|
||||
Set to 0 to disable (default: 1800, i.e. 30 minutes).
|
||||
.SH FILES
|
||||
.TP
|
||||
.I messages.log
|
||||
|
|
|
|||
Loading…
Reference in a new issue