Compare commits

..

No commits in common. "0de13a63140eae4a27be1da285c52ae95355572e" and "49674b75e833a585c64224c6e7da28f205b5b2cc" have entirely different histories.

9 changed files with 101 additions and 136 deletions

View file

@ -39,8 +39,8 @@ void room_broadcast(chat_room_t *room, const message_t *msg);
/* Add message to room history */ /* Add message to room history */
void room_add_message(chat_room_t *room, const message_t *msg); void room_add_message(chat_room_t *room, const message_t *msg);
/* Get message by index (thread-safe value copy) */ /* Get message by index */
bool room_get_message(chat_room_t *room, int index, message_t *out); const message_t* room_get_message(chat_room_t *room, int index);
/* Get total message count */ /* Get total message count */
int room_get_message_count(chat_room_t *room); int room_get_message_count(chat_room_t *room);

View file

@ -6,7 +6,6 @@
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdatomic.h>
#include <time.h> #include <time.h>
#include <limits.h> #include <limits.h>
#include <pthread.h> #include <pthread.h>

View file

@ -9,6 +9,7 @@
/* Client connection structure */ /* Client connection structure */
typedef struct client { typedef struct client {
int fd; /* Socket file descriptor (not used with SSH) */
ssh_session session; /* SSH session */ ssh_session session; /* SSH session */
ssh_channel channel; /* SSH channel */ ssh_channel channel; /* SSH channel */
char username[MAX_USERNAME_LEN]; char username[MAX_USERNAME_LEN];
@ -24,9 +25,9 @@ typedef struct client {
char command_output[2048]; char command_output[2048];
char exec_command[MAX_EXEC_COMMAND_LEN]; char exec_command[MAX_EXEC_COMMAND_LEN];
char ssh_login[MAX_USERNAME_LEN]; char ssh_login[MAX_USERNAME_LEN];
atomic_bool redraw_pending; bool redraw_pending;
pthread_t thread; pthread_t thread;
atomic_bool connected; bool connected;
int ref_count; /* Reference count for safe cleanup */ int ref_count; /* Reference count for safe cleanup */
pthread_mutex_t ref_lock; /* Lock for ref_count */ pthread_mutex_t ref_lock; /* Lock for ref_count */
pthread_mutex_t io_lock; /* Serialize SSH channel writes */ pthread_mutex_t io_lock; /* Serialize SSH channel writes */

View file

@ -10,13 +10,12 @@ static int room_capacity_from_env(void) {
return MAX_CLIENTS; return MAX_CLIENTS;
} }
char *end; int capacity = atoi(env);
long capacity = strtol(env, &end, 10); if (capacity < 1 || capacity > 1024) {
if (*end != '\0' || capacity < 1 || capacity > 1024) {
return MAX_CLIENTS; return MAX_CLIENTS;
} }
return (int)capacity; return capacity;
} }
/* Initialize chat room */ /* Initialize chat room */
@ -112,20 +111,17 @@ void room_add_message(chat_room_t *room, const message_t *msg) {
room->messages[room->message_count++] = *msg; room->messages[room->message_count++] = *msg;
} }
/* Get message by index (thread-safe value copy) */ /* Get message by index */
bool room_get_message(chat_room_t *room, int index, message_t *out) { const message_t* room_get_message(chat_room_t *room, int index) {
if (!room || !out) return false;
pthread_rwlock_rdlock(&room->lock); pthread_rwlock_rdlock(&room->lock);
bool found = false; const message_t *msg = NULL;
if (index >= 0 && index < room->message_count) { if (index >= 0 && index < room->message_count) {
*out = room->messages[index]; msg = &room->messages[index];
found = true;
} }
pthread_rwlock_unlock(&room->lock); pthread_rwlock_unlock(&room->lock);
return found; return msg;
} }
/* Get total message count */ /* Get total message count */

View file

@ -21,24 +21,14 @@ int main(int argc, char **argv) {
/* Environment provides defaults; command-line flags override it. */ /* Environment provides defaults; command-line flags override it. */
const char *port_env = getenv("PORT"); const char *port_env = getenv("PORT");
if (port_env && port_env[0] != '\0') { if (port_env) {
char *end; port = atoi(port_env);
long val = strtol(port_env, &end, 10);
if (*end == '\0' && val > 0 && val <= 65535) {
port = (int)val;
}
} }
/* Parse command line arguments */ /* Parse command line arguments */
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
char *end; port = atoi(argv[i + 1]);
long val = strtol(argv[i + 1], &end, 10);
if (*end != '\0' || val <= 0 || val > 65535) {
fprintf(stderr, "Invalid port: %s\n", argv[i + 1]);
return 1;
}
port = (int)val;
i++; i++;
} else if ((strcmp(argv[i], "-d") == 0 || } else if ((strcmp(argv[i], "-d") == 0 ||
strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) { strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) {
@ -55,10 +45,6 @@ int main(int argc, char **argv) {
printf(" -d DIR Store host key and logs in DIR\n"); printf(" -d DIR Store host key and logs in DIR\n");
printf(" -h Show this help\n"); printf(" -h Show this help\n");
return 0; return 0;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
fprintf(stderr, "Usage: %s [-p PORT] [-d DIR] [-h]\n", argv[0]);
return 1;
} }
} }

View file

@ -1,9 +1,3 @@
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE /* for timegm() on glibc */
#endif
#ifdef __APPLE__
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
#endif
#include "message.h" #include "message.h"
#include "utf8.h" #include "utf8.h"
#include <unistd.h> #include <unistd.h>
@ -13,17 +7,44 @@ static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
static time_t parse_rfc3339_utc(const char *timestamp_str) { static time_t parse_rfc3339_utc(const char *timestamp_str) {
struct tm tm = {0}; struct tm tm = {0};
char *result;
char *old_tz = NULL;
time_t parsed;
if (!timestamp_str) { if (!timestamp_str) {
return (time_t)-1; return (time_t)-1;
} }
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm); result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
if (!result || *result != '\0') { if (!result || *result != '\0') {
return (time_t)-1; return (time_t)-1;
} }
return timegm(&tm); const char *tz = getenv("TZ");
if (tz) {
old_tz = strdup(tz);
if (!old_tz) {
return (time_t)-1;
}
}
if (setenv("TZ", "UTC0", 1) != 0) {
free(old_tz);
return (time_t)-1;
}
tzset();
parsed = mktime(&tm);
if (old_tz) {
setenv("TZ", old_tz, 1);
free(old_tz);
} else {
unsetenv("TZ");
}
tzset();
return parsed;
} }
/* Initialize message subsystem */ /* Initialize message subsystem */
@ -239,22 +260,9 @@ void message_format(const message_t *msg, char *buffer, size_t buf_size, int wid
char time_str[64]; char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M %Z", &tm_info); strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M %Z", &tm_info);
int written = snprintf(buffer, buf_size, "[%s] %s: %s", time_str, msg->username, msg->content); snprintf(buffer, buf_size, "[%s] %s: %s", time_str, msg->username, msg->content);
/* If snprintf truncated, the last UTF-8 character may be incomplete. /* Truncate if too long */
* Re-validate and trim any trailing partial sequence. */
if (written >= (int)buf_size) {
size_t len = strlen(buffer);
while (len > 0 && (buffer[len - 1] & 0xC0) == 0x80) {
len--; /* walk back continuation bytes */
}
if (len > 0 && (unsigned char)buffer[len - 1] >= 0xC0) {
/* This is a start byte whose sequence was truncated */
buffer[len - 1] = '\0';
}
}
/* Truncate to terminal width */
if (utf8_string_width(buffer) > width) { if (utf8_string_width(buffer) > width) {
utf8_truncate(buffer, width); utf8_truncate(buffer, width);
} }

View file

@ -108,38 +108,34 @@ static void buffer_append_bytes(char *buffer, size_t buf_size, size_t *pos,
buffer[*pos] = '\0'; buffer[*pos] = '\0';
} }
/* Constant-time string comparison to prevent timing side-channel attacks */
static bool constant_time_strcmp(const char *a, const char *b) {
size_t len_a = strlen(a);
size_t len_b = strlen(b);
/* Use len_b (the secret) for iteration to avoid leaking its length
* through early termination. The XOR of lengths catches mismatches. */
volatile unsigned char result = (unsigned char)(len_a ^ len_b);
size_t len = (len_a < len_b) ? len_a : len_b;
for (size_t i = 0; i < len; i++) {
result |= (unsigned char)((unsigned char)a[i] ^ (unsigned char)b[i]);
}
return result == 0;
}
/* Safe integer parse from environment variable; returns fallback on error. */
static int env_int(const char *name, int fallback, int min_val, int max_val) {
const char *env = getenv(name);
if (!env || env[0] == '\0') return fallback;
char *end;
long val = strtol(env, &end, 10);
if (*end != '\0' || val < min_val || val > max_val) return fallback;
return (int)val;
}
/* Initialize rate limit configuration from environment */ /* Initialize rate limit configuration from environment */
static void init_rate_limit_config(void) { static void init_rate_limit_config(void) {
const char *env; const char *env;
g_max_connections = env_int("TNT_MAX_CONNECTIONS", 64, 1, 1024); if ((env = getenv("TNT_MAX_CONNECTIONS")) != NULL) {
g_max_conn_per_ip = env_int("TNT_MAX_CONN_PER_IP", 5, 1, 1024); int val = atoi(env);
g_max_conn_rate_per_ip = env_int("TNT_MAX_CONN_RATE_PER_IP", 10, 1, 1024); if (val > 0 && val <= 1024) {
g_rate_limit_enabled = env_int("TNT_RATE_LIMIT", 1, 0, 1); g_max_connections = val;
}
}
if ((env = getenv("TNT_MAX_CONN_PER_IP")) != NULL) {
int val = atoi(env);
if (val > 0 && val <= 1024) {
g_max_conn_per_ip = val;
}
}
if ((env = getenv("TNT_MAX_CONN_RATE_PER_IP")) != NULL) {
int val = atoi(env);
if (val > 0 && val <= 1024) {
g_max_conn_rate_per_ip = val;
}
}
if ((env = getenv("TNT_RATE_LIMIT")) != NULL) {
g_rate_limit_enabled = atoi(env);
}
if ((env = getenv("TNT_ACCESS_TOKEN")) != NULL) { if ((env = getenv("TNT_ACCESS_TOKEN")) != NULL) {
strncpy(g_access_token, env, sizeof(g_access_token) - 1); strncpy(g_access_token, env, sizeof(g_access_token) - 1);
@ -184,8 +180,6 @@ static ip_rate_limit_t* get_rate_limit_entry(const char *ip) {
} }
if (oldest_idx < 0) { if (oldest_idx < 0) {
/* All slots have active connections — evicting will corrupt their
* concurrency accounting. Pick the oldest entry but warn. */
oldest_idx = 0; oldest_idx = 0;
oldest_time = g_rate_limits[0].window_start; oldest_time = g_rate_limits[0].window_start;
for (int i = 1; i < MAX_TRACKED_IPS; i++) { for (int i = 1; i < MAX_TRACKED_IPS; i++) {
@ -194,8 +188,6 @@ static ip_rate_limit_t* get_rate_limit_entry(const char *ip) {
oldest_idx = i; oldest_idx = i;
} }
} }
fprintf(stderr, "Warning: rate-limit table full, evicting active IP %s\n",
g_rate_limits[oldest_idx].ip);
} }
/* Reset and reuse */ /* Reset and reuse */
@ -241,7 +233,7 @@ static bool check_ip_connection_policy(const char *ip) {
} }
entry->recent_connection_count++; entry->recent_connection_count++;
if (entry->recent_connection_count >= g_max_conn_rate_per_ip) { if (entry->recent_connection_count > g_max_conn_rate_per_ip) {
entry->is_blocked = true; entry->is_blocked = true;
entry->block_until = now + BLOCK_DURATION; entry->block_until = now + BLOCK_DURATION;
pthread_mutex_unlock(&g_rate_limit_lock); pthread_mutex_unlock(&g_rate_limit_lock);
@ -606,14 +598,15 @@ static int read_username(client_t *client) {
} }
buf[0] = b; buf[0] = b;
if (len > 1) { if (len > 1) {
int read_bytes = ssh_channel_read_timeout(client->channel, &buf[1], len - 1, 0, 5000); int read_bytes = ssh_channel_read(client->channel, &buf[1], len - 1, 0);
if (read_bytes != len - 1) { if (read_bytes != len - 1) {
/* Incomplete or timed-out UTF-8 continuation */ /* Incomplete UTF-8 */
continue; continue;
} }
} }
/* Validate the complete UTF-8 sequence */ /* Validate the complete UTF-8 sequence */
if (!utf8_is_valid_sequence(buf, len)) { if (!utf8_is_valid_sequence(buf, len)) {
/* Invalid UTF-8 sequence */
continue; continue;
} }
if (pos + len < MAX_USERNAME_LEN - 1) { if (pos + len < MAX_USERNAME_LEN - 1) {
@ -1451,9 +1444,9 @@ void* client_handle_session(void *arg) {
} }
buf[0] = b; buf[0] = b;
if (char_len > 1) { if (char_len > 1) {
int read_bytes = ssh_channel_read_timeout(client->channel, &buf[1], char_len - 1, 0, 5000); int read_bytes = ssh_channel_read(client->channel, &buf[1], char_len - 1, 0);
if (read_bytes != char_len - 1) { if (read_bytes != char_len - 1) {
/* Incomplete or timed-out UTF-8 continuation */ /* Incomplete UTF-8 sequence */
continue; continue;
} }
} }
@ -1500,9 +1493,6 @@ cleanup:
release_ip_connection(client->client_ip); release_ip_connection(client->client_ip);
/* Release the callback reference (paired with addref before install_client_channel_callbacks) */
client_release(client);
/* Release the main reference - client will be freed when all refs are gone */ /* Release the main reference - client will be freed when all refs are gone */
client_release(client); client_release(client);
@ -1536,7 +1526,7 @@ static int auth_password(ssh_session session, const char *user,
/* If access token is configured, require it */ /* If access token is configured, require it */
if (g_access_token[0] != '\0') { if (g_access_token[0] != '\0') {
if (password && constant_time_strcmp(password, g_access_token)) { if (password && strcmp(password, g_access_token) == 0) {
/* Token matches */ /* Token matches */
ctx->auth_success = true; ctx->auth_success = true;
return SSH_AUTH_SUCCESS; return SSH_AUTH_SUCCESS;
@ -1577,8 +1567,9 @@ static int auth_none(ssh_session session, const char *user, void *userdata) {
static int auth_pubkey(ssh_session session, const char *user, static int auth_pubkey(ssh_session session, const char *user,
struct ssh_key_struct *pubkey, char signature_state, struct ssh_key_struct *pubkey, char signature_state,
void *userdata) { void *userdata) {
(void)session; (void)session; /* Unused */
(void)pubkey; (void)pubkey; /* Unused */
(void)signature_state; /* Unused in anonymous mode */
session_context_t *ctx = (session_context_t *)userdata; session_context_t *ctx = (session_context_t *)userdata;
if (user && user[0] != '\0') { if (user && user[0] != '\0') {
@ -1586,17 +1577,10 @@ static int auth_pubkey(ssh_session session, const char *user,
ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0'; ctx->requested_user[sizeof(ctx->requested_user) - 1] = '\0';
} }
/* Reject if access token is required (pubkey auth not supported with tokens) */
if (g_access_token[0] != '\0') { if (g_access_token[0] != '\0') {
return SSH_AUTH_DENIED; return SSH_AUTH_DENIED;
} }
/* 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_PARTIAL;
}
ctx->auth_success = true; ctx->auth_success = true;
return SSH_AUTH_SUCCESS; return SSH_AUTH_SUCCESS;
} }
@ -1942,6 +1926,7 @@ static void *bootstrap_client_session(void *arg) {
client->session = session; client->session = session;
client->channel = channel; client->channel = channel;
client->fd = -1;
client->width = ctx->pty_width; client->width = ctx->pty_width;
client->height = ctx->pty_height; client->height = ctx->pty_height;
sanitize_terminal_size(&client->width, &client->height); sanitize_terminal_size(&client->width, &client->height);
@ -1964,12 +1949,7 @@ static void *bootstrap_client_session(void *arg) {
client->exec_command[sizeof(client->exec_command) - 1] = '\0'; client->exec_command[sizeof(client->exec_command) - 1] = '\0';
} }
/* Add a ref for the channel callbacks (eof/close/window_change) so the
* client_t outlives any in-flight callback invocation. */
client_addref(client);
if (install_client_channel_callbacks(client) < 0) { if (install_client_channel_callbacks(client) < 0) {
client_release(client); /* drop the callback ref */
pthread_mutex_destroy(&client->io_lock); pthread_mutex_destroy(&client->io_lock);
pthread_mutex_destroy(&client->ref_lock); pthread_mutex_destroy(&client->ref_lock);
free(client); free(client);
@ -2018,7 +1998,14 @@ int ssh_server_init(int port) {
ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDADDR, bind_addr); ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDADDR, bind_addr);
/* Configurable SSH log level (default: SSH_LOG_WARNING=1) */ /* Configurable SSH log level (default: SSH_LOG_WARNING=1) */
int verbosity = env_int("TNT_SSH_LOG_LEVEL", SSH_LOG_WARNING, 0, 4); int verbosity = SSH_LOG_WARNING;
const char *log_level_env = getenv("TNT_SSH_LOG_LEVEL");
if (log_level_env) {
int level = atoi(log_level_env);
if (level >= 0 && level <= 4) {
verbosity = level;
}
}
ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &verbosity); ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &verbosity);
if (ssh_bind_listen(g_sshbind) < 0) { if (ssh_bind_listen(g_sshbind) < 0) {
@ -2104,5 +2091,7 @@ int ssh_server_start(int unused) {
continue; continue;
} }
} }
/* Unreachable — the while(1) loop only exits via signal/_exit(). */
pthread_attr_destroy(&attr);
return 0;
} }

View file

@ -64,11 +64,10 @@ void tui_render_screen(client_t *client) {
size_t pos = 0; size_t pos = 0;
buffer[0] = '\0'; buffer[0] = '\0';
/* First pass under lock: compute indices and counts */ /* Acquire all data in one lock to prevent TOCTOU */
pthread_rwlock_rdlock(&g_room->lock); pthread_rwlock_rdlock(&g_room->lock);
int online = g_room->client_count; int online = g_room->client_count;
int msg_count = g_room->message_count; int msg_count = g_room->message_count;
pthread_rwlock_unlock(&g_room->lock);
/* Calculate which messages to show */ /* Calculate which messages to show */
int msg_height = client->height - 3; int msg_height = client->height - 3;
@ -91,31 +90,19 @@ void tui_render_screen(client_t *client) {
int end = start + msg_height; int end = start + msg_height;
if (end > msg_count) end = msg_count; if (end > msg_count) end = msg_count;
/* Allocate snapshot outside the lock to avoid blocking writers */ /* Create snapshot of messages to display */
message_t *msg_snapshot = NULL; message_t *msg_snapshot = NULL;
int snapshot_count = end - start; int snapshot_count = end - start;
if (snapshot_count > 0) { if (snapshot_count > 0) {
msg_snapshot = calloc(snapshot_count, sizeof(message_t)); msg_snapshot = calloc(snapshot_count, sizeof(message_t));
if (msg_snapshot) {
memcpy(msg_snapshot, &g_room->messages[start],
snapshot_count * sizeof(message_t));
}
} }
/* Second pass under lock: copy messages */ pthread_rwlock_unlock(&g_room->lock);
if (msg_snapshot) {
pthread_rwlock_rdlock(&g_room->lock);
/* Re-clamp in case msg_count changed */
int actual_count = g_room->message_count;
int actual_end = (end <= actual_count) ? end : actual_count;
int actual_start = (start < actual_end) ? start : actual_end;
int actual_snapshot = actual_end - actual_start;
if (actual_snapshot > 0 && actual_snapshot <= snapshot_count) {
memcpy(msg_snapshot, &g_room->messages[actual_start],
actual_snapshot * sizeof(message_t));
snapshot_count = actual_snapshot;
} else {
snapshot_count = 0;
}
pthread_rwlock_unlock(&g_room->lock);
}
/* Now render using snapshot (no lock held) */ /* Now render using snapshot (no lock held) */

View file

@ -20,8 +20,7 @@ uint32_t utf8_decode(const char *str, int *bytes_read) {
} }
for (int i = 1; i < len; i++) { for (int i = 1; i < len; i++) {
if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) { if (s[i] == '\0') {
/* Truncated or invalid continuation byte — treat as single byte */
*bytes_read = 1; *bytes_read = 1;
return s[0]; return s[0];
} }