diff --git a/Makefile b/Makefile index 168c947..e4595d9 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,18 @@ CC = gcc CFLAGS = -Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700 -LDFLAGS = -pthread +LDFLAGS = -pthread -lssh INCLUDES = -Iinclude +# Detect libssh location (homebrew on macOS) +ifeq ($(shell uname), Darwin) + LIBSSH_PREFIX := $(shell brew --prefix libssh 2>/dev/null) + ifneq ($(LIBSSH_PREFIX),) + INCLUDES += -I$(LIBSSH_PREFIX)/include + LDFLAGS += -L$(LIBSSH_PREFIX)/lib + endif +endif + SRC_DIR = src INC_DIR = include OBJ_DIR = obj diff --git a/README.md b/README.md index 56eca30..6a897ed 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,30 @@ - 🕐 **Full timestamps** - Year-month-day hour:minute with timezone - 📖 **Bilingual help** - Press ? for Chinese/English help - 🌏 **UTF-8 support** - Full support for Chinese, Japanese, Korean -- 📦 **Single binary** - Lightweight ~50KB executable -- 🚀 **Telnet access** - No client installation needed +- 📦 **Single binary** - Lightweight executable +- 🔒 **SSH access** - Secure encrypted connections +- 🖥️ **Auto terminal detection** - Adapts to your terminal size - 💾 **Message persistence** - All messages saved to log file - ⚡ **Low resource usage** - Minimal memory and CPU ## Building +**Dependencies:** +- libssh (required for SSH support) + +**Install dependencies:** +```bash +# On macOS +brew install libssh + +# On Ubuntu/Debian +sudo apt-get install libssh-dev + +# On Fedora/RHEL +sudo dnf install libssh-devel +``` + +**Build:** ```bash make ``` @@ -36,9 +53,11 @@ make debug Connect from another terminal: ```bash -telnet localhost 2222 +ssh -p 2222 localhost ``` +The server will prompt for a password (any password is accepted) and then ask for your username. + ## Usage ### Operating Modes @@ -78,10 +97,11 @@ telnet localhost 2222 ## Architecture -- **Network**: Multi-threaded TCP server -- **TUI**: ANSI escape sequences +- **Network**: Multi-threaded SSH server using libssh +- **TUI**: ANSI escape sequences with automatic terminal size detection - **Storage**: Append-only log file - **Concurrency**: pthread + rwlock +- **Security**: Encrypted SSH connections with host key authentication ## Configuration @@ -98,6 +118,16 @@ PORT=3333 ./tnt - Thread-safe operations - Proper UTF-8 handling for CJK characters - Box-drawing characters for UI +- SSH protocol with PTY support for terminal size detection +- Dynamic window resize handling + +## Security + +- **Encrypted connections**: All traffic is encrypted via SSH +- **Host key authentication**: RSA host key generated on first run +- **Password authentication**: Currently accepts any password (customize for production) +- **Host key persistence**: Stored in `host_key` file +- **No plaintext**: Unlike telnet, all data is encrypted in transit ## License diff --git a/include/ssh_server.h b/include/ssh_server.h index a393a8c..ed5092c 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -3,10 +3,14 @@ #include "common.h" #include "chat_room.h" +#include +#include /* Client connection structure */ typedef struct client { - int fd; /* Socket file descriptor */ + int fd; /* Socket file descriptor (not used with SSH) */ + ssh_session session; /* SSH session */ + ssh_channel channel; /* SSH channel */ char username[MAX_USERNAME_LEN]; int width; int height; diff --git a/include/tui.h b/include/tui.h index 4e34cbd..9dcaf5d 100644 --- a/include/tui.h +++ b/include/tui.h @@ -20,7 +20,7 @@ void tui_render_command_output(struct client *client); void tui_render_input(struct client *client, const char *input); /* Clear the screen */ -void tui_clear_screen(int fd); +void tui_clear_screen(struct client *client); /* Get help text based on language */ const char* tui_get_help_text(help_lang_t lang); diff --git a/src/ssh_server.c b/src/ssh_server.c index 9a48a8c..4549f9f 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -1,6 +1,9 @@ #include "ssh_server.h" #include "tui.h" #include "utf8.h" +#include +#include +#include #include #include #include @@ -9,11 +12,59 @@ #include #include #include +#include -/* Send data to client */ +/* Global SSH bind instance */ +static ssh_bind g_sshbind = NULL; + +/* Generate or load SSH host key */ +static int setup_host_key(ssh_bind sshbind) { + struct stat st; + + /* Check if host key exists */ + if (stat(HOST_KEY_FILE, &st) == 0) { + /* Load existing key */ + if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, HOST_KEY_FILE) < 0) { + fprintf(stderr, "Failed to load host key: %s\n", ssh_get_error(sshbind)); + return -1; + } + return 0; + } + + /* Generate new key */ + printf("Generating new RSA host key...\n"); + ssh_key key; + if (ssh_pki_generate(SSH_KEYTYPE_RSA, 2048, &key) < 0) { + fprintf(stderr, "Failed to generate RSA key\n"); + return -1; + } + + /* Export key to file */ + if (ssh_pki_export_privkey_file(key, NULL, NULL, NULL, HOST_KEY_FILE) < 0) { + fprintf(stderr, "Failed to export host key\n"); + ssh_key_free(key); + return -1; + } + + ssh_key_free(key); + + /* Set restrictive permissions */ + chmod(HOST_KEY_FILE, 0600); + + /* Load the newly created key */ + if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, HOST_KEY_FILE) < 0) { + fprintf(stderr, "Failed to load host key: %s\n", ssh_get_error(sshbind)); + return -1; + } + + return 0; +} + +/* Send data to client via SSH channel */ int client_send(client_t *client, const char *data, size_t len) { - if (!client || !client->connected) return -1; - ssize_t sent = write(client->fd, data, len); + if (!client || !client->connected || !client->channel) return -1; + + int sent = ssh_channel_write(client->channel, data, len); return (sent < 0) ? -1 : 0; } @@ -31,13 +82,13 @@ int client_printf(client_t *client, const char *fmt, ...) { static int read_username(client_t *client) { char username[MAX_USERNAME_LEN] = {0}; int pos = 0; - unsigned char buf[4]; + char buf[4]; - tui_clear_screen(client->fd); + tui_clear_screen(client); client_printf(client, "请输入用户名: "); while (1) { - ssize_t n = read(client->fd, buf, 1); + int n = ssh_channel_read(client->channel, buf, 1, 0); if (n <= 0) return -1; unsigned char b = buf[0]; @@ -57,20 +108,20 @@ static int read_username(client_t *client) { if (pos < MAX_USERNAME_LEN - 1) { username[pos++] = b; username[pos] = '\0'; - write(client->fd, &b, 1); + client_send(client, &b, 1); } } else { /* UTF-8 multi-byte */ int len = utf8_byte_length(b); buf[0] = b; if (len > 1) { - read(client->fd, &buf[1], len - 1); + ssh_channel_read(client->channel, &buf[1], len - 1, 0); } if (pos + len < MAX_USERNAME_LEN - 1) { memcpy(username + pos, buf, len); pos += len; username[pos] = '\0'; - write(client->fd, buf, len); + client_send(client, buf, len); } } } @@ -286,11 +337,9 @@ static void handle_key(client_t *client, unsigned char key, char *input) { void* client_handle_session(void *arg) { client_t *client = (client_t*)arg; char input[MAX_MESSAGE_LEN] = {0}; - unsigned char buf[4]; + char buf[4]; - /* Get terminal size (assume 80x24 for telnet) */ - client->width = 80; - client->height = 24; + /* Terminal size already set from PTY request */ client->mode = MODE_INSERT; client->help_lang = LANG_ZH; client->connected = true; @@ -318,8 +367,8 @@ void* client_handle_session(void *arg) { tui_render_screen(client); /* Main input loop */ - while (client->connected) { - ssize_t n = read(client->fd, buf, 1); + while (client->connected && ssh_channel_is_open(client->channel)) { + int n = ssh_channel_read(client->channel, buf, 1, 0); if (n <= 0) break; unsigned char b = buf[0]; @@ -344,7 +393,7 @@ void* client_handle_session(void *arg) { int char_len = utf8_byte_length(b); buf[0] = b; if (char_len > 1) { - read(client->fd, &buf[1], char_len - 1); + ssh_channel_read(client->channel, &buf[1], char_len - 1, 0); } int len = strlen(input); if (len + char_len < MAX_MESSAGE_LEN - 1) { @@ -380,73 +429,225 @@ cleanup: room_broadcast(g_room, &leave_msg); } - close(client->fd); + if (client->channel) { + ssh_channel_close(client->channel); + ssh_channel_free(client->channel); + } + if (client->session) { + ssh_disconnect(client->session); + ssh_free(client->session); + } free(client); return NULL; } -/* Initialize server socket */ -int ssh_server_init(int port) { - int listen_fd = socket(AF_INET, SOCK_STREAM, 0); - if (listen_fd < 0) { - perror("socket"); - return -1; - } +/* Handle SSH authentication */ +static int handle_auth(ssh_session session) { + ssh_message message; - /* Set socket options */ - int opt = 1; - setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + do { + message = ssh_message_get(session); + if (!message) break; - struct sockaddr_in addr = {0}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(port); + if (ssh_message_type(message) == SSH_REQUEST_AUTH) { + if (ssh_message_subtype(message) == SSH_AUTH_METHOD_PASSWORD) { + /* Accept any password for simplicity */ + /* In production, you'd want to verify against a user database */ + ssh_message_auth_reply_success(message, 0); + ssh_message_free(message); + return 0; + } else if (ssh_message_subtype(message) == SSH_AUTH_METHOD_NONE) { + /* Deny and ask for password */ + ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_PASSWORD); + } + } - if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - perror("bind"); - close(listen_fd); - return -1; - } + ssh_message_reply_default(message); + ssh_message_free(message); + } while (1); - if (listen(listen_fd, 10) < 0) { - perror("listen"); - close(listen_fd); - return -1; - } - - return listen_fd; + return -1; } -/* Start server (blocking) */ -int ssh_server_start(int listen_fd) { - printf("TNT chat server listening on port %d\n", DEFAULT_PORT); - printf("Connect with: telnet localhost %d\n", DEFAULT_PORT); +/* Handle SSH channel requests */ +static ssh_channel handle_channel_open(ssh_session session) { + ssh_message message; + ssh_channel channel = NULL; + + do { + message = ssh_message_get(session); + if (!message) break; + + if (ssh_message_type(message) == SSH_REQUEST_CHANNEL_OPEN && + ssh_message_subtype(message) == SSH_CHANNEL_SESSION) { + channel = ssh_message_channel_request_open_reply_accept(message); + ssh_message_free(message); + return channel; + } + + ssh_message_reply_default(message); + ssh_message_free(message); + } while (1); + + return NULL; +} + +/* Handle PTY request and get terminal size */ +static int handle_pty_request(ssh_channel channel, client_t *client) { + ssh_message message; + + do { + message = ssh_message_get(ssh_channel_get_session(channel)); + if (!message) break; + + if (ssh_message_type(message) == SSH_REQUEST_CHANNEL) { + if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_PTY) { + /* Get terminal dimensions from PTY request */ + client->width = ssh_message_channel_request_pty_width(message); + client->height = ssh_message_channel_request_pty_height(message); + + /* Default to 80x24 if invalid */ + if (client->width <= 0 || client->width > 500) client->width = 80; + if (client->height <= 0 || client->height > 200) client->height = 24; + + ssh_message_channel_request_reply_success(message); + ssh_message_free(message); + return 0; + } else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_SHELL) { + ssh_message_channel_request_reply_success(message); + ssh_message_free(message); + return 0; + } else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_WINDOW_CHANGE) { + /* Handle terminal resize */ + client->width = ssh_message_channel_request_pty_width(message); + client->height = ssh_message_channel_request_pty_height(message); + + if (client->width <= 0 || client->width > 500) client->width = 80; + if (client->height <= 0 || client->height > 200) client->height = 24; + + /* Re-render screen with new dimensions */ + if (client->connected) { + tui_render_screen(client); + } + + ssh_message_free(message); + continue; + } + } + + ssh_message_reply_default(message); + ssh_message_free(message); + } while (1); + + return -1; +} + +/* Initialize SSH server */ +int ssh_server_init(int port) { + g_sshbind = ssh_bind_new(); + if (!g_sshbind) { + fprintf(stderr, "Failed to create SSH bind\n"); + return -1; + } + + /* Set up host key */ + if (setup_host_key(g_sshbind) < 0) { + ssh_bind_free(g_sshbind); + return -1; + } + + /* Bind to port */ + ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDPORT, &port); + ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDADDR, "0.0.0.0"); + + /* Set verbose level for debugging */ + int verbosity = SSH_LOG_WARNING; + ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &verbosity); + + if (ssh_bind_listen(g_sshbind) < 0) { + fprintf(stderr, "Failed to bind to port %d: %s\n", port, ssh_get_error(g_sshbind)); + ssh_bind_free(g_sshbind); + return -1; + } + + return 0; +} + +/* Start SSH server (blocking) */ +int ssh_server_start(int unused) { + (void)unused; + + printf("TNT chat server listening on port %d (SSH)\n", DEFAULT_PORT); + printf("Connect with: ssh -p %d localhost\n", DEFAULT_PORT); while (1) { - struct sockaddr_in client_addr; - socklen_t client_len = sizeof(client_addr); + ssh_session session = ssh_new(); + if (!session) { + fprintf(stderr, "Failed to create SSH session\n"); + continue; + } - int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); - if (client_fd < 0) { - if (errno == EINTR) continue; - perror("accept"); + /* Accept connection */ + if (ssh_bind_accept(g_sshbind, session) != SSH_OK) { + fprintf(stderr, "Error accepting connection: %s\n", ssh_get_error(g_sshbind)); + ssh_free(session); + continue; + } + + /* Perform key exchange */ + if (ssh_handle_key_exchange(session) != SSH_OK) { + fprintf(stderr, "Key exchange failed: %s\n", ssh_get_error(session)); + ssh_disconnect(session); + ssh_free(session); + continue; + } + + /* Handle authentication */ + if (handle_auth(session) < 0) { + fprintf(stderr, "Authentication failed\n"); + ssh_disconnect(session); + ssh_free(session); + continue; + } + + /* Open channel */ + ssh_channel channel = handle_channel_open(session); + if (!channel) { + fprintf(stderr, "Failed to open channel\n"); + ssh_disconnect(session); + ssh_free(session); continue; } /* Create client structure */ client_t *client = calloc(1, sizeof(client_t)); if (!client) { - close(client_fd); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); continue; } - client->fd = client_fd; + client->session = session; + client->channel = channel; + client->fd = -1; /* Not used with SSH */ + + /* Handle PTY request and get terminal size */ + if (handle_pty_request(channel, client) < 0) { + /* Set defaults if PTY request fails */ + client->width = 80; + client->height = 24; + } /* Create thread for client */ pthread_t thread; if (pthread_create(&thread, NULL, client_handle_session, client) != 0) { - close(client_fd); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); free(client); continue; } diff --git a/src/tui.c b/src/tui.c index 784a36d..ea5431b 100644 --- a/src/tui.c +++ b/src/tui.c @@ -6,9 +6,10 @@ #include /* Clear the screen */ -void tui_clear_screen(int fd) { +void tui_clear_screen(client_t *client) { + if (!client || !client->connected) return; const char *clear = ANSI_CLEAR ANSI_HOME; - write(fd, clear, strlen(clear)); + client_send(client, clear, strlen(clear)); } /* Render the main screen */