feat: rewrite terminal to use vte_terminal_spawn_sync

Replaced the SSH library + IO thread + intermediate PTY bridge with
vte_terminal_spawn_sync(). This eliminates the broken terminal behavior
(backspace, Ctrl+C, copy/paste, bash completion) caused by the
intermediate PTY with no process on the slave side.

- SshTerminal now spawns 'ssh' as a VTE subprocess
- Removed libssh dependency from terminal (still used for key validation)
- Removed IO thread, manual PTY creation, and thread synchronization
- VTE now handles all terminal I/O, line discipline, resize, etc.
- Password saved: uses sshpass -e with SSHPASS env var if available
- Password not saved: ssh prompts interactively in the terminal
- Removed show_password_dialog (no longer needed)
- Simplified connect flow in ssh_client.cpp
This commit is contained in:
x3
2026-09-07 16:54:21 +00:00
parent 69369aedb8
commit 916a167f67
12 changed files with 142 additions and 576 deletions
-2
View File
@@ -267,8 +267,6 @@ INSTALL_STRIP_PROGRAM = $(install_sh) -c -s
LDFLAGS = LDFLAGS =
LIBOBJS = LIBOBJS =
LIBS = LIBS =
LIBSSH_CFLAGS =
LIBSSH_LIBS = -lssh
LTLIBOBJS = LTLIBOBJS =
MAKEINFO = ${SHELL} '/home/administrator/Programms/ssh/x3-ssh/build-aux/missing' makeinfo MAKEINFO = ${SHELL} '/home/administrator/Programms/ssh/x3-ssh/build-aux/missing' makeinfo
MKDIR_P = /usr/bin/mkdir -p MKDIR_P = /usr/bin/mkdir -p
-2
View File
@@ -267,8 +267,6 @@ INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
LDFLAGS = @LDFLAGS@ LDFLAGS = @LDFLAGS@
LIBOBJS = @LIBOBJS@ LIBOBJS = @LIBOBJS@
LIBS = @LIBS@ LIBS = @LIBS@
LIBSSH_CFLAGS = @LIBSSH_CFLAGS@
LIBSSH_LIBS = @LIBSSH_LIBS@
LTLIBOBJS = @LTLIBOBJS@ LTLIBOBJS = @LTLIBOBJS@
MAKEINFO = @MAKEINFO@ MAKEINFO = @MAKEINFO@
MKDIR_P = @MKDIR_P@ MKDIR_P = @MKDIR_P@
Vendored
+1 -99
View File
@@ -613,8 +613,6 @@ ac_subst_vars='am__EXEEXT_FALSE
am__EXEEXT_TRUE am__EXEEXT_TRUE
LTLIBOBJS LTLIBOBJS
LIBOBJS LIBOBJS
LIBSSH_LIBS
LIBSSH_CFLAGS
VTE_LIBS VTE_LIBS
VTE_CFLAGS VTE_CFLAGS
GTK_LIBS GTK_LIBS
@@ -732,9 +730,7 @@ PKG_CONFIG_LIBDIR
GTK_CFLAGS GTK_CFLAGS
GTK_LIBS GTK_LIBS
VTE_CFLAGS VTE_CFLAGS
VTE_LIBS VTE_LIBS'
LIBSSH_CFLAGS
LIBSSH_LIBS'
# Initialize some variables set by options. # Initialize some variables set by options.
@@ -1382,9 +1378,6 @@ Some influential environment variables:
GTK_LIBS linker flags for GTK, overriding pkg-config GTK_LIBS linker flags for GTK, overriding pkg-config
VTE_CFLAGS C compiler flags for VTE, overriding pkg-config VTE_CFLAGS C compiler flags for VTE, overriding pkg-config
VTE_LIBS linker flags for VTE, overriding pkg-config VTE_LIBS linker flags for VTE, overriding pkg-config
LIBSSH_CFLAGS
C compiler flags for LIBSSH, overriding pkg-config
LIBSSH_LIBS linker flags for LIBSSH, overriding pkg-config
Use these variables to override the choices made by 'configure' or to help Use these variables to override the choices made by 'configure' or to help
it to find libraries and programs with nonstandard names/locations. it to find libraries and programs with nonstandard names/locations.
@@ -5120,97 +5113,6 @@ printf "%s\n" "yes" >&6; }
fi fi
pkg_failed=no
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libssh >= 0.9" >&5
printf %s "checking for libssh >= 0.9... " >&6; }
if test -n "$LIBSSH_CFLAGS"; then
pkg_cv_LIBSSH_CFLAGS="$LIBSSH_CFLAGS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libssh >= 0.9\""; } >&5
($PKG_CONFIG --exists --print-errors "libssh >= 0.9") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_LIBSSH_CFLAGS=`$PKG_CONFIG --cflags "libssh >= 0.9" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test -n "$LIBSSH_LIBS"; then
pkg_cv_LIBSSH_LIBS="$LIBSSH_LIBS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libssh >= 0.9\""; } >&5
($PKG_CONFIG --exists --print-errors "libssh >= 0.9") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_LIBSSH_LIBS=`$PKG_CONFIG --libs "libssh >= 0.9" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test $pkg_failed = yes; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
_pkg_short_errors_supported=yes
else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
LIBSSH_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libssh >= 0.9" 2>&1`
else
LIBSSH_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libssh >= 0.9" 2>&1`
fi
# Put the nasty error message in config.log where it belongs
echo "$LIBSSH_PKG_ERRORS" >&5
as_fn_error $? "Package requirements (libssh >= 0.9) were not met:
$LIBSSH_PKG_ERRORS
Consider adjusting the PKG_CONFIG_PATH environment variable if you
installed software in a non-standard prefix.
Alternatively, you may set the environment variables LIBSSH_CFLAGS
and LIBSSH_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details." "$LINENO" 5
elif test $pkg_failed = untried; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5
printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;}
as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
is in your PATH or set the PKG_CONFIG environment variable to the full
path to pkg-config.
Alternatively, you may set the environment variables LIBSSH_CFLAGS
and LIBSSH_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details.
To get pkg-config, see <http://pkg-config.freedesktop.org/>.
See 'config.log' for more details" "$LINENO" 5; }
else
LIBSSH_CFLAGS=$pkg_cv_LIBSSH_CFLAGS
LIBSSH_LIBS=$pkg_cv_LIBSSH_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
fi
ac_config_files="$ac_config_files Makefile src/Makefile" ac_config_files="$ac_config_files Makefile src/Makefile"
cat >confcache <<\_ACEOF cat >confcache <<\_ACEOF
-1
View File
@@ -8,7 +8,6 @@ AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.24]) PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.24])
PKG_CHECK_MODULES([VTE], [vte-2.91 >= 0.50]) PKG_CHECK_MODULES([VTE], [vte-2.91 >= 0.50])
PKG_CHECK_MODULES([LIBSSH], [libssh >= 0.9])
AC_CONFIG_FILES([Makefile src/Makefile]) AC_CONFIG_FILES([Makefile src/Makefile])
AC_OUTPUT AC_OUTPUT
+2 -7
View File
@@ -107,8 +107,7 @@ am_x3_ssh_OBJECTS = x3_ssh-main.$(OBJEXT) \
x3_ssh-ssh_client.$(OBJEXT) x3_ssh-key_store.$(OBJEXT) x3_ssh-ssh_client.$(OBJEXT) x3_ssh-key_store.$(OBJEXT)
x3_ssh_OBJECTS = $(am_x3_ssh_OBJECTS) x3_ssh_OBJECTS = $(am_x3_ssh_OBJECTS)
am__DEPENDENCIES_1 = am__DEPENDENCIES_1 =
x3_ssh_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ x3_ssh_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
$(am__DEPENDENCIES_1)
x3_ssh_LINK = $(CXXLD) $(x3_ssh_CXXFLAGS) $(CXXFLAGS) $(AM_LDFLAGS) \ x3_ssh_LINK = $(CXXLD) $(x3_ssh_CXXFLAGS) $(CXXFLAGS) $(AM_LDFLAGS) \
$(LDFLAGS) -o $@ $(LDFLAGS) -o $@
AM_V_P = $(am__v_P_$(V)) AM_V_P = $(am__v_P_$(V))
@@ -234,8 +233,6 @@ INSTALL_STRIP_PROGRAM = $(install_sh) -c -s
LDFLAGS = LDFLAGS =
LIBOBJS = LIBOBJS =
LIBS = LIBS =
LIBSSH_CFLAGS =
LIBSSH_LIBS = -lssh
LTLIBOBJS = LTLIBOBJS =
MAKEINFO = ${SHELL} '/home/administrator/Programms/ssh/x3-ssh/build-aux/missing' makeinfo MAKEINFO = ${SHELL} '/home/administrator/Programms/ssh/x3-ssh/build-aux/missing' makeinfo
MKDIR_P = /usr/bin/mkdir -p MKDIR_P = /usr/bin/mkdir -p
@@ -313,14 +310,12 @@ x3_ssh_CXXFLAGS = \
-std=c++17 \ -std=c++17 \
$(GTK_CFLAGS) \ $(GTK_CFLAGS) \
$(VTE_CFLAGS) \ $(VTE_CFLAGS) \
$(LIBSSH_CFLAGS) \
-DLOCALEDIR=\"$(localedir)\" \ -DLOCALEDIR=\"$(localedir)\" \
-DICONDIR=\"$(datadir)/icons\" -DICONDIR=\"$(datadir)/icons\"
x3_ssh_LDADD = \ x3_ssh_LDADD = \
$(GTK_LIBS) \ $(GTK_LIBS) \
$(VTE_LIBS) \ $(VTE_LIBS)
$(LIBSSH_LIBS)
iconsdir = $(datadir)/icons iconsdir = $(datadir)/icons
dist_icons_DATA = ../icons/x3-ssh.png dist_icons_DATA = ../icons/x3-ssh.png
+1 -3
View File
@@ -11,14 +11,12 @@ x3_ssh_CXXFLAGS = \
-std=c++17 \ -std=c++17 \
$(GTK_CFLAGS) \ $(GTK_CFLAGS) \
$(VTE_CFLAGS) \ $(VTE_CFLAGS) \
$(LIBSSH_CFLAGS) \
-DLOCALEDIR=\"$(localedir)\" \ -DLOCALEDIR=\"$(localedir)\" \
-DICONDIR=\"$(datadir)/icons\" -DICONDIR=\"$(datadir)/icons\"
x3_ssh_LDADD = \ x3_ssh_LDADD = \
$(GTK_LIBS) \ $(GTK_LIBS) \
$(VTE_LIBS) \ $(VTE_LIBS)
$(LIBSSH_LIBS)
iconsdir = $(datadir)/icons iconsdir = $(datadir)/icons
dist_icons_DATA = ../icons/x3-ssh.png dist_icons_DATA = ../icons/x3-ssh.png
+2 -7
View File
@@ -107,8 +107,7 @@ am_x3_ssh_OBJECTS = x3_ssh-main.$(OBJEXT) \
x3_ssh-ssh_client.$(OBJEXT) x3_ssh-key_store.$(OBJEXT) x3_ssh-ssh_client.$(OBJEXT) x3_ssh-key_store.$(OBJEXT)
x3_ssh_OBJECTS = $(am_x3_ssh_OBJECTS) x3_ssh_OBJECTS = $(am_x3_ssh_OBJECTS)
am__DEPENDENCIES_1 = am__DEPENDENCIES_1 =
x3_ssh_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ x3_ssh_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
$(am__DEPENDENCIES_1)
x3_ssh_LINK = $(CXXLD) $(x3_ssh_CXXFLAGS) $(CXXFLAGS) $(AM_LDFLAGS) \ x3_ssh_LINK = $(CXXLD) $(x3_ssh_CXXFLAGS) $(CXXFLAGS) $(AM_LDFLAGS) \
$(LDFLAGS) -o $@ $(LDFLAGS) -o $@
AM_V_P = $(am__v_P_@AM_V@) AM_V_P = $(am__v_P_@AM_V@)
@@ -234,8 +233,6 @@ INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
LDFLAGS = @LDFLAGS@ LDFLAGS = @LDFLAGS@
LIBOBJS = @LIBOBJS@ LIBOBJS = @LIBOBJS@
LIBS = @LIBS@ LIBS = @LIBS@
LIBSSH_CFLAGS = @LIBSSH_CFLAGS@
LIBSSH_LIBS = @LIBSSH_LIBS@
LTLIBOBJS = @LTLIBOBJS@ LTLIBOBJS = @LTLIBOBJS@
MAKEINFO = @MAKEINFO@ MAKEINFO = @MAKEINFO@
MKDIR_P = @MKDIR_P@ MKDIR_P = @MKDIR_P@
@@ -313,14 +310,12 @@ x3_ssh_CXXFLAGS = \
-std=c++17 \ -std=c++17 \
$(GTK_CFLAGS) \ $(GTK_CFLAGS) \
$(VTE_CFLAGS) \ $(VTE_CFLAGS) \
$(LIBSSH_CFLAGS) \
-DLOCALEDIR=\"$(localedir)\" \ -DLOCALEDIR=\"$(localedir)\" \
-DICONDIR=\"$(datadir)/icons\" -DICONDIR=\"$(datadir)/icons\"
x3_ssh_LDADD = \ x3_ssh_LDADD = \
$(GTK_LIBS) \ $(GTK_LIBS) \
$(VTE_LIBS) \ $(VTE_LIBS)
$(LIBSSH_LIBS)
iconsdir = $(datadir)/icons iconsdir = $(datadir)/icons
dist_icons_DATA = ../icons/x3-ssh.png dist_icons_DATA = ../icons/x3-ssh.png
+5 -7
View File
@@ -4,7 +4,6 @@
#include <cstdlib> #include <cstdlib>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <libssh/libssh.h>
static std::string trim(const std::string& s) { static std::string trim(const std::string& s) {
size_t a = s.find_first_not_of(" \t\r\n"); size_t a = s.find_first_not_of(" \t\r\n");
@@ -128,15 +127,14 @@ bool KeyStore::generate_keypair(const std::string& name, int bits) {
} }
bool KeyStore::import_key(const std::string& name, const std::string& privkey_path) { bool KeyStore::import_key(const std::string& name, const std::string& privkey_path) {
ssh_key key = nullptr; /* Validate key by running ssh-keygen -l */
if (ssh_pki_import_privkey_file(privkey_path.c_str(), nullptr, nullptr, nullptr, &key) != SSH_OK) { std::string check_cmd = "ssh-keygen -l -f \"" + privkey_path + "\" >/dev/null 2>&1";
return false; int rc = system(check_cmd.c_str());
} if (rc != 0) return false;
ssh_key_free(key);
std::string dest = keys_dir_ + "/" + name; std::string dest = keys_dir_ + "/" + name;
std::string cp_cmd = "cp \"" + privkey_path + "\" \"" + dest + "\" && chmod 600 \"" + dest + "\""; std::string cp_cmd = "cp \"" + privkey_path + "\" \"" + dest + "\" && chmod 600 \"" + dest + "\"";
int rc = system(cp_cmd.c_str()); rc = system(cp_cmd.c_str());
if (rc != 0) return false; if (rc != 0) return false;
std::string pub_src = privkey_path + ".pub"; std::string pub_src = privkey_path + ".pub";
+13 -112
View File
@@ -315,8 +315,7 @@ void SshClientWindow::disconnect() {
void SshClientWindow::connect_to_server(const std::string& host, int port, void SshClientWindow::connect_to_server(const std::string& host, int port,
const std::string& user, const std::string& keyfile, const std::string& user, const std::string& keyfile,
const std::string& password, const std::string& name, const std::string& password, const std::string& name) {
const std::string& error_msg) {
g_message("connect_to_server: %s@%s:%d (key=%s, saved_pw=%s)", g_message("connect_to_server: %s@%s:%d (key=%s, saved_pw=%s)",
user.c_str(), host.c_str(), port, user.c_str(), host.c_str(), port,
@@ -346,14 +345,7 @@ void SshClientWindow::connect_to_server(const std::string& host, int port,
/* Remove page FIRST, then delete terminal */ /* Remove page FIRST, then delete terminal */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx); gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
delete term; delete term;
/* If password auth (no keyfile), re-show password dialog with error */
if (keyfile.empty()) {
std::string err = ssh_error.empty() ? "Authentication failed" : ssh_error;
show_password_dialog(host, port, user, keyfile, name, err);
} else {
show_notification("Failed to connect to " + host + ": " + ssh_error, true); show_notification("Failed to connect to " + host + ": " + ssh_error, true);
}
} else { } else {
update_status("Connected to " + user + "@" + host); update_status("Connected to " + user + "@" + host);
show_notification("Server connected: " + user + "@" + host + ":" + std::to_string(port)); show_notification("Server connected: " + user + "@" + host + ":" + std::to_string(port));
@@ -377,20 +369,13 @@ void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
int sel = gtk_combo_box_get_active(GTK_COMBO_BOX(self->server_combo_)); int sel = gtk_combo_box_get_active(GTK_COMBO_BOX(self->server_combo_));
if (sel > 0 && sel <= (int)self->server_store_.servers().size()) { if (sel > 0 && sel <= (int)self->server_store_.servers().size()) {
const auto& s = self->server_store_.servers()[sel - 1]; const auto& s = self->server_store_.servers()[sel - 1];
if (s.use_key && !s.keyfile.empty()) { /* Pass saved password if available, otherwise ssh prompts interactively */
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name); std::string pw = (s.save_password) ? s.password : "";
return;
}
std::string pw = s.save_password ? s.password : "";
if (pw.empty()) {
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
} else {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name); self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name);
}
return; return;
} }
self->show_password_dialog(host, port, u, "", host); self->connect_to_server(host, port, u, "", "", host);
} }
/* ─── server row activated ────────────────────────────────── */ /* ─── server row activated ────────────────────────────────── */
@@ -401,16 +386,9 @@ void SshClientWindow::on_server_row_activated(GtkListBox*, GtkListBoxRow* row, g
if (idx < 0 || idx >= (int)self->server_store_.servers().size()) return; if (idx < 0 || idx >= (int)self->server_store_.servers().size()) return;
const auto& s = self->server_store_.servers()[idx]; const auto& s = self->server_store_.servers()[idx];
if (s.use_key && !s.keyfile.empty()) { /* Pass saved password if available, otherwise ssh prompts interactively */
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name); std::string pw = (s.save_password) ? s.password : "";
} else {
std::string pw = s.save_password ? s.password : "";
if (pw.empty()) {
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
} else {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name); self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name);
}
}
} }
/* ─── server right-click context menu ─────────────────────── */ /* ─── server right-click context menu ─────────────────────── */
@@ -432,18 +410,14 @@ gboolean SshClientWindow::on_server_row_button(GtkWidget*, GdkEventButton* event
/* Connect item */ /* Connect item */
GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect"); GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect");
struct ConnectCtx { SshClientWindow* w; int i; bool uk; bool sp; }; struct ConnectCtx { SshClientWindow* w; int i; };
auto* connect_ctx = new ConnectCtx{self, idx, use_key, has_saved_pw}; auto* connect_ctx = new ConnectCtx{self, idx};
g_signal_connect_data(connect_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { g_signal_connect_data(connect_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<ConnectCtx*>(d); auto* c = static_cast<ConnectCtx*>(d);
const auto& sv = c->w->server_store_.servers()[c->i]; const auto& sv = c->w->server_store_.servers()[c->i];
if (c->uk) { /* Pass saved password if available, otherwise ssh prompts interactively */
c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, "", sv.name); std::string pw = (sv.save_password) ? sv.password : "";
} else if (c->sp) { c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, pw, sv.name);
c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, sv.password, sv.name);
} else {
c->w->show_password_dialog(sv.host, sv.port, sv.user, sv.keyfile, sv.name);
}
delete c; delete c;
}), connect_ctx, nullptr, (GConnectFlags)0); }), connect_ctx, nullptr, (GConnectFlags)0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item);
@@ -486,81 +460,7 @@ void SshClientWindow::on_add_server(GtkWidget*, gpointer d) {
self->show_server_dialog(-1); self->show_server_dialog(-1);
} }
/* ─── password dialog ─────────────────────────────────────── */
void SshClientWindow::show_password_dialog(const std::string& host, int port,
const std::string& user, const std::string& keyfile, const std::string& name,
const std::string& error_msg) {
GtkWidget* dialog = gtk_dialog_new_with_buttons(
"SSH Authentication", GTK_WINDOW(window_),
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
"_Cancel", GTK_RESPONSE_CANCEL,
"_Connect", GTK_RESPONSE_ACCEPT,
nullptr);
GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
GtkWidget* grid = gtk_grid_new();
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
gtk_widget_set_margin_start(grid, 12);
gtk_widget_set_margin_end(grid, 12);
gtk_widget_set_margin_top(grid, 12);
gtk_widget_set_margin_bottom(grid, 12);
int row = 0;
if (!error_msg.empty()) {
GtkWidget* err_label = gtk_label_new(nullptr);
char err_markup[1024];
snprintf(err_markup, sizeof(err_markup),
"<span foreground=\"red\"><b>Error: %s</b></span>", error_msg.c_str());
gtk_label_set_markup(GTK_LABEL(err_label), err_markup);
gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE);
gtk_widget_set_hexpand(err_label, TRUE);
gtk_grid_attach(GTK_GRID(grid), err_label, 0, row, 2, 1);
row++;
}
GtkWidget* info_label = gtk_label_new(nullptr);
char markup[512];
snprintf(markup, sizeof(markup), "<b>Connect to %s@%s:%d</b>", user.c_str(), host.c_str(), port);
gtk_label_set_markup(GTK_LABEL(info_label), markup);
gtk_grid_attach(GTK_GRID(grid), info_label, 0, row, 2, 1);
row++;
GtkWidget* pass_label = gtk_label_new("Password:");
gtk_widget_set_halign(pass_label, GTK_ALIGN_START);
gtk_grid_attach(GTK_GRID(grid), pass_label, 0, row, 1, 1);
GtkWidget* pass_entry = gtk_entry_new();
gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE);
gtk_entry_set_placeholder_text(GTK_ENTRY(pass_entry), "Enter password");
gtk_widget_set_hexpand(pass_entry, TRUE);
gtk_grid_attach(GTK_GRID(grid), pass_entry, 1, row, 1, 1);
row++;
if (!error_msg.empty()) {
gtk_widget_grab_focus(pass_entry);
}
gtk_container_add(GTK_CONTAINER(content), grid);
gtk_widget_show_all(dialog);
while (true) {
gint result = gtk_dialog_run(GTK_DIALOG(dialog));
if (result == GTK_RESPONSE_ACCEPT) {
const char* password = gtk_entry_get_text(GTK_ENTRY(pass_entry));
if (password && *password) {
gtk_widget_destroy(dialog);
connect_to_server(host, port, user, keyfile, password, name);
return;
}
}
break;
}
gtk_widget_destroy(dialog);
}
/* ─── server dialog ───────────────────────────────────────── */ /* ─── server dialog ───────────────────────────────────────── */
@@ -841,7 +741,8 @@ void SshClientWindow::on_close_tab(GtkWidget*, gpointer d) {
void SshClientWindow::on_connect(GtkWidget*, gpointer d) { void SshClientWindow::on_connect(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d); auto* self = static_cast<SshClientWindow*>(d);
self->show_password_dialog("localhost", 22, "root", "", "Local"); /* For local SSH, just use connect with empty password — ssh will prompt */
self->connect_to_server("localhost", 22, "root", "", "", "Local");
} }
void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) { void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) {
+1 -5
View File
@@ -22,14 +22,10 @@ private:
void disconnect(); void disconnect();
void connect_to_server(const std::string& host, int port, void connect_to_server(const std::string& host, int port,
const std::string& user, const std::string& keyfile, const std::string& user, const std::string& keyfile,
const std::string& password, const std::string& name, const std::string& password, const std::string& name);
const std::string& error_msg = "");
void show_server_dialog(int edit_index = -1); void show_server_dialog(int edit_index = -1);
void show_key_dialog(); void show_key_dialog();
void show_password_dialog(const std::string& host, int port, const std::string& user,
const std::string& keyfile, const std::string& name,
const std::string& error_msg = "");
void update_server_combo(); void update_server_combo();
void update_server_list(); void update_server_list();
void update_status(const std::string& msg); void update_status(const std::string& msg);
+112 -313
View File
@@ -1,24 +1,18 @@
#include "ssh_terminal.h" #include "ssh_terminal.h"
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <signal.h>
#include <cstring> #include <cstring>
#include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <glib.h>
SshTerminal::SshTerminal() SshTerminal::SshTerminal()
: terminal_(nullptr), vte_box_(nullptr), vte_pty_(nullptr), : vte_box_(nullptr), terminal_(nullptr),
session_(nullptr), channel_(nullptr), child_pid_(0), connected_(false) {
pty_master_(-1), pty_slave_(-1),
running_(false), connected_(false), cleanup_done_(true), thread_(nullptr) {
vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
terminal_ = VTE_TERMINAL(vte_terminal_new()); terminal_ = VTE_TERMINAL(vte_terminal_new());
gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0);
gtk_widget_show_all(vte_box_); gtk_widget_show_all(vte_box_);
g_signal_connect(terminal_, "size-allocate", G_CALLBACK(on_size_changed), this); g_signal_connect(terminal_, "child-exited", G_CALLBACK(on_child_exited), this);
} }
SshTerminal::~SshTerminal() { SshTerminal::~SshTerminal() {
@@ -29,108 +23,17 @@ SshTerminal::~SshTerminal() {
} }
} }
void SshTerminal::on_size_changed(GtkWidget* widget, GtkAllocation* alloc, gpointer data) { void SshTerminal::on_child_exited(VteTerminal* terminal, int status, gpointer data) {
(void)alloc; (void)terminal;
auto* self = static_cast<SshTerminal*>(data); auto* self = static_cast<SshTerminal*>(data);
if (self->channel_ && self->connected_) {
VteTerminal* term = VTE_TERMINAL(widget);
guint cols = vte_terminal_get_column_count(term);
guint rows = vte_terminal_get_row_count(term);
ssh_channel_change_pty_size(self->channel_, cols, rows);
}
}
void SshTerminal::cleanup() {
std::lock_guard<std::mutex> lock(mutex_);
if (cleanup_done_) return;
cleanup_done_ = true;
if (channel_) {
ssh_channel_close(channel_);
ssh_channel_free(channel_);
channel_ = nullptr;
}
if (session_) {
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
}
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
connected_ = false;
}
gpointer SshTerminal::io_thread_func(gpointer data) {
auto* self = static_cast<SshTerminal*>(data);
ssh_session sess = self->session_;
ssh_channel chan = self->channel_;
int ssh_fd = ssh_get_fd(sess);
int slave_fd = self->pty_slave_;
if (ssh_fd < 0 || slave_fd < 0) {
g_idle_add([](gpointer d) -> gboolean {
auto* s = static_cast<SshTerminal*>(d);
if (s->on_status_change) s->on_status_change(false, "SSH fd unavailable");
return G_SOURCE_REMOVE;
}, self);
return nullptr;
}
char buf[4096];
fd_set rfds;
struct timeval tv;
while (self->running_) {
FD_ZERO(&rfds);
FD_SET(slave_fd, &rfds);
FD_SET(ssh_fd, &rfds);
int maxfd = (slave_fd > ssh_fd ? slave_fd : ssh_fd) + 1;
tv.tv_sec = 0;
tv.tv_usec = 50000;
int ret = select(maxfd, &rfds, nullptr, nullptr, &tv);
if (ret < 0) {
if (errno == EINTR) continue;
break;
}
if (ret == 0) continue;
if (FD_ISSET(slave_fd, &rfds)) {
ssize_t n = read(slave_fd, buf, sizeof(buf));
if (n > 0) {
if (ssh_channel_write(chan, buf, n) < 0) break;
} else if (n <= 0) {
break;
}
}
if (FD_ISSET(ssh_fd, &rfds)) {
int poll_result = ssh_channel_poll_timeout(chan, 50, 0);
if (poll_result > 0) {
ssize_t n = ssh_channel_read_timeout(chan, buf, sizeof(buf), 0, 0);
if (n > 0) {
if (write(slave_fd, buf, n) < 0 && errno != EINTR) break;
} else if (n == SSH_EOF || n == SSH_ERROR) {
break;
}
} else if (poll_result < 0) {
break;
}
}
if (ssh_channel_is_eof(chan)) break;
}
self->connected_ = false; self->connected_ = false;
g_idle_add([](gpointer d) -> gboolean { self->child_pid_ = 0;
auto* s = static_cast<SshTerminal*>(d);
if (s->on_status_change) s->on_status_change(false, "Disconnected");
return G_SOURCE_REMOVE;
}, self);
return nullptr; if (self->on_status_change) {
char msg[128];
snprintf(msg, sizeof(msg), "Process exited with status %d", status);
self->on_status_change(false, msg);
}
} }
bool SshTerminal::connect(const std::string& host, int port, const std::string& user, bool SshTerminal::connect(const std::string& host, int port, const std::string& user,
@@ -138,232 +41,128 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
std::string& out_error) { std::string& out_error) {
if (connected_) disconnect(); if (connected_) disconnect();
/* Reset state for a fresh connect attempt */ g_message("Connecting to %s@%s:%d ...", user.c_str(), host.c_str(), port);
cleanup_done_ = true;
session_ = nullptr;
channel_ = nullptr;
pty_master_ = -1;
pty_slave_ = -1;
vte_pty_ = nullptr;
g_message("Connecting to %s:%d as %s ...", host.c_str(), port, user.c_str()); /* Build ssh command arguments */
std::vector<std::string> args;
g_message("ssh_new..."); /* Use sshpass if password is provided and sshpass is available */
session_ = ssh_new(); bool use_sshpass = false;
if (!session_) { out_error = "Failed to create SSH session"; g_warning("ssh_new() failed"); return false; } if (!password.empty()) {
if (g_find_program_in_path("sshpass")) {
g_message("ssh_options_set host..."); use_sshpass = true;
ssh_options_set(session_, SSH_OPTIONS_HOST, host.c_str()); args.push_back("sshpass");
unsigned int p = port; args.push_back("-e");
g_message("ssh_options_set port..."); g_message("Using sshpass for auto-login");
ssh_options_set(session_, SSH_OPTIONS_PORT, &p); } else {
g_message("ssh_options_set user..."); g_message("sshpass not found, ssh will prompt for password");
ssh_options_set(session_, SSH_OPTIONS_USER, user.c_str()); }
int strict = 0;
g_message("ssh_options_set stricthostkeycheck...");
ssh_options_set(session_, SSH_OPTIONS_STRICTHOSTKEYCHECK, &strict);
g_message("ssh_connect...");
if (ssh_connect(session_) != SSH_OK) {
std::string err = ssh_get_error(session_);
g_warning("ssh_connect failed: %s", err.c_str());
out_error = "Connection failed: " + err;
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, out_error);
return false;
} }
g_message("ssh_connect OK");
g_message("ssh_session_update_known_hosts..."); args.push_back("ssh");
ssh_session_update_known_hosts(session_); args.push_back("-p");
g_message("known_hosts OK"); args.push_back(std::to_string(port));
args.push_back("-o");
args.push_back("StrictHostKeyChecking=no");
args.push_back("-o");
args.push_back("UserKnownHostsFile=/dev/null");
args.push_back("-o");
args.push_back("LogLevel=QUIET");
if (!keyfile.empty()) { if (!keyfile.empty()) {
g_message("Trying keyfile auth..."); args.push_back("-i");
ssh_key key = nullptr; args.push_back(keyfile);
if (ssh_pki_import_privkey_file(keyfile.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) { g_message("Using keyfile: %s", keyfile.c_str());
if (ssh_userauth_publickey(session_, nullptr, key) == SSH_AUTH_SUCCESS) {
ssh_key_free(key);
goto auth_done;
}
ssh_key_free(key);
}
} }
{ args.push_back(user + "@" + host);
g_message("ssh_userauth_password...");
int rc = ssh_userauth_password(session_, nullptr, password.c_str()); /* Convert to char** for spawn */
g_message("auth result: %d", rc); std::vector<char*> c_args;
if (rc != SSH_AUTH_SUCCESS) { for (auto& arg : args) {
std::string err = ssh_get_error(session_); c_args.push_back(const_cast<char*>(arg.c_str()));
g_warning("Auth failed: %s", err.c_str());
out_error = "Authentication failed: " + err;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, out_error);
return false;
} }
c_args.push_back(nullptr);
/* Set up environment with password if using sshpass */
gchar** envp = nullptr;
if (use_sshpass) {
GHashTable* env_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
/* Copy current environment */
for (char** ep = environ; *ep; ep++) {
gchar** parts = g_strsplit(*ep, "=", 2);
if (parts[0] && parts[1]) {
g_hash_table_insert(env_table, g_strdup(parts[0]), g_strdup(parts[1]));
}
g_strfreev(parts);
} }
auth_done: g_hash_table_insert(env_table, g_strdup("SSHPASS"), g_strdup(password.c_str()));
g_message("ssh_channel_new...");
channel_ = ssh_channel_new(session_);
if (!channel_) { out_error = "Channel creation failed"; g_warning("ssh_channel_new() failed"); return false; }
g_message("ssh_channel_open_session..."); /* Convert hash table to envp array */
if (ssh_channel_open_session(channel_) != SSH_OK) { GHashTableIter iter;
out_error = "Channel open failed"; gpointer key, value;
g_warning("ssh_channel_open_session failed"); GArray* env_arr = g_array_new(TRUE, TRUE, sizeof(gchar*));
ssh_channel_free(channel_);
channel_ = nullptr; g_hash_table_iter_init(&iter, env_table);
ssh_disconnect(session_); while (g_hash_table_iter_next(&iter, &key, &value)) {
ssh_free(session_); gchar* entry = g_strdup_printf("%s=%s", (const char*)key, (const char*)value);
session_ = nullptr; g_array_append_val(env_arr, entry);
if (on_status_change) on_status_change(false, "Session open failed"); }
return false; envp = (gchar**)g_array_free(env_arr, FALSE);
g_hash_table_destroy(env_table);
} }
g_message("channel open OK");
guint cols = vte_terminal_get_column_count(terminal_); /* Spawn ssh process */
guint rows = vte_terminal_get_row_count(terminal_); GError* error = nullptr;
g_message("requesting PTY %dx%d...", cols, rows); GPid child_pid = 0;
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
out_error = "PTY request failed"; g_message("Spawning: %s", args[0].c_str());
g_warning("PTY request failed");
ssh_channel_free(channel_); gboolean ok = vte_terminal_spawn_sync(
channel_ = nullptr; terminal_,
ssh_disconnect(session_); VTE_PTY_DEFAULT,
ssh_free(session_); nullptr, /* working directory */
session_ = nullptr; c_args.data(),
if (on_status_change) on_status_change(false, "PTY request failed"); envp, /* environment */
(GSpawnFlags)(G_SPAWN_CHILD_INHERITS_STDIN),
nullptr, /* child_setup */
nullptr, /* child_setup_data */
&child_pid,
nullptr, /* cancellable */
&error
);
if (envp) {
for (gchar** p = envp; *p; p++) g_free(*p);
g_free(envp);
}
if (!ok) {
out_error = error ? error->message : "Failed to spawn SSH process";
g_warning("spawn failed: %s", out_error.c_str());
if (error) g_error_free(error);
return false; return false;
} }
g_message("requesting shell..."); child_pid_ = child_pid;
if (ssh_channel_request_shell(channel_) != SSH_OK) {
out_error = "Shell request failed";
g_warning("Shell request failed");
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "Shell request failed");
return false;
}
g_message("shell OK");
g_message("openpty (posix_openpt)...");
pty_master_ = posix_openpt(O_RDWR | O_NOCTTY);
if (pty_master_ < 0) {
g_warning("posix_openpt failed: %s", strerror(errno));
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY master open failed");
return false;
}
g_message("grantpt...");
if (grantpt(pty_master_) < 0) {
g_warning("grantpt failed: %s", strerror(errno));
close(pty_master_); pty_master_ = -1;
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY grantpt failed");
return false;
}
g_message("unlockpt...");
if (unlockpt(pty_master_) < 0) {
g_warning("unlockpt failed: %s", strerror(errno));
close(pty_master_); pty_master_ = -1;
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY unlockpt failed");
return false;
}
const char* slave_name = ptsname(pty_master_);
g_message("slave name: %s", slave_name ? slave_name : "(null)");
if (!slave_name) {
g_warning("ptsname failed");
close(pty_master_); pty_master_ = -1;
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY ptsname failed");
return false;
}
g_message("opening slave %s...", slave_name);
pty_slave_ = open(slave_name, O_RDWR | O_NOCTTY);
if (pty_slave_ < 0) {
g_warning("slave open failed: %s", strerror(errno));
close(pty_master_); pty_master_ = -1;
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY slave open failed");
return false;
}
g_message("PTY ready (master=%d, slave=%d)", pty_master_, pty_slave_);
g_message("vte_pty_new_foreign_sync...");
vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
if (vte_pty_) {
g_message("setting VTE pty...");
vte_terminal_set_pty(terminal_, vte_pty_);
} else {
close(pty_slave_); pty_slave_ = -1;
close(pty_master_); pty_master_ = -1;
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "VTE pty setup failed");
return false;
}
cleanup_done_ = false;
connected_ = true; connected_ = true;
running_ = true;
g_message("SSH connection established, starting IO thread");
thread_ = g_thread_new("ssh_io", io_thread_func, this);
if (on_status_change) on_status_change(true, "Connected to " + user + "@" + host); g_message("SSH process spawned (PID %d)", (int)child_pid);
if (on_status_change)
on_status_change(true, "Connected to " + user + "@" + host + ":" + std::to_string(port));
return true; return true;
} }
void SshTerminal::disconnect() { void SshTerminal::disconnect() {
running_ = false; if (child_pid_ > 0) {
g_message("Killing SSH process (PID %d)", (int)child_pid_);
if (thread_) { kill(child_pid_, SIGHUP);
g_thread_join(thread_); g_spawn_close_pid(child_pid_);
thread_ = nullptr; child_pid_ = 0;
} }
connected_ = false;
/* Flush any pending idle callbacks from the IO thread
that might still reference this object */
while (g_main_context_pending(NULL))
g_main_context_iteration(NULL, FALSE);
if (vte_pty_) {
g_object_unref(vte_pty_);
vte_pty_ = nullptr;
if (terminal_) vte_terminal_set_pty(terminal_, nullptr);
}
cleanup();
} }
+3 -16
View File
@@ -1,10 +1,7 @@
#pragma once #pragma once
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <vte/vte.h> #include <vte/vte.h>
#include <libssh/libssh.h>
#include <atomic>
#include <string> #include <string>
#include <mutex>
#include <functional> #include <functional>
class SshTerminal { class SshTerminal {
@@ -24,21 +21,11 @@ public:
std::function<void(bool, const std::string&)> on_status_change; std::function<void(bool, const std::string&)> on_status_change;
private: private:
void cleanup(); static void on_child_exited(VteTerminal* terminal, int status, gpointer data);
static gpointer io_thread_func(gpointer data);
static void on_size_changed(GtkWidget* widget, GtkAllocation* alloc, gpointer data);
GtkWidget* vte_box_; GtkWidget* vte_box_;
VteTerminal* terminal_; VteTerminal* terminal_;
VtePty* vte_pty_; GPid child_pid_;
ssh_session session_; bool connected_;
ssh_channel channel_;
int pty_master_;
int pty_slave_;
std::atomic<bool> running_;
std::atomic<bool> connected_;
std::atomic<bool> cleanup_done_;
GThread* thread_;
std::mutex mutex_;
std::string server_name_; std::string server_name_;
}; };