diff --git a/include/client.h b/include/client.h new file mode 100644 index 0000000..968f770 --- /dev/null +++ b/include/client.h @@ -0,0 +1,34 @@ +#ifndef CLIENT_H +#define CLIENT_H + +#include "ssh_server.h" /* for client_t */ + +/* Send `len` bytes to the client over its SSH channel. Serialised on + * client->io_lock so concurrent senders don't interleave. Returns 0 on + * success, -1 if the channel is gone or a partial write fails. */ +int client_send(client_t *client, const char *data, size_t len); + +/* printf-style wrapper around client_send(). The formatted string must + * fit in 2048 bytes; truncation or encoding errors return -1. */ +int client_printf(client_t *client, const char *fmt, ...); + +/* Reference counting for safe cross-thread cleanup. + * + * Lifecycle: bootstrap_run() creates the client_t with ref_count = 1 + * (the "main" ref), then adds a second ref before installing the channel + * callbacks (the "callback" ref) so the client outlives any in-flight + * eof / close / window-change callback invocation. The interactive + * session releases both refs in its cleanup path; the final release + * frees the SSH session, channel, callback struct, and the client_t. */ +void client_addref(client_t *client); +void client_release(client_t *client); + +/* Install the post-bootstrap channel callbacks (window-change, eof, close) + * that target this client_t. Caller MUST have already added one + * client_addref() to keep the client alive across in-flight callback + * invocations; the matching client_release() happens during cleanup in + * input_run_session(). Returns 0 on success, -1 on failure (in which + * case the caller still owns both refs and must release them). */ +int client_install_channel_callbacks(client_t *client); + +#endif /* CLIENT_H */ diff --git a/include/ssh_server.h b/include/ssh_server.h index 07b97b1..a9cb80c 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -45,24 +45,6 @@ int ssh_server_init(int port); /* Start SSH server (blocking) */ int ssh_server_start(int listen_fd); -/* Send data to client */ -int client_send(client_t *client, const char *data, size_t len); - -/* Send formatted string to client */ -int client_printf(client_t *client, const char *fmt, ...); - -/* Reference counting helpers */ -void client_addref(client_t *client); -void client_release(client_t *client); - -/* Install the post-bootstrap channel callbacks (window-change, eof, close) - * that target this client_t. Caller MUST have already added one - * client_addref() to keep the client alive across in-flight callback - * invocations; the matching client_release() happens during cleanup in - * input_run_session(). Returns 0 on success, -1 on failure (in which - * case the caller still owns both refs and must release them). */ -int client_install_channel_callbacks(client_t *client); - /* Read-only accessor for the server start time (used by exec stats). */ time_t ssh_server_start_time(void); diff --git a/src/bootstrap.c b/src/bootstrap.c index be1eb2c..2d77284 100644 --- a/src/bootstrap.c +++ b/src/bootstrap.c @@ -1,4 +1,5 @@ #include "bootstrap.h" +#include "client.h" #include "common.h" #include "input.h" #include "ratelimit.h" diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..def3c9d --- /dev/null +++ b/src/client.c @@ -0,0 +1,162 @@ +#include "client.h" +#include "common.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* Send data to client via SSH channel */ +int client_send(client_t *client, const char *data, size_t len) { + size_t total = 0; + + if (!client || !data) return -1; + + pthread_mutex_lock(&client->io_lock); + + if (!client->connected || !client->channel) { + pthread_mutex_unlock(&client->io_lock); + return -1; + } + + while (total < len) { + size_t remaining = len - total; + uint32_t chunk = (remaining > 32768) ? 32768 : (uint32_t)remaining; + int sent = ssh_channel_write(client->channel, data + total, chunk); + if (sent <= 0) { + pthread_mutex_unlock(&client->io_lock); + return -1; + } + total += (size_t)sent; + } + + pthread_mutex_unlock(&client->io_lock); + return 0; +} + +void client_addref(client_t *client) { + if (!client) return; + pthread_mutex_lock(&client->ref_lock); + client->ref_count++; + pthread_mutex_unlock(&client->ref_lock); +} + +void client_release(client_t *client) { + if (!client) return; + + pthread_mutex_lock(&client->ref_lock); + client->ref_count--; + int count = client->ref_count; + pthread_mutex_unlock(&client->ref_lock); + + if (count == 0) { + /* Safe to free now */ + if (client->channel && client->channel_cb) { + ssh_remove_channel_callbacks(client->channel, client->channel_cb); + } + if (client->channel) { + ssh_channel_close(client->channel); + ssh_channel_free(client->channel); + } + if (client->session) { + ssh_disconnect(client->session); + ssh_free(client->session); + } + if (client->channel_cb) { + free(client->channel_cb); + } + pthread_mutex_destroy(&client->io_lock); + pthread_mutex_destroy(&client->ref_lock); + free(client); + } +} + +/* Send formatted string to client */ +int client_printf(client_t *client, const char *fmt, ...) { + char buffer[2048]; + va_list args; + va_start(args, fmt); + int len = vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + + /* Check for buffer overflow or encoding error */ + if (len < 0 || len >= (int)sizeof(buffer)) { + return -1; + } + + return client_send(client, buffer, len); +} + +static int client_channel_window_change(ssh_session session, ssh_channel channel, + int width, int height, + int pxwidth, int pxheight, + void *userdata) { + (void)session; + (void)channel; + (void)pxwidth; + (void)pxheight; + + client_t *client = (client_t *)userdata; + if (!client) { + return SSH_ERROR; + } + + int w = width; + int h = height; + sanitize_terminal_size(&w, &h); + client->width = w; + client->height = h; + client->redraw_pending = true; + return SSH_OK; +} + +static void client_channel_eof(ssh_session session, ssh_channel channel, + void *userdata) { + (void)session; + (void)channel; + + client_t *client = (client_t *)userdata; + if (client) { + client->connected = false; + } +} + +static void client_channel_close(ssh_session session, ssh_channel channel, + void *userdata) { + (void)session; + (void)channel; + + client_t *client = (client_t *)userdata; + if (client) { + client->connected = false; + } +} + +int client_install_channel_callbacks(client_t *client) { + if (!client || !client->channel) { + return -1; + } + + client->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct)); + if (!client->channel_cb) { + return -1; + } + + ssh_callbacks_init(client->channel_cb); + client->channel_cb->userdata = client; + client->channel_cb->channel_eof_function = client_channel_eof; + client->channel_cb->channel_close_function = client_channel_close; + client->channel_cb->channel_pty_window_change_function = + client_channel_window_change; + + if (ssh_set_channel_callbacks(client->channel, client->channel_cb) != SSH_OK) { + free(client->channel_cb); + client->channel_cb = NULL; + return -1; + } + + return 0; +} diff --git a/src/commands.c b/src/commands.c index 1bd08d0..86d32e0 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1,5 +1,6 @@ #include "commands.h" #include "chat_room.h" +#include "client.h" #include "common.h" #include "message.h" #include "tui.h" diff --git a/src/exec.c b/src/exec.c index c2a0074..fae6ba6 100644 --- a/src/exec.c +++ b/src/exec.c @@ -1,5 +1,6 @@ #include "exec.h" #include "chat_room.h" +#include "client.h" #include "common.h" #include "input.h" #include "message.h" diff --git a/src/input.c b/src/input.c index a185b07..c9742f0 100644 --- a/src/input.c +++ b/src/input.c @@ -1,5 +1,6 @@ #include "input.h" #include "chat_room.h" +#include "client.h" #include "commands.h" #include "common.h" #include "exec.h" diff --git a/src/ssh_server.c b/src/ssh_server.c index 6f4617d..1559988 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -122,159 +122,6 @@ static int setup_host_key(ssh_bind sshbind) { return 0; } -/* Send data to client via SSH channel */ -int client_send(client_t *client, const char *data, size_t len) { - size_t total = 0; - - if (!client || !data) return -1; - - pthread_mutex_lock(&client->io_lock); - - if (!client->connected || !client->channel) { - pthread_mutex_unlock(&client->io_lock); - return -1; - } - - while (total < len) { - size_t remaining = len - total; - uint32_t chunk = (remaining > 32768) ? 32768 : (uint32_t)remaining; - int sent = ssh_channel_write(client->channel, data + total, chunk); - if (sent <= 0) { - pthread_mutex_unlock(&client->io_lock); - return -1; - } - total += (size_t)sent; - } - - pthread_mutex_unlock(&client->io_lock); - return 0; -} - -void client_addref(client_t *client) { - if (!client) return; - pthread_mutex_lock(&client->ref_lock); - client->ref_count++; - pthread_mutex_unlock(&client->ref_lock); -} - -void client_release(client_t *client) { - if (!client) return; - - pthread_mutex_lock(&client->ref_lock); - client->ref_count--; - int count = client->ref_count; - pthread_mutex_unlock(&client->ref_lock); - - if (count == 0) { - /* Safe to free now */ - if (client->channel && client->channel_cb) { - ssh_remove_channel_callbacks(client->channel, client->channel_cb); - } - if (client->channel) { - ssh_channel_close(client->channel); - ssh_channel_free(client->channel); - } - if (client->session) { - ssh_disconnect(client->session); - ssh_free(client->session); - } - if (client->channel_cb) { - free(client->channel_cb); - } - pthread_mutex_destroy(&client->io_lock); - pthread_mutex_destroy(&client->ref_lock); - free(client); - } -} - -/* Send formatted string to client */ -int client_printf(client_t *client, const char *fmt, ...) { - char buffer[2048]; - va_list args; - va_start(args, fmt); - int len = vsnprintf(buffer, sizeof(buffer), fmt, args); - va_end(args); - - /* Check for buffer overflow or encoding error */ - if (len < 0 || len >= (int)sizeof(buffer)) { - return -1; - } - - return client_send(client, buffer, len); -} - -static int client_channel_window_change(ssh_session session, ssh_channel channel, - int width, int height, - int pxwidth, int pxheight, - void *userdata) { - (void)session; - (void)channel; - (void)pxwidth; - (void)pxheight; - - client_t *client = (client_t *)userdata; - if (!client) { - return SSH_ERROR; - } - - int w = width; - int h = height; - sanitize_terminal_size(&w, &h); - client->width = w; - client->height = h; - client->redraw_pending = true; - return SSH_OK; -} - -static void client_channel_eof(ssh_session session, ssh_channel channel, - void *userdata) { - (void)session; - (void)channel; - - client_t *client = (client_t *)userdata; - if (client) { - client->connected = false; - } -} - -static void client_channel_close(ssh_session session, ssh_channel channel, - void *userdata) { - (void)session; - (void)channel; - - client_t *client = (client_t *)userdata; - if (client) { - client->connected = false; - } -} - -int client_install_channel_callbacks(client_t *client) { - if (!client || !client->channel) { - return -1; - } - - client->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct)); - if (!client->channel_cb) { - return -1; - } - - ssh_callbacks_init(client->channel_cb); - client->channel_cb->userdata = client; - client->channel_cb->channel_eof_function = client_channel_eof; - client->channel_cb->channel_close_function = client_channel_close; - client->channel_cb->channel_pty_window_change_function = - client_channel_window_change; - - if (ssh_set_channel_callbacks(client->channel, client->channel_cb) != SSH_OK) { - free(client->channel_cb); - client->channel_cb = NULL; - return -1; - } - - return 0; -} - - /* Initialize SSH server */ int ssh_server_init(int port) { /* Initialize rate-limit / connection-count subsystem */ diff --git a/src/tui.c b/src/tui.c index c26e3fc..cb3670e 100644 --- a/src/tui.c +++ b/src/tui.c @@ -1,4 +1,5 @@ #include "tui.h" +#include "client.h" #include "ssh_server.h" #include "chat_room.h" #include "utf8.h"