mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:54:38 +08:00
refactor: extract exec module (PR2-M2)
Move the SSH exec subcommand interface (help, health, users, stats, tail, post) and the dispatcher out of ssh_server.c into a dedicated module. New API (include/exec.h): - exec_dispatch(client_t *) -- single entry point invoked from the bootstrap path when client->exec_command[0] != '\0'. Helpers that travel with the exec subcommands: - format_timestamp_utc, trim_ascii_whitespace, json_append_string, resolve_exec_username, parse_tail_count Two cross-module bridges: - is_valid_username() lifted into common.c/h since exec, the input read path, and the :nick command all need it. - ssh_server_start_time() added to ssh_server.h as a read-only accessor; exec_command_stats no longer reaches into the global. - notify_mentions stays in ssh_server.c for now and is exposed via ssh_server.h. Will move to a dedicated client.c during PR2-M6. ssh_server.c shrinks from 2200 to 1769 lines (-431). Behaviour is preserved: implementations are byte-for-byte the same.
This commit is contained in:
parent
562ee5296d
commit
7f9babf4f4
6 changed files with 519 additions and 466 deletions
|
|
@ -67,4 +67,9 @@ void buffer_appendf(char *buffer, size_t buf_size, size_t *pos,
|
|||
* non-numeric, or out of range. */
|
||||
int env_int(const char *name, int fallback, int min_val, int max_val);
|
||||
|
||||
/* Reject usernames containing shell metacharacters, control characters, or
|
||||
* a leading space/dot/dash. Used by username read, exec post (SSH login as
|
||||
* author), and the :nick command. */
|
||||
bool is_valid_username(const char *username);
|
||||
|
||||
#endif /* COMMON_H */
|
||||
|
|
|
|||
17
include/exec.h
Normal file
17
include/exec.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#ifndef EXEC_H
|
||||
#define EXEC_H
|
||||
|
||||
#include "ssh_server.h" /* for client_t */
|
||||
|
||||
/* Dispatch the non-interactive SSH exec command stored in
|
||||
* client->exec_command. Returns the exit status to send back to the
|
||||
* SSH client:
|
||||
* 0 = success
|
||||
* 1 = runtime error (I/O, OOM, persistence failure)
|
||||
* 64 = usage error (unknown command, bad args)
|
||||
*
|
||||
* Reads g_room and shared client state. Safe to call once per
|
||||
* exec-mode session before the channel is closed. */
|
||||
int exec_dispatch(client_t *client);
|
||||
|
||||
#endif /* EXEC_H */
|
||||
|
|
@ -58,4 +58,12 @@ int client_printf(client_t *client, const char *fmt, ...);
|
|||
void client_addref(client_t *client);
|
||||
void client_release(client_t *client);
|
||||
|
||||
/* Bell-notify any clients whose @username appears in the broadcast content,
|
||||
* skipping the sender. Defined in ssh_server.c (will move to a dedicated
|
||||
* client.c during PR2-M6). */
|
||||
void notify_mentions(const char *content, const client_t *sender);
|
||||
|
||||
/* Read-only accessor for the server start time (used by exec stats). */
|
||||
time_t ssh_server_start_time(void);
|
||||
|
||||
#endif /* SSH_SERVER_H */
|
||||
|
|
|
|||
26
src/common.c
26
src/common.c
|
|
@ -135,3 +135,29 @@ int env_int(const char *name, int fallback, int min_val, int max_val) {
|
|||
if (*end != '\0' || val < min_val || val > max_val) return fallback;
|
||||
return (int)val;
|
||||
}
|
||||
|
||||
bool is_valid_username(const char *username) {
|
||||
if (!username || username[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reject usernames starting with special characters */
|
||||
if (username[0] == ' ' || username[0] == '.' || username[0] == '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check for illegal characters that could cause injection */
|
||||
const char *illegal_chars = "|;&$`\n\r<>(){}[]'\"\\";
|
||||
for (size_t i = 0; i < strlen(username); i++) {
|
||||
/* Reject control characters (except tab) */
|
||||
if (username[i] < 32 && username[i] != 9) {
|
||||
return false;
|
||||
}
|
||||
/* Reject shell metacharacters */
|
||||
if (strchr(illegal_chars, username[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
452
src/exec.c
Normal file
452
src/exec.c
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
#include "exec.h"
|
||||
#include "chat_room.h"
|
||||
#include "common.h"
|
||||
#include "message.h"
|
||||
#include "ratelimit.h"
|
||||
#include "utf8.h"
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* `notify_mentions` is shared with the interactive INSERT-mode send path
|
||||
* (currently still in ssh_server.c, will move to its own home in PR2-M5/M6).
|
||||
* Declared in ssh_server.h. */
|
||||
|
||||
static void format_timestamp_utc(time_t ts, char *buffer, size_t buf_size) {
|
||||
struct tm tm_info;
|
||||
|
||||
if (!buffer || buf_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
gmtime_r(&ts, &tm_info);
|
||||
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
||||
}
|
||||
|
||||
static void trim_ascii_whitespace(char *text) {
|
||||
char *start;
|
||||
char *end;
|
||||
|
||||
if (!text || text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
start = text;
|
||||
while (*start && isspace((unsigned char)*start)) {
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start != text) {
|
||||
memmove(text, start, strlen(start) + 1);
|
||||
}
|
||||
|
||||
if (text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
end = text + strlen(text) - 1;
|
||||
while (end >= text && isspace((unsigned char)*end)) {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
static void json_append_string(char *buffer, size_t buf_size, size_t *pos,
|
||||
const char *text) {
|
||||
const unsigned char *p = (const unsigned char *)(text ? text : "");
|
||||
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\"", 1);
|
||||
|
||||
while (*p && *pos < buf_size - 1) {
|
||||
char escaped[7];
|
||||
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\\\", 2);
|
||||
break;
|
||||
case '"':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\\"", 2);
|
||||
break;
|
||||
case '\n':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\n", 2);
|
||||
break;
|
||||
case '\r':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\r", 2);
|
||||
break;
|
||||
case '\t':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\t", 2);
|
||||
break;
|
||||
default:
|
||||
if (*p < 0x20) {
|
||||
snprintf(escaped, sizeof(escaped), "\\u%04x", *p);
|
||||
buffer_append_bytes(buffer, buf_size, pos,
|
||||
escaped, strlen(escaped));
|
||||
} else {
|
||||
buffer_append_bytes(buffer, buf_size, pos,
|
||||
(const char *)p, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\"", 1);
|
||||
}
|
||||
|
||||
static void resolve_exec_username(const client_t *client, char *buffer,
|
||||
size_t buf_size) {
|
||||
if (!buffer || buf_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client && client->ssh_login[0] != '\0' &&
|
||||
is_valid_username(client->ssh_login)) {
|
||||
snprintf(buffer, buf_size, "%s", client->ssh_login);
|
||||
} else {
|
||||
snprintf(buffer, buf_size, "%s", "anonymous");
|
||||
}
|
||||
|
||||
if (utf8_strlen(buffer) > 20) {
|
||||
utf8_truncate(buffer, 20);
|
||||
}
|
||||
}
|
||||
|
||||
static int exec_command_help(client_t *client) {
|
||||
static const char help_text[] =
|
||||
"TNT exec interface\n"
|
||||
"Commands:\n"
|
||||
" help Show this help\n"
|
||||
" health Print service health\n"
|
||||
" users [--json] List online users\n"
|
||||
" stats [--json] Print room statistics\n"
|
||||
" 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;
|
||||
}
|
||||
|
||||
static int exec_command_health(client_t *client) {
|
||||
static const char ok[] = "ok\n";
|
||||
return client_send(client, ok, sizeof(ok) - 1) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int exec_command_users(client_t *client, bool json) {
|
||||
int count;
|
||||
char (*usernames)[MAX_USERNAME_LEN] = NULL;
|
||||
char *output;
|
||||
size_t output_size;
|
||||
size_t pos = 0;
|
||||
int rc;
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
count = g_room->client_count;
|
||||
if (count > 0) {
|
||||
usernames = calloc((size_t)count, sizeof(*usernames));
|
||||
if (!usernames) {
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
client_printf(client, "users: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
snprintf(usernames[i], MAX_USERNAME_LEN, "%s",
|
||||
g_room->clients[i]->username);
|
||||
}
|
||||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
output_size = json ? ((size_t)count * (MAX_USERNAME_LEN * 2 + 8) + 8)
|
||||
: ((size_t)count * (MAX_USERNAME_LEN + 1) + 1);
|
||||
if (output_size < 8) {
|
||||
output_size = 8;
|
||||
}
|
||||
|
||||
output = calloc(output_size, 1);
|
||||
if (!output) {
|
||||
free(usernames);
|
||||
client_printf(client, "users: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
buffer_append_bytes(output, output_size, &pos, "[", 1);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i > 0) {
|
||||
buffer_append_bytes(output, output_size, &pos, ",", 1);
|
||||
}
|
||||
json_append_string(output, output_size, &pos, usernames[i]);
|
||||
}
|
||||
buffer_append_bytes(output, output_size, &pos, "]\n", 2);
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer_appendf(output, output_size, &pos, "%s\n", usernames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
rc = client_send(client, output, pos) == 0 ? 0 : 1;
|
||||
free(output);
|
||||
free(usernames);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int exec_command_stats(client_t *client, bool json) {
|
||||
int online_users;
|
||||
int message_count;
|
||||
int client_capacity;
|
||||
int active_connections;
|
||||
time_t now = time(NULL);
|
||||
long uptime_seconds;
|
||||
char buffer[512];
|
||||
int len;
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
online_users = g_room->client_count;
|
||||
message_count = g_room->message_count;
|
||||
client_capacity = g_room->client_capacity;
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
active_connections = ratelimit_get_active_total();
|
||||
|
||||
time_t start = ssh_server_start_time();
|
||||
uptime_seconds = (start > 0 && now >= start) ? (long)(now - start) : 0;
|
||||
|
||||
if (json) {
|
||||
len = snprintf(buffer, sizeof(buffer),
|
||||
"{\"status\":\"ok\",\"online_users\":%d,"
|
||||
"\"message_count\":%d,\"client_capacity\":%d,"
|
||||
"\"active_connections\":%d,\"uptime_seconds\":%ld}\n",
|
||||
online_users, message_count, client_capacity,
|
||||
active_connections, uptime_seconds);
|
||||
} else {
|
||||
len = snprintf(buffer, sizeof(buffer),
|
||||
"status ok\n"
|
||||
"online_users %d\n"
|
||||
"message_count %d\n"
|
||||
"client_capacity %d\n"
|
||||
"active_connections %d\n"
|
||||
"uptime_seconds %ld\n",
|
||||
online_users, message_count, client_capacity,
|
||||
active_connections, uptime_seconds);
|
||||
}
|
||||
|
||||
if (len < 0 || len >= (int)sizeof(buffer)) {
|
||||
client_printf(client, "stats: output overflow\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return client_send(client, buffer, (size_t)len) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int parse_tail_count(const char *args, int *count) {
|
||||
char *end = NULL;
|
||||
long value;
|
||||
|
||||
if (!count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*count = 20;
|
||||
if (!args || args[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(args, "-n", 2) == 0) {
|
||||
args += 2;
|
||||
while (*args && isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
}
|
||||
|
||||
value = strtol(args, &end, 10);
|
||||
if (end == args) {
|
||||
return -1;
|
||||
}
|
||||
while (*end) {
|
||||
if (!isspace((unsigned char)*end)) {
|
||||
return -1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
|
||||
if (value < 1 || value > MAX_MESSAGES) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*count = (int)value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int exec_command_tail(client_t *client, const char *args) {
|
||||
int requested = 20;
|
||||
int total_messages;
|
||||
int start;
|
||||
int count;
|
||||
message_t *snapshot = NULL;
|
||||
char *output;
|
||||
size_t output_size;
|
||||
size_t pos = 0;
|
||||
int rc;
|
||||
|
||||
if (parse_tail_count(args, &requested) < 0) {
|
||||
client_printf(client, "tail: usage: tail [N] | tail -n N\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
total_messages = g_room->message_count;
|
||||
start = total_messages - requested;
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
}
|
||||
count = total_messages - start;
|
||||
|
||||
if (count > 0) {
|
||||
snapshot = calloc((size_t)count, sizeof(message_t));
|
||||
if (!snapshot) {
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
client_printf(client, "tail: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
memcpy(snapshot, &g_room->messages[start], (size_t)count * sizeof(message_t));
|
||||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
output_size = (size_t)(count > 0 ? count : 1) *
|
||||
(MAX_USERNAME_LEN + MAX_MESSAGE_LEN + 48);
|
||||
output = calloc(output_size, 1);
|
||||
if (!output) {
|
||||
free(snapshot);
|
||||
client_printf(client, "tail: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char timestamp[64];
|
||||
format_timestamp_utc(snapshot[i].timestamp, timestamp, sizeof(timestamp));
|
||||
buffer_appendf(output, output_size, &pos, "%s\t%s\t%s\n",
|
||||
timestamp, snapshot[i].username, snapshot[i].content);
|
||||
}
|
||||
|
||||
rc = client_send(client, output, pos) == 0 ? 0 : 1;
|
||||
free(output);
|
||||
free(snapshot);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int exec_command_post(client_t *client, const char *args) {
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
char username[MAX_USERNAME_LEN];
|
||||
message_t msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
|
||||
if (!args || args[0] == '\0') {
|
||||
client_printf(client, "post: usage: post MESSAGE\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
strncpy(content, args, sizeof(content) - 1);
|
||||
content[sizeof(content) - 1] = '\0';
|
||||
trim_ascii_whitespace(content);
|
||||
|
||||
if (content[0] == '\0') {
|
||||
client_printf(client, "post: message cannot be empty\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
if (!utf8_is_valid_string(content)) {
|
||||
client_printf(client, "post: invalid UTF-8 input\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return client_send(client, "posted\n", 7) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
int exec_dispatch(client_t *client) {
|
||||
char command_copy[MAX_EXEC_COMMAND_LEN];
|
||||
char *cmd;
|
||||
char *args;
|
||||
|
||||
strncpy(command_copy, client->exec_command, sizeof(command_copy) - 1);
|
||||
command_copy[sizeof(command_copy) - 1] = '\0';
|
||||
trim_ascii_whitespace(command_copy);
|
||||
|
||||
cmd = command_copy;
|
||||
if (*cmd == '\0') {
|
||||
return exec_command_help(client);
|
||||
}
|
||||
|
||||
args = cmd;
|
||||
while (*args && !isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
if (*args) {
|
||||
*args++ = '\0';
|
||||
while (*args && isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
} else {
|
||||
args = NULL;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
|
||||
return exec_command_help(client);
|
||||
}
|
||||
if (strcmp(cmd, "health") == 0) {
|
||||
return exec_command_health(client);
|
||||
}
|
||||
if (strcmp(cmd, "users") == 0) {
|
||||
if (args && strcmp(args, "--json") != 0) {
|
||||
client_printf(client, "users: usage: users [--json]\n");
|
||||
return 64;
|
||||
}
|
||||
return exec_command_users(client, args != NULL);
|
||||
}
|
||||
if (strcmp(cmd, "stats") == 0) {
|
||||
if (args && strcmp(args, "--json") != 0) {
|
||||
client_printf(client, "stats: usage: stats [--json]\n");
|
||||
return 64;
|
||||
}
|
||||
return exec_command_stats(client, args != NULL);
|
||||
}
|
||||
if (strcmp(cmd, "tail") == 0) {
|
||||
return exec_command_tail(client, args);
|
||||
}
|
||||
if (strcmp(cmd, "post") == 0) {
|
||||
return exec_command_post(client, args);
|
||||
}
|
||||
if (strcmp(cmd, "exit") == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
client_printf(client, "Unknown command: %s\n", cmd);
|
||||
return 64;
|
||||
}
|
||||
477
src/ssh_server.c
477
src/ssh_server.c
|
|
@ -1,4 +1,5 @@
|
|||
#include "ssh_server.h"
|
||||
#include "exec.h"
|
||||
#include "ratelimit.h"
|
||||
#include "tui.h"
|
||||
#include "utf8.h"
|
||||
|
|
@ -41,6 +42,10 @@ typedef struct {
|
|||
|
||||
static time_t g_server_start_time = 0;
|
||||
|
||||
time_t ssh_server_start_time(void) {
|
||||
return g_server_start_time;
|
||||
}
|
||||
|
||||
/* Configuration from environment variables. Rate-limiting / connection-count
|
||||
* config has moved to ratelimit.{c,h}; the two below stay here until the auth
|
||||
* and input modules are extracted in later PR2 steps. */
|
||||
|
|
@ -102,43 +107,6 @@ static void sanitize_terminal_size(int *width, int *height) {
|
|||
}
|
||||
}
|
||||
|
||||
static void format_timestamp_utc(time_t ts, char *buffer, size_t buf_size) {
|
||||
struct tm tm_info;
|
||||
|
||||
if (!buffer || buf_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
gmtime_r(&ts, &tm_info);
|
||||
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
||||
}
|
||||
|
||||
/* Validate username to prevent injection attacks */
|
||||
static bool is_valid_username(const char *username) {
|
||||
if (!username || username[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reject usernames starting with special characters */
|
||||
if (username[0] == ' ' || username[0] == '.' || username[0] == '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check for illegal characters that could cause injection */
|
||||
const char *illegal_chars = "|;&$`\n\r<>(){}[]'\"\\";
|
||||
for (size_t i = 0; i < strlen(username); i++) {
|
||||
/* Reject control characters (except tab) */
|
||||
if (username[i] < 32 && username[i] != 9) {
|
||||
return false;
|
||||
}
|
||||
/* Reject shell metacharacters */
|
||||
if (strchr(illegal_chars, username[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/* Generate or load SSH host key */
|
||||
static int setup_host_key(ssh_bind sshbind) {
|
||||
struct stat st;
|
||||
|
|
@ -415,321 +383,11 @@ static int read_username(client_t *client) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
static void trim_ascii_whitespace(char *text) {
|
||||
char *start;
|
||||
char *end;
|
||||
|
||||
if (!text || text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
start = text;
|
||||
while (*start && isspace((unsigned char)*start)) {
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start != text) {
|
||||
memmove(text, start, strlen(start) + 1);
|
||||
}
|
||||
|
||||
if (text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
end = text + strlen(text) - 1;
|
||||
while (end >= text && isspace((unsigned char)*end)) {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
static void json_append_string(char *buffer, size_t buf_size, size_t *pos,
|
||||
const char *text) {
|
||||
const unsigned char *p = (const unsigned char *)(text ? text : "");
|
||||
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\"", 1);
|
||||
|
||||
while (*p && *pos < buf_size - 1) {
|
||||
char escaped[7];
|
||||
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\\\", 2);
|
||||
break;
|
||||
case '"':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\\"", 2);
|
||||
break;
|
||||
case '\n':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\n", 2);
|
||||
break;
|
||||
case '\r':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\r", 2);
|
||||
break;
|
||||
case '\t':
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\\t", 2);
|
||||
break;
|
||||
default:
|
||||
if (*p < 0x20) {
|
||||
snprintf(escaped, sizeof(escaped), "\\u%04x", *p);
|
||||
buffer_append_bytes(buffer, buf_size, pos,
|
||||
escaped, strlen(escaped));
|
||||
} else {
|
||||
buffer_append_bytes(buffer, buf_size, pos,
|
||||
(const char *)p, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
buffer_append_bytes(buffer, buf_size, pos, "\"", 1);
|
||||
}
|
||||
|
||||
static void resolve_exec_username(const client_t *client, char *buffer,
|
||||
size_t buf_size) {
|
||||
if (!buffer || buf_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client && client->ssh_login[0] != '\0' &&
|
||||
is_valid_username(client->ssh_login)) {
|
||||
snprintf(buffer, buf_size, "%s", client->ssh_login);
|
||||
} else {
|
||||
snprintf(buffer, buf_size, "%s", "anonymous");
|
||||
}
|
||||
|
||||
if (utf8_strlen(buffer) > 20) {
|
||||
utf8_truncate(buffer, 20);
|
||||
}
|
||||
}
|
||||
|
||||
static int exec_command_help(client_t *client) {
|
||||
static const char help_text[] =
|
||||
"TNT exec interface\n"
|
||||
"Commands:\n"
|
||||
" help Show this help\n"
|
||||
" health Print service health\n"
|
||||
" users [--json] List online users\n"
|
||||
" stats [--json] Print room statistics\n"
|
||||
" 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;
|
||||
}
|
||||
|
||||
static int exec_command_health(client_t *client) {
|
||||
static const char ok[] = "ok\n";
|
||||
return client_send(client, ok, sizeof(ok) - 1) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int exec_command_users(client_t *client, bool json) {
|
||||
int count;
|
||||
char (*usernames)[MAX_USERNAME_LEN] = NULL;
|
||||
char *output;
|
||||
size_t output_size;
|
||||
size_t pos = 0;
|
||||
int rc;
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
count = g_room->client_count;
|
||||
if (count > 0) {
|
||||
usernames = calloc((size_t)count, sizeof(*usernames));
|
||||
if (!usernames) {
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
client_printf(client, "users: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
snprintf(usernames[i], MAX_USERNAME_LEN, "%s",
|
||||
g_room->clients[i]->username);
|
||||
}
|
||||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
output_size = json ? ((size_t)count * (MAX_USERNAME_LEN * 2 + 8) + 8)
|
||||
: ((size_t)count * (MAX_USERNAME_LEN + 1) + 1);
|
||||
if (output_size < 8) {
|
||||
output_size = 8;
|
||||
}
|
||||
|
||||
output = calloc(output_size, 1);
|
||||
if (!output) {
|
||||
free(usernames);
|
||||
client_printf(client, "users: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
buffer_append_bytes(output, output_size, &pos, "[", 1);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i > 0) {
|
||||
buffer_append_bytes(output, output_size, &pos, ",", 1);
|
||||
}
|
||||
json_append_string(output, output_size, &pos, usernames[i]);
|
||||
}
|
||||
buffer_append_bytes(output, output_size, &pos, "]\n", 2);
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer_appendf(output, output_size, &pos, "%s\n", usernames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
rc = client_send(client, output, pos) == 0 ? 0 : 1;
|
||||
free(output);
|
||||
free(usernames);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int exec_command_stats(client_t *client, bool json) {
|
||||
int online_users;
|
||||
int message_count;
|
||||
int client_capacity;
|
||||
int active_connections;
|
||||
time_t now = time(NULL);
|
||||
long uptime_seconds;
|
||||
char buffer[512];
|
||||
int len;
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
online_users = g_room->client_count;
|
||||
message_count = g_room->message_count;
|
||||
client_capacity = g_room->client_capacity;
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
active_connections = ratelimit_get_active_total();
|
||||
|
||||
uptime_seconds = (g_server_start_time > 0 && now >= g_server_start_time)
|
||||
? (long)(now - g_server_start_time)
|
||||
: 0;
|
||||
|
||||
if (json) {
|
||||
len = snprintf(buffer, sizeof(buffer),
|
||||
"{\"status\":\"ok\",\"online_users\":%d,"
|
||||
"\"message_count\":%d,\"client_capacity\":%d,"
|
||||
"\"active_connections\":%d,\"uptime_seconds\":%ld}\n",
|
||||
online_users, message_count, client_capacity,
|
||||
active_connections, uptime_seconds);
|
||||
} else {
|
||||
len = snprintf(buffer, sizeof(buffer),
|
||||
"status ok\n"
|
||||
"online_users %d\n"
|
||||
"message_count %d\n"
|
||||
"client_capacity %d\n"
|
||||
"active_connections %d\n"
|
||||
"uptime_seconds %ld\n",
|
||||
online_users, message_count, client_capacity,
|
||||
active_connections, uptime_seconds);
|
||||
}
|
||||
|
||||
if (len < 0 || len >= (int)sizeof(buffer)) {
|
||||
client_printf(client, "stats: output overflow\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return client_send(client, buffer, (size_t)len) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int parse_tail_count(const char *args, int *count) {
|
||||
char *end = NULL;
|
||||
long value;
|
||||
|
||||
if (!count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*count = 20;
|
||||
if (!args || args[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(args, "-n", 2) == 0) {
|
||||
args += 2;
|
||||
while (*args && isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
}
|
||||
|
||||
value = strtol(args, &end, 10);
|
||||
if (end == args) {
|
||||
return -1;
|
||||
}
|
||||
while (*end) {
|
||||
if (!isspace((unsigned char)*end)) {
|
||||
return -1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
|
||||
if (value < 1 || value > MAX_MESSAGES) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*count = (int)value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int exec_command_tail(client_t *client, const char *args) {
|
||||
int requested = 20;
|
||||
int total_messages;
|
||||
int start;
|
||||
int count;
|
||||
message_t *snapshot = NULL;
|
||||
char *output;
|
||||
size_t output_size;
|
||||
size_t pos = 0;
|
||||
int rc;
|
||||
|
||||
if (parse_tail_count(args, &requested) < 0) {
|
||||
client_printf(client, "tail: usage: tail [N] | tail -n N\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
total_messages = g_room->message_count;
|
||||
start = total_messages - requested;
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
}
|
||||
count = total_messages - start;
|
||||
|
||||
if (count > 0) {
|
||||
snapshot = calloc((size_t)count, sizeof(message_t));
|
||||
if (!snapshot) {
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
client_printf(client, "tail: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
memcpy(snapshot, &g_room->messages[start], (size_t)count * sizeof(message_t));
|
||||
}
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
output_size = (size_t)(count > 0 ? count : 1) *
|
||||
(MAX_USERNAME_LEN + MAX_MESSAGE_LEN + 48);
|
||||
output = calloc(output_size, 1);
|
||||
if (!output) {
|
||||
free(snapshot);
|
||||
client_printf(client, "tail: out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char timestamp[64];
|
||||
format_timestamp_utc(snapshot[i].timestamp, timestamp, sizeof(timestamp));
|
||||
buffer_appendf(output, output_size, &pos, "%s\t%s\t%s\n",
|
||||
timestamp, snapshot[i].username, snapshot[i].content);
|
||||
}
|
||||
|
||||
rc = client_send(client, output, pos) == 0 ? 0 : 1;
|
||||
free(output);
|
||||
free(snapshot);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void notify_mentions(const char *content, const client_t *sender) {
|
||||
/* Notify any clients whose usernames appear as @mentions in `content`.
|
||||
* Lives here because it bridges chat_room (target lookup) and the client
|
||||
* I/O API; will move into a proper home when a `client.c` is carved out
|
||||
* during PR2-M6 server cleanup. */
|
||||
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];
|
||||
|
|
@ -754,119 +412,6 @@ static void notify_mentions(const char *content, const client_t *sender) {
|
|||
}
|
||||
}
|
||||
|
||||
static int exec_command_post(client_t *client, const char *args) {
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
char username[MAX_USERNAME_LEN];
|
||||
message_t msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
|
||||
if (!args || args[0] == '\0') {
|
||||
client_printf(client, "post: usage: post MESSAGE\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
strncpy(content, args, sizeof(content) - 1);
|
||||
content[sizeof(content) - 1] = '\0';
|
||||
trim_ascii_whitespace(content);
|
||||
|
||||
if (content[0] == '\0') {
|
||||
client_printf(client, "post: message cannot be empty\n");
|
||||
return 64;
|
||||
}
|
||||
|
||||
if (!utf8_is_valid_string(content)) {
|
||||
client_printf(client, "post: invalid UTF-8 input\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return client_send(client, "posted\n", 7) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int execute_exec_command(client_t *client) {
|
||||
char command_copy[MAX_EXEC_COMMAND_LEN];
|
||||
char *cmd;
|
||||
char *args;
|
||||
|
||||
strncpy(command_copy, client->exec_command, sizeof(command_copy) - 1);
|
||||
command_copy[sizeof(command_copy) - 1] = '\0';
|
||||
trim_ascii_whitespace(command_copy);
|
||||
|
||||
cmd = command_copy;
|
||||
if (*cmd == '\0') {
|
||||
return exec_command_help(client);
|
||||
}
|
||||
|
||||
args = cmd;
|
||||
while (*args && !isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
if (*args) {
|
||||
*args++ = '\0';
|
||||
while (*args && isspace((unsigned char)*args)) {
|
||||
args++;
|
||||
}
|
||||
} else {
|
||||
args = NULL;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
|
||||
return exec_command_help(client);
|
||||
}
|
||||
if (strcmp(cmd, "health") == 0) {
|
||||
return exec_command_health(client);
|
||||
}
|
||||
if (strcmp(cmd, "users") == 0) {
|
||||
if (args && strcmp(args, "--json") != 0) {
|
||||
client_printf(client, "users: usage: users [--json]\n");
|
||||
return 64;
|
||||
}
|
||||
return exec_command_users(client, args != NULL);
|
||||
}
|
||||
if (strcmp(cmd, "stats") == 0) {
|
||||
if (args && strcmp(args, "--json") != 0) {
|
||||
client_printf(client, "stats: usage: stats [--json]\n");
|
||||
return 64;
|
||||
}
|
||||
return exec_command_stats(client, args != NULL);
|
||||
}
|
||||
if (strcmp(cmd, "tail") == 0) {
|
||||
return exec_command_tail(client, args);
|
||||
}
|
||||
if (strcmp(cmd, "post") == 0) {
|
||||
return exec_command_post(client, args);
|
||||
}
|
||||
if (strcmp(cmd, "exit") == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
client_printf(client, "Unknown command: %s\n", cmd);
|
||||
return 64;
|
||||
}
|
||||
|
||||
/* Execute a command */
|
||||
static void execute_command(client_t *client) {
|
||||
char cmd_buf[256];
|
||||
|
|
@ -1388,7 +933,7 @@ void* client_handle_session(void *arg) {
|
|||
|
||||
/* Check for exec command */
|
||||
if (client->exec_command[0] != '\0') {
|
||||
int exit_status = execute_exec_command(client);
|
||||
int exit_status = exec_dispatch(client);
|
||||
ssh_channel_request_send_exit_status(client->channel, exit_status);
|
||||
goto cleanup;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue