feat: initial x3-ssh client

- GTK3 GUI with VTE terminal emulation
- Tabbed SSH sessions
- Server management (add/edit/remove)
- SSH key management (generate/import/delete)
- Quick connect bar
- libssh for SSH connections
- autotools build system (configure && make)
This commit is contained in:
x3
2026-09-07 14:50:20 +00:00
commit 77f9568cb1
15 changed files with 1476 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
#include "ssh_terminal.h"
#include <unistd.h>
#include <pty.h>
#include <sys/select.h>
#include <signal.h>
#include <cstring>
SshTerminal::SshTerminal()
: terminal_(nullptr), vte_box_(nullptr), session_(nullptr), channel_(nullptr),
pty_master_(-1), pty_slave_(-1), running_(false), connected_(false), thread_(nullptr) {
vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
terminal_ = VTE_TERMINAL(vte_terminal_new());
gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0);
gtk_widget_show_all(vte_box_);
VtePty *pty = vte_terminal_pty_new_sync(terminal_, VTE_PTY_DEFAULT, nullptr, nullptr);
(void)pty;
g_signal_connect(terminal_, "size-changed", G_CALLBACK(on_size_changed), this);
}
SshTerminal::~SshTerminal() {
disconnect();
if (vte_box_) gtk_widget_destroy(vte_box_);
}
void SshTerminal::on_size_changed(VteTerminal* term, guint cols, guint rows, gpointer data) {
(void)term;
auto* self = static_cast<SshTerminal*>(data);
if (self->channel_ && self->connected_) {
ssh_channel_change_pty_size(self->channel_, cols, rows);
}
}
void SshTerminal::cleanup() {
if (channel_) {
ssh_channel_close(channel_);
ssh_channel_free(channel_);
channel_ = nullptr;
}
if (session_) {
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
}
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
connected_ = false;
}
gpointer SshTerminal::io_thread_func(gpointer data) {
auto* self = static_cast<SshTerminal*>(data);
int ssh_fd = ssh_get_fd(self->session_);
if (ssh_fd < 0) {
self->cleanup();
g_idle_add([](gpointer d) -> gboolean {
auto* s = static_cast<SshTerminal*>(d);
if (s->on_status_change) s->on_status_change(false, "SSH get_fd failed");
return G_SOURCE_REMOVE;
}, self);
return nullptr;
}
char buf[4096];
fd_set rfds;
struct timeval tv;
while (self->running_) {
FD_ZERO(&rfds);
FD_SET(self->pty_master_, &rfds);
FD_SET(ssh_fd, &rfds);
int maxfd = (self->pty_master_ > ssh_fd ? self->pty_master_ : ssh_fd) + 1;
tv.tv_sec = 0;
tv.tv_usec = 50000;
int ret = select(maxfd, &rfds, nullptr, nullptr, &tv);
if (ret < 0) break;
if (ret == 0) continue;
if (FD_ISSET(self->pty_master_, &rfds)) {
ssize_t n = read(self->pty_master_, buf, sizeof(buf));
if (n > 0) {
ssh_channel_write(self->channel_, buf, n);
} else if (n <= 0) {
break;
}
}
if (FD_ISSET(ssh_fd, &rfds)) {
if (ssh_channel_poll_timeout(self->channel_, 0, 0) > 0) {
ssize_t n = ssh_channel_read_timeout(self->channel_, buf, sizeof(buf), 0, 0);
if (n > 0) {
write(self->pty_master_, buf, n);
} else if (n == SSH_EOF || n == SSH_ERROR) {
break;
}
} else if (ssh_channel_poll_timeout(self->channel_, 0, 0) < 0) {
break;
}
}
if (ssh_channel_is_eof(self->channel_)) break;
}
self->connected_ = false;
g_idle_add([](gpointer d) -> gboolean {
auto* s = static_cast<SshTerminal*>(d);
if (s->on_status_change) s->on_status_change(false, "Disconnected");
return G_SOURCE_REMOVE;
}, self);
return nullptr;
}
bool SshTerminal::connect(const std::string& host, int port, const std::string& user,
const std::string& keyfile, const std::string& password) {
if (connected_) disconnect();
session_ = ssh_new();
if (!session_) return false;
ssh_options_set(session_, SSH_OPTIONS_HOST, host.c_str());
unsigned int p = port;
ssh_options_set(session_, SSH_OPTIONS_PORT, &p);
ssh_options_set(session_, SSH_OPTIONS_USER, user.c_str());
int strict = 0;
ssh_options_set(session_, SSH_OPTIONS_STRICTHOSTKEYCHECK, &strict);
if (ssh_connect(session_) != SSH_OK) {
std::string err = ssh_get_error(session_);
cleanup();
if (on_status_change) on_status_change(false, "Connection failed: " + err);
return false;
}
ssh_session_update_known_hosts(session_);
if (!keyfile.empty()) {
ssh_key key = nullptr;
if (ssh_pki_import_privkey_file(keyfile.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) {
if (ssh_userauth_publickey(session_, nullptr, key) == SSH_AUTH_SUCCESS) {
ssh_key_free(key);
goto auth_done;
}
ssh_key_free(key);
}
}
{
int rc = ssh_userauth_password(session_, nullptr, password.c_str());
if (rc != SSH_AUTH_SUCCESS) {
std::string err = ssh_get_error(session_);
cleanup();
if (on_status_change) on_status_change(false, "Auth failed: " + err);
return false;
}
}
auth_done:
channel_ = ssh_channel_new(session_);
if (!channel_) {
cleanup();
if (on_status_change) on_status_change(false, "Channel creation failed");
return false;
}
if (ssh_channel_open_session(channel_) != SSH_OK) {
cleanup();
if (on_status_change) on_status_change(false, "Session open failed");
return false;
}
guint cols = vte_terminal_get_column_count(terminal_);
guint rows = vte_terminal_get_row_count(terminal_);
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
cleanup();
if (on_status_change) on_status_change(false, "PTY request failed");
return false;
}
if (ssh_channel_request_shell(channel_) != SSH_OK) {
cleanup();
if (on_status_change) on_status_change(false, "Shell request failed");
return false;
}
pty_master_ = openpty(&pty_slave_, nullptr, nullptr, nullptr, nullptr);
if (pty_master_ < 0) {
cleanup();
if (on_status_change) on_status_change(false, "PTY creation failed");
return false;
}
VtePty *pty = vte_terminal_get_pty(terminal_);
if (pty) {
int vte_fd = vte_pty_get_fd(pty);
if (vte_fd >= 0) close(vte_fd);
}
vte_terminal_set_pty(terminal_, nullptr);
VtePty *new_pty = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
if (new_pty) {
vte_terminal_set_pty(terminal_, new_pty);
}
connected_ = true;
running_ = true;
thread_ = g_thread_new("ssh_io", io_thread_func, this);
if (on_status_change) on_status_change(true, "Connected to " + user + "@" + host);
return true;
}
void SshTerminal::disconnect() {
running_ = false;
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
if (thread_) {
g_thread_join(thread_);
thread_ = nullptr;
}
cleanup();
}