From 614d31fa71744c762977af42e8521cfd8a4d3e37 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sun, 1 Mar 2026 09:53:17 -0700 Subject: [PATCH] Restore built-in askpass helper for SSH password auth --- src/ssh_session_backend.cpp | 91 ++++++++++++++++++++++++++++++++++++- src/ssh_session_backend.h | 5 ++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/ssh_session_backend.cpp b/src/ssh_session_backend.cpp index 15f6c58..a2f87a8 100644 --- a/src/ssh_session_backend.cpp +++ b/src/ssh_session_backend.cpp @@ -1,7 +1,20 @@ #include "ssh_session_backend.h" +#include +#include #include #include +#include +#include + +namespace { +QString escapeForShellSingleQuotes(const QString& value) +{ + QString escaped = value; + escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'")); + return escaped; +} +} SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) : SessionBackend(profile, parent), @@ -45,6 +58,7 @@ SshSessionBackend::~SshSessionBackend() m_process->kill(); m_process->waitForFinished(500); } + cleanupAskPassScript(); } void SshSessionBackend::connectSession(const SessionConnectOptions& options) @@ -161,6 +175,7 @@ void SshSessionBackend::onProcessErrorOccurred(QProcess::ProcessError) void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus) { m_connectedProbeTimer->stop(); + cleanupAskPassScript(); if (m_reconnectPending) { m_reconnectPending = false; @@ -311,6 +326,8 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes"); } + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + if (p.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) { if (options.password.isEmpty()) { const QString message = QStringLiteral("Password is required for password authentication."); @@ -322,7 +339,14 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password") << QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no") << QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1"); - m_waitingForPasswordPrompt = true; + m_waitingForPasswordPrompt = false; + + QString askPassError; + if (!configureAskPass(options, environment, askPassError)) { + setState(SessionState::Failed, askPassError); + emit connectionError(askPassError, askPassError); + return false; + } } else if (p.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) { QString keyPath = options.privateKeyPath.trimmed(); if (keyPath.isEmpty()) { @@ -354,7 +378,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) : QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed()); args << target; - m_process->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); + m_process->setProcessEnvironment(environment); m_process->setProgram(QStringLiteral("ssh")); m_process->setArguments(args); m_process->setProcessChannelMode(QProcess::SeparateChannels); @@ -371,6 +395,66 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) return true; } +bool SshSessionBackend::configureAskPass(const SessionConnectOptions& options, + QProcessEnvironment& environment, + QString& error) +{ + cleanupAskPassScript(); + +#ifdef Q_OS_WIN + m_askPassScriptPath = QDir::temp().filePath( + QStringLiteral("orbithub_askpass_%1.cmd") + .arg(QUuid::createUuid().toString(QUuid::WithoutBraces))); +#else + m_askPassScriptPath = QDir::temp().filePath( + QStringLiteral("orbithub_askpass_%1.sh") + .arg(QUuid::createUuid().toString(QUuid::WithoutBraces))); +#endif + + QFile script(m_askPassScriptPath); + if (!script.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + error = QStringLiteral("Failed to create temporary askpass helper script."); + cleanupAskPassScript(); + return false; + } + + QTextStream out(&script); +#ifdef Q_OS_WIN + out << "@echo off\r\n"; + out << "echo " << options.password << "\r\n"; +#else + const QString escapedPassword = escapeForShellSingleQuotes(options.password); + out << "#!/bin/sh\n"; + out << "printf '%s\\n' '" << escapedPassword << "'\n"; +#endif + out.flush(); + script.close(); + +#ifndef Q_OS_WIN + if (!QFile::setPermissions(m_askPassScriptPath, + QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) { + error = QStringLiteral("Failed to set permissions on askpass helper script."); + cleanupAskPassScript(); + return false; + } +#endif + + environment.insert(QStringLiteral("SSH_ASKPASS"), m_askPassScriptPath); + environment.insert(QStringLiteral("SSH_ASKPASS_REQUIRE"), QStringLiteral("force")); + if (!environment.contains(QStringLiteral("DISPLAY"))) { + environment.insert(QStringLiteral("DISPLAY"), QStringLiteral(":0")); + } + return true; +} + +void SshSessionBackend::cleanupAskPassScript() +{ + if (!m_askPassScriptPath.isEmpty()) { + QFile::remove(m_askPassScriptPath); + m_askPassScriptPath.clear(); + } +} + QString SshSessionBackend::mapSshError(const QString& rawError) const { const QString raw = rawError.trimmed(); @@ -398,6 +482,9 @@ QString SshSessionBackend::mapSshError(const QString& rawError) const return QStringLiteral("Private key file is not accessible."); } if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) { + if (raw.contains(QStringLiteral("ssh-askpass"), Qt::CaseInsensitive)) { + return QStringLiteral("SSH password helper is missing or failed to launch."); + } return QStringLiteral("Required file was not found."); } if (raw.isEmpty()) { diff --git a/src/ssh_session_backend.h b/src/ssh_session_backend.h index c7b50a4..c651c7e 100644 --- a/src/ssh_session_backend.h +++ b/src/ssh_session_backend.h @@ -39,12 +39,17 @@ private: SessionConnectOptions m_reconnectOptions; SessionConnectOptions m_activeOptions; QString m_lastRawError; + QString m_askPassScriptPath; bool m_waitingForPasswordPrompt; bool m_waitingForHostKeyConfirmation; bool m_passwordSubmitted; void setState(SessionState state, const QString& message); bool startSshProcess(const SessionConnectOptions& options); + bool configureAskPass(const SessionConnectOptions& options, + QProcessEnvironment& environment, + QString& error); + void cleanupAskPassScript(); QString mapSshError(const QString& rawError) const; QString knownHostsFileForNullDevice() const; };