Restore built-in askpass helper for SSH password auth

This commit is contained in:
Keith Smith
2026-03-01 09:53:17 -07:00
parent ceed19d517
commit 614d31fa71
2 changed files with 94 additions and 2 deletions

View File

@@ -1,7 +1,20 @@
#include "ssh_session_backend.h" #include "ssh_session_backend.h"
#include <QDir>
#include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QTextStream>
#include <QUuid>
namespace {
QString escapeForShellSingleQuotes(const QString& value)
{
QString escaped = value;
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
return escaped;
}
}
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
: SessionBackend(profile, parent), : SessionBackend(profile, parent),
@@ -45,6 +58,7 @@ SshSessionBackend::~SshSessionBackend()
m_process->kill(); m_process->kill();
m_process->waitForFinished(500); m_process->waitForFinished(500);
} }
cleanupAskPassScript();
} }
void SshSessionBackend::connectSession(const SessionConnectOptions& options) void SshSessionBackend::connectSession(const SessionConnectOptions& options)
@@ -161,6 +175,7 @@ void SshSessionBackend::onProcessErrorOccurred(QProcess::ProcessError)
void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus) void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus)
{ {
m_connectedProbeTimer->stop(); m_connectedProbeTimer->stop();
cleanupAskPassScript();
if (m_reconnectPending) { if (m_reconnectPending) {
m_reconnectPending = false; m_reconnectPending = false;
@@ -311,6 +326,8 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes"); args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
} }
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
if (p.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) { if (p.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
if (options.password.isEmpty()) { if (options.password.isEmpty()) {
const QString message = QStringLiteral("Password is required for password authentication."); 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") args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password")
<< QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no") << QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no")
<< QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1"); << 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) { } else if (p.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
QString keyPath = options.privateKeyPath.trimmed(); QString keyPath = options.privateKeyPath.trimmed();
if (keyPath.isEmpty()) { if (keyPath.isEmpty()) {
@@ -354,7 +378,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed()); : QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed());
args << target; args << target;
m_process->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); m_process->setProcessEnvironment(environment);
m_process->setProgram(QStringLiteral("ssh")); m_process->setProgram(QStringLiteral("ssh"));
m_process->setArguments(args); m_process->setArguments(args);
m_process->setProcessChannelMode(QProcess::SeparateChannels); m_process->setProcessChannelMode(QProcess::SeparateChannels);
@@ -371,6 +395,66 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
return true; 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 QString SshSessionBackend::mapSshError(const QString& rawError) const
{ {
const QString raw = rawError.trimmed(); const QString raw = rawError.trimmed();
@@ -398,6 +482,9 @@ QString SshSessionBackend::mapSshError(const QString& rawError) const
return QStringLiteral("Private key file is not accessible."); return QStringLiteral("Private key file is not accessible.");
} }
if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) { 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."); return QStringLiteral("Required file was not found.");
} }
if (raw.isEmpty()) { if (raw.isEmpty()) {

View File

@@ -39,12 +39,17 @@ private:
SessionConnectOptions m_reconnectOptions; SessionConnectOptions m_reconnectOptions;
SessionConnectOptions m_activeOptions; SessionConnectOptions m_activeOptions;
QString m_lastRawError; QString m_lastRawError;
QString m_askPassScriptPath;
bool m_waitingForPasswordPrompt; bool m_waitingForPasswordPrompt;
bool m_waitingForHostKeyConfirmation; bool m_waitingForHostKeyConfirmation;
bool m_passwordSubmitted; bool m_passwordSubmitted;
void setState(SessionState state, const QString& message); void setState(SessionState state, const QString& message);
bool startSshProcess(const SessionConnectOptions& options); bool startSshProcess(const SessionConnectOptions& options);
bool configureAskPass(const SessionConnectOptions& options,
QProcessEnvironment& environment,
QString& error);
void cleanupAskPassScript();
QString mapSshError(const QString& rawError) const; QString mapSshError(const QString& rawError) const;
QString knownHostsFileForNullDevice() const; QString knownHostsFileForNullDevice() const;
}; };