mirror of
https://github.com/m1ngsama/TNT.git
synced 2025-12-24 10:51:41 +00:00
Replace telnet with SSH and fix full-screen display
- Implement SSH server using libssh for secure connections - Replace insecure telnet with encrypted SSH protocol - Add automatic terminal size detection via PTY requests - Support dynamic window resize (SIGWINCH handling) - Fix UI display bug by using SSH channel instead of fd - Update tui_clear_screen to work with SSH connections - Add RSA host key auto-generation on first run - Update README with SSH instructions and security notes - Add libssh dependency to Makefile with auto-detection - Remove all telnet-related code Security improvements: - All traffic now encrypted - Host key authentication - No more plaintext transmission
This commit is contained in:
parent
82cfb5795b
commit
a4d67be103
6 changed files with 311 additions and 66 deletions
11
Makefile
11
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
|
||||
|
|
|
|||
40
README.md
40
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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@
|
|||
|
||||
#include "common.h"
|
||||
#include "chat_room.h"
|
||||
#include <libssh/libssh.h>
|
||||
#include <libssh/server.h>
|
||||
|
||||
/* 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
301
src/ssh_server.c
301
src/ssh_server.c
|
|
@ -1,6 +1,9 @@
|
|||
#include "ssh_server.h"
|
||||
#include "tui.h"
|
||||
#include "utf8.h"
|
||||
#include <libssh/libssh.h>
|
||||
#include <libssh/server.h>
|
||||
#include <libssh/callbacks.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
|
@ -9,11 +12,59 @@
|
|||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* 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 */
|
||||
/* Handle SSH authentication */
|
||||
static int handle_auth(ssh_session session) {
|
||||
ssh_message message;
|
||||
|
||||
do {
|
||||
message = ssh_message_get(session);
|
||||
if (!message) break;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ssh_message_reply_default(message);
|
||||
ssh_message_free(message);
|
||||
} while (1);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (listen_fd < 0) {
|
||||
perror("socket");
|
||||
g_sshbind = ssh_bind_new();
|
||||
if (!g_sshbind) {
|
||||
fprintf(stderr, "Failed to create SSH bind\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Set socket options */
|
||||
int opt = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind");
|
||||
close(listen_fd);
|
||||
/* Set up host key */
|
||||
if (setup_host_key(g_sshbind) < 0) {
|
||||
ssh_bind_free(g_sshbind);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(listen_fd, 10) < 0) {
|
||||
perror("listen");
|
||||
close(listen_fd);
|
||||
/* 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 listen_fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@
|
|||
#include <unistd.h>
|
||||
|
||||
/* 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 */
|
||||
|
|
|
|||
Loading…
Reference in a new issue