@@ -0,0 +1,56 @@
+add_library(QSsh
+ sshsendfacility.cpp
+ sshremoteprocess.cpp
+ sshpacketparser.cpp
+ sshpacket.cpp
+ sshoutgoingpacket.cpp
+ sshkeygenerator.cpp
+ sshkeyexchange.cpp
+ sshincomingpacket.cpp
+ sshcryptofacility.cpp
+ sshconnection.cpp
+ sshchannelmanager.cpp
+ sshchannel.cpp
+ sshcapabilities.cpp
+ sftppacket.cpp
+ sftpoutgoingpacket.cpp
+ sftpoperation.cpp
+ sftpincomingpacket.cpp
+ sftpdefs.cpp
+ sftpchannel.cpp
+ sshremoteprocessrunner.cpp
+ sshconnectionmanager.cpp
+ sshkeypasswordretriever.cpp
+ sftpfilesystemmodel.cpp
+ sshdirecttcpiptunnel.cpp
+ sshhostkeydatabase.cpp
+ sshlogging.cpp
+ sshtcpipforwardserver.cpp
+ sshtcpiptunnel.cpp
+ sshforwardedtcpiptunnel.cpp
+ sshagent.cpp
+ sshx11channel.cpp
+ sshx11inforetriever.cpp
+ opensshkeyfilereader.cpp
+ qssh.qrc)
+
+get_property(BOTAN_LIB GLOBAL PROPERTY BOTAN_LIB)
+target_link_libraries( QSsh Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ${BOTAN_LIB})
+# state that anybody linking to us needs to include the current source dir
+target_include_directories(QSsh
+ INTERFACE
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
+ $<INSTALL_INTERFACE:include>
+ $<INSTALL_INTERFACE:include/qssh>
+ )
+#INSTALL RULES
+install(
+ DIRECTORY .
+ DESTINATION include
+ COMPONENT headers
+ FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
+)
+install(TARGETS QSsh DESTINATION lib EXPORT QSsh-targets)
@@ -0,0 +1,203 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+** This file is part of Qt Creator.
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+****************************************************************************/
+#include "opensshkeyfilereader_p.h"
+#include "sshcapabilities_p.h"
+#include "ssherrors.h"
+#include "sshexception_p.h"
+#include "sshlogging_p.h"
+#include "sshpacketparser_p.h"
+#include "ssh_global.h"
+#include <botan/dl_group.h>
+#include <botan/dsa.h>
+#include <botan/ecdsa.h>
+#include <botan/rsa.h>
+#include <memory>
+namespace QSsh {
+namespace Internal {
+using namespace Botan;
+bool OpenSshKeyFileReader::parseKey(const QByteArray &privKeyFileContents)
+{
+ static const QByteArray magicPrefix = "-----BEGIN OPENSSH PRIVATE KEY-----\n";
+ static const QByteArray magicSuffix = "-----END OPENSSH PRIVATE KEY-----\n";
+ if (!privKeyFileContents.startsWith(magicPrefix)) {
+ qCDebug(sshLog) << "not an OpenSSH key file: prefix does not match";
+ return false;
+ }
+ if (!privKeyFileContents.endsWith(magicSuffix))
+ throwException(SSH_TR("Unexpected end-of-file marker."));
+ const QByteArray payload = QByteArray::fromBase64
+ (privKeyFileContents.mid(magicPrefix.size(), privKeyFileContents.size()
+ - magicPrefix.size() - magicSuffix.size()));
+ doParse(payload);
+ return true;
+}
+std::unique_ptr<Private_Key> OpenSshKeyFileReader::privateKey() const
+ if (m_keyType == SshCapabilities::PubKeyRsa) {
+ QSSH_ASSERT_AND_RETURN_VALUE(m_parameters.size() == 5, nullptr);
+ const BigInt &e = m_parameters.at(0);
+ const BigInt &n = m_parameters.at(1);
+ const BigInt &p = m_parameters.at(2);
+ const BigInt &q = m_parameters.at(3);
+ const BigInt &d = m_parameters.at(4);
+ return std::make_unique<RSA_PrivateKey>(p, q, e, d, n);
+ } else if (m_keyType == SshCapabilities::PubKeyDss) {
+ const BigInt &p = m_parameters.at(0);
+ const BigInt &q = m_parameters.at(1);
+ const BigInt &g = m_parameters.at(2);
+ const BigInt &x = m_parameters.at(4);
+ return std::make_unique<DSA_PrivateKey>(m_rng, DL_Group(p, q, g), x);
+ } else if (m_keyType.startsWith(SshCapabilities::PubKeyEcdsaPrefix)) {
+ QSSH_ASSERT_AND_RETURN_VALUE(m_parameters.size() == 1, nullptr);
+ const BigInt &value = m_parameters.first();
+ const EC_Group group(SshCapabilities::oid(m_keyType));
+ return std::make_unique<ECDSA_PrivateKey>(m_rng, group, value);
+ QSSH_ASSERT_AND_RETURN_VALUE(false, nullptr);
+QList<BigInt> OpenSshKeyFileReader::publicParameters() const
+ if (m_keyType == SshCapabilities::PubKeyRsa)
+ return m_parameters.mid(0, 2);
+ if (m_keyType == SshCapabilities::PubKeyDss)
+ return m_parameters.mid(0, 4);
+ if (m_keyType.startsWith(SshCapabilities::PubKeyEcdsaPrefix))
+ return QList<BigInt>();
+ QSSH_ASSERT_AND_RETURN_VALUE(false, QList<BigInt>());
+void OpenSshKeyFileReader::doParse(const QByteArray &payload)
+ // See PROTOCOL.key in OpenSSH sources.
+ static const QByteArray magicString = "openssh-key-v1";
+ if (!payload.startsWith(magicString))
+ throwException(SSH_TR("Unexpected magic string."));
+ try {
+ quint32 offset = magicString.size() + 1; // null byte
+ m_cipherName = SshPacketParser::asString(payload, &offset);
+ qCDebug(sshLog) << "cipher:" << m_cipherName;
+ m_kdf = SshPacketParser::asString(payload, &offset);
+ qCDebug(sshLog) << "kdf:" << m_kdf;
+ parseKdfOptions(SshPacketParser::asString(payload, &offset));
+ const quint32 keyCount = SshPacketParser::asUint32(payload, &offset);
+ if (keyCount != 1) {
+ qCWarning(sshLog) << "more than one key found in OpenSSH private key file, ignoring "
+ "all but the first one";
+ for (quint32 i = 0; i < keyCount; ++i) // Skip the public key blob(s).
+ SshPacketParser::asString(payload, &offset);
+ m_privateKeyList = SshPacketParser::asString(payload, &offset);
+ decryptPrivateKeyList();
+ parsePrivateKeyList();
+ } catch (const SshPacketParseException &) {
+ throwException(SSH_TR("Parse error."));
+ } catch (const Exception &e) {
+ throwException(QString::fromLocal8Bit(e.what()));
+void OpenSshKeyFileReader::parseKdfOptions(const QByteArray &kdfOptions)
+ if (m_cipherName == "none")
+ return;
+ quint32 offset = 0;
+ m_salt = SshPacketParser::asString(kdfOptions, &offset);
+ if (m_salt.size() != 16)
+ throwException(SSH_TR("Invalid salt size %1.").arg(m_salt.size()));
+ m_rounds = SshPacketParser::asUint32(kdfOptions, &offset);
+ qCDebug(sshLog) << "salt:" << m_salt.toHex();
+ qCDebug(sshLog) << "rounds:" << m_rounds;
+void OpenSshKeyFileReader::decryptPrivateKeyList()
+ if (m_kdf != "bcrypt") {
+ throwException(SSH_TR("Unexpected key derivation function '%1'.")
+ .arg(QString::fromLatin1(m_kdf)));
+ // OpenSSH uses a proprietary algorithm for the key derivation. We'd basically have to
+ // copy the code.
+ // TODO: If the lower-level operations (hashing primitives, blowfish stuff) can be taken
+ // over by Botan, that might be feasible. Investigate.
+ throwException(SSH_TR("Encrypted keys are currently not supported in this format."));
+void OpenSshKeyFileReader::parsePrivateKeyList()
+ const quint32 checkInt1 = SshPacketParser::asUint32(m_privateKeyList, &offset);
+ const quint32 checkInt2 = SshPacketParser::asUint32(m_privateKeyList, &offset);
+ if (checkInt1 != checkInt2)
+ throwException(SSH_TR("Verification failed."));
+ m_keyType = SshPacketParser::asString(m_privateKeyList, &offset);
+ qCDebug(sshLog) << "key type:" << m_keyType;
+ const BigInt n = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ const BigInt e = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ const BigInt d = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ SshPacketParser::asBigInt(m_privateKeyList, &offset); // iqmp
+ const BigInt p = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ const BigInt q = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ m_parameters = QList<BigInt>{e, n, p, q, d};
+ const BigInt g = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ const BigInt y = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ const BigInt x = SshPacketParser::asBigInt(m_privateKeyList, &offset);
+ m_parameters = QList<BigInt>{p, q, g, y, x};
+ SshPacketParser::asString(m_privateKeyList, &offset); // name
+ SshPacketParser::asString(m_privateKeyList, &offset); // pubkey representation
+ m_parameters = {SshPacketParser::asBigInt(m_privateKeyList, &offset)};
+ } else {
+ throwException(SSH_TR("Private key type '%1' is not supported.")
+ .arg(QString::fromLatin1(m_keyType)));
+ const QByteArray comment = SshPacketParser::asString(m_privateKeyList, &offset);
+ qCDebug(sshLog) << "comment:" << comment;
+void OpenSshKeyFileReader::throwException(const QString &reason)
+ throw SshClientException(SshKeyFileError,
+ SSH_TR("Processing OpenSSH private key file failed: %1").arg(reason));
+} // namespace Internal
+} // namespace QSsh
@@ -0,0 +1,73 @@
+#pragma once
+#include <QByteArray>
+#include <QList>
+#include <botan/bigint.h>
+namespace Botan {
+class Private_Key;
+class RandomNumberGenerator;
+class OpenSshKeyFileReader
+public:
+ OpenSshKeyFileReader(Botan::RandomNumberGenerator &rng) : m_rng(rng) {}
+ bool parseKey(const QByteArray &privKeyFileContents);
+ QByteArray keyType() const { return m_keyType; }
+ std::unique_ptr<Botan::Private_Key> privateKey() const;
+ QList<Botan::BigInt> allParameters() const { return m_parameters; }
+ QList<Botan::BigInt> publicParameters() const;
+private:
+ void doParse(const QByteArray &payload);
+ void parseKdfOptions(const QByteArray &kdfOptions);
+ void decryptPrivateKeyList();
+ void parsePrivateKeyList();
+ [[noreturn]] void throwException(const QString &reason);
+ Botan::RandomNumberGenerator &m_rng;
+ QByteArray m_keyType;
+ QList<Botan::BigInt> m_parameters;
+ QByteArray m_cipherName;
+ QByteArray m_kdf;
+ QByteArray m_salt;
+ quint32 m_rounds;
+ QByteArray m_privateKeyList;
+};
@@ -0,0 +1 @@
+LIBS *= -l$$qtLibraryName(QSsh)
@@ -0,0 +1,123 @@
+TEMPLATE = lib
+TARGET = QSsh
+QT += network
+DEFINES += QTCSSH_LIBRARY
+#Enable debug log
+#DEFINES += CREATOR_SSH_DEBUG
+INCLUDEPATH += $$QSSH_PREFIX/include/botan-2/
+DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050F00
+include(../../../qssh.pri)
+win32 {
+ DLLDESTDIR = $$[QT_INSTALL_LIBS]
+!win32-msvc* {
+ QMAKE_CXXFLAGS += -Wextra -pedantic
+DESTDIR = $$IDE_LIBRARY_PATH
+TARGET = $$qtLibraryName($$TARGET)
+CONFIG += shared dll warn_on
+DEFINES += QT_DEPRECATED_WARNINGS
+contains(QT_CONFIG, reduce_exports):CONFIG += hide_symbols
+SOURCES = $$PWD/sshsendfacility.cpp \
+ $$PWD/sshremoteprocess.cpp \
+ $$PWD/sshpacketparser.cpp \
+ $$PWD/sshpacket.cpp \
+ $$PWD/sshoutgoingpacket.cpp \
+ $$PWD/sshkeygenerator.cpp \
+ $$PWD/sshkeyexchange.cpp \
+ $$PWD/sshincomingpacket.cpp \
+ $$PWD/sshcryptofacility.cpp \
+ $$PWD/sshconnection.cpp \
+ $$PWD/sshchannelmanager.cpp \
+ $$PWD/sshchannel.cpp \
+ $$PWD/sshcapabilities.cpp \
+ $$PWD/sftppacket.cpp \
+ $$PWD/sftpoutgoingpacket.cpp \
+ $$PWD/sftpoperation.cpp \
+ $$PWD/sftpincomingpacket.cpp \
+ $$PWD/sftpdefs.cpp \
+ $$PWD/sftpchannel.cpp \
+ $$PWD/sshremoteprocessrunner.cpp \
+ $$PWD/sshconnectionmanager.cpp \
+ $$PWD/sshkeypasswordretriever.cpp \
+ $$PWD/sftpfilesystemmodel.cpp \
+ $$PWD/sshdirecttcpiptunnel.cpp \
+ $$PWD/sshhostkeydatabase.cpp \
+ $$PWD/sshlogging.cpp \
+ $$PWD/sshtcpipforwardserver.cpp \
+ $$PWD/sshtcpiptunnel.cpp \
+ $$PWD/sshforwardedtcpiptunnel.cpp \
+ $$PWD/sshagent.cpp \
+ $$PWD/sshx11channel.cpp \
+ $$PWD/sshx11inforetriever.cpp \
+ $$PWD/opensshkeyfilereader.cpp \
+PUBLIC_HEADERS = \
+ $$PWD/sftpdefs.h \
+ $$PWD/ssherrors.h \
+ $$PWD/sshremoteprocess.h \
+ $$PWD/sftpchannel.h \
+ $$PWD/sshkeygenerator.h \
+ $$PWD/sshremoteprocessrunner.h \
+ $$PWD/sshconnectionmanager.h \
+ $$PWD/sshpseudoterminal.h \
+ $$PWD/sftpfilesystemmodel.h \
+ $$PWD/sshdirecttcpiptunnel.h \
+ $$PWD/sshtcpipforwardserver.h \
+ $$PWD/sshhostkeydatabase.h \
+ $$PWD/sshforwardedtcpiptunnel.h \
+ $$PWD/ssh_global.h \
+ $$PWD/sshconnection.h \
+HEADERS = $$PUBLIC_HEADERS \
+ $$PWD/sshsendfacility_p.h \
+ $$PWD/sshremoteprocess_p.h \
+ $$PWD/sshpacketparser_p.h \
+ $$PWD/sshpacket_p.h \
+ $$PWD/sshoutgoingpacket_p.h \
+ $$PWD/sshkeyexchange_p.h \
+ $$PWD/sshincomingpacket_p.h \
+ $$PWD/sshexception_p.h \
+ $$PWD/sshcryptofacility_p.h \
+ $$PWD/sshconnection_p.h \
+ $$PWD/sshchannelmanager_p.h \
+ $$PWD/sshchannel_p.h \
+ $$PWD/sshcapabilities_p.h \
+ $$PWD/sshbotanconversions_p.h \
+ $$PWD/sftppacket_p.h \
+ $$PWD/sftpoutgoingpacket_p.h \
+ $$PWD/sftpoperation_p.h \
+ $$PWD/sftpincomingpacket_p.h \
+ $$PWD/sftpchannel_p.h \
+ $$PWD/sshkeypasswordretriever_p.h \
+ $$PWD/sshdirecttcpiptunnel_p.h \
+ $$PWD/sshlogging_p.h \
+ $$PWD/sshtcpipforwardserver_p.h \
+ $$PWD/sshtcpiptunnel_p.h \
+ $$PWD/sshforwardedtcpiptunnel_p.h \
+ $$PWD/sshagent_p.h \
+ $$PWD/sshx11channel_p.h \
+ $$PWD/sshx11displayinfo_p.h \
+ $$PWD/sshx11inforetriever_p.h \
+ $$PWD/opensshkeyfilereader_p.h \
+RESOURCES += $$PWD/qssh.qrc
+headers.files = $$PUBLIC_HEADERS
+headers.path = $$[QT_INSTALL_PREFIX]/include/QSsh
+target.path = $$[QT_INSTALL_LIBS]
+INSTALLS += target headers
@@ -0,0 +1,57 @@
+import qbs.base 1.0
+import "../QtcLibrary.qbs" as QtcLibrary
+QtcLibrary {
+ name: "QtcSsh"
+ cpp.defines: base.concat(["QSSH_LIBRARY"])
+ cpp.includePaths: [
+ ".",
+ "..",
+ "../..",
+ buildDirectory
+ ]
+ Depends { name: "cpp" }
+ Depends { name: "Qt"; submodules: ["widgets", "network" ] }
+ Depends { name: "Botan" }
+ files: [
+ "sftpchannel.h", "sftpchannel_p.h", "sftpchannel.cpp",
+ "sftpdefs.cpp", "sftpdefs.h",
+ "sftpincomingpacket.cpp", "sftpincomingpacket_p.h",
+ "sftpoperation.cpp", "sftpoperation_p.h",
+ "sftpoutgoingpacket.cpp", "sftpoutgoingpacket_p.h",
+ "sftppacket.cpp", "sftppacket_p.h",
+ "sshcapabilities_p.h", "sshcapabilities.cpp",
+ "sshchannel.cpp", "sshchannel_p.h",
+ "sshchannelmanager.cpp", "sshchannelmanager_p.h",
+ "sshconnection.h", "sshconnection_p.h", "sshconnection.cpp",
+ "sshconnectionmanager.cpp", "sshconnectionmanager.h",
+ "sshcryptofacility.cpp", "sshcryptofacility_p.h",
+ "sshkeyexchange.cpp", "sshkeyexchange_p.h",
+ "sshkeypasswordretriever_p.h",
+ "sshoutgoingpacket.cpp", "sshoutgoingpacket_p.h",
+ "sshpacket.cpp", "sshpacket_p.h",
+ "sshpacketparser.cpp", "sshpacketparser_p.h",
+ "sshremoteprocess.cpp", "sshremoteprocess.h", "sshremoteprocess_p.h",
+ "sshdirecttcpiptunnel.h", "sshdirecttcpiptunnel_p.h", "sshdirecttcpiptunnel.cpp",
+ "sshremoteprocessrunner.cpp", "sshremoteprocessrunner.h",
+ "sshsendfacility.cpp", "sshsendfacility_p.h",
+ "sshkeypasswordretriever.cpp",
+ "sshkeygenerator.cpp", "sshkeygenerator.h",
+ "sshkeycreationdialog.cpp", "sshkeycreationdialog.h", "sshkeycreationdialog.ui",
+ "sftpfilesystemmodel.cpp", "sftpfilesystemmodel.h",
+ "sshincomingpacket_p.h", "sshincomingpacket.cpp",
+ "ssherrors.h",
+ "sshexception_p.h",
+ "sshpseudoterminal.h",
+ "sshbotanconversions_p.h"
+ ProductModule {
+ Depends { name: "Qt"; submodules: ["widgets", "network"] }
+ cpp.includePaths: [".."]
@@ -0,0 +1,7 @@
+<RCC>
+ <qresource prefix="/ssh">
+ <file>images/dir.png</file>
+ <file>images/help.png</file>
+ <file>images/unknownfile.png</file>
+ </qresource>
+</RCC>
@@ -0,0 +1,1175 @@
+/**************************************************************************
+** This file is part of Qt Creator
+** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/
+** GNU Lesser General Public License Usage
+** This file may be used under the terms of the GNU Lesser General Public
+** License version 2.1 as published by the Free Software Foundation and
+** appearing in the file LICENSE.LGPL included in the packaging of this file.
+** Please review the following information to ensure the GNU Lesser General
+** Public License version 2.1 requirements will be met:
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+** In addition, as a special exception, Nokia gives you certain additional
+** rights. These rights are described in the Nokia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+** Other Usage
+** Alternatively, this file may be used in accordance with the terms and
+** conditions contained in a signed written agreement between you and Nokia.
+**************************************************************************/
+#include "sftpchannel.h"
+#include "sftpchannel_p.h"
+#include "sftpdefs.h"
+#include "sshincomingpacket_p.h"
+#include "sshsendfacility_p.h"
+#include <QDir>
+#include <QFile>
+namespace {
+ const quint32 ProtocolVersion = 3;
+ QString errorMessage(const QString &serverMessage,
+ const QString &alternativeMessage)
+ {
+ return serverMessage.isEmpty() ? alternativeMessage : serverMessage;
+ QString errorMessage(const SftpStatusResponse &response,
+ return response.status == SSH_FX_OK ? QString()
+ : errorMessage(response.errorString, alternativeMessage);
+ bool openFile(QFile *localFile, SftpOverwriteMode mode)
+ if (mode == SftpSkipExisting && localFile->exists())
+ QIODevice::OpenMode openMode = QIODevice::WriteOnly;
+ if (mode == SftpOverwriteExisting)
+ openMode |= QIODevice::Truncate;
+ else if (mode == SftpAppendToExisting)
+ openMode |= QIODevice::Append;
+ return localFile->open(openMode);
+ SftpError sftpStatusToError(const SftpStatusCode status)
+ switch (status) {
+ case SSH_FX_OK:
+ return SftpError::NoError;
+ case SSH_FX_EOF:
+ return SftpError::EndOfFile;
+ case SSH_FX_NO_SUCH_FILE:
+ return SftpError::FileNotFound;
+ case SSH_FX_PERMISSION_DENIED:
+ return SftpError::PermissionDenied;
+ case SSH_FX_BAD_MESSAGE:
+ return SftpError::BadMessage;
+ case SSH_FX_NO_CONNECTION:
+ return SftpError::NoConnection;
+ case SSH_FX_CONNECTION_LOST:
+ return SftpError::ConnectionLost;
+ case SSH_FX_OP_UNSUPPORTED:
+ return SftpError::UnsupportedOperation;
+ case SSH_FX_FAILURE:
+ default:
+ return SftpError::GenericFailure;
+} // anonymous namespace
+//--------------------------------------------------------------------------------------------------
+// SftpChannel
+SftpChannel::SftpChannel(quint32 channelId,
+ Internal::SshSendFacility &sendFacility)
+ : d(new Internal::SftpChannelPrivate(channelId, sendFacility, this))
+ connect(d, &Internal::SftpChannelPrivate::initialized,
+ this, &SftpChannel::initialized, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::channelError,
+ this, &SftpChannel::channelError, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::dataAvailable,
+ this, &SftpChannel::dataAvailable, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::fileInfoAvailable,
+ this, &SftpChannel::fileInfoAvailable, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::finished,
+ this, &SftpChannel::finished, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::closed,
+ this, &SftpChannel::closed, Qt::QueuedConnection);
+ connect(d, &Internal::SftpChannelPrivate::transferProgress,
+ this, &SftpChannel::transferProgress, Qt::QueuedConnection);
+SftpChannel::State SftpChannel::state() const
+ switch (d->channelState()) {
+ case Internal::AbstractSshChannel::Inactive:
+ return Uninitialized;
+ case Internal::AbstractSshChannel::SessionRequested:
+ return Initializing;
+ case Internal::AbstractSshChannel::CloseRequested:
+ return Closing;
+ case Internal::AbstractSshChannel::Closed:
+ return Closed;
+ case Internal::AbstractSshChannel::SessionEstablished:
+ return d->m_sftpState == Internal::SftpChannelPrivate::Initialized
+ ? Initialized : Initializing;
+ Q_ASSERT(!"Oh no, we forgot to handle a channel state!");
+ return Closed; // For the compiler.
+void SftpChannel::initialize()
+ d->requestSessionStart();
+ d->m_sftpState = Internal::SftpChannelPrivate::SubsystemRequested;
+void SftpChannel::closeChannel()
+ d->closeChannel();
+SftpJobId SftpChannel::statFile(const QString &path)
+ return d->createJob(Internal::SftpStatFile::Ptr(
+ new Internal::SftpStatFile(++d->m_nextJobId, path)));
+SftpJobId SftpChannel::listDirectory(const QString &path)
+ return d->createJob(Internal::SftpListDir::Ptr(
+ new Internal::SftpListDir(++d->m_nextJobId, path)));
+SftpJobId SftpChannel::createDirectory(const QString &path)
+ return d->createJob(Internal::SftpMakeDir::Ptr(
+ new Internal::SftpMakeDir(++d->m_nextJobId, path)));
+SftpJobId SftpChannel::removeDirectory(const QString &path)
+ return d->createJob(Internal::SftpRmDir::Ptr(
+ new Internal::SftpRmDir(++d->m_nextJobId, path)));
+SftpJobId SftpChannel::removeFile(const QString &path)
+ return d->createJob(Internal::SftpRm::Ptr(
+ new Internal::SftpRm(++d->m_nextJobId, path)));
+SftpJobId SftpChannel::renameFileOrDirectory(const QString &oldPath,
+ const QString &newPath)
+ return d->createJob(Internal::SftpRename::Ptr(
+ new Internal::SftpRename(++d->m_nextJobId, oldPath, newPath)));
+SftpJobId SftpChannel::createLink(const QString &filePath, const QString &target)
+ return d->createJob(Internal::SftpCreateLink::Ptr(
+ new Internal::SftpCreateLink(++d->m_nextJobId, filePath, target)));
+SftpJobId SftpChannel::createFile(const QString &path, SftpOverwriteMode mode)
+ return d->createJob(Internal::SftpCreateFile::Ptr(
+ new Internal::SftpCreateFile(++d->m_nextJobId, path, mode)));
+SftpJobId SftpChannel::uploadFile(QSharedPointer<QIODevice> device,
+ const QString &remoteFilePath, SftpOverwriteMode mode)
+ if (!device->isOpen() && !device->open(QIODevice::ReadOnly))
+ return SftpInvalidJob;
+ return d->createJob(Internal::SftpUploadFile::Ptr(
+ new Internal::SftpUploadFile(++d->m_nextJobId, remoteFilePath, device, mode)));
+SftpJobId SftpChannel::uploadFile(const QString &localFilePath,
+ QSharedPointer<QFile> localFile(new QFile(localFilePath));
+ if (!localFile->open(QIODevice::ReadOnly))
+ new Internal::SftpUploadFile(++d->m_nextJobId, remoteFilePath, localFile, mode)));
+SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath,
+ const QString &localFilePath, SftpOverwriteMode mode)
+ return d->createJob(Internal::SftpDownload::Ptr(
+ new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, localFile, mode)));
+SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath, QSharedPointer<QIODevice> device)
+ new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, device, SftpOverwriteExisting)));
+SftpJobId SftpChannel::uploadDir(const QString &localDirPath,
+ const QString &remoteParentDirPath)
+ if (state() != Initialized)
+ const QDir localDir(localDirPath);
+ if (!localDir.exists() || !localDir.isReadable())
+ const Internal::SftpUploadDir::Ptr uploadDirOp(
+ new Internal::SftpUploadDir(++d->m_nextJobId));
+ const QString remoteDirPath
+ = remoteParentDirPath + u'/' + localDir.dirName();
+ const Internal::SftpMakeDir::Ptr mkdirOp(
+ new Internal::SftpMakeDir(++d->m_nextJobId, remoteDirPath, uploadDirOp));
+ uploadDirOp->mkdirsInProgress.insert(mkdirOp,
+ Internal::SftpUploadDir::Dir(localDirPath, remoteDirPath));
+ d->createJob(mkdirOp);
+ return uploadDirOp->jobId;
+SftpJobId SftpChannel::downloadDir(const QString &remoteDirPath,
+ const QString &localDirPath, SftpOverwriteMode mode)
+ if (!QDir().mkpath(localDirPath))
+ const Internal::SftpDownloadDir::Ptr downloadDirOp(
+ new Internal::SftpDownloadDir(++d->m_nextJobId, mode));
+ const Internal::SftpListDir::Ptr lsdirOp(
+ new Internal::SftpListDir(++d->m_nextJobId, remoteDirPath, downloadDirOp));
+ downloadDirOp->lsdirsInProgress.insert(lsdirOp,
+ Internal::SftpDownloadDir::Dir(localDirPath, remoteDirPath));
+ d->createJob(lsdirOp);
+ return downloadDirOp->jobId;
+SftpChannel::~SftpChannel()
+ delete d;
+// SftpChannelPrivate
+SftpChannelPrivate::SftpChannelPrivate(quint32 channelId,
+ SshSendFacility &sendFacility, SftpChannel *sftp)
+ : AbstractSshChannel(channelId, sendFacility),
+ m_nextJobId(0), m_sftpState(Inactive), m_sftp(sftp)
+SftpJobId SftpChannelPrivate::createJob(const AbstractSftpOperation::Ptr &job)
+ if (m_sftp->state() != SftpChannel::Initialized)
+ m_jobs.insert(job->jobId, job);
+ sendData(job->initialPacket(m_outgoingPacket).rawData());
+ return job->jobId;
+void SftpChannelPrivate::handleChannelSuccess()
+ if (channelState() == CloseRequested)
+ qCDebug(sshLog, "sftp subsystem initialized");
+ sendData(m_outgoingPacket.generateInit(ProtocolVersion).rawData());
+ m_sftpState = InitSent;
+void SftpChannelPrivate::handleChannelFailure()
+ if (m_sftpState != SubsystemRequested) {
+ throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR,
+ "Unexpected SSH_MSG_CHANNEL_FAILURE packet.");
+ emit channelError(tr("Server could not start SFTP subsystem."));
+ closeChannel();
+void SftpChannelPrivate::handleChannelDataInternal(const QByteArray &data)
+ m_incomingData += data;
+ m_incomingPacket.consumeData(m_incomingData);
+ while (m_incomingPacket.isComplete()) {
+ handleCurrentPacket();
+ m_incomingPacket.clear();
+void SftpChannelPrivate::handleChannelExtendedDataInternal(quint32 type,
+ const QByteArray &data)
+ qCWarning(sshLog, "Unexpected extended data '%s' of type %d on SFTP channel.",
+ data.data(), type);
+void SftpChannelPrivate::handleExitStatus(const SshChannelExitStatus &exitStatus)
+ qCDebug(sshLog, "Remote SFTP service exited with exit code %d", exitStatus.exitStatus);
+ if (channelState() == CloseRequested || channelState() == Closed)
+ emit channelError(tr("The SFTP server finished unexpectedly with exit code %1.")
+ .arg(exitStatus.exitStatus));
+ // Note: According to the specs, the server must close the channel after this happens,
+ // but OpenSSH doesn't do that, so we need to initiate the closing procedure ourselves.
+void SftpChannelPrivate::handleExitSignal(const SshChannelExitSignal &signal)
+ emit channelError(tr("The SFTP server crashed: %1.").arg(signal.error));
+ closeChannel(); // See above.
+void SftpChannelPrivate::handleCurrentPacket()
+ qCDebug(sshLog, "Handling SFTP packet of type %d", m_incomingPacket.type());
+ switch (m_incomingPacket.type()) {
+ case SSH_FXP_VERSION:
+ handleServerVersion();
+ break;
+ case SSH_FXP_HANDLE:
+ handleHandle();
+ case SSH_FXP_NAME:
+ handleName();
+ case SSH_FXP_STATUS:
+ handleStatus();
+ case SSH_FXP_DATA:
+ handleReadData();
+ case SSH_FXP_ATTRS:
+ handleAttrs();
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
+ "Unexpected packet.",
+ tr("Unexpected packet of type %1.").arg(m_incomingPacket.type()));
+void SftpChannelPrivate::handleServerVersion()
+ checkChannelActive();
+ if (m_sftpState != InitSent) {
+ "Unexpected SSH_FXP_VERSION packet.");
+ qCDebug(sshLog, "sftp init received");
+ const quint32 serverVersion = m_incomingPacket.extractServerVersion();
+ if (serverVersion != ProtocolVersion) {
+ emit channelError(tr("Protocol version mismatch: Expected %1, got %2")
+ .arg(serverVersion).arg(ProtocolVersion));
+ m_sftpState = Initialized;
+ emit initialized();
+void SftpChannelPrivate::handleHandle()
+ const SftpHandleResponse &response = m_incomingPacket.asHandleResponse();
+ JobMap::Iterator it = lookupJob(response.requestId);
+ const QSharedPointer<AbstractSftpOperationWithHandle> job
+ = it.value().dynamicCast<AbstractSftpOperationWithHandle>();
+ if (job.isNull()) {
+ "Unexpected SSH_FXP_HANDLE packet.");
+ if (job->state != AbstractSftpOperationWithHandle::OpenRequested) {
+ job->remoteHandle = response.handle;
+ job->state = AbstractSftpOperationWithHandle::Open;
+ switch (it.value()->type()) {
+ case AbstractSftpOperation::ListDir:
+ handleLsHandle(it);
+ case AbstractSftpOperation::CreateFile:
+ handleCreateFileHandle(it);
+ case AbstractSftpOperation::Download:
+ handleGetHandle(it);
+ case AbstractSftpOperation::UploadFile:
+ handlePutHandle(it);
+ Q_ASSERT(!"Oh no, I forgot to handle an SFTP operation type!");
+void SftpChannelPrivate::handleLsHandle(JobMap::Iterator it)
+ SftpListDir::Ptr op = it.value().staticCast<SftpListDir>();
+ sendData(m_outgoingPacket.generateReadDir(op->remoteHandle,
+ op->jobId).rawData());
+void SftpChannelPrivate::handleCreateFileHandle(JobMap::Iterator it)
+ SftpCreateFile::Ptr op = it.value().staticCast<SftpCreateFile>();
+ sendData(m_outgoingPacket.generateCloseHandle(op->remoteHandle,
+void SftpChannelPrivate::handleGetHandle(JobMap::Iterator it)
+ SftpDownload::Ptr op = it.value().staticCast<SftpDownload>();
+ sendData(m_outgoingPacket.generateFstat(op->remoteHandle,
+ op->statRequested = true;
+void SftpChannelPrivate::handlePutHandle(JobMap::Iterator it)
+ SftpUploadFile::Ptr op = it.value().staticCast<SftpUploadFile>();
+ if (op->parentJob && op->parentJob->hasError)
+ sendTransferCloseHandle(op, it.key());
+ // OpenSSH does not implement the RFC's append functionality, so we
+ // have to emulate it.
+ if (op->mode == SftpAppendToExisting) {
+ spawnWriteRequests(it);
+void SftpChannelPrivate::handleStatus()
+ const SftpStatusResponse &response = m_incomingPacket.asStatusResponse();
+ qCDebug(sshLog, "%s: status = %d", Q_FUNC_INFO, response.status);
+ handleLsStatus(it, response);
+ handleGetStatus(it, response);
+ handlePutStatus(it, response);
+ case AbstractSftpOperation::MakeDir:
+ handleMkdirStatus(it, response);
+ case AbstractSftpOperation::StatFile:
+ case AbstractSftpOperation::RmDir:
+ case AbstractSftpOperation::Rm:
+ case AbstractSftpOperation::Rename:
+ case AbstractSftpOperation::CreateLink:
+ handleStatusGeneric(it, response);
+void SftpChannelPrivate::handleStatusGeneric(JobMap::Iterator it,
+ const SftpStatusResponse &response)
+ AbstractSftpOperation::Ptr op = it.value();
+ const QString error = errorMessage(response, tr("Unknown error."));
+ emit finished(op->jobId, sftpStatusToError(response.status), error);
+ m_jobs.erase(it);
+void SftpChannelPrivate::handleMkdirStatus(JobMap::Iterator it,
+ SftpMakeDir::Ptr op = it.value().staticCast<SftpMakeDir>();
+ QSharedPointer<SftpUploadDir> parentJob = op->parentJob;
+ if (parentJob == SftpUploadDir::Ptr()) {
+ if (parentJob->hasError) {
+ typedef QMap<SftpMakeDir::Ptr, SftpUploadDir::Dir>::Iterator DirIt;
+ DirIt dirIt = parentJob->mkdirsInProgress.find(op);
+ Q_ASSERT(dirIt != parentJob->mkdirsInProgress.end());
+ const QString &remoteDir = dirIt.value().remoteDir;
+ if (response.status == SSH_FX_OK) {
+ emit dataAvailable(parentJob->jobId,
+ tr("Created remote directory \"%1\".").arg(remoteDir));
+ } else if (response.status == SSH_FX_FAILURE) {
+ tr("Remote directory \"%1\" already exists.").arg(remoteDir));
+ parentJob->setError();
+ emit finished(parentJob->jobId,
+ sftpStatusToError(response.status),
+ tr("Error creating directory \"%1\": %2")
+ .arg(remoteDir, response.errorString));
+ QDir localDir(dirIt.value().localDir);
+ const QFileInfoList &dirInfos
+ = localDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo &dirInfo : dirInfos) {
+ const QString remoteSubDir = remoteDir + u'/' + dirInfo.fileName();
+ const SftpMakeDir::Ptr mkdirOp(
+ new SftpMakeDir(++m_nextJobId, remoteSubDir, parentJob));
+ parentJob->mkdirsInProgress.insert(mkdirOp,
+ SftpUploadDir::Dir(dirInfo.absoluteFilePath(), remoteSubDir));
+ createJob(mkdirOp);
+ const QFileInfoList &fileInfos = localDir.entryInfoList(QDir::Files);
+ for (const QFileInfo &fileInfo : fileInfos) {
+ QSharedPointer<QFile> localFile(new QFile(fileInfo.absoluteFilePath()));
+ if (!localFile->open(QIODevice::ReadOnly)) {
+ tr("Could not open local file \"%1\": %2")
+ .arg(fileInfo.absoluteFilePath(), localFile->errorString()));
+ const QString remoteFilePath = remoteDir + u'/' + fileInfo.fileName();
+ SftpUploadFile::Ptr uploadFileOp(new SftpUploadFile(++m_nextJobId,
+ remoteFilePath, localFile, SftpOverwriteExisting, parentJob));
+ createJob(uploadFileOp);
+ parentJob->uploadsInProgress.append(uploadFileOp);
+ parentJob->mkdirsInProgress.erase(dirIt);
+ if (parentJob->mkdirsInProgress.isEmpty()
+ && parentJob->uploadsInProgress.isEmpty())
+ emit finished(parentJob->jobId);
+void SftpChannelPrivate::handleLsStatus(JobMap::Iterator it,
+ if (op->parentJob && op->parentJob->hasError) {
+ switch (op->state) {
+ case SftpListDir::OpenRequested:
+ reportRequestError(op, sftpStatusToError(response.status), errorMessage(response.errorString,
+ tr("Remote directory could not be opened for reading.")));
+ case SftpListDir::Open:
+ if (response.status != SSH_FX_EOF)
+ reportRequestError(op,
+ errorMessage(response.errorString,
+ tr("Failed to list remote directory contents.")));
+ op->state = SftpListDir::CloseRequested;
+ case SftpListDir::CloseRequested:
+ if (op->hasError || (op->parentJob && op->parentJob->hasError)) {
+ const QString error = errorMessage(response,
+ tr("Failed to close remote directory."));
+ if (op->parentJob) {
+ if (!error.isEmpty()) {
+ op->parentJob->setError();
+ if (op->parentJob->hasError) {
+ emit finished(op->parentJob->jobId, sftpStatusToError(response.status), error);
+ op->parentJob->lsdirsInProgress.remove(op);
+ if (op->parentJob->lsdirsInProgress.isEmpty() &&
+ op->parentJob->downloadsInProgress.isEmpty()) {
+ emit finished(op->parentJob->jobId);
+ "Unexpected SSH_FXP_STATUS packet.");
+void SftpChannelPrivate::handleGetStatus(JobMap::Iterator it,
+ case SftpDownload::OpenRequested:
+ tr("Failed to open remote file for reading.")));
+ case SftpDownload::Open:
+ if (op->statRequested) {
+ tr("Failed to retrieve information on the remote file ('stat' failed).")));
+ sendTransferCloseHandle(op, response.requestId);
+ if ((response.status != SSH_FX_EOF || response.requestId != op->eofId)
+ && !op->hasError)
+ tr("Failed to read remote file.")));
+ finishTransferRequest(it);
+ case SftpDownload::CloseRequested:
+ Q_ASSERT(op->inFlightCount == 1);
+ if (!op->hasError) {
+ op->parentJob->downloadsInProgress.removeOne(op);
+ if (op->parentJob->lsdirsInProgress.isEmpty()
+ && op->parentJob->downloadsInProgress.isEmpty())
+ emit finished(op->jobId);
+ const QString error = errorMessage(response.errorString,
+ tr("Failed to close remote file."));
+ reportRequestError(op, sftpStatusToError(response.status), error);
+ removeTransferRequest(it);
+void SftpChannelPrivate::handlePutStatus(JobMap::Iterator it,
+ SftpUploadFile::Ptr job = it.value().staticCast<SftpUploadFile>();
+ switch (job->state) {
+ case SftpUploadFile::OpenRequested: {
+ bool emitError = false;
+ if (job->parentJob) {
+ if (!job->parentJob->hasError) {
+ job->parentJob->setError();
+ emitError = true;
+ if (emitError) {
+ emit finished(job->jobId,
+ tr("Failed to open remote file for writing.")));
+ case SftpUploadFile::Open:
+ if (job->hasError || (job->parentJob && job->parentJob->hasError)) {
+ job->hasError = true;
+ sendWriteRequest(it);
+ if (job->parentJob)
+ reportRequestError(job, sftpStatusToError(response.status), errorMessage(response.errorString,
+ tr("Failed to write remote file.")));
+ case SftpUploadFile::CloseRequested:
+ Q_ASSERT(job->inFlightCount == 1);
+ job->parentJob->uploadsInProgress.removeOne(job);
+ if (job->parentJob->mkdirsInProgress.isEmpty()
+ && job->parentJob->uploadsInProgress.isEmpty())
+ emit finished(job->parentJob->jobId);
+ emit finished(job->jobId);
+ emit finished(job->parentJob->jobId, sftpStatusToError(response.status), error);
+ emit finished(job->jobId, sftpStatusToError(response.status), error);
+void SftpChannelPrivate::handleName()
+ const SftpNameResponse &response = m_incomingPacket.asNameResponse();
+ case AbstractSftpOperation::ListDir: {
+ if (op->state != SftpListDir::Open) {
+ "Unexpected SSH_FXP_NAME packet.");
+ QList<SftpFileInfo> fileInfoList;
+ for (int i = 0; i < response.files.count(); ++i) {
+ const SftpFile &file = response.files.at(i);
+ SftpFileInfo fileInfo;
+ fileInfo.name = file.fileName;
+ attributesToFileInfo(file.attributes, fileInfo);
+ fileInfoList << fileInfo;
+ handleDownloadDir(op, fileInfoList);
+ emit fileInfoAvailable(op->jobId, fileInfoList);
+void SftpChannelPrivate::handleReadData()
+ const SftpDataResponse &response = m_incomingPacket.asDataResponse();
+ if (it.value()->type() != AbstractSftpOperation::Download) {
+ "Unexpected SSH_FXP_DATA packet.");
+ if (op->hasError) {
+ if (!op->localFile->isOpen()) {
+ QFile *fileDevice = qobject_cast<QFile*>(op->localFile.data());
+ if (fileDevice){
+ if (!Internal::openFile(fileDevice, op->mode)) {
+ reportRequestError(op, SftpError::GenericFailure, tr("Cannot open file ") + fileDevice->fileName());
+ reportRequestError(op, SftpError::GenericFailure, tr("File to upload is not open"));
+ if (!op->localFile->seek(op->offsets[response.requestId])) {
+ reportRequestError(op, SftpError::GenericFailure, op->localFile->errorString());
+ if (op->localFile->write(response.data) != response.data.size()) {
+ emit transferProgress(op->jobId, op->localFile->pos(), op->fileSize);
+ if (op->offset >= op->fileSize && op->fileSize != 0)
+ else
+ sendReadRequest(op, response.requestId);
+void SftpChannelPrivate::handleAttrs()
+ const SftpAttrsResponse &response = m_incomingPacket.asAttrsResponse();
+ SftpStatFile::Ptr statOp = it.value().dynamicCast<SftpStatFile>();
+ if (statOp) {
+ fileInfo.name = QFileInfo(statOp->path).fileName();
+ attributesToFileInfo(response.attrs, fileInfo);
+ emit fileInfoAvailable(it.key(), QList<SftpFileInfo>() << fileInfo);
+ emit finished(it.key());
+ AbstractSftpTransfer::Ptr transfer
+ = it.value().dynamicCast<AbstractSftpTransfer>();
+ if (!transfer || transfer->state != AbstractSftpTransfer::Open
+ || !transfer->statRequested) {
+ "Unexpected SSH_FXP_ATTRS packet.");
+ Q_ASSERT(transfer->type() == AbstractSftpOperation::UploadFile
+ || transfer->type() == AbstractSftpOperation::Download);
+ if (transfer->type() == AbstractSftpOperation::Download) {
+ SftpDownload::Ptr op = transfer.staticCast<SftpDownload>();
+ if (response.attrs.sizePresent) {
+ op->fileSize = response.attrs.size;
+ op->fileSize = 0;
+ op->eofId = op->jobId;
+ op->statRequested = false;
+ emit transferProgress(op->jobId, op->offset, op->fileSize);
+ spawnReadRequests(op);
+ SftpUploadFile::Ptr op = transfer.staticCast<SftpUploadFile>();
+ op->hasError = true;
+ sendTransferCloseHandle(op, op->jobId);
+ op->offset = response.attrs.size;
+ if (op->parentJob)
+ reportRequestError(op, SftpError::UnsupportedOperation, tr("Cannot append to remote file: "
+ "Server does not support the file size attribute."));
+void SftpChannelPrivate::handleDownloadDir(SftpListDir::Ptr op,
+ const QList<SftpFileInfo> &fileInfoList)
+ for (SftpFileInfo fileInfo : fileInfoList) {
+ Internal::SftpDownloadDir::Dir dir = op->parentJob->lsdirsInProgress[op];
+ QString fullPathRemote = QDir(dir.remoteDir).path() + u'/' + fileInfo.name;
+ QString fullPathLocal = QDir(dir.localDir).path() + u'/' + fileInfo.name;
+ if (fileInfo.type == FileTypeRegular) {
+ QSharedPointer<QFile> localFile(new QFile(fullPathLocal));
+ Internal::SftpDownload::Ptr downloadJob = Internal::SftpDownload::Ptr(
+ new Internal::SftpDownload(++m_nextJobId, fullPathRemote, localFile,
+ op->parentJob->mode, op->parentJob));
+ op->parentJob->downloadsInProgress.append(downloadJob);
+ createJob(downloadJob);
+ } else if (fileInfo.type == FileTypeDirectory) {
+ if (fileInfo.name == u"." || fileInfo.name == u"..") {
+ continue;
+ if (!QDir().mkpath(fullPathLocal)) {
+ reportRequestError(op, SftpError::GenericFailure, tr("Cannot create directory ") + fullPathLocal);
+ Internal::SftpListDir::Ptr lsdir = Internal::SftpListDir::Ptr(
+ new Internal::SftpListDir(++m_nextJobId, fullPathRemote, op->parentJob));
+ op->parentJob->lsdirsInProgress.insert(lsdir,
+ Internal::SftpDownloadDir::Dir(fullPathLocal, fullPathRemote));
+ createJob(lsdir);
+ // andres.pagliano TODO handle?
+SftpChannelPrivate::JobMap::Iterator SftpChannelPrivate::lookupJob(SftpJobId id)
+ JobMap::Iterator it = m_jobs.find(id);
+ if (it == m_jobs.end()) {
+ "Invalid request id in SFTP packet.");
+ return it;
+void SftpChannelPrivate::closeHook()
+ for (JobMap::ConstIterator it = m_jobs.constBegin(); it != m_jobs.constEnd(); ++it)
+ emit finished(it.key(), SftpError::EndOfFile, tr("SFTP channel closed unexpectedly."));
+ m_jobs.clear();
+ m_incomingData.clear();
+ emit closed();
+void SftpChannelPrivate::handleOpenSuccessInternal()
+ qCDebug(sshLog, "SFTP session started");
+ m_sendFacility.sendSftpPacket(remoteChannel());
+ m_sftpState = SubsystemRequested;
+void SftpChannelPrivate::handleOpenFailureInternal(const QString &reason)
+ if (channelState() != SessionRequested) {
+ "Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE packet.");
+ emit channelError(tr("Server could not start session: %1").arg(reason));
+void SftpChannelPrivate::sendReadRequest(const SftpDownload::Ptr &job,
+ quint32 requestId)
+ Q_ASSERT(job->eofId == SftpInvalidJob);
+ sendData(m_outgoingPacket.generateReadFile(job->remoteHandle, job->offset,
+ AbstractSftpPacket::MaxDataSize, requestId).rawData());
+ job->offsets[requestId] = job->offset;
+ job->offset += AbstractSftpPacket::MaxDataSize;
+ if (job->offset >= job->fileSize)
+ job->eofId = requestId;
+void SftpChannelPrivate::reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job,
+ const SftpError errorType,
+ const QString &error)
+ // andres.pagliano TODO refactor
+ // Report list error during download dir
+ SftpListDir::Ptr lsjob = job.dynamicCast<SftpListDir>();
+ if (!lsjob.isNull() && lsjob->parentJob) {
+ if (!lsjob->parentJob->hasError) {
+ emit finished(lsjob->parentJob->jobId, errorType, error);
+ lsjob->parentJob->hasError = true;
+ // Report download error during recursive download dir
+ SftpDownload::Ptr djob = job.dynamicCast<SftpDownload>();
+ if (!djob.isNull() && djob->parentJob) {
+ if (!djob->parentJob->hasError) {
+ emit finished(djob->parentJob->jobId, errorType, error);
+ djob->parentJob->hasError = true;
+ // Other error
+ emit finished(job->jobId, errorType, error);
+void SftpChannelPrivate::finishTransferRequest(JobMap::Iterator it)
+ AbstractSftpTransfer::Ptr job = it.value().staticCast<AbstractSftpTransfer>();
+ if (job->inFlightCount == 1)
+ sendTransferCloseHandle(job, it.key());
+void SftpChannelPrivate::sendTransferCloseHandle(const AbstractSftpTransfer::Ptr &job,
+ sendData(m_outgoingPacket.generateCloseHandle(job->remoteHandle,
+ requestId).rawData());
+ job->state = SftpDownload::CloseRequested;
+void SftpChannelPrivate::attributesToFileInfo(const SftpFileAttributes &attributes,
+ SftpFileInfo &fileInfo) const
+ if (attributes.sizePresent) {
+ fileInfo.sizeValid = true;
+ fileInfo.size = attributes.size;
+ if (attributes.permissionsPresent) {
+ if (attributes.permissions & 0x8000) // S_IFREG
+ fileInfo.type = FileTypeRegular;
+ else if (attributes.permissions & 0x4000) // S_IFDIR
+ fileInfo.type = FileTypeDirectory;
+ fileInfo.type = FileTypeOther;
+ fileInfo.permissionsValid = true;
+ fileInfo.permissions = {};
+ if (attributes.timesPresent) {
+ fileInfo.atime = attributes.atime;
+ fileInfo.mtime = attributes.mtime;
+ fileInfo.timestampsValid = true;
+ if (attributes.permissions & 00001) // S_IXOTH
+ fileInfo.permissions |= QFile::ExeOther;
+ if (attributes.permissions & 00002) // S_IWOTH
+ fileInfo.permissions |= QFile::WriteOther;
+ if (attributes.permissions & 00004) // S_IROTH
+ fileInfo.permissions |= QFile::ReadOther;
+ if (attributes.permissions & 00010) // S_IXGRP
+ fileInfo.permissions |= QFile::ExeGroup;
+ if (attributes.permissions & 00020) // S_IWGRP
+ fileInfo.permissions |= QFile::WriteGroup;
+ if (attributes.permissions & 00040) // S_IRGRP
+ fileInfo.permissions |= QFile::ReadGroup;
+ if (attributes.permissions & 00100) // S_IXUSR
+ fileInfo.permissions |= QFile::ExeUser | QFile::ExeOwner;
+ if (attributes.permissions & 00200) // S_IWUSR
+ fileInfo.permissions |= QFile::WriteUser | QFile::WriteOwner;
+ if (attributes.permissions & 00400) // S_IRUSR
+ fileInfo.permissions |= QFile::ReadUser | QFile::ReadOwner;
+void SftpChannelPrivate::removeTransferRequest(JobMap::Iterator it)
+ --it.value().staticCast<AbstractSftpTransfer>()->inFlightCount;
+void SftpChannelPrivate::sendWriteRequest(JobMap::Iterator it)
+ emit transferProgress(job->jobId, job->localFile->pos(), job->localFile->size());
+ QByteArray data = job->localFile->read(AbstractSftpPacket::MaxDataSize);
+ QFileDevice *fileDevice = qobject_cast<QFileDevice*>(job->localFile.data());
+ if (fileDevice && fileDevice->error() != QFileDevice::NoError) {
+ reportRequestError(job, SftpError::GenericFailure, tr("Error reading local file: %1")
+ .arg(job->localFile->errorString()));
+ } else if (data.isEmpty()) {
+ sendData(m_outgoingPacket.generateWriteFile(job->remoteHandle,
+ job->offset, data, it.key()).rawData());
+void SftpChannelPrivate::spawnWriteRequests(JobMap::Iterator it)
+ op->calculateInFlightCount(AbstractSftpPacket::MaxDataSize);
+ for (int i = 1; !op->hasError && i < op->inFlightCount; ++i)
+ sendWriteRequest(m_jobs.insert(++m_nextJobId, op));
+void SftpChannelPrivate::spawnReadRequests(const SftpDownload::Ptr &job)
+ job->calculateInFlightCount(AbstractSftpPacket::MaxDataSize);
+ sendReadRequest(job, job->jobId);
+ for (int i = 1; i < job->inFlightCount; ++i) {
+ const quint32 requestId = ++m_nextJobId;
+ m_jobs.insert(requestId, job);
+ sendReadRequest(job, requestId);
@@ -0,0 +1,263 @@
+#ifndef SFTCHANNEL_H
+#define SFTCHANNEL_H
+#include <QObject>
+#include <QSharedPointer>
+#include <QString>
+class SftpChannelPrivate;
+class SshChannelManager;
+class SshSendFacility;
+/*!
+ \class QSsh::SftpChannel
+ \brief This class provides SFTP operations.
+ Objects are created via SshConnection::createSftpChannel().
+ The channel needs to be initialized with
+ a call to initialize() and is closed via closeChannel(). After closing
+ a channel, no more operations are possible. It cannot be re-opened
+ using initialize(); use SshConnection::createSftpChannel() if you need
+ a new one.
+ After the initialized() signal has been emitted, operations can be started.
+ All SFTP operations are asynchronous (non-blocking) and can be in-flight
+ simultaneously (though callers must ensure that concurrently running jobs
+ are independent of each other, e.g. they must not write to the same file).
+ Operations are identified by their job id, which is returned by
+ the respective member function. If the function can right away detect that
+ the operation cannot succeed, it returns SftpInvalidJob. If an error occurs
+ later, the finished() signal is emitted for the respective job with a
+ non-empty error string.
+ Note that directory names must not have a trailing slash.
+*/
+class QSSH_EXPORT SftpChannel : public QObject
+ Q_OBJECT
+ friend class Internal::SftpChannelPrivate;
+ friend class Internal::SshChannelManager;
+ /// Convenience typedef
+ typedef QSharedPointer<SftpChannel> Ptr;
+ /// \see state
+ enum State { Uninitialized, Initializing, Initialized, Closing, Closed };
+ /// Current state of this channel
+ State state() const;
+ /*!
+ * @brief Makes this channel ready to use.
+ */
+ void initialize();
+ * @brief Call this when you are done with the channel.
+ void closeChannel();
+ * \brief Get information about a remote path, file or directory
+ * \param path Remote path to state
+ * \return A unique ID identifying this job
+ SftpJobId statFile(const QString &path);
+ * \brief Get list of contents of a directory
+ * \param dirPath Remote path of directory
+ SftpJobId listDirectory(const QString &dirPath);
+ * \brief Create remote directory
+ SftpJobId createDirectory(const QString &dirPath);
+ * \brief Remove remote directory
+ SftpJobId removeDirectory(const QString &dirPath);
+ * \brief Remove remote file
+ * \param filePath Remote path of file
+ SftpJobId removeFile(const QString &filePath);
+ * \brief Rename or move a remote file or directory
+ * \param oldPath Path of existing file or directory
+ * \param newPath New path the file or directory should be available as
+ SftpJobId renameFileOrDirectory(const QString &oldPath,
+ const QString &newPath);
+ * \brief Create a new empty file.
+ * \param filePath Remote path of the file.
+ * \param mode The behavior if the file already exists.
+ SftpJobId createFile(const QString &filePath, SftpOverwriteMode mode);
+ * \brief Creates a symbolic link pointing to another file.
+ * \param filePath The path of the symbolic
+ * \param target The path the symbolic link should point to
+ SftpJobId createLink(const QString &filePath, const QString &target);
+ * \brief Creates a remote file and fills it with data from \a device
+ * \param device If this is not open already it will be opened in \a QIODevice::ReadOnly mode
+ * \param remoteFilePath The path on the server to upload the file to
+ * \param mode #QSsh::SftpOverwriteMode defines the behavior if the file already exists
+ SftpJobId uploadFile(QSharedPointer<QIODevice> device,
+ const QString &remoteFilePath, SftpOverwriteMode mode);
+ * \brief Uploads a local file to the remote host.
+ * \param localFilePath The local path to an existing file
+ * \param remoteFilePath The remote path the file should be uploaded to
+ * \param mode What it will do if the file already exists
+ SftpJobId uploadFile(const QString &localFilePath,
+ * \brief Downloads a remote file to a local path
+ * \param remoteFilePath The remote path to the file to be downloaded
+ * \param localFilePath The local path for where to download the file
+ * \param mode Controls what happens if the local file already exists
+ SftpJobId downloadFile(const QString &remoteFilePath,
+ const QString &localFilePath, SftpOverwriteMode mode);
+ * \brief Retrieves the contents of a remote file and writes it to \a device
+ * \param remoteFilePath The remote path of the file to retrieve the contents of
+ * \param device The QIODevice to write the data to, this needs to be open in a writable mode
+ QSharedPointer<QIODevice> device);
+ * \brief Uploads a local directory (recursively) with files to the remote host
+ * \param localDirPath The path to an existing local directory
+ * \param remoteParentDirPath The remote path to upload it to, the name of the local directory will be appended to this
+ SftpJobId uploadDir(const QString &localDirPath,
+ const QString &remoteParentDirPath);
+ * \brief Downloads a remote directory (recursively) to a local path
+ * \param remoteDirPath The remote path of an existing directory to download
+ * \param localDirPath The local path to download the directory to
+ * \param mode
+ * \return
+ SftpJobId downloadDir(const QString &remoteDirPath,
+ const QString &localDirPath, SftpOverwriteMode mode);
+ ~SftpChannel();
+signals:
+ /// Emitted when you can start using the channel
+ void initialized();
+ /// Emitted when an error happened
+ void channelError(const QString &reason);
+ /// Emitted when the channel has closed for some reason, either an error occured or it was asked for.
+ void closed();
+ /// error.isEmpty means it finished successfully
+ void finished(QSsh::SftpJobId job, const SftpError errorType = SftpError::NoError, const QString &error = QString());
+ * Continously emitted during data transfer.
+ * Does not emit for each file copied by uploadDir().
+ void dataAvailable(QSsh::SftpJobId job, const QString &data);
+ * This signal is emitted as a result of:
+ * - statFile() (with the list having exactly one element)
+ * - listDirectory() (potentially more than once)
+ * It will continously be emitted as data is discovered, not only when the job is done.
+ void fileInfoAvailable(QSsh::SftpJobId job, const QList<QSsh::SftpFileInfo> &fileInfoList);
+ * Emitted during upload or download
+ void transferProgress(QSsh::SftpJobId job, quint64 progress, quint64 total);
+ SftpChannel(quint32 channelId, Internal::SshSendFacility &sendFacility);
+ Internal::SftpChannelPrivate *d;
+#endif // SFTPCHANNEL_H
@@ -0,0 +1,135 @@
+#ifndef SFTCHANNEL_P_H
+#define SFTCHANNEL_P_H
+#include "sftpincomingpacket_p.h"
+#include "sftpoperation_p.h"
+#include "sftpoutgoingpacket_p.h"
+#include "sshchannel_p.h"
+#include <QMap>
+class SftpChannel;
+class SftpChannelPrivate : public AbstractSshChannel
+ friend class QSsh::SftpChannel;
+ enum SftpState { Inactive, SubsystemRequested, InitSent, Initialized };
+ typedef QMap<SftpJobId, AbstractSftpOperation::Ptr> JobMap;
+ SftpChannelPrivate(quint32 channelId, SshSendFacility &sendFacility,
+ SftpChannel *sftp);
+ SftpJobId createJob(const AbstractSftpOperation::Ptr &job);
+ virtual void handleChannelSuccess();
+ virtual void handleChannelFailure();
+ virtual void handleOpenSuccessInternal();
+ virtual void handleOpenFailureInternal(const QString &reason);
+ virtual void handleChannelDataInternal(const QByteArray &data);
+ virtual void handleChannelExtendedDataInternal(quint32 type,
+ const QByteArray &data);
+ virtual void handleExitStatus(const SshChannelExitStatus &exitStatus);
+ virtual void handleExitSignal(const SshChannelExitSignal &signal);
+ virtual void closeHook();
+ void handleCurrentPacket();
+ void handleServerVersion();
+ void handleHandle();
+ void handleStatus();
+ void handleName();
+ void handleReadData();
+ void handleAttrs();
+ void handleDownloadDir(SftpListDir::Ptr op, const QList<SftpFileInfo> & fileInfoList);
+ void handleStatusGeneric(JobMap::Iterator it,
+ const SftpStatusResponse &response);
+ void handleMkdirStatus(JobMap::Iterator it,
+ void handleLsStatus(JobMap::Iterator it,
+ void handleGetStatus(JobMap::Iterator it,
+ void handlePutStatus(JobMap::Iterator it,
+ void handleLsHandle(JobMap::Iterator it);
+ void handleCreateFileHandle(JobMap::Iterator it);
+ void handleGetHandle(JobMap::Iterator it);
+ void handlePutHandle(JobMap::Iterator it);
+ void spawnReadRequests(const SftpDownload::Ptr &job);
+ void spawnWriteRequests(JobMap::Iterator it);
+ void sendReadRequest(const SftpDownload::Ptr &job, quint32 requestId);
+ void sendWriteRequest(JobMap::Iterator it);
+ void finishTransferRequest(JobMap::Iterator it);
+ void removeTransferRequest(JobMap::Iterator it);
+ void reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job, const SftpError errorType,
+ const QString &error);
+ void sendTransferCloseHandle(const AbstractSftpTransfer::Ptr &job,
+ quint32 requestId);
+ void attributesToFileInfo(const SftpFileAttributes &attributes, SftpFileInfo &fileInfo) const;
+ JobMap::Iterator lookupJob(SftpJobId id);
+ JobMap m_jobs;
+ SftpOutgoingPacket m_outgoingPacket;
+ SftpIncomingPacket m_incomingPacket;
+ QByteArray m_incomingData;
+ SftpJobId m_nextJobId;
+ SftpState m_sftpState;
+ SftpChannel *m_sftp;
+#endif // SFTPCHANNEL_P_H
@@ -0,0 +1,33 @@
+namespace QSsh { const SftpJobId SftpInvalidJob = 0; }
@@ -0,0 +1,119 @@
+#ifndef SFTPDEFS_H
+#define SFTPDEFS_H
+ * \namespace QSsh
+ * \brief The namespace used for the entire library
+ *\brief Unique ID used for tracking individual jobs.
+typedef quint32 SftpJobId;
+ Special ID representing an invalid job, e. g. if a requested job could not be started.
+QSSH_EXPORT extern const SftpJobId SftpInvalidJob;
+ * \brief The behavior when uploading a file and the remote path already exists
+enum SftpOverwriteMode {
+ /*! Overwrite any existing files */
+ SftpOverwriteExisting,
+ /*! Append new content if the file already exists */
+ SftpAppendToExisting,
+ /*! If the file or directory already exists skip it */
+ SftpSkipExisting
+ * \brief The type of a remote file.
+enum SftpFileType { FileTypeRegular, FileTypeDirectory, FileTypeOther, FileTypeUnknown };
+ * \brief Possible errors.
+enum SftpError { NoError, EndOfFile, FileNotFound, PermissionDenied, GenericFailure, BadMessage, NoConnection, ConnectionLost, UnsupportedOperation };
+ \brief Contains information about a remote file.
+class QSSH_EXPORT SftpFileInfo
+ SftpFileInfo() : type(FileTypeUnknown), sizeValid(false), permissionsValid(false) { }
+ /// The remote file name, only file attribute required by the RFC to be present so this is always set
+ QString name;
+ /// The type of file
+ SftpFileType type = FileTypeUnknown;
+ /// The remote file size in bytes.
+ quint64 size = 0;
+ /// The permissions set on the file, might be empty as the RFC allows an SFTP server not to support any file attributes beyond the name.
+ QFileDevice::Permissions permissions{};
+ /// Last time file was accessed.
+ quint32 atime = 0;
+ /// Last time file was modified.
+ quint32 mtime = 0;
+ /// If the timestamps (\ref atime and \ref mtime) are valid, the RFC allows an SFTP server not to support any file attributes beyond the name.
+ bool timestampsValid = false;
+ /// The RFC allows an SFTP server not to support any file attributes beyond the name.
+ bool sizeValid = false;
+ bool permissionsValid = false;
+#endif // SFTPDEFS_H
@@ -0,0 +1,407 @@
+#include "sftpfilesystemmodel.h"
+#include "sshconnection.h"
+#include "sshconnectionmanager.h"
+#include <QFileInfo>
+#include <QHash>
+#include <QIcon>
+class SftpDirNode;
+class SftpFileNode
+ Q_DISABLE_COPY(SftpFileNode)
+ SftpFileNode() : parent(nullptr) { }
+ virtual ~SftpFileNode() { }
+ QString path;
+ SftpDirNode *parent;
+class SftpDirNode : public SftpFileNode
+ Q_DISABLE_COPY(SftpDirNode)
+ SftpDirNode() : lsState(LsNotYetCalled) { }
+ ~SftpDirNode() { qDeleteAll(children); }
+ enum { LsNotYetCalled, LsRunning, LsFinished } lsState;
+ QList<SftpFileNode *> children;
+typedef QHash<SftpJobId, SftpDirNode *> DirNodeHash;
+SftpFileNode *indexToFileNode(const QModelIndex &index)
+ return static_cast<SftpFileNode *>(index.internalPointer());
+SftpDirNode *indexToDirNode(const QModelIndex &index)
+ SftpFileNode * const fileNode = indexToFileNode(index);
+ QSSH_ASSERT(fileNode);
+ return dynamic_cast<SftpDirNode *>(fileNode);
+class SftpFileSystemModelPrivate
+ SshConnection *sshConnection;
+ SftpChannel::Ptr sftpChannel;
+ QString rootDirectory;
+ SftpFileNode *rootNode;
+ SftpJobId statJobId;
+ DirNodeHash lsOps;
+ QList<SftpJobId> externalJobs;
+using namespace Internal;
+SftpFileSystemModel::SftpFileSystemModel(QObject *parent)
+ : QAbstractItemModel(parent), d(new SftpFileSystemModelPrivate)
+ d->sshConnection = nullptr;
+ d->rootDirectory = QLatin1Char('/');
+ d->rootNode = nullptr;
+ d->statJobId = SftpInvalidJob;
+SftpFileSystemModel::~SftpFileSystemModel()
+ shutDown();
+void SftpFileSystemModel::setSshConnection(const SshConnectionParameters &sshParams)
+ QSSH_ASSERT_AND_RETURN(!d->sshConnection);
+ d->sshConnection = QSsh::acquireConnection(sshParams);
+ connect(d->sshConnection, &SshConnection::error,
+ this, &SftpFileSystemModel::handleSshConnectionFailure);
+ if (d->sshConnection->state() == SshConnection::Connected) {
+ handleSshConnectionEstablished();
+ connect(d->sshConnection, &SshConnection::connected,
+ this, &SftpFileSystemModel::handleSshConnectionEstablished);
+ if (d->sshConnection->state() == SshConnection::Unconnected)
+ d->sshConnection->connectToHost();
+void SftpFileSystemModel::setRootDirectory(const QString &path)
+ beginResetModel();
+ d->rootDirectory = path;
+ delete d->rootNode;
+ d->lsOps.clear();
+ endResetModel();
+ statRootDirectory();
+QString SftpFileSystemModel::rootDirectory() const
+ return d->rootDirectory;
+SftpJobId SftpFileSystemModel::downloadFile(const QModelIndex &index, const QString &targetFilePath)
+ QSSH_ASSERT_AND_RETURN_VALUE(d->rootNode, SftpInvalidJob);
+ const SftpFileNode * const fileNode = indexToFileNode(index);
+ QSSH_ASSERT_AND_RETURN_VALUE(fileNode, SftpInvalidJob);
+ QSSH_ASSERT_AND_RETURN_VALUE(fileNode->fileInfo.type == FileTypeRegular, SftpInvalidJob);
+ const SftpJobId jobId = d->sftpChannel->downloadFile(fileNode->path, targetFilePath,
+ SftpOverwriteExisting);
+ if (jobId != SftpInvalidJob)
+ d->externalJobs << jobId;
+ return jobId;
+int SftpFileSystemModel::columnCount(const QModelIndex &parent) const
+ Q_UNUSED(parent);
+ return 2; // type + name
+QVariant SftpFileSystemModel::data(const QModelIndex &index, int role) const
+ if (!index.internalPointer()) {
+ return QVariant();
+ const SftpFileNode * const node = indexToFileNode(index);
+ if (index.column() == 0 && role == Qt::DecorationRole) {
+ switch (node->fileInfo.type) {
+ case FileTypeRegular:
+ case FileTypeOther:
+ return QIcon(QStringLiteral(":/ssh/images/unknownfile.png"));
+ case FileTypeDirectory:
+ return QIcon(QStringLiteral(":/ssh/images/dir.png"));
+ case FileTypeUnknown:
+ return QIcon(QStringLiteral(":/ssh/images/help.png")); // Shows a question mark.
+ if (index.column() == 1) {
+ if (role == Qt::DisplayRole)
+ return node->fileInfo.name;
+ if (role == PathRole)
+ return node->path;
+Qt::ItemFlags SftpFileSystemModel::flags(const QModelIndex &index) const
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+QVariant SftpFileSystemModel::headerData(int section, Qt::Orientation orientation, int role) const
+ if (orientation != Qt::Horizontal)
+ if (role != Qt::DisplayRole)
+ if (section == 0)
+ return tr("File Type");
+ if (section == 1)
+ return tr("File Name");
+QModelIndex SftpFileSystemModel::index(int row, int column, const QModelIndex &parent) const
+ if (row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent))
+ return QModelIndex();
+ if (!d->rootNode)
+ if (!parent.isValid())
+ return createIndex(row, column, d->rootNode);
+ const SftpDirNode * const parentNode = indexToDirNode(parent);
+ QSSH_ASSERT_AND_RETURN_VALUE(parentNode, QModelIndex());
+ QSSH_ASSERT_AND_RETURN_VALUE(row < parentNode->children.count(), QModelIndex());
+ SftpFileNode * const childNode = parentNode->children.at(row);
+ return createIndex(row, column, childNode);
+QModelIndex SftpFileSystemModel::parent(const QModelIndex &child) const
+ if (!child.isValid()) // Don't assert on this, since the model tester tries it.
+ const SftpFileNode * const childNode = indexToFileNode(child);
+ QSSH_ASSERT_AND_RETURN_VALUE(childNode, QModelIndex());
+ if (childNode == d->rootNode)
+ SftpDirNode * const parentNode = childNode->parent;
+ if (parentNode == d->rootNode)
+ return createIndex(0, 0, d->rootNode);
+ const SftpDirNode * const grandParentNode = parentNode->parent;
+ QSSH_ASSERT_AND_RETURN_VALUE(grandParentNode, QModelIndex());
+ return createIndex(grandParentNode->children.indexOf(parentNode), 0, parentNode);
+int SftpFileSystemModel::rowCount(const QModelIndex &parent) const
+ return 1; // fake it until we make it, otherwise QTreeView isn't happy
+ return 1;
+ if (parent.column() != 0)
+ return 0;
+ SftpDirNode * const dirNode = indexToDirNode(parent);
+ if (!dirNode)
+ if (dirNode->lsState != SftpDirNode::LsNotYetCalled)
+ return dirNode->children.count();
+ d->lsOps.insert(d->sftpChannel->listDirectory(dirNode->path), dirNode);
+ dirNode->lsState = SftpDirNode::LsRunning;
+void SftpFileSystemModel::statRootDirectory()
+ if (!d->sftpChannel) {
+ d->statJobId = d->sftpChannel->statFile(d->rootDirectory);
+void SftpFileSystemModel::shutDown()
+ if (d->sftpChannel) {
+ disconnect(d->sftpChannel.data(), nullptr, this, nullptr);
+ d->sftpChannel->closeChannel();
+ d->sftpChannel.clear();
+ if (d->sshConnection) {
+ disconnect(d->sshConnection, nullptr, this, nullptr);
+ QSsh::releaseConnection(d->sshConnection);
+void SftpFileSystemModel::handleSshConnectionFailure()
+ emit connectionError(d->sshConnection->errorString());
+void SftpFileSystemModel::handleSftpChannelInitialized()
+ connect(d->sftpChannel.data(),
+ &SftpChannel::fileInfoAvailable,
+ this, &SftpFileSystemModel::handleFileInfo);
+ connect(d->sftpChannel.data(), &SftpChannel::finished,
+ this, &SftpFileSystemModel::handleSftpJobFinished);
+void SftpFileSystemModel::handleSshConnectionEstablished()
+ d->sftpChannel = d->sshConnection->createSftpChannel();
+ connect(d->sftpChannel.data(), &SftpChannel::initialized,
+ this, &SftpFileSystemModel::handleSftpChannelInitialized);
+ connect(d->sftpChannel.data(), &SftpChannel::channelError,
+ this, &SftpFileSystemModel::handleSftpChannelError);
+ d->sftpChannel->initialize();
+void SftpFileSystemModel::handleSftpChannelError(const QString &reason)
+ emit connectionError(reason);
+void SftpFileSystemModel::handleFileInfo(SftpJobId jobId, const QList<SftpFileInfo> &fileInfoList)
+ if (jobId == d->statJobId) {
+ QSSH_ASSERT_AND_RETURN(!d->rootNode);
+ beginInsertRows(QModelIndex(), 0, 0);
+ d->rootNode = new SftpDirNode;
+ d->rootNode->path = d->rootDirectory;
+ d->rootNode->fileInfo = fileInfoList.first();
+ d->rootNode->fileInfo.name = d->rootDirectory == QLatin1String("/")
+ ? d->rootDirectory : QFileInfo(d->rootDirectory).fileName();
+ endInsertRows();
+ SftpDirNode * const parentNode = d->lsOps.value(jobId);
+ QSSH_ASSERT_AND_RETURN(parentNode);
+ QList<SftpFileInfo> filteredList;
+ for (const SftpFileInfo &fi : fileInfoList) {
+ if (fi.name != QLatin1String(".") && fi.name != QLatin1String("..")) {
+ filteredList << fi;
+ if (filteredList.isEmpty())
+ if (parentNode->parent) {
+ QModelIndex parentIndex = createIndex(parentNode->parent->children.indexOf(parentNode), 0, parentNode);
+ beginInsertRows(parentIndex, rowCount(parentIndex), rowCount(parentIndex) + filteredList.count());
+ // root node
+ beginInsertRows(QModelIndex(), 0, 1);
+ for (const SftpFileInfo &fileInfo : filteredList) {
+ SftpFileNode *childNode;
+ if (fileInfo.type == FileTypeDirectory) {
+ childNode = new SftpDirNode;
+ childNode = new SftpFileNode;
+ childNode->path = parentNode->path;
+ if (!childNode->path.endsWith(QLatin1Char('/')))
+ childNode->path += QLatin1Char('/');
+ childNode->path += fileInfo.name;
+ childNode->fileInfo = fileInfo;
+ childNode->parent = parentNode;
+ parentNode->children << childNode;
+void SftpFileSystemModel::handleSftpJobFinished(SftpJobId jobId, const SftpError error, const QString &errorMessage)
+ Q_UNUSED(error);
+ if (!errorMessage.isEmpty())
+ emit sftpOperationFailed(tr("Error getting \"stat\" info about \"%1\": %2")
+ .arg(rootDirectory(), errorMessage));
+ DirNodeHash::Iterator it = d->lsOps.find(jobId);
+ if (it != d->lsOps.end()) {
+ QSSH_ASSERT(it.value()->lsState == SftpDirNode::LsRunning);
+ it.value()->lsState = SftpDirNode::LsFinished;
+ emit sftpOperationFailed(tr("Error listing contents of directory \"%1\": %2")
+ .arg(it.value()->path, errorMessage));
+ d->lsOps.erase(it);
+ const int jobIndex = d->externalJobs.indexOf(jobId);
+ QSSH_ASSERT_AND_RETURN(jobIndex != -1);
+ d->externalJobs.removeAt(jobIndex);
+ emit sftpOperationFinished(jobId, errorMessage);
@@ -0,0 +1,107 @@
+#ifndef SFTPFILESYSTEMMODEL_H
+#define SFTPFILESYSTEMMODEL_H
+#include <QAbstractItemModel>
+class SshConnectionParameters;
+namespace Internal { class SftpFileSystemModelPrivate; }
+// Very simple read-only model. Symbolic links are not followed.
+class QSSH_EXPORT SftpFileSystemModel : public QAbstractItemModel
+ explicit SftpFileSystemModel(QObject *parent = nullptr);
+ ~SftpFileSystemModel();
+ /*
+ * Once this is called, an SFTP connection is established and the model is populated.
+ * The effect of additional calls is undefined.
+ void setSshConnection(const SshConnectionParameters &sshParams);
+ void setRootDirectory(const QString &path); // Default is "/".
+ QString rootDirectory() const;
+ SftpJobId downloadFile(const QModelIndex &index, const QString &targetFilePath);
+ // Use this to get the full path of a file or directory.
+ static const int PathRole = Qt::UserRole;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ * E.g. "Permission denied". Note that this can happen without direct user intervention,
+ * due to e.g. the view calling rowCount() on a non-readable directory. This signal should
+ * therefore not result in a message box or similar, since it might occur very often.
+ void sftpOperationFailed(const QString &errorMessage);
+ * This error is not recoverable. The model will not have any content after
+ * the signal has been emitted.
+ void connectionError(const QString &errorMessage);
+ // Success <=> error.isEmpty().
+ void sftpOperationFinished(QSsh::SftpJobId, const QString &error);
+ void handleSshConnectionEstablished();
+ void handleSshConnectionFailure();
+ void handleSftpChannelInitialized();
+ void handleSftpChannelError(const QString &reason);
+ void handleFileInfo(QSsh::SftpJobId jobId, const QList<QSsh::SftpFileInfo> &fileInfoList);
+ void handleSftpJobFinished(QSsh::SftpJobId jobId, const SftpError error, const QString &errorMessage);
+ int columnCount(const QModelIndex &parent = QModelIndex()) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
+ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
+ QModelIndex parent(const QModelIndex &child) const;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ void statRootDirectory();
+ void shutDown();
+ Internal::SftpFileSystemModelPrivate * const d;
+} // namespace QSsh;
+#endif // SFTPFILESYSTEMMODEL_H
@@ -0,0 +1,223 @@
+SftpIncomingPacket::SftpIncomingPacket() : m_length(0)
+void SftpIncomingPacket::consumeData(QByteArray &newData)
+ qCDebug(sshLog, "%s: current data size = %d, new data size = %d", Q_FUNC_INFO,
+ int(m_data.size()), int(newData.size()));
+ if (isComplete() || dataSize() + newData.size() < int(sizeof m_length))
+ if (dataSize() < sizeof m_length) {
+ moveFirstBytes(m_data, newData, sizeof m_length - m_data.size());
+ m_length = SshPacketParser::asUint32(m_data, static_cast<quint32>(0));
+ if (m_length < static_cast<quint32>(TypeOffset + 1)
+ || m_length > MaxPacketSize) {
+ "Invalid length field in SFTP packet.");
+ moveFirstBytes(m_data, newData,
+ qMin<quint32>(m_length - dataSize() + 4, newData.size()));
+void SftpIncomingPacket::moveFirstBytes(QByteArray &target, QByteArray &source,
+ int n)
+ target.append(source.left(n));
+ source.remove(0, n);
+bool SftpIncomingPacket::isComplete() const
+ return m_length == dataSize() - 4;
+void SftpIncomingPacket::clear()
+ m_data.clear();
+ m_length = 0;
+quint32 SftpIncomingPacket::extractServerVersion() const
+ Q_ASSERT(isComplete());
+ Q_ASSERT(type() == SSH_FXP_VERSION);
+ return SshPacketParser::asUint32(m_data, TypeOffset + 1);
+ "Invalid SSH_FXP_VERSION packet.");
+SftpHandleResponse SftpIncomingPacket::asHandleResponse() const
+ Q_ASSERT(type() == SSH_FXP_HANDLE);
+ SftpHandleResponse response;
+ quint32 offset = RequestIdOffset;
+ response.requestId = SshPacketParser::asUint32(m_data, &offset);
+ response.handle = SshPacketParser::asString(m_data, &offset);
+ return response;
+ "Invalid SSH_FXP_HANDLE packet");
+SftpStatusResponse SftpIncomingPacket::asStatusResponse() const
+ Q_ASSERT(type() == SSH_FXP_STATUS);
+ SftpStatusResponse response;
+ response.status = static_cast<SftpStatusCode>(SshPacketParser::asUint32(m_data, &offset));
+ response.errorString = SshPacketParser::asUserString(m_data, &offset);
+ response.language = SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_FXP_STATUS packet.");
+SftpNameResponse SftpIncomingPacket::asNameResponse() const
+ Q_ASSERT(type() == SSH_FXP_NAME);
+ SftpNameResponse response;
+ const quint32 count = SshPacketParser::asUint32(m_data, &offset);
+ for (quint32 i = 0; i < count; ++i)
+ response.files << asFile(offset);
+ "Invalid SSH_FXP_NAME packet.");
+SftpDataResponse SftpIncomingPacket::asDataResponse() const
+ Q_ASSERT(type() == SSH_FXP_DATA);
+ SftpDataResponse response;
+ response.data = SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_FXP_DATA packet.");
+SftpAttrsResponse SftpIncomingPacket::asAttrsResponse() const
+ Q_ASSERT(type() == SSH_FXP_ATTRS);
+ SftpAttrsResponse response;
+ response.attrs = asFileAttributes(offset);
+ "Invalid SSH_FXP_ATTRS packet.");
+SftpFile SftpIncomingPacket::asFile(quint32 &offset) const
+ SftpFile file;
+ file.fileName
+ = QString::fromUtf8(SshPacketParser::asString(m_data, &offset));
+ file.longName
+ file.attributes = asFileAttributes(offset);
+ return file;
+SftpFileAttributes SftpIncomingPacket::asFileAttributes(quint32 &offset) const
+ SftpFileAttributes attributes;
+ const quint32 flags = SshPacketParser::asUint32(m_data, &offset);
+ attributes.sizePresent = flags & SSH_FILEXFER_ATTR_SIZE;
+ attributes.timesPresent = flags & SSH_FILEXFER_ATTR_ACMODTIME;
+ attributes.uidAndGidPresent = flags & SSH_FILEXFER_ATTR_UIDGID;
+ attributes.permissionsPresent = flags & SSH_FILEXFER_ATTR_PERMISSIONS;
+ attributes.size = SshPacketParser::asUint64(m_data, &offset);
+ if (attributes.uidAndGidPresent) {
+ attributes.uid = SshPacketParser::asUint32(m_data, &offset);
+ attributes.gid = SshPacketParser::asUint32(m_data, &offset);
+ if (attributes.permissionsPresent)
+ attributes.permissions = SshPacketParser::asUint32(m_data, &offset);
+ attributes.atime = SshPacketParser::asUint32(m_data, &offset);
+ attributes.mtime = SshPacketParser::asUint32(m_data, &offset);
+ if (flags & SSH_FILEXFER_ATTR_EXTENDED) {
+ for (quint32 i = 0; i < count; ++i) {
+ SshPacketParser::asString(m_data, &offset);
+ return attributes;
@@ -0,0 +1,112 @@
+#ifndef SFTPINCOMINGPACKET_P_H
+#define SFTPINCOMINGPACKET_P_H
+#include "sftppacket_p.h"
+struct SftpHandleResponse {
+ quint32 requestId;
+ QByteArray handle;
+struct SftpStatusResponse {
+ SftpStatusCode status;
+ QString errorString;
+ QByteArray language;
+struct SftpFileAttributes {
+ bool sizePresent;
+ bool timesPresent;
+ bool uidAndGidPresent;
+ bool permissionsPresent;
+ quint64 size;
+ quint32 uid;
+ quint32 gid;
+ quint32 permissions;
+ quint32 atime;
+ quint32 mtime;
+struct SftpFile {
+ QString fileName;
+ QString longName; // Not present in later RFCs, so we don't expose this to the user.
+struct SftpNameResponse {
+ QList<SftpFile> files;
+struct SftpDataResponse {
+ QByteArray data;
+struct SftpAttrsResponse {
+ SftpFileAttributes attrs;
+class SftpIncomingPacket : public AbstractSftpPacket
+ SftpIncomingPacket();
+ void consumeData(QByteArray &data);
+ void clear();
+ bool isComplete() const;
+ quint32 extractServerVersion() const;
+ SftpHandleResponse asHandleResponse() const;
+ SftpStatusResponse asStatusResponse() const;
+ SftpNameResponse asNameResponse() const;
+ SftpDataResponse asDataResponse() const;
+ SftpAttrsResponse asAttrsResponse() const;
+ void moveFirstBytes(QByteArray &target, QByteArray &source, int n);
+ SftpFileAttributes asFileAttributes(quint32 &offset) const;
+ SftpFile asFile(quint32 &offset) const;
+ quint32 m_length;
+#endif // SFTPINCOMINGPACKET_P_H
@@ -0,0 +1,236 @@
+AbstractSftpOperation::AbstractSftpOperation(SftpJobId jobId) : jobId(jobId)
+AbstractSftpOperation::~AbstractSftpOperation() { }
+SftpStatFile::SftpStatFile(SftpJobId jobId, const QString &path)
+ : AbstractSftpOperation(jobId), path(path)
+SftpOutgoingPacket &SftpStatFile::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateStat(path, jobId);
+SftpMakeDir::SftpMakeDir(SftpJobId jobId, const QString &path,
+ const SftpUploadDir::Ptr &parentJob)
+ : AbstractSftpOperation(jobId), parentJob(parentJob), remoteDir(path)
+SftpOutgoingPacket &SftpMakeDir::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateMkDir(remoteDir, jobId);
+SftpRmDir::SftpRmDir(SftpJobId id, const QString &path)
+ : AbstractSftpOperation(id), remoteDir(path)
+SftpOutgoingPacket &SftpRmDir::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateRmDir(remoteDir, jobId);
+SftpRm::SftpRm(SftpJobId jobId, const QString &path)
+ : AbstractSftpOperation(jobId), remoteFile(path) {}
+SftpOutgoingPacket &SftpRm::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateRm(remoteFile, jobId);
+SftpRename::SftpRename(SftpJobId jobId, const QString &oldPath,
+ : AbstractSftpOperation(jobId), oldPath(oldPath), newPath(newPath)
+SftpOutgoingPacket &SftpRename::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateRename(oldPath, newPath, jobId);
+SftpCreateLink::SftpCreateLink(SftpJobId jobId, const QString &filePath, const QString &target)
+ : AbstractSftpOperation(jobId), filePath(filePath), target(target)
+SftpOutgoingPacket &SftpCreateLink::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateCreateLink(filePath, target, jobId);
+AbstractSftpOperationWithHandle::AbstractSftpOperationWithHandle(SftpJobId jobId,
+ const QString &remotePath)
+ : AbstractSftpOperation(jobId),
+ remotePath(remotePath), state(Inactive), hasError(false)
+AbstractSftpOperationWithHandle::~AbstractSftpOperationWithHandle() { }
+SftpListDir::SftpListDir(SftpJobId jobId, const QString &path,
+ const QSharedPointer<SftpDownloadDir> &parentJob)
+ : AbstractSftpOperationWithHandle(jobId, path), parentJob(parentJob)
+SftpOutgoingPacket &SftpListDir::initialPacket(SftpOutgoingPacket &packet)
+ state = OpenRequested;
+ return packet.generateOpenDir(remotePath, jobId);
+SftpCreateFile::SftpCreateFile(SftpJobId jobId, const QString &path,
+ SftpOverwriteMode mode)
+ : AbstractSftpOperationWithHandle(jobId, path), mode(mode)
+SftpOutgoingPacket & SftpCreateFile::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateOpenFileForWriting(remotePath, mode,
+ SftpOutgoingPacket::DefaultPermissions, jobId);
+const int AbstractSftpTransfer::MaxInFlightCount = 10; // Experimentally found to be enough.
+AbstractSftpTransfer::AbstractSftpTransfer(SftpJobId jobId, const QString &remotePath,
+ const QSharedPointer<QIODevice> &localFile)
+ : AbstractSftpOperationWithHandle(jobId, remotePath),
+ localFile(localFile), fileSize(0), offset(0), inFlightCount(0),
+ statRequested(false)
+AbstractSftpTransfer::~AbstractSftpTransfer() {}
+void AbstractSftpTransfer::calculateInFlightCount(quint32 chunkSize)
+ if (fileSize == 0) {
+ inFlightCount = 1;
+ inFlightCount = fileSize / chunkSize;
+ if (fileSize % chunkSize)
+ ++inFlightCount;
+ if (inFlightCount > MaxInFlightCount)
+ inFlightCount = MaxInFlightCount;
+SftpDownload::SftpDownload(SftpJobId jobId, const QString &remotePath,
+ const QSharedPointer<QIODevice> &localFile, SftpOverwriteMode mode,
+ const QSharedPointer<QSsh::Internal::SftpDownloadDir> &parentJob)
+ : AbstractSftpTransfer(jobId, remotePath, localFile), eofId(SftpInvalidJob), mode(mode),
+ parentJob(parentJob)
+SftpOutgoingPacket &SftpDownload::initialPacket(SftpOutgoingPacket &packet)
+ return packet.generateOpenFileForReading(remotePath, jobId);
+SftpUploadFile::SftpUploadFile(SftpJobId jobId, const QString &remotePath,
+ : AbstractSftpTransfer(jobId, remotePath, localFile),
+ parentJob(parentJob), mode(mode)
+ fileSize = localFile->size();
+SftpOutgoingPacket &SftpUploadFile::initialPacket(SftpOutgoingPacket &packet)
+ quint32 permissions = 0;
+ QFileDevice *fileDevice = qobject_cast<QFileDevice*>(localFile.data());
+ if (fileDevice) {
+ const QFile::Permissions &qtPermissions = fileDevice->permissions();
+ if (qtPermissions & QFile::ExeOther)
+ permissions |= 1 << 0;
+ if (qtPermissions & QFile::WriteOther)
+ permissions |= 1 << 1;
+ if (qtPermissions & QFile::ReadOther)
+ permissions |= 1 << 2;
+ if (qtPermissions & QFile::ExeGroup)
+ permissions |= 1<< 3;
+ if (qtPermissions & QFile::WriteGroup)
+ permissions |= 1<< 4;
+ if (qtPermissions & QFile::ReadGroup)
+ permissions |= 1<< 5;
+ if (qtPermissions & QFile::ExeOwner)
+ permissions |= 1<< 6;
+ if (qtPermissions & QFile::WriteOwner)
+ permissions |= 1<< 7;
+ if (qtPermissions & QFile::ReadOwner)
+ permissions |= 1<< 8;
+ // write owner
+ // read owner
+ return packet.generateOpenFileForWriting(remotePath, mode, permissions, jobId);
+SftpUploadDir::~SftpUploadDir() {}
@@ -0,0 +1,290 @@
+#ifndef SFTPOPERATION_P_H
+#define SFTPOPERATION_P_H
+QT_BEGIN_NAMESPACE
+class QIODevice;
+QT_END_NAMESPACE
+class SftpOutgoingPacket;
+struct AbstractSftpOperation
+ typedef QSharedPointer<AbstractSftpOperation> Ptr;
+ enum Type {
+ StatFile, ListDir, MakeDir, RmDir, Rm, Rename, CreateLink, CreateFile, Download, UploadFile
+ };
+ AbstractSftpOperation(SftpJobId jobId);
+ virtual ~AbstractSftpOperation();
+ virtual Type type() const = 0;
+ virtual SftpOutgoingPacket &initialPacket(SftpOutgoingPacket &packet) = 0;
+ const SftpJobId jobId;
+ AbstractSftpOperation(const AbstractSftpOperation &);
+ AbstractSftpOperation &operator=(const AbstractSftpOperation &);
+struct SftpUploadDir;
+struct SftpDownloadDir;
+struct SftpStatFile : public AbstractSftpOperation
+ typedef QSharedPointer<SftpStatFile> Ptr;
+ SftpStatFile(SftpJobId jobId, const QString &path);
+ virtual Type type() const { return StatFile; }
+ virtual SftpOutgoingPacket &initialPacket(SftpOutgoingPacket &packet);
+ const QString path;
+struct SftpMakeDir : public AbstractSftpOperation
+ typedef QSharedPointer<SftpMakeDir> Ptr;
+ SftpMakeDir(SftpJobId jobId, const QString &path,
+ const QSharedPointer<SftpUploadDir> &parentJob = QSharedPointer<SftpUploadDir>());
+ virtual Type type() const { return MakeDir; }
+ const QSharedPointer<SftpUploadDir> parentJob;
+ const QString remoteDir;
+struct SftpRmDir : public AbstractSftpOperation
+ typedef QSharedPointer<SftpRmDir> Ptr;
+ SftpRmDir(SftpJobId id, const QString &path);
+ virtual Type type() const { return RmDir; }
+struct SftpRm : public AbstractSftpOperation
+ typedef QSharedPointer<SftpRm> Ptr;
+ SftpRm(SftpJobId jobId, const QString &path);
+ virtual Type type() const { return Rm; }
+ const QString remoteFile;
+struct SftpRename : public AbstractSftpOperation
+ typedef QSharedPointer<SftpRename> Ptr;
+ SftpRename(SftpJobId jobId, const QString &oldPath, const QString &newPath);
+ virtual Type type() const { return Rename; }
+ const QString oldPath;
+ const QString newPath;
+struct SftpCreateLink : public AbstractSftpOperation
+ typedef QSharedPointer<SftpCreateLink> Ptr;
+ SftpCreateLink(SftpJobId jobId, const QString &filePath, const QString &target);
+ virtual Type type() const { return CreateLink; }
+ const QString filePath;
+ const QString target;
+struct AbstractSftpOperationWithHandle : public AbstractSftpOperation
+ typedef QSharedPointer<AbstractSftpOperationWithHandle> Ptr;
+ enum State { Inactive, OpenRequested, Open, CloseRequested };
+ AbstractSftpOperationWithHandle(SftpJobId jobId, const QString &remotePath);
+ ~AbstractSftpOperationWithHandle();
+ const QString remotePath;
+ QByteArray remoteHandle;
+ State state;
+ bool hasError;
+struct SftpListDir : public AbstractSftpOperationWithHandle
+ typedef QSharedPointer<SftpListDir> Ptr;
+ SftpListDir(SftpJobId jobId, const QString &path,
+ const QSharedPointer<SftpDownloadDir> &parentJob = QSharedPointer<SftpDownloadDir>());
+ virtual Type type() const { return ListDir; }
+ const QSharedPointer<SftpDownloadDir> parentJob;
+struct SftpCreateFile : public AbstractSftpOperationWithHandle
+ typedef QSharedPointer<SftpCreateFile> Ptr;
+ SftpCreateFile(SftpJobId jobId, const QString &path, SftpOverwriteMode mode);
+ virtual Type type() const { return CreateFile; }
+ const SftpOverwriteMode mode;
+struct AbstractSftpTransfer : public AbstractSftpOperationWithHandle
+ typedef QSharedPointer<AbstractSftpTransfer> Ptr;
+ AbstractSftpTransfer(SftpJobId jobId, const QString &remotePath,
+ const QSharedPointer<QIODevice> &localFile);
+ ~AbstractSftpTransfer();
+ void calculateInFlightCount(quint32 chunkSize);
+ static const int MaxInFlightCount;
+ const QSharedPointer<QIODevice> localFile;
+ quint64 fileSize;
+ quint64 offset;
+ int inFlightCount;
+ bool statRequested;
+struct SftpDownload : public AbstractSftpTransfer
+ typedef QSharedPointer<SftpDownload> Ptr;
+ SftpDownload(SftpJobId jobId, const QString &remotePath,
+ virtual Type type() const { return Download; }
+ QMap<quint32, quint64> offsets;
+ SftpJobId eofId;
+ SftpOverwriteMode mode;
+ const QSharedPointer<QSsh::Internal::SftpDownloadDir> parentJob;
+struct SftpUploadFile : public AbstractSftpTransfer
+ typedef QSharedPointer<SftpUploadFile> Ptr;
+ SftpUploadFile(SftpJobId jobId, const QString &remotePath,
+ virtual Type type() const { return UploadFile; }
+// Composite operation.
+struct SftpUploadDir
+ typedef QSharedPointer<SftpUploadDir> Ptr;
+ struct Dir {
+ Dir(const QString &l, const QString &r) : localDir(l), remoteDir(r) {}
+ QString localDir;
+ QString remoteDir;
+ SftpUploadDir(SftpJobId jobId) : jobId(jobId), hasError(false) {}
+ ~SftpUploadDir();
+ void setError()
+ hasError = true;
+ uploadsInProgress.clear();
+ mkdirsInProgress.clear();
+ QList<SftpUploadFile::Ptr> uploadsInProgress;
+ QMap<SftpMakeDir::Ptr, Dir> mkdirsInProgress;
+struct SftpDownloadDir
+ typedef QSharedPointer<SftpDownloadDir> Ptr;
+ Dir() {}
+ SftpDownloadDir(SftpJobId jobId, SftpOverwriteMode mode)
+ : jobId(jobId), hasError(false), mode(mode) {}
+ ~SftpDownloadDir() {}
+ downloadsInProgress.clear();
+ lsdirsInProgress.clear();
+ QList<SftpDownload::Ptr> downloadsInProgress;
+ QMap<SftpListDir::Ptr, Dir> lsdirsInProgress;
+#endif // SFTPOPERATION_P_H
@@ -0,0 +1,226 @@
+#include "sshpacket_p.h"
+#include <QtEndian>
+#include <limits>
+ const quint32 DefaultAttributes = 0;
+ const quint32 SSH_FXF_READ = 0x00000001;
+ const quint32 SSH_FXF_WRITE = 0x00000002;
+ const quint32 SSH_FXF_APPEND = 0x00000004;
+ const quint32 SSH_FXF_CREAT = 0x00000008;
+ const quint32 SSH_FXF_TRUNC = 0x00000010;
+ const quint32 SSH_FXF_EXCL = 0x00000020;
+SftpOutgoingPacket::SftpOutgoingPacket()
+SftpOutgoingPacket &SftpOutgoingPacket::generateInit(quint32 version)
+ return init(SSH_FXP_INIT, 0).appendInt(version).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateStat(const QString &path, quint32 requestId)
+ return init(SSH_FXP_LSTAT, requestId).appendString(path).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateOpenDir(const QString &path,
+ return init(SSH_FXP_OPENDIR, requestId).appendString(path).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateReadDir(const QByteArray &handle,
+ return init(SSH_FXP_READDIR, requestId).appendString(handle).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateCloseHandle(const QByteArray &handle,
+ return init(SSH_FXP_CLOSE, requestId).appendString(handle).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateMkDir(const QString &path,
+ return init(SSH_FXP_MKDIR, requestId).appendString(path)
+ .appendInt(DefaultAttributes).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateRmDir(const QString &path,
+ return init(SSH_FXP_RMDIR, requestId).appendString(path).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateRm(const QString &path,
+ return init(SSH_FXP_REMOVE, requestId).appendString(path).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateRename(const QString &oldPath,
+ const QString &newPath, quint32 requestId)
+ return init(SSH_FXP_RENAME, requestId).appendString(oldPath)
+ .appendString(newPath).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateOpenFileForWriting(const QString &path,
+ SftpOverwriteMode mode, quint32 permissions, quint32 requestId)
+ QList<quint32> attributes;
+ if (permissions != DefaultPermissions)
+ attributes << SSH_FILEXFER_ATTR_PERMISSIONS << permissions;
+ attributes << DefaultAttributes;
+ return generateOpenFile(path, Write, mode, attributes, requestId);
+SftpOutgoingPacket &SftpOutgoingPacket::generateOpenFileForReading(const QString &path,
+ // Note: Overwrite mode is irrelevant and will be ignored.
+ return generateOpenFile(path, Read, SftpSkipExisting, QList<quint32>() << DefaultAttributes,
+ requestId);
+SftpOutgoingPacket &SftpOutgoingPacket::generateReadFile(const QByteArray &handle,
+ quint64 offset, quint32 length, quint32 requestId)
+ return init(SSH_FXP_READ, requestId).appendString(handle).appendInt64(offset)
+ .appendInt(length).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateFstat(const QByteArray &handle,
+ return init(SSH_FXP_FSTAT, requestId).appendString(handle).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateWriteFile(const QByteArray &handle,
+ quint64 offset, const QByteArray &data, quint32 requestId)
+ return init(SSH_FXP_WRITE, requestId).appendString(handle)
+ .appendInt64(offset).appendString(data).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateCreateLink(const QString &filePath,
+ const QString &target, quint32 requestId)
+ return init(SSH_FXP_SYMLINK, requestId).appendString(filePath).appendString(target).finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::generateOpenFile(const QString &path,
+ OpenType openType, SftpOverwriteMode mode, const QList<quint32> &attributes, quint32 requestId)
+ quint32 pFlags = 0;
+ switch (openType) {
+ case Read:
+ pFlags = SSH_FXF_READ;
+ case Write:
+ pFlags = SSH_FXF_WRITE | SSH_FXF_CREAT;
+ switch (mode) {
+ case SftpOverwriteExisting: pFlags |= SSH_FXF_TRUNC; break;
+ case SftpAppendToExisting: pFlags |= SSH_FXF_APPEND; break;
+ case SftpSkipExisting: pFlags |= SSH_FXF_EXCL; break;
+ init(SSH_FXP_OPEN, requestId).appendString(path).appendInt(pFlags);
+ for (const quint32 attribute : attributes) {
+ appendInt(attribute);
+ return finalize();
+SftpOutgoingPacket &SftpOutgoingPacket::init(SftpPacketType type,
+ m_data.resize(TypeOffset + 1);
+ m_data[TypeOffset] = type;
+ if (type != SSH_FXP_INIT) {
+ appendInt(requestId);
+ qCDebug(sshLog, "Generating SFTP packet of type %d with request id %u", type, requestId);
+ return *this;
+SftpOutgoingPacket &SftpOutgoingPacket::appendInt(quint32 val)
+ m_data.append(AbstractSshPacket::encodeInt(val));
+SftpOutgoingPacket &SftpOutgoingPacket::appendInt64(quint64 value)
+ m_data.append(AbstractSshPacket::encodeInt(value));
+SftpOutgoingPacket &SftpOutgoingPacket::appendString(const QString &string)
+ m_data.append(AbstractSshPacket::encodeString(string.toUtf8()));
+SftpOutgoingPacket &SftpOutgoingPacket::appendString(const QByteArray &string)
+ m_data += AbstractSshPacket::encodeString(string);
+SftpOutgoingPacket &SftpOutgoingPacket::finalize()
+ AbstractSshPacket::setLengthField(m_data);
+const quint32 SftpOutgoingPacket::DefaultPermissions = std::numeric_limits<quint32>::max();
@@ -0,0 +1,92 @@
+#ifndef SFTPOUTGOINGPACKET_P_H
+#define SFTPOUTGOINGPACKET_P_H
+class SftpOutgoingPacket : public AbstractSftpPacket
+ SftpOutgoingPacket();
+ SftpOutgoingPacket &generateInit(quint32 version);
+ SftpOutgoingPacket &generateStat(const QString &path, quint32 requestId);
+ SftpOutgoingPacket &generateOpenDir(const QString &path, quint32 requestId);
+ SftpOutgoingPacket &generateReadDir(const QByteArray &handle,
+ SftpOutgoingPacket &generateCloseHandle(const QByteArray &handle,
+ SftpOutgoingPacket &generateMkDir(const QString &path, quint32 requestId);
+ SftpOutgoingPacket &generateRmDir(const QString &path, quint32 requestId);
+ SftpOutgoingPacket &generateRm(const QString &path, quint32 requestId);
+ SftpOutgoingPacket &generateRename(const QString &oldPath,
+ const QString &newPath, quint32 requestId);
+ SftpOutgoingPacket &generateOpenFileForWriting(const QString &path,
+ SftpOverwriteMode mode, quint32 permissions, quint32 requestId);
+ SftpOutgoingPacket &generateOpenFileForReading(const QString &path,
+ SftpOutgoingPacket &generateReadFile(const QByteArray &handle,
+ quint64 offset, quint32 length, quint32 requestId);
+ SftpOutgoingPacket &generateFstat(const QByteArray &handle,
+ SftpOutgoingPacket &generateWriteFile(const QByteArray &handle,
+ quint64 offset, const QByteArray &data, quint32 requestId);
+ // Note: OpenSSH's SFTP server has a bug that reverses the filePath and target
+ // arguments, so this operation is not portable.
+ SftpOutgoingPacket &generateCreateLink(const QString &filePath, const QString &target,
+ static const quint32 DefaultPermissions;
+ static QByteArray encodeString(const QString &string);
+ enum OpenType { Read, Write };
+ SftpOutgoingPacket &generateOpenFile(const QString &path, OpenType openType,
+ SftpOverwriteMode mode, const QList<quint32> &attributes, quint32 requestId);
+ SftpOutgoingPacket &init(SftpPacketType type, quint32 requestId);
+ SftpOutgoingPacket &appendInt(quint32 value);
+ SftpOutgoingPacket &appendInt64(quint64 value);
+ SftpOutgoingPacket &appendString(const QString &string);
+ SftpOutgoingPacket &appendString(const QByteArray &string);
+ SftpOutgoingPacket &finalize();
+#endif // SFTPOUTGOINGPACKET_P_H
+// There's no "standard" or negotiation between server and client for this, so
+// just use the same as openssh's sftp implementation
+const quint32 AbstractSftpPacket::MaxDataSize = 32768;
+const quint32 AbstractSftpPacket::MaxPacketSize = 256 * 1024;
+const int AbstractSftpPacket::TypeOffset = 4;
+const int AbstractSftpPacket::RequestIdOffset = TypeOffset + 1;
+const int AbstractSftpPacket::PayloadOffset = RequestIdOffset + 4;
+AbstractSftpPacket::AbstractSftpPacket()
+quint32 AbstractSftpPacket::requestId() const
+ return SshPacketParser::asUint32(m_data, RequestIdOffset);
@@ -0,0 +1,117 @@
+#ifndef SFTPPACKET_P_H
+#define SFTPPACKET_P_H
+enum SftpPacketType {
+ SSH_FXP_INIT = 1,
+ SSH_FXP_VERSION = 2,
+ SSH_FXP_OPEN = 3,
+ SSH_FXP_CLOSE = 4,
+ SSH_FXP_READ = 5,
+ SSH_FXP_WRITE = 6,
+ SSH_FXP_LSTAT = 7,
+ SSH_FXP_FSTAT = 8,
+ SSH_FXP_SETSTAT = 9,
+ SSH_FXP_FSETSTAT = 10,
+ SSH_FXP_OPENDIR = 11,
+ SSH_FXP_READDIR = 12,
+ SSH_FXP_REMOVE = 13,
+ SSH_FXP_MKDIR = 14,
+ SSH_FXP_RMDIR = 15,
+ SSH_FXP_REALPATH = 16,
+ SSH_FXP_STAT = 17,
+ SSH_FXP_RENAME = 18,
+ SSH_FXP_READLINK = 19,
+ SSH_FXP_SYMLINK = 20, // Removed from later protocol versions. Try not to use.
+ SSH_FXP_STATUS = 101,
+ SSH_FXP_HANDLE = 102,
+ SSH_FXP_DATA = 103,
+ SSH_FXP_NAME = 104,
+ SSH_FXP_ATTRS = 105,
+ SSH_FXP_EXTENDED = 200,
+ SSH_FXP_EXTENDED_REPLY = 201
+enum SftpStatusCode {
+ SSH_FX_OK = 0,
+ SSH_FX_EOF = 1,
+ SSH_FX_NO_SUCH_FILE = 2,
+ SSH_FX_PERMISSION_DENIED = 3,
+ SSH_FX_FAILURE = 4,
+ SSH_FX_BAD_MESSAGE = 5,
+ SSH_FX_NO_CONNECTION = 6,
+ SSH_FX_CONNECTION_LOST = 7,
+ SSH_FX_OP_UNSUPPORTED = 8
+enum SftpAttributeType {
+ SSH_FILEXFER_ATTR_SIZE = 0x00000001,
+ SSH_FILEXFER_ATTR_UIDGID = 0x00000002,
+ SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004,
+ SSH_FILEXFER_ATTR_ACMODTIME = 0x00000008,
+ SSH_FILEXFER_ATTR_EXTENDED = 0x80000000
+class AbstractSftpPacket
+ AbstractSftpPacket();
+ quint32 requestId() const;
+ const QByteArray &rawData() const { return m_data; }
+ SftpPacketType type() const { return static_cast<SftpPacketType>(m_data.at(TypeOffset)); }
+ static const quint32 MaxDataSize; // "Pure" data size per read/writepacket.
+ static const quint32 MaxPacketSize;
+protected:
+ quint32 dataSize() const { return static_cast<quint32>(m_data.size()); }
+ static const int TypeOffset;
+ static const int RequestIdOffset;
+ static const int PayloadOffset;
+ QByteArray m_data;
+#endif // SFTPPACKET_P_H
@@ -0,0 +1,54 @@
+#ifndef SSH_GLOBAL_H
+#define SSH_GLOBAL_H
+#include <QtGlobal>
+#ifdef _MSC_VER
+// For static cmake building removing dll export/import
+# define QSSH_EXPORT
+#else
+#if defined(QTCSSH_LIBRARY)
+# define QSSH_EXPORT Q_DECL_EXPORT
+# define QSSH_EXPORT Q_DECL_IMPORT
+#endif
+#define QSSH_PRINT_WARNING qWarning("Soft assert at %s:%d", __FILE__, __LINE__)
+#define QSSH_ASSERT(cond) do { if (!(cond)) { QSSH_PRINT_WARNING; } } while (false)
+#define QSSH_ASSERT_AND_RETURN(cond) do { if (!(cond)) { QSSH_PRINT_WARNING; return; } } while (false)
+#define QSSH_ASSERT_AND_RETURN_VALUE(cond, value) do { if (!(cond)) { QSSH_PRINT_WARNING; return value; } } while (false)
+#endif // SSH_GLOBAL_H
@@ -0,0 +1,312 @@
+** Copyright (C) 2016 The Qt Company Ltd.
+#include "sshagent_p.h"
+#include <QTimer>
+#include <algorithm>
+// https://github.com/openssh/openssh-portable/blob/V_7_2/PROTOCOL.agent
+enum PacketType {
+ SSH_AGENT_FAILURE = 5,
+ SSH2_AGENTC_REQUEST_IDENTITIES = 11,
+ SSH2_AGENTC_SIGN_REQUEST = 13,
+ SSH2_AGENT_IDENTITIES_ANSWER = 12,
+ SSH2_AGENT_SIGN_RESPONSE = 14,
+// TODO: Remove once we require 5.7, where the endianness functions have a sane input type.
+template<typename T> static T fromBigEndian(const QByteArray &ba)
+ return qFromBigEndian<T>(reinterpret_cast<const uchar *>(ba.constData()));
+void SshAgent::refreshKeysImpl()
+ if (state() != Connected)
+ const auto keysRequestIt = std::find_if(m_pendingRequests.constBegin(),
+ m_pendingRequests.constEnd(), [](const Request &r) { return r.isKeysRequest(); });
+ if (keysRequestIt != m_pendingRequests.constEnd()) {
+ qCDebug(sshLog) << "keys request already pending, not adding another one";
+ qCDebug(sshLog) << "queueing keys request";
+ m_pendingRequests << Request();
+ sendNextRequest();
+void SshAgent::requestSignatureImpl(const QByteArray &key, uint token)
+ const QByteArray data = m_dataToSign.take(qMakePair(key, token));
+ QSSH_ASSERT(!data.isEmpty());
+ qCDebug(sshLog) << "queueing signature request";
+ m_pendingRequests.enqueue(Request(key, data, token));
+void SshAgent::sendNextRequest()
+ if (m_pendingRequests.isEmpty())
+ if (m_outgoingPacket.isComplete())
+ if (hasError())
+ const Request &request = m_pendingRequests.head();
+ m_outgoingPacket = request.isKeysRequest() ? generateKeysPacket() : generateSigPacket(request);
+ sendPacket();
+SshAgent::Packet SshAgent::generateKeysPacket()
+ qCDebug(sshLog) << "requesting keys from agent";
+ Packet p;
+ p.size = 1;
+ p.data += char(SSH2_AGENTC_REQUEST_IDENTITIES);
+ return p;
+SshAgent::Packet SshAgent::generateSigPacket(const SshAgent::Request &request)
+ qCDebug(sshLog) << "requesting signature from agent for key" << request.key << "and token"
+ << request.token;
+ p.data += char(SSH2_AGENTC_SIGN_REQUEST);
+ p.data += AbstractSshPacket::encodeString(request.key);
+ p.data += AbstractSshPacket::encodeString(request.dataToSign);
+ p.data += AbstractSshPacket::encodeInt(quint32(0));
+ p.size = p.data.count();
+SshAgent::~SshAgent()
+ m_agentSocket.disconnect(this);
+void SshAgent::storeDataToSign(const QByteArray &key, const QByteArray &data, uint token)
+ instance().m_dataToSign.insert(qMakePair(key, token), data);
+void SshAgent::removeDataToSign(const QByteArray &key, uint token)
+ instance().m_dataToSign.remove(qMakePair(key, token));
+SshAgent &QSsh::Internal::SshAgent::instance()
+ static SshAgent agent;
+ return agent;
+SshAgent::SshAgent()
+ connect(&m_agentSocket, &QLocalSocket::connected, this, &SshAgent::handleConnected);
+ connect(&m_agentSocket, &QLocalSocket::disconnected, this, &SshAgent::handleDisconnected);
+// connect(&m_agentSocket, &QLocalSocket::errorOccurred, this, &SshAgent::handleSocketError);
+ connect(&m_agentSocket, &QLocalSocket::readyRead, this, &SshAgent::handleIncomingData);
+ QTimer::singleShot(0, this, &SshAgent::connectToServer);
+void SshAgent::connectToServer()
+ const QByteArray serverAddress = qgetenv("SSH_AUTH_SOCK");
+ if (serverAddress.isEmpty()) {
+ qCDebug(sshLog) << "agent failure: socket address unknown";
+ m_error = tr("Cannot connect to ssh-agent: SSH_AUTH_SOCK is not set.");
+ emit errorOccurred();
+ qCDebug(sshLog) << "connecting to ssh-agent socket" << serverAddress;
+ m_state = Connecting;
+ m_agentSocket.connectToServer(QString::fromLocal8Bit(serverAddress));
+void SshAgent::handleConnected()
+ m_state = Connected;
+ qCDebug(sshLog) << "connection to ssh-agent established";
+ refreshKeys();
+void SshAgent::handleDisconnected()
+ qCDebug(sshLog) << "lost connection to ssh-agent";
+ m_error = tr("Lost connection to ssh-agent for unknown reason.");
+ setDisconnected();
+void SshAgent::handleSocketError()
+ qCDebug(sshLog) << "agent socket error" << m_agentSocket.error();
+ m_error = m_agentSocket.errorString();
+void SshAgent::handleIncomingData()
+ qCDebug(sshLog) << "getting data from agent";
+ m_incomingData += m_agentSocket.readAll();
+ while (!hasError() && !m_incomingData.isEmpty()) {
+ if (m_incomingPacket.size == 0) {
+ if (m_incomingData.count() < int(sizeof m_incomingPacket.size))
+ m_incomingPacket.size = fromBigEndian<quint32>(m_incomingData);
+ m_incomingData.remove(0, sizeof m_incomingPacket.size);
+ const int bytesToTake = qMin<quint32>(m_incomingPacket.size - m_incomingPacket.data.count(),
+ m_incomingData.count());
+ m_incomingPacket.data += m_incomingData.left(bytesToTake);
+ m_incomingData.remove(0, bytesToTake);
+ if (m_incomingPacket.isComplete())
+ handleIncomingPacket();
+void SshAgent::handleIncomingPacket()
+ qCDebug(sshLog) << "received packet from agent:" << m_incomingPacket.data.toHex();
+ const char messageType = m_incomingPacket.data.at(0);
+ switch (messageType) {
+ case SSH2_AGENT_IDENTITIES_ANSWER:
+ handleIdentitiesPacket();
+ case SSH2_AGENT_SIGN_RESPONSE:
+ handleSignaturePacket();
+ case SSH_AGENT_FAILURE:
+ if (m_pendingRequests.isEmpty()) {
+ qCWarning(sshLog) << "unexpected failure message from agent";
+ const Request request = m_pendingRequests.dequeue();
+ if (request.isSignatureRequest()) {
+ qCWarning(sshLog) << "agent failed to sign message for key"
+ << request.key.toHex();
+ emit signatureAvailable(request.key, QByteArray(), request.token);
+ qCWarning(sshLog) << "agent failed to retrieve key list";
+ if (m_keys.isEmpty()) {
+ m_error = tr("ssh-agent failed to retrieve keys.");
+ qCWarning(sshLog) << "unexpected message type from agent:" << messageType;
+ qCWarning(sshLog()) << "received malformed packet from agent";
+ handleProtocolError();
+ m_incomingPacket.invalidate();
+ m_incomingPacket.size = 0;
+ m_outgoingPacket.invalidate();
+void SshAgent::handleIdentitiesPacket()
+ qCDebug(sshLog) << "got keys packet from agent";
+ if (m_pendingRequests.isEmpty() || !m_pendingRequests.dequeue().isKeysRequest()) {
+ qCDebug(sshLog) << "packet was not requested";
+ quint32 offset = 1;
+ const auto keyCount = SshPacketParser::asUint32(m_incomingPacket.data, &offset);
+ qCDebug(sshLog) << "packet contains" << keyCount << "keys";
+ QList<QByteArray> newKeys;
+ for (quint32 i = 0; i < keyCount; ++i) {
+ const QByteArray key = SshPacketParser::asString(m_incomingPacket.data, &offset);
+ quint32 keyOffset = 0;
+ const QByteArray algoName = SshPacketParser::asString(key, &keyOffset);
+ SshPacketParser::asString(key, &keyOffset); // rest of key blob
+ SshPacketParser::asString(m_incomingPacket.data, &offset); // comment
+ qCDebug(sshLog) << "adding key of type" << algoName;
+ newKeys << key;
+ m_keys = newKeys;
+ emit keysUpdated();
+void SshAgent::handleSignaturePacket()
+ qCDebug(sshLog) << "got signature packet from agent";
+ qCDebug(sshLog) << "signature packet was not requested";
+ if (!request.isSignatureRequest()) {
+ const QByteArray signature = SshPacketParser::asString(m_incomingPacket.data, 1);
+ qCDebug(sshLog) << "signature for key" << request.key.toHex() << "is" << signature.toHex();
+ emit signatureAvailable(request.key, signature, request.token);
+void SshAgent::handleProtocolError()
+ m_error = tr("Protocol error when talking to ssh-agent.");
+void SshAgent::setDisconnected()
+ m_state = Unconnected;
+void SshAgent::sendPacket()
+ const quint32 sizeMsb = qToBigEndian(m_outgoingPacket.size);
+ m_agentSocket.write(reinterpret_cast<const char *>(&sizeMsb), sizeof sizeMsb);
+ m_agentSocket.write(m_outgoingPacket.data);
@@ -0,0 +1,125 @@
+#include <QLocalSocket>
+#include <QPair>
+#include <QQueue>
+class SshAgent : public QObject
+ enum State { Unconnected, Connecting, Connected, };
+ ~SshAgent();
+ static State state() { return instance().m_state; }
+ static bool hasError() { return !instance().m_error.isEmpty(); }
+ static QString errorString() { return instance().m_error; }
+ static QList<QByteArray> publicKeys() { return instance().m_keys; }
+ static void refreshKeys() { instance().refreshKeysImpl(); }
+ static void storeDataToSign(const QByteArray &key, const QByteArray &data, uint token);
+ static void removeDataToSign(const QByteArray &key, uint token);
+ static void requestSignature(const QByteArray &key, uint token) {
+ instance().requestSignatureImpl(key, token);
+ static SshAgent &instance();
+ void errorOccurred();
+ void keysUpdated();
+ // Empty signature means signing failure.
+ void signatureAvailable(const QByteArray &key, const QByteArray &signature, uint token);
+ struct Request {
+ Request() { }
+ Request(const QByteArray &k, const QByteArray &d, uint t)
+ : key(k), dataToSign(d), token(t) { }
+ bool isKeysRequest() const { return !isSignatureRequest(); }
+ bool isSignatureRequest() const { return !key.isEmpty(); }
+ QByteArray key;
+ QByteArray dataToSign;
+ uint token = 0;
+ struct Packet {
+ bool isComplete() const { return size != 0 && int(size) == data.count(); }
+ void invalidate() { size = 0; data.clear(); }
+ quint32 size = 0;
+ SshAgent();
+ void connectToServer();
+ void refreshKeysImpl();
+ void requestSignatureImpl(const QByteArray &key, uint token);
+ void sendNextRequest();
+ Packet generateKeysPacket();
+ Packet generateSigPacket(const Request &request);
+ void handleConnected();
+ void handleDisconnected();
+ void handleSocketError();
+ void handleIncomingData();
+ void handleIncomingPacket();
+ void handleIdentitiesPacket();
+ void handleSignaturePacket();
+ void handleProtocolError();
+ void setDisconnected();
+ void sendPacket();
+ State m_state = Unconnected;
+ QString m_error;
+ QList<QByteArray> m_keys;
+ QHash<QPair<QByteArray, uint>, QByteArray> m_dataToSign;
+ QLocalSocket m_agentSocket;
+ Packet m_incomingPacket;
+ Packet m_outgoingPacket;
+ QQueue<Request> m_pendingRequests;
@@ -0,0 +1,172 @@
+#ifndef BYTEARRAYCONVERSIONS_P_H
+#define BYTEARRAYCONVERSIONS_P_H
+#include <botan/secmem.h>
+inline const Botan::byte *convertByteArray(const QByteArray &a)
+ return reinterpret_cast<const Botan::byte *>(a.constData());
+inline Botan::byte *convertByteArray(QByteArray &a)
+ return reinterpret_cast<Botan::byte *>(a.data());
+inline QByteArray convertByteArray(const Botan::secure_vector<Botan::byte> &v)
+ return QByteArray(reinterpret_cast<const char *>(v.data()), static_cast<int>(v.size()));
+inline QByteArray convertByteArray(const std::vector<uint8_t> &v)
+ return QByteArray(reinterpret_cast<const char *>(v.data()), v.size());
+inline const char *botanKeyExchangeAlgoName(const QByteArray &rfcAlgoName)
+ if (rfcAlgoName == SshCapabilities::DiffieHellmanGroup1Sha1)
+ return "modp/ietf/1024";
+ if (rfcAlgoName == SshCapabilities::DiffieHellmanGroup14Sha1)
+ return "modp/ietf/2048";
+ if (rfcAlgoName == SshCapabilities::EcdhNistp256)
+ return "secp256r1";
+ if (rfcAlgoName == SshCapabilities::EcdhNistp384)
+ return "secp384r1";
+ if (rfcAlgoName == SshCapabilities::EcdhNistp521)
+ return "secp521r1";
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected key exchange algorithm \"%1\"")
+ .arg(QString::fromLatin1(rfcAlgoName)));
+inline const char *botanCipherAlgoName(const QByteArray &rfcAlgoName)
+ if (rfcAlgoName == SshCapabilities::CryptAlgoAes128Cbc) {
+ return "CBC(AES-128)";
+ if (rfcAlgoName == SshCapabilities::CryptAlgoAes128Ctr) {
+ return "CTR(AES-128)";
+ if (rfcAlgoName == SshCapabilities::CryptAlgo3DesCbc) {
+ return "CBC(TripleDES)";
+ if (rfcAlgoName == SshCapabilities::CryptAlgo3DesCtr) {
+ return "CTR(TripleDES)";
+ if (rfcAlgoName == SshCapabilities::CryptAlgoAes192Ctr) {
+ return "CBR(AES-192)";
+ if (rfcAlgoName == SshCapabilities::CryptAlgoAes256Ctr) {
+ return "CTR(AES-256)";
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected cipher \"%1\"")
+inline const char *botanCryptAlgoName(const QByteArray &rfcAlgoName)
+ if (rfcAlgoName == SshCapabilities::CryptAlgoAes128Cbc
+ || rfcAlgoName == SshCapabilities::CryptAlgoAes128Ctr) {
+ return "AES-128";
+ if (rfcAlgoName == SshCapabilities::CryptAlgo3DesCbc
+ || rfcAlgoName == SshCapabilities::CryptAlgo3DesCtr) {
+ return "TripleDES";
+ return "AES-192";
+ return "AES-256";
+inline const char *botanEmsaAlgoName(const QByteArray &rfcAlgoName)
+ if (rfcAlgoName == SshCapabilities::PubKeyDss)
+ return "EMSA1(SHA-1)";
+ if (rfcAlgoName == SshCapabilities::PubKeyRsa)
+ return "EMSA3(SHA-1)";
+ if (rfcAlgoName == SshCapabilities::PubKeyEcdsa256)
+ return "EMSA1(SHA-256)";
+ if (rfcAlgoName == SshCapabilities::PubKeyEcdsa384)
+ return "EMSA1_BSI(SHA-384)";
+ if (rfcAlgoName == SshCapabilities::PubKeyEcdsa521)
+ return "EMSA1_BSI(SHA-512)";
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected host key algorithm \"%1\"")
+inline const char *botanHMacAlgoName(const QByteArray &rfcAlgoName)
+ if (rfcAlgoName == SshCapabilities::HMacSha1)
+ return "SHA-1";
+ if (rfcAlgoName == SshCapabilities::HMacSha256)
+ return "SHA-256";
+ if (rfcAlgoName == SshCapabilities::HMacSha384)
+ return "SHA-384";
+ if (rfcAlgoName == SshCapabilities::HMacSha512)
+ return "SHA-512";
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected hashing algorithm \"%1\"")
+inline quint32 botanHMacKeyLen(const QByteArray &rfcAlgoName)
+ return 20;
+ return 32;
+ return 48;
+ return 64;
+#endif // BYTEARRAYCONVERSIONS_P_H
@@ -0,0 +1,180 @@
+#include <QCoreApplication>
+ QByteArray listAsByteArray(const QList<QByteArray> &list)
+ QByteArray array;
+ for (const QByteArray &elem : list) {
+ array += elem + ',';
+ if (!array.isEmpty()) {
+ array.remove(array.count() - 1, 1);
+ return array;
+} // anonymous namspace
+const QByteArray SshCapabilities::DiffieHellmanGroup1Sha1("diffie-hellman-group1-sha1");
+const QByteArray SshCapabilities::DiffieHellmanGroup14Sha1("diffie-hellman-group14-sha1");
+const QByteArray SshCapabilities::EcdhKexNamePrefix("ecdh-sha2-nistp");
+const QByteArray SshCapabilities::EcdhNistp256 = EcdhKexNamePrefix + "256";
+const QByteArray SshCapabilities::EcdhNistp384 = EcdhKexNamePrefix + "384";
+const QByteArray SshCapabilities::EcdhNistp521 = EcdhKexNamePrefix + "521";
+const QList<QByteArray> SshCapabilities::KeyExchangeMethods = QList<QByteArray>()
+ << SshCapabilities::EcdhNistp256
+ << SshCapabilities::EcdhNistp384
+ << SshCapabilities::EcdhNistp521
+ << SshCapabilities::DiffieHellmanGroup1Sha1
+ << SshCapabilities::DiffieHellmanGroup14Sha1;
+const QByteArray SshCapabilities::PubKeyDss("ssh-dss");
+const QByteArray SshCapabilities::PubKeyRsa("ssh-rsa");
+const QByteArray SshCapabilities::PubKeyEcdsaPrefix("ecdsa-sha2-nistp");
+const QByteArray SshCapabilities::PubKeyEcdsa256 = SshCapabilities::PubKeyEcdsaPrefix + "256";
+const QByteArray SshCapabilities::PubKeyEcdsa384 = SshCapabilities::PubKeyEcdsaPrefix + "384";
+const QByteArray SshCapabilities::PubKeyEcdsa521 = SshCapabilities::PubKeyEcdsaPrefix + "521";
+const QList<QByteArray> SshCapabilities::PublicKeyAlgorithms = QList<QByteArray>()
+ << SshCapabilities::PubKeyEcdsa256
+ << SshCapabilities::PubKeyEcdsa384
+ << SshCapabilities::PubKeyEcdsa521
+ << SshCapabilities::PubKeyRsa
+ << SshCapabilities::PubKeyDss;
+const QByteArray SshCapabilities::CryptAlgo3DesCbc("3des-cbc");
+const QByteArray SshCapabilities::CryptAlgo3DesCtr("3des-ctr");
+const QByteArray SshCapabilities::CryptAlgoAes128Cbc("aes128-cbc");
+const QByteArray SshCapabilities::CryptAlgoAes128Ctr("aes128-ctr");
+const QByteArray SshCapabilities::CryptAlgoAes192Ctr("aes192-ctr");
+const QByteArray SshCapabilities::CryptAlgoAes256Ctr("aes256-ctr");
+const QList<QByteArray> SshCapabilities::EncryptionAlgorithms
+ = QList<QByteArray>() << SshCapabilities::CryptAlgoAes256Ctr
+ << SshCapabilities::CryptAlgoAes192Ctr
+ << SshCapabilities::CryptAlgoAes128Ctr
+ << SshCapabilities::CryptAlgo3DesCtr
+ << SshCapabilities::CryptAlgoAes128Cbc
+ << SshCapabilities::CryptAlgo3DesCbc;
+const QByteArray SshCapabilities::HMacSha1("hmac-sha1");
+const QByteArray SshCapabilities::HMacSha196("hmac-sha1-96");
+const QByteArray SshCapabilities::HMacSha256("hmac-sha2-256");
+const QByteArray SshCapabilities::HMacSha384("hmac-sha2-384");
+const QByteArray SshCapabilities::HMacSha512("hmac-sha2-512");
+const QList<QByteArray> SshCapabilities::MacAlgorithms
+ = QList<QByteArray>() /* << SshCapabilities::HMacSha196 */
+ << SshCapabilities::HMacSha256
+ << SshCapabilities::HMacSha384
+ << SshCapabilities::HMacSha512
+ << SshCapabilities::HMacSha1;
+const QList<QByteArray> SshCapabilities::CompressionAlgorithms
+ = QList<QByteArray>() << "none";
+const QByteArray SshCapabilities::SshConnectionService("ssh-connection");
+QList<QByteArray> SshCapabilities::commonCapabilities(const QList<QByteArray> &myCapabilities,
+ const QList<QByteArray> &serverCapabilities, const QByteArray &group)
+ QList<QByteArray> capabilities;
+ for (const QByteArray &myCapability : myCapabilities) {
+ if (serverCapabilities.contains(myCapability)) {
+ capabilities << myCapability;
+ if (!capabilities.isEmpty())
+ return capabilities;
+ throw SshServerException(SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ "Server and client capabilities do not match.",
+ QCoreApplication::translate("SshConnection",
+ "Server and client %1 capabilities don't match.\n"
+ "Client list: %2\n"
+ "Server list: %3")
+ .arg(QString::fromLatin1(group))
+ .arg(QString::fromLocal8Bit(listAsByteArray(myCapabilities)))
+ .arg(QString::fromLocal8Bit(listAsByteArray(serverCapabilities))));
+QByteArray SshCapabilities::findBestMatch(const QList<QByteArray> &myCapabilities,
+ return commonCapabilities(myCapabilities, serverCapabilities, group).first();
+int SshCapabilities::ecdsaIntegerWidthInBytes(const QByteArray &ecdsaAlgo)
+ if (ecdsaAlgo == PubKeyEcdsa256)
+ if (ecdsaAlgo == PubKeyEcdsa384)
+ if (ecdsaAlgo == PubKeyEcdsa521)
+ return 66;
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected ecdsa algorithm \"%1\"")
+ .arg(QString::fromLatin1(ecdsaAlgo)));
+QByteArray SshCapabilities::ecdsaPubKeyAlgoForKeyWidth(int keyWidthInBytes)
+ if (keyWidthInBytes <= 32)
+ return PubKeyEcdsa256;
+ if (keyWidthInBytes <= 48)
+ return PubKeyEcdsa384;
+ if (keyWidthInBytes <= 66)
+ return PubKeyEcdsa521;
+ throw SshClientException(SshInternalError, SSH_TR("Unexpected ecdsa key size (%1 bytes)")
+ .arg(keyWidthInBytes));
+const char *SshCapabilities::oid(const QByteArray &ecdsaAlgo)
@@ -0,0 +1,91 @@
+#ifndef CAPABILITIES_P_H
+#define CAPABILITIES_P_H
+class SshCapabilities
+ static const QByteArray DiffieHellmanGroup1Sha1;
+ static const QByteArray DiffieHellmanGroup14Sha1;
+ static const QByteArray EcdhKexNamePrefix;
+ static const QByteArray EcdhNistp256;
+ static const QByteArray EcdhNistp384;
+ static const QByteArray EcdhNistp521; // sic
+ static const QList<QByteArray> KeyExchangeMethods;
+ static const QByteArray PubKeyDss;
+ static const QByteArray PubKeyRsa;
+ static const QByteArray PubKeyEcdsaPrefix;
+ static const QByteArray PubKeyEcdsa256;
+ static const QByteArray PubKeyEcdsa384;
+ static const QByteArray PubKeyEcdsa521;
+ static const QList<QByteArray> PublicKeyAlgorithms;
+ static const QByteArray CryptAlgo3DesCbc;
+ static const QByteArray CryptAlgo3DesCtr;
+ static const QByteArray CryptAlgoAes128Cbc;
+ static const QByteArray CryptAlgoAes128Ctr;
+ static const QByteArray CryptAlgoAes192Ctr;
+ static const QByteArray CryptAlgoAes256Ctr;
+ static const QList<QByteArray> EncryptionAlgorithms;
+ static const QByteArray HMacSha1;
+ static const QByteArray HMacSha196;
+ static const QByteArray HMacSha256;
+ static const QByteArray HMacSha384;
+ static const QByteArray HMacSha512;
+ static const QList<QByteArray> MacAlgorithms;
+ static const QList<QByteArray> CompressionAlgorithms;
+ static const QByteArray SshConnectionService;
+ static QList<QByteArray> commonCapabilities(const QList<QByteArray> &myCapabilities,
+ const QList<QByteArray> &serverCapabilities, const QByteArray &group);
+ static QByteArray findBestMatch(const QList<QByteArray> &myCapabilities,
+ static int ecdsaIntegerWidthInBytes(const QByteArray &ecdsaAlgo);
+ static QByteArray ecdsaPubKeyAlgoForKeyWidth(int keyWidthInBytes);
+ static const char *oid(const QByteArray &ecdsaAlgo);
+#endif // CAPABILITIES_P_H
@@ -0,0 +1,277 @@
+#include <botan/exceptn.h>
+const quint32 NoChannel = 0xffffffffu;
+AbstractSshChannel::AbstractSshChannel(quint32 channelId,
+ SshSendFacility &sendFacility)
+ : m_sendFacility(sendFacility),
+ m_localChannel(channelId), m_remoteChannel(NoChannel),
+ m_localWindowSize(initialWindowSize()), m_remoteWindowSize(0),
+ m_state(Inactive)
+ m_timeoutTimer.setTimerType(Qt::VeryCoarseTimer);
+ m_timeoutTimer.setSingleShot(true);
+ connect(&m_timeoutTimer, &QTimer::timeout, this, &AbstractSshChannel::timeout);
+AbstractSshChannel::~AbstractSshChannel()
+void AbstractSshChannel::setChannelState(ChannelState state)
+ m_state = state;
+ if (state == Closed)
+ closeHook();
+void AbstractSshChannel::requestSessionStart()
+ // Note: We are just being paranoid here about the Botan exceptions,
+ // which are extremely unlikely to happen, because if there was a problem
+ // with our cryptography stuff, it would have hit us before, on
+ // establishing the connection.
+ m_sendFacility.sendSessionPacket(m_localChannel, initialWindowSize(), maxPacketSize());
+ setChannelState(SessionRequested);
+ m_timeoutTimer.start(ReplyTimeout);
+ } catch (const std::exception &e) {
+ qCWarning(sshLog, "Botan error: %s", e.what());
+void AbstractSshChannel::sendData(const QByteArray &data)
+ m_sendBuffer += data;
+ flushSendBuffer();
+quint32 AbstractSshChannel::initialWindowSize()
+ return maxPacketSize();
+quint32 AbstractSshChannel::maxPacketSize()
+ return 16 * 1024 * 1024;
+void AbstractSshChannel::handleWindowAdjust(quint64 bytesToAdd)
+ const quint64 newValue = m_remoteWindowSize + bytesToAdd;
+ if (newValue > 0xffffffffu) {
+ "Illegal window size requested.");
+ m_remoteWindowSize = newValue;
+void AbstractSshChannel::flushSendBuffer()
+ while (true) {
+ const quint32 bytesToSend = qMin(m_remoteMaxPacketSize,
+ qMin<quint32>(m_remoteWindowSize, m_sendBuffer.size()));
+ if (bytesToSend == 0)
+ const QByteArray &data = m_sendBuffer.left(bytesToSend);
+ m_sendFacility.sendChannelDataPacket(m_remoteChannel, data);
+ m_sendBuffer.remove(0, bytesToSend);
+ m_remoteWindowSize -= bytesToSend;
+void AbstractSshChannel::handleOpenSuccess(quint32 remoteChannelId,
+ quint32 remoteWindowSize, quint32 remoteMaxPacketSize)
+ const ChannelState oldState = m_state;
+ switch (oldState) {
+ case CloseRequested: // closeChannel() was called while we were in SessionRequested state
+ case SessionRequested:
+ break; // Ok, continue.
+ "Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet.");
+ m_timeoutTimer.stop();
+ qCDebug(sshLog, "Channel opened. remote channel id: %u, remote window size: %u, "
+ "remote max packet size: %u",
+ remoteChannelId, remoteWindowSize, remoteMaxPacketSize);
+ m_remoteChannel = remoteChannelId;
+ m_remoteWindowSize = remoteWindowSize;
+ m_remoteMaxPacketSize = remoteMaxPacketSize;
+ setChannelState(SessionEstablished);
+ if (oldState == CloseRequested)
+ handleOpenSuccessInternal();
+void AbstractSshChannel::handleOpenFailure(const QString &reason)
+ switch (m_state) {
+ case CloseRequested:
+ return; // Late server reply; we requested a channel close in the meantime.
+ qCDebug(sshLog, "Channel open request failed for channel %u", m_localChannel);
+ handleOpenFailureInternal(reason);
+void AbstractSshChannel::handleChannelEof()
+ if (m_state == Inactive || m_state == Closed) {
+ "Unexpected SSH_MSG_CHANNEL_EOF message.");
+ m_localWindowSize = 0;
+ emit eof();
+void AbstractSshChannel::handleChannelClose()
+ qCDebug(sshLog, "Receiving CLOSE for channel %u", m_localChannel);
+ if (channelState() == Inactive || channelState() == Closed) {
+ "Unexpected SSH_MSG_CHANNEL_CLOSE message.");
+ setChannelState(Closed);
+void AbstractSshChannel::handleChannelData(const QByteArray &data)
+ const int bytesToDeliver = handleChannelOrExtendedChannelData(data);
+ handleChannelDataInternal(bytesToDeliver == data.size()
+ ? data : data.left(bytesToDeliver));
+void AbstractSshChannel::handleChannelExtendedData(quint32 type, const QByteArray &data)
+ handleChannelExtendedDataInternal(type, bytesToDeliver == data.size()
+void AbstractSshChannel::handleChannelRequest(const SshIncomingPacket &packet)
+ const QByteArray &requestType = packet.extractChannelRequestType();
+ if (requestType == SshIncomingPacket::ExitStatusType)
+ handleExitStatus(packet.extractChannelExitStatus());
+ else if (requestType == SshIncomingPacket::ExitSignalType)
+ handleExitSignal(packet.extractChannelExitSignal());
+ else if (requestType != "eow@openssh.com") // Suppress warning for this one, as it's sent all the time.
+ qCWarning(sshLog, "Ignoring unknown request type '%s'", requestType.data());
+int AbstractSshChannel::handleChannelOrExtendedChannelData(const QByteArray &data)
+ const int bytesToDeliver = qMin<quint32>(data.size(), maxDataSize());
+ if (bytesToDeliver != data.size())
+ qCWarning(sshLog, "Misbehaving server does not respect local window, clipping.");
+ m_localWindowSize -= bytesToDeliver;
+ if (m_localWindowSize < maxPacketSize()) {
+ m_localWindowSize += maxPacketSize();
+ m_sendFacility.sendWindowAdjustPacket(m_remoteChannel, maxPacketSize());
+ return bytesToDeliver;
+void AbstractSshChannel::closeChannel()
+ if (m_state == CloseRequested) {
+ } else if (m_state != Closed) {
+ if (m_state == Inactive) {
+ setChannelState(CloseRequested);
+ if (m_remoteChannel != NoChannel) {
+ m_sendFacility.sendChannelEofPacket(m_remoteChannel);
+ m_sendFacility.sendChannelClosePacket(m_remoteChannel);
+ QSSH_ASSERT(oldState == SessionRequested);
+void AbstractSshChannel::checkChannelActive() const
+ if (channelState() == Inactive || channelState() == Closed)
+ "Channel not open.");
+quint32 AbstractSshChannel::maxDataSize() const
+ return qMin(m_localWindowSize, maxPacketSize());
+#ifndef SSHCHANNEL_P_H
+#define SSHCHANNEL_P_H
+struct SshChannelExitSignal;
+struct SshChannelExitStatus;
+class SshIncomingPacket;
+class AbstractSshChannel : public QObject
+ enum ChannelState {
+ Inactive, SessionRequested, SessionEstablished, CloseRequested, Closed
+ quint32 localChannelId() const { return m_localChannel; }
+ quint32 remoteChannel() const { return m_remoteChannel; }
+ virtual void handleChannelSuccess() = 0;
+ virtual void handleChannelFailure() = 0;
+ void handleOpenSuccess(quint32 remoteChannelId, quint32 remoteWindowSize,
+ quint32 remoteMaxPacketSize);
+ void handleOpenFailure(const QString &reason);
+ void handleWindowAdjust(quint64 bytesToAdd);
+ void handleChannelEof();
+ void handleChannelClose();
+ void handleChannelData(const QByteArray &data);
+ void handleChannelExtendedData(quint32 type, const QByteArray &data);
+ void handleChannelRequest(const SshIncomingPacket &packet);
+ virtual ~AbstractSshChannel();
+ static const int ReplyTimeout = 10000; // milli seconds
+ ChannelState channelState() const { return m_state; }
+ void timeout();
+ void eof();
+ AbstractSshChannel(quint32 channelId, SshSendFacility &sendFacility);
+ void setChannelState(ChannelState state);
+ void requestSessionStart();
+ void sendData(const QByteArray &data);
+ static quint32 initialWindowSize();
+ static quint32 maxPacketSize();
+ quint32 maxDataSize() const;
+ void checkChannelActive() const;
+ SshSendFacility &m_sendFacility;
+ QTimer m_timeoutTimer;
+ virtual void handleOpenSuccessInternal() = 0;
+ virtual void handleOpenFailureInternal(const QString &reason) = 0;
+ virtual void handleChannelDataInternal(const QByteArray &data) = 0;
+ const QByteArray &data) = 0;
+ virtual void handleExitStatus(const SshChannelExitStatus &exitStatus) = 0;
+ virtual void handleExitSignal(const SshChannelExitSignal &signal) = 0;
+ virtual void closeHook() = 0;
+ void flushSendBuffer();
+ int handleChannelOrExtendedChannelData(const QByteArray &data);
+ const quint32 m_localChannel;
+ quint32 m_remoteChannel;
+ quint32 m_localWindowSize;
+ quint32 m_remoteWindowSize;
+ quint32 m_remoteMaxPacketSize;
+ ChannelState m_state;
+ QByteArray m_sendBuffer;
+#endif // SSHCHANNEL_P_H
@@ -0,0 +1,413 @@
+#include "sshchannelmanager_p.h"
+#include "sshdirecttcpiptunnel.h"
+#include "sshdirecttcpiptunnel_p.h"
+#include "sshforwardedtcpiptunnel.h"
+#include "sshforwardedtcpiptunnel_p.h"
+#include "sshremoteprocess.h"
+#include "sshremoteprocess_p.h"
+#include "sshtcpipforwardserver.h"
+#include "sshtcpipforwardserver_p.h"
+#include "sshx11channel_p.h"
+#include "sshx11inforetriever_p.h"
+SshChannelManager::SshChannelManager(SshSendFacility &sendFacility,
+ QObject *parent)
+ : QObject(parent), m_sendFacility(sendFacility), m_nextLocalChannelId(0)
+void SshChannelManager::handleChannelRequest(const SshIncomingPacket &packet)
+ lookupChannel(packet.extractRecipientChannel())
+ ->handleChannelRequest(packet);
+void SshChannelManager::handleChannelOpen(const SshIncomingPacket &packet)
+ const SshChannelOpenGeneric channelOpen = packet.extractChannelOpen();
+ if (channelOpen.channelType == SshIncomingPacket::ForwardedTcpIpType) {
+ handleChannelOpenForwardedTcpIp(channelOpen);
+ if (channelOpen.channelType == "x11") {
+ handleChannelOpenX11(channelOpen);
+ m_sendFacility.sendChannelOpenFailurePacket(channelOpen.commonData.remoteChannel,
+ SSH_OPEN_UNKNOWN_CHANNEL_TYPE, QByteArray());
+void SshChannelManager::handleChannelOpenFailure(const SshIncomingPacket &packet)
+ const SshChannelOpenFailure &failure = packet.extractChannelOpenFailure();
+ ChannelIterator it = lookupChannelAsIterator(failure.localChannel);
+ it.value()->handleOpenFailure(failure.reasonString);
+ } catch (const SshServerException &e) {
+ Q_UNUSED(e);
+ removeChannel(it);
+ throw;
+void SshChannelManager::handleChannelOpenConfirmation(const SshIncomingPacket &packet)
+ const SshChannelOpenConfirmation &confirmation
+ = packet.extractChannelOpenConfirmation();
+ lookupChannel(confirmation.localChannel)->handleOpenSuccess(confirmation.remoteChannel,
+ confirmation.remoteWindowSize, confirmation.remoteMaxPacketSize);
+void SshChannelManager::handleChannelSuccess(const SshIncomingPacket &packet)
+ lookupChannel(packet.extractRecipientChannel())->handleChannelSuccess();
+void SshChannelManager::handleChannelFailure(const SshIncomingPacket &packet)
+ lookupChannel(packet.extractRecipientChannel())->handleChannelFailure();
+void SshChannelManager::handleChannelWindowAdjust(const SshIncomingPacket &packet)
+ const SshChannelWindowAdjust adjust = packet.extractWindowAdjust();
+ lookupChannel(adjust.localChannel)->handleWindowAdjust(adjust.bytesToAdd);
+void SshChannelManager::handleChannelData(const SshIncomingPacket &packet)
+ const SshChannelData &data = packet.extractChannelData();
+ lookupChannel(data.localChannel)->handleChannelData(data.data);
+void SshChannelManager::handleChannelExtendedData(const SshIncomingPacket &packet)
+ const SshChannelExtendedData &data = packet.extractChannelExtendedData();
+ lookupChannel(data.localChannel)->handleChannelExtendedData(data.type, data.data);
+void SshChannelManager::handleChannelEof(const SshIncomingPacket &packet)
+ AbstractSshChannel * const channel
+ = lookupChannel(packet.extractRecipientChannel(), true);
+ if (channel)
+ channel->handleChannelEof();
+void SshChannelManager::handleChannelClose(const SshIncomingPacket &packet)
+ const quint32 channelId = packet.extractRecipientChannel();
+ ChannelIterator it = lookupChannelAsIterator(channelId, true);
+ if (it != m_channels.end()) {
+ it.value()->handleChannelClose();
+void SshChannelManager::handleRequestSuccess(const SshIncomingPacket &packet)
+ if (m_waitingForwardServers.isEmpty()) {
+ "Unexpected request success packet.",
+ tr("Unexpected request success packet."));
+ SshTcpIpForwardServer::Ptr server = m_waitingForwardServers.takeFirst();
+ if (server->state() == SshTcpIpForwardServer::Closing) {
+ server->setClosed();
+ } else if (server->state() == SshTcpIpForwardServer::Initializing) {
+ quint16 port = server->port();
+ if (port == 0)
+ port = packet.extractRequestSuccess().bindPort;
+ server->setListening(port);
+ m_listeningForwardServers.append(server);
+ QSSH_ASSERT(false);
+void SshChannelManager::handleRequestFailure(const SshIncomingPacket &packet)
+ Q_UNUSED(packet);
+ "Unexpected request failure packet.",
+ tr("Unexpected request failure packet."));
+ SshTcpIpForwardServer::Ptr tunnel = m_waitingForwardServers.takeFirst();
+ tunnel->setClosed();
+SshChannelManager::ChannelIterator SshChannelManager::lookupChannelAsIterator(quint32 channelId,
+ bool allowNotFound)
+ ChannelIterator it = m_channels.find(channelId);
+ if (it == m_channels.end() && !allowNotFound) {
+ "Invalid channel id.",
+ tr("Invalid channel id %1").arg(channelId));
+AbstractSshChannel *SshChannelManager::lookupChannel(quint32 channelId,
+ ChannelIterator it = lookupChannelAsIterator(channelId, allowNotFound);
+ return it == m_channels.end() ? nullptr : it.value();
+QSsh::SshRemoteProcess::Ptr SshChannelManager::createRemoteProcess(const QByteArray &command)
+ SshRemoteProcess::Ptr proc(new SshRemoteProcess(command, m_nextLocalChannelId++, m_sendFacility));
+ insertChannel(proc->d, proc);
+ connect(proc->d, &SshRemoteProcessPrivate::destroyed, this, [this] {
+ m_x11ForwardingRequests.removeOne(static_cast<SshRemoteProcessPrivate *>(sender()));
+ });
+ connect(proc->d, &SshRemoteProcessPrivate::x11ForwardingRequested, this,
+ [this, proc = proc->d](const QString &displayName) {
+ if (!x11DisplayName().isEmpty()) {
+ if (x11DisplayName() != displayName) {
+ proc->failToStart(tr("Cannot forward to display %1 on SSH connection that is "
+ "already forwarding to display %2.")
+ .arg(displayName, x11DisplayName()));
+ if (!m_x11DisplayInfo.cookie.isEmpty())
+ proc->startProcess(m_x11DisplayInfo);
+ m_x11ForwardingRequests << proc;
+ m_x11DisplayInfo.displayName = displayName;
+ auto * const x11InfoRetriever = new SshX11InfoRetriever(displayName, this);
+ const auto failureHandler = [this](const QString &errorMessage) {
+ for (SshRemoteProcessPrivate * const proc : qAsConst(m_x11ForwardingRequests))
+ proc->failToStart(errorMessage);
+ m_x11ForwardingRequests.clear();
+ connect(x11InfoRetriever, &SshX11InfoRetriever::failure, this, failureHandler);
+ const auto successHandler = [this](const X11DisplayInfo &displayInfo) {
+ m_x11DisplayInfo = displayInfo;
+ proc->startProcess(displayInfo);
+ connect(x11InfoRetriever, &SshX11InfoRetriever::success, this, successHandler);
+ qCDebug(sshLog) << "starting x11 info retriever";
+ x11InfoRetriever->start();
+ return proc;
+QSsh::SshRemoteProcess::Ptr SshChannelManager::createRemoteShell()
+ SshRemoteProcess::Ptr proc(new SshRemoteProcess(m_nextLocalChannelId++, m_sendFacility));
+QSsh::SftpChannel::Ptr SshChannelManager::createSftpChannel()
+ SftpChannel::Ptr sftp(new SftpChannel(m_nextLocalChannelId++, m_sendFacility));
+ insertChannel(sftp->d, sftp);
+ return sftp;
+SshDirectTcpIpTunnel::Ptr SshChannelManager::createDirectTunnel(const QString &originatingHost,
+ quint16 originatingPort, const QString &remoteHost, quint16 remotePort)
+ SshDirectTcpIpTunnel::Ptr tunnel(new SshDirectTcpIpTunnel(m_nextLocalChannelId++,
+ originatingHost, originatingPort, remoteHost, remotePort, m_sendFacility));
+ insertChannel(tunnel->d, tunnel);
+ return tunnel;
+SshTcpIpForwardServer::Ptr SshChannelManager::createForwardServer(const QString &remoteHost,
+ quint16 remotePort)
+ SshTcpIpForwardServer::Ptr server(new SshTcpIpForwardServer(remoteHost, remotePort,
+ m_sendFacility));
+ connect(server.data(), &SshTcpIpForwardServer::stateChanged,
+ this, [this, server](SshTcpIpForwardServer::State state) {
+ switch (state) {
+ case SshTcpIpForwardServer::Closing:
+ m_listeningForwardServers.removeOne(server);
+ Q_FALLTHROUGH();
+ case SshTcpIpForwardServer::Initializing:
+ m_waitingForwardServers.append(server);
+ case SshTcpIpForwardServer::Listening:
+ case SshTcpIpForwardServer::Inactive:
+ return server;
+void SshChannelManager::insertChannel(AbstractSshChannel *priv,
+ const QSharedPointer<QObject> &pub)
+ connect(priv, &AbstractSshChannel::timeout, this, &SshChannelManager::timeout);
+ m_channels.insert(priv->localChannelId(), priv);
+ m_sessions.insert(priv, pub);
+void SshChannelManager::handleChannelOpenForwardedTcpIp(
+ const SshChannelOpenGeneric &channelOpenGeneric)
+ const SshChannelOpenForwardedTcpIp channelOpen
+ = SshIncomingPacket::extractChannelOpenForwardedTcpIp(channelOpenGeneric);
+ SshTcpIpForwardServer::Ptr server;
+ for (const SshTcpIpForwardServer::Ptr &candidate : m_listeningForwardServers) {
+ if (candidate->port() == channelOpen.remotePort
+ && candidate->bindAddress().toUtf8() == channelOpen.remoteAddress) {
+ server = candidate;
+ if (server.isNull()) {
+ // Apparently the server knows a remoteAddress we are not aware of. There are plenty of ways
+ // to make that happen: /etc/hosts on the server, different writings for localhost,
+ // different DNS servers, ...
+ // Rather than trying to figure that out, we just use the first listening forwarder with the
+ // same port.
+ if (candidate->port() == channelOpen.remotePort) {
+ m_sendFacility.sendChannelOpenFailurePacket(channelOpen.common.remoteChannel,
+ SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
+ QByteArray());
+ SshForwardedTcpIpTunnel::Ptr tunnel(new SshForwardedTcpIpTunnel(m_nextLocalChannelId++,
+ tunnel->d->handleOpenSuccess(channelOpen.common.remoteChannel,
+ channelOpen.common.remoteWindowSize,
+ channelOpen.common.remoteMaxPacketSize);
+ tunnel->open(QIODevice::ReadWrite);
+ server->setNewConnection(tunnel);
+void SshChannelManager::handleChannelOpenX11(const SshChannelOpenGeneric &channelOpenGeneric)
+ qCDebug(sshLog) << "incoming X11 channel open request";
+ const SshChannelOpenX11 channelOpen
+ = SshIncomingPacket::extractChannelOpenX11(channelOpenGeneric);
+ if (m_x11DisplayInfo.cookie.isEmpty()) {
+ "Server attempted to open an unrequested X11 channel.");
+ SshX11Channel * const x11Channel = new SshX11Channel(m_x11DisplayInfo,
+ m_nextLocalChannelId++,
+ m_sendFacility);
+ x11Channel->setParent(this);
+ x11Channel->handleOpenSuccess(channelOpen.common.remoteChannel,
+ insertChannel(x11Channel, QSharedPointer<QObject>());
+int SshChannelManager::closeAllChannels(CloseAllMode mode)
+ int count = 0;
+ for (ChannelIterator it = m_channels.begin(); it != m_channels.end(); ++it) {
+ AbstractSshChannel * const channel = it.value();
+ QSSH_ASSERT(channel->channelState() != AbstractSshChannel::Closed);
+ if (channel->channelState() != AbstractSshChannel::CloseRequested) {
+ channel->closeChannel();
+ ++count;
+ if (mode == CloseAllAndReset) {
+ m_channels.clear();
+ m_sessions.clear();
+ return count;
+int SshChannelManager::channelCount() const
+ return m_channels.count();
+void SshChannelManager::removeChannel(ChannelIterator it)
+ if (it == m_channels.end()) {
+ throw SshClientException(SshInternalError,
+ QLatin1String("Internal error: Unexpected channel lookup failure"));
+ const int removeCount = m_sessions.remove(it.value());
+ if (removeCount != 1) {
+ QString::fromLatin1("Internal error: Unexpected session count %1 for channel.")
+ .arg(removeCount));
+ m_channels.erase(it);
+#ifndef SSHCHANNELLAYER_P_H
+#define SSHCHANNELLAYER_P_H
+#include "sshx11displayinfo_p.h"
+class SshDirectTcpIpTunnel;
+class SshRemoteProcess;
+class SshTcpIpForwardServer;
+class AbstractSshChannel;
+struct SshChannelOpenGeneric;
+class SshRemoteProcessPrivate;
+class SshChannelManager : public QObject
+ SshChannelManager(SshSendFacility &sendFacility, QObject *parent);
+ QSharedPointer<SshRemoteProcess> createRemoteProcess(const QByteArray &command);
+ QSharedPointer<SshRemoteProcess> createRemoteShell();
+ QSharedPointer<SftpChannel> createSftpChannel();
+ QSharedPointer<SshDirectTcpIpTunnel> createDirectTunnel(const QString &originatingHost,
+ quint16 originatingPort, const QString &remoteHost, quint16 remotePort);
+ QSharedPointer<SshTcpIpForwardServer> createForwardServer(const QString &remoteHost,
+ quint16 remotePort);
+ int channelCount() const;
+ enum CloseAllMode { CloseAllRegular, CloseAllAndReset };
+ int closeAllChannels(CloseAllMode mode);
+ QString x11DisplayName() const { return m_x11DisplayInfo.displayName; }
+ void handleChannelOpen(const SshIncomingPacket &packet);
+ void handleChannelOpenFailure(const SshIncomingPacket &packet);
+ void handleChannelOpenConfirmation(const SshIncomingPacket &packet);
+ void handleChannelSuccess(const SshIncomingPacket &packet);
+ void handleChannelFailure(const SshIncomingPacket &packet);
+ void handleChannelWindowAdjust(const SshIncomingPacket &packet);
+ void handleChannelData(const SshIncomingPacket &packet);
+ void handleChannelExtendedData(const SshIncomingPacket &packet);
+ void handleChannelEof(const SshIncomingPacket &packet);
+ void handleChannelClose(const SshIncomingPacket &packet);
+ void handleRequestSuccess(const SshIncomingPacket &packet);
+ void handleRequestFailure(const SshIncomingPacket &packet);
+ typedef QHash<quint32, AbstractSshChannel *>::Iterator ChannelIterator;
+ ChannelIterator lookupChannelAsIterator(quint32 channelId,
+ bool allowNotFound = false);
+ AbstractSshChannel *lookupChannel(quint32 channelId,
+ void removeChannel(ChannelIterator it);
+ void insertChannel(AbstractSshChannel *priv,
+ const QSharedPointer<QObject> &pub);
+ void handleChannelOpenForwardedTcpIp(const SshChannelOpenGeneric &channelOpenGeneric);
+ void handleChannelOpenX11(const SshChannelOpenGeneric &channelOpenGeneric);
+ QHash<quint32, AbstractSshChannel *> m_channels;
+ QHash<AbstractSshChannel *, QSharedPointer<QObject> > m_sessions;
+ quint32 m_nextLocalChannelId;
+ QList<QSharedPointer<SshTcpIpForwardServer>> m_waitingForwardServers;
+ QList<QSharedPointer<SshTcpIpForwardServer>> m_listeningForwardServers;
+ QList<SshRemoteProcessPrivate *> m_x11ForwardingRequests;
+ X11DisplayInfo m_x11DisplayInfo;
+#endif // SSHCHANNELLAYER_P_H
@@ -0,0 +1,1035 @@
+#include "sshconnection_p.h"
+#include "sshcryptofacility_p.h"
+#include "sshkeyexchange_p.h"
+#include <QMutex>
+#include <QMutexLocker>
+#include <QNetworkProxy>
+#include <QRegularExpression>
+#include <QTcpSocket>
+const QByteArray ClientId("SSH-2.0-QtCreator\r\n");
+SshConnectionParameters::SshConnectionParameters() :
+ timeout(0), authenticationType(AuthenticationTypePublicKey),
+ hostKeyCheckingMode(SshHostKeyCheckingNone)
+ url.setPort(0);
+ options |= SshIgnoreDefaultProxy;
+ hostKeyDatabase = SshHostKeyDatabasePtr(new SshHostKeyDatabase);
+static inline bool equals(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
+ return p1.url == p2.url
+ && p1.authenticationType == p2.authenticationType
+ && p1.privateKeyFile == p2.privateKeyFile
+ && p1.hostKeyCheckingMode == p2.hostKeyCheckingMode
+ && p1.timeout == p2.timeout;
+bool operator==(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
+ return equals(p1, p2);
+bool operator!=(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
+ return !equals(p1, p2);
+SshConnection::SshConnection(const SshConnectionParameters &serverInfo, QObject *parent)
+ : QObject(parent)
+ qRegisterMetaType<QSsh::SshError>("QSsh::SshError");
+ qRegisterMetaType<QSsh::SftpJobId>("QSsh::SftpJobId");
+ qRegisterMetaType<QSsh::SftpFileInfo>("QSsh::SftpFileInfo");
+ qRegisterMetaType<QSsh::SftpError>("QSsh::SftpError");
+ qRegisterMetaType<QSsh::SftpError>("SftpError");
+ qRegisterMetaType<QList <QSsh::SftpFileInfo> >("QList<QSsh::SftpFileInfo>");
+ d = new Internal::SshConnectionPrivate(this, serverInfo);
+ connect(d, &Internal::SshConnectionPrivate::connected, this, &SshConnection::connected,
+ Qt::QueuedConnection);
+ connect(d, &Internal::SshConnectionPrivate::dataAvailable, this,
+ &SshConnection::dataAvailable, Qt::QueuedConnection);
+ connect(d, &Internal::SshConnectionPrivate::disconnected, this, &SshConnection::disconnected,
+ connect(d, &Internal::SshConnectionPrivate::error, this,
+ &SshConnection::error, Qt::QueuedConnection);
+const QByteArray &SshConnection::hostKeyFingerprint() const
+ return d->hostKeyFingerprint();
+void SshConnection::connectToHost()
+ d->connectToHost();
+void SshConnection::disconnectFromHost()
+ d->closeConnection(Internal::SSH_DISCONNECT_BY_APPLICATION, SshNoError, "",
+ QString());
+SshConnection::State SshConnection::state() const
+ switch (d->state()) {
+ case Internal::SocketUnconnected:
+ return Unconnected;
+ case Internal::ConnectionEstablished:
+ return Connected;
+ return Connecting;
+SshError SshConnection::errorState() const
+ return d->errorState();
+QString SshConnection::errorString() const
+ return d->errorString();
+SshConnectionParameters SshConnection::connectionParameters() const
+ return d->m_connParams;
+SshConnectionInfo SshConnection::connectionInfo() const
+ QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, SshConnectionInfo());
+ return SshConnectionInfo(d->m_socket->localAddress(), d->m_socket->localPort(),
+ d->m_socket->peerAddress(), d->m_socket->peerPort());
+SshConnection::~SshConnection()
+ disconnect();
+ disconnectFromHost();
+QSharedPointer<SshRemoteProcess> SshConnection::createRemoteProcess(const QByteArray &command)
+ QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, QSharedPointer<SshRemoteProcess>());
+ return d->createRemoteProcess(command);
+QSharedPointer<SshRemoteProcess> SshConnection::createRemoteShell()
+ return d->createRemoteShell();
+QSharedPointer<SftpChannel> SshConnection::createSftpChannel()
+ QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, QSharedPointer<SftpChannel>());
+ return d->createSftpChannel();
+SshDirectTcpIpTunnel::Ptr SshConnection::createDirectTunnel(const QString &originatingHost,
+ QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, SshDirectTcpIpTunnel::Ptr());
+ return d->createDirectTunnel(originatingHost, originatingPort, remoteHost, remotePort);
+QSharedPointer<SshTcpIpForwardServer> SshConnection::createForwardServer(const QString &remoteHost,
+ QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, SshTcpIpForwardServer::Ptr());
+ return d->createForwardServer(remoteHost, remotePort);
+int SshConnection::closeAllChannels()
+ return d->m_channelManager->closeAllChannels(Internal::SshChannelManager::CloseAllRegular);
+ qCWarning(Internal::sshLog, "%s: %s", Q_FUNC_INFO, e.what());
+ return -1;
+int SshConnection::channelCount() const
+ return d->m_channelManager->channelCount();
+QString SshConnection::x11DisplayName() const
+ return d->m_channelManager->x11DisplayName();
+SshConnectionPrivate::SshConnectionPrivate(SshConnection *conn,
+ const SshConnectionParameters &serverInfo)
+ : m_socket(new QTcpSocket(this)), m_state(SocketUnconnected),
+ m_sendFacility(m_socket),
+ m_channelManager(new SshChannelManager(m_sendFacility, this)),
+ m_connParams(serverInfo), m_error(SshNoError), m_ignoreNextPacket(false),
+ m_conn(conn)
+ setupPacketHandlers();
+ if (m_connParams.options & SshLowDelaySocket) {
+ m_socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
+ m_socket->setProxy((m_connParams.options & SshIgnoreDefaultProxy)
+ ? QNetworkProxy::NoProxy : QNetworkProxy::DefaultProxy);
+ m_timeoutTimer.setInterval(m_connParams.timeout * 1000);
+ m_keepAliveTimer.setTimerType(Qt::VeryCoarseTimer);
+ m_keepAliveTimer.setSingleShot(true);
+ m_keepAliveTimer.setInterval(10000);
+ connect(m_channelManager, &SshChannelManager::timeout,
+ this, &SshConnectionPrivate::handleTimeout);
+SshConnectionPrivate::~SshConnectionPrivate()
+void SshConnectionPrivate::setupPacketHandlers()
+ typedef SshConnectionPrivate This;
+ setupPacketHandler(SSH_MSG_KEXINIT, StateList() << SocketConnected
+ << ConnectionEstablished, &This::handleKeyExchangeInitPacket);
+ setupPacketHandler(SSH_MSG_KEXDH_REPLY, StateList() << SocketConnected
+ << ConnectionEstablished, &This::handleKeyExchangeReplyPacket);
+ setupPacketHandler(SSH_MSG_NEWKEYS, StateList() << SocketConnected
+ << ConnectionEstablished, &This::handleNewKeysPacket);
+ setupPacketHandler(SSH_MSG_SERVICE_ACCEPT,
+ StateList() << UserAuthServiceRequested,
+ &This::handleServiceAcceptPacket);
+ if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypePassword
+ || m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods) {
+ setupPacketHandler(SSH_MSG_USERAUTH_PASSWD_CHANGEREQ,
+ StateList() << UserAuthRequested, &This::handlePasswordExpiredPacket);
+ setupPacketHandler(SSH_MSG_GLOBAL_REQUEST,
+ StateList() << ConnectionEstablished, &This::handleGlobalRequest);
+ const StateList authReqList = StateList() << UserAuthRequested;
+ setupPacketHandler(SSH_MSG_USERAUTH_BANNER, authReqList,
+ &This::handleUserAuthBannerPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_SUCCESS, authReqList,
+ &This::handleUserAuthSuccessPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_FAILURE, authReqList,
+ &This::handleUserAuthFailurePacket);
+ if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeKeyboardInteractive
+ setupPacketHandler(SSH_MSG_USERAUTH_INFO_REQUEST, authReqList,
+ &This::handleUserAuthInfoRequestPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_PK_OK, authReqList, &This::handleUserAuthKeyOkPacket);
+ const StateList connectedList
+ = StateList() << ConnectionEstablished;
+ setupPacketHandler(SSH_MSG_CHANNEL_REQUEST, connectedList,
+ &This::handleChannelRequest);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN, connectedList,
+ &This::handleChannelOpen);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN_FAILURE, connectedList,
+ &This::handleChannelOpenFailure);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, connectedList,
+ &This::handleChannelOpenConfirmation);
+ setupPacketHandler(SSH_MSG_CHANNEL_SUCCESS, connectedList,
+ &This::handleChannelSuccess);
+ setupPacketHandler(SSH_MSG_CHANNEL_FAILURE, connectedList,
+ &This::handleChannelFailure);
+ setupPacketHandler(SSH_MSG_CHANNEL_WINDOW_ADJUST, connectedList,
+ &This::handleChannelWindowAdjust);
+ setupPacketHandler(SSH_MSG_CHANNEL_DATA, connectedList,
+ &This::handleChannelData);
+ setupPacketHandler(SSH_MSG_CHANNEL_EXTENDED_DATA, connectedList,
+ &This::handleChannelExtendedData);
+ const StateList connectedOrClosedList
+ = StateList() << SocketUnconnected << ConnectionEstablished;
+ setupPacketHandler(SSH_MSG_CHANNEL_EOF, connectedOrClosedList,
+ &This::handleChannelEof);
+ setupPacketHandler(SSH_MSG_CHANNEL_CLOSE, connectedOrClosedList,
+ &This::handleChannelClose);
+ setupPacketHandler(SSH_MSG_DISCONNECT, StateList() << SocketConnected << WaitingForAgentKeys
+ << UserAuthServiceRequested << UserAuthRequested
+ << ConnectionEstablished, &This::handleDisconnect);
+ setupPacketHandler(SSH_MSG_UNIMPLEMENTED,
+ StateList() << ConnectionEstablished, &This::handleUnimplementedPacket);
+ setupPacketHandler(SSH_MSG_REQUEST_SUCCESS, connectedList,
+ &This::handleRequestSuccess);
+ setupPacketHandler(SSH_MSG_REQUEST_FAILURE, connectedList,
+ &This::handleRequestFailure);
+void SshConnectionPrivate::setupPacketHandler(SshPacketType type,
+ const SshConnectionPrivate::StateList &states,
+ SshConnectionPrivate::PacketHandler handler)
+ m_packetHandlers.insert(type, HandlerInStates(states, handler));
+void SshConnectionPrivate::handleSocketConnected()
+ m_state = SocketConnected;
+ sendData(ClientId);
+void SshConnectionPrivate::handleIncomingData()
+ if (m_state == SocketUnconnected)
+ return; // For stuff queued in the event loop after we've called closeConnection();
+ if (!canUseSocket())
+ m_incomingData += m_socket->readAll();
+ qCDebug(sshLog, "state = %d, remote data size = %d", int(m_state), int(m_incomingData.count()));
+ if (m_serverId.isEmpty())
+ handleServerId();
+ handlePackets();
+ closeConnection(e.error, SshProtocolError, e.errorStringServer,
+ tr("SSH Protocol error: %1").arg(e.errorStringUser));
+ } catch (const SshClientException &e) {
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, e.error, "",
+ e.errorString);
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshInternalError, "",
+ tr("Botan library exception: %1").arg(QString::fromLocal8Bit(e.what())));
+// RFC 4253, 4.2.
+void SshConnectionPrivate::handleServerId()
+ qCDebug(sshLog, "%s: incoming data size = %d, incoming data = '%s'",
+ Q_FUNC_INFO, int(m_incomingData.count()), m_incomingData.data());
+ const int newLinePos = m_incomingData.indexOf('\n');
+ if (newLinePos == -1)
+ return; // Not enough data yet.
+ // Lines not starting with "SSH-" are ignored.
+ if (!m_incomingData.startsWith("SSH-")) {
+ m_incomingData.remove(0, newLinePos + 1);
+ m_serverHasSentDataBeforeId = true;
+ if (newLinePos > 255 - 1) {
+ "Identification string too long.",
+ tr("Server identification string is %1 characters long, but the maximum "
+ "allowed length is 255.").arg(newLinePos + 1));
+ const bool hasCarriageReturn = m_incomingData.at(newLinePos - 1) == '\r';
+ m_serverId = m_incomingData.left(newLinePos);
+ if (hasCarriageReturn)
+ m_serverId.chop(1);
+ if (m_serverId.contains('\0')) {
+ "Identification string contains illegal NUL character.",
+ tr("Server identification string contains illegal NUL character."));
+ // "printable US-ASCII characters, with the exception of whitespace characters
+ // and the minus sign"
+ QString legalString = QLatin1String("[]!\"#$!&'()*+,./0-9:;<=>?@A-Z[\\\\^_`a-z{|}~]+");
+ const QRegularExpression versionIdpattern(QString::fromLatin1("^SSH-(%1)-%1(?: .+)?.*$").arg(legalString));
+ const QRegularExpressionMatch match = versionIdpattern.match(QString::fromLatin1(m_serverId));
+ if (!match.hasMatch()) {
+ "Identification string is invalid.",
+ tr("Server Identification string \"%1\" is invalid.")
+ .arg(QString::fromLatin1(m_serverId)));
+ const QString serverProtoVersion = match.captured(1);
+ if (serverProtoVersion != QLatin1String("2.0") && serverProtoVersion != QLatin1String("1.99")) {
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED,
+ "Invalid protocol version.",
+ tr("Server protocol version is \"%1\", but needs to be 2.0 or 1.99.")
+ .arg(serverProtoVersion));
+ if (serverProtoVersion == QLatin1String("2.0") && !hasCarriageReturn) {
+ if (m_connParams.options & SshEnableStrictConformanceChecks) {
+ tr("Server identification string is invalid (missing carriage return)."));
+ qCWarning(Internal::sshLog, "Server identification string is invalid (missing carriage return).");
+ if (serverProtoVersion == QLatin1String("1.99") && m_serverHasSentDataBeforeId) {
+ "No extra data preceding identification string allowed for 1.99.",
+ tr("Server reports protocol version 1.99, but sends data "
+ "before the identification string, which is not allowed."));
+ qCWarning(Internal::sshLog, "Server reports protocol version 1.99, but sends data "
+ "before the identification string, which is not allowed.");
+ m_keyExchange.reset(new SshKeyExchange(m_connParams, m_sendFacility));
+ m_keyExchange->sendKexInitPacket(m_serverId);
+ m_keyExchangeState = KexInitSent;
+void SshConnectionPrivate::handlePackets()
+void SshConnectionPrivate::handleCurrentPacket()
+ Q_ASSERT(m_incomingPacket.isComplete());
+ Q_ASSERT(m_keyExchangeState == DhInitSent || !m_ignoreNextPacket);
+ if (m_ignoreNextPacket) {
+ m_ignoreNextPacket = false;
+ QHash<SshPacketType, HandlerInStates>::ConstIterator it
+ = m_packetHandlers.constFind(m_incomingPacket.type());
+ if (it == m_packetHandlers.constEnd()) {
+ m_sendFacility.sendMsgUnimplementedPacket(m_incomingPacket.serverSeqNr());
+ if (!it.value().first.contains(m_state)) {
+ handleUnexpectedPacket();
+ (this->*it.value().second)();
+void SshConnectionPrivate::handleKeyExchangeInitPacket()
+ if (m_keyExchangeState != NoKeyExchange
+ && m_keyExchangeState != KexInitSent) {
+ "Unexpected packet.", tr("Unexpected packet of type %1.")
+ .arg(m_incomingPacket.type()));
+ // Server-initiated re-exchange.
+ if (m_keyExchangeState == NoKeyExchange) {
+ // If the server sends a guessed packet, the guess must be wrong,
+ // because the algorithms we support require us to initiate the
+ // key exchange.
+ if (m_keyExchange->sendDhInitPacket(m_incomingPacket)) {
+ m_ignoreNextPacket = true;
+ m_keyExchangeState = DhInitSent;
+void SshConnectionPrivate::handleKeyExchangeReplyPacket()
+ if (m_keyExchangeState != DhInitSent) {
+ m_keyExchange->sendNewKeysPacket(m_incomingPacket,
+ ClientId.left(ClientId.size() - 2));
+ m_hostFingerprint = m_keyExchange->hostKeyFingerprint();
+ m_sendFacility.recreateKeys(*m_keyExchange);
+ m_keyExchangeState = NewKeysSent;
+void SshConnectionPrivate::handleNewKeysPacket()
+ if (m_keyExchangeState != NewKeysSent) {
+ m_incomingPacket.recreateKeys(*m_keyExchange);
+ m_keyExchange.reset();
+ m_keyExchangeState = NoKeyExchange;
+ if (m_state == SocketConnected) {
+ m_sendFacility.sendUserAuthServiceRequestPacket();
+ m_state = UserAuthServiceRequested;
+void SshConnectionPrivate::handleServiceAcceptPacket()
+ switch (m_connParams.authenticationType) {
+ case SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods:
+ m_triedAllPasswordBasedMethods = false;
+ case SshConnectionParameters::AuthenticationTypePassword:
+ m_sendFacility.sendUserAuthByPasswordRequestPacket(m_connParams.userName().toUtf8(),
+ SshCapabilities::SshConnectionService, m_connParams.password().toUtf8());
+ case SshConnectionParameters::AuthenticationTypeKeyboardInteractive:
+ m_sendFacility.sendUserAuthByKeyboardInteractiveRequestPacket(m_connParams.userName().toUtf8(),
+ SshCapabilities::SshConnectionService);
+ case SshConnectionParameters::AuthenticationTypePublicKey:
+ authenticateWithPublicKey();
+ case SshConnectionParameters::AuthenticationTypeAgent:
+ if (SshAgent::publicKeys().isEmpty()) {
+ if (m_agentKeysUpToDate)
+ throw SshClientException(SshAuthenticationError, tr("ssh-agent has no keys."));
+ qCDebug(sshLog) << "agent has no keys yet, waiting";
+ m_state = WaitingForAgentKeys;
+ tryAllAgentKeys();
+ m_state = UserAuthRequested;
+void SshConnectionPrivate::handlePasswordExpiredPacket()
+ if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods
+ && m_triedAllPasswordBasedMethods) {
+ // This means we just tried to authorize via "keyboard-interactive", in which case
+ // this type of packet is not allowed.
+ throw SshClientException(SshAuthenticationError, tr("Password expired."));
+void SshConnectionPrivate::handleUserAuthInfoRequestPacket()
+ && !m_triedAllPasswordBasedMethods) {
+ // This means we just tried to authorize via "password", in which case
+ const SshUserAuthInfoRequestPacket requestPacket
+ = m_incomingPacket.extractUserAuthInfoRequest();
+ QStringList responses;
+ responses.reserve(requestPacket.prompts.count());
+ // Not very interactive, admittedly, but we don't want to be for now.
+ for (int i = 0; i < requestPacket.prompts.count(); ++i)
+ responses << m_connParams.password();
+ m_sendFacility.sendUserAuthInfoResponsePacket(responses);
+void SshConnectionPrivate::handleUserAuthBannerPacket()
+ emit dataAvailable(m_incomingPacket.extractUserAuthBanner().message);
+void SshConnectionPrivate::handleUnexpectedPacket()
+void SshConnectionPrivate::handleGlobalRequest()
+ m_sendFacility.sendRequestFailurePacket();
+void SshConnectionPrivate::handleUserAuthSuccessPacket()
+ m_state = ConnectionEstablished;
+ emit connected();
+ m_lastInvalidMsgSeqNr = InvalidSeqNr;
+ connect(&m_keepAliveTimer, &QTimer::timeout, this, &SshConnectionPrivate::sendKeepAlivePacket);
+ m_keepAliveTimer.start();
+void SshConnectionPrivate::handleUserAuthFailurePacket()
+ if (!m_pendingKeyChecks.isEmpty()) {
+ const QByteArray key = m_pendingKeyChecks.dequeue();
+ SshAgent::removeDataToSign(key, tokenForAgent());
+ qCDebug(sshLog) << "server rejected one of the keys supplied by the agent,"
+ << m_pendingKeyChecks.count() << "keys remaining";
+ if (m_pendingKeyChecks.isEmpty() && m_agentKeyToUse.isEmpty()) {
+ throw SshClientException(SshAuthenticationError, tr("The server rejected all keys "
+ "known to the ssh-agent."));
+ // TODO: Evaluate "authentications that can continue" field and act on it.
+ if (m_connParams.authenticationType
+ == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods
+ m_triedAllPasswordBasedMethods = true;
+ m_sendFacility.sendUserAuthByKeyboardInteractiveRequestPacket(
+ m_connParams.userName().toUtf8(),
+ QString errorMsg;
+ errorMsg = tr("Server rejected key.");
+ errorMsg = tr("Server rejected password.");
+ throw SshClientException(SshAuthenticationError, errorMsg);
+void SshConnectionPrivate::handleUserAuthKeyOkPacket()
+ const SshUserAuthPkOkPacket &msg = m_incomingPacket.extractUserAuthPkOk();
+ qCDebug(sshLog) << "server accepted key of type" << msg.algoName;
+ if (m_pendingKeyChecks.isEmpty()) {
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected packet",
+ tr("Server sent unexpected SSH_MSG_USERAUTH_PK_OK packet."));
+ if (key != msg.keyBlob) {
+ // The server must answer the requests in the order we sent them.
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected packet content",
+ tr("Server sent unexpected key in SSH_MSG_USERAUTH_PK_OK packet."));
+ const uint token = tokenForAgent();
+ if (!m_agentKeyToUse.isEmpty()) {
+ qCDebug(sshLog) << "another key has already been accepted, ignoring this one";
+ SshAgent::removeDataToSign(key, token);
+ m_agentKeyToUse = key;
+ qCDebug(sshLog) << "requesting signature from agent";
+ SshAgent::requestSignature(key, token);
+void SshConnectionPrivate::handleDebugPacket()
+ const SshDebug &msg = m_incomingPacket.extractDebug();
+ if (msg.display)
+ emit dataAvailable(msg.message);
+void SshConnectionPrivate::handleUnimplementedPacket()
+ const SshUnimplemented &msg = m_incomingPacket.extractUnimplemented();
+ if (msg.invalidMsgSeqNr != m_lastInvalidMsgSeqNr) {
+ "Unexpected packet", tr("The server sent an unexpected SSH packet "
+ "of type SSH_MSG_UNIMPLEMENTED."));
+void SshConnectionPrivate::handleChannelRequest()
+ m_channelManager->handleChannelRequest(m_incomingPacket);
+void SshConnectionPrivate::handleChannelOpen()
+ m_channelManager->handleChannelOpen(m_incomingPacket);
+void SshConnectionPrivate::handleChannelOpenFailure()
+ m_channelManager->handleChannelOpenFailure(m_incomingPacket);
+void SshConnectionPrivate::handleChannelOpenConfirmation()
+ m_channelManager->handleChannelOpenConfirmation(m_incomingPacket);
+void SshConnectionPrivate::handleChannelSuccess()
+ m_channelManager->handleChannelSuccess(m_incomingPacket);
+void SshConnectionPrivate::handleChannelFailure()
+ m_channelManager->handleChannelFailure(m_incomingPacket);
+void SshConnectionPrivate::handleChannelWindowAdjust()
+ m_channelManager->handleChannelWindowAdjust(m_incomingPacket);
+void SshConnectionPrivate::handleChannelData()
+ m_channelManager->handleChannelData(m_incomingPacket);
+void SshConnectionPrivate::handleChannelExtendedData()
+ m_channelManager->handleChannelExtendedData(m_incomingPacket);
+void SshConnectionPrivate::handleChannelEof()
+ m_channelManager->handleChannelEof(m_incomingPacket);
+void SshConnectionPrivate::handleChannelClose()
+ m_channelManager->handleChannelClose(m_incomingPacket);
+void SshConnectionPrivate::handleDisconnect()
+ const SshDisconnect msg = m_incomingPacket.extractDisconnect();
+ throw SshServerException(SSH_DISCONNECT_CONNECTION_LOST,
+ "", tr("Server closed connection: %1").arg(msg.description));
+void SshConnectionPrivate::handleRequestSuccess()
+ m_channelManager->handleRequestSuccess(m_incomingPacket);
+void SshConnectionPrivate::handleRequestFailure()
+ m_channelManager->handleRequestFailure(m_incomingPacket);
+void SshConnectionPrivate::sendData(const QByteArray &data)
+ if (canUseSocket())
+ m_socket->write(data);
+uint SshConnectionPrivate::tokenForAgent() const
+ return qHash(m_sendFacility.sessionId());
+void SshConnectionPrivate::handleSocketDisconnected()
+ closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshClosedByServerError,
+ "Connection closed unexpectedly.",
+ tr("Connection closed unexpectedly."));
+void SshConnectionPrivate::handleSocketError()
+ if (m_error == SshNoError) {
+ closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshSocketError,
+ "Network error", m_socket->errorString());
+void SshConnectionPrivate::handleTimeout()
+ const QString errorMessage = m_state == WaitingForAgentKeys
+ ? tr("Timeout waiting for keys from ssh-agent.")
+ : tr("Timeout waiting for reply from server.");
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshTimeoutError, "", errorMessage);
+void SshConnectionPrivate::sendKeepAlivePacket()
+ // This type of message is not allowed during key exchange.
+ if (m_keyExchangeState != NoKeyExchange) {
+ Q_ASSERT(m_lastInvalidMsgSeqNr == InvalidSeqNr);
+ m_lastInvalidMsgSeqNr = m_sendFacility.nextClientSeqNr();
+ m_sendFacility.sendInvalidPacket();
+ m_timeoutTimer.start();
+void SshConnectionPrivate::handleAgentKeysUpdated()
+ m_agentKeysUpToDate = true;
+ if (m_state == WaitingForAgentKeys) {
+void SshConnectionPrivate::handleSignatureFromAgent(const QByteArray &key,
+ const QByteArray &signature, uint token)
+ if (token != tokenForAgent()) {
+ qCDebug(sshLog) << "signature is for different connection, ignoring";
+ QSSH_ASSERT(key == m_agentKeyToUse);
+ m_agentSignature = signature;
+void SshConnectionPrivate::tryAllAgentKeys()
+ const QList<QByteArray> &keys = SshAgent::publicKeys();
+ if (keys.isEmpty())
+ qCDebug(sshLog) << "trying authentication with" << keys.count()
+ << "public keys received from agent";
+ for (const QByteArray &key : keys) {
+ m_sendFacility.sendQueryPublicKeyPacket(m_connParams.userName().toUtf8(),
+ SshCapabilities::SshConnectionService, key);
+ m_pendingKeyChecks.enqueue(key);
+void SshConnectionPrivate::authenticateWithPublicKey()
+ qCDebug(sshLog) << "sending actual authentication request";
+ QByteArray signature;
+ if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeAgent) {
+ // Agent is not needed anymore after this point.
+ disconnect(&SshAgent::instance(), nullptr, this, nullptr);
+ key = m_agentKeyToUse;
+ signature = m_agentSignature;
+ m_sendFacility.sendUserAuthByPublicKeyRequestPacket(m_connParams.userName().toUtf8(),
+ SshCapabilities::SshConnectionService, key, signature);
+void SshConnectionPrivate::setAgentError()
+ m_error = SshAgentError;
+ m_errorString = SshAgent::errorString();
+ emit error(m_error);
+void SshConnectionPrivate::connectToHost()
+ QSSH_ASSERT_AND_RETURN(m_state == SocketUnconnected);
+ m_incomingPacket.reset();
+ m_sendFacility.reset();
+ m_error = SshNoError;
+ m_errorString.clear();
+ m_serverId.clear();
+ m_serverHasSentDataBeforeId = false;
+ m_agentSignature.clear();
+ m_agentKeysUpToDate = false;
+ m_pendingKeyChecks.clear();
+ m_agentKeyToUse.clear();
+ createPrivateKey();
+ } catch (const SshClientException &ex) {
+ m_error = ex.error;
+ m_errorString = ex.errorString;
+ if (SshAgent::hasError()) {
+ setAgentError();
+ connect(&SshAgent::instance(), &SshAgent::errorOccurred,
+ this, &SshConnectionPrivate::setAgentError);
+ connect(&SshAgent::instance(), &SshAgent::keysUpdated,
+ this, &SshConnectionPrivate::handleAgentKeysUpdated);
+ SshAgent::refreshKeys();
+ connect(&SshAgent::instance(), &SshAgent::signatureAvailable,
+ this, &SshConnectionPrivate::handleSignatureFromAgent);
+ connect(m_socket, &QAbstractSocket::connected,
+ this, &SshConnectionPrivate::handleSocketConnected);
+ connect(m_socket, &QIODevice::readyRead,
+ this, &SshConnectionPrivate::handleIncomingData);
+// connect(m_socket, &QAbstractSocket::errorOccurred, this, &SshConnectionPrivate::handleSocketError);
+ connect(m_socket, &QAbstractSocket::disconnected,
+ this, &SshConnectionPrivate::handleSocketDisconnected);
+ connect(&m_timeoutTimer, &QTimer::timeout, this, &SshConnectionPrivate::handleTimeout);
+ m_state = SocketConnecting;
+ m_socket->connectToHost(m_connParams.host(), m_connParams.port());
+void SshConnectionPrivate::closeConnection(SshErrorCode sshError,
+ SshError userError, const QByteArray &serverErrorString,
+ const QString &userErrorString)
+ // Prevent endless loops by recursive exceptions.
+ if (m_state == SocketUnconnected || m_error != SshNoError)
+ m_error = userError;
+ m_errorString = userErrorString;
+ disconnect(m_socket, nullptr, this, nullptr);
+ disconnect(&m_timeoutTimer, nullptr, this, nullptr);
+ m_keepAliveTimer.stop();
+ disconnect(&m_keepAliveTimer, nullptr, this, nullptr);
+ m_channelManager->closeAllChannels(SshChannelManager::CloseAllAndReset);
+ // Crypto initialization failed
+ if (m_sendFacility.encrypterIsValid()) {
+ m_sendFacility.sendDisconnectPacket(sshError, serverErrorString);
+ } catch (...) {} // Nothing sensible to be done here.
+ if (m_error != SshNoError)
+ emit error(userError);
+ if (m_state == ConnectionEstablished)
+ emit disconnected();
+ m_socket->disconnectFromHost();
+ m_state = SocketUnconnected;
+bool SshConnectionPrivate::canUseSocket() const
+ return m_socket->isValid()
+ && m_socket->state() == QAbstractSocket::ConnectedState;
+void SshConnectionPrivate::createPrivateKey()
+ if (m_connParams.privateKeyFile.isEmpty())
+ throw SshClientException(SshKeyFileError, tr("No private key file given."));
+ QFile keyFile(m_connParams.privateKeyFile);
+ if (!keyFile.open(QIODevice::ReadOnly)) {
+ tr("Private key file error: %1").arg(keyFile.errorString()));
+ m_sendFacility.createAuthenticationKey(keyFile.readAll());
+QSharedPointer<SshRemoteProcess> SshConnectionPrivate::createRemoteProcess(const QByteArray &command)
+ return m_channelManager->createRemoteProcess(command);
+QSharedPointer<SshRemoteProcess> SshConnectionPrivate::createRemoteShell()
+ return m_channelManager->createRemoteShell();
+QSharedPointer<SftpChannel> SshConnectionPrivate::createSftpChannel()
+ return m_channelManager->createSftpChannel();
+SshDirectTcpIpTunnel::Ptr SshConnectionPrivate::createDirectTunnel(const QString &originatingHost,
+ return m_channelManager->createDirectTunnel(originatingHost, originatingPort, remoteHost,
+ remotePort);
+SshTcpIpForwardServer::Ptr SshConnectionPrivate::createForwardServer(const QString &bindAddress,
+ quint16 bindPort)
+ return m_channelManager->createForwardServer(bindAddress, bindPort);
+const quint64 SshConnectionPrivate::InvalidSeqNr = static_cast<quint64>(-1);
@@ -0,0 +1,298 @@
+#ifndef SSHCONNECTION_H
+#define SSHCONNECTION_H
+#include "sshhostkeydatabase.h"
+#include <QFlags>
+#include <QMetaType>
+#include <QHostAddress>
+#include <QUrl>
+class SshConnectionPrivate;
+ * \brief Flags that control various general behavior
+enum SshConnectionOption {
+ /// Set this to ignore the system defined proxy
+ SshIgnoreDefaultProxy = 0x1,
+ /// Fail instead of warn if the remote host violates the standard
+ SshEnableStrictConformanceChecks = 0x2,
+ /// Set the QAbstractSocket::LowDelayOption, which is the same as TCP_NODELAY
+ SshLowDelaySocket = 0x4
+Q_DECLARE_FLAGS(SshConnectionOptions, SshConnectionOption)
+ * \brief How strict to be when checking the remote key
+enum SshHostKeyCheckingMode {
+ /// Ignore the remote key
+ SshHostKeyCheckingNone,
+ /// Fail connection if either there is no key stored for this host or the key is not the same as earlier
+ SshHostKeyCheckingStrict,
+ /// Allow connecting if there is no stored key for the host, but fail if the key has changed
+ SshHostKeyCheckingAllowNoMatch,
+ /// Continue connection if the key doesn't match the stored key for the host
+ SshHostKeyCheckingAllowMismatch
+ * \brief Class to use to specify parameters used during connection.
+class QSSH_EXPORT SshConnectionParameters
+ * \brief What kinds of authentication to attempt
+ enum AuthenticationType {
+ AuthenticationTypePassword, ///< Only attempt to connect using the password set with setPassword().
+ AuthenticationTypePublicKey, ///< Only attempt to authenticate with public key
+ /// Only attempt keyboard interactive authentication.
+ /// For now this only changes what to send to the server,
+ /// we will still just try to use the password set here.
+ AuthenticationTypeKeyboardInteractive,
+ /// Any method using the password set with setPassword().
+ /// Some servers disable \a "password", others disable \a "keyboard-interactive"
+ AuthenticationTypeTryAllPasswordBasedMethods,
+ /// ssh-agent authentication only
+ AuthenticationTypeAgent,
+ SshConnectionParameters();
+ * \brief Returns the hostname or IP set with setHost()
+ QString host() const { return url.host(); }
+ * \brief Returns the port set with setPort()
+ int port() const { return url.port(); }
+ * \brief Returns the username set with setUsername()
+ QString userName() const { return url.userName(); }
+ * \brief Returns the password set with setPassword()
+ QString password() const { return url.password(); }
+ * \brief Sets the hostname or IP to connect to
+ * \param host The remote host
+ void setHost(const QString &host) { url.setHost(host); }
+ * \brief Sets the remote port to use
+ * \param port
+ void setPort(int port) { url.setPort(port); }
+ * \brief Sets the username to use
+ * \param name Username
+ void setUserName(const QString &name) { url.setUserName(name); }
+ * \brief Sets the password to attempt to use
+ * \param password
+ void setPassword(const QString &password) { url.setPassword(password); }
+ QUrl url;
+ QString privateKeyFile;
+ int timeout; // In seconds.
+ AuthenticationType authenticationType;
+ SshConnectionOptions options;
+ SshHostKeyCheckingMode hostKeyCheckingMode;
+ SshHostKeyDatabasePtr hostKeyDatabase;
+/// @cond
+QSSH_EXPORT bool operator==(const SshConnectionParameters &p1, const SshConnectionParameters &p2);
+QSSH_EXPORT bool operator!=(const SshConnectionParameters &p1, const SshConnectionParameters &p2);
+/// @endcond
+ * \brief Network connection info.
+class QSSH_EXPORT SshConnectionInfo
+ SshConnectionInfo() : localPort(0), peerPort(0) {}
+ SshConnectionInfo(const QHostAddress &la, quint16 lp, const QHostAddress &pa, quint16 pp)
+ : localAddress(la), localPort(lp), peerAddress(pa), peerPort(pp) {}
+ QHostAddress localAddress;
+ quint16 localPort;
+ QHostAddress peerAddress;
+ quint16 peerPort;
+ \class QSsh::SshConnection
+ \brief This class provides an SSH connection, implementing protocol version 2.0
+ See acquireConnection() which provides a pool mechanism for re-use.
+ It can spawn channels for remote execution and SFTP operations (version 3).
+ It operates asynchronously (non-blocking) and is not thread-safe.
+class QSSH_EXPORT SshConnection : public QObject
+ * \brief The current state of a connection
+ enum State { Unconnected, Connecting, Connected };
+ * \param serverInfo serverInfo connection parameters
+ * \param parent Parent object.
+ explicit SshConnection(const SshConnectionParameters &serverInfo, QObject *parent = nullptr);
+ void connectToHost();
+ void disconnectFromHost();
+ * \brief Current state of this connection
+ * \brief Returns the error state of the connection
+ * \returns If there is no error, returns \ref SshNoError if the connection is OK
+ SshError errorState() const;
+ QString errorString() const;
+ SshConnectionParameters connectionParameters() const;
+ SshConnectionInfo connectionInfo() const;
+ ~SshConnection();
+ * \brief Use this to launch remote commands
+ * \param command The command to execute
+ * \brief Creates a remote interactive session with a shell
+ // -1 if an error occurred, number of channels closed otherwise.
+ int closeAllChannels();
+ const QByteArray &hostKeyFingerprint() const;
+ * \brief The X11 display name used for X11 forwarding
+ * \return The name of the X11 display set for this connection
+ QString x11DisplayName() const;
+ * \brief Emitted when ready for use
+ void connected();
+ * \brief Emitted when the connection has been closed
+ void disconnected();
+ * \brief Emitted when data has been received
+ * \param message The content of the data, same as the output you would get when running \a ssh on the command line
+ void dataAvailable(const QString &message);
+ * \brief Emitted when an error occured
+ void error(QSsh::SshError);
+ Internal::SshConnectionPrivate *d;
+Q_DECLARE_METATYPE(QSsh::SshConnectionParameters::AuthenticationType)
+#endif // SSHCONNECTION_H
@@ -0,0 +1,204 @@
+#ifndef SSHCONNECTION_P_H
+#define SSHCONNECTION_P_H
+#include <QScopedPointer>
+class QTcpSocket;
+// NOTE: When you add stuff here, don't forget to update m_packetHandlers.
+enum SshStateInternal {
+ SocketUnconnected, // initial and after disconnect
+ SocketConnecting, // After connectToHost()
+ SocketConnected, // After socket's connected() signal
+ UserAuthServiceRequested,
+ WaitingForAgentKeys,
+ UserAuthRequested,
+ ConnectionEstablished // After service has been started
+ // ...
+enum SshKeyExchangeState {
+ NoKeyExchange,
+ KexInitSent,
+ DhInitSent,
+ NewKeysSent,
+ KeyExchangeSuccess // After server's DH_REPLY message
+class SshConnectionPrivate : public QObject
+ friend class QSsh::SshConnection;
+ SshConnectionPrivate(SshConnection *conn,
+ const SshConnectionParameters &serverInfo);
+ ~SshConnectionPrivate();
+ void closeConnection(SshErrorCode sshError, SshError userError,
+ const QByteArray &serverErrorString, const QString &userErrorString);
+ SshStateInternal state() const { return m_state; }
+ SshError errorState() const { return m_error; }
+ QString errorString() const { return m_errorString; }
+ const QByteArray &hostKeyFingerprint() const { return m_hostFingerprint; }
+ void handleSocketConnected();
+ void handleSocketDisconnected();
+ void handleTimeout();
+ void sendKeepAlivePacket();
+ void handleAgentKeysUpdated();
+ void handleSignatureFromAgent(const QByteArray &key, const QByteArray &signature, uint token);
+ void tryAllAgentKeys();
+ void authenticateWithPublicKey();
+ void setAgentError();
+ void handleServerId();
+ void handlePackets();
+ void handleKeyExchangeInitPacket();
+ void handleKeyExchangeReplyPacket();
+ void handleNewKeysPacket();
+ void handleServiceAcceptPacket();
+ void handlePasswordExpiredPacket();
+ void handleUserAuthInfoRequestPacket();
+ void handleUserAuthSuccessPacket();
+ void handleUserAuthFailurePacket();
+ void handleUserAuthKeyOkPacket();
+ void handleUserAuthBannerPacket();
+ void handleUnexpectedPacket();
+ void handleGlobalRequest();
+ void handleDebugPacket();
+ void handleUnimplementedPacket();
+ void handleChannelRequest();
+ void handleChannelOpen();
+ void handleChannelOpenFailure();
+ void handleChannelOpenConfirmation();
+ void handleChannelSuccess();
+ void handleChannelFailure();
+ void handleChannelWindowAdjust();
+ void handleChannelData();
+ void handleChannelExtendedData();
+ void handleDisconnect();
+ void handleRequestSuccess();
+ void handleRequestFailure();
+ bool canUseSocket() const;
+ void createPrivateKey();
+ uint tokenForAgent() const;
+ typedef void (SshConnectionPrivate::*PacketHandler)();
+ typedef QList<SshStateInternal> StateList;
+ void setupPacketHandlers();
+ void setupPacketHandler(SshPacketType type, const StateList &states,
+ PacketHandler handler);
+ typedef QPair<StateList, PacketHandler> HandlerInStates;
+ QHash<SshPacketType, HandlerInStates> m_packetHandlers;
+ static const quint64 InvalidSeqNr;
+ QTcpSocket *m_socket;
+ SshStateInternal m_state;
+ SshKeyExchangeState m_keyExchangeState;
+ SshIncomingPacket m_incomingPacket;
+ SshSendFacility m_sendFacility;
+ SshChannelManager * const m_channelManager;
+ const SshConnectionParameters m_connParams;
+ SshError m_error;
+ QString m_errorString;
+ QScopedPointer<SshKeyExchange> m_keyExchange;
+ QByteArray m_hostFingerprint;
+ QTimer m_keepAliveTimer;
+ bool m_ignoreNextPacket;
+ SshConnection *m_conn;
+ quint64 m_lastInvalidMsgSeqNr;
+ QByteArray m_serverId;
+ QByteArray m_agentSignature;
+ QQueue<QByteArray> m_pendingKeyChecks;
+ QByteArray m_agentKeyToUse;
+ bool m_serverHasSentDataBeforeId;
+ bool m_triedAllPasswordBasedMethods;
+ bool m_agentKeysUpToDate;
+#endif // SSHCONNECTION_P_H
@@ -0,0 +1,276 @@
+#include <QThread>
+class UnaquiredConnection {
+ UnaquiredConnection(SshConnection *conn) : connection(conn), scheduledForRemoval(false) {}
+ SshConnection *connection;
+ bool scheduledForRemoval;
+bool operator==(UnaquiredConnection c1, UnaquiredConnection c2) {
+ return c1.connection == c2.connection;
+bool operator!=(UnaquiredConnection c1, UnaquiredConnection c2) {
+ return !(c1 == c2);
+class SshConnectionManager : public QObject
+ SshConnectionManager()
+ moveToThread(QCoreApplication::instance()->thread());
+ connect(&m_removalTimer, &QTimer::timeout,
+ this, &SshConnectionManager::removeInactiveConnections);
+ m_removalTimer.setTimerType(Qt::VeryCoarseTimer);
+ m_removalTimer.start(150000); // For a total timeout of five minutes.
+ ~SshConnectionManager()
+ for (const UnaquiredConnection &connection : m_unacquiredConnections) {
+ disconnect(connection.connection, nullptr, this, nullptr);
+ delete connection.connection;
+ QSSH_ASSERT(m_acquiredConnections.isEmpty());
+ QSSH_ASSERT(m_deprecatedConnections.isEmpty());
+ SshConnection *acquireConnection(const SshConnectionParameters &sshParams)
+ QMutexLocker locker(&m_listMutex);
+ // Check in-use connections:
+ for (SshConnection * const connection : m_acquiredConnections) {
+ if (connection->connectionParameters() != sshParams)
+ if (connection->thread() != QThread::currentThread())
+ if (m_deprecatedConnections.contains(connection)) // we were asked to no longer use this one...
+ m_acquiredConnections.append(connection);
+ return connection;
+ // Check cached open connections:
+ for (const UnaquiredConnection &c : m_unacquiredConnections) {
+ SshConnection * const connection = c.connection;
+ if (connection->state() != SshConnection::Connected
+ || connection->connectionParameters() != sshParams)
+ if (connection->thread() != QThread::currentThread()) {
+ if (connection->channelCount() != 0)
+ QMetaObject::invokeMethod(this, "switchToCallerThread",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(SshConnection *, connection),
+ Q_ARG(QObject *, QThread::currentThread()));
+ m_unacquiredConnections.removeOne(c);
+ // create a new connection:
+ SshConnection * const connection = new SshConnection(sshParams);
+ connect(connection, &SshConnection::disconnected,
+ this, &SshConnectionManager::cleanup);
+ void releaseConnection(SshConnection *connection)
+ const bool wasAquired = m_acquiredConnections.removeOne(connection);
+ QSSH_ASSERT_AND_RETURN(wasAquired);
+ if (m_acquiredConnections.contains(connection))
+ bool doDelete = false;
+ connection->moveToThread(QCoreApplication::instance()->thread());
+ if (m_deprecatedConnections.removeOne(connection)
+ || connection->state() != SshConnection::Connected) {
+ doDelete = true;
+ QSSH_ASSERT_AND_RETURN(!m_unacquiredConnections.contains(UnaquiredConnection(connection)));
+ // It can happen that two or more connections with the same parameters were acquired
+ // if the clients were running in different threads. Only keep one of them in
+ // such a case.
+ bool haveConnection = false;
+ if (c.connection->connectionParameters() == connection->connectionParameters()) {
+ haveConnection = true;
+ if (!haveConnection) {
+ connection->closeAllChannels(); // Clean up after neglectful clients.
+ m_unacquiredConnections.append(UnaquiredConnection(connection));
+ if (doDelete) {
+ disconnect(connection, nullptr, this, nullptr);
+ m_deprecatedConnections.removeAll(connection);
+ connection->deleteLater();
+ void forceNewConnection(const SshConnectionParameters &sshParams)
+ for (int i = 0; i < m_unacquiredConnections.count(); ++i) {
+ SshConnection * const connection = m_unacquiredConnections.at(i).connection;
+ if (connection->connectionParameters() == sshParams) {
+ delete connection;
+ m_unacquiredConnections.removeAt(i);
+ if (!m_deprecatedConnections.contains(connection))
+ m_deprecatedConnections.append(connection);
+ Q_INVOKABLE void switchToCallerThread(SshConnection *connection, QObject *threadObj)
+ connection->moveToThread(qobject_cast<QThread *>(threadObj));
+ void cleanup()
+ SshConnection *currentConnection = qobject_cast<SshConnection *>(sender());
+ if (!currentConnection)
+ if (m_unacquiredConnections.removeOne(UnaquiredConnection(currentConnection))) {
+ disconnect(currentConnection, nullptr, this, nullptr);
+ currentConnection->deleteLater();
+ void removeInactiveConnections()
+ for (int i = m_unacquiredConnections.count() - 1; i >= 0; --i) {
+ UnaquiredConnection &c = m_unacquiredConnections[i];
+ if (c.scheduledForRemoval) {
+ disconnect(c.connection, nullptr, this, nullptr);
+ c.connection->deleteLater();
+ c.scheduledForRemoval = true;
+ // We expect the number of concurrently open connections to be small.
+ // If that turns out to not be the case, we can still use a data
+ // structure with faster access.
+ QList<UnaquiredConnection> m_unacquiredConnections;
+ // Can contain the same connection more than once; this acts as a reference count.
+ QList<SshConnection *> m_acquiredConnections;
+ QList<SshConnection *> m_deprecatedConnections;
+ QMutex m_listMutex;
+ QTimer m_removalTimer;
+static QMutex instanceMutex;
+static Internal::SshConnectionManager &instance()
+ static Internal::SshConnectionManager manager;
+ return manager;
+SshConnection *acquireConnection(const SshConnectionParameters &sshParams)
+ QMutexLocker locker(&instanceMutex);
+ return instance().acquireConnection(sshParams);
+void releaseConnection(SshConnection *connection)
+ instance().releaseConnection(connection);
+void forceNewConnection(const SshConnectionParameters &sshParams)
+ instance().forceNewConnection(sshParams);
+#include "sshconnectionmanager.moc"
@@ -0,0 +1,63 @@
+#ifndef SSHCONNECTIONMANAGER_H
+#define SSHCONNECTIONMANAGER_H
+class SshConnection;
+ * \brief Creates a new connection or returns an existing one if there already is one with identical sshParams
+ * \param sshParams Parameters used during connection
+ * \return A connection
+QSSH_EXPORT SshConnection *acquireConnection(const SshConnectionParameters &sshParams);
+ * \brief Call this when you are done with a connection, might be disconnected and destroyed if there are no others who have called acquireConnection()
+ * \param connection The connection to be released
+QSSH_EXPORT void releaseConnection(SshConnection *connection);
+ * \brief Creates a new connection, unlike acquireConnection() it will not reuse an existing one.
+ * Make sure the next acquireConnection with the given parameters will return a new connection.
+QSSH_EXPORT void forceNewConnection(const SshConnectionParameters &sshParams);
+#endif // SSHCONNECTIONMANAGER_H
@@ -0,0 +1,465 @@
+#include "sshbotanconversions_p.h"
+#include "sshkeypasswordretriever_p.h"
+#include <botan/block_cipher.h>
+#include <botan/hash.h>
+#include <botan/pkcs8.h>
+#include <botan/ber_dec.h>
+#include <botan/pubkey.h>
+#include <botan/filters.h>
+#include <QDebug>
+#include <string>
+SshAbstractCryptoFacility::SshAbstractCryptoFacility()
+ : m_cipherBlockSize(0), m_macLength(0)
+SshAbstractCryptoFacility::~SshAbstractCryptoFacility() {}
+void SshAbstractCryptoFacility::clearKeys()
+ m_cipherBlockSize = 0;
+ m_macLength = 0;
+ m_sessionId.clear();
+ m_pipe.reset(nullptr);
+ m_hMac.reset(nullptr);
+SshAbstractCryptoFacility::Mode SshAbstractCryptoFacility::getMode(const QByteArray &algoName)
+ if (algoName.endsWith("-ctr"))
+ return CtrMode;
+ if (algoName.endsWith("-cbc"))
+ return CbcMode;
+ .arg(QString::fromLatin1(algoName)));
+void SshAbstractCryptoFacility::recreateKeys(const SshKeyExchange &kex)
+ checkInvariant();
+ if (m_sessionId.isEmpty())
+ m_sessionId = kex.h();
+ const QByteArray &rfcCryptAlgoName = cryptAlgoName(kex);
+ { // Don't know how else to get this with the new botan API
+ std::unique_ptr<BlockCipher> cipher
+ = BlockCipher::create_or_throw(botanCryptAlgoName(rfcCryptAlgoName));
+ m_cipherBlockSize = static_cast<quint32>(cipher->block_size());
+ const QByteArray ivData = generateHash(kex, ivChar(), m_cipherBlockSize);
+ const InitializationVector iv(convertByteArray(ivData), m_cipherBlockSize);
+ Keyed_Filter * const cipherMode
+ = makeCipherMode(botanCipherAlgoName(rfcCryptAlgoName), getMode(rfcCryptAlgoName));
+ const quint32 keySize = static_cast<quint32>(cipherMode->key_spec().maximum_keylength());
+ const QByteArray cryptKeyData = generateHash(kex, keyChar(), keySize);
+ SymmetricKey cryptKey(convertByteArray(cryptKeyData), keySize);
+ cipherMode->set_key(cryptKey);
+ cipherMode->set_iv(iv);
+ m_pipe.reset(new Pipe(cipherMode));
+ m_macLength = botanHMacKeyLen(hMacAlgoName(kex));
+ const QByteArray hMacKeyData = generateHash(kex, macChar(), macLength());
+ SymmetricKey hMacKey(convertByteArray(hMacKeyData), macLength());
+ m_hMac = MessageAuthenticationCode::create_or_throw("HMAC(" + std::string(botanHMacAlgoName(hMacAlgoName(kex))) + ")");
+ m_hMac->set_key(hMacKey);
+void SshAbstractCryptoFacility::convert(QByteArray &data, quint32 offset,
+ quint32 dataSize) const
+ Q_ASSERT(offset + dataSize <= static_cast<quint32>(data.size()));
+ // Session id empty => No key exchange has happened yet.
+ if (dataSize == 0 || m_sessionId.isEmpty())
+ if (dataSize % cipherBlockSize() != 0) {
+ "Invalid packet size");
+ m_pipe->process_msg(reinterpret_cast<const byte *>(data.constData()) + offset,
+ dataSize);
+ // Can't use Pipe::LAST_MESSAGE because of a VC bug.
+ quint32 bytesRead = static_cast<quint32>(m_pipe->read(
+ reinterpret_cast<byte *>(data.data()) + offset, dataSize, m_pipe->message_count() - 1));
+ if (bytesRead != dataSize) {
+ QLatin1String("Internal error: Botan::Pipe::read() returned unexpected value"));
+Keyed_Filter *SshAbstractCryptoFacility::makeCtrCipherMode(const QByteArray &cipher)
+ StreamCipher_Filter *filter = new StreamCipher_Filter(cipher.toStdString());
+ return filter;
+QByteArray SshAbstractCryptoFacility::generateMac(const QByteArray &data,
+ return m_sessionId.isEmpty()
+ ? QByteArray()
+ : convertByteArray(m_hMac->process(reinterpret_cast<const byte *>(data.constData()),
+ dataSize));
+QByteArray SshAbstractCryptoFacility::generateHash(const SshKeyExchange &kex,
+ char c, quint32 length)
+ const QByteArray &k = kex.k();
+ const QByteArray &h = kex.h();
+ QByteArray data(k);
+ data.append(h).append(c).append(m_sessionId);
+ SecureVector<byte> key
+ = kex.hash()->process(convertByteArray(data), data.size());
+ while (key.size() < length) {
+ secure_vector<byte> tmpKey;
+ tmpKey += secure_vector<byte>(k.begin(), k.end());
+ tmpKey += secure_vector<byte>(h.begin(), h.end());
+ tmpKey += key;
+ key += kex.hash()->process(tmpKey);
+ return QByteArray(reinterpret_cast<const char *>(key.data()), length);
+void SshAbstractCryptoFacility::checkInvariant() const
+ Q_ASSERT(m_sessionId.isEmpty() == !m_pipe);
+const QByteArray SshEncryptionFacility::PrivKeyFileStartLineRsa("-----BEGIN RSA PRIVATE KEY-----");
+const QByteArray SshEncryptionFacility::PrivKeyFileStartLineDsa("-----BEGIN DSA PRIVATE KEY-----");
+const QByteArray SshEncryptionFacility::PrivKeyFileEndLineRsa("-----END RSA PRIVATE KEY-----");
+const QByteArray SshEncryptionFacility::PrivKeyFileEndLineDsa("-----END DSA PRIVATE KEY-----");
+const QByteArray SshEncryptionFacility::PrivKeyFileStartLineEcdsa("-----BEGIN EC PRIVATE KEY-----");
+const QByteArray SshEncryptionFacility::PrivKeyFileEndLineEcdsa("-----END EC PRIVATE KEY-----");
+QByteArray SshEncryptionFacility::cryptAlgoName(const SshKeyExchange &kex) const
+ return kex.encryptionAlgo();
+QByteArray SshEncryptionFacility::hMacAlgoName(const SshKeyExchange &kex) const
+ return kex.hMacAlgoClientToServer();
+Keyed_Filter *SshEncryptionFacility::makeCipherMode(const QByteArray &cipher, const Mode mode)
+ if (mode == CtrMode) {
+ return new StreamCipher_Filter(cipher.toStdString());
+ qWarning() << "I haven't been able to test the CBC encryption modes, so if this files file a bug at https://github.com/sandsmark/QSsh";
+ Cipher_Mode_Filter *filter = new Cipher_Mode_Filter(
+ Cipher_Mode::create_or_throw(cipher.toStdString(), ENCRYPTION).release()); // We have to release, otherwise clang fails to link
+void SshEncryptionFacility::encrypt(QByteArray &data) const
+ convert(data, 0, data.size());
+void SshEncryptionFacility::createAuthenticationKey(const QByteArray &privKeyFileContents)
+ if (privKeyFileContents == m_cachedPrivKeyContents)
+ m_authKeyAlgoName.clear();
+ qCDebug(sshLog, "%s: Key not cached, reading", Q_FUNC_INFO);
+ QList<BigInt> pubKeyParams;
+ QList<BigInt> allKeyParams;
+ QString error1;
+ QString error2;
+ OpenSshKeyFileReader openSshReader(m_rng);
+ if (openSshReader.parseKey(privKeyFileContents)) {
+ m_authKeyAlgoName = openSshReader.keyType();
+ m_authKey.reset(openSshReader.privateKey().release());
+ pubKeyParams = openSshReader.publicParameters();
+ allKeyParams = openSshReader.allParameters();
+ } else if (!createAuthenticationKeyFromPKCS8(privKeyFileContents, pubKeyParams, allKeyParams,
+ error1)
+ && !createAuthenticationKeyFromOpenSSL(privKeyFileContents, pubKeyParams, allKeyParams,
+ error2)) {
+ qCDebug(sshLog, "%s: %s\n\t%s\n", Q_FUNC_INFO, qPrintable(error1), qPrintable(error2));
+ throw SshClientException(SshKeyFileError, SSH_TR("Decoding of private key file failed: "
+ "Format not understood."));
+ for (const BigInt &b : allKeyParams) {
+ if (b.is_zero()) {
+ SSH_TR("Decoding of private key file failed: Invalid zero parameter."));
+ m_authPubKeyBlob = AbstractSshPacket::encodeString(m_authKeyAlgoName);
+ auto * const ecdsaKey = dynamic_cast<ECDSA_PrivateKey *>(m_authKey.data());
+ if (ecdsaKey) {
+ m_authPubKeyBlob += AbstractSshPacket::encodeString(m_authKeyAlgoName.mid(11)); // Without "ecdsa-sha2-" prefix.
+ m_authPubKeyBlob += AbstractSshPacket::encodeString(
+ convertByteArray(ecdsaKey->public_point().encode(PointGFp::UNCOMPRESSED)));
+ for (const BigInt &b : pubKeyParams) {
+ m_authPubKeyBlob += AbstractSshPacket::encodeMpInt(b);
+ m_cachedPrivKeyContents = privKeyFileContents;
+bool SshEncryptionFacility::createAuthenticationKeyFromPKCS8(const QByteArray &privKeyFileContents,
+ QList<BigInt> &pubKeyParams, QList<BigInt> &allKeyParams, QString &error)
+ Pipe pipe;
+ pipe.process_msg(convertByteArray(privKeyFileContents), privKeyFileContents.size());
+ m_authKey.reset(PKCS8::load_key(pipe, m_rng, SshKeyPasswordRetriever::get_passphrase));
+ if (auto * const dsaKey = dynamic_cast<DSA_PrivateKey *>(m_authKey.data())) {
+ m_authKeyAlgoName = SshCapabilities::PubKeyDss;
+ pubKeyParams << dsaKey->group_p() << dsaKey->group_q()
+ << dsaKey->group_g() << dsaKey->get_y();
+ allKeyParams << pubKeyParams << dsaKey->get_x();
+ } else if (auto * const rsaKey = dynamic_cast<RSA_PrivateKey *>(m_authKey.data())) {
+ m_authKeyAlgoName = SshCapabilities::PubKeyRsa;
+ pubKeyParams << rsaKey->get_e() << rsaKey->get_n();
+ allKeyParams << pubKeyParams << rsaKey->get_p() << rsaKey->get_q()
+ << rsaKey->get_d();
+ } else if (auto * const ecdsaKey = dynamic_cast<ECDSA_PrivateKey *>(m_authKey.data())) {
+ const BigInt value = ecdsaKey->private_value();
+ m_authKeyAlgoName = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth(
+ static_cast<int>(value.bytes()));
+ pubKeyParams << ecdsaKey->public_point().get_affine_x()
+ << ecdsaKey->public_point().get_affine_y();
+ allKeyParams << pubKeyParams << value;
+ qCWarning(sshLog, "%s: Unexpected code flow, expected success or exception.",
+ Q_FUNC_INFO);
+ } catch (const std::exception &ex) {
+ error = QLatin1String(ex.what());
+bool SshEncryptionFacility::createAuthenticationKeyFromOpenSSL(const QByteArray &privKeyFileContents,
+ bool syntaxOk = true;
+ QList<QByteArray> lines = privKeyFileContents.split('\n');
+ while (lines.last().isEmpty())
+ lines.removeLast();
+ if (lines.count() < 3) {
+ syntaxOk = false;
+ } else if (lines.first() == PrivKeyFileStartLineRsa) {
+ if (lines.last() != PrivKeyFileEndLineRsa)
+ } else if (lines.first() == PrivKeyFileStartLineDsa) {
+ if (lines.last() != PrivKeyFileEndLineDsa)
+ } else if (lines.first() == PrivKeyFileStartLineEcdsa) {
+ if (lines.last() != PrivKeyFileEndLineEcdsa)
+ // m_authKeyAlgoName set below, as we don't know the size yet.
+ if (!syntaxOk) {
+ error = SSH_TR("Unexpected format.");
+ QByteArray privateKeyBlob;
+ for (int i = 1; i < lines.size() - 1; ++i)
+ privateKeyBlob += lines.at(i);
+ privateKeyBlob = QByteArray::fromBase64(privateKeyBlob);
+ BER_Decoder decoder(convertByteArray(privateKeyBlob), privateKeyBlob.size());
+ BER_Decoder sequence = decoder.start_cons(SEQUENCE);
+ size_t version;
+ sequence.decode (version);
+ const size_t expectedVersion = m_authKeyAlgoName.isEmpty() ? 1 : 0;
+ if (version != expectedVersion) {
+ error = SSH_TR("Key encoding has version %1, expected %2.")
+ .arg(version).arg(expectedVersion);
+ if (m_authKeyAlgoName == SshCapabilities::PubKeyDss) {
+ BigInt p, q, g, y, x;
+ sequence.decode (p).decode (q).decode (g).decode (y).decode (x);
+ DSA_PrivateKey * const dsaKey = new DSA_PrivateKey(m_rng, DL_Group(p, q, g), x);
+ m_authKey.reset(dsaKey);
+ pubKeyParams << p << q << g << y;
+ allKeyParams << pubKeyParams << x;
+ } else if (m_authKeyAlgoName == SshCapabilities::PubKeyRsa) {
+ BigInt p, q, e, d, n;
+ sequence.decode(n).decode(e).decode(d).decode(p).decode(q);
+ RSA_PrivateKey * const rsaKey = new RSA_PrivateKey(p, q, e, d, n);
+ m_authKey.reset(rsaKey);
+ pubKeyParams << e << n;
+ allKeyParams << pubKeyParams << p << q << d;
+ BigInt privKey;
+ sequence.decode_octet_string_bigint(privKey);
+ static_cast<int>(privKey.bytes()));
+ const EC_Group group(SshCapabilities::oid(m_authKeyAlgoName));
+ auto * const key = new ECDSA_PrivateKey(m_rng, group, privKey);
+ m_authKey.reset(key);
+ pubKeyParams << key->public_point().get_affine_x()
+ << key->public_point().get_affine_y();
+ allKeyParams << pubKeyParams << privKey;
+ sequence.discard_remaining();
+ sequence.verify_end();
+QByteArray SshEncryptionFacility::authenticationAlgorithmName() const
+ Q_ASSERT(m_authKey);
+ return m_authKeyAlgoName;
+QByteArray SshEncryptionFacility::authenticationKeySignature(const QByteArray &data) const
+ QScopedPointer<PK_Signer> signer(new PK_Signer(*m_authKey,
+ m_rng,
+ botanEmsaAlgoName(m_authKeyAlgoName)));
+ QByteArray dataToSign = AbstractSshPacket::encodeString(sessionId()) + data;
+ QByteArray signature
+ = convertByteArray(signer->sign_message(convertByteArray(dataToSign),
+ dataToSign.size(), m_rng));
+ if (m_authKeyAlgoName.startsWith(SshCapabilities::PubKeyEcdsaPrefix)) {
+ // The Botan output is not quite in the format that SSH defines.
+ const int halfSize = signature.count() / 2;
+ const BigInt r = BigInt::decode(convertByteArray(signature), halfSize);
+ const BigInt s = BigInt::decode(convertByteArray(signature.mid(halfSize)), halfSize);
+ signature = AbstractSshPacket::encodeMpInt(r) + AbstractSshPacket::encodeMpInt(s);
+ return AbstractSshPacket::encodeString(m_authKeyAlgoName)
+ + AbstractSshPacket::encodeString(signature);
+QByteArray SshEncryptionFacility::getRandomNumbers(int count) const
+ data.resize(count);
+ m_rng.randomize(convertByteArray(data), count);
+ return data;
+SshEncryptionFacility::~SshEncryptionFacility() {}
+QByteArray SshDecryptionFacility::cryptAlgoName(const SshKeyExchange &kex) const
+ return kex.decryptionAlgo();
+QByteArray SshDecryptionFacility::hMacAlgoName(const SshKeyExchange &kex) const
+ return kex.hMacAlgoServerToClient();
+Keyed_Filter *SshDecryptionFacility::makeCipherMode(const QByteArray &cipher, const Mode mode)
+ qWarning() << "I haven't been able to test the CBC decryption modes, so if this files file a bug at https://github.com/sandsmark/QSsh";
+ Cipher_Mode::create_or_throw(cipher.toStdString(), DECRYPTION).release()); // We have to release, otherwise clang fails to link
+void SshDecryptionFacility::decrypt(QByteArray &data, quint32 offset,
+ convert(data, offset, dataSize);
+ qCDebug(sshLog, "Decrypted data:");
+ const char * const start = data.constData() + offset;
+ const char * const end = start + dataSize;
+ for (const char *c = start; c < end; ++c)
+ qCDebug(sshLog) << "'" << *c << "' (0x" << (static_cast<int>(*c) & 0xff) << ")";
@@ -0,0 +1,149 @@
+#ifndef SSHABSTRACTCRYPTOFACILITY_P_H
+#define SSHABSTRACTCRYPTOFACILITY_P_H
+#include <botan/pipe.h>
+#include <botan/pk_keys.h>
+#include <botan/auto_rng.h>
+class SshKeyExchange;
+class SshAbstractCryptoFacility
+ virtual ~SshAbstractCryptoFacility();
+ void clearKeys();
+ void recreateKeys(const SshKeyExchange &kex);
+ QByteArray generateMac(const QByteArray &data, quint32 dataSize) const;
+ quint32 cipherBlockSize() const { return m_cipherBlockSize; }
+ quint32 macLength() const { return m_macLength; }
+ QByteArray sessionId() const { return m_sessionId; }
+ bool isValid() const { return m_hMac && m_pipe; } // TODO: probably more, but this stops segfaulting
+ enum Mode { CbcMode, CtrMode };
+ SshAbstractCryptoFacility();
+ void convert(QByteArray &data, quint32 offset, quint32 dataSize) const;
+ Botan::Keyed_Filter *makeCtrCipherMode(const QByteArray &cipher);
+ SshAbstractCryptoFacility(const SshAbstractCryptoFacility &);
+ SshAbstractCryptoFacility &operator=(const SshAbstractCryptoFacility &);
+ virtual QByteArray cryptAlgoName(const SshKeyExchange &kex) const = 0;
+ virtual QByteArray hMacAlgoName(const SshKeyExchange &kex) const = 0;
+ virtual Botan::Keyed_Filter *makeCipherMode(const QByteArray &cipher, const Mode mode) = 0;
+ virtual char ivChar() const = 0;
+ virtual char keyChar() const = 0;
+ virtual char macChar() const = 0;
+ QByteArray generateHash(const SshKeyExchange &kex, char c, quint32 length);
+ void checkInvariant() const;
+ static Mode getMode(const QByteArray &algoName);
+ QByteArray m_sessionId;
+ std::unique_ptr<Botan::Pipe> m_pipe;
+ std::unique_ptr<Botan::MessageAuthenticationCode> m_hMac;
+ quint32 m_cipherBlockSize;
+ quint32 m_macLength;
+class SshEncryptionFacility : public SshAbstractCryptoFacility
+ void encrypt(QByteArray &data) const;
+ void createAuthenticationKey(const QByteArray &privKeyFileContents);
+ QByteArray authenticationAlgorithmName() const;
+ QByteArray authenticationPublicKey() const { return m_authPubKeyBlob; }
+ QByteArray authenticationKeySignature(const QByteArray &data) const;
+ QByteArray getRandomNumbers(int count) const;
+ ~SshEncryptionFacility();
+ QByteArray cryptAlgoName(const SshKeyExchange &kex) const override;
+ QByteArray hMacAlgoName(const SshKeyExchange &kex) const override;
+ Botan::Keyed_Filter *makeCipherMode(const QByteArray &cipher, const Mode mode) override;
+ char ivChar() const override { return 'A'; }
+ char keyChar() const override { return 'C'; }
+ char macChar() const override { return 'E'; }
+ bool createAuthenticationKeyFromPKCS8(const QByteArray &privKeyFileContents,
+ QList<Botan::BigInt> &pubKeyParams, QList<Botan::BigInt> &allKeyParams, QString &error);
+ bool createAuthenticationKeyFromOpenSSL(const QByteArray &privKeyFileContents,
+ static const QByteArray PrivKeyFileStartLineRsa;
+ static const QByteArray PrivKeyFileStartLineDsa;
+ static const QByteArray PrivKeyFileEndLineRsa;
+ static const QByteArray PrivKeyFileEndLineDsa;
+ static const QByteArray PrivKeyFileStartLineEcdsa;
+ static const QByteArray PrivKeyFileEndLineEcdsa;
+ QByteArray m_authKeyAlgoName;
+ QByteArray m_authPubKeyBlob;
+ QByteArray m_cachedPrivKeyContents;
+ QScopedPointer<Botan::Private_Key> m_authKey;
+ mutable Botan::AutoSeeded_RNG m_rng;
+class SshDecryptionFacility : public SshAbstractCryptoFacility
+ void decrypt(QByteArray &data, quint32 offset, quint32 dataSize) const;
+ char ivChar() const override { return 'B'; }
+ char keyChar() const override { return 'D'; }
+ char macChar() const override { return 'F'; }
+#endif // SSHABSTRACTCRYPTOFACILITY_P_H
@@ -0,0 +1,129 @@
+** Contact: Nokia Corporation (qt-info@nokia.com)
+** If you have questions regarding the use of this file, please contact
+** Nokia at qt-info@nokia.com.
+SshDirectTcpIpTunnelPrivate::SshDirectTcpIpTunnelPrivate(quint32 channelId,
+ const QString &originatingHost, quint16 originatingPort, const QString &remoteHost,
+ quint16 remotePort, SshSendFacility &sendFacility)
+ : SshTcpIpTunnelPrivate(channelId, sendFacility),
+ m_originatingHost(originatingHost),
+ m_originatingPort(originatingPort),
+ m_remoteHost(remoteHost),
+ m_remotePort(remotePort)
+void SshDirectTcpIpTunnelPrivate::handleOpenSuccessInternal()
+SshDirectTcpIpTunnel::SshDirectTcpIpTunnel(quint32 channelId, const QString &originatingHost,
+ quint16 originatingPort, const QString &remoteHost, quint16 remotePort,
+ : d(new SshDirectTcpIpTunnelPrivate(channelId, originatingHost, originatingPort, remoteHost,
+ remotePort, sendFacility))
+ d->init(this);
+ connect(d, &SshDirectTcpIpTunnelPrivate::initialized,
+ this, &SshDirectTcpIpTunnel::initialized, Qt::QueuedConnection);
+SshDirectTcpIpTunnel::~SshDirectTcpIpTunnel()
+bool SshDirectTcpIpTunnel::atEnd() const
+ return QIODevice::atEnd() && d->m_data.isEmpty();
+qint64 SshDirectTcpIpTunnel::bytesAvailable() const
+ return QIODevice::bytesAvailable() + d->m_data.count();
+bool SshDirectTcpIpTunnel::canReadLine() const
+ return QIODevice::canReadLine() || d->m_data.contains('\n');
+void SshDirectTcpIpTunnel::close()
+ QIODevice::close();
+void SshDirectTcpIpTunnel::initialize()
+ QSSH_ASSERT_AND_RETURN(d->channelState() == AbstractSshChannel::Inactive);
+ QIODevice::open(QIODevice::ReadWrite);
+ d->m_sendFacility.sendDirectTcpIpPacket(d->localChannelId(), SshDirectTcpIpTunnelPrivate::initialWindowSize(),
+ SshDirectTcpIpTunnelPrivate::maxPacketSize(), d->m_remoteHost.toUtf8(), d->m_remotePort,
+ d->m_originatingHost.toUtf8(), d->m_originatingPort);
+ d->setChannelState(AbstractSshChannel::SessionRequested);
+ d->m_timeoutTimer.setTimerType(Qt::VeryCoarseTimer);
+ d->m_timeoutTimer.start(SshDirectTcpIpTunnelPrivate::ReplyTimeout);
+ } catch (const std::exception &e) { // Won't happen, but let's play it safe.
+qint64 SshDirectTcpIpTunnel::readData(char *data, qint64 maxlen)
+ return d->readData(data, maxlen);
+qint64 SshDirectTcpIpTunnel::writeData(const char *data, qint64 len)
+ return d->writeData(data, len);
@@ -0,0 +1,89 @@
+#ifndef SSHDIRECTTCPIPTUNNEL_H
+#define SSHDIRECTTCPIPTUNNEL_H
+#include <QIODevice>
+class SshDirectTcpIpTunnelPrivate;
+class SshTcpIpTunnelPrivate;
+class QSSH_EXPORT SshDirectTcpIpTunnel : public QIODevice
+ friend class Internal::SshTcpIpTunnelPrivate;
+ typedef QSharedPointer<SshDirectTcpIpTunnel> Ptr;
+ ~SshDirectTcpIpTunnel();
+ // QIODevice stuff
+ bool atEnd() const;
+ qint64 bytesAvailable() const;
+ bool canReadLine() const;
+ void close();
+ bool isSequential() const { return true; }
+ void error(const QString &reason);
+ SshDirectTcpIpTunnel(quint32 channelId, const QString &originatingHost,
+ Internal::SshSendFacility &sendFacility);
+ qint64 readData(char *data, qint64 maxlen);
+ qint64 writeData(const char *data, qint64 len);
+ Internal::SshDirectTcpIpTunnelPrivate * const d;
+#endif // SSHDIRECTTCPIPTUNNEL_H
@@ -0,0 +1,68 @@
+#ifndef DIRECTTCPIPCHANNEL_P_H
+#define DIRECTTCPIPCHANNEL_P_H
+#include "sshtcpiptunnel_p.h"
+class SshDirectTcpIpTunnelPrivate : public SshTcpIpTunnelPrivate
+ friend class QSsh::SshDirectTcpIpTunnel;
+ explicit SshDirectTcpIpTunnelPrivate(quint32 channelId, const QString &originatingHost,
+ SshSendFacility &sendFacility);
+ void handleOpenSuccessInternal();
+ const QString m_originatingHost;
+ const quint16 m_originatingPort;
+ const QString m_remoteHost;
+ const quint16 m_remotePort;
+#endif // DIRECTTCPIPCHANNEL_P_H
@@ -0,0 +1,77 @@
+#ifndef SSHERRORS_P_H
+#define SSHERRORS_P_H
+ * \brief SSH specific errors
+enum SshError {
+ /// No error has occured
+ SshNoError,
+ /// There was a network socket error
+ SshSocketError,
+ /// The connection timed out
+ SshTimeoutError,
+ /// There was an error communicating with the server
+ SshProtocolError,
+ /// There was a problem with the remote host key
+ SshHostKeyError,
+ /// We failed to read or parse the key file used for authentication
+ SshKeyFileError,
+ /// We failed to authenticate
+ SshAuthenticationError,
+ /// The server closed our connection
+ SshClosedByServerError,
+ /// The ssh-agent used for authenticating failed somehow
+ SshAgentError,
+ /// Something bad happened on the server
+ SshInternalError
+Q_DECLARE_METATYPE(QSsh::SshError)
+#endif // SSHERRORS_P_H
@@ -0,0 +1,95 @@
+#ifndef SSHEXCEPTION_P_H
+#define SSHEXCEPTION_P_H
+#include <exception>
+enum SshErrorCode {
+ SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1,
+ SSH_DISCONNECT_PROTOCOL_ERROR = 2,
+ SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3,
+ SSH_DISCONNECT_RESERVED = 4,
+ SSH_DISCONNECT_MAC_ERROR = 5,
+ SSH_DISCONNECT_COMPRESSION_ERROR = 6,
+ SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7,
+ SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8,
+ SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9,
+ SSH_DISCONNECT_CONNECTION_LOST = 10,
+ SSH_DISCONNECT_BY_APPLICATION = 11,
+ SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12,
+ SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13,
+ SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14,
+ SSH_DISCONNECT_ILLEGAL_USER_NAME = 15
+#define SSH_TR(string) QCoreApplication::translate("SshConnection", string)
+#define SSH_SERVER_EXCEPTION(error, errorString) \
+ SshServerException((error), (errorString), SSH_TR(errorString))
+struct SshServerException : public std::exception
+ SshServerException(SshErrorCode error, const QByteArray &errorStringServer,
+ const QString &errorStringUser)
+ : error(error), errorStringServer(errorStringServer),
+ errorStringUser(errorStringUser) {}
+ const char *what() const noexcept override { return errorStringServer.constData(); }
+ const SshErrorCode error;
+ const QByteArray errorStringServer;
+ const QString errorStringUser;
+struct SshClientException : public std::exception
+ SshClientException(SshError error, const QString &errorString)
+ : error(error), errorString(errorString), errorStringPrintable(errorString.toLocal8Bit()) {}
+ const char *what() const noexcept override { return errorStringPrintable.constData(); }
+ const SshError error;
+ const QString errorString;
+ const QByteArray errorStringPrintable;
+#endif // SSHEXCEPTION_P_H
@@ -0,0 +1,100 @@
+SshForwardedTcpIpTunnelPrivate::SshForwardedTcpIpTunnelPrivate(quint32 channelId,
+ SshSendFacility &sendFacility) :
+ SshTcpIpTunnelPrivate(channelId, sendFacility)
+void SshForwardedTcpIpTunnelPrivate::handleOpenSuccessInternal()
+ QSSH_ASSERT_AND_RETURN(channelState() == AbstractSshChannel::SessionEstablished);
+ m_sendFacility.sendChannelOpenConfirmationPacket(remoteChannel(), localChannelId(),
+ initialWindowSize(), maxPacketSize());
+SshForwardedTcpIpTunnel::SshForwardedTcpIpTunnel(quint32 channelId, SshSendFacility &sendFacility) :
+ d(new SshForwardedTcpIpTunnelPrivate(channelId, sendFacility))
+SshForwardedTcpIpTunnel::~SshForwardedTcpIpTunnel()
+bool SshForwardedTcpIpTunnel::atEnd() const
+qint64 SshForwardedTcpIpTunnel::bytesAvailable() const
+bool SshForwardedTcpIpTunnel::canReadLine() const
+void SshForwardedTcpIpTunnel::close()
+qint64 SshForwardedTcpIpTunnel::readData(char *data, qint64 maxlen)
+qint64 SshForwardedTcpIpTunnel::writeData(const char *data, qint64 len)
@@ -0,0 +1,70 @@
+class SshForwardedTcpIpTunnelPrivate;
+class QSSH_EXPORT SshForwardedTcpIpTunnel : public QIODevice
+ typedef QSharedPointer<SshForwardedTcpIpTunnel> Ptr;
+ ~SshForwardedTcpIpTunnel() override;
+ bool atEnd() const override;
+ qint64 bytesAvailable() const override;
+ bool canReadLine() const override;
+ void close() override;
+ bool isSequential() const override { return true; }
+ SshForwardedTcpIpTunnel(quint32 channelId, Internal::SshSendFacility &sendFacility);
+ qint64 readData(char *data, qint64 maxlen) override;
+ qint64 writeData(const char *data, qint64 len) override;
+ Internal::SshForwardedTcpIpTunnelPrivate * const d;
@@ -0,0 +1,44 @@
+class SshForwardedTcpIpTunnelPrivate : public SshTcpIpTunnelPrivate
+ friend class QSsh::SshForwardedTcpIpTunnel;
+ SshForwardedTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility);
+ void handleOpenSuccessInternal() override;
@@ -0,0 +1,127 @@
+** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://www.qt.io/licensing. For further information
+** use the contact form at http://www.qt.io/contact-us.
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+class SshHostKeyDatabase::SshHostKeyDatabasePrivate
+ QHash<QString, QByteArray> hostKeys;
+SshHostKeyDatabase::SshHostKeyDatabase() : d(new SshHostKeyDatabasePrivate)
+SshHostKeyDatabase::~SshHostKeyDatabase()
+bool SshHostKeyDatabase::load(const QString &filePath, QString *error)
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ if (error) {
+ *error = QCoreApplication::translate("QSsh::Ssh",
+ "Failed to open key file \"%1\" for reading: %2")
+ .arg(QDir::toNativeSeparators(filePath), file.errorString());
+ d->hostKeys.clear();
+ const QByteArray content = file.readAll().trimmed();
+ if (content.isEmpty())
+ for (const QByteArray &line : content.split('\n')) {
+ const QList<QByteArray> &lineData = line.trimmed().split(' ');
+ if (lineData.count() != 2) {
+ qCDebug(Internal::sshLog, "Unexpected line \"%s\" in file \"%s\".", line.constData(),
+ qPrintable(filePath));
+ d->hostKeys.insert(QString::fromUtf8(lineData.first()),
+ QByteArray::fromHex(lineData.last()));
+bool SshHostKeyDatabase::store(const QString &filePath, QString *error) const
+ if (!file.open(QIODevice::WriteOnly)) {
+ "Failed to open key file \"%1\" for writing: %2")
+ file.resize(0);
+ for (auto it = d->hostKeys.constBegin(); it != d->hostKeys.constEnd(); ++it)
+ file.write(it.key().toUtf8() + ' ' + it.value().toHex() + '\n');
+SshHostKeyDatabase::KeyLookupResult SshHostKeyDatabase::matchHostKey(const QString &hostName,
+ const QByteArray &key) const
+ auto it = d->hostKeys.constFind(hostName);
+ if (it == d->hostKeys.constEnd())
+ return KeyLookupNoMatch;
+ if (it.value() == key)
+ return KeyLookupMatch;
+ return KeyLookupMismatch;
+void SshHostKeyDatabase::insertHostKey(const QString &hostName, const QByteArray &key)
+ d->hostKeys.insert(hostName, key);
+QByteArray SshHostKeyDatabase::retrieveHostKey(const QString &hostName)
+ return d->hostKeys.value(hostName);
@@ -0,0 +1,75 @@
+#ifndef SSHHOSTKEYDATABASE_H
+#define SSHHOSTKEYDATABASE_H
+class QByteArray;
+class QString;
+class SshHostKeyDatabase;
+/// Convenience typedef
+typedef QSharedPointer<SshHostKeyDatabase> SshHostKeyDatabasePtr;
+class QSSH_EXPORT SshHostKeyDatabase
+ friend class QSharedPointer<SshHostKeyDatabase>; // To give create() access to our constructor.
+ enum KeyLookupResult {
+ KeyLookupMatch,
+ KeyLookupNoMatch,
+ KeyLookupMismatch
+ SshHostKeyDatabase();
+ ~SshHostKeyDatabase();
+ bool load(const QString &filePath, QString *error = nullptr);
+ bool store(const QString &filePath, QString *error = nullptr) const;
+ KeyLookupResult matchHostKey(const QString &hostName, const QByteArray &key) const;
+ void insertHostKey(const QString &hostName, const QByteArray &key);
+ QByteArray retrieveHostKey(const QString &hostName);
+ class SshHostKeyDatabasePrivate;
+ SshHostKeyDatabasePrivate * const d;
+#endif // Include guard.
@@ -0,0 +1,609 @@
+const QByteArray SshIncomingPacket::ExitStatusType("exit-status");
+const QByteArray SshIncomingPacket::ExitSignalType("exit-signal");
+const QByteArray SshIncomingPacket::ForwardedTcpIpType("forwarded-tcpip");
+SshIncomingPacket::SshIncomingPacket() : m_serverSeqNr(0) { }
+quint32 SshIncomingPacket::cipherBlockSize() const
+ return qMax(m_decrypter.cipherBlockSize(), 8U);
+quint32 SshIncomingPacket::macLength() const
+ return m_decrypter.macLength();
+void SshIncomingPacket::recreateKeys(const SshKeyExchange &keyExchange)
+ m_decrypter.recreateKeys(keyExchange);
+void SshIncomingPacket::reset()
+ clear();
+ m_serverSeqNr = 0;
+ m_decrypter.clearKeys();
+void SshIncomingPacket::consumeData(QByteArray &newData)
+ qCDebug(sshLog, "%s: current data size = %d, new data size = %d",
+ Q_FUNC_INFO, int(m_data.size()), int(newData.size()));
+ if (isComplete() || newData.isEmpty())
+ * Until we have reached the minimum packet size, we cannot decrypt the
+ * length field.
+ const quint32 minSize = minPacketSize();
+ if (currentDataSize() < minSize) {
+ const int bytesToTake
+ = qMin<quint32>(minSize - currentDataSize(), newData.size());
+ moveFirstBytes(m_data, newData, bytesToTake);
+ qCDebug(sshLog, "Took %d bytes from new data", bytesToTake);
+ if (currentDataSize() < minSize)
+ if (4 + length() + macLength() < currentDataSize())
+ throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Server sent invalid packet.");
+ = qMin<quint32>(length() + 4 + macLength() - currentDataSize(),
+ newData.size());
+ if (isComplete()) {
+ qCDebug(sshLog, "Message complete. Overall size: %u, payload size: %u",
+ int(m_data.size()), m_length - paddingLength() - 1);
+ decrypt();
+ ++m_serverSeqNr;
+void SshIncomingPacket::decrypt()
+ const quint32 netDataLength = length() + 4;
+ m_decrypter.decrypt(m_data, cipherBlockSize(),
+ netDataLength - cipherBlockSize());
+ const QByteArray &mac = m_data.mid(netDataLength, macLength());
+ if (mac != generateMac(m_decrypter, m_serverSeqNr)) {
+ throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_MAC_ERROR,
+ "Message authentication failed.");
+void SshIncomingPacket::moveFirstBytes(QByteArray &target, QByteArray &source,
+SshKeyExchangeInit SshIncomingPacket::extractKeyExchangeInitData() const
+ Q_ASSERT(type() == SSH_MSG_KEXINIT);
+ SshKeyExchangeInit exchangeData;
+ quint32 offset = TypeOffset + 1;
+ std::memcpy(exchangeData.cookie, &m_data.constData()[offset],
+ sizeof exchangeData.cookie);
+ offset += sizeof exchangeData.cookie;
+ exchangeData.keyAlgorithms
+ = SshPacketParser::asNameList(m_data, &offset);
+ exchangeData.serverHostKeyAlgorithms
+ exchangeData.encryptionAlgorithmsClientToServer
+ exchangeData.encryptionAlgorithmsServerToClient
+ exchangeData.macAlgorithmsClientToServer
+ exchangeData.macAlgorithmsServerToClient
+ exchangeData.compressionAlgorithmsClientToServer
+ exchangeData.compressionAlgorithmsServerToClient
+ exchangeData.languagesClientToServer
+ exchangeData.languagesServerToClient
+ exchangeData.firstKexPacketFollows
+ = SshPacketParser::asBool(m_data, &offset);
+ throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ "Key exchange failed: Server sent invalid SSH_MSG_KEXINIT packet.");
+ return exchangeData;
+static void getHostKeySpecificReplyData(SshKeyExchangeReply &replyData,
+ const QByteArray &hostKeyAlgo, const QByteArray &input)
+ if (hostKeyAlgo == SshCapabilities::PubKeyDss || hostKeyAlgo == SshCapabilities::PubKeyRsa) {
+ // DSS: p and q, RSA: e and n
+ replyData.hostKeyParameters << SshPacketParser::asBigInt(input, &offset);
+ // g and y
+ if (hostKeyAlgo == SshCapabilities::PubKeyDss) {
+ QSSH_ASSERT_AND_RETURN(hostKeyAlgo.startsWith(SshCapabilities::PubKeyEcdsaPrefix));
+ if (SshPacketParser::asString(input, &offset)
+ != hostKeyAlgo.mid(11)) { // Without "ecdsa-sha2-" prefix.
+ throw SshPacketParseException();
+ replyData.q = SshPacketParser::asString(input, &offset);
+static QByteArray &padToWidth(QByteArray &data, int targetWidth)
+ return data.prepend(QByteArray(targetWidth - data.count(), 0));
+SshKeyExchangeReply SshIncomingPacket::extractKeyExchangeReply(const QByteArray &kexAlgo,
+ const QByteArray &hostKeyAlgo) const
+ Q_ASSERT(type() == SSH_MSG_KEXDH_REPLY);
+ SshKeyExchangeReply replyData;
+ quint32 topLevelOffset = TypeOffset + 1;
+ replyData.k_s = SshPacketParser::asString(m_data, &topLevelOffset);
+ quint32 k_sOffset = 0;
+ if (SshPacketParser::asString(replyData.k_s, &k_sOffset) != hostKeyAlgo)
+ getHostKeySpecificReplyData(replyData, hostKeyAlgo, replyData.k_s.mid(k_sOffset));
+ if (kexAlgo == SshCapabilities::DiffieHellmanGroup1Sha1
+ || kexAlgo == SshCapabilities::DiffieHellmanGroup14Sha1) {
+ replyData.f = SshPacketParser::asBigInt(m_data, &topLevelOffset);
+ QSSH_ASSERT_AND_RETURN_VALUE(kexAlgo.startsWith(SshCapabilities::EcdhKexNamePrefix),
+ SshKeyExchangeReply());
+ replyData.q_s = SshPacketParser::asString(m_data, &topLevelOffset);
+ const QByteArray fullSignature = SshPacketParser::asString(m_data, &topLevelOffset);
+ quint32 sigOffset = 0;
+ if (SshPacketParser::asString(fullSignature, &sigOffset) != hostKeyAlgo)
+ replyData.signatureBlob = SshPacketParser::asString(fullSignature, &sigOffset);
+ if (hostKeyAlgo.startsWith(SshCapabilities::PubKeyEcdsaPrefix)) {
+ // Botan's PK_Verifier wants the signature in this format.
+ quint32 blobOffset = 0;
+ const Botan::BigInt r = SshPacketParser::asBigInt(replyData.signatureBlob, &blobOffset);
+ const Botan::BigInt s = SshPacketParser::asBigInt(replyData.signatureBlob, &blobOffset);
+ const int width = SshCapabilities::ecdsaIntegerWidthInBytes(hostKeyAlgo);
+ QByteArray encodedR = convertByteArray(Botan::BigInt::encode(r));
+ replyData.signatureBlob = padToWidth(encodedR, width);
+ QByteArray encodedS = convertByteArray(Botan::BigInt::encode(s));
+ replyData.signatureBlob += padToWidth(encodedS, width);
+ replyData.k_s.prepend(m_data.mid(TypeOffset + 1, 4));
+ return replyData;
+ "Key exchange failed: "
+ "Server sent invalid key exchange reply packet.");
+SshDisconnect SshIncomingPacket::extractDisconnect() const
+ Q_ASSERT(type() == SSH_MSG_DISCONNECT);
+ SshDisconnect msg;
+ msg.reasonCode = SshPacketParser::asUint32(m_data, &offset);
+ msg.description = SshPacketParser::asUserString(m_data, &offset);
+ msg.language = SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_MSG_DISCONNECT.");
+ return msg;
+SshUserAuthBanner SshIncomingPacket::extractUserAuthBanner() const
+ Q_ASSERT(type() == SSH_MSG_USERAUTH_BANNER);
+ SshUserAuthBanner msg;
+ msg.message = SshPacketParser::asUserString(m_data, &offset);
+ "Invalid SSH_MSG_USERAUTH_BANNER.");
+SshUserAuthInfoRequestPacket SshIncomingPacket::extractUserAuthInfoRequest() const
+ Q_ASSERT(type() == SSH_MSG_USERAUTH_INFO_REQUEST);
+ SshUserAuthInfoRequestPacket msg;
+ msg.name = SshPacketParser::asUserString(m_data, &offset);
+ msg.instruction = SshPacketParser::asUserString(m_data, &offset);
+ msg.languageTag = SshPacketParser::asString(m_data, &offset);
+ const quint32 promptCount = SshPacketParser::asUint32(m_data, &offset);
+ msg.prompts.reserve(promptCount);
+ msg.echos.reserve(promptCount);
+ for (quint32 i = 0; i < promptCount; ++i) {
+ msg.prompts << SshPacketParser::asUserString(m_data, &offset);
+ msg.echos << SshPacketParser::asBool(m_data, &offset);
+ "Invalid SSH_MSG_USERAUTH_INFO_REQUEST.");
+SshUserAuthPkOkPacket SshIncomingPacket::extractUserAuthPkOk() const
+ Q_ASSERT(type() == SSH_MSG_USERAUTH_PK_OK);
+ SshUserAuthPkOkPacket msg;
+ msg.algoName= SshPacketParser::asString(m_data, &offset);
+ msg.keyBlob = SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_MSG_USERAUTH_PK_OK.");
+SshDebug SshIncomingPacket::extractDebug() const
+ Q_ASSERT(type() == SSH_MSG_DEBUG);
+ SshDebug msg;
+ msg.display = SshPacketParser::asBool(m_data, &offset);
+ "Invalid SSH_MSG_DEBUG.");
+SshRequestSuccess SshIncomingPacket::extractRequestSuccess() const
+ Q_ASSERT(type() == SSH_MSG_REQUEST_SUCCESS);
+ SshRequestSuccess msg;
+ msg.bindPort = SshPacketParser::asUint32(m_data, &offset);
+ "Invalid SSH_MSG_REQUEST_SUCCESS.");
+SshUnimplemented SshIncomingPacket::extractUnimplemented() const
+ Q_ASSERT(type() == SSH_MSG_UNIMPLEMENTED);
+ SshUnimplemented msg;
+ msg.invalidMsgSeqNr = SshPacketParser::asUint32(m_data, &offset);
+ "Invalid SSH_MSG_UNIMPLEMENTED.");
+SshChannelOpenGeneric SshIncomingPacket::extractChannelOpen() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_OPEN);
+ SshChannelOpenGeneric channelOpen;
+ channelOpen.channelType = SshPacketParser::asString(m_data, &offset);
+ channelOpen.commonData.remoteChannel = SshPacketParser::asUint32(m_data, &offset);
+ channelOpen.commonData.remoteWindowSize = SshPacketParser::asUint32(m_data, &offset);
+ channelOpen.commonData.remoteMaxPacketSize = SshPacketParser::asUint32(m_data, &offset);
+ channelOpen.typeSpecificData = m_data.mid(offset, length() - paddingLength() - offset
+ + int(sizeof m_length));
+ return channelOpen;
+ "Server sent invalid SSH_MSG_CHANNEL_OPEN packet.");
+SshChannelOpenForwardedTcpIp SshIncomingPacket::extractChannelOpenForwardedTcpIp(
+ const SshChannelOpenGeneric &genericData)
+ SshChannelOpenForwardedTcpIp specificData;
+ specificData.common = genericData.commonData;
+ specificData.remoteAddress = SshPacketParser::asString(genericData.typeSpecificData,
+ &offset);
+ specificData.remotePort = SshPacketParser::asUint32(genericData.typeSpecificData, &offset);
+ specificData.originatorAddress = SshPacketParser::asString(genericData.typeSpecificData,
+ specificData.originatorPort = SshPacketParser::asUint32(genericData.typeSpecificData,
+ return specificData;
+SshChannelOpenX11 SshIncomingPacket::extractChannelOpenX11(const SshChannelOpenGeneric &genericData)
+ SshChannelOpenX11 specificData;
+SshChannelOpenFailure SshIncomingPacket::extractChannelOpenFailure() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_OPEN_FAILURE);
+ SshChannelOpenFailure openFailure;
+ openFailure.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ openFailure.reasonCode = SshPacketParser::asUint32(m_data, &offset);
+ openFailure.reasonString = QString::fromLocal8Bit(SshPacketParser::asString(m_data, &offset));
+ openFailure.language = SshPacketParser::asString(m_data, &offset);
+ "Server sent invalid SSH_MSG_CHANNEL_OPEN_FAILURE packet.");
+ return openFailure;
+SshChannelOpenConfirmation SshIncomingPacket::extractChannelOpenConfirmation() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_OPEN_CONFIRMATION);
+ SshChannelOpenConfirmation confirmation;
+ confirmation.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ confirmation.remoteChannel = SshPacketParser::asUint32(m_data, &offset);
+ confirmation.remoteWindowSize = SshPacketParser::asUint32(m_data, &offset);
+ confirmation.remoteMaxPacketSize = SshPacketParser::asUint32(m_data, &offset);
+ "Server sent invalid SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet.");
+ return confirmation;
+SshChannelWindowAdjust SshIncomingPacket::extractWindowAdjust() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_WINDOW_ADJUST);
+ SshChannelWindowAdjust adjust;
+ adjust.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ adjust.bytesToAdd = SshPacketParser::asUint32(m_data, &offset);
+ "Invalid SSH_MSG_CHANNEL_WINDOW_ADJUST packet.");
+ return adjust;
+SshChannelData SshIncomingPacket::extractChannelData() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_DATA);
+ SshChannelData data;
+ data.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ data.data = SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_MSG_CHANNEL_DATA packet.");
+SshChannelExtendedData SshIncomingPacket::extractChannelExtendedData() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_EXTENDED_DATA);
+ SshChannelExtendedData data;
+ data.type = SshPacketParser::asUint32(m_data, &offset);
+ "Invalid SSH_MSG_CHANNEL_EXTENDED_DATA packet.");
+SshChannelExitStatus SshIncomingPacket::extractChannelExitStatus() const
+ Q_ASSERT(type() == SSH_MSG_CHANNEL_REQUEST);
+ SshChannelExitStatus exitStatus;
+ exitStatus.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ const QByteArray &type = SshPacketParser::asString(m_data, &offset);
+ Q_ASSERT(type == ExitStatusType);
+ Q_UNUSED(type);
+ if (SshPacketParser::asBool(m_data, &offset))
+ exitStatus.exitStatus = SshPacketParser::asUint32(m_data, &offset);
+ "Invalid exit-status packet.");
+ return exitStatus;
+SshChannelExitSignal SshIncomingPacket::extractChannelExitSignal() const
+ SshChannelExitSignal exitSignal;
+ exitSignal.localChannel = SshPacketParser::asUint32(m_data, &offset);
+ Q_ASSERT(type == ExitSignalType);
+ exitSignal.signal = SshPacketParser::asString(m_data, &offset);
+ exitSignal.coreDumped = SshPacketParser::asBool(m_data, &offset);
+ exitSignal.error = SshPacketParser::asUserString(m_data, &offset);
+ exitSignal.language = SshPacketParser::asString(m_data, &offset);
+ "Invalid exit-signal packet.");
+ return exitSignal;
+quint32 SshIncomingPacket::extractRecipientChannel() const
+ return SshPacketParser::asUint32(m_data, &offset);
+ "Server sent invalid packet.");
+QByteArray SshIncomingPacket::extractChannelRequestType() const
+ SshPacketParser::asUint32(m_data, &offset);
+ return SshPacketParser::asString(m_data, &offset);
+ "Invalid SSH_MSG_CHANNEL_REQUEST packet.");
+void SshIncomingPacket::calculateLength() const
+ Q_ASSERT(currentDataSize() >= minPacketSize());
+ qCDebug(sshLog, "Length field before decryption: %d-%d-%d-%d", m_data.at(0) & 0xff,
+ m_data.at(1) & 0xff, m_data.at(2) & 0xff, m_data.at(3) & 0xff);
+ m_decrypter.decrypt(m_data, 0, cipherBlockSize());
+ qCDebug(sshLog, "Length field after decryption: %d-%d-%d-%d", m_data.at(0) & 0xff, m_data.at(1) & 0xff, m_data.at(2) & 0xff, m_data.at(3) & 0xff);
+ qCDebug(sshLog, "message type = %d", m_data.at(TypeOffset));
+ qCDebug(sshLog, "decrypted length is %u", m_length);
@@ -0,0 +1,252 @@
+#ifndef SSHINCOMINGPACKET_P_H
+#define SSHINCOMINGPACKET_P_H
+#include <QStringList>
+struct SshKeyExchangeInit
+ char cookie[16];
+ SshNameList keyAlgorithms;
+ SshNameList serverHostKeyAlgorithms;
+ SshNameList encryptionAlgorithmsClientToServer;
+ SshNameList encryptionAlgorithmsServerToClient;
+ SshNameList macAlgorithmsClientToServer;
+ SshNameList macAlgorithmsServerToClient;
+ SshNameList compressionAlgorithmsClientToServer;
+ SshNameList compressionAlgorithmsServerToClient;
+ SshNameList languagesClientToServer;
+ SshNameList languagesServerToClient;
+ bool firstKexPacketFollows;
+struct SshKeyExchangeReply
+ QByteArray k_s;
+ QList<Botan::BigInt> hostKeyParameters; // DSS: p, q, g, y. RSA: e, n.
+ QByteArray q; // For ECDSA host keys only.
+ Botan::BigInt f; // For DH only.
+ QByteArray q_s; // For ECDH only.
+ QByteArray signatureBlob;
+struct SshDisconnect
+ quint32 reasonCode;
+ QString description;
+struct SshUserAuthBanner
+ QString message;
+struct SshUserAuthPkOkPacket
+ QByteArray algoName;
+ QByteArray keyBlob;
+struct SshUserAuthInfoRequestPacket
+ QString instruction;
+ QByteArray languageTag;
+ QStringList prompts;
+ QList<bool> echos;
+struct SshDebug
+ bool display;
+struct SshUnimplemented
+ quint32 invalidMsgSeqNr;
+struct SshRequestSuccess
+ quint32 bindPort;
+struct SshChannelOpenCommon
+ quint32 remoteChannel;
+ quint32 remoteWindowSize;
+ quint32 remoteMaxPacketSize;
+struct SshChannelOpenGeneric
+ QByteArray channelType;
+ SshChannelOpenCommon commonData;
+ QByteArray typeSpecificData;
+struct SshChannelOpenForwardedTcpIp
+ SshChannelOpenCommon common;
+ QByteArray remoteAddress;
+ quint32 remotePort;
+ QByteArray originatorAddress;
+ quint32 originatorPort;
+struct SshChannelOpenX11
+struct SshChannelOpenFailure
+ quint32 localChannel;
+ QString reasonString;
+struct SshChannelOpenConfirmation
+struct SshChannelWindowAdjust
+ quint32 bytesToAdd;
+struct SshChannelData
+struct SshChannelExtendedData
+ quint32 type;
+struct SshChannelExitStatus
+ quint32 exitStatus;
+struct SshChannelExitSignal
+ QByteArray signal;
+ bool coreDumped;
+ QString error;
+class SshIncomingPacket : public AbstractSshPacket
+ SshIncomingPacket();
+ void recreateKeys(const SshKeyExchange &keyExchange);
+ void reset();
+ SshKeyExchangeInit extractKeyExchangeInitData() const;
+ SshKeyExchangeReply extractKeyExchangeReply(const QByteArray &kexAlgo,
+ const QByteArray &hostKeyAlgo) const;
+ SshDisconnect extractDisconnect() const;
+ SshUserAuthBanner extractUserAuthBanner() const;
+ SshUserAuthInfoRequestPacket extractUserAuthInfoRequest() const;
+ SshUserAuthPkOkPacket extractUserAuthPkOk() const;
+ SshDebug extractDebug() const;
+ SshRequestSuccess extractRequestSuccess() const;
+ SshUnimplemented extractUnimplemented() const;
+ SshChannelOpenGeneric extractChannelOpen() const;
+ static SshChannelOpenForwardedTcpIp extractChannelOpenForwardedTcpIp(
+ const SshChannelOpenGeneric &genericData);
+ static SshChannelOpenX11 extractChannelOpenX11(const SshChannelOpenGeneric &genericData);
+ SshChannelOpenFailure extractChannelOpenFailure() const;
+ SshChannelOpenConfirmation extractChannelOpenConfirmation() const;
+ SshChannelWindowAdjust extractWindowAdjust() const;
+ SshChannelData extractChannelData() const;
+ SshChannelExtendedData extractChannelExtendedData() const;
+ SshChannelExitStatus extractChannelExitStatus() const;
+ SshChannelExitSignal extractChannelExitSignal() const;
+ quint32 extractRecipientChannel() const;
+ QByteArray extractChannelRequestType() const;
+ quint32 serverSeqNr() const { return m_serverSeqNr; }
+ static const QByteArray ExitStatusType;
+ static const QByteArray ExitSignalType;
+ static const QByteArray ForwardedTcpIpType;
+ virtual quint32 cipherBlockSize() const;
+ virtual quint32 macLength() const;
+ virtual void calculateLength() const;
+ void decrypt();
+ quint32 m_serverSeqNr;
+ SshDecryptionFacility m_decrypter;
+#endif // SSHINCOMINGPACKET_P_H
@@ -0,0 +1,301 @@
+#include <botan/dh.h>
+#include <botan/numthry.h>
+#include <botan/pk_ops.h>
+#include <botan/ecdh.h>
+#ifdef CREATOR_SSH_DEBUG
+#include <iostream>
+ // For debugging
+ void printNameList(const char *listName, const SshNameList &list)
+ qCDebug(sshLog, "%s:", listName);
+ for (const QByteArray &name : list.names) {
+ qCDebug(sshLog, "%s", name.constData());
+ void printData(const char *name, const QByteArray &data)
+ qCDebug(sshLog, "The client thinks the %s has length %d and is: %s", name, int(data.count()),
+ data.toHex().constData());
+SshKeyExchange::SshKeyExchange(const SshConnectionParameters &connParams,
+ : m_connParams(connParams), m_sendFacility(sendFacility)
+SshKeyExchange::~SshKeyExchange() {}
+void SshKeyExchange::sendKexInitPacket(const QByteArray &serverId)
+ m_serverId = serverId;
+ m_clientKexInitPayload = m_sendFacility.sendKeyExchangeInitPacket();
+bool SshKeyExchange::sendDhInitPacket(const SshIncomingPacket &serverKexInit)
+ qCDebug(sshLog, "server requests key exchange");
+ serverKexInit.printRawBytes();
+ SshKeyExchangeInit kexInitParams
+ = serverKexInit.extractKeyExchangeInitData();
+ printNameList("Key Algorithms", kexInitParams.keyAlgorithms);
+ printNameList("Server Host Key Algorithms", kexInitParams.serverHostKeyAlgorithms);
+ printNameList("Encryption algorithms client to server", kexInitParams.encryptionAlgorithmsClientToServer);
+ printNameList("Encryption algorithms server to client", kexInitParams.encryptionAlgorithmsServerToClient);
+ printNameList("MAC algorithms client to server", kexInitParams.macAlgorithmsClientToServer);
+ printNameList("MAC algorithms server to client", kexInitParams.macAlgorithmsServerToClient);
+ printNameList("Compression algorithms client to server", kexInitParams.compressionAlgorithmsClientToServer);
+ printNameList("Languages client to server", kexInitParams.languagesClientToServer);
+ printNameList("Languages server to client", kexInitParams.languagesServerToClient);
+ qCDebug(sshLog, "First packet follows: %d", kexInitParams.firstKexPacketFollows);
+ m_kexAlgoName = SshCapabilities::findBestMatch(SshCapabilities::KeyExchangeMethods,
+ kexInitParams.keyAlgorithms.names,
+ "KeyExchange");
+ m_serverHostKeyAlgo = SshCapabilities::findBestMatch(SshCapabilities::PublicKeyAlgorithms,
+ kexInitParams.serverHostKeyAlgorithms.names, "HostKey");
+ determineHashingAlgorithm(kexInitParams, true);
+ determineHashingAlgorithm(kexInitParams, false);
+ m_encryptionAlgo
+ = SshCapabilities::findBestMatch(SshCapabilities::EncryptionAlgorithms,
+ kexInitParams.encryptionAlgorithmsClientToServer.names, "Encryption");
+ m_decryptionAlgo
+ kexInitParams.encryptionAlgorithmsServerToClient.names, "Decryption");
+ SshCapabilities::findBestMatch(SshCapabilities::CompressionAlgorithms,
+ kexInitParams.compressionAlgorithmsClientToServer.names, "Compression Client to Server");
+ kexInitParams.compressionAlgorithmsServerToClient.names, "Compression Server to Client");
+ AutoSeeded_RNG rng;
+ if (m_kexAlgoName.startsWith(SshCapabilities::EcdhKexNamePrefix)) {
+ m_ecdhKey.reset(new ECDH_PrivateKey(rng, EC_Group(botanKeyExchangeAlgoName(m_kexAlgoName))));
+ m_sendFacility.sendKeyEcdhInitPacket(convertByteArray(m_ecdhKey->public_value()));
+ m_dhKey.reset(new DH_PrivateKey(rng, DL_Group(botanKeyExchangeAlgoName(m_kexAlgoName))));
+ m_sendFacility.sendKeyDhInitPacket(m_dhKey->get_y());
+ m_serverKexInitPayload = serverKexInit.payLoad();
+ return kexInitParams.firstKexPacketFollows;
+void SshKeyExchange::sendNewKeysPacket(const SshIncomingPacket &dhReply,
+ const QByteArray &clientId)
+ const SshKeyExchangeReply &reply
+ = dhReply.extractKeyExchangeReply(m_kexAlgoName, m_serverHostKeyAlgo);
+ if (m_dhKey && (reply.f <= 0 || reply.f >= m_dhKey->group_p())) {
+ "Server sent invalid f.");
+ QByteArray concatenatedData = AbstractSshPacket::encodeString(clientId);
+ concatenatedData += AbstractSshPacket::encodeString(m_serverId);
+ concatenatedData += AbstractSshPacket::encodeString(m_clientKexInitPayload);
+ concatenatedData += AbstractSshPacket::encodeString(m_serverKexInitPayload);
+ concatenatedData += reply.k_s;
+ printData("Client Id", AbstractSshPacket::encodeString(clientId));
+ printData("Server Id", AbstractSshPacket::encodeString(m_serverId));
+ printData("Client Payload", AbstractSshPacket::encodeString(m_clientKexInitPayload));
+ printData("Server payload", AbstractSshPacket::encodeString(m_serverKexInitPayload));
+ printData("K_S", reply.k_s);
+ SecureVector<byte> encodedK;
+ if (m_dhKey) {
+ concatenatedData += AbstractSshPacket::encodeMpInt(m_dhKey->get_y());
+ concatenatedData += AbstractSshPacket::encodeMpInt(reply.f);
+ std::unique_ptr<PK_Ops::Key_Agreement> dhOp = m_dhKey->create_key_agreement_op(rng, "Raw", "base");
+ std::vector<byte> encodedF = BigInt::encode(reply.f);
+ encodedK = dhOp->agree(0, encodedF.data(), encodedF.size(), nullptr, 0);
+ printData("y", AbstractSshPacket::encodeMpInt(m_dhKey->get_y()));
+ printData("f", AbstractSshPacket::encodeMpInt(reply.f));
+ m_dhKey.reset();
+ Q_ASSERT(m_ecdhKey);
+ concatenatedData // Q_C.
+ += AbstractSshPacket::encodeString(convertByteArray(m_ecdhKey->public_value()));
+ concatenatedData += AbstractSshPacket::encodeString(reply.q_s);
+ std::unique_ptr<PK_Ops::Key_Agreement> ecdhOp = m_ecdhKey->create_key_agreement_op(rng, "Raw", "base");
+ encodedK = ecdhOp->agree(0, convertByteArray(reply.q_s), reply.q_s.count(), nullptr, 0);
+ m_ecdhKey.reset();
+ // If we try to just use "BigInt::decode(encodedK)" clang fails to link
+ const BigInt k = BigInt::decode(encodedK.data(), encodedK.size());
+ m_k = AbstractSshPacket::encodeMpInt(k); // Roundtrip, as Botan encodes BigInts somewhat differently.
+ printData("K", m_k);
+ concatenatedData += m_k;
+ printData("Concatenated data", concatenatedData);
+ m_hash = HashFunction::create_or_throw(botanHMacAlgoName(hashAlgoForKexAlgo()));
+ const SecureVector<byte> &hashResult = m_hash->process(convertByteArray(concatenatedData),
+ concatenatedData.size());
+ m_h = convertByteArray(hashResult);
+ printData("H", m_h);
+ QScopedPointer<Public_Key> sigKey;
+ if (m_serverHostKeyAlgo == SshCapabilities::PubKeyDss) {
+ const DL_Group group(reply.hostKeyParameters.at(0), reply.hostKeyParameters.at(1),
+ reply.hostKeyParameters.at(2));
+ DSA_PublicKey * const dsaKey
+ = new DSA_PublicKey(group, reply.hostKeyParameters.at(3));
+ sigKey.reset(dsaKey);
+ } else if (m_serverHostKeyAlgo == SshCapabilities::PubKeyRsa) {
+ RSA_PublicKey * const rsaKey
+ = new RSA_PublicKey(reply.hostKeyParameters.at(1), reply.hostKeyParameters.at(0));
+ sigKey.reset(rsaKey);
+ QSSH_ASSERT_AND_RETURN(m_serverHostKeyAlgo.startsWith(SshCapabilities::PubKeyEcdsaPrefix));
+ const EC_Group domain(SshCapabilities::oid(m_serverHostKeyAlgo));
+ const PointGFp point = domain.OS2ECP(convertByteArray(reply.q), reply.q.count());
+ ECDSA_PublicKey * const ecdsaKey = new ECDSA_PublicKey(domain, point);
+ sigKey.reset(ecdsaKey);
+ const byte * const botanH = convertByteArray(m_h);
+ const Botan::byte * const botanSig = convertByteArray(reply.signatureBlob);
+ PK_Verifier verifier(*sigKey, botanEmsaAlgoName(m_serverHostKeyAlgo));
+ if (!verifier.verify_message(botanH, m_h.size(), botanSig, reply.signatureBlob.size())) {
+ "Invalid signature in key exchange reply packet.");
+ checkHostKey(reply.k_s);
+ m_sendFacility.sendNewKeysPacket();
+ m_hostFingerprint = QByteArray::fromStdString(sigKey->fingerprint_public("SHA-256"));
+QByteArray SshKeyExchange::hashAlgoForKexAlgo() const
+ if (m_kexAlgoName == SshCapabilities::EcdhNistp256)
+ return SshCapabilities::HMacSha256;
+ if (m_kexAlgoName == SshCapabilities::EcdhNistp384)
+ return SshCapabilities::HMacSha384;
+ if (m_kexAlgoName == SshCapabilities::EcdhNistp521)
+ return SshCapabilities::HMacSha512;
+ return SshCapabilities::HMacSha1;
+void SshKeyExchange::determineHashingAlgorithm(const SshKeyExchangeInit &kexInit,
+ bool serverToClient)
+ QByteArray * const algo = serverToClient ? &m_s2cHMacAlgo : &m_c2sHMacAlgo;
+ const QList<QByteArray> &serverCapabilities = serverToClient
+ ? kexInit.macAlgorithmsServerToClient.names
+ : kexInit.macAlgorithmsClientToServer.names;
+ *algo = SshCapabilities::findBestMatch(SshCapabilities::MacAlgorithms,
+ serverCapabilities,
+ "MacAlgorithms");
+void SshKeyExchange::checkHostKey(const QByteArray &hostKey)
+ if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingNone) {
+ if (m_connParams.hostKeyDatabase)
+ m_connParams.hostKeyDatabase->insertHostKey(m_connParams.host(), hostKey);
+ if (!m_connParams.hostKeyDatabase) {
+ SSH_TR("Host key database must exist "
+ "if host key checking is enabled."));
+ switch (m_connParams.hostKeyDatabase->matchHostKey(m_connParams.host(), hostKey)) {
+ case SshHostKeyDatabase::KeyLookupMatch:
+ return; // Nothing to do.
+ case SshHostKeyDatabase::KeyLookupMismatch:
+ if (m_connParams.hostKeyCheckingMode != SshHostKeyCheckingAllowMismatch)
+ throwHostKeyException();
+ case SshHostKeyDatabase::KeyLookupNoMatch:
+ if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingStrict)
+void SshKeyExchange::throwHostKeyException()
+ throw SshServerException(SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE, "Host key changed",
+ SSH_TR("Host key of machine \"%1\" has changed.")
+ .arg(m_connParams.host()));
@@ -0,0 +1,105 @@
+#ifndef SSHKEYEXCHANGE_P_H
+#define SSHKEYEXCHANGE_P_H
+class DH_PrivateKey;
+class ECDH_PrivateKey;
+class HashFunction;
+struct SshKeyExchangeInit;
+class SshKeyExchange
+ SshKeyExchange(const SshConnectionParameters &connParams, SshSendFacility &sendFacility);
+ ~SshKeyExchange();
+ const QByteArray &hostKeyFingerprint() { return m_hostFingerprint; }
+ void sendKexInitPacket(const QByteArray &serverId);
+ // Returns true <=> the server sends a guessed package.
+ bool sendDhInitPacket(const SshIncomingPacket &serverKexInit);
+ void sendNewKeysPacket(const SshIncomingPacket &dhReply,
+ const QByteArray &clientId);
+ QByteArray k() const { return m_k; }
+ QByteArray h() const { return m_h; }
+ Botan::HashFunction *hash() const { return m_hash.get(); }
+ QByteArray encryptionAlgo() const { return m_encryptionAlgo; }
+ QByteArray decryptionAlgo() const { return m_decryptionAlgo; }
+ QByteArray hMacAlgoClientToServer() const { return m_c2sHMacAlgo; }
+ QByteArray hMacAlgoServerToClient() const { return m_s2cHMacAlgo; }
+ QByteArray hashAlgoForKexAlgo() const;
+ void determineHashingAlgorithm(const SshKeyExchangeInit &kexInit, bool serverToClient);
+ void checkHostKey(const QByteArray &hostKey);
+ Q_NORETURN void throwHostKeyException();
+ QByteArray m_clientKexInitPayload;
+ QByteArray m_serverKexInitPayload;
+ QScopedPointer<Botan::DH_PrivateKey> m_dhKey;
+ QScopedPointer<Botan::ECDH_PrivateKey> m_ecdhKey;
+ QByteArray m_kexAlgoName;
+ QByteArray m_k;
+ QByteArray m_h;
+ QByteArray m_serverHostKeyAlgo;
+ QByteArray m_encryptionAlgo;
+ QByteArray m_decryptionAlgo;
+ QByteArray m_c2sHMacAlgo;
+ QByteArray m_s2cHMacAlgo;
+ std::unique_ptr<Botan::HashFunction> m_hash;
+#endif // SSHKEYEXCHANGE_P_H
@@ -0,0 +1,245 @@
+#include "sshkeygenerator.h"
+#include <botan/der_enc.h>
+#include <botan/pem.h>
+#include <botan/x509cert.h>
+#include <botan/x509_key.h>
+#include <QDateTime>
+#include <QInputDialog>
+SshKeyGenerator::SshKeyGenerator() : m_type(Rsa)
+bool SshKeyGenerator::generateKeys(KeyType type, PrivateKeyFormat format, int keySize,
+ EncryptionMode encryptionMode)
+ m_type = type;
+ m_encryptionMode = encryptionMode;
+ KeyPtr key;
+ switch (m_type) {
+ case Rsa:
+ key = KeyPtr(new RSA_PrivateKey(rng, keySize));
+ case Dsa:
+ key = KeyPtr(new DSA_PrivateKey(rng, DL_Group(rng, DL_Group::DSA_Kosherizer, keySize)));
+ case Ecdsa: {
+ const QByteArray algo = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth(keySize / 8);
+ key = KeyPtr(new ECDSA_PrivateKey(rng, EC_Group(SshCapabilities::oid(algo))));
+ switch (format) {
+ case Pkcs8:
+ generatePkcs8KeyStrings(key, rng);
+ case OpenSsl:
+ generateOpenSslKeyStrings(key);
+ case Mixed:
+ generatePkcs8KeyString(key, true, rng);
+ generateOpenSslPublicKeyString(key);
+ m_error = tr("Error generating key: %1").arg(QString::fromLocal8Bit(e.what()));
+void SshKeyGenerator::generatePkcs8KeyStrings(const KeyPtr &key, RandomNumberGenerator &rng)
+ generatePkcs8KeyString(key, false, rng);
+void SshKeyGenerator::generatePkcs8KeyString(const KeyPtr &key, bool privateKey,
+ RandomNumberGenerator &rng)
+ pipe.start_msg();
+ QByteArray *keyData;
+ if (privateKey) {
+ QString password;
+ if (m_encryptionMode == DoOfferEncryption)
+ password = getPassword();
+ if (!password.isEmpty())
+ pipe.write(PKCS8::PEM_encode(*key, rng, password.toLocal8Bit().data()));
+ pipe.write(PKCS8::PEM_encode(*key));
+ keyData = &m_privateKey;
+ pipe.write(X509::PEM_encode(*key));
+ keyData = &m_publicKey;
+ pipe.end_msg();
+ keyData->resize(static_cast<int>(pipe.remaining(pipe.message_count() - 1)));
+ size_t readSize = pipe.read(convertByteArray(*keyData), keyData->size(),
+ pipe.message_count() - 1);
+ if (readSize != size_t(keyData->size())) {
+ qCWarning(sshLog, "Didn't manage to read in all key data, only read %lu bytes", readSize);
+void SshKeyGenerator::generateOpenSslKeyStrings(const KeyPtr &key)
+ generateOpenSslPrivateKeyString(key);
+void SshKeyGenerator::generateOpenSslPublicKeyString(const KeyPtr &key)
+ QList<BigInt> params;
+ QByteArray keyId;
+ QByteArray q;
+ case Rsa: {
+ const QSharedPointer<RSA_PrivateKey> rsaKey = key.dynamicCast<RSA_PrivateKey>();
+ params << rsaKey->get_e() << rsaKey->get_n();
+ keyId = SshCapabilities::PubKeyRsa;
+ case Dsa: {
+ const QSharedPointer<DSA_PrivateKey> dsaKey = key.dynamicCast<DSA_PrivateKey>();
+ params << dsaKey->group_p() << dsaKey->group_q() << dsaKey->group_g() << dsaKey->get_y();
+ keyId = SshCapabilities::PubKeyDss;
+ const auto ecdsaKey = key.dynamicCast<ECDSA_PrivateKey>();
+ q = convertByteArray(ecdsaKey->public_point().encode(PointGFp::UNCOMPRESSED));
+ keyId = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth(
+ static_cast<int>(ecdsaKey->private_value().bytes()));
+ QByteArray publicKeyBlob = AbstractSshPacket::encodeString(keyId);
+ for (const BigInt &b : params) {
+ publicKeyBlob += AbstractSshPacket::encodeMpInt(b);
+ if (!q.isEmpty()) {
+ publicKeyBlob += AbstractSshPacket::encodeString(keyId.mid(11)); // Without "ecdsa-sha2-" prefix.
+ publicKeyBlob += AbstractSshPacket::encodeString(q);
+ publicKeyBlob = publicKeyBlob.toBase64();
+ const QByteArray id = "QtCreator/"
+ + QDateTime::currentDateTime().toString(Qt::ISODate).toUtf8();
+ m_publicKey = keyId + ' ' + publicKeyBlob + ' ' + id;
+void SshKeyGenerator::generateOpenSslPrivateKeyString(const KeyPtr &key)
+ const char *label = "";
+ const QSharedPointer<RSA_PrivateKey> rsaKey
+ = key.dynamicCast<RSA_PrivateKey>();
+ params << rsaKey->get_n() << rsaKey->get_e() << rsaKey->get_d() << rsaKey->get_p()
+ << rsaKey->get_q();
+ const BigInt dmp1 = rsaKey->get_d() % (rsaKey->get_p() - 1);
+ const BigInt dmq1 = rsaKey->get_d() % (rsaKey->get_q() - 1);
+ const BigInt iqmp = inverse_mod(rsaKey->get_q(), rsaKey->get_p());
+ params << dmp1 << dmq1 << iqmp;
+ label = "RSA PRIVATE KEY";
+ params << dsaKey->group_p() << dsaKey->group_q() << dsaKey->group_g() << dsaKey->get_y()
+ << dsaKey->get_x();
+ label = "DSA PRIVATE KEY";
+ case Ecdsa:
+ params << key.dynamicCast<ECDSA_PrivateKey>()->private_value();
+ label = "EC PRIVATE KEY";
+ DER_Encoder encoder;
+ encoder.start_cons(SEQUENCE).encode(size_t(0));
+ encoder.encode(b);
+ encoder.end_cons();
+ m_privateKey = QByteArray(PEM_Code::encode (encoder.get_contents(), label).c_str());
+QString SshKeyGenerator::getPassword() const
+ QInputDialog d;
+ d.setInputMode(QInputDialog::TextInput);
+ d.setTextEchoMode(QLineEdit::Password);
+ d.setWindowTitle(tr("Password for Private Key"));
+ d.setLabelText(tr("It is recommended that you secure your private key\n"
+ "with a password, which you can enter below."));
+ d.setOkButtonText(tr("Encrypt Key File"));
+ d.setCancelButtonText(tr("Do Not Encrypt Key File"));
+ int result = QDialog::Accepted;
+ while (result == QDialog::Accepted && password.isEmpty()) {
+ result = d.exec();
+ password = d.textValue();
+ return result == QDialog::Accepted ? password : QString();
@@ -0,0 +1,83 @@
+#ifndef SSHKEYGENERATOR_H
+#define SSHKEYGENERATOR_H
+ class Private_Key;
+ class RandomNumberGenerator;
+class QSSH_EXPORT SshKeyGenerator
+ Q_DECLARE_TR_FUNCTIONS(SshKeyGenerator)
+ enum KeyType { Rsa, Dsa, Ecdsa };
+ enum PrivateKeyFormat { Pkcs8, OpenSsl, Mixed };
+ enum EncryptionMode { DoOfferEncryption, DoNotOfferEncryption }; // Only relevant for Pkcs8 format.
+ SshKeyGenerator();
+ bool generateKeys(KeyType type, PrivateKeyFormat format, int keySize,
+ EncryptionMode encryptionMode = DoOfferEncryption);
+ QString error() const { return m_error; }
+ QByteArray privateKey() const { return m_privateKey; }
+ QByteArray publicKey() const { return m_publicKey; }
+ KeyType type() const { return m_type; }
+ typedef QSharedPointer<Botan::Private_Key> KeyPtr;
+ void generatePkcs8KeyStrings(const KeyPtr &key, Botan::RandomNumberGenerator &rng);
+ void generatePkcs8KeyString(const KeyPtr &key, bool privateKey,
+ Botan::RandomNumberGenerator &rng);
+ void generateOpenSslKeyStrings(const KeyPtr &key);
+ void generateOpenSslPrivateKeyString(const KeyPtr &key);
+ void generateOpenSslPublicKeyString(const KeyPtr &key);
+ QString getPassword() const;
+ QByteArray m_publicKey;
+ QByteArray m_privateKey;
+ KeyType m_type;
+ EncryptionMode m_encryptionMode;
+#endif // SSHKEYGENERATOR_H
@@ -0,0 +1,60 @@
+#include <QApplication>
+std::string SshKeyPasswordRetriever::get_passphrase()
+ const bool hasGui = dynamic_cast<QApplication *>(QApplication::instance());
+ if (hasGui) {
+ bool ok;
+ const QString &password = QInputDialog::getText(nullptr,
+ QCoreApplication::translate("QSsh::Ssh", "Password Required"),
+ QCoreApplication::translate("QSsh::Ssh", "Please enter the password for your private key."),
+ QLineEdit::Password, QString(), &ok);
+ return std::string(password.toLocal8Bit().data());
+ std::string password;
+ std::cout << "Please enter the password for your private key (set echo off beforehand!): " << std::flush;
+ std::cin >> password;
+ return password;
@@ -0,0 +1,47 @@
+#ifndef KEYPASSWORDRETRIEVER_H
+#define KEYPASSWORDRETRIEVER_H
+class SshKeyPasswordRetriever
+ static std::string get_passphrase();
+#endif // KEYPASSWORDRETRIEVER_H
@@ -0,0 +1,37 @@
+** Contact: http://www.qt.io/licensing
+** a written agreement between you and The Qt Company. For licensing terms and
+** conditions see http://www.qt.io/terms-conditions. For further information
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+Q_LOGGING_CATEGORY(sshLog, "qtc.ssh", QtWarningMsg)
@@ -0,0 +1,42 @@
+#ifndef SSHLOGGING_P_H
+#define SSHLOGGING_P_H
+#include <QLoggingCategory>
+Q_DECLARE_LOGGING_CATEGORY(sshLog)
+#endif // Include guard
@@ -0,0 +1,421 @@
+#include "sshoutgoingpacket_p.h"
+SshOutgoingPacket::SshOutgoingPacket(const SshEncryptionFacility &encrypter,
+ const quint32 &seqNr) : m_encrypter(encrypter), m_seqNr(seqNr)
+quint32 SshOutgoingPacket::cipherBlockSize() const
+ return qMax(m_encrypter.cipherBlockSize(), 4U);
+quint32 SshOutgoingPacket::macLength() const
+ return m_encrypter.macLength();
+QByteArray SshOutgoingPacket::generateKeyExchangeInitPacket()
+ const QByteArray &supportedkeyExchangeMethods
+ = encodeNameList(SshCapabilities::KeyExchangeMethods);
+ const QByteArray &supportedPublicKeyAlgorithms
+ = encodeNameList(SshCapabilities::PublicKeyAlgorithms);
+ const QByteArray &supportedEncryptionAlgorithms
+ = encodeNameList(SshCapabilities::EncryptionAlgorithms);
+ const QByteArray &supportedMacAlgorithms
+ = encodeNameList(SshCapabilities::MacAlgorithms);
+ const QByteArray &supportedCompressionAlgorithms
+ = encodeNameList(SshCapabilities::CompressionAlgorithms);
+ const QByteArray &supportedLanguages = encodeNameList(QList<QByteArray>());
+ init(SSH_MSG_KEXINIT);
+ m_data += m_encrypter.getRandomNumbers(16);
+ m_data.append(supportedkeyExchangeMethods);
+ m_data.append(supportedPublicKeyAlgorithms);
+ m_data.append(supportedEncryptionAlgorithms)
+ .append(supportedEncryptionAlgorithms);
+ m_data.append(supportedMacAlgorithms).append(supportedMacAlgorithms);
+ m_data.append(supportedCompressionAlgorithms)
+ .append(supportedCompressionAlgorithms);
+ m_data.append(supportedLanguages).append(supportedLanguages);
+ appendBool(false); // No guessed packet.
+ m_data.append(QByteArray(4, 0)); // Reserved.
+ QByteArray payload = m_data.mid(PayloadOffset);
+ finalize();
+ return payload;
+void SshOutgoingPacket::generateKeyDhInitPacket(const Botan::BigInt &e)
+ init(SSH_MSG_KEXDH_INIT).appendMpInt(e).finalize();
+void SshOutgoingPacket::generateKeyEcdhInitPacket(const QByteArray &clientQ)
+ init(SSH_MSG_KEX_ECDH_INIT).appendString(clientQ).finalize();
+void SshOutgoingPacket::generateNewKeysPacket()
+ init(SSH_MSG_NEWKEYS).finalize();
+void SshOutgoingPacket::generateUserAuthServiceRequestPacket()
+ generateServiceRequest("ssh-userauth");
+void SshOutgoingPacket::generateServiceRequest(const QByteArray &service)
+ init(SSH_MSG_SERVICE_REQUEST).appendString(service).finalize();
+void SshOutgoingPacket::generateUserAuthByPasswordRequestPacket(const QByteArray &user,
+ const QByteArray &service, const QByteArray &pwd)
+ init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service);
+ if (pwd.isEmpty())
+ appendString("none"); // RFC 4252, 5.2
+ appendString("password").appendBool(false).appendString(pwd);
+void SshOutgoingPacket::generateUserAuthByPublicKeyRequestPacket(const QByteArray &user,
+ const QByteArray &service, const QByteArray &key, const QByteArray &signature)
+ init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service)
+ .appendString("publickey").appendBool(true);
+ if (!key.isEmpty()) {
+ appendString(SshPacketParser::asString(key, quint32(0)));
+ appendString(key);
+ appendString(signature);
+ appendString(m_encrypter.authenticationAlgorithmName());
+ appendString(m_encrypter.authenticationPublicKey());
+ const QByteArray &dataToSign = m_data.mid(PayloadOffset);
+ appendString(m_encrypter.authenticationKeySignature(dataToSign));
+void SshOutgoingPacket::generateQueryPublicKeyPacket(const QByteArray &user,
+ const QByteArray &service, const QByteArray &publicKey)
+ // Name extraction cannot fail, we already verified this when receiving the key
+ // from the agent.
+ const QByteArray algoName = SshPacketParser::asString(publicKey, quint32(0));
+ SshOutgoingPacket packetToSign(m_encrypter, m_seqNr);
+ packetToSign.init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service)
+ .appendString("publickey").appendBool(true).appendString(algoName)
+ .appendString(publicKey);
+ const QByteArray &dataToSign
+ = encodeString(m_encrypter.sessionId()) + packetToSign.m_data.mid(PayloadOffset);
+ SshAgent::storeDataToSign(publicKey, dataToSign, qHash(m_encrypter.sessionId()));
+ .appendString("publickey").appendBool(false).appendString(algoName)
+ .appendString(publicKey).finalize();
+void SshOutgoingPacket::generateUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user,
+ const QByteArray &service)
+ // RFC 4256, 3.1
+ .appendString("keyboard-interactive")
+ .appendString(QByteArray()) // Language tag. Deprecated and should be empty
+ .appendString(QByteArray()) // Submethods.
+ .finalize();
+void SshOutgoingPacket::generateUserAuthInfoResponsePacket(const QStringList &responses)
+ // RFC 4256, 3.4
+ init(SSH_MSG_USERAUTH_INFO_RESPONSE).appendInt(responses.count());
+ for (const QString &response : responses) {
+ appendString(response.toUtf8());
+void SshOutgoingPacket::generateRequestFailurePacket()
+ init(SSH_MSG_REQUEST_FAILURE).finalize();
+void SshOutgoingPacket::generateIgnorePacket()
+ init(SSH_MSG_IGNORE).finalize();
+void SshOutgoingPacket::generateInvalidMessagePacket()
+ init(SSH_MSG_INVALID).finalize();
+void SshOutgoingPacket::generateSessionPacket(quint32 channelId,
+ quint32 windowSize, quint32 maxPacketSize)
+ init(SSH_MSG_CHANNEL_OPEN).appendString("session").appendInt(channelId)
+ .appendInt(windowSize).appendInt(maxPacketSize).finalize();
+void SshOutgoingPacket::generateDirectTcpIpPacket(quint32 channelId, quint32 windowSize,
+ quint32 maxPacketSize, const QByteArray &remoteHost, quint32 remotePort,
+ const QByteArray &localIpAddress, quint32 localPort)
+ init(SSH_MSG_CHANNEL_OPEN).appendString("direct-tcpip").appendInt(channelId)
+ .appendInt(windowSize).appendInt(maxPacketSize).appendString(remoteHost)
+ .appendInt(remotePort).appendString(localIpAddress).appendInt(localPort).finalize();
+void SshOutgoingPacket::generateTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort)
+ init(SSH_MSG_GLOBAL_REQUEST).appendString("tcpip-forward").appendBool(true)
+ .appendString(bindAddress).appendInt(bindPort).finalize();
+void SshOutgoingPacket::generateCancelTcpIpForwardPacket(const QByteArray &bindAddress,
+ quint32 bindPort)
+ init(SSH_MSG_GLOBAL_REQUEST).appendString("cancel-tcpip-forward").appendBool(true)
+void SshOutgoingPacket::generateEnvPacket(quint32 remoteChannel,
+ const QByteArray &var, const QByteArray &value)
+ init(SSH_MSG_CHANNEL_REQUEST).appendInt(remoteChannel).appendString("env")
+ .appendBool(false).appendString(var).appendString(value).finalize();
+void SshOutgoingPacket::generateX11ForwardingPacket(quint32 remoteChannel,
+ const QByteArray &protocol, const QByteArray &cookie, quint32 screenNumber)
+ init(SSH_MSG_CHANNEL_REQUEST).appendInt(remoteChannel).appendString("x11-req")
+ .appendBool(false).appendBool(false).appendString(protocol)
+ .appendString(cookie).appendInt(screenNumber).finalize();
+void SshOutgoingPacket::generatePtyRequestPacket(quint32 remoteChannel,
+ const SshPseudoTerminal &terminal)
+ init(SSH_MSG_CHANNEL_REQUEST).appendInt(remoteChannel)
+ .appendString("pty-req").appendBool(false)
+ .appendString(terminal.termType).appendInt(terminal.columnCount)
+ .appendInt(terminal.rowCount).appendInt(0).appendInt(0);
+ QByteArray modeString;
+ for (SshPseudoTerminal::ModeMap::ConstIterator it = terminal.modes.constBegin();
+ it != terminal.modes.constEnd(); ++it) {
+ modeString += char(it.key());
+ modeString += encodeInt(it.value());
+ modeString += char(0); // TTY_OP_END
+ appendString(modeString).finalize();
+void SshOutgoingPacket::generateExecPacket(quint32 remoteChannel,
+ const QByteArray &command)
+ init(SSH_MSG_CHANNEL_REQUEST).appendInt(remoteChannel).appendString("exec")
+ .appendBool(true).appendString(command).finalize();
+void SshOutgoingPacket::generateShellPacket(quint32 remoteChannel)
+ init(SSH_MSG_CHANNEL_REQUEST).appendInt(remoteChannel).appendString("shell")
+ .appendBool(true).finalize();
+void SshOutgoingPacket::generateSftpPacket(quint32 remoteChannel)
+ .appendString("subsystem").appendBool(true).appendString("sftp")
+void SshOutgoingPacket::generateWindowAdjustPacket(quint32 remoteChannel,
+ quint32 bytesToAdd)
+ init(SSH_MSG_CHANNEL_WINDOW_ADJUST).appendInt(remoteChannel)
+ .appendInt(bytesToAdd).finalize();
+void SshOutgoingPacket::generateChannelDataPacket(quint32 remoteChannel,
+ init(SSH_MSG_CHANNEL_DATA).appendInt(remoteChannel).appendString(data)
+void SshOutgoingPacket::generateChannelSignalPacket(quint32 remoteChannel,
+ const QByteArray &signalName)
+ .appendString("signal").appendBool(false).appendString(signalName)
+void SshOutgoingPacket::generateChannelEofPacket(quint32 remoteChannel)
+ init(SSH_MSG_CHANNEL_EOF).appendInt(remoteChannel).finalize();
+void SshOutgoingPacket::generateChannelClosePacket(quint32 remoteChannel)
+ init(SSH_MSG_CHANNEL_CLOSE).appendInt(remoteChannel).finalize();
+void SshOutgoingPacket::generateChannelOpenConfirmationPacket(quint32 remoteChannel,
+ quint32 localChannel,
+ quint32 localWindowSize,
+ quint32 maxPacketSize)
+ init(SSH_MSG_CHANNEL_OPEN_CONFIRMATION).appendInt(remoteChannel).appendInt(localChannel)
+ .appendInt(localWindowSize).appendInt(maxPacketSize).finalize();
+void SshOutgoingPacket::generateChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason,
+ const QByteArray &reasonString)
+ init(SSH_MSG_CHANNEL_OPEN_FAILURE).appendInt(remoteChannel).appendInt(reason)
+ .appendString(reasonString).appendString(QByteArray()).finalize();
+void SshOutgoingPacket::generateDisconnectPacket(SshErrorCode reason,
+ init(SSH_MSG_DISCONNECT).appendInt(reason).appendString(reasonString)
+ .appendString(QByteArray()).finalize();
+void SshOutgoingPacket::generateMsgUnimplementedPacket(quint32 serverSeqNr)
+ init(SSH_MSG_UNIMPLEMENTED).appendInt(serverSeqNr).finalize();
+SshOutgoingPacket &SshOutgoingPacket::appendInt(quint32 val)
+ m_data.append(encodeInt(val));
+SshOutgoingPacket &SshOutgoingPacket::appendMpInt(const Botan::BigInt &number)
+ m_data.append(encodeMpInt(number));
+SshOutgoingPacket &SshOutgoingPacket::appendBool(bool b)
+ m_data += static_cast<char>(b);
+SshOutgoingPacket &SshOutgoingPacket::appendString(const QByteArray &string)
+ m_data.append(encodeString(string));
+SshOutgoingPacket &SshOutgoingPacket::init(SshPacketType type)
+SshOutgoingPacket &SshOutgoingPacket::setPadding()
+ m_data += m_encrypter.getRandomNumbers(MinPaddingLength);
+ int padLength = MinPaddingLength;
+ const int divisor = sizeDivisor();
+ const int mod = m_data.size() % divisor;
+ padLength += divisor - mod;
+ m_data += m_encrypter.getRandomNumbers(padLength - MinPaddingLength);
+ m_data[PaddingLengthOffset] = padLength;
+SshOutgoingPacket &SshOutgoingPacket::encrypt()
+ const QByteArray &mac
+ = generateMac(m_encrypter, m_seqNr);
+ m_encrypter.encrypt(m_data);
+ m_data += mac;
+void SshOutgoingPacket::finalize()
+ setPadding();
+ setLengthField(m_data);
+ m_length = m_data.size() - 4;
+ qCDebug(sshLog) << "Encrypting packet of type" << int(m_data.at(TypeOffset));
+ encrypt();
+ qCDebug(sshLog) << "Sending packet of size" << rawData().count();
+int SshOutgoingPacket::sizeDivisor() const
+ return qMax(cipherBlockSize(), 8U);
+QByteArray SshOutgoingPacket::encodeNameList(const QList<QByteArray> &list)
+ data.resize(4);
+ for (int i = 0; i < list.count(); ++i) {
+ if (i > 0)
+ data.append(',');
+ data.append(list.at(i));
+ AbstractSshPacket::setLengthField(data);
+#ifndef SSHOUTGOINGPACKET_P_H
+#define SSHOUTGOINGPACKET_P_H
+#include "sshpseudoterminal.h"
+class SshEncryptionFacility;
+class SshOutgoingPacket : public AbstractSshPacket
+ SshOutgoingPacket(const SshEncryptionFacility &encrypter,
+ const quint32 &seqNr);
+ QByteArray generateKeyExchangeInitPacket(); // Returns payload.
+ void generateKeyDhInitPacket(const Botan::BigInt &e);
+ void generateKeyEcdhInitPacket(const QByteArray &clientQ);
+ void generateNewKeysPacket();
+ void generateDisconnectPacket(SshErrorCode reason,
+ const QByteArray &reasonString);
+ void generateMsgUnimplementedPacket(quint32 serverSeqNr);
+ void generateUserAuthServiceRequestPacket();
+ void generateUserAuthByPasswordRequestPacket(const QByteArray &user,
+ const QByteArray &service, const QByteArray &pwd);
+ void generateUserAuthByPublicKeyRequestPacket(const QByteArray &user,
+ const QByteArray &service, const QByteArray &key, const QByteArray &signature);
+ void generateQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service,
+ const QByteArray &publicKey);
+ void generateUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user,
+ const QByteArray &service);
+ void generateUserAuthInfoResponsePacket(const QStringList &responses);
+ void generateRequestFailurePacket();
+ void generateIgnorePacket();
+ void generateInvalidMessagePacket();
+ void generateSessionPacket(quint32 channelId, quint32 windowSize,
+ quint32 maxPacketSize);
+ void generateDirectTcpIpPacket(quint32 channelId, quint32 windowSize,
+ const QByteArray &localIpAddress, quint32 localPort);
+ void generateTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort);
+ void generateCancelTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort);
+ void generateEnvPacket(quint32 remoteChannel, const QByteArray &var,
+ const QByteArray &value);
+ void generateX11ForwardingPacket(quint32 remoteChannel, const QByteArray &protocol,
+ const QByteArray &cookie, quint32 screenNumber);
+ void generatePtyRequestPacket(quint32 remoteChannel,
+ const SshPseudoTerminal &terminal);
+ void generateExecPacket(quint32 remoteChannel, const QByteArray &command);
+ void generateShellPacket(quint32 remoteChannel);
+ void generateSftpPacket(quint32 remoteChannel);
+ void generateWindowAdjustPacket(quint32 remoteChannel, quint32 bytesToAdd);
+ void generateChannelDataPacket(quint32 remoteChannel,
+ void generateChannelSignalPacket(quint32 remoteChannel,
+ const QByteArray &signalName);
+ void generateChannelEofPacket(quint32 remoteChannel);
+ void generateChannelClosePacket(quint32 remoteChannel);
+ void generateChannelOpenConfirmationPacket(quint32 remoteChannel, quint32 localChannel,
+ quint32 localWindowSize, quint32 maxPackeSize);
+ void generateChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason,
+ static QByteArray encodeNameList(const QList<QByteArray> &list);
+ void generateServiceRequest(const QByteArray &service);
+ SshOutgoingPacket &init(SshPacketType type);
+ SshOutgoingPacket &setPadding();
+ SshOutgoingPacket &encrypt();
+ void finalize();
+ SshOutgoingPacket &appendInt(quint32 val);
+ SshOutgoingPacket &appendString(const QByteArray &string);
+ SshOutgoingPacket &appendMpInt(const Botan::BigInt &number);
+ SshOutgoingPacket &appendBool(bool b);
+ int sizeDivisor() const;
+ const SshEncryptionFacility &m_encrypter;
+ const quint32 &m_seqNr;
+#endif // SSHOUTGOINGPACKET_P_H
@@ -0,0 +1,163 @@
+#include <cctype>
+const quint32 AbstractSshPacket::PaddingLengthOffset = 4;
+const quint32 AbstractSshPacket::PayloadOffset = PaddingLengthOffset + 1;
+const quint32 AbstractSshPacket::TypeOffset = PayloadOffset;
+const quint32 AbstractSshPacket::MinPaddingLength = 4;
+static void printByteArray(const QByteArray &data)
+ qCDebug(sshLog, "%s", data.toHex().constData());
+AbstractSshPacket::AbstractSshPacket() : m_length(0) { }
+AbstractSshPacket::~AbstractSshPacket() {}
+bool AbstractSshPacket::isComplete() const
+ if (currentDataSize() < minPacketSize())
+ return 4 + length() + macLength() == currentDataSize();
+void AbstractSshPacket::clear()
+SshPacketType AbstractSshPacket::type() const
+ return static_cast<SshPacketType>(m_data.at(TypeOffset));
+QByteArray AbstractSshPacket::payLoad() const
+ return QByteArray(m_data.constData() + PayloadOffset,
+ length() - paddingLength() - 1);
+void AbstractSshPacket::printRawBytes() const
+ printByteArray(m_data);
+QByteArray AbstractSshPacket::encodeString(const QByteArray &string)
+ data += string;
+ setLengthField(data);
+QByteArray AbstractSshPacket::encodeMpInt(const Botan::BigInt &number)
+ if (number.is_zero())
+ return QByteArray(4, 0);
+ int stringLength = static_cast<int>(number.bytes());
+ const bool positiveAndMsbSet = number.sign() == Botan::BigInt::Positive
+ && (number.byte_at(stringLength - 1) & 0x80);
+ if (positiveAndMsbSet)
+ ++stringLength;
+ data.resize(4 + stringLength);
+ int pos = 4;
+ data[pos++] = '\0';
+ number.binary_encode(reinterpret_cast<Botan::byte *>(data.data()) + pos);
+int AbstractSshPacket::paddingLength() const
+ return m_data[PaddingLengthOffset];
+quint32 AbstractSshPacket::length() const
+ //Q_ASSERT(currentDataSize() >= minPacketSize());
+ if (m_length == 0)
+ calculateLength();
+ return m_length;
+void AbstractSshPacket::calculateLength() const
+QByteArray AbstractSshPacket::generateMac(const SshAbstractCryptoFacility &crypt,
+ quint32 seqNr) const
+ const quint32 seqNrBe = qToBigEndian(seqNr);
+ QByteArray data(reinterpret_cast<const char *>(&seqNrBe), int(sizeof seqNrBe));
+ data += QByteArray(m_data.constData(), length() + 4);
+ return crypt.generateMac(data, data.size());
+quint32 AbstractSshPacket::minPacketSize() const
+ return qMax<quint32>(cipherBlockSize(), 16) + macLength();
+void AbstractSshPacket::setLengthField(QByteArray &data)
+ Q_ASSERT(data.size() >= 4);
+ const quint32 length = qToBigEndian<quint32>(data.size() - 4);
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ data.replace(qsizetype(0), qsizetype(4), reinterpret_cast<const char *>(&length), qsizetype(4));
+ data.replace(0, 4, reinterpret_cast<const char *>(&length), 4);
@@ -0,0 +1,157 @@
+#ifndef SSHPACKET_P_H
+#define SSHPACKET_P_H
+namespace Botan { class BigInt; }
+enum SshPacketType {
+ SSH_MSG_DISCONNECT = 1,
+ SSH_MSG_IGNORE = 2,
+ SSH_MSG_UNIMPLEMENTED = 3,
+ SSH_MSG_DEBUG = 4,
+ SSH_MSG_SERVICE_REQUEST = 5,
+ SSH_MSG_SERVICE_ACCEPT = 6,
+ SSH_MSG_KEXINIT = 20,
+ SSH_MSG_NEWKEYS = 21,
+ SSH_MSG_KEXDH_INIT = 30,
+ SSH_MSG_KEX_ECDH_INIT = 30,
+ SSH_MSG_KEXDH_REPLY = 31,
+ SSH_MSG_KEX_ECDH_REPLY = 31,
+ SSH_MSG_USERAUTH_REQUEST = 50,
+ SSH_MSG_USERAUTH_FAILURE = 51,
+ SSH_MSG_USERAUTH_SUCCESS = 52,
+ SSH_MSG_USERAUTH_BANNER = 53,
+ SSH_MSG_USERAUTH_PK_OK = 60,
+ SSH_MSG_USERAUTH_PASSWD_CHANGEREQ = 60,
+ SSH_MSG_USERAUTH_INFO_REQUEST = 60,
+ SSH_MSG_USERAUTH_INFO_RESPONSE = 61,
+ SSH_MSG_GLOBAL_REQUEST = 80,
+ SSH_MSG_REQUEST_SUCCESS = 81,
+ SSH_MSG_REQUEST_FAILURE = 82,
+ // TODO: We currently take no precautions against sending these messages
+ // during a key re-exchange, which is not allowed.
+ SSH_MSG_CHANNEL_OPEN = 90,
+ SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91,
+ SSH_MSG_CHANNEL_OPEN_FAILURE = 92,
+ SSH_MSG_CHANNEL_WINDOW_ADJUST = 93,
+ SSH_MSG_CHANNEL_DATA = 94,
+ SSH_MSG_CHANNEL_EXTENDED_DATA = 95,
+ SSH_MSG_CHANNEL_EOF = 96,
+ SSH_MSG_CHANNEL_CLOSE = 97,
+ SSH_MSG_CHANNEL_REQUEST = 98,
+ SSH_MSG_CHANNEL_SUCCESS = 99,
+ SSH_MSG_CHANNEL_FAILURE = 100,
+ // Not completely safe, since the server may actually understand this
+ // message type as an extension. Switch to a different value in that case
+ // (between 128 and 191).
+ SSH_MSG_INVALID = 128
+enum SshOpenFailureType {
+ SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1,
+ SSH_OPEN_CONNECT_FAILED = 2,
+ SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3,
+ SSH_OPEN_RESOURCE_SHORTAGE = 4
+enum SshExtendedDataType { SSH_EXTENDED_DATA_STDERR = 1 };
+class SshAbstractCryptoFacility;
+class AbstractSshPacket
+ Q_DISABLE_COPY(AbstractSshPacket)
+ virtual ~AbstractSshPacket();
+ SshPacketType type() const;
+ static QByteArray encodeString(const QByteArray &string);
+ static QByteArray encodeMpInt(const Botan::BigInt &number);
+ template<typename T> static QByteArray encodeInt(T value)
+ const T valMsb = qToBigEndian(value);
+ return QByteArray(reinterpret_cast<const char *>(&valMsb), sizeof valMsb);
+ static void setLengthField(QByteArray &data);
+ void printRawBytes() const; // For Debugging.
+ QByteArray payLoad() const;
+ AbstractSshPacket();
+ virtual quint32 cipherBlockSize() const = 0;
+ virtual quint32 macLength() const = 0;
+ quint32 length() const;
+ int paddingLength() const;
+ quint32 minPacketSize() const;
+ quint32 currentDataSize() const { return m_data.size(); }
+ QByteArray generateMac(const SshAbstractCryptoFacility &crypt,
+ quint32 seqNr) const;
+ static const quint32 PaddingLengthOffset;
+ static const quint32 PayloadOffset;
+ static const quint32 TypeOffset;
+ static const quint32 MinPaddingLength;
+ mutable QByteArray m_data;
+ mutable quint32 m_length;
+#endif // SSHPACKET_P_H
+namespace { quint32 size(const QByteArray &data) { return data.size(); } }
+QString SshPacketParser::asUserString(const QByteArray &rawString)
+ QByteArray filteredString;
+ filteredString.resize(rawString.size());
+ for (int i = 0; i < rawString.size(); ++i) {
+ const char c = rawString.at(i);
+ filteredString[i]
+ = std::isprint(c) || c == '\n' || c == '\r' || c == '\t' ? c : '?';
+ return QString::fromUtf8(filteredString);
+bool SshPacketParser::asBool(const QByteArray &data, quint32 offset)
+ if (size(data) <= offset)
+ return data.at(offset);
+bool SshPacketParser::asBool(const QByteArray &data, quint32 *offset)
+ bool b = asBool(data, *offset);
+ ++(*offset);
+ return b;
+quint32 SshPacketParser::asUint32(const QByteArray &data, quint32 offset)
+ if (size(data) < offset + 4)
+ return qFromBigEndian<quint32>(data.constData() + offset);
+quint32 SshPacketParser::asUint32(const QByteArray &data, quint32 *offset)
+ const quint32 v = asUint32(data, *offset);
+ *offset += 4;
+ return v;
+quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 offset)
+ if (size(data) < offset + 8)
+ return qFromBigEndian<quint64>(data.constData() + offset);
+quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 *offset)
+ const quint64 val = asUint64(data, *offset);
+ *offset += 8;
+ return val;
+QByteArray SshPacketParser::asString(const QByteArray &data, quint32 offset)
+ return asString(data, &offset);
+QByteArray SshPacketParser::asString(const QByteArray &data, quint32 *offset)
+ const quint32 length = asUint32(data, offset);
+ if (size(data) < *offset + length)
+ const QByteArray &string = data.mid(*offset, length);
+ *offset += length;
+ return string;
+QString SshPacketParser::asUserString(const QByteArray &data, quint32 *offset)
+ return asUserString(asString(data, offset));
+SshNameList SshPacketParser::asNameList(const QByteArray &data, quint32 *offset)
+ const int listEndPos = *offset + length;
+ if (data.size() < listEndPos)
+ SshNameList names(length + 4);
+ int nextNameOffset = *offset;
+ int nextCommaOffset = data.indexOf(',', nextNameOffset);
+ while (nextNameOffset > 0 && nextNameOffset < listEndPos) {
+ const int stringEndPos = nextCommaOffset == -1
+ || nextCommaOffset > listEndPos ? listEndPos : nextCommaOffset;
+ names.names << QByteArray(data.constData() + nextNameOffset,
+ stringEndPos - nextNameOffset);
+ nextNameOffset = nextCommaOffset + 1;
+ nextCommaOffset = data.indexOf(',', nextNameOffset);
+ return names;
+Botan::BigInt SshPacketParser::asBigInt(const QByteArray &data, quint32 *offset)
+ if (length == 0)
+ return Botan::BigInt();
+ const Botan::byte *numberStart
+ = reinterpret_cast<const Botan::byte *>(data.constData() + *offset);
+ return Botan::BigInt::decode(numberStart, length);
+#ifndef SSHPACKETPARSER_P_H
+#define SSHPACKETPARSER_P_H
+struct SshNameList
+ SshNameList() : originalLength(0) {}
+ SshNameList(quint32 originalLength) : originalLength(originalLength) {}
+ quint32 originalLength;
+ QList<QByteArray> names;
+class SshPacketParseException { };
+// This class's functions try to read a byte array at a certain offset
+// as the respective chunk of data as specified in the SSH RFCs.
+// If they succeed, they update the offset, so they can easily
+// be called in succession by client code.
+// For convenience, some have also versions that don't update the offset,
+// so they can be called with rvalues if the new value is not needed.
+// If they fail, they throw an SshPacketParseException.
+class SshPacketParser
+ static bool asBool(const QByteArray &data, quint32 offset);
+ static bool asBool(const QByteArray &data, quint32 *offset);
+ static quint16 asUint16(const QByteArray &data, quint32 offset);
+ static quint16 asUint16(const QByteArray &data, quint32 *offset);
+ static quint64 asUint64(const QByteArray &data, quint32 offset);
+ static quint64 asUint64(const QByteArray &data, quint32 *offset);
+ static quint32 asUint32(const QByteArray &data, quint32 offset);
+ static quint32 asUint32(const QByteArray &data, quint32 *offset);
+ static QByteArray asString(const QByteArray &data, quint32 offset);
+ static QByteArray asString(const QByteArray &data, quint32 *offset);
+ static QString asUserString(const QByteArray &data, quint32 *offset);
+ static SshNameList asNameList(const QByteArray &data, quint32 *offset);
+ static Botan::BigInt asBigInt(const QByteArray &data, quint32 *offset);
+ static QString asUserString(const QByteArray &rawString);
+#endif // SSHPACKETPARSER_P_H
@@ -0,0 +1,118 @@
+#ifndef SSHPSEUDOTERMINAL_H
+#define SSHPSEUDOTERMINAL_H
+class QSSH_EXPORT SshPseudoTerminal
+ public:
+ explicit SshPseudoTerminal(const QByteArray &termType = "vt100",
+ int rowCount = 24, int columnCount = 80)
+ : termType(termType), rowCount(rowCount), columnCount(columnCount) {}
+ QByteArray termType;
+ int rowCount;
+ int columnCount;
+ enum Mode {
+ VINTR = 1, // Interrupt character.
+ VQUIT = 2, // The quit character (sends SIGQUIT signal on POSIX systems).
+ VERASE = 3, // Erase the character to left of the cursor.
+ VKILL = 4, // Kill the current input line.
+ VEOF = 5, // End-of-file character (sends EOF from the terminal).
+ VEOL = 6, // End-of-line character in addition to carriage return and/or linefeed.
+ VEOL2 = 7, // Additional end-of-line character.
+ VSTART = 8, // Continues paused output (normally control-Q).
+ VSTOP = 9, // Pauses output (normally control-S).
+ VSUSP = 10, // Suspends the current program.
+ VDSUSP = 11, // Another suspend character.
+ VREPRINT = 12, // Reprints the current input line.
+ VWERASE = 13, // Erases a word left of cursor.
+ VLNEXT = 14, // Enter the next character typed literally, even if it is a special character.
+ VFLUSH = 15, // Character to flush output.
+ VSWTCH = 16, // Switch to a different shell layer.
+ VSTATUS = 17, // Prints system status line (load, command, pid, etc).
+ VDISCARD = 18, // Toggles the flushing of terminal output.
+ IGNPAR = 30, // The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, and 1 if it is TRUE.
+ PARMRK = 31, // Mark parity and framing errors.
+ INPCK = 32, // Enable checking of parity errors.
+ ISTRIP = 33, // Strip 8th bit off characters.
+ INLCR = 34, // Map NL into CR on input.
+ IGNCR = 35, // Ignore CR on input.
+ ICRNL = 36, // Map CR to NL on input.
+ IUCLC = 37, // Translate uppercase characters to lowercase.
+ IXON = 38, // Enable output flow control.
+ IXANY = 39, // Any char will restart after stop.
+ IXOFF = 40, // Enable input flow control.
+ IMAXBEL = 41, // Ring bell on input queue full.
+ ISIG = 50, // Enable signals INTR, QUIT, [D]SUSP.
+ ICANON = 51, // Canonicalize input lines.
+ XCASE = 52, // Enable input and output of uppercase characters by preceding their lowercase equivalents with "\".
+ ECHO = 53, // Enable echoing.
+ ECHOE = 54, // Visually erase chars.
+ ECHOK = 55, // Kill character discards current line.
+ ECHONL = 56, // Echo NL even if ECHO is off.
+ NOFLSH = 57, // Don't flush after interrupt.
+ TOSTOP = 58, // Stop background jobs from output.
+ IEXTEN = 59, // Enable extensions.
+ ECHOCTL = 60, // Echo control characters as ^(Char).
+ ECHOKE = 61, // Visual erase for line kill.
+ PENDIN = 62, // Retype pending input.
+ OPOST = 70, // Enable output processing.
+ OLCUC = 71, // Convert lowercase to uppercase.
+ ONLCR = 72, // Map NL to CR-NL.
+ OCRNL = 73, // Translate carriage return to newline (output).
+ ONOCR = 74, // Translate newline to carriage return-newline (output).
+ ONLRET = 75, // Newline performs a carriage return (output).
+ CS7 = 90, // 7 bit mode.
+ CS8 = 91, // 8 bit mode.
+ PARENB = 92, // Parity enable.
+ PARODD = 93, // Odd parity, else even.
+ TTY_OP_ISPEED = 128, // Specifies the input baud rate in bits per second.
+ TTY_OP_OSPEED = 129 // Specifies the output baud rate in bits per second.
+ typedef QHash<Mode, quint32> ModeMap;
+ ModeMap modes;
+#endif // SSHPSEUDOTERMINAL_H
@@ -0,0 +1,401 @@
+#include <cstring>
+#include <cstdlib>
+const struct {
+ SshRemoteProcess::Signal signalEnum;
+ const char * const signalString;
+} signalMap[] = {
+ {SshRemoteProcess::AbrtSignal, "ABRT"}, {SshRemoteProcess::AlrmSignal, "ALRM"},
+ {SshRemoteProcess::FpeSignal, "FPE"}, {SshRemoteProcess::HupSignal, "HUP"},
+ {SshRemoteProcess::IllSignal, "ILL"}, {SshRemoteProcess::IntSignal, "INT"},
+ {SshRemoteProcess::KillSignal, "KILL"}, {SshRemoteProcess::PipeSignal, "PIPE"},
+ {SshRemoteProcess::QuitSignal, "QUIT"}, {SshRemoteProcess::SegvSignal, "SEGV"},
+ {SshRemoteProcess::TermSignal, "TERM"}, {SshRemoteProcess::Usr1Signal, "USR1"},
+ {SshRemoteProcess::Usr2Signal, "USR2"}
+SshRemoteProcess::SshRemoteProcess(const QByteArray &command, quint32 channelId,
+ : d(new Internal::SshRemoteProcessPrivate(command, channelId, sendFacility, this))
+ init();
+SshRemoteProcess::SshRemoteProcess(quint32 channelId, Internal::SshSendFacility &sendFacility)
+ : d(new Internal::SshRemoteProcessPrivate(channelId, sendFacility, this))
+SshRemoteProcess::~SshRemoteProcess()
+ QSSH_ASSERT(d->channelState() != Internal::AbstractSshChannel::SessionEstablished);
+ SshRemoteProcess::close();
+bool SshRemoteProcess::atEnd() const
+ return QIODevice::atEnd() && d->data().isEmpty();
+qint64 SshRemoteProcess::bytesAvailable() const
+ return QIODevice::bytesAvailable() + d->data().count();
+bool SshRemoteProcess::canReadLine() const
+ return QIODevice::canReadLine() || d->data().contains('\n');
+QByteArray SshRemoteProcess::readAllStandardOutput()
+ return readAllFromChannel(QProcess::StandardOutput);
+QByteArray SshRemoteProcess::readAllStandardError()
+ return readAllFromChannel(QProcess::StandardError);
+QByteArray SshRemoteProcess::readAllFromChannel(QProcess::ProcessChannel channel)
+ const QProcess::ProcessChannel currentReadChannel = readChannel();
+ setReadChannel(channel);
+ const QByteArray &data = readAll();
+ setReadChannel(currentReadChannel);
+void SshRemoteProcess::close()
+qint64 SshRemoteProcess::readData(char *data, qint64 maxlen)
+ const qint64 bytesRead = qMin(qint64(d->data().count()), maxlen);
+ memcpy(data, d->data().constData(), bytesRead);
+ d->data().remove(0, bytesRead);
+ return bytesRead;
+qint64 SshRemoteProcess::writeData(const char *data, qint64 len)
+ if (isRunning()) {
+ d->sendData(QByteArray(data, len));
+ return len;
+QProcess::ProcessChannel SshRemoteProcess::readChannel() const
+ return d->m_readChannel;
+void SshRemoteProcess::setReadChannel(QProcess::ProcessChannel channel)
+ d->m_readChannel = channel;
+void SshRemoteProcess::init()
+ connect(d, &Internal::SshRemoteProcessPrivate::started,
+ this, &SshRemoteProcess::started, Qt::QueuedConnection);
+ connect(d, &Internal::SshRemoteProcessPrivate::readyReadStandardOutput,
+ this, &SshRemoteProcess::readyReadStandardOutput, Qt::QueuedConnection);
+ connect(d, &Internal::SshRemoteProcessPrivate::readyRead,
+ this, &SshRemoteProcess::readyRead, Qt::QueuedConnection);
+ connect(d, &Internal::SshRemoteProcessPrivate::readyReadStandardError,
+ this, &SshRemoteProcess::readyReadStandardError, Qt::QueuedConnection);
+ connect(d, &Internal::SshRemoteProcessPrivate::closed,
+ this, &SshRemoteProcess::closed, Qt::QueuedConnection);
+ connect(d, &Internal::SshRemoteProcessPrivate::eof,
+ this, &SshRemoteProcess::readChannelFinished, Qt::QueuedConnection);
+void SshRemoteProcess::addToEnvironment(const QByteArray &var, const QByteArray &value)
+ if (d->channelState() == Internal::SshRemoteProcessPrivate::Inactive)
+ d->m_env << qMakePair(var, value); // Cached locally and sent on start()
+void SshRemoteProcess::clearEnvironment()
+ d->m_env.clear();
+void SshRemoteProcess::requestTerminal(const SshPseudoTerminal &terminal)
+ QSSH_ASSERT_AND_RETURN(d->channelState() == Internal::SshRemoteProcessPrivate::Inactive);
+ d->m_useTerminal = true;
+ d->m_terminal = terminal;
+void SshRemoteProcess::requestX11Forwarding(const QString &displayName)
+ d->m_x11DisplayName = displayName;
+void SshRemoteProcess::start()
+ if (d->channelState() == Internal::SshRemoteProcessPrivate::Inactive) {
+ qCDebug(Internal::sshLog, "process start requested, channel id = %u", d->localChannelId());
+void SshRemoteProcess::sendSignal(Signal signal)
+ const char *signalString = nullptr;
+ for (size_t i = 0; i < sizeof signalMap/sizeof *signalMap && !signalString; ++i) {
+ if (signalMap[i].signalEnum == signal)
+ signalString = signalMap[i].signalString;
+ QSSH_ASSERT_AND_RETURN(signalString);
+ d->m_sendFacility.sendChannelSignalPacket(d->remoteChannel(), signalString);
+ setErrorString(QString::fromLocal8Bit(e.what()));
+bool SshRemoteProcess::isRunning() const
+ return d->m_procState == Internal::SshRemoteProcessPrivate::Running;
+int SshRemoteProcess::exitCode() const { return d->m_exitCode; }
+SshRemoteProcess::Signal SshRemoteProcess::exitSignal() const
+ return static_cast<SshRemoteProcess::Signal>(d->m_signal);
+void SshRemoteProcessPrivate::failToStart(const QString &reason)
+ if (m_procState != NotYetStarted)
+ m_proc->setErrorString(reason);
+ setProcState(StartFailed);
+SshRemoteProcessPrivate::SshRemoteProcessPrivate(const QByteArray &command,
+ quint32 channelId, SshSendFacility &sendFacility, SshRemoteProcess *proc)
+ m_command(command),
+ m_isShell(false),
+ m_useTerminal(false),
+ m_proc(proc)
+SshRemoteProcessPrivate::SshRemoteProcessPrivate(quint32 channelId, SshSendFacility &sendFacility,
+ SshRemoteProcess *proc)
+ m_isShell(true),
+ m_useTerminal(true),
+void SshRemoteProcessPrivate::init()
+ m_procState = NotYetStarted;
+ m_wasRunning = false;
+ m_exitCode = 0;
+ m_readChannel = QProcess::StandardOutput;
+ m_signal = SshRemoteProcess::NoSignal;
+void SshRemoteProcessPrivate::setProcState(ProcessState newState)
+ qCDebug(sshLog, "channel: old state = %d,new state = %d", m_procState, newState);
+ m_procState = newState;
+ if (newState == StartFailed) {
+ emit closed(SshRemoteProcess::FailedToStart);
+ } else if (newState == Running) {
+ m_wasRunning = true;
+ emit started();
+QByteArray &SshRemoteProcessPrivate::data()
+ return m_readChannel == QProcess::StandardOutput ? m_stdout : m_stderr;
+void SshRemoteProcessPrivate::closeHook()
+ if (m_wasRunning) {
+ if (m_signal != SshRemoteProcess::NoSignal)
+ emit closed(SshRemoteProcess::CrashExit);
+ emit closed(SshRemoteProcess::NormalExit);
+void SshRemoteProcessPrivate::handleOpenSuccessInternal()
+ if (m_x11DisplayName.isEmpty())
+ startProcess(X11DisplayInfo());
+ emit x11ForwardingRequested(m_x11DisplayName);
+void SshRemoteProcessPrivate::startProcess(const X11DisplayInfo &displayInfo)
+ for (const EnvVar &envVar : m_env) {
+ m_sendFacility.sendEnvPacket(remoteChannel(), envVar.first,
+ envVar.second);
+ if (!m_x11DisplayName.isEmpty()) {
+ m_sendFacility.sendX11ForwardingPacket(remoteChannel(), displayInfo.protocol,
+ displayInfo.randomCookie.toHex(), 0);
+ if (m_useTerminal)
+ m_sendFacility.sendPtyRequestPacket(remoteChannel(), m_terminal);
+ if (m_isShell)
+ m_sendFacility.sendShellPacket(remoteChannel());
+ m_sendFacility.sendExecPacket(remoteChannel(), m_command);
+ setProcState(ExecRequested);
+void SshRemoteProcessPrivate::handleOpenFailureInternal(const QString &reason)
+ failToStart(reason);
+void SshRemoteProcessPrivate::handleChannelSuccess()
+ if (m_procState != ExecRequested) {
+ "Unexpected SSH_MSG_CHANNEL_SUCCESS message.");
+ setProcState(Running);
+void SshRemoteProcessPrivate::handleChannelFailure()
+ "Unexpected SSH_MSG_CHANNEL_FAILURE message.");
+void SshRemoteProcessPrivate::handleChannelDataInternal(const QByteArray &data)
+ m_stdout += data;
+ emit readyReadStandardOutput();
+ if (m_readChannel == QProcess::StandardOutput)
+ emit readyRead();
+void SshRemoteProcessPrivate::handleChannelExtendedDataInternal(quint32 type,
+ if (type != SSH_EXTENDED_DATA_STDERR) {
+ qCWarning(sshLog, "Unknown extended data type %u", type);
+ m_stderr += data;
+ emit readyReadStandardError();
+ if (m_readChannel == QProcess::StandardError)
+void SshRemoteProcessPrivate::handleExitStatus(const SshChannelExitStatus &exitStatus)
+ qCDebug(sshLog, "Process exiting with exit code %d", exitStatus.exitStatus);
+ m_exitCode = exitStatus.exitStatus;
+ m_procState = Exited;
+void SshRemoteProcessPrivate::handleExitSignal(const SshChannelExitSignal &signal)
+ qCDebug(sshLog, "Exit due to signal %s", signal.signal.data());
+ for (size_t i = 0; i < sizeof signalMap/sizeof *signalMap; ++i) {
+ if (signalMap[i].signalString == signal.signal) {
+ m_signal = signalMap[i].signalEnum;
+ m_proc->setErrorString(tr("Process killed by signal"));
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid signal",
+ tr("Server sent invalid signal \"%1\"").arg(QString::fromUtf8(signal.signal)));
@@ -0,0 +1,143 @@
+#ifndef SSHREMOTECOMMAND_H
+#define SSHREMOTECOMMAND_H
+#include <QProcess>
+class SshPseudoTerminal;
+ \class QSsh::SshRemoteProcess
+ \brief This class implements an SSH channel for running a remote process.
+ Objects are created via SshConnection::createRemoteProcess.
+ The process is started via the start() member function.
+ If the process needs a pseudo terminal, you can request one
+ via requestTerminal() before calling start().
+ Note that this class does not support QIODevice's waitFor*() functions, i.e. it has
+ no synchronous mode.
+// TODO: ProcessChannel
+class QSSH_EXPORT SshRemoteProcess : public QIODevice
+ friend class Internal::SshRemoteProcessPrivate;
+ typedef QSharedPointer<SshRemoteProcess> Ptr;
+ enum ExitStatus { FailedToStart, CrashExit, NormalExit };
+ enum Signal {
+ AbrtSignal, AlrmSignal, FpeSignal, HupSignal, IllSignal, IntSignal, KillSignal, PipeSignal,
+ QuitSignal, SegvSignal, TermSignal, Usr1Signal, Usr2Signal, NoSignal
+ ~SshRemoteProcess();
+ QProcess::ProcessChannel readChannel() const;
+ void setReadChannel(QProcess::ProcessChannel channel);
+ * Note that this is of limited value in practice, because servers are
+ * usually configured to ignore such requests for security reasons.
+ void addToEnvironment(const QByteArray &var, const QByteArray &value);
+ void clearEnvironment();
+ void requestTerminal(const SshPseudoTerminal &terminal);
+ void requestX11Forwarding(const QString &displayName);
+ void start();
+ bool isRunning() const;
+ int exitCode() const;
+ Signal exitSignal() const;
+ QByteArray readAllStandardOutput();
+ QByteArray readAllStandardError();
+ // Note: This is ignored by the OpenSSH server.
+ void sendSignal(Signal signal);
+ void kill() { sendSignal(KillSignal); }
+ void started();
+ void readyReadStandardOutput();
+ void readyReadStandardError();
+ * Parameter is of type ExitStatus, but we use int because of
+ * signal/slot awkwardness (full namespace required).
+ void closed(int exitStatus);
+ SshRemoteProcess(const QByteArray &command, quint32 channelId,
+ SshRemoteProcess(quint32 channelId, Internal::SshSendFacility &sendFacility);
+ void init();
+ QByteArray readAllFromChannel(QProcess::ProcessChannel channel);
+ Internal::SshRemoteProcessPrivate *d;
+#endif // SSHREMOTECOMMAND_H
+#ifndef SSHREMOTEPROCESS_P_H
+#define SSHREMOTEPROCESS_P_H
+class X11DisplayInfo;
+class SshRemoteProcessPrivate : public AbstractSshChannel
+ friend class QSsh::SshRemoteProcess;
+ enum ProcessState {
+ NotYetStarted, ExecRequested, StartFailed, Running, Exited
+ void failToStart(const QString &reason);
+ void startProcess(const X11DisplayInfo &displayInfo);
+ void readyRead();
+ void x11ForwardingRequested(const QString &display);
+ SshRemoteProcessPrivate(const QByteArray &command, quint32 channelId,
+ SshSendFacility &sendFacility, SshRemoteProcess *proc);
+ SshRemoteProcessPrivate(quint32 channelId, SshSendFacility &sendFacility,
+ SshRemoteProcess *proc);
+ void setProcState(ProcessState newState);
+ QByteArray &data();
+ QProcess::ProcessChannel m_readChannel;
+ ProcessState m_procState;
+ bool m_wasRunning;
+ int m_signal;
+ int m_exitCode;
+ const QByteArray m_command;
+ const bool m_isShell;
+ typedef QPair<QByteArray, QByteArray> EnvVar;
+ QList<EnvVar> m_env;
+ bool m_useTerminal;
+ SshPseudoTerminal m_terminal;
+ QString m_x11DisplayName;
+ QByteArray m_stdout;
+ QByteArray m_stderr;
+ SshRemoteProcess *m_proc;
+#endif // SSHREMOTEPROCESS_P_H
@@ -0,0 +1,283 @@
+#include "sshremoteprocessrunner.h"
+enum State { Inactive, Connecting, Connected, ProcessRunning };
+class SshRemoteProcessRunnerPrivate
+ SshRemoteProcessRunnerPrivate() : m_state(Inactive) {}
+ SshRemoteProcess::Ptr m_process;
+ SshConnection *m_connection;
+ bool m_runInTerminal;
+ QByteArray m_command;
+ QSsh::SshError m_lastConnectionError;
+ QString m_lastConnectionErrorString;
+ SshRemoteProcess::ExitStatus m_exitStatus;
+ SshRemoteProcess::Signal m_exitSignal;
+ QString m_processErrorString;
+ State m_state;
+SshRemoteProcessRunner::SshRemoteProcessRunner(QObject *parent)
+ : QObject(parent), d(new SshRemoteProcessRunnerPrivate)
+SshRemoteProcessRunner::~SshRemoteProcessRunner()
+ setState(Inactive);
+void SshRemoteProcessRunner::run(const QByteArray &command,
+ const SshConnectionParameters &sshParams)
+ QSSH_ASSERT_AND_RETURN(d->m_state == Inactive);
+ d->m_runInTerminal = false;
+ runInternal(command, sshParams);
+void SshRemoteProcessRunner::runInTerminal(const QByteArray &command,
+ const SshPseudoTerminal &terminal, const SshConnectionParameters &sshParams)
+ d->m_runInTerminal = true;
+void SshRemoteProcessRunner::runInternal(const QByteArray &command,
+ setState(Connecting);
+ d->m_lastConnectionError = SshNoError;
+ d->m_lastConnectionErrorString.clear();
+ d->m_processErrorString.clear();
+ d->m_exitSignal = SshRemoteProcess::NoSignal;
+ d->m_exitCode = -1;
+ d->m_command = command;
+ d->m_connection = QSsh::acquireConnection(sshParams);
+ connect(d->m_connection, &SshConnection::error,
+ this, &SshRemoteProcessRunner::handleConnectionError);
+ connect(d->m_connection, &SshConnection::disconnected,
+ this, &SshRemoteProcessRunner::handleDisconnected);
+ if (d->m_connection->state() == SshConnection::Connected) {
+ handleConnected();
+ connect(d->m_connection, &SshConnection::connected, this, &SshRemoteProcessRunner::handleConnected);
+ if (d->m_connection->state() == SshConnection::Unconnected)
+ d->m_connection->connectToHost();
+void SshRemoteProcessRunner::handleConnected()
+ QSSH_ASSERT_AND_RETURN(d->m_state == Connecting);
+ setState(Connected);
+ d->m_process = d->m_connection->createRemoteProcess(d->m_command);
+ connect(d->m_process.data(), &SshRemoteProcess::started,
+ this, &SshRemoteProcessRunner::handleProcessStarted);
+ connect(d->m_process.data(), &SshRemoteProcess::closed,
+ this, &SshRemoteProcessRunner::handleProcessFinished);
+ connect(d->m_process.data(), &SshRemoteProcess::readyReadStandardOutput,
+ this, &SshRemoteProcessRunner::handleStdout);
+ connect(d->m_process.data(), &SshRemoteProcess::readyReadStandardError,
+ this, &SshRemoteProcessRunner::handleStderr);
+ if (d->m_runInTerminal)
+ d->m_process->requestTerminal(d->m_terminal);
+ d->m_process->start();
+void SshRemoteProcessRunner::handleConnectionError(QSsh::SshError error)
+ d->m_lastConnectionError = error;
+ d->m_lastConnectionErrorString = d->m_connection->errorString();
+ handleDisconnected();
+ emit connectionError();
+void SshRemoteProcessRunner::handleDisconnected()
+ QSSH_ASSERT_AND_RETURN(d->m_state == Connecting || d->m_state == Connected
+ || d->m_state == ProcessRunning);
+void SshRemoteProcessRunner::handleProcessStarted()
+ QSSH_ASSERT_AND_RETURN(d->m_state == Connected);
+ setState(ProcessRunning);
+ emit processStarted();
+void SshRemoteProcessRunner::handleProcessFinished(int exitStatus)
+ d->m_exitStatus = static_cast<SshRemoteProcess::ExitStatus>(exitStatus);
+ switch (d->m_exitStatus) {
+ case SshRemoteProcess::FailedToStart:
+ case SshRemoteProcess::CrashExit:
+ QSSH_ASSERT_AND_RETURN(d->m_state == ProcessRunning);
+ d->m_exitSignal = d->m_process->exitSignal();
+ case SshRemoteProcess::NormalExit:
+ d->m_exitCode = d->m_process->exitCode();
+ Q_ASSERT_X(false, Q_FUNC_INFO, "Impossible exit status.");
+ d->m_processErrorString = d->m_process->errorString();
+ emit processClosed(exitStatus);
+void SshRemoteProcessRunner::handleStdout()
+ d->m_stdout += d->m_process->readAllStandardOutput();
+void SshRemoteProcessRunner::handleStderr()
+ d->m_stderr += d->m_process->readAllStandardError();
+void SshRemoteProcessRunner::setState(int newState)
+ if (d->m_state == newState)
+ d->m_state = static_cast<State>(newState);
+ if (d->m_state == Inactive) {
+ if (d->m_process) {
+ disconnect(d->m_process.data(), nullptr, this, nullptr);
+ d->m_process->close();
+ d->m_process.clear();
+ if (d->m_connection) {
+ disconnect(d->m_connection, nullptr, this, nullptr);
+ QSsh::releaseConnection(d->m_connection);
+ d->m_connection = nullptr;
+QByteArray SshRemoteProcessRunner::command() const { return d->m_command; }
+SshError SshRemoteProcessRunner::lastConnectionError() const { return d->m_lastConnectionError; }
+QString SshRemoteProcessRunner::lastConnectionErrorString() const {
+ return d->m_lastConnectionErrorString;
+bool SshRemoteProcessRunner::isProcessRunning() const
+ return d->m_process && d->m_process->isRunning();
+SshRemoteProcess::ExitStatus SshRemoteProcessRunner::processExitStatus() const
+ QSSH_ASSERT(!isProcessRunning());
+ return d->m_exitStatus;
+SshRemoteProcess::Signal SshRemoteProcessRunner::processExitSignal() const
+ QSSH_ASSERT(processExitStatus() == SshRemoteProcess::CrashExit);
+ return d->m_exitSignal;
+int SshRemoteProcessRunner::processExitCode() const
+ QSSH_ASSERT(processExitStatus() == SshRemoteProcess::NormalExit);
+ return d->m_exitCode;
+QString SshRemoteProcessRunner::processErrorString() const
+ return d->m_processErrorString;
+QByteArray SshRemoteProcessRunner::readAllStandardOutput()
+ const QByteArray data = d->m_stdout;
+ d->m_stdout.clear();
+QByteArray SshRemoteProcessRunner::readAllStandardError()
+ const QByteArray data = d->m_stderr;
+ d->m_stderr.clear();
+void SshRemoteProcessRunner::writeDataToProcess(const QByteArray &data)
+ QSSH_ASSERT(isProcessRunning());
+ d->m_process->write(data);
+void SshRemoteProcessRunner::sendSignalToProcess(SshRemoteProcess::Signal signal)
+ d->m_process->sendSignal(signal);
+void SshRemoteProcessRunner::cancel()
@@ -0,0 +1,98 @@
+#ifndef SSHREMOTEPROCESSRUNNER_H
+#define SSHREMOTEPROCESSRUNNER_H
+class SshRemoteProcessRunnerPrivate;
+ \class QSsh::SshRemoteProcessRunner
+ \brief Convenience class for running a remote process over an SSH connection.
+class QSSH_EXPORT SshRemoteProcessRunner : public QObject
+ SshRemoteProcessRunner(QObject *parent = nullptr);
+ ~SshRemoteProcessRunner();
+ void run(const QByteArray &command, const SshConnectionParameters &sshParams);
+ void runInTerminal(const QByteArray &command, const SshPseudoTerminal &terminal,
+ const SshConnectionParameters &sshParams);
+ QByteArray command() const;
+ QSsh::SshError lastConnectionError() const;
+ QString lastConnectionErrorString() const;
+ bool isProcessRunning() const;
+ void writeDataToProcess(const QByteArray &data);
+ void sendSignalToProcess(SshRemoteProcess::Signal signal); // No effect with OpenSSH server.
+ void cancel(); // Does not stop remote process, just frees SSH-related process resources.
+ SshRemoteProcess::ExitStatus processExitStatus() const;
+ SshRemoteProcess::Signal processExitSignal() const;
+ int processExitCode() const;
+ QString processErrorString() const;
+ void connectionError();
+ void processStarted();
+ void processClosed(int exitStatus); // values are of type SshRemoteProcess::ExitStatus
+ void handleConnectionError(QSsh::SshError error);
+ void handleProcessStarted();
+ void handleProcessFinished(int exitStatus);
+ void handleStdout();
+ void handleStderr();
+ void runInternal(const QByteArray &command, const QSsh::SshConnectionParameters &sshParams);
+ void setState(int newState);
+ Internal::SshRemoteProcessRunnerPrivate * const d;
+#endif // SSHREMOTEPROCESSRUNNER_H
@@ -0,0 +1,288 @@
+SshSendFacility::SshSendFacility(QTcpSocket *socket)
+ : m_clientSeqNr(0), m_socket(socket),
+ m_outgoingPacket(m_encrypter, m_clientSeqNr)
+void SshSendFacility::sendPacket()
+ qCDebug(sshLog, "Sending packet, client seq nr is %u", m_clientSeqNr);
+ if (m_socket->isValid()
+ && m_socket->state() == QAbstractSocket::ConnectedState) {
+ m_socket->write(m_outgoingPacket.rawData());
+ ++m_clientSeqNr;
+void SshSendFacility::reset()
+ m_clientSeqNr = 0;
+ m_encrypter.clearKeys();
+void SshSendFacility::recreateKeys(const SshKeyExchange &keyExchange)
+ m_encrypter.recreateKeys(keyExchange);
+void SshSendFacility::createAuthenticationKey(const QByteArray &privKeyFileContents)
+ m_encrypter.createAuthenticationKey(privKeyFileContents);
+QByteArray SshSendFacility::sendKeyExchangeInitPacket()
+ const QByteArray &payLoad = m_outgoingPacket.generateKeyExchangeInitPacket();
+ return payLoad;
+void SshSendFacility::sendKeyDhInitPacket(const Botan::BigInt &e)
+ m_outgoingPacket.generateKeyDhInitPacket(e);
+void SshSendFacility::sendKeyEcdhInitPacket(const QByteArray &clientQ)
+ m_outgoingPacket.generateKeyEcdhInitPacket(clientQ);
+void SshSendFacility::sendNewKeysPacket()
+ m_outgoingPacket.generateNewKeysPacket();
+void SshSendFacility::sendDisconnectPacket(SshErrorCode reason,
+ m_outgoingPacket.generateDisconnectPacket(reason, reasonString);
+void SshSendFacility::sendMsgUnimplementedPacket(quint32 serverSeqNr)
+ m_outgoingPacket.generateMsgUnimplementedPacket(serverSeqNr);
+void SshSendFacility::sendUserAuthServiceRequestPacket()
+ m_outgoingPacket.generateUserAuthServiceRequestPacket();
+void SshSendFacility::sendUserAuthByPasswordRequestPacket(const QByteArray &user,
+ m_outgoingPacket.generateUserAuthByPasswordRequestPacket(user, service, pwd);
+void SshSendFacility::sendUserAuthByPublicKeyRequestPacket(const QByteArray &user,
+ m_outgoingPacket.generateUserAuthByPublicKeyRequestPacket(user, service, key, signature);
+void SshSendFacility::sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service,
+ const QByteArray &publicKey)
+ m_outgoingPacket.generateQueryPublicKeyPacket(user, service, publicKey);
+void SshSendFacility::sendUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user,
+ m_outgoingPacket.generateUserAuthByKeyboardInteractiveRequestPacket(user, service);
+void SshSendFacility::sendUserAuthInfoResponsePacket(const QStringList &responses)
+ m_outgoingPacket.generateUserAuthInfoResponsePacket(responses);
+void SshSendFacility::sendRequestFailurePacket()
+ m_outgoingPacket.generateRequestFailurePacket();
+void SshSendFacility::sendIgnorePacket()
+ m_outgoingPacket.generateIgnorePacket();
+void SshSendFacility::sendInvalidPacket()
+ m_outgoingPacket.generateInvalidMessagePacket();
+void SshSendFacility::sendSessionPacket(quint32 channelId, quint32 windowSize,
+ m_outgoingPacket.generateSessionPacket(channelId, windowSize,
+ maxPacketSize);
+void SshSendFacility::sendDirectTcpIpPacket(quint32 channelId, quint32 windowSize,
+ m_outgoingPacket.generateDirectTcpIpPacket(channelId, windowSize, maxPacketSize, remoteHost,
+ remotePort, localIpAddress, localPort);
+void SshSendFacility::sendTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort)
+ m_outgoingPacket.generateTcpIpForwardPacket(bindAddress, bindPort);
+void SshSendFacility::sendCancelTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort)
+ m_outgoingPacket.generateCancelTcpIpForwardPacket(bindAddress, bindPort);
+void SshSendFacility::sendPtyRequestPacket(quint32 remoteChannel,
+ m_outgoingPacket.generatePtyRequestPacket(remoteChannel, terminal);
+void SshSendFacility::sendEnvPacket(quint32 remoteChannel,
+ m_outgoingPacket.generateEnvPacket(remoteChannel, var, value);
+void SshSendFacility::sendX11ForwardingPacket(quint32 remoteChannel, const QByteArray &protocol,
+ const QByteArray &cookie, quint32 screenNumber)
+ m_outgoingPacket.generateX11ForwardingPacket(remoteChannel, protocol, cookie, screenNumber);
+void SshSendFacility::sendExecPacket(quint32 remoteChannel,
+ m_outgoingPacket.generateExecPacket(remoteChannel, command);
+void SshSendFacility::sendShellPacket(quint32 remoteChannel)
+ m_outgoingPacket.generateShellPacket(remoteChannel);
+void SshSendFacility::sendSftpPacket(quint32 remoteChannel)
+ m_outgoingPacket.generateSftpPacket(remoteChannel);
+void SshSendFacility::sendWindowAdjustPacket(quint32 remoteChannel,
+ m_outgoingPacket.generateWindowAdjustPacket(remoteChannel, bytesToAdd);
+void SshSendFacility::sendChannelDataPacket(quint32 remoteChannel,
+ m_outgoingPacket.generateChannelDataPacket(remoteChannel, data);
+void SshSendFacility::sendChannelSignalPacket(quint32 remoteChannel,
+ m_outgoingPacket.generateChannelSignalPacket(remoteChannel, signalName);
+void SshSendFacility::sendChannelEofPacket(quint32 remoteChannel)
+ m_outgoingPacket.generateChannelEofPacket(remoteChannel);
+void SshSendFacility::sendChannelClosePacket(quint32 remoteChannel)
+ m_outgoingPacket.generateChannelClosePacket(remoteChannel);
+void SshSendFacility::sendChannelOpenConfirmationPacket(quint32 remoteChannel, quint32 localChannel,
+ quint32 localWindowSize, quint32 maxPacketSize)
+ m_outgoingPacket.generateChannelOpenConfirmationPacket(remoteChannel, localChannel,
+ localWindowSize, maxPacketSize);
+void SshSendFacility::sendChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason,
+ m_outgoingPacket.generateChannelOpenFailurePacket(remoteChannel, reason, reasonString);
@@ -0,0 +1,122 @@
+#ifndef SSHCONNECTIONOUTSTATE_P_H
+#define SSHCONNECTIONOUTSTATE_P_H
+class SshSendFacility
+ SshSendFacility(QTcpSocket *socket);
+ QByteArray sessionId() const { return m_encrypter.sessionId(); }
+ QByteArray sendKeyExchangeInitPacket();
+ void sendKeyDhInitPacket(const Botan::BigInt &e);
+ void sendKeyEcdhInitPacket(const QByteArray &clientQ);
+ void sendNewKeysPacket();
+ void sendDisconnectPacket(SshErrorCode reason,
+ void sendMsgUnimplementedPacket(quint32 serverSeqNr);
+ void sendUserAuthServiceRequestPacket();
+ void sendUserAuthByPasswordRequestPacket(const QByteArray &user,
+ void sendUserAuthByPublicKeyRequestPacket(const QByteArray &user,
+ void sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service,
+ void sendUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user,
+ void sendUserAuthInfoResponsePacket(const QStringList &responses);
+ void sendRequestFailurePacket();
+ void sendIgnorePacket();
+ void sendInvalidPacket();
+ void sendSessionPacket(quint32 channelId, quint32 windowSize,
+ void sendDirectTcpIpPacket(quint32 channelId, quint32 windowSize, quint32 maxPacketSize,
+ const QByteArray &remoteHost, quint32 remotePort, const QByteArray &localIpAddress,
+ quint32 localPort);
+ void sendTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort);
+ void sendCancelTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort);
+ void sendPtyRequestPacket(quint32 remoteChannel,
+ void sendEnvPacket(quint32 remoteChannel, const QByteArray &var,
+ void sendX11ForwardingPacket(quint32 remoteChannel, const QByteArray &protocol,
+ void sendExecPacket(quint32 remoteChannel, const QByteArray &command);
+ void sendShellPacket(quint32 remoteChannel);
+ void sendSftpPacket(quint32 remoteChannel);
+ void sendWindowAdjustPacket(quint32 remoteChannel, quint32 bytesToAdd);
+ void sendChannelDataPacket(quint32 remoteChannel, const QByteArray &data);
+ void sendChannelSignalPacket(quint32 remoteChannel,
+ void sendChannelEofPacket(quint32 remoteChannel);
+ void sendChannelClosePacket(quint32 remoteChannel);
+ void sendChannelOpenConfirmationPacket(quint32 remoteChannel, quint32 localChannel,
+ void sendChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason,
+ quint32 nextClientSeqNr() const { return m_clientSeqNr; }
+ bool encrypterIsValid() const { return m_encrypter.isValid(); }
+ quint32 m_clientSeqNr;
+ SshEncryptionFacility m_encrypter;
+ SshOutgoingPacket m_outgoingPacket;
+#endif // SSHCONNECTIONOUTSTATE_P_H
@@ -0,0 +1,137 @@
+SshTcpIpForwardServerPrivate::SshTcpIpForwardServerPrivate(const QString &bindAddress,
+ quint16 bindPort, SshSendFacility &sendFacility)
+ m_bindAddress(bindAddress),
+ m_bindPort(bindPort),
+ m_state(SshTcpIpForwardServer::Inactive)
+SshTcpIpForwardServer::SshTcpIpForwardServer(const QString &bindAddress, quint16 bindPort,
+ : d(new SshTcpIpForwardServerPrivate(bindAddress, bindPort, sendFacility))
+ connect(&d->m_timeoutTimer, &QTimer::timeout, this, &SshTcpIpForwardServer::setClosed);
+void SshTcpIpForwardServer::setListening(quint16 port)
+ QSSH_ASSERT_AND_RETURN(d->m_state != Listening);
+ d->m_bindPort = port;
+ d->m_state = Listening;
+ emit stateChanged(Listening);
+void SshTcpIpForwardServer::setClosed()
+ QSSH_ASSERT_AND_RETURN(d->m_state != Inactive);
+ d->m_state = Inactive;
+ emit stateChanged(Inactive);
+void SshTcpIpForwardServer::setNewConnection(const SshForwardedTcpIpTunnel::Ptr &connection)
+ d->m_pendingConnections.append(connection);
+ emit newConnection();
+SshTcpIpForwardServer::~SshTcpIpForwardServer()
+void SshTcpIpForwardServer::initialize()
+ if (d->m_state == Inactive || d->m_state == Closing) {
+ d->m_state = Initializing;
+ emit stateChanged(Initializing);
+ d->m_sendFacility.sendTcpIpForwardPacket(d->m_bindAddress.toUtf8(), d->m_bindPort);
+ d->m_timeoutTimer.start(SshTcpIpForwardServerPrivate::ReplyTimeout);
+ d->m_timeoutTimer.stop();
+ setClosed();
+void SshTcpIpForwardServer::close()
+ if (d->m_state == Initializing || d->m_state == Listening) {
+ d->m_state = Closing;
+ emit stateChanged(Closing);
+ d->m_sendFacility.sendCancelTcpIpForwardPacket(d->m_bindAddress.toUtf8(),
+ d->m_bindPort);
+const QString &SshTcpIpForwardServer::bindAddress() const
+ return d->m_bindAddress;
+quint16 SshTcpIpForwardServer::port() const
+ return d->m_bindPort;
+SshTcpIpForwardServer::State SshTcpIpForwardServer::state() const
+ return d->m_state;
+SshForwardedTcpIpTunnel::Ptr SshTcpIpForwardServer::nextPendingConnection()
+ return d->m_pendingConnections.takeFirst();
@@ -0,0 +1,81 @@
+class SshTcpIpForwardServerPrivate;
+class QSSH_EXPORT SshTcpIpForwardServer : public QObject
+ friend class Internal::SshConnectionPrivate;
+ enum State {
+ Inactive,
+ Initializing,
+ Listening,
+ Closing
+ typedef QSharedPointer<SshTcpIpForwardServer> Ptr;
+ ~SshTcpIpForwardServer();
+ const QString &bindAddress() const;
+ quint16 port() const;
+ SshForwardedTcpIpTunnel::Ptr nextPendingConnection();
+ void newConnection();
+ void stateChanged(State state);
+ SshTcpIpForwardServer(const QString &bindAddress, quint16 bindPort,
+ void setListening(quint16 port);
+ void setClosed();
+ void setNewConnection(const SshForwardedTcpIpTunnel::Ptr &connection);
+ Internal::SshTcpIpForwardServerPrivate * const d;
+class SshTcpIpForwardServerPrivate
+ SshTcpIpForwardServerPrivate(const QString &bindAddress, quint16 bindPort,
+ const QString m_bindAddress;
+ quint16 m_bindPort;
+ SshTcpIpForwardServer::State m_state;
+ QList<SshForwardedTcpIpTunnel::Ptr> m_pendingConnections;
+SshTcpIpTunnelPrivate::SshTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility)
+ : AbstractSshChannel(channelId, sendFacility)
+ connect(this, &AbstractSshChannel::eof, this, &SshTcpIpTunnelPrivate::handleEof);
+SshTcpIpTunnelPrivate::~SshTcpIpTunnelPrivate()
+void SshTcpIpTunnelPrivate::handleChannelSuccess()
+void SshTcpIpTunnelPrivate::handleChannelFailure()
+void SshTcpIpTunnelPrivate::handleOpenFailureInternal(const QString &reason)
+ emit error(reason);
+void SshTcpIpTunnelPrivate::handleChannelDataInternal(const QByteArray &data)
+ m_data += data;
+void SshTcpIpTunnelPrivate::handleChannelExtendedDataInternal(quint32 type,
+ qCWarning(sshLog, "%s: Unexpected extended channel data. Type is %u, content is '%s'.",
+ Q_FUNC_INFO, type, data.constData());
+void SshTcpIpTunnelPrivate::handleExitStatus(const SshChannelExitStatus &exitStatus)
+ qCWarning(sshLog, "%s: Unexpected exit status %d.", Q_FUNC_INFO, exitStatus.exitStatus);
+void SshTcpIpTunnelPrivate::handleExitSignal(const SshChannelExitSignal &signal)
+ qCWarning(sshLog, "%s: Unexpected exit signal %s.", Q_FUNC_INFO, signal.signal.constData());
+void SshTcpIpTunnelPrivate::closeHook()
+void SshTcpIpTunnelPrivate::handleEof()
+ * For some reason, the OpenSSH server only sends EOF when the remote port goes away,
+ * but does not close the channel, even though it becomes useless in that case.
+ * So we close it ourselves.
+qint64 SshTcpIpTunnelPrivate::readData(char *data, qint64 maxlen)
+ const qint64 bytesRead = qMin(qint64(m_data.count()), maxlen);
+ memcpy(data, m_data.constData(), bytesRead);
+ m_data.remove(0, bytesRead);
+qint64 SshTcpIpTunnelPrivate::writeData(const char *data, qint64 len)
+ QSSH_ASSERT_AND_RETURN_VALUE(channelState() == AbstractSshChannel::SessionEstablished, 0);
+ sendData(QByteArray(data, len));
@@ -0,0 +1,82 @@
+class SshTcpIpTunnelPrivate : public AbstractSshChannel
+ SshTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility);
+ ~SshTcpIpTunnelPrivate();
+ template<class SshTcpIpTunnel>
+ void init(SshTcpIpTunnel *q)
+ connect(this, &SshTcpIpTunnelPrivate::closed,
+ q, &SshTcpIpTunnel::close, Qt::QueuedConnection);
+ connect(this, &SshTcpIpTunnelPrivate::readyRead,
+ q, &SshTcpIpTunnel::readyRead, Qt::QueuedConnection);
+ connect(this, &SshTcpIpTunnelPrivate::error, q, [q](const QString &reason) {
+ q->setErrorString(reason);
+ emit q->error(reason);
+ }, Qt::QueuedConnection);
+ void handleChannelSuccess() override;
+ void handleChannelFailure() override;
+ void handleOpenFailureInternal(const QString &reason) override;
+ void handleChannelDataInternal(const QByteArray &data) override;
+ void handleChannelExtendedDataInternal(quint32 type, const QByteArray &data) override;
+ void handleExitStatus(const SshChannelExitStatus &exitStatus) override;
+ void handleExitSignal(const SshChannelExitSignal &signal) override;
+ void closeHook() override;
+ void handleEof();
@@ -0,0 +1,222 @@
+class X11Socket : public QObject
+ X11Socket(QObject *parent) : QObject(parent) { }
+ void establishConnection(const X11DisplayInfo &displayInfo)
+ const bool hostNameIsPath = displayInfo.hostName.startsWith(QLatin1Char('/')); // macOS
+ const bool hasActualHostName = !displayInfo.hostName.isEmpty()
+ && displayInfo.hostName != QLatin1String("unix") && !displayInfo.hostName.endsWith(QLatin1String("/unix"))
+ && !hostNameIsPath;
+ if (hasActualHostName) {
+ QTcpSocket * const socket = new QTcpSocket(this);
+ connect(socket, &QTcpSocket::connected, this, &X11Socket::connected);
+// connect(socket, &QTcpSocket::errorOccurred, this, [this, socket] {
+// emit error(socket->errorString());
+// });
+ socket->connectToHost(displayInfo.hostName, 6000 + displayInfo.display);
+ m_socket = socket;
+ const QString serverBasePath = hostNameIsPath ? QString(displayInfo.hostName + QLatin1Char(':'))
+ : QLatin1String("/tmp/.X11-unix/X");
+ QLocalSocket * const socket = new QLocalSocket(this);
+ connect(socket, &QLocalSocket::connected, this, &X11Socket::connected);
+// connect(socket, SIGNAL(error()), this, [this, socket] {
+ socket->connectToServer(serverBasePath + QString::number(displayInfo.display));
+ connect(m_socket, &QIODevice::readyRead, this,
+ [this] { emit dataAvailable(m_socket->readAll()); });
+ void closeConnection()
+ m_socket->disconnect();
+ if (localSocket())
+ localSocket()->disconnectFromServer();
+ tcpSocket()->disconnectFromHost();
+ void write(const QByteArray &data)
+ bool hasError() const
+ return (localSocket() && localSocket()->error() != QLocalSocket::UnknownSocketError)
+ || (tcpSocket() && tcpSocket()->error() != QTcpSocket::UnknownSocketError);
+ bool isConnected() const
+ return (localSocket() && localSocket()->state() == QLocalSocket::ConnectedState)
+ || (tcpSocket() && tcpSocket()->state() == QTcpSocket::ConnectedState);
+ void error(const QString &message);
+ void dataAvailable(const QByteArray &data);
+ QLocalSocket *localSocket() const { return qobject_cast<QLocalSocket *>(m_socket); }
+ QTcpSocket *tcpSocket() const { return qobject_cast<QTcpSocket *>(m_socket); }
+ QIODevice *m_socket = nullptr;
+SshX11Channel::SshX11Channel(const X11DisplayInfo &displayInfo, quint32 channelId,
+ : AbstractSshChannel (channelId, sendFacility),
+ m_x11Socket(new X11Socket(this)),
+ m_displayInfo(displayInfo)
+ setChannelState(SessionRequested); // Invariant for parent class.
+void SshX11Channel::handleChannelSuccess()
+ qCWarning(sshLog) << "unexpected channel success message for X11 channel";
+void SshX11Channel::handleChannelFailure()
+ qCWarning(sshLog) << "unexpected channel failure message for X11 channel";
+void SshX11Channel::handleOpenSuccessInternal()
+ connect(m_x11Socket, &X11Socket::connected, this,
+ [this] {
+ qCDebug(sshLog) << "x11 socket connected for channel" << localChannelId();
+ if (!m_queuedRemoteData.isEmpty())
+ handleRemoteData(QByteArray());
+ );
+ connect(m_x11Socket, &X11Socket::error, this,
+ [this](const QString &msg) {
+ emit error(tr("X11 socket error: %1").arg(msg));
+ connect(m_x11Socket, &X11Socket::dataAvailable, this,
+ [this](const QByteArray &data) {
+ qCDebug(sshLog) << "sending " << data.size() << "bytes from x11 socket to remote side "
+ "in channel" << localChannelId();
+ sendData(data);
+ m_x11Socket->establishConnection(m_displayInfo);
+void SshX11Channel::handleOpenFailureInternal(const QString &reason)
+ qCWarning(sshLog) << "unexpected channel open failure message for X11 channel:" << reason;
+void SshX11Channel::handleChannelDataInternal(const QByteArray &data)
+ handleRemoteData(data);
+void SshX11Channel::handleChannelExtendedDataInternal(quint32 type, const QByteArray &data)
+ qCWarning(sshLog) << "unexpected extended data for X11 channel" << type << data;
+void SshX11Channel::handleExitStatus(const SshChannelExitStatus &exitStatus)
+ qCWarning(sshLog) << "unexpected exit status message on X11 channel" << exitStatus.exitStatus;
+void SshX11Channel::handleExitSignal(const SshChannelExitSignal &signal)
+ qCWarning(sshLog) << "unexpected exit signal message on X11 channel" << signal.error;
+void SshX11Channel::closeHook()
+ m_x11Socket->disconnect();
+ m_x11Socket->closeConnection();
+void SshX11Channel::handleRemoteData(const QByteArray &data)
+ if (m_x11Socket->hasError())
+ qCDebug(sshLog) << "received" << data.size() << "bytes from remote side in x11 channel"
+ << localChannelId();
+ if (!m_x11Socket->isConnected()) {
+ qCDebug(sshLog) << "x11 socket not yet connected, queueing data";
+ m_queuedRemoteData += data;
+ if (m_haveReplacedRandomCookie) {
+ qCDebug(sshLog) << "forwarding data to x11 socket";
+ m_x11Socket->write(data);
+ const int randomCookieOffset = m_queuedRemoteData.indexOf(m_displayInfo.randomCookie);
+ if (randomCookieOffset == -1) {
+ qCDebug(sshLog) << "random cookie has not appeared in remote data yet, queueing data";
+ m_queuedRemoteData.replace(randomCookieOffset, m_displayInfo.cookie.size(),
+ m_displayInfo.cookie);
+ qCDebug(sshLog) << "found and replaced random cookie, forwarding data to x11 socket";
+ m_x11Socket->write(m_queuedRemoteData);
+ m_queuedRemoteData.clear();
+ m_haveReplacedRandomCookie = true;
+#include <sshx11channel.moc>
+class X11Socket;
+class SshX11Channel : public AbstractSshChannel
+ SshX11Channel(const X11DisplayInfo &displayInfo, quint32 channelId,
+ void handleRemoteData(const QByteArray &data);
+ X11Socket * const m_x11Socket;
+ const X11DisplayInfo m_displayInfo;
+ QByteArray m_queuedRemoteData;
+ bool m_haveReplacedRandomCookie = false;
@@ -0,0 +1,49 @@
+class QSSH_EXPORT X11DisplayInfo
+ QString displayName;
+ QString hostName;
+ QByteArray protocol;
+ QByteArray cookie;
+ QByteArray randomCookie;
+ int display = 0;
+ int screen = 0;
@@ -0,0 +1,161 @@
+#include <QByteArrayList>
+#include <QTemporaryFile>
+static QByteArray xauthProtocol() { return "MIT-MAGIC-COOKIE-1"; }
+SshX11InfoRetriever::SshX11InfoRetriever(const QString &displayName, QObject *parent)
+ : QObject(parent),
+ m_displayName(displayName),
+ m_xauthProc(new QProcess(this)),
+ m_xauthFile(new QTemporaryFile(this))
+ connect(m_xauthProc, &QProcess::errorOccurred, this,
+ if (m_xauthProc->error() == QProcess::FailedToStart) {
+ emitFailure(tr("Could not start xauth: %1").arg(m_xauthProc->errorString()));
+ connect(m_xauthProc, static_cast<void (QProcess::*)(int,QProcess::ExitStatus)>(&QProcess::finished), this,
+ if (m_xauthProc->exitStatus() != QProcess::NormalExit) {
+ emitFailure(tr("xauth crashed: %1").arg(m_xauthProc->errorString()));
+ if (m_xauthProc->exitCode() != 0) {
+ emitFailure(tr("xauth failed with exit code %1.").arg(m_xauthProc->exitCode()));
+ case State::RunningGenerate:
+ m_state = State::RunningList;
+ m_xauthProc->start(QStringLiteral("xauth"), QStringList{
+ QStringLiteral("-f"),
+ m_xauthFile->fileName(),
+ QStringLiteral("list"),
+ m_displayName});
+ case State::RunningList: {
+ const QByteArrayList outputLines = m_xauthProc->readAllStandardOutput().split('\n');
+ if (outputLines.empty()) {
+ emitFailure(tr("Unexpected xauth output."));
+ const QByteArrayList data = outputLines.first().simplified().split(' ');
+ if (data.size() < 3 || data.at(1) != xauthProtocol() || data.at(2).isEmpty()) {
+ X11DisplayInfo displayInfo;
+ displayInfo.displayName = m_displayName;
+ const int colonIndex = m_displayName.indexOf(QLatin1Char(':'));
+ if (colonIndex == -1) {
+ emitFailure(tr("Invalid display name \"%1\"").arg(m_displayName));
+ displayInfo.hostName = m_displayName.mid(0, colonIndex);
+ const int dotIndex = m_displayName.indexOf(QLatin1Char('.'), colonIndex + 1);
+ const QString display = m_displayName.mid(colonIndex + 1,
+ dotIndex == -1 ? -1
+ : dotIndex - colonIndex - 1);
+ if (display.isEmpty()) {
+ displayInfo.display = display.toInt(&ok);
+ if (!ok) {
+ if (dotIndex != -1) {
+ displayInfo.screen = m_displayName.mid(dotIndex + 1).toInt(&ok);
+ displayInfo.protocol = data.at(1);
+ displayInfo.cookie = QByteArray::fromHex(data.at(2));
+ displayInfo.randomCookie.resize(displayInfo.cookie.size());
+ Botan::AutoSeeded_RNG rng;
+ rng.randomize(reinterpret_cast<Botan::uint8_t *>(displayInfo.randomCookie.data()),
+ displayInfo.randomCookie.size());
+ emitFailure(tr("Failed to generate random cookie: %1")
+ .arg(QLatin1String(ex.what())));
+ emit success(displayInfo);
+ deleteLater();
+ emitFailure(tr("Internal error"));
+void SshX11InfoRetriever::start()
+ if (!m_xauthFile->open()) {
+ emitFailure(tr("Could not create temporary file: %1").arg(m_xauthFile->errorString()));
+ m_state = State::RunningGenerate;
+ QStringLiteral("generate"),
+ m_displayName, QString::fromLatin1(xauthProtocol())});
+void SshX11InfoRetriever::emitFailure(const QString &reason)
+ QTimer::singleShot(0, this, [this, reason] {
+ emit failure(tr("Could not retrieve X11 authentication cookie: %1").arg(reason));
@@ -0,0 +1,65 @@
+class QProcess;
+class QTemporaryFile;
+class QSSH_EXPORT SshX11InfoRetriever : public QObject
+ SshX11InfoRetriever(const QString &displayName, QObject *parent = nullptr);
+ void failure(const QString &message);
+ void success(const X11DisplayInfo &displayInfo);
+ void emitFailure(const QString &reason);
+ const QString m_displayName;
+ QProcess * const m_xauthProc;
+ QTemporaryFile * const m_xauthFile;
+ enum class State { Inactive, RunningGenerate, RunningList } m_state = State::Inactive;
@@ -37,6 +37,7 @@ void MainWindow::on_pushButton_Start_clicked()
char strout[100];
memcpy(strout,&nCode,sizeof(int));
mpSocket->writeDatagram(strout,sizeof(int),QHostAddress::Broadcast,60009);
+ mpSocket->writeDatagram(strout,sizeof(int),QHostAddress("10.16.0.107"),60009);
}
void MainWindow::on_pushButton_Stop_clicked()
@@ -46,6 +47,7 @@ void MainWindow::on_pushButton_Stop_clicked()
void MainWindow::on_pushButton_Query_clicked()
@@ -55,6 +57,7 @@ void MainWindow::on_pushButton_Query_clicked()
void MainWindow::onRecvData()