diff --git a/src/profile_dialog.cpp b/src/profile_dialog.cpp index 465adf2..438020d 100644 --- a/src/profile_dialog.cpp +++ b/src/profile_dialog.cpp @@ -52,7 +52,7 @@ ProfileDialog::ProfileDialog(QWidget* parent) m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")}); m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")}); m_knownHostsPolicyInput->addItems( - {QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")}); + {QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")}); m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519")); diff --git a/src/profile_repository.cpp b/src/profile_repository.cpp index bfd4555..6680541 100644 --- a/src/profile_repository.cpp +++ b/src/profile_repository.cpp @@ -32,7 +32,7 @@ void bindProfileFields(QSqlQuery& query, const Profile& profile) query.addBindValue(profile.authMode.trimmed()); query.addBindValue(profile.privateKeyPath.trimmed()); query.addBindValue(profile.knownHostsPolicy.trimmed().isEmpty() - ? QStringLiteral("Strict") + ? QStringLiteral("Ask") : profile.knownHostsPolicy.trimmed()); } @@ -49,7 +49,7 @@ Profile profileFromQuery(const QSqlQuery& query) profile.privateKeyPath = query.value(7).toString(); profile.knownHostsPolicy = query.value(8).toString(); if (profile.knownHostsPolicy.isEmpty()) { - profile.knownHostsPolicy = QStringLiteral("Strict"); + profile.knownHostsPolicy = QStringLiteral("Ask"); } return profile; } @@ -253,7 +253,7 @@ bool ProfileRepository::initializeDatabase() "protocol TEXT NOT NULL DEFAULT 'SSH'," "auth_mode TEXT NOT NULL DEFAULT 'Password'," "private_key_path TEXT NOT NULL DEFAULT ''," - "known_hosts_policy TEXT NOT NULL DEFAULT 'Strict'" + "known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'" ")")); if (!created) { @@ -299,7 +299,7 @@ bool ProfileRepository::ensureProfileSchema() const {QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")}, {QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")}, {QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")}, - {QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Strict'")}}; + {QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")}}; for (const ColumnDef& column : required) { if (columns.contains(column.name)) { diff --git a/src/profile_repository.h b/src/profile_repository.h index 51155b9..e1566e8 100644 --- a/src/profile_repository.h +++ b/src/profile_repository.h @@ -17,7 +17,7 @@ struct Profile QString protocol = QStringLiteral("SSH"); QString authMode = QStringLiteral("Password"); QString privateKeyPath; - QString knownHostsPolicy = QStringLiteral("Strict"); + QString knownHostsPolicy = QStringLiteral("Ask"); }; class ProfileRepository diff --git a/src/session_backend.h b/src/session_backend.h index b189cf4..0a02ff1 100644 --- a/src/session_backend.h +++ b/src/session_backend.h @@ -41,11 +41,15 @@ public slots: virtual void connectSession(const SessionConnectOptions& options) = 0; virtual void disconnectSession() = 0; virtual void reconnectSession(const SessionConnectOptions& options) = 0; + virtual void sendInput(const QString& input) = 0; + virtual void confirmHostKey(bool trustHost) = 0; signals: void stateChanged(SessionState state, const QString& message); void eventLogged(const QString& message); void connectionError(const QString& displayMessage, const QString& rawMessage); + void outputReceived(const QString& text); + void hostKeyConfirmationRequested(const QString& prompt); private: Profile m_profile; diff --git a/src/session_tab.cpp b/src/session_tab.cpp index febf605..f748f1f 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -28,11 +29,15 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent) m_state(SessionState::Disconnected), m_statusLabel(nullptr), m_errorLabel(nullptr), + m_terminalOutput(nullptr), + m_terminalInput(nullptr), m_eventLog(nullptr), m_connectButton(nullptr), m_disconnectButton(nullptr), m_reconnectButton(nullptr), - m_copyErrorButton(nullptr) + m_copyErrorButton(nullptr), + m_sendInputButton(nullptr), + m_clearTerminalButton(nullptr) { qRegisterMetaType("SessionConnectOptions"); qRegisterMetaType("SessionState"); @@ -59,6 +64,16 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent) m_backend, &SessionBackend::reconnectSession, Qt::QueuedConnection); + connect(this, + &SessionTab::requestInput, + m_backend, + &SessionBackend::sendInput, + Qt::QueuedConnection); + connect(this, + &SessionTab::requestHostKeyConfirmation, + m_backend, + &SessionBackend::confirmHostKey, + Qt::QueuedConnection); connect(m_backend, &SessionBackend::stateChanged, @@ -75,6 +90,16 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent) this, &SessionTab::onBackendConnectionError, Qt::QueuedConnection); + connect(m_backend, + &SessionBackend::outputReceived, + this, + &SessionTab::onBackendOutputReceived, + Qt::QueuedConnection); + connect(m_backend, + &SessionBackend::hostKeyConfirmationRequested, + this, + &SessionTab::onBackendHostKeyConfirmationRequested, + Qt::QueuedConnection); m_backendThread->start(); @@ -139,6 +164,22 @@ void SessionTab::onCopyErrorClicked() appendEvent(QStringLiteral("Copied last error to clipboard.")); } +void SessionTab::onSendInputClicked() +{ + const QString input = m_terminalInput->text(); + if (input.isEmpty()) { + return; + } + + emit requestInput(input + QStringLiteral("\n")); + m_terminalInput->clear(); +} + +void SessionTab::onClearTerminalClicked() +{ + m_terminalOutput->clear(); +} + void SessionTab::onBackendStateChanged(SessionState state, const QString& message) { setState(state, message); @@ -156,6 +197,35 @@ void SessionTab::onBackendConnectionError(const QString& displayMessage, const Q m_copyErrorButton->setEnabled(true); } +void SessionTab::onBackendOutputReceived(const QString& text) +{ + if (text.isEmpty()) { + return; + } + + QTextCursor cursor = m_terminalOutput->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(text); + m_terminalOutput->setTextCursor(cursor); + m_terminalOutput->ensureCursorVisible(); +} + +void SessionTab::onBackendHostKeyConfirmationRequested(const QString& prompt) +{ + const QString question = prompt.isEmpty() + ? QStringLiteral("Unknown SSH host key. Do you trust this host?") + : prompt; + + const QMessageBox::StandardButton reply = QMessageBox::question( + this, + QStringLiteral("SSH Host Key Confirmation"), + QStringLiteral("%1\n\nTrust and continue?").arg(question), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + emit requestHostKeyConfirmation(reply == QMessageBox::Yes); +} + void SessionTab::setupUi() { auto* rootLayout = new QVBoxLayout(this); @@ -186,20 +256,33 @@ void SessionTab::setupUi() actionRow->addWidget(m_copyErrorButton); actionRow->addStretch(); - auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), this); - QFont surfaceFont = surfaceLabel->font(); - surfaceFont.setPointSize(surfaceFont.pointSize() + 6); - surfaceFont.setBold(true); - surfaceLabel->setFont(surfaceFont); - surfaceLabel->setAlignment(Qt::AlignCenter); - surfaceLabel->setMinimumHeight(180); - surfaceLabel->setStyleSheet( - QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;")); + auto* terminalHeader = new QHBoxLayout(); + auto* terminalLabel = new QLabel(QStringLiteral("SSH Terminal"), this); + m_clearTerminalButton = new QPushButton(QStringLiteral("Clear"), this); + terminalHeader->addWidget(terminalLabel); + terminalHeader->addStretch(); + terminalHeader->addWidget(m_clearTerminalButton); + m_terminalOutput = new QPlainTextEdit(this); + m_terminalOutput->setReadOnly(true); + m_terminalOutput->setMaximumBlockCount(4000); + QFont terminalFont(QStringLiteral("Monospace")); + terminalFont.setStyleHint(QFont::TypeWriter); + m_terminalOutput->setFont(terminalFont); + m_terminalOutput->setMinimumHeight(220); + + auto* terminalInputRow = new QHBoxLayout(); + m_terminalInput = new QLineEdit(this); + m_terminalInput->setPlaceholderText(QStringLiteral("Type command and press Enter...")); + m_sendInputButton = new QPushButton(QStringLiteral("Send"), this); + terminalInputRow->addWidget(m_terminalInput, 1); + terminalInputRow->addWidget(m_sendInputButton); + + auto* eventHeader = new QLabel(QStringLiteral("Session Events"), this); m_eventLog = new QPlainTextEdit(this); m_eventLog->setReadOnly(true); m_eventLog->setPlaceholderText(QStringLiteral("Session event log...")); - m_eventLog->setMinimumHeight(180); + m_eventLog->setMinimumHeight(140); rootLayout->addWidget(profileLabel); rootLayout->addWidget(endpointLabel); @@ -207,13 +290,22 @@ void SessionTab::setupUi() rootLayout->addWidget(m_statusLabel); rootLayout->addWidget(m_errorLabel); rootLayout->addLayout(actionRow); - rootLayout->addWidget(surfaceLabel); + rootLayout->addLayout(terminalHeader); + rootLayout->addWidget(m_terminalOutput, 1); + rootLayout->addLayout(terminalInputRow); + rootLayout->addWidget(eventHeader); rootLayout->addWidget(m_eventLog, 1); connect(m_connectButton, &QPushButton::clicked, this, &SessionTab::onConnectClicked); connect(m_disconnectButton, &QPushButton::clicked, this, &SessionTab::onDisconnectClicked); connect(m_reconnectButton, &QPushButton::clicked, this, &SessionTab::onReconnectClicked); connect(m_copyErrorButton, &QPushButton::clicked, this, &SessionTab::onCopyErrorClicked); + connect(m_sendInputButton, &QPushButton::clicked, this, &SessionTab::onSendInputClicked); + connect(m_terminalInput, &QLineEdit::returnPressed, this, &SessionTab::onSendInputClicked); + connect(m_clearTerminalButton, + &QPushButton::clicked, + this, + &SessionTab::onClearTerminalClicked); } std::optional SessionTab::buildConnectOptions() @@ -352,12 +444,17 @@ QString SessionTab::stateSuffix() const void SessionTab::refreshActionButtons() { - m_connectButton->setEnabled(m_state == SessionState::Disconnected - || m_state == SessionState::Failed); + const bool isConnected = m_state == SessionState::Connected; + const bool canConnect = m_state == SessionState::Disconnected || m_state == SessionState::Failed; + + m_connectButton->setEnabled(canConnect); m_disconnectButton->setEnabled(m_state == SessionState::Connected || m_state == SessionState::Connecting); m_reconnectButton->setEnabled(m_state == SessionState::Connected || m_state == SessionState::Failed || m_state == SessionState::Disconnected); m_copyErrorButton->setEnabled(!m_lastError.isEmpty()); + + m_terminalInput->setEnabled(isConnected); + m_sendInputButton->setEnabled(isConnected); } diff --git a/src/session_tab.h b/src/session_tab.h index 8b7a824..061d7de 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -9,6 +9,7 @@ #include class QLabel; +class QLineEdit; class QPlainTextEdit; class QPushButton; class QThread; @@ -29,16 +30,22 @@ signals: void requestConnect(const SessionConnectOptions& options); void requestDisconnect(); void requestReconnect(const SessionConnectOptions& options); + void requestInput(const QString& input); + void requestHostKeyConfirmation(bool trustHost); private slots: void onConnectClicked(); void onDisconnectClicked(); void onReconnectClicked(); void onCopyErrorClicked(); + void onSendInputClicked(); + void onClearTerminalClicked(); void onBackendStateChanged(SessionState state, const QString& message); void onBackendEventLogged(const QString& message); void onBackendConnectionError(const QString& displayMessage, const QString& rawMessage); + void onBackendOutputReceived(const QString& text); + void onBackendHostKeyConfirmationRequested(const QString& prompt); private: Profile m_profile; @@ -49,11 +56,15 @@ private: QLabel* m_statusLabel; QLabel* m_errorLabel; + QPlainTextEdit* m_terminalOutput; + QLineEdit* m_terminalInput; QPlainTextEdit* m_eventLog; QPushButton* m_connectButton; QPushButton* m_disconnectButton; QPushButton* m_reconnectButton; QPushButton* m_copyErrorButton; + QPushButton* m_sendInputButton; + QPushButton* m_clearTerminalButton; void setupUi(); std::optional buildConnectOptions(); diff --git a/src/ssh_session_backend.cpp b/src/ssh_session_backend.cpp index 8aa93bf..15f6c58 100644 --- a/src/ssh_session_backend.cpp +++ b/src/ssh_session_backend.cpp @@ -1,31 +1,7 @@ #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; -} - -QString escapedForWindowsEcho(const QString& value) -{ - QString escaped = value; - escaped.replace(QStringLiteral("^"), QStringLiteral("^^")); - escaped.replace(QStringLiteral("&"), QStringLiteral("^&")); - escaped.replace(QStringLiteral("|"), QStringLiteral("^|")); - escaped.replace(QStringLiteral("<"), QStringLiteral("^<")); - escaped.replace(QStringLiteral(">"), QStringLiteral("^>")); - return escaped; -} -} SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) : SessionBackend(profile, parent), @@ -33,7 +9,10 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) m_connectedProbeTimer(new QTimer(this)), m_state(SessionState::Disconnected), m_userInitiatedDisconnect(false), - m_reconnectPending(false) + m_reconnectPending(false), + m_waitingForPasswordPrompt(false), + m_waitingForHostKeyConfirmation(false), + m_passwordSubmitted(false) { m_connectedProbeTimer->setSingleShot(true); @@ -46,6 +25,10 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) qOverload(&QProcess::finished), this, &SshSessionBackend::onProcessFinished); + connect(m_process, + &QProcess::readyReadStandardOutput, + this, + &SshSessionBackend::onReadyReadStandardOutput); connect(m_process, &QProcess::readyReadStandardError, this, @@ -62,7 +45,6 @@ SshSessionBackend::~SshSessionBackend() m_process->kill(); m_process->waitForFinished(500); } - cleanupAskPassScript(); } void SshSessionBackend::connectSession(const SessionConnectOptions& options) @@ -75,6 +57,10 @@ void SshSessionBackend::connectSession(const SessionConnectOptions& options) m_userInitiatedDisconnect = false; m_reconnectPending = false; m_lastRawError.clear(); + m_activeOptions = options; + m_waitingForPasswordPrompt = false; + m_waitingForHostKeyConfirmation = false; + m_passwordSubmitted = false; if (!startSshProcess(options)) { return; @@ -123,6 +109,35 @@ void SshSessionBackend::reconnectSession(const SessionConnectOptions& options) m_process->terminate(); } +void SshSessionBackend::sendInput(const QString& input) +{ + if (m_process->state() != QProcess::Running) { + emit eventLogged(QStringLiteral("Input ignored: session is not running.")); + return; + } + + if (input.isEmpty()) { + return; + } + + m_process->write(input.toUtf8()); +} + +void SshSessionBackend::confirmHostKey(bool trustHost) +{ + if (m_process->state() != QProcess::Running || !m_waitingForHostKeyConfirmation) { + return; + } + + m_waitingForHostKeyConfirmation = false; + const QString response = trustHost ? QStringLiteral("yes\n") : QStringLiteral("no\n"); + m_process->write(response.toUtf8()); + + emit eventLogged(trustHost + ? QStringLiteral("Host key accepted by user.") + : QStringLiteral("Host key rejected by user.")); +} + void SshSessionBackend::onProcessStarted() { emit eventLogged(QStringLiteral("ssh process started.")); @@ -146,7 +161,6 @@ void SshSessionBackend::onProcessErrorOccurred(QProcess::ProcessError) void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus) { m_connectedProbeTimer->stop(); - cleanupAskPassScript(); if (m_reconnectPending) { m_reconnectPending = false; @@ -178,6 +192,21 @@ void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus) setState(SessionState::Disconnected, QStringLiteral("SSH session ended.")); } +void SshSessionBackend::onReadyReadStandardOutput() +{ + const QString chunk = QString::fromUtf8(m_process->readAllStandardOutput()); + if (chunk.isEmpty()) { + return; + } + + emit outputReceived(chunk); + + if (m_state == SessionState::Connecting && !m_waitingForHostKeyConfirmation + && !m_waitingForPasswordPrompt) { + setState(SessionState::Connected, QStringLiteral("SSH session established.")); + } +} + void SshSessionBackend::onReadyReadStandardError() { const QString chunk = QString::fromUtf8(m_process->readAllStandardError()); @@ -186,10 +215,40 @@ void SshSessionBackend::onReadyReadStandardError() } m_lastRawError += chunk; + emit outputReceived(chunk); const QStringList lines = chunk.split(QLatin1Char('\n'), Qt::SkipEmptyParts); for (const QString& line : lines) { - emit eventLogged(line.trimmed()); + const QString trimmed = line.trimmed(); + if (!trimmed.isEmpty()) { + emit eventLogged(trimmed); + } + + if (trimmed.contains(QStringLiteral("Are you sure you want to continue connecting"), + Qt::CaseInsensitive) + && !m_waitingForHostKeyConfirmation) { + m_waitingForHostKeyConfirmation = true; + emit eventLogged(QStringLiteral("Awaiting host key confirmation from user.")); + emit hostKeyConfirmationRequested(trimmed); + continue; + } + + if (trimmed.contains(QStringLiteral("password:"), Qt::CaseInsensitive) + && profile().authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0 + && !m_passwordSubmitted) { + if (m_activeOptions.password.isEmpty()) { + const QString message = QStringLiteral("Password prompt received but no password is available."); + setState(SessionState::Failed, message); + emit connectionError(message, trimmed); + return; + } + + m_waitingForPasswordPrompt = false; + m_passwordSubmitted = true; + m_process->write((m_activeOptions.password + QStringLiteral("\n")).toUtf8()); + emit eventLogged(QStringLiteral("Password prompt received; credentials submitted.")); + continue; + } } } @@ -199,7 +258,8 @@ void SshSessionBackend::onConnectedProbeTimeout() return; } - if (m_process->state() == QProcess::Running) { + if (m_process->state() == QProcess::Running && !m_waitingForHostKeyConfirmation + && !m_waitingForPasswordPrompt) { setState(SessionState::Connected, QStringLiteral("SSH session established.")); } } @@ -229,12 +289,9 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) return false; } - cleanupAskPassScript(); - QStringList args; - args << QStringLiteral("-N") << QStringLiteral("-T") << QStringLiteral("-p") - << QString::number(p.port) << QStringLiteral("-o") - << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o") + args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(p.port) + << QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o") << QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o") << QStringLiteral("ServerAliveCountMax=2"); @@ -248,12 +305,12 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) << QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsFileForNullDevice()); } else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) { args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new"); + } else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) { + args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask"); } else { 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."); @@ -265,13 +322,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password") << QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no") << QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1"); - - QString askPassError; - if (!configureAskPass(options, environment, askPassError)) { - setState(SessionState::Failed, askPassError); - emit connectionError(askPassError, askPassError); - return false; - } + m_waitingForPasswordPrompt = true; } else if (p.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) { QString keyPath = options.privateKeyPath.trimmed(); if (keyPath.isEmpty()) { @@ -303,7 +354,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) : QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed()); args << target; - m_process->setProcessEnvironment(environment); + m_process->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); m_process->setProgram(QStringLiteral("ssh")); m_process->setArguments(args); m_process->setProcessChannelMode(QProcess::SeparateChannels); @@ -320,66 +371,6 @@ 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 " << escapedForWindowsEcho(options.password) << "\r\n"; -#else - out << "#!/bin/sh\n"; - out << "printf '%s\\n' '" << escapeForShellSingleQuotes(options.password) << "'\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(); @@ -409,9 +400,6 @@ QString SshSessionBackend::mapSshError(const QString& rawError) const if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) { return QStringLiteral("Required file was not found."); } - if (raw.contains(QStringLiteral("Text file busy"), Qt::CaseInsensitive)) { - return QStringLiteral("Credential helper could not start (text file busy). Retry the connection."); - } if (raw.isEmpty()) { return QStringLiteral("SSH connection failed for an unknown reason."); } diff --git a/src/ssh_session_backend.h b/src/ssh_session_backend.h index 9c92576..c7b50a4 100644 --- a/src/ssh_session_backend.h +++ b/src/ssh_session_backend.h @@ -19,11 +19,14 @@ public slots: void connectSession(const SessionConnectOptions& options) override; void disconnectSession() override; void reconnectSession(const SessionConnectOptions& options) override; + void sendInput(const QString& input) override; + void confirmHostKey(bool trustHost) override; private slots: void onProcessStarted(); void onProcessErrorOccurred(QProcess::ProcessError error); void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onReadyReadStandardOutput(); void onReadyReadStandardError(); void onConnectedProbeTimeout(); @@ -34,15 +37,14 @@ private: bool m_userInitiatedDisconnect; bool m_reconnectPending; 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; }; diff --git a/src/unsupported_session_backend.cpp b/src/unsupported_session_backend.cpp index 3b35757..32436f3 100644 --- a/src/unsupported_session_backend.cpp +++ b/src/unsupported_session_backend.cpp @@ -24,3 +24,12 @@ void UnsupportedSessionBackend::reconnectSession(const SessionConnectOptions& op { connectSession(options); } + +void UnsupportedSessionBackend::sendInput(const QString&) +{ + emit eventLogged(QStringLiteral("Input ignored: protocol backend is not interactive.")); +} + +void UnsupportedSessionBackend::confirmHostKey(bool) +{ +} diff --git a/src/unsupported_session_backend.h b/src/unsupported_session_backend.h index 743f5cc..d50051d 100644 --- a/src/unsupported_session_backend.h +++ b/src/unsupported_session_backend.h @@ -14,6 +14,8 @@ public slots: void connectSession(const SessionConnectOptions& options) override; void disconnectSession() override; void reconnectSession(const SessionConnectOptions& options) override; + void sendInput(const QString& input) override; + void confirmHostKey(bool trustHost) override; }; #endif