diff --git a/README b/README deleted file mode 100644 index e1f13e0..0000000 --- a/README +++ /dev/null @@ -1,32 +0,0 @@ - -About QSsh -========== - -QSsh provides SSH and SFTP support for Qt applications. The aim of this project -is to provide a easy way to use these protocols in any Qt application. - -This project is based on Qt Creator's libQtcSsh.so. All the credits to -Qt Creator's team! - - -Compiling QSsh -============== - -Prerequisites: - * Qt 4.7.4 (May work with older versions) - * On Windows: - - MinGW 4.4 or later, Visual Studio 2008 or later - * On Mac: XCode 2.5 or later, compiling on 10.4 requires setting the - environment variable QTC_TIGER_COMPAT before running qmake - -mkdir $BUILD_DIRECTORY -cd $BUILD_DIRECTORY -qmake $SOURCE_DIRECTORY/qssh.pro -make (or mingw32-make or nmake depending on your platform) - - -Examples -======== - - - Directory examples/SecureUploader/ contains an example on how to upload - a file using SFTP diff --git a/README.md b/README.md index 8461bff..57a0e3b 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,34 @@ About QSsh QSsh provides SSH and SFTP support for Qt applications. The aim of this project is to provide a easy way to use these protocols in any Qt application. -This project is based on Qt Creator's libQtcSsh.so. All the credits to -Qt Creator's team! (http://qt.gitorious.org/qt-creator) +This project is based on Qt Creator's libQtcSsh.so. All credits to +Qt Creator's team! +Unfortunately Qt Creator has decided to start using openssh instead, so this is +now the most up to date maintained version. -### Compiling QSsh + +Compiling QSsh +-------------- Prerequisites: - * Qt 4.7.4 (May work with older versions) - * On Windows: MinGW 4.4 or later, Visual Studio 2008 or later - * On Mac: XCode 2.5 or later, compiling on 10.4 requires setting the - environment variable QTC_TIGER_COMPAT before running qmake + * [Qt](https://www.qt.io/) + * [Botan](https://botan.randombit.net/) Steps: ```bash -mkdir $BUILD_DIRECTORY -cd $BUILD_DIRECTORY -qmake $SOURCE_DIRECTORY/qssh.pro +git clone https://github.com/sandsmark/QSsh.git +cd QSsh +mkdir build +cd build +qmake ../qssh.pro make (or mingw32-make or nmake depending on your platform) ``` -### Examples +Examples +-------- -Directory examples/SecureUploader/ contains an example on how to upload -a file using SFTP + * [ssh shell](tests/manual/ssh/shell/), similar to a normal command line `ssh` client. + * [Graphical SFTP browser](tests/manual/ssh/sftpfsmodel/), how to use the SFTP file system model with a QTreeView. + * [Auto tests](tests/auto/ssh/), how to do X11 forwarding, remote command execution, file upload and download, and basically everything else that is supported. + * [Secure Uploader](examples/SecureUploader/), how to upload a file. diff --git a/examples/SecureUploader/securefileuploader.cpp b/examples/SecureUploader/securefileuploader.cpp index 5b5d83c..8ddf8fe 100644 --- a/examples/SecureUploader/securefileuploader.cpp +++ b/examples/SecureUploader/securefileuploader.cpp @@ -36,12 +36,12 @@ void SecureFileUploader::upload(const QString &localFile, const QString &dest, c m_remoteFilename = dest + "/" + info.fileName(); QSsh::SshConnectionParameters params; - params.host = host; - params.userName = username; - params.password = passwd; - params.authenticationType = QSsh::SshConnectionParameters::AuthenticationByPassword; + params.setHost(host); + params.setUserName(username); + params.setPassword(passwd); + params.authenticationType = QSsh::SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods; params.timeout = 30; - params.port = 22; + params.setPort(22); m_connection = new QSsh::SshConnection(params, this); // TODO free this pointer! diff --git a/qssh.pri b/qssh.pri index 8cf151c..3011406 100644 --- a/qssh.pri +++ b/qssh.pri @@ -2,40 +2,13 @@ QSSH_PRI_INCLUDED = 1 isEmpty(QSSH_PREFIX) { - unix:!macx: QSSH_PREFIX = /usr/local + unix:!macx: QSSH_PREFIX = $$[QT_INSTALL_PREFIX] } -isEqual(QT_MAJOR_VERSION, 5) { - defineReplace(cleanPath) { return($$clean_path($$1)) } -defineReplace(targetPath) { - return($$shell_path($$1)) -} - -} else { # qt5 - -defineReplace(cleanPath) { - win32:1 ~= s|\\\\|/|g - contains(1, ^/.*):pfx = / - else:pfx = - segs = $$split(1, /) - out = - for(seg, segs) { - equals(seg, ..):out = $$member(out, 0, -2) - else:!equals(seg, .):out += $$seg - } - return($$join(out, /, $$pfx)) -} - -defineReplace(targetPath) { - return($$replace(1, /, $$QMAKE_DIR_SEP)) -} - -} # qt5 - defineReplace(qtLibraryName) { unset(LIBRARY_NAME) LIBRARY_NAME = $$1 @@ -49,69 +22,10 @@ defineReplace(qtLibraryName) { return($$RET) } -defineTest(minQtVersion) { - maj = $$1 - min = $$2 - patch = $$3 - isEqual(QT_MAJOR_VERSION, $$maj) { - isEqual(QT_MINOR_VERSION, $$min) { - isEqual(QT_PATCH_VERSION, $$patch) { - return(true) - } - greaterThan(QT_PATCH_VERSION, $$patch) { - return(true) - } - } - greaterThan(QT_MINOR_VERSION, $$min) { - return(true) - } - } - greaterThan(QT_MAJOR_VERSION, $$maj) { - return(true) - } - return(false) -} - -isEqual(QT_MAJOR_VERSION, 5) { - -# For use in custom compilers which just copy files -defineReplace(stripSrcDir) { - return($$relative_path($$absolute_path($$1, $$OUT_PWD), $$_PRO_FILE_PWD_)) -} - -} else { # qt5 - -# For use in custom compilers which just copy files -win32:i_flag = i -defineReplace(stripSrcDir) { - win32 { - !contains(1, ^.:.*):1 = $$OUT_PWD/$$1 - } else { - !contains(1, ^/.*):1 = $$OUT_PWD/$$1 - } - out = $$cleanPath($$1) - out ~= s|^$$re_escape($$_PRO_FILE_PWD_/)||$$i_flag - return($$out) -} - -} # qt5 - -isEmpty(TEST):CONFIG(debug, debug|release) { - !debug_and_release|build_pass { - TEST = 1 - } -} - isEmpty(IDE_LIBRARY_BASENAME) { IDE_LIBRARY_BASENAME = lib } -equals(TEST, 1) { - QT +=testlib - DEFINES += WITH_TESTS -} - -IDE_SOURCE_TREE = $$PWD isEmpty(IDE_BUILD_TREE) { sub_dir = $$_PRO_FILE_PWD_ sub_dir ~= s,^$$re_escape($$PWD),, @@ -125,7 +39,7 @@ macx { IDE_LIBEXEC_PATH = $$IDE_APP_PATH/$${IDE_APP_TARGET}.app/Contents/Resources IDE_BIN_PATH = $$IDE_APP_PATH/$${IDE_APP_TARGET}.app/Contents/MacOS copydata = 1 - isEmpty(TIGER_COMPAT_MODE):TIGER_COMPAT_MODE=$$(QTC_TIGER_COMPAT) + isEmpty(TIGER_COMPAT_MODE):TIGER_COMPAT_MODE=$$(QSSH_TIGER_COMPAT) isEmpty(TIGER_COMPAT_MODE) { QMAKE_CXXFLAGS *= -mmacosx-version-min=10.5 QMAKE_LFLAGS *= -mmacosx-version-min=10.5 @@ -136,17 +50,21 @@ macx { IDE_LIBRARY_PATH = $$IDE_BUILD_TREE/$$IDE_LIBRARY_BASENAME IDE_LIBEXEC_PATH = $$IDE_APP_PATH # FIXME IDE_BIN_PATH = $$IDE_APP_PATH - !isEqual(IDE_SOURCE_TREE, $$IDE_BUILD_TREE):copydata = 1 } -INCLUDEPATH += \ - $$IDE_BUILD_TREE/src \ # for - $$IDE_SOURCE_TREE/src/libs +QT += widgets + +CONFIG += warn_on + +# Find botan2 +CONFIG += link_pkgconfig +PKGCONFIG += botan-2 CONFIG += depend_includepath LIBS += -L$$IDE_LIBRARY_PATH -LIBS += -lbotan-2 +LIBS += -l$$qtLibraryName(botan-2) +INCLUDEPATH += $${PWD}/src/libs/ !isEmpty(vcproj) { DEFINES += IDE_LIBRARY_BASENAME=\"$$IDE_LIBRARY_BASENAME\" @@ -158,24 +76,7 @@ LIBS += -lbotan-2 DEFINES += QT_NO_CAST_TO_ASCII !macx:DEFINES += QT_USE_FAST_OPERATOR_PLUS QT_USE_FAST_CONCATENATION -unix { - CONFIG(debug, debug|release):OBJECTS_DIR = $${OUT_PWD}/.obj/debug-shared - CONFIG(release, debug|release):OBJECTS_DIR = $${OUT_PWD}/.obj/release-shared - - CONFIG(debug, debug|release):MOC_DIR = $${OUT_PWD}/.moc/debug-shared - CONFIG(release, debug|release):MOC_DIR = $${OUT_PWD}/.moc/release-shared - - RCC_DIR = $${OUT_PWD}/.rcc - UI_DIR = $${OUT_PWD}/.uic -} - win32-msvc* { #Don't warn about sprintf, fopen etc being 'unsafe' DEFINES += _CRT_SECURE_NO_WARNINGS } - -qt:greaterThan(QT_MAJOR_VERSION, 4) { - contains(QT, core): QT += concurrent - contains(QT, gui): QT += widgets - DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x040900 -} diff --git a/qssh.pro b/qssh.pro index ce99496..c5e8b2b 100644 --- a/qssh.pro +++ b/qssh.pro @@ -3,5 +3,5 @@ CONFIG += ordered SUBDIRS = \ src \ - examples \ - tests + examples \ + tests diff --git a/src/libs/ssh/images/dir.png b/src/libs/ssh/images/dir.png new file mode 100644 index 0000000..57cec6b Binary files /dev/null and b/src/libs/ssh/images/dir.png differ diff --git a/src/libs/ssh/images/help.png b/src/libs/ssh/images/help.png new file mode 100644 index 0000000..a12ef28 Binary files /dev/null and b/src/libs/ssh/images/help.png differ diff --git a/src/libs/ssh/images/unknownfile.png b/src/libs/ssh/images/unknownfile.png new file mode 100644 index 0000000..88f7759 Binary files /dev/null and b/src/libs/ssh/images/unknownfile.png differ diff --git a/src/libs/ssh/opensshkeyfilereader.cpp b/src/libs/ssh/opensshkeyfilereader.cpp new file mode 100644 index 0000000..1a5cb2e --- /dev/null +++ b/src/libs/ssh/opensshkeyfilereader.cpp @@ -0,0 +1,201 @@ +/**************************************************************************** +** +** 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 +#include +#include +#include + +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 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(p, q, e, d, n); + } else if (m_keyType == SshCapabilities::PubKeyDss) { + QSSH_ASSERT_AND_RETURN_VALUE(m_parameters.size() == 5, nullptr); + 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(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(m_rng, group, value); + } + QSSH_ASSERT_AND_RETURN_VALUE(false, nullptr); +} + +QList 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(); + QSSH_ASSERT_AND_RETURN_VALUE(false, QList()); +} + +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(QLatin1String(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_cipherName == "none") + return; + if (m_kdf != "bcrypt") { + throwException(SSH_TR("Unexpected key derivation function '%1'.") + .arg(QLatin1String(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() +{ + quint32 offset = 0; + 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; + if (m_keyType == SshCapabilities::PubKeyRsa) { + 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{e, n, p, q, d}; + } else if (m_keyType == SshCapabilities::PubKeyDss) { + const BigInt p = SshPacketParser::asBigInt(m_privateKeyList, &offset); + const BigInt q = SshPacketParser::asBigInt(m_privateKeyList, &offset); + 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{p, q, g, y, x}; + } else if (m_keyType.startsWith(SshCapabilities::PubKeyEcdsaPrefix)) { + 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 diff --git a/src/libs/ssh/opensshkeyfilereader_p.h b/src/libs/ssh/opensshkeyfilereader_p.h new file mode 100644 index 0000000..0ea0eb5 --- /dev/null +++ b/src/libs/ssh/opensshkeyfilereader_p.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include +#include + +#include + +#include + +namespace Botan { +class Private_Key; +class RandomNumberGenerator; +} + +namespace QSsh { +namespace Internal { + +class OpenSshKeyFileReader +{ +public: + OpenSshKeyFileReader(Botan::RandomNumberGenerator &rng) : m_rng(rng) {} + + bool parseKey(const QByteArray &privKeyFileContents); + QByteArray keyType() const { return m_keyType; } + std::unique_ptr privateKey() const; + QList allParameters() const { return m_parameters; } + QList 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 m_parameters; + QByteArray m_cipherName; + QByteArray m_kdf; + QByteArray m_salt; + quint32 m_rounds; + QByteArray m_privateKeyList; +}; + +} // namespace Internal +} // namespace QSsh + diff --git a/src/libs/ssh/sftpchannel.cpp b/src/libs/ssh/sftpchannel.cpp index 1574775..7a0e227 100644 --- a/src/libs/ssh/sftpchannel.cpp +++ b/src/libs/ssh/sftpchannel.cpp @@ -31,40 +31,18 @@ #include "sftpchannel.h" #include "sftpchannel_p.h" +#include "sftpdefs.h" #include "sshexception_p.h" #include "sshincomingpacket_p.h" +#include "sshlogging_p.h" #include "sshsendfacility_p.h" #include #include -/*! - \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. -*/ - namespace QSsh { namespace Internal { + namespace { const quint32 ProtocolVersion = 3; @@ -94,6 +72,32 @@ namespace { 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 } // namespace Internal @@ -105,18 +109,20 @@ SftpChannel::SftpChannel(quint32 channelId, Internal::SshSendFacility &sendFacility) : d(new Internal::SftpChannelPrivate(channelId, sendFacility, this)) { - connect(d, SIGNAL(initialized()), this, SIGNAL(initialized()), - Qt::QueuedConnection); - connect(d, SIGNAL(initializationFailed(QString)), this, - SIGNAL(initializationFailed(QString)), Qt::QueuedConnection); - connect(d, SIGNAL(dataAvailable(QSsh::SftpJobId,QString)), this, - SIGNAL(dataAvailable(QSsh::SftpJobId,QString)), Qt::QueuedConnection); - connect(d, SIGNAL(fileInfoAvailable(QSsh::SftpJobId,QList)), this, - SIGNAL(fileInfoAvailable(QSsh::SftpJobId,QList)), - Qt::QueuedConnection); - connect(d, SIGNAL(finished(QSsh::SftpJobId,QString)), this, - SIGNAL(finished(QSsh::SftpJobId,QString)), Qt::QueuedConnection); - connect(d, SIGNAL(closed()), this, SIGNAL(closed()), Qt::QueuedConnection); + 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 @@ -199,13 +205,13 @@ SftpJobId SftpChannel::createFile(const QString &path, SftpOverwriteMode mode) new Internal::SftpCreateFile(++d->m_nextJobId, path, mode))); } -SftpJobId SftpChannel::uploadFile(QSharedPointer localFile, +SftpJobId SftpChannel::uploadFile(QSharedPointer device, const QString &remoteFilePath, SftpOverwriteMode mode) { - if (!localFile->isOpen() && !localFile->open(QIODevice::ReadOnly)) + if (!device->isOpen() && !device->open(QIODevice::ReadOnly)) return SftpInvalidJob; return d->createJob(Internal::SftpUploadFile::Ptr( - new Internal::SftpUploadFile(++d->m_nextJobId, remoteFilePath, localFile, mode))); + new Internal::SftpUploadFile(++d->m_nextJobId, remoteFilePath, device, mode))); } SftpJobId SftpChannel::uploadFile(const QString &localFilePath, @@ -226,10 +232,10 @@ SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath, new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, localFile, mode))); } -SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath, QSharedPointer localFile) +SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath, QSharedPointer device) { return d->createJob(Internal::SftpDownload::Ptr( - new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, localFile, SftpOverwriteExisting))); + new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, device, SftpOverwriteExisting))); } SftpJobId SftpChannel::uploadDir(const QString &localDirPath, @@ -300,9 +306,7 @@ void SftpChannelPrivate::handleChannelSuccess() { if (channelState() == CloseRequested) return; -#ifdef CREATOR_SSH_DEBUG - qDebug("sftp subsystem initialized"); -#endif + qCDebug(sshLog, "sftp subsystem initialized"); sendData(m_outgoingPacket.generateInit(ProtocolVersion).rawData()); m_sftpState = InitSent; } @@ -316,7 +320,7 @@ void SftpChannelPrivate::handleChannelFailure() throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_FAILURE packet."); } - emit initializationFailed(tr("Server could not start SFTP subsystem.")); + emit channelError(tr("Server could not start SFTP subsystem.")); closeChannel(); } @@ -337,31 +341,34 @@ void SftpChannelPrivate::handleChannelDataInternal(const QByteArray &data) void SftpChannelPrivate::handleChannelExtendedDataInternal(quint32 type, const QByteArray &data) { - qWarning("Unexpected extended data '%s' of type %d on SFTP channel.", - data.data(), type); + qCWarning(sshLog, "Unexpected extended data '%s' of type %d on SFTP channel.", + data.data(), type); } void SftpChannelPrivate::handleExitStatus(const SshChannelExitStatus &exitStatus) { - const char * const message = "Remote SFTP service exited with exit code %d"; -#ifdef CREATOR_SSH_DEBUG - qDebug(message, exitStatus.exitStatus); -#else - if (exitStatus.exitStatus != 0) - qWarning(message, exitStatus.exitStatus); -#endif + qCDebug(sshLog, "Remote SFTP service exited with exit code %d", exitStatus.exitStatus); + + if (channelState() == CloseRequested || channelState() == Closed) + return; + + 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. + closeChannel(); } void SftpChannelPrivate::handleExitSignal(const SshChannelExitSignal &signal) { - qWarning("Remote SFTP service killed; signal was %s", signal.signal.data()); + emit channelError(tr("The SFTP server crashed: %1.").arg(signal.error)); + closeChannel(); // See above. } void SftpChannelPrivate::handleCurrentPacket() { -#ifdef CREATOR_SSH_DEBUG - qDebug("Handling SFTP packet of type %d", m_incomingPacket.type()); -#endif + qCDebug(sshLog, "Handling SFTP packet of type %d", m_incomingPacket.type()); switch (m_incomingPacket.type()) { case SSH_FXP_VERSION: handleServerVersion(); @@ -396,12 +403,10 @@ void SftpChannelPrivate::handleServerVersion() "Unexpected SSH_FXP_VERSION packet."); } -#ifdef CREATOR_SSH_DEBUG - qDebug("sftp init received"); -#endif + qCDebug(sshLog, "sftp init received"); const quint32 serverVersion = m_incomingPacket.extractServerVersion(); if (serverVersion != ProtocolVersion) { - emit initializationFailed(tr("Protocol version mismatch: Expected %1, got %2") + emit channelError(tr("Protocol version mismatch: Expected %1, got %2") .arg(serverVersion).arg(ProtocolVersion)); closeChannel(); } else { @@ -487,9 +492,7 @@ void SftpChannelPrivate::handlePutHandle(const JobMap::Iterator &it) void SftpChannelPrivate::handleStatus() { const SftpStatusResponse &response = m_incomingPacket.asStatusResponse(); -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: status = %d", Q_FUNC_INFO, response.status); -#endif + qCDebug(sshLog, "%s: status = %d", Q_FUNC_INFO, response.status); JobMap::Iterator it = lookupJob(response.requestId); switch (it.value()->type()) { case AbstractSftpOperation::ListDir: @@ -520,7 +523,7 @@ void SftpChannelPrivate::handleStatusGeneric(const JobMap::Iterator &it, { AbstractSftpOperation::Ptr op = it.value(); const QString error = errorMessage(response, tr("Unknown error.")); - emit finished(op->jobId, error); + emit finished(op->jobId, sftpStatusToError(response.status), error); m_jobs.erase(it); } @@ -528,29 +531,31 @@ void SftpChannelPrivate::handleMkdirStatus(const JobMap::Iterator &it, const SftpStatusResponse &response) { SftpMakeDir::Ptr op = it.value().staticCast(); - if (op->parentJob == SftpUploadDir::Ptr()) { + QSharedPointer parentJob = op->parentJob; + if (parentJob == SftpUploadDir::Ptr()) { handleStatusGeneric(it, response); return; } - if (op->parentJob->hasError) { + if (parentJob->hasError) { m_jobs.erase(it); return; } typedef QMap::Iterator DirIt; - DirIt dirIt = op->parentJob->mkdirsInProgress.find(op); - Q_ASSERT(dirIt != op->parentJob->mkdirsInProgress.end()); + 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(op->parentJob->jobId, - tr("Created remote directory '%1'.").arg(remoteDir)); + emit dataAvailable(parentJob->jobId, + tr("Created remote directory \"%1\".").arg(remoteDir)); } else if (response.status == SSH_FX_FAILURE) { - emit dataAvailable(op->parentJob->jobId, - tr("Remote directory '%1' already exists.").arg(remoteDir)); + emit dataAvailable(parentJob->jobId, + tr("Remote directory \"%1\" already exists.").arg(remoteDir)); } else { - op->parentJob->setError(); - emit finished(op->parentJob->jobId, - tr("Error creating directory '%1': %2") + parentJob->setError(); + emit finished(parentJob->jobId, + sftpStatusToError(response.status), + tr("Error creating directory \"%1\": %2") .arg(remoteDir, response.errorString)); m_jobs.erase(it); return; @@ -562,8 +567,8 @@ void SftpChannelPrivate::handleMkdirStatus(const JobMap::Iterator &it, foreach (const QFileInfo &dirInfo, dirInfos) { const QString remoteSubDir = remoteDir + QLatin1Char('/') + dirInfo.fileName(); const SftpMakeDir::Ptr mkdirOp( - new SftpMakeDir(++m_nextJobId, remoteSubDir, op->parentJob)); - op->parentJob->mkdirsInProgress.insert(mkdirOp, + new SftpMakeDir(++m_nextJobId, remoteSubDir, parentJob)); + parentJob->mkdirsInProgress.insert(mkdirOp, SftpUploadDir::Dir(dirInfo.absoluteFilePath(), remoteSubDir)); createJob(mkdirOp); } @@ -572,9 +577,10 @@ void SftpChannelPrivate::handleMkdirStatus(const JobMap::Iterator &it, foreach (const QFileInfo &fileInfo, fileInfos) { QSharedPointer localFile(new QFile(fileInfo.absoluteFilePath())); if (!localFile->open(QIODevice::ReadOnly)) { - op->parentJob->setError(); - emit finished(op->parentJob->jobId, - tr("Could not open local file '%1': %2") + parentJob->setError(); + emit finished(parentJob->jobId, + sftpStatusToError(response.status), + tr("Could not open local file \"%1\": %2") .arg(fileInfo.absoluteFilePath(), localFile->errorString())); m_jobs.erase(it); return; @@ -582,15 +588,15 @@ void SftpChannelPrivate::handleMkdirStatus(const JobMap::Iterator &it, const QString remoteFilePath = remoteDir + QLatin1Char('/') + fileInfo.fileName(); SftpUploadFile::Ptr uploadFileOp(new SftpUploadFile(++m_nextJobId, - remoteFilePath, localFile, SftpOverwriteExisting, op->parentJob)); + remoteFilePath, localFile, SftpOverwriteExisting, parentJob)); createJob(uploadFileOp); - op->parentJob->uploadsInProgress.append(uploadFileOp); + parentJob->uploadsInProgress.append(uploadFileOp); } - op->parentJob->mkdirsInProgress.erase(dirIt); - if (op->parentJob->mkdirsInProgress.isEmpty() - && op->parentJob->uploadsInProgress.isEmpty()) - emit finished(op->parentJob->jobId); + parentJob->mkdirsInProgress.erase(dirIt); + if (parentJob->mkdirsInProgress.isEmpty() + && parentJob->uploadsInProgress.isEmpty()) + emit finished(parentJob->jobId); m_jobs.erase(it); } @@ -606,13 +612,15 @@ void SftpChannelPrivate::handleLsStatus(const JobMap::Iterator &it, switch (op->state) { case SftpListDir::OpenRequested: - reportRequestError(op, errorMessage(response.errorString, + reportRequestError(op, sftpStatusToError(response.status), errorMessage(response.errorString, tr("Remote directory could not be opened for reading."))); m_jobs.erase(it); break; case SftpListDir::Open: if (response.status != SSH_FX_EOF) - reportRequestError(op, errorMessage(response.errorString, + reportRequestError(op, + sftpStatusToError(response.status), + errorMessage(response.errorString, tr("Failed to list remote directory contents."))); op->state = SftpListDir::CloseRequested; sendData(m_outgoingPacket.generateCloseHandle(op->remoteHandle, @@ -633,7 +641,7 @@ void SftpChannelPrivate::handleLsStatus(const JobMap::Iterator &it, op->parentJob->setError(); } if (op->parentJob->hasError) { - emit finished(op->parentJob->jobId, error); + emit finished(op->parentJob->jobId, sftpStatusToError(response.status), error); } else { op->parentJob->lsdirsInProgress.remove(op); if (op->parentJob->lsdirsInProgress.isEmpty() && @@ -642,7 +650,7 @@ void SftpChannelPrivate::handleLsStatus(const JobMap::Iterator &it, } } } else { - emit finished(op->jobId, error); + emit finished(op->jobId, sftpStatusToError(response.status), error); } } m_jobs.erase(it); @@ -665,19 +673,19 @@ void SftpChannelPrivate::handleGetStatus(const JobMap::Iterator &it, switch (op->state) { case SftpDownload::OpenRequested: - reportRequestError(op, errorMessage(response.errorString, + reportRequestError(op, sftpStatusToError(response.status), errorMessage(response.errorString, tr("Failed to open remote file for reading."))); m_jobs.erase(it); break; case SftpDownload::Open: if (op->statRequested) { - reportRequestError(op, errorMessage(response.errorString, + reportRequestError(op, sftpStatusToError(response.status), errorMessage(response.errorString, tr("Failed to retrieve information on the remote file ('stat' failed)."))); sendTransferCloseHandle(op, response.requestId); } else { if ((response.status != SSH_FX_EOF || response.requestId != op->eofId) && !op->hasError) - reportRequestError(op, errorMessage(response.errorString, + reportRequestError(op, sftpStatusToError(response.status), errorMessage(response.errorString, tr("Failed to read remote file."))); finishTransferRequest(it); } @@ -697,7 +705,7 @@ void SftpChannelPrivate::handleGetStatus(const JobMap::Iterator &it, } else { const QString error = errorMessage(response.errorString, tr("Failed to close remote file.")); - reportRequestError(op, error); + reportRequestError(op, sftpStatusToError(response.status), error); } } removeTransferRequest(it); @@ -726,6 +734,7 @@ void SftpChannelPrivate::handlePutStatus(const JobMap::Iterator &it, if (emitError) { emit finished(job->jobId, + sftpStatusToError(response.status), errorMessage(response.errorString, tr("Failed to open remote file for writing."))); } @@ -744,7 +753,7 @@ void SftpChannelPrivate::handlePutStatus(const JobMap::Iterator &it, } else { if (job->parentJob) job->parentJob->setError(); - reportRequestError(job, errorMessage(response.errorString, + reportRequestError(job, sftpStatusToError(response.status), errorMessage(response.errorString, tr("Failed to write remote file."))); finishTransferRequest(it); } @@ -770,9 +779,9 @@ void SftpChannelPrivate::handlePutStatus(const JobMap::Iterator &it, tr("Failed to close remote file.")); if (job->parentJob) { job->parentJob->setError(); - emit finished(job->parentJob->jobId, error); + emit finished(job->parentJob->jobId, sftpStatusToError(response.status), error); } else { - emit finished(job->jobId, error); + emit finished(job->jobId, sftpStatusToError(response.status), error); } } m_jobs.erase(it); @@ -840,29 +849,31 @@ void SftpChannelPrivate::handleReadData() QFile *fileDevice = qobject_cast(op->localFile.data()); if (fileDevice){ if (!Internal::openFile(fileDevice, op->mode)) { - reportRequestError(op, tr("Cannot open file ") + fileDevice->fileName()); + reportRequestError(op, SftpError::GenericFailure, tr("Cannot open file ") + fileDevice->fileName()); finishTransferRequest(it); return; } } else { - reportRequestError(op, tr("File to upload is not open")); + reportRequestError(op, SftpError::GenericFailure, tr("File to upload is not open")); finishTransferRequest(it); return; } } if (!op->localFile->seek(op->offsets[response.requestId])) { - reportRequestError(op, op->localFile->errorString()); + reportRequestError(op, SftpError::GenericFailure, op->localFile->errorString()); finishTransferRequest(it); return; } if (op->localFile->write(response.data) != response.data.size()) { - reportRequestError(op, op->localFile->errorString()); + reportRequestError(op, SftpError::GenericFailure, op->localFile->errorString()); finishTransferRequest(it); return; } + emit transferProgress(op->jobId, op->localFile->pos(), op->fileSize); + if (op->offset >= op->fileSize && op->fileSize != 0) finishTransferRequest(it); else @@ -904,6 +915,7 @@ void SftpChannelPrivate::handleAttrs() op->eofId = op->jobId; } op->statRequested = false; + emit transferProgress(op->jobId, op->offset, op->fileSize); spawnReadRequests(op); } else { SftpUploadFile::Ptr op = transfer.staticCast(); @@ -915,11 +927,12 @@ void SftpChannelPrivate::handleAttrs() if (response.attrs.sizePresent) { op->offset = response.attrs.size; + emit transferProgress(op->jobId, op->offset, op->fileSize); spawnWriteRequests(it); } else { if (op->parentJob) op->parentJob->setError(); - reportRequestError(op, tr("Cannot append to remote file: " + reportRequestError(op, SftpError::UnsupportedOperation, tr("Cannot append to remote file: " "Server does not support the file size attribute.")); sendTransferCloseHandle(op, op->jobId); } @@ -953,7 +966,7 @@ void SftpChannelPrivate::handleDownloadDir(SftpListDir::Ptr op, } if (!QDir().mkpath(fullPathLocal)) { - reportRequestError(op, tr("Cannot create directory ") + fullPathLocal); + reportRequestError(op, SftpError::GenericFailure, tr("Cannot create directory ") + fullPathLocal); break; } @@ -983,7 +996,7 @@ SftpChannelPrivate::JobMap::Iterator SftpChannelPrivate::lookupJob(SftpJobId id) void SftpChannelPrivate::closeHook() { for (JobMap::ConstIterator it = m_jobs.constBegin(); it != m_jobs.constEnd(); ++it) - emit finished(it.key(), tr("SFTP channel closed unexpectedly.")); + emit finished(it.key(), SftpError::EndOfFile, tr("SFTP channel closed unexpectedly.")); m_jobs.clear(); m_incomingData.clear(); m_incomingPacket.clear(); @@ -992,9 +1005,7 @@ void SftpChannelPrivate::closeHook() void SftpChannelPrivate::handleOpenSuccessInternal() { -#ifdef CREATOR_SSH_DEBUG - qDebug("SFTP session started"); -#endif + qCDebug(sshLog, "SFTP session started"); m_sendFacility.sendSftpPacket(remoteChannel()); m_sftpState = SubsystemRequested; } @@ -1005,7 +1016,7 @@ void SftpChannelPrivate::handleOpenFailureInternal(const QString &reason) throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE packet."); } - emit initializationFailed(tr("Server could not start session: %1").arg(reason)); + emit channelError(tr("Server could not start session: %1").arg(reason)); } void SftpChannelPrivate::sendReadRequest(const SftpDownload::Ptr &job, @@ -1021,6 +1032,7 @@ void SftpChannelPrivate::sendReadRequest(const SftpDownload::Ptr &job, } void SftpChannelPrivate::reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job, + const SftpError errorType, const QString &error) { // andres.pagliano TODO refactor @@ -1029,7 +1041,7 @@ void SftpChannelPrivate::reportRequestError(const AbstractSftpOperationWithHandl SftpListDir::Ptr lsjob = job.dynamicCast(); if (!lsjob.isNull() && lsjob->parentJob) { if (!lsjob->parentJob->hasError) { - emit finished(lsjob->parentJob->jobId, error); + emit finished(lsjob->parentJob->jobId, errorType, error); lsjob->parentJob->hasError = true; } } else { @@ -1037,12 +1049,12 @@ void SftpChannelPrivate::reportRequestError(const AbstractSftpOperationWithHandl SftpDownload::Ptr djob = job.dynamicCast(); if (!djob.isNull() && djob->parentJob) { if (!djob->parentJob->hasError) { - emit finished(djob->parentJob->jobId, error); + emit finished(djob->parentJob->jobId, errorType, error); djob->parentJob->hasError = true; } } else { // Other error - emit finished(job->jobId, error); + emit finished(job->jobId, errorType, error); } } job->hasError = true; @@ -1081,8 +1093,13 @@ void SftpChannelPrivate::attributesToFileInfo(const SftpFileAttributes &attribut fileInfo.type = FileTypeOther; fileInfo.permissionsValid = true; fileInfo.permissions = 0; - fileInfo.atime = attributes.atime; - fileInfo.mtime = attributes.mtime; + + 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 @@ -1113,13 +1130,16 @@ void SftpChannelPrivate::removeTransferRequest(const JobMap::Iterator &it) void SftpChannelPrivate::sendWriteRequest(const JobMap::Iterator &it) { SftpUploadFile::Ptr job = it.value().staticCast(); + + emit transferProgress(job->jobId, job->localFile->pos(), job->localFile->size()); + QByteArray data = job->localFile->read(AbstractSftpPacket::MaxDataSize); QFileDevice *fileDevice = qobject_cast(job->localFile.data()); if (fileDevice && fileDevice->error() != QFileDevice::NoError) { if (job->parentJob) job->parentJob->setError(); - reportRequestError(job, tr("Error reading local file: %1") + reportRequestError(job, SftpError::GenericFailure, tr("Error reading local file: %1") .arg(job->localFile->errorString())); finishTransferRequest(it); } else if (data.isEmpty()) { diff --git a/src/libs/ssh/sftpchannel.h b/src/libs/ssh/sftpchannel.h index 1502449..ee577ea 100644 --- a/src/libs/ssh/sftpchannel.h +++ b/src/libs/ssh/sftpchannel.h @@ -32,7 +32,6 @@ #define SFTCHANNEL_H #include "sftpdefs.h" -#include "sftpincomingpacket_p.h" #include "ssh_global.h" @@ -49,6 +48,32 @@ class SshChannelManager; class SshSendFacility; } // namespace Internal + +/*! + \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 @@ -56,56 +81,177 @@ class QSSH_EXPORT SftpChannel : public QObject friend class Internal::SftpChannelPrivate; friend class Internal::SshChannelManager; public: + /// Convenience typedef typedef QSharedPointer 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 + * \return A unique ID identifying this job + */ SftpJobId listDirectory(const QString &dirPath); + + /*! + * \brief Create remote directory + * \param dirPath Remote path of directory + * \return A unique ID identifying this job + */ SftpJobId createDirectory(const QString &dirPath); + + /*! + * \brief Remove remote directory + * \param dirPath Remote path of directory + * \return A unique ID identifying this job + */ SftpJobId removeDirectory(const QString &dirPath); + + /*! + * \brief Remove remote file + * \param filePath Remote path of file + * \return A unique ID identifying this job + */ 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 + * \return A unique ID identifying this job + */ 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. + * \return A unique ID identifying this job + */ 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 + * \return A unique ID identifying this job + */ SftpJobId createLink(const QString &filePath, const QString &target); - SftpJobId uploadFile(QSharedPointer localFile, + + /*! + * \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 + * \return A unique ID identifying this job + */ + SftpJobId uploadFile(QSharedPointer 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 + * \return A unique ID identifying this job + */ SftpJobId uploadFile(const QString &localFilePath, const QString &remoteFilePath, SftpOverwriteMode mode); + + /*! + * \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 + * \return A unique ID identifying this job + */ 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 + * \return A unique ID identifying this job + */ SftpJobId downloadFile(const QString &remoteFilePath, - QSharedPointer localFile); + QSharedPointer 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 + * \return A unique ID identifying this job + */ 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(); - void initializationFailed(const QString &reason); + + /// 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 <=> finished successfully - void finished(QSsh::SftpJobId job, const QString &error = QString()); + /// error.isEmpty means it finished successfully + void finished(QSsh::SftpJobId job, const SftpError errorType = SftpError::NoError, const QString &error = QString()); - // TODO: Also emit for each file copied by uploadDir(). + /*! + * 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 &fileInfoList); + /*! + * Emitted during upload or download + */ + void transferProgress(QSsh::SftpJobId job, quint64 progress, quint64 total); + private: SftpChannel(quint32 channelId, Internal::SshSendFacility &sendFacility); diff --git a/src/libs/ssh/sftpchannel_p.h b/src/libs/ssh/sftpchannel_p.h index e57b057..96126ae 100644 --- a/src/libs/ssh/sftpchannel_p.h +++ b/src/libs/ssh/sftpchannel_p.h @@ -49,21 +49,16 @@ class SftpChannelPrivate : public AbstractSshChannel Q_OBJECT friend class QSsh::SftpChannel; public: - enum SftpState { Inactive, SubsystemRequested, InitSent, Initialized }; - virtual void handleChannelSuccess(); - virtual void handleChannelFailure(); - - virtual void closeHook(); - signals: void initialized(); - void initializationFailed(const QString &reason); + void channelError(const QString &reason); void closed(); - void finished(QSsh::SftpJobId job, const QString &error = QString()); + void finished(QSsh::SftpJobId job, const SftpError errorType = SftpError::NoError, const QString &error = QString()); void dataAvailable(QSsh::SftpJobId job, const QString &data); void fileInfoAvailable(QSsh::SftpJobId job, const QList &fileInfoList); + void transferProgress(QSsh::SftpJobId job, quint64 progress, quint64 total); private: typedef QMap JobMap; @@ -72,6 +67,9 @@ class SftpChannelPrivate : public AbstractSshChannel 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); @@ -80,6 +78,8 @@ class SftpChannelPrivate : public AbstractSshChannel virtual void handleExitStatus(const SshChannelExitStatus &exitStatus); virtual void handleExitSignal(const SshChannelExitSignal &signal); + virtual void closeHook(); + void handleCurrentPacket(); void handleServerVersion(); void handleHandle(); @@ -112,7 +112,7 @@ class SftpChannelPrivate : public AbstractSshChannel void sendWriteRequest(const JobMap::Iterator &it); void finishTransferRequest(const JobMap::Iterator &it); void removeTransferRequest(const JobMap::Iterator &it); - void reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job, + void reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job, const SftpError errorType, const QString &error); void sendTransferCloseHandle(const AbstractSftpTransfer::Ptr &job, quint32 requestId); diff --git a/src/libs/ssh/sftpdefs.h b/src/libs/ssh/sftpdefs.h index d7337a1..697a204 100644 --- a/src/libs/ssh/sftpdefs.h +++ b/src/libs/ssh/sftpdefs.h @@ -36,34 +36,82 @@ #include #include +/*! + * \namespace QSsh + * \brief The namespace used for the entire library + */ namespace QSsh { + +/*! + *\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 { - SftpOverwriteExisting, SftpAppendToExisting, SftpSkipExisting + /*! 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 { public: 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; - SftpFileType type; - quint64 size; - QFile::Permissions permissions; - - //add by hadesjaky 2017.7.2 add file time - quint32 atime; - quint32 mtime;//modify time - - // The RFC allows an SFTP server not to support any file attributes beyond the name. - bool sizeValid; - bool permissionsValid; + + /// 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; + + /// The RFC allows an SFTP server not to support any file attributes beyond the name. + bool permissionsValid = false; }; } // namespace QSsh diff --git a/src/libs/ssh/sftpfilesystemmodel.cpp b/src/libs/ssh/sftpfilesystemmodel.cpp index 4753127..d6f2848 100644 --- a/src/libs/ssh/sftpfilesystemmodel.cpp +++ b/src/libs/ssh/sftpfilesystemmodel.cpp @@ -100,7 +100,7 @@ SftpFileSystemModel::SftpFileSystemModel(QObject *parent) : QAbstractItemModel(parent), d(new SftpFileSystemModelPrivate) { d->sshConnection = 0; - d->rootDirectory = QLatin1String("/"); + d->rootDirectory = QLatin1Char('/'); d->rootNode = 0; d->statJobId = SftpInvalidJob; } @@ -114,13 +114,15 @@ SftpFileSystemModel::~SftpFileSystemModel() void SftpFileSystemModel::setSshConnection(const SshConnectionParameters &sshParams) { QSSH_ASSERT_AND_RETURN(!d->sshConnection); - d->sshConnection = SshConnectionManager::instance().acquireConnection(sshParams); - connect(d->sshConnection, SIGNAL(error(QSsh::SshError)), SLOT(handleSshConnectionFailure())); + d->sshConnection = QSsh::acquireConnection(sshParams); + connect(d->sshConnection, &SshConnection::error, + this, &SftpFileSystemModel::handleSshConnectionFailure); if (d->sshConnection->state() == SshConnection::Connected) { handleSshConnectionEstablished(); return; } - connect(d->sshConnection, SIGNAL(connected()), SLOT(handleSshConnectionEstablished())); + connect(d->sshConnection, &SshConnection::connected, + this, &SftpFileSystemModel::handleSshConnectionEstablished); if (d->sshConnection->state() == SshConnection::Unconnected) d->sshConnection->connectToHost(); } @@ -168,11 +170,11 @@ QVariant SftpFileSystemModel::data(const QModelIndex &index, int role) const switch (node->fileInfo.type) { case FileTypeRegular: case FileTypeOther: - return QIcon(QLatin1String(":/core/images/unknownfile.png")); + return QIcon(":/ssh/images/unknownfile.png"); case FileTypeDirectory: - return QIcon(QLatin1String(":/core/images/dir.png")); + return QIcon(":/ssh/images/dir.png"); case FileTypeUnknown: - return QIcon(QLatin1String(":/core/images/help.png")); // Shows a question mark. + return QIcon(":/ssh/images/help.png"); // Shows a question mark. } } if (index.column() == 1) { @@ -256,6 +258,10 @@ int SftpFileSystemModel::rowCount(const QModelIndex &parent) const void SftpFileSystemModel::statRootDirectory() { + if (!d->sftpChannel) { + return; + } + d->statJobId = d->sftpChannel->statFile(d->rootDirectory); } @@ -268,7 +274,7 @@ void SftpFileSystemModel::shutDown() } if (d->sshConnection) { disconnect(d->sshConnection, 0, this, 0); - SshConnectionManager::instance().releaseConnection(d->sshConnection); + QSsh::releaseConnection(d->sshConnection); d->sshConnection = 0; } delete d->rootNode; @@ -286,23 +292,24 @@ void SftpFileSystemModel::handleSshConnectionFailure() void SftpFileSystemModel::handleSftpChannelInitialized() { connect(d->sftpChannel.data(), - SIGNAL(fileInfoAvailable(QSsh::SftpJobId,QList)), - SLOT(handleFileInfo(QSsh::SftpJobId,QList))); - connect(d->sftpChannel.data(), SIGNAL(finished(QSsh::SftpJobId,QString)), - SLOT(handleSftpJobFinished(QSsh::SftpJobId,QString))); + &SftpChannel::fileInfoAvailable, + this, &SftpFileSystemModel::handleFileInfo); + connect(d->sftpChannel.data(), &SftpChannel::finished, + this, &SftpFileSystemModel::handleSftpJobFinished); statRootDirectory(); } void SftpFileSystemModel::handleSshConnectionEstablished() { d->sftpChannel = d->sshConnection->createSftpChannel(); - connect(d->sftpChannel.data(), SIGNAL(initialized()), SLOT(handleSftpChannelInitialized())); - connect(d->sftpChannel.data(), SIGNAL(initializationFailed(QString)), - SLOT(handleSftpChannelInitializationFailed(QString))); + connect(d->sftpChannel.data(), &SftpChannel::initialized, + this, &SftpFileSystemModel::handleSftpChannelInitialized); + connect(d->sftpChannel.data(), &SftpChannel::channelError, + this, &SftpFileSystemModel::handleSftpChannelError); d->sftpChannel->initialize(); } -void SftpFileSystemModel::handleSftpChannelInitializationFailed(const QString &reason) +void SftpFileSystemModel::handleSftpChannelError(const QString &reason) { emit connectionError(reason); beginResetModel(); @@ -354,12 +361,14 @@ void SftpFileSystemModel::handleFileInfo(SftpJobId jobId, const QListstatJobId) { d->statJobId = SftpInvalidJob; if (!errorMessage.isEmpty()) - emit sftpOperationFailed(tr("Error getting 'stat' info about '%1': %2") + emit sftpOperationFailed(tr("Error getting \"stat\" info about \"%1\": %2") .arg(rootDirectory(), errorMessage)); return; } @@ -369,7 +378,7 @@ void SftpFileSystemModel::handleSftpJobFinished(SftpJobId jobId, const QString & QSSH_ASSERT(it.value()->lsState == SftpDirNode::LsRunning); it.value()->lsState = SftpDirNode::LsFinished; if (!errorMessage.isEmpty()) - emit sftpOperationFailed(tr("Error listing contents of directory '%1': %2") + emit sftpOperationFailed(tr("Error listing contents of directory \"%1\": %2") .arg(it.value()->path, errorMessage)); d->lsOps.erase(it); return; diff --git a/src/libs/ssh/sftpfilesystemmodel.h b/src/libs/ssh/sftpfilesystemmodel.h index f6233ea..47e168f 100644 --- a/src/libs/ssh/sftpfilesystemmodel.h +++ b/src/libs/ssh/sftpfilesystemmodel.h @@ -81,15 +81,14 @@ class QSSH_EXPORT SftpFileSystemModel : public QAbstractItemModel // Success <=> error.isEmpty(). void sftpOperationFinished(QSsh::SftpJobId, const QString &error); -private slots: +private: void handleSshConnectionEstablished(); void handleSshConnectionFailure(); void handleSftpChannelInitialized(); - void handleSftpChannelInitializationFailed(const QString &reason); + void handleSftpChannelError(const QString &reason); void handleFileInfo(QSsh::SftpJobId jobId, const QList &fileInfoList); - void handleSftpJobFinished(QSsh::SftpJobId jobId, const QString &errorMessage); + void handleSftpJobFinished(QSsh::SftpJobId jobId, const SftpError error, const QString &errorMessage); -private: 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; diff --git a/src/libs/ssh/sftpincomingpacket.cpp b/src/libs/ssh/sftpincomingpacket.cpp index d669787..bc1fd4a 100644 --- a/src/libs/ssh/sftpincomingpacket.cpp +++ b/src/libs/ssh/sftpincomingpacket.cpp @@ -31,6 +31,7 @@ #include "sftpincomingpacket_p.h" #include "sshexception_p.h" +#include "sshlogging_p.h" #include "sshpacketparser_p.h" namespace QSsh { @@ -42,10 +43,8 @@ SftpIncomingPacket::SftpIncomingPacket() : m_length(0) void SftpIncomingPacket::consumeData(QByteArray &newData) { -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: current data size = %d, new data size = %d", Q_FUNC_INFO, + qCDebug(sshLog, "%s: current data size = %d, new data size = %d", Q_FUNC_INFO, m_data.size(), newData.size()); -#endif if (isComplete() || dataSize() + newData.size() < sizeof m_length) return; @@ -88,7 +87,7 @@ quint32 SftpIncomingPacket::extractServerVersion() const Q_ASSERT(type() == SSH_FXP_VERSION); try { return SshPacketParser::asUint32(m_data, TypeOffset + 1); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_VERSION packet."); } @@ -104,7 +103,7 @@ SftpHandleResponse SftpIncomingPacket::asHandleResponse() const response.requestId = SshPacketParser::asUint32(m_data, &offset); response.handle = SshPacketParser::asString(m_data, &offset); return response; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_HANDLE packet"); } @@ -122,7 +121,7 @@ SftpStatusResponse SftpIncomingPacket::asStatusResponse() const response.errorString = SshPacketParser::asUserString(m_data, &offset); response.language = SshPacketParser::asString(m_data, &offset); return response; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_STATUS packet."); } @@ -140,7 +139,7 @@ SftpNameResponse SftpIncomingPacket::asNameResponse() const for (quint32 i = 0; i < count; ++i) response.files << asFile(offset); return response; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_NAME packet."); } @@ -156,7 +155,7 @@ SftpDataResponse SftpIncomingPacket::asDataResponse() const response.requestId = SshPacketParser::asUint32(m_data, &offset); response.data = SshPacketParser::asString(m_data, &offset); return response; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_DATA packet."); } @@ -172,7 +171,7 @@ SftpAttrsResponse SftpIncomingPacket::asAttrsResponse() const response.requestId = SshPacketParser::asUint32(m_data, &offset); response.attrs = asFileAttributes(offset); return response; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_FXP_ATTRS packet."); } @@ -197,8 +196,9 @@ SftpFileAttributes SftpIncomingPacket::asFileAttributes(quint32 &offset) const attributes.timesPresent = flags & SSH_FILEXFER_ATTR_ACMODTIME; attributes.uidAndGidPresent = flags & SSH_FILEXFER_ATTR_UIDGID; attributes.permissionsPresent = flags & SSH_FILEXFER_ATTR_PERMISSIONS; - if (attributes.sizePresent) + if (attributes.sizePresent) { attributes.size = SshPacketParser::asUint64(m_data, &offset); + } if (attributes.uidAndGidPresent) { attributes.uid = SshPacketParser::asUint32(m_data, &offset); attributes.gid = SshPacketParser::asUint32(m_data, &offset); diff --git a/src/libs/ssh/sftpoperation.cpp b/src/libs/ssh/sftpoperation.cpp index 89ab691..7e34e04 100644 --- a/src/libs/ssh/sftpoperation.cpp +++ b/src/libs/ssh/sftpoperation.cpp @@ -66,8 +66,8 @@ SftpOutgoingPacket &SftpMakeDir::initialPacket(SftpOutgoingPacket &packet) } -SftpRmDir::SftpRmDir(SftpJobId, const QString &path) - : AbstractSftpOperation(jobId), remoteDir(path) +SftpRmDir::SftpRmDir(SftpJobId id, const QString &path) + : AbstractSftpOperation(id), remoteDir(path) { } diff --git a/src/libs/ssh/sftpoperation_p.h b/src/libs/ssh/sftpoperation_p.h index 143540e..1190390 100644 --- a/src/libs/ssh/sftpoperation_p.h +++ b/src/libs/ssh/sftpoperation_p.h @@ -97,7 +97,7 @@ struct SftpRmDir : public AbstractSftpOperation { typedef QSharedPointer Ptr; - SftpRmDir(SftpJobId jobId, const QString &path); + SftpRmDir(SftpJobId id, const QString &path); virtual Type type() const { return RmDir; } virtual SftpOutgoingPacket &initialPacket(SftpOutgoingPacket &packet); diff --git a/src/libs/ssh/sftpoutgoingpacket.cpp b/src/libs/ssh/sftpoutgoingpacket.cpp index ae2cd58..02d370c 100644 --- a/src/libs/ssh/sftpoutgoingpacket.cpp +++ b/src/libs/ssh/sftpoutgoingpacket.cpp @@ -30,6 +30,7 @@ #include "sftpoutgoingpacket_p.h" +#include "sshlogging_p.h" #include "sshpacket_p.h" #include @@ -155,7 +156,7 @@ SftpOutgoingPacket &SftpOutgoingPacket::generateCreateLink(const QString &filePa SftpOutgoingPacket &SftpOutgoingPacket::generateOpenFile(const QString &path, OpenType openType, SftpOverwriteMode mode, const QList &attributes, quint32 requestId) { - quint32 pFlags; + quint32 pFlags = 0; switch (openType) { case Read: pFlags = SSH_FXF_READ; @@ -183,10 +184,7 @@ SftpOutgoingPacket &SftpOutgoingPacket::init(SftpPacketType type, m_data[TypeOffset] = type; if (type != SSH_FXP_INIT) { appendInt(requestId); -#ifdef CREATOR_SSH_DEBUG - qDebug("Generating SFTP packet of type %d with request id %u", type, - requestId); -#endif + qCDebug(sshLog, "Generating SFTP packet of type %d with request id %u", type, requestId); } return *this; } diff --git a/src/libs/ssh/sftppacket.cpp b/src/libs/ssh/sftppacket.cpp index 3590ca9..245787d 100644 --- a/src/libs/ssh/sftppacket.cpp +++ b/src/libs/ssh/sftppacket.cpp @@ -35,8 +35,11 @@ namespace QSsh { namespace Internal { -const quint32 AbstractSftpPacket::MaxDataSize = 32000; -const quint32 AbstractSftpPacket::MaxPacketSize = 34000; +// 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; diff --git a/src/libs/ssh/ssh.pro b/src/libs/ssh/ssh.pro index c94dba9..12c4e72 100644 --- a/src/libs/ssh/ssh.pro +++ b/src/libs/ssh/ssh.pro @@ -1,16 +1,32 @@ TEMPLATE = lib TARGET = QSsh QT += network -DEFINES += QSSH_LIBRARY - -LIBS += -lbotan-2 +DEFINES += QTCSSH_LIBRARY #Enable debug log #DEFINES += CREATOR_SSH_DEBUG -INCLUDEPATH += /usr/include/botan-2/ +INCLUDEPATH += $$QSSH_PREFIX/include/botan-2/ + +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 -include(../../qtcreatorlibrary.pri) +contains(QT_CONFIG, reduce_exports):CONFIG += hide_symbols SOURCES = $$PWD/sshsendfacility.cpp \ $$PWD/sshremoteprocess.cpp \ @@ -34,21 +50,46 @@ SOURCES = $$PWD/sshsendfacility.cpp \ $$PWD/sshremoteprocessrunner.cpp \ $$PWD/sshconnectionmanager.cpp \ $$PWD/sshkeypasswordretriever.cpp \ - $$PWD/sftpfilesystemmodel.cpp + $$PWD/sftpfilesystemmodel.cpp \ + $$PWD/sshinit.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 \ -HEADERS = $$PWD/sshsendfacility_p.h \ +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/sshkeygenerator.h \ $$PWD/sshkeyexchange_p.h \ $$PWD/sshincomingpacket_p.h \ $$PWD/sshexception_p.h \ - $$PWD/ssherrors.h \ $$PWD/sshcryptofacility_p.h \ - $$PWD/sshconnection.h \ $$PWD/sshconnection_p.h \ $$PWD/sshchannelmanager_p.h \ $$PWD/sshchannel_p.h \ @@ -58,14 +99,26 @@ HEADERS = $$PWD/sshsendfacility_p.h \ $$PWD/sftpoutgoingpacket_p.h \ $$PWD/sftpoperation_p.h \ $$PWD/sftpincomingpacket_p.h \ - $$PWD/sftpdefs.h \ - $$PWD/sftpchannel.h \ $$PWD/sftpchannel_p.h \ - $$PWD/sshremoteprocessrunner.h \ - $$PWD/sshconnectionmanager.h \ - $$PWD/sshpseudoterminal.h \ $$PWD/sshkeypasswordretriever_p.h \ - $$PWD/sftpfilesystemmodel.h \ - $$PWD/ssh_global.h + $$PWD/sshdirecttcpiptunnel_p.h \ + $$PWD/sshinit_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/ssh.qrc + + +headers.files = $$PUBLIC_HEADERS +headers.path = $$[QT_INSTALL_PREFIX]/include/QSsh +target.path = $$[QT_INSTALL_LIBS] +INSTALLS += target headers diff --git a/src/libs/ssh/ssh.qbs b/src/libs/ssh/ssh.qbs index 755bb22..f3010b1 100644 --- a/src/libs/ssh/ssh.qbs +++ b/src/libs/ssh/ssh.qbs @@ -35,6 +35,7 @@ QtcLibrary { "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", diff --git a/src/libs/ssh/ssh.qrc b/src/libs/ssh/ssh.qrc new file mode 100644 index 0000000..4b0c6a8 --- /dev/null +++ b/src/libs/ssh/ssh.qrc @@ -0,0 +1,7 @@ + + + images/dir.png + images/help.png + images/unknownfile.png + + diff --git a/src/libs/ssh/ssh_global.h b/src/libs/ssh/ssh_global.h index 199647a..920f52c 100644 --- a/src/libs/ssh/ssh_global.h +++ b/src/libs/ssh/ssh_global.h @@ -33,7 +33,7 @@ #include -#if defined(QSSH_LIBRARY) +#if defined(QTCSSH_LIBRARY) # define QSSH_EXPORT Q_DECL_EXPORT #else # define QSSH_EXPORT Q_DECL_IMPORT diff --git a/src/libs/ssh/sshagent.cpp b/src/libs/ssh/sshagent.cpp new file mode 100644 index 0000000..4e581fa --- /dev/null +++ b/src/libs/ssh/sshagent.cpp @@ -0,0 +1,314 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "sshagent_p.h" + +#include "sshlogging_p.h" +#include "sshpacket_p.h" +#include "sshpacketparser_p.h" +#include "ssh_global.h" + +#include +#include + +#include + +namespace QSsh { +namespace Internal { + +// 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 static T fromBigEndian(const QByteArray &ba) +{ + return qFromBigEndian(reinterpret_cast(ba.constData())); +} + +void SshAgent::refreshKeysImpl() +{ + if (state() != Connected) + return; + 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"; + return; + } + qCDebug(sshLog) << "queueing keys request"; + m_pendingRequests << Request(); + sendNextRequest(); +} + +void SshAgent::requestSignatureImpl(const QByteArray &key, uint token) +{ + if (state() != Connected) + return; + 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)); + sendNextRequest(); +} + +void SshAgent::sendNextRequest() +{ + if (m_pendingRequests.isEmpty()) + return; + if (m_outgoingPacket.isComplete()) + return; + if (hasError()) + return; + 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; + Packet p; + 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(); + return p; +} + +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, + static_cast(&QLocalSocket::error), + 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(); + return; + } + 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(); + setDisconnected(); +} + +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)) + break; + m_incomingPacket.size = fromBigEndian(m_incomingData); + m_incomingData.remove(0, sizeof m_incomingPacket.size); + } + const int bytesToTake = qMin(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(); + else + break; + } +} + +void SshAgent::handleIncomingPacket() +{ + try { + 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(); + break; + case SSH2_AGENT_SIGN_RESPONSE: + handleSignaturePacket(); + break; + case SSH_AGENT_FAILURE: + if (m_pendingRequests.isEmpty()) { + qCWarning(sshLog) << "unexpected failure message from agent"; + } else { + 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); + } else { + qCWarning(sshLog) << "agent failed to retrieve key list"; + if (m_keys.isEmpty()) { + m_error = tr("ssh-agent failed to retrieve keys."); + setDisconnected(); + } + } + } + break; + default: + qCWarning(sshLog) << "unexpected message type from agent:" << messageType; + } + } catch (const SshPacketParseException &) { + qCWarning(sshLog()) << "received malformed packet from agent"; + handleProtocolError(); + } + m_incomingPacket.invalidate(); + m_incomingPacket.size = 0; + m_outgoingPacket.invalidate(); + sendNextRequest(); +} + +void SshAgent::handleIdentitiesPacket() +{ + qCDebug(sshLog) << "got keys packet from agent"; + if (m_pendingRequests.isEmpty() || !m_pendingRequests.dequeue().isKeysRequest()) { + qCDebug(sshLog) << "packet was not requested"; + handleProtocolError(); + return; + } + quint32 offset = 1; + const auto keyCount = SshPacketParser::asUint32(m_incomingPacket.data, &offset); + qCDebug(sshLog) << "packet contains" << keyCount << "keys"; + QList 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"; + if (m_pendingRequests.isEmpty()) { + qCDebug(sshLog) << "signature packet was not requested"; + handleProtocolError(); + return; + } + const Request request = m_pendingRequests.dequeue(); + if (!request.isSignatureRequest()) { + qCDebug(sshLog) << "signature packet was not requested"; + handleProtocolError(); + return; + } + 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."); + setDisconnected(); +} + +void SshAgent::setDisconnected() +{ + m_state = Unconnected; + m_agentSocket.disconnect(this); + emit errorOccurred(); +} + +void SshAgent::sendPacket() +{ + const quint32 sizeMsb = qToBigEndian(m_outgoingPacket.size); + m_agentSocket.write(reinterpret_cast(&sizeMsb), sizeof sizeMsb); + m_agentSocket.write(m_outgoingPacket.data); +} + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshagent_p.h b/src/libs/ssh/sshagent_p.h new file mode 100644 index 0000000..f30867b --- /dev/null +++ b/src/libs/ssh/sshagent_p.h @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QSsh { +namespace Internal { + +class SshAgent : public QObject +{ + Q_OBJECT +public: + 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 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(); + +signals: + void errorOccurred(); + void keysUpdated(); + + // Empty signature means signing failure. + void signatureAvailable(const QByteArray &key, const QByteArray &signature, uint token); + +private: + 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; + QByteArray data; + }; + + 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 m_keys; + QHash, QByteArray> m_dataToSign; + QLocalSocket m_agentSocket; + QByteArray m_incomingData; + Packet m_incomingPacket; + Packet m_outgoingPacket; + + QQueue m_pendingRequests; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshbotanconversions_p.h b/src/libs/ssh/sshbotanconversions_p.h index 0400265..7ee7186 100644 --- a/src/libs/ssh/sshbotanconversions_p.h +++ b/src/libs/ssh/sshbotanconversions_p.h @@ -32,6 +32,7 @@ #define BYTEARRAYCONVERSIONS_P_H #include "sshcapabilities_p.h" +#include "sshexception_p.h" #include @@ -50,7 +51,7 @@ inline Botan::byte *convertByteArray(QByteArray &a) inline QByteArray convertByteArray(const Botan::secure_vector &v) { - return QByteArray(reinterpret_cast(v.data()), v.size()); + return QByteArray(reinterpret_cast(v.data()), static_cast(v.size())); } inline QByteArray convertByteArray(const std::vector &v) @@ -60,42 +61,82 @@ inline QByteArray convertByteArray(const std::vector &v) inline const char *botanKeyExchangeAlgoName(const QByteArray &rfcAlgoName) { - Q_ASSERT(rfcAlgoName == SshCapabilities::DiffieHellmanGroup1Sha1 - || rfcAlgoName == SshCapabilities::DiffieHellmanGroup14Sha1); - return rfcAlgoName == SshCapabilities::DiffieHellmanGroup1Sha1 - ? "modp/ietf/1024" : "modp/ietf/2048"; + 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 *botanCryptAlgoName(const QByteArray &rfcAlgoName) { - Q_ASSERT(rfcAlgoName == SshCapabilities::CryptAlgo3Des - || rfcAlgoName == SshCapabilities::CryptAlgoAes128); - return rfcAlgoName == SshCapabilities::CryptAlgo3Des - ? "TripleDES" : "AES-128"; + if (rfcAlgoName == SshCapabilities::CryptAlgoAes128Cbc + || rfcAlgoName == SshCapabilities::CryptAlgoAes128Ctr) { + return "AES-128"; + } + if (rfcAlgoName == SshCapabilities::CryptAlgo3DesCbc + || rfcAlgoName == SshCapabilities::CryptAlgo3DesCtr) { + return "TripleDES"; + } + if (rfcAlgoName == SshCapabilities::CryptAlgoAes192Ctr) { + return "AES-192"; + } + if (rfcAlgoName == SshCapabilities::CryptAlgoAes256Ctr) { + return "AES-256"; + } + throw SshClientException(SshInternalError, SSH_TR("Unexpected cipher \"%1\"") + .arg(QString::fromLatin1(rfcAlgoName))); } inline const char *botanEmsaAlgoName(const QByteArray &rfcAlgoName) { - Q_ASSERT(rfcAlgoName == SshCapabilities::PubKeyDss - || rfcAlgoName == SshCapabilities::PubKeyRsa); - return rfcAlgoName == SshCapabilities::PubKeyDss - ? "EMSA1(SHA-1)" : "EMSA3(SHA-1)"; + 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\"") + .arg(QString::fromLatin1(rfcAlgoName))); } -inline const char *botanSha1Name() { return "SHA-1"; } - inline const char *botanHMacAlgoName(const QByteArray &rfcAlgoName) { - Q_ASSERT(rfcAlgoName == SshCapabilities::HMacSha1); - Q_UNUSED(rfcAlgoName); - return botanSha1Name(); + 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\"") + .arg(QString::fromLatin1(rfcAlgoName))); } inline quint32 botanHMacKeyLen(const QByteArray &rfcAlgoName) { - Q_ASSERT(rfcAlgoName == SshCapabilities::HMacSha1); - Q_UNUSED(rfcAlgoName); - return 20; + if (rfcAlgoName == SshCapabilities::HMacSha1) + return 20; + if (rfcAlgoName == SshCapabilities::HMacSha256) + return 32; + if (rfcAlgoName == SshCapabilities::HMacSha384) + return 48; + if (rfcAlgoName == SshCapabilities::HMacSha512) + return 64; + throw SshClientException(SshInternalError, SSH_TR("Unexpected hashing algorithm \"%1\"") + .arg(QString::fromLatin1(rfcAlgoName))); } } // namespace Internal diff --git a/src/libs/ssh/sshcapabilities.cpp b/src/libs/ssh/sshcapabilities.cpp index b4fa549..7f5ad59 100644 --- a/src/libs/ssh/sshcapabilities.cpp +++ b/src/libs/ssh/sshcapabilities.cpp @@ -52,26 +52,54 @@ namespace { const QByteArray SshCapabilities::DiffieHellmanGroup1Sha1("diffie-hellman-group1-sha1"); const QByteArray SshCapabilities::DiffieHellmanGroup14Sha1("diffie-hellman-group14-sha1"); -const QList SshCapabilities::KeyExchangeMethods - = QList() << SshCapabilities::DiffieHellmanGroup1Sha1 - << SshCapabilities::DiffieHellmanGroup14Sha1; +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 SshCapabilities::KeyExchangeMethods = QList() + << SshCapabilities::EcdhNistp256 + << SshCapabilities::EcdhNistp384 + << SshCapabilities::EcdhNistp521 + << SshCapabilities::DiffieHellmanGroup1Sha1 + << SshCapabilities::DiffieHellmanGroup14Sha1; const QByteArray SshCapabilities::PubKeyDss("ssh-dss"); const QByteArray SshCapabilities::PubKeyRsa("ssh-rsa"); -const QList SshCapabilities::PublicKeyAlgorithms - = QList() << SshCapabilities::PubKeyRsa - << SshCapabilities::PubKeyDss; - -const QByteArray SshCapabilities::CryptAlgo3Des("3des-cbc"); -const QByteArray SshCapabilities::CryptAlgoAes128("aes128-cbc"); +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 SshCapabilities::PublicKeyAlgorithms = QList() + << 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 SshCapabilities::EncryptionAlgorithms - = QList() << SshCapabilities::CryptAlgoAes128 - << SshCapabilities::CryptAlgo3Des; + = QList() << 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 SshCapabilities::MacAlgorithms = QList() /* << SshCapabilities::HMacSha196 */ + << SshCapabilities::HMacSha256 + << SshCapabilities::HMacSha384 + << SshCapabilities::HMacSha512 << SshCapabilities::HMacSha1; const QList SshCapabilities::CompressionAlgorithms @@ -79,18 +107,18 @@ const QList SshCapabilities::CompressionAlgorithms const QByteArray SshCapabilities::SshConnectionService("ssh-connection"); -const QByteArray SshCapabilities::PublicKeyAuthMethod("publickey"); -const QByteArray SshCapabilities::PasswordAuthMethod("password"); - - -QByteArray SshCapabilities::findBestMatch(const QList &myCapabilities, - const QList &serverCapabilities) +QList SshCapabilities::commonCapabilities(const QList &myCapabilities, + const QList &serverCapabilities) { + QList capabilities; foreach (const QByteArray &myCapability, myCapabilities) { if (serverCapabilities.contains(myCapability)) - return 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", @@ -98,6 +126,49 @@ QByteArray SshCapabilities::findBestMatch(const QList &myCapabilitie "Client list was: %1.\nServer list was %2.") .arg(QString::fromLocal8Bit(listAsByteArray(myCapabilities).data())) .arg(QString::fromLocal8Bit(listAsByteArray(serverCapabilities).data()))); + +} + +QByteArray SshCapabilities::findBestMatch(const QList &myCapabilities, + const QList &serverCapabilities) +{ + return commonCapabilities(myCapabilities, serverCapabilities).first(); +} + +int SshCapabilities::ecdsaIntegerWidthInBytes(const QByteArray &ecdsaAlgo) +{ + if (ecdsaAlgo == PubKeyEcdsa256) + return 32; + if (ecdsaAlgo == PubKeyEcdsa384) + return 48; + 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) +{ + if (ecdsaAlgo == PubKeyEcdsa256) + return "secp256r1"; + if (ecdsaAlgo == PubKeyEcdsa384) + return "secp384r1"; + if (ecdsaAlgo == PubKeyEcdsa521) + return "secp521r1"; + throw SshClientException(SshInternalError, SSH_TR("Unexpected ecdsa algorithm \"%1\"") + .arg(QString::fromLatin1(ecdsaAlgo))); } } // namespace Internal diff --git a/src/libs/ssh/sshcapabilities_p.h b/src/libs/ssh/sshcapabilities_p.h index 99486d8..e7ab5c7 100644 --- a/src/libs/ssh/sshcapabilities_p.h +++ b/src/libs/ssh/sshcapabilities_p.h @@ -42,29 +42,47 @@ class SshCapabilities public: 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 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 PublicKeyAlgorithms; - static const QByteArray CryptAlgo3Des; - static const QByteArray CryptAlgoAes128; + 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 EncryptionAlgorithms; static const QByteArray HMacSha1; static const QByteArray HMacSha196; + static const QByteArray HMacSha256; + static const QByteArray HMacSha384; + static const QByteArray HMacSha512; static const QList MacAlgorithms; static const QList CompressionAlgorithms; static const QByteArray SshConnectionService; - static const QByteArray PublicKeyAuthMethod; - static const QByteArray PasswordAuthMethod; - + static QList commonCapabilities(const QList &myCapabilities, + const QList &serverCapabilities); static QByteArray findBestMatch(const QList &myCapabilities, const QList &serverCapabilities); + + static int ecdsaIntegerWidthInBytes(const QByteArray &ecdsaAlgo); + static QByteArray ecdsaPubKeyAlgoForKeyWidth(int keyWidthInBytes); + static const char *oid(const QByteArray &ecdsaAlgo); }; } // namespace Internal diff --git a/src/libs/ssh/sshchannel.cpp b/src/libs/ssh/sshchannel.cpp index c323407..9d72ba8 100644 --- a/src/libs/ssh/sshchannel.cpp +++ b/src/libs/ssh/sshchannel.cpp @@ -32,6 +32,7 @@ #include "sshincomingpacket_p.h" #include "sshsendfacility_p.h" +#include "sshlogging_p.h" #include @@ -40,22 +41,17 @@ namespace QSsh { namespace Internal { -namespace { - const quint32 MinMaxPacketSize = 32768; - const quint32 MaxPacketSize = 16 * 1024 * 1024; - const quint32 InitialWindowSize = MaxPacketSize; - const quint32 NoChannel = 0xffffffffu; -} // anonymous namespace +const quint32 NoChannel = 0xffffffffu; AbstractSshChannel::AbstractSshChannel(quint32 channelId, SshSendFacility &sendFacility) - : m_sendFacility(sendFacility), m_timeoutTimer(new QTimer(this)), + : m_sendFacility(sendFacility), m_localChannel(channelId), m_remoteChannel(NoChannel), - m_localWindowSize(InitialWindowSize), m_remoteWindowSize(0), + m_localWindowSize(initialWindowSize()), m_remoteWindowSize(0), m_state(Inactive) { - m_timeoutTimer->setSingleShot(true); - connect(m_timeoutTimer, SIGNAL(timeout()), this, SIGNAL(timeout())); + m_timeoutTimer.setSingleShot(true); + connect(&m_timeoutTimer, &QTimer::timeout, this, &AbstractSshChannel::timeout); } AbstractSshChannel::~AbstractSshChannel() @@ -77,12 +73,11 @@ void AbstractSshChannel::requestSessionStart() // with our cryptography stuff, it would have hit us before, on // establishing the connection. try { - m_sendFacility.sendSessionPacket(m_localChannel, InitialWindowSize, - MaxPacketSize); + m_sendFacility.sendSessionPacket(m_localChannel, initialWindowSize(), maxPacketSize()); setChannelState(SessionRequested); - m_timeoutTimer->start(ReplyTimeout); - } catch (Botan::Exception &e) { - qDebug("Botan error: %s", e.what()); + m_timeoutTimer.start(ReplyTimeout); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); closeChannel(); } } @@ -92,12 +87,22 @@ void AbstractSshChannel::sendData(const QByteArray &data) try { m_sendBuffer += data; flushSendBuffer(); - } catch (Botan::Exception &e) { - qDebug("Botan error: %s", e.what()); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); closeChannel(); } } +quint32 AbstractSshChannel::initialWindowSize() +{ + return maxPacketSize(); +} + +quint32 AbstractSshChannel::maxPacketSize() +{ + return 16 * 1024 * 1024; +} + void AbstractSshChannel::handleWindowAdjust(quint32 bytesToAdd) { checkChannelActive(); @@ -129,41 +134,46 @@ void AbstractSshChannel::flushSendBuffer() void AbstractSshChannel::handleOpenSuccess(quint32 remoteChannelId, quint32 remoteWindowSize, quint32 remoteMaxPacketSize) { - if (m_state != SessionRequested) { - throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, - "Invalid SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet."); - } - m_timeoutTimer->stop(); - - if (remoteMaxPacketSize < MinMaxPacketSize) { - throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, - "Maximum packet size too low."); - } - -#ifdef CREATOR_SSH_DEBUG - qDebug("Channel opened. remote channel id: %u, remote window size: %u, " + const ChannelState oldState = m_state; + switch (oldState) { + case CloseRequested: // closeChannel() was called while we were in SessionRequested state + case SessionRequested: + break; // Ok, continue. + default: + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "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); -#endif m_remoteChannel = remoteChannelId; m_remoteWindowSize = remoteWindowSize; - m_remoteMaxPacketSize = remoteMaxPacketSize - sizeof(quint32) - sizeof m_remoteChannel - 1; - // Original value includes packet type, channel number and length field for string. + m_remoteMaxPacketSize = remoteMaxPacketSize; setChannelState(SessionEstablished); - handleOpenSuccessInternal(); + if (oldState == CloseRequested) + closeChannel(); + else + handleOpenSuccessInternal(); } void AbstractSshChannel::handleOpenFailure(const QString &reason) { - if (m_state != SessionRequested) { - throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, - "Invalid SSH_MSG_CHANNEL_OPEN_FAILURE packet."); - } - m_timeoutTimer->stop(); - -#ifdef CREATOR_SSH_DEBUG - qDebug("Channel open request failed for channel %u", m_localChannel); -#endif + switch (m_state) { + case SessionRequested: + break; // Ok, continue. + case CloseRequested: + return; // Late server reply; we requested a channel close in the meantime. + default: + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE packet."); + } + + m_timeoutTimer.stop(); + + qCDebug(sshLog, "Channel open request failed for channel %u", m_localChannel); handleOpenFailureInternal(reason); } @@ -174,13 +184,12 @@ void AbstractSshChannel::handleChannelEof() "Unexpected SSH_MSG_CHANNEL_EOF message."); } m_localWindowSize = 0; + emit eof(); } void AbstractSshChannel::handleChannelClose() { -#ifdef CREATOR_SSH_DEBUG - qDebug("Receiving CLOSE for channel %u", m_localChannel); -#endif + qCDebug(sshLog, "Receiving CLOSE for channel %u", m_localChannel); if (channelState() == Inactive || channelState() == Closed) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_CLOSE message."); @@ -212,7 +221,7 @@ void AbstractSshChannel::handleChannelRequest(const SshIncomingPacket &packet) 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. - qWarning("Ignoring unknown request type '%s'", requestType.data()); + qCWarning(sshLog, "Ignoring unknown request type '%s'", requestType.data()); } int AbstractSshChannel::handleChannelOrExtendedChannelData(const QByteArray &data) @@ -221,13 +230,12 @@ int AbstractSshChannel::handleChannelOrExtendedChannelData(const QByteArray &dat const int bytesToDeliver = qMin(data.size(), maxDataSize()); if (bytesToDeliver != data.size()) - qWarning("Misbehaving server does not respect local window, clipping."); + 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); + if (m_localWindowSize < maxPacketSize()) { + m_localWindowSize += maxPacketSize(); + m_sendFacility.sendWindowAdjustPacket(m_remoteChannel, maxPacketSize()); } return bytesToDeliver; } @@ -235,14 +243,19 @@ int AbstractSshChannel::handleChannelOrExtendedChannelData(const QByteArray &dat void AbstractSshChannel::closeChannel() { if (m_state == CloseRequested) { - m_timeoutTimer->stop(); + m_timeoutTimer.stop(); } else if (m_state != Closed) { if (m_state == Inactive) { setChannelState(Closed); } else { + const ChannelState oldState = m_state; setChannelState(CloseRequested); - m_sendFacility.sendChannelEofPacket(m_remoteChannel); - m_sendFacility.sendChannelClosePacket(m_remoteChannel); + if (m_remoteChannel != NoChannel) { + m_sendFacility.sendChannelEofPacket(m_remoteChannel); + m_sendFacility.sendChannelClosePacket(m_remoteChannel); + } else { + QSSH_ASSERT(oldState == SessionRequested); + } } } } @@ -256,7 +269,7 @@ void AbstractSshChannel::checkChannelActive() quint32 AbstractSshChannel::maxDataSize() const { - return qMin(m_localWindowSize, MaxPacketSize); + return qMin(m_localWindowSize, maxPacketSize()); } } // namespace Internal diff --git a/src/libs/ssh/sshchannel_p.h b/src/libs/ssh/sshchannel_p.h index d1a84e1..41754e3 100644 --- a/src/libs/ssh/sshchannel_p.h +++ b/src/libs/ssh/sshchannel_p.h @@ -34,8 +34,7 @@ #include #include #include - -QT_FORWARD_DECLARE_CLASS(QTimer) +#include namespace QSsh { namespace Internal { @@ -53,17 +52,12 @@ class AbstractSshChannel : public QObject Inactive, SessionRequested, SessionEstablished, CloseRequested, Closed }; - ChannelState channelState() const { return m_state; } - void setChannelState(ChannelState state); - quint32 localChannelId() const { return m_localChannel; } quint32 remoteChannel() const { return m_remoteChannel; } virtual void handleChannelSuccess() = 0; virtual void handleChannelFailure() = 0; - virtual void closeHook() = 0; - void handleOpenSuccess(quint32 remoteChannelId, quint32 remoteWindowSize, quint32 remoteMaxPacketSize); void handleOpenFailure(const QString &reason); @@ -74,25 +68,33 @@ class AbstractSshChannel : public QObject void handleChannelExtendedData(quint32 type, const QByteArray &data); void handleChannelRequest(const SshIncomingPacket &packet); - void requestSessionStart(); - void sendData(const QByteArray &data); void closeChannel(); virtual ~AbstractSshChannel(); static const int ReplyTimeout = 10000; // milli seconds + ChannelState channelState() const { return m_state; } signals: void timeout(); + void eof(); protected: 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(); SshSendFacility &m_sendFacility; - QTimer * const m_timeoutTimer; + QTimer m_timeoutTimer; private: virtual void handleOpenSuccessInternal() = 0; @@ -103,7 +105,8 @@ class AbstractSshChannel : public QObject virtual void handleExitStatus(const SshChannelExitStatus &exitStatus) = 0; virtual void handleExitSignal(const SshChannelExitSignal &signal) = 0; - void setState(ChannelState newState); + virtual void closeHook() = 0; + void flushSendBuffer(); int handleChannelOrExtendedChannelData(const QByteArray &data); diff --git a/src/libs/ssh/sshchannelmanager.cpp b/src/libs/ssh/sshchannelmanager.cpp index 7929637..82a4164 100644 --- a/src/libs/ssh/sshchannelmanager.cpp +++ b/src/libs/ssh/sshchannelmanager.cpp @@ -32,10 +32,19 @@ #include "sftpchannel.h" #include "sftpchannel_p.h" +#include "sshdirecttcpiptunnel.h" +#include "sshdirecttcpiptunnel_p.h" +#include "sshforwardedtcpiptunnel.h" +#include "sshforwardedtcpiptunnel_p.h" #include "sshincomingpacket_p.h" +#include "sshlogging_p.h" #include "sshremoteprocess.h" #include "sshremoteprocess_p.h" #include "sshsendfacility_p.h" +#include "sshtcpipforwardserver.h" +#include "sshtcpipforwardserver_p.h" +#include "sshx11channel_p.h" +#include "sshx11inforetriever_p.h" #include @@ -54,10 +63,23 @@ void SshChannelManager::handleChannelRequest(const SshIncomingPacket &packet) ->handleChannelRequest(packet); } -void SshChannelManager::handleChannelOpen(const SshIncomingPacket &) +void SshChannelManager::handleChannelOpen(const SshIncomingPacket &packet) { - throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, - "Server tried to open channel on client."); + const SshChannelOpenGeneric channelOpen = packet.extractChannelOpen(); + if (channelOpen.channelType == SshIncomingPacket::ForwardedTcpIpType) { + handleChannelOpenForwardedTcpIp(channelOpen); + return; + } + if (channelOpen.channelType == "x11") { + handleChannelOpenX11(channelOpen); + return; + } + try { + m_sendFacility.sendChannelOpenFailurePacket(channelOpen.commonData.remoteChannel, + SSH_OPEN_UNKNOWN_CHANNEL_TYPE, QByteArray()); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); + } } void SshChannelManager::handleChannelOpenFailure(const SshIncomingPacket &packet) @@ -66,7 +88,7 @@ void SshChannelManager::handleChannelOpenFailure(const SshIncomingPacket &packet ChannelIterator it = lookupChannelAsIterator(failure.localChannel); try { it.value()->handleOpenFailure(failure.reasonString); - } catch (SshServerException &e) { + } catch (const SshServerException &e) { removeChannel(it); throw e; } @@ -128,6 +150,39 @@ void SshChannelManager::handleChannelClose(const SshIncomingPacket &packet) } } +void SshChannelManager::handleRequestSuccess(const SshIncomingPacket &packet) +{ + if (m_waitingForwardServers.isEmpty()) { + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, + "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); + } else { + QSSH_ASSERT(false); + } +} + +void SshChannelManager::handleRequestFailure(const SshIncomingPacket &packet) +{ + Q_UNUSED(packet); + if (m_waitingForwardServers.isEmpty()) { + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, + "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) { @@ -151,6 +206,43 @@ QSsh::SshRemoteProcess::Ptr SshChannelManager::createRemoteProcess(const QByteAr { 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(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())); + return; + } + if (!m_x11DisplayInfo.cookie.isEmpty()) + proc->startProcess(m_x11DisplayInfo); + else + m_x11ForwardingRequests << proc; + return; + } + m_x11DisplayInfo.displayName = displayName; + m_x11ForwardingRequests << proc; + 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; + for (SshRemoteProcessPrivate * const proc : qAsConst(m_x11ForwardingRequests)) + proc->startProcess(displayInfo); + m_x11ForwardingRequests.clear(); + }; + connect(x11InfoRetriever, &SshX11InfoRetriever::success, this, successHandler); + qCDebug(sshLog) << "starting x11 info retriever"; + x11InfoRetriever->start(); + }); return proc; } @@ -168,19 +260,127 @@ QSsh::SftpChannel::Ptr SshChannelManager::createSftpChannel() 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); + // fall through + case SshTcpIpForwardServer::Initializing: + m_waitingForwardServers.append(server); + break; + case SshTcpIpForwardServer::Listening: + case SshTcpIpForwardServer::Inactive: + break; + } + }); + return server; +} + void SshChannelManager::insertChannel(AbstractSshChannel *priv, const QSharedPointer &pub) { - connect(priv, SIGNAL(timeout()), this, SIGNAL(timeout())); + 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; + + foreach (const SshTcpIpForwardServer::Ptr &candidate, m_listeningForwardServers) { + if (candidate->port() == channelOpen.remotePort + && candidate->bindAddress().toUtf8() == channelOpen.remoteAddress) { + server = candidate; + break; + } + }; + + + 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. + foreach (const SshTcpIpForwardServer::Ptr &candidate, m_listeningForwardServers) { + if (candidate->port() == channelOpen.remotePort) { + server = candidate; + break; + } + }; + } + + if (server.isNull()) { + try { + m_sendFacility.sendChannelOpenFailurePacket(channelOpen.common.remoteChannel, + SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, + QByteArray()); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); + } + return; + } + + SshForwardedTcpIpTunnel::Ptr tunnel(new SshForwardedTcpIpTunnel(m_nextLocalChannelId++, + m_sendFacility)); + tunnel->d->handleOpenSuccess(channelOpen.common.remoteChannel, + channelOpen.common.remoteWindowSize, + channelOpen.common.remoteMaxPacketSize); + tunnel->open(QIODevice::ReadWrite); + server->setNewConnection(tunnel); + insertChannel(tunnel->d, 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()) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "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, + channelOpen.common.remoteWindowSize, + channelOpen.common.remoteMaxPacketSize); + insertChannel(x11Channel, QSharedPointer()); +} + int SshChannelManager::closeAllChannels(CloseAllMode mode) { - const int count = m_channels.count(); - for (ChannelIterator it = m_channels.begin(); it != m_channels.end(); ++it) - it.value()->closeChannel(); + 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(); @@ -195,9 +395,16 @@ int SshChannelManager::channelCount() const void SshChannelManager::removeChannel(ChannelIterator it) { - Q_ASSERT(it != m_channels.end() && "Unexpected channel lookup failure."); + if (it == m_channels.end()) { + throw SshClientException(SshInternalError, + QLatin1String("Internal error: Unexpected channel lookup failure")); + } const int removeCount = m_sessions.remove(it.value()); - Q_ASSERT(removeCount == 1 && "Session for channel not found."); + if (removeCount != 1) { + throw SshClientException(SshInternalError, + QString::fromLatin1("Internal error: Unexpected session count %1 for channel.") + .arg(removeCount)); + } m_channels.erase(it); } diff --git a/src/libs/ssh/sshchannelmanager_p.h b/src/libs/ssh/sshchannelmanager_p.h index 16626ed..dcd5183 100644 --- a/src/libs/ssh/sshchannelmanager_p.h +++ b/src/libs/ssh/sshchannelmanager_p.h @@ -31,20 +31,25 @@ #ifndef SSHCHANNELLAYER_P_H #define SSHCHANNELLAYER_P_H +#include "sshx11displayinfo_p.h" + #include #include #include namespace QSsh { - class SftpChannel; +class SshDirectTcpIpTunnel; class SshRemoteProcess; +class SshTcpIpForwardServer; namespace Internal { class AbstractSshChannel; +struct SshChannelOpenGeneric; class SshIncomingPacket; class SshSendFacility; +class SshRemoteProcessPrivate; class SshChannelManager : public QObject { @@ -55,10 +60,15 @@ class SshChannelManager : public QObject QSharedPointer createRemoteProcess(const QByteArray &command); QSharedPointer createRemoteShell(); QSharedPointer createSftpChannel(); - int channelCount() const; + QSharedPointer createDirectTunnel(const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort); + QSharedPointer 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 handleChannelRequest(const SshIncomingPacket &packet); void handleChannelOpen(const SshIncomingPacket &packet); @@ -71,6 +81,8 @@ class SshChannelManager : public QObject 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); signals: void timeout(); @@ -86,10 +98,17 @@ class SshChannelManager : public QObject void insertChannel(AbstractSshChannel *priv, const QSharedPointer &pub); + void handleChannelOpenForwardedTcpIp(const SshChannelOpenGeneric &channelOpenGeneric); + void handleChannelOpenX11(const SshChannelOpenGeneric &channelOpenGeneric); + SshSendFacility &m_sendFacility; QHash m_channels; QHash > m_sessions; quint32 m_nextLocalChannelId; + QList> m_waitingForwardServers; + QList> m_listeningForwardServers; + QList m_x11ForwardingRequests; + X11DisplayInfo m_x11DisplayInfo; }; } // namespace Internal diff --git a/src/libs/ssh/sshconnection.cpp b/src/libs/ssh/sshconnection.cpp index 99cea7c..6546a6d 100644 --- a/src/libs/ssh/sshconnection.cpp +++ b/src/libs/ssh/sshconnection.cpp @@ -32,11 +32,17 @@ #include "sshconnection_p.h" #include "sftpchannel.h" +#include "sshagent_p.h" #include "sshcapabilities_p.h" #include "sshchannelmanager_p.h" #include "sshcryptofacility_p.h" +#include "sshdirecttcpiptunnel.h" +#include "sshtcpipforwardserver.h" #include "sshexception_p.h" +#include "sshinit_p.h" #include "sshkeyexchange_p.h" +#include "sshremoteprocess.h" +#include "sshlogging_p.h" #include @@ -47,50 +53,27 @@ #include #include -/*! - \class QSsh::SshConnection - - \brief This class provides an SSH connection, implementing protocol version 2.0 - - It can spawn channels for remote execution and SFTP operations (version 3). - It operates asynchronously (non-blocking) and is not thread-safe. -*/ - namespace QSsh { namespace { - const QByteArray ClientId("SSH-2.0-QtCreator\r\n"); - - bool staticInitializationsDone = false; - QMutex staticInitMutex; - - void doStaticInitializationsIfNecessary() - { - QMutexLocker locker(&staticInitMutex); - if (!staticInitializationsDone) { - Botan::LibraryInitializer::initialize("thread_safe=true"); - qRegisterMetaType("QSsh::SshError"); - qRegisterMetaType("QSsh::SftpJobId"); - qRegisterMetaType("QSsh::SftpFileInfo"); - qRegisterMetaType >("QList"); - staticInitializationsDone = true; - } - } -} // anonymous namespace - +const QByteArray ClientId("SSH-2.0-QtCreator\r\n"); +} SshConnectionParameters::SshConnectionParameters() : - timeout(0), authenticationType(AuthenticationByKey), port(0), proxyType(NoProxy) + timeout(0), authenticationType(AuthenticationTypePublicKey), + hostKeyCheckingMode(SshHostKeyCheckingNone) { + url.setPort(0); + options |= SshIgnoreDefaultProxy; } static inline bool equals(const SshConnectionParameters &p1, const SshConnectionParameters &p2) { - return p1.host == p2.host && p1.userName == p2.userName + return p1.url == p2.url && p1.authenticationType == p2.authenticationType - && (p1.authenticationType == SshConnectionParameters::AuthenticationByPassword ? - p1.password == p2.password : p1.privateKeyFile == p2.privateKeyFile) - && p1.timeout == p2.timeout && p1.port == p2.port; + && p1.privateKeyFile == p2.privateKeyFile + && p1.hostKeyCheckingMode == p2.hostKeyCheckingMode + && p1.timeout == p2.timeout; } bool operator==(const SshConnectionParameters &p1, const SshConnectionParameters &p2) @@ -103,22 +86,26 @@ bool operator!=(const SshConnectionParameters &p1, const SshConnectionParameters return !equals(p1, p2); } -// TODO: Mechanism for checking the host key. First connection to host: save, later: compare - SshConnection::SshConnection(const SshConnectionParameters &serverInfo, QObject *parent) : QObject(parent) { - doStaticInitializationsIfNecessary(); + Internal::initSsh(); + qRegisterMetaType("QSsh::SshError"); + qRegisterMetaType("QSsh::SftpJobId"); + qRegisterMetaType("QSsh::SftpFileInfo"); + qRegisterMetaType("QSsh::SftpError"); + qRegisterMetaType("SftpError"); + qRegisterMetaType >("QList"); d = new Internal::SshConnectionPrivate(this, serverInfo); - connect(d, SIGNAL(connected()), this, SIGNAL(connected()), + connect(d, &Internal::SshConnectionPrivate::connected, this, &SshConnection::connected, Qt::QueuedConnection); - connect(d, SIGNAL(dataAvailable(QString)), this, - SIGNAL(dataAvailable(QString)), Qt::QueuedConnection); - connect(d, SIGNAL(disconnected()), this, SIGNAL(disconnected()), + connect(d, &Internal::SshConnectionPrivate::dataAvailable, this, + &SshConnection::dataAvailable, Qt::QueuedConnection); + connect(d, &Internal::SshConnectionPrivate::disconnected, this, &SshConnection::disconnected, Qt::QueuedConnection); - connect(d, SIGNAL(error(QSsh::SshError)), this, - SIGNAL(error(QSsh::SshError)), Qt::QueuedConnection); + connect(d, &Internal::SshConnectionPrivate::error, this, + &SshConnection::error, Qt::QueuedConnection); } void SshConnection::connectToHost() @@ -146,7 +133,7 @@ SshConnection::State SshConnection::state() const SshError SshConnection::errorState() const { - return d->error(); + return d->errorState(); } QString SshConnection::errorString() const @@ -192,12 +179,26 @@ QSharedPointer SshConnection::createSftpChannel() return d->createSftpChannel(); } +SshDirectTcpIpTunnel::Ptr SshConnection::createDirectTunnel(const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort) +{ + QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, SshDirectTcpIpTunnel::Ptr()); + return d->createDirectTunnel(originatingHost, originatingPort, remoteHost, remotePort); +} + +QSharedPointer SshConnection::createForwardServer(const QString &remoteHost, + quint16 remotePort) +{ + QSSH_ASSERT_AND_RETURN_VALUE(state() == Connected, SshTcpIpForwardServer::Ptr()); + return d->createForwardServer(remoteHost, remotePort); +} + int SshConnection::closeAllChannels() { try { return d->m_channelManager->closeAllChannels(Internal::SshChannelManager::CloseAllRegular); - } catch (const Botan::Exception &e) { - qDebug("%s: %s", Q_FUNC_INFO, e.what()); + } catch (const std::exception &e) { + qCWarning(Internal::sshLog, "%s: %s", Q_FUNC_INFO, e.what()); return -1; } } @@ -207,6 +208,11 @@ int SshConnection::channelCount() const return d->m_channelManager->channelCount(); } +QString SshConnection::x11DisplayName() const +{ + return d->m_channelManager->x11DisplayName(); +} + namespace Internal { SshConnectionPrivate::SshConnectionPrivate(SshConnection *conn, @@ -218,13 +224,14 @@ SshConnectionPrivate::SshConnectionPrivate(SshConnection *conn, m_conn(conn) { setupPacketHandlers(); - m_socket->setProxy(m_connParams.proxyType == SshConnectionParameters::DefaultProxy - ? QNetworkProxy::DefaultProxy : QNetworkProxy::NoProxy); + m_socket->setProxy((m_connParams.options & SshIgnoreDefaultProxy) + ? QNetworkProxy::NoProxy : QNetworkProxy::DefaultProxy); m_timeoutTimer.setSingleShot(true); m_timeoutTimer.setInterval(m_connParams.timeout * 1000); m_keepAliveTimer.setSingleShot(true); m_keepAliveTimer.setInterval(10000); - connect(m_channelManager, SIGNAL(timeout()), this, SLOT(handleTimeout())); + connect(m_channelManager, &SshChannelManager::timeout, + this, &SshConnectionPrivate::handleTimeout); } SshConnectionPrivate::~SshConnectionPrivate() @@ -246,8 +253,11 @@ void SshConnectionPrivate::setupPacketHandlers() setupPacketHandler(SSH_MSG_SERVICE_ACCEPT, StateList() << UserAuthServiceRequested, &This::handleServiceAcceptPacket); - setupPacketHandler(SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, - StateList() << UserAuthRequested, &This::handlePasswordExpiredPacket); + 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); @@ -258,6 +268,12 @@ void SshConnectionPrivate::setupPacketHandlers() &This::handleUserAuthSuccessPacket); setupPacketHandler(SSH_MSG_USERAUTH_FAILURE, authReqList, &This::handleUserAuthFailurePacket); + if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeKeyboardInteractive + || m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods) { + setupPacketHandler(SSH_MSG_USERAUTH_INFO_REQUEST, authReqList, + &This::handleUserAuthInfoRequestPacket); + } + setupPacketHandler(SSH_MSG_USERAUTH_PK_OK, authReqList, &This::handleUserAuthKeyOkPacket); const StateList connectedList = StateList() << ConnectionEstablished; @@ -287,12 +303,17 @@ void SshConnectionPrivate::setupPacketHandlers() setupPacketHandler(SSH_MSG_CHANNEL_CLOSE, connectedOrClosedList, &This::handleChannelClose); - setupPacketHandler(SSH_MSG_DISCONNECT, StateList() << SocketConnected + 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, @@ -317,32 +338,27 @@ void SshConnectionPrivate::handleIncomingData() if (!canUseSocket()) return; m_incomingData += m_socket->readAll(); -#ifdef CREATOR_SSH_DEBUG - qDebug("state = %d, remote data size = %d", m_state, - m_incomingData.count()); -#endif + qCDebug(sshLog, "state = %d, remote data size = %d", m_state, m_incomingData.count()); if (m_serverId.isEmpty()) handleServerId(); handlePackets(); - } catch (SshServerException &e) { + } catch (const SshServerException &e) { closeConnection(e.error, SshProtocolError, e.errorStringServer, tr("SSH Protocol error: %1").arg(e.errorStringUser)); - } catch (SshClientException &e) { + } catch (const SshClientException &e) { closeConnection(SSH_DISCONNECT_BY_APPLICATION, e.error, "", e.errorString); - } catch (Botan::Exception &e) { + } catch (const std::exception &e) { closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshInternalError, "", - tr("Botan library exception: %1").arg(QString::fromAscii(e.what()))); + tr("Botan library exception: %1").arg(QString::fromLocal8Bit(e.what()))); } } // RFC 4253, 4.2. void SshConnectionPrivate::handleServerId() { -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: incoming data size = %d, incoming data = '%s'", + qCDebug(sshLog, "%s: incoming data size = %d, incoming data = '%s'", Q_FUNC_INFO, m_incomingData.count(), m_incomingData.data()); -#endif const int newLinePos = m_incomingData.indexOf('\n'); if (newLinePos == -1) return; // Not enough data yet. @@ -380,32 +396,40 @@ void SshConnectionPrivate::handleServerId() if (!versionIdpattern.exactMatch(QString::fromLatin1(m_serverId))) { throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Identification string is invalid.", - tr("Server Identification string '%1' is invalid.") + tr("Server Identification string \"%1\" is invalid.") .arg(QString::fromLatin1(m_serverId))); } const QString serverProtoVersion = versionIdpattern.cap(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.") + tr("Server protocol version is \"%1\", but needs to be 2.0 or 1.99.") .arg(serverProtoVersion)); } - // Disable this check to accept older OpenSSH servers that do this wrong. if (serverProtoVersion == QLatin1String("2.0") && !hasCarriageReturn) { - throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, - "Identification string is invalid.", - tr("Server identification string is invalid (missing carriage return).")); + if (m_connParams.options & SshEnableStrictConformanceChecks) { + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, + "Identification string is invalid.", + tr("Server identification string is invalid (missing carriage return).")); + } else { + qCWarning(Internal::sshLog, "Server identification string is invalid (missing carriage return)."); + } } if (serverProtoVersion == QLatin1String("1.99") && m_serverHasSentDataBeforeId) { - throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, - "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.")); + if (m_connParams.options & SshEnableStrictConformanceChecks) { + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, + "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.")); + } else { + 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_sendFacility)); + m_keyExchange.reset(new SshKeyExchange(m_connParams, m_sendFacility)); m_keyExchange->sendKexInitPacket(m_serverId); m_keyExchangeState = KexInitSent; } @@ -431,15 +455,14 @@ void SshConnectionPrivate::handleCurrentPacket() } QHash::ConstIterator it - = m_packetHandlers.find(m_incomingPacket.type()); - if (it == m_packetHandlers.end()) { + = m_packetHandlers.constFind(m_incomingPacket.type()); + if (it == m_packetHandlers.constEnd()) { m_sendFacility.sendMsgUnimplementedPacket(m_incomingPacket.serverSeqNr()); return; } if (!it.value().first.contains(m_state)) { - throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, - "Unexpected packet.", tr("Unexpected packet of type %1.") - .arg(m_incomingPacket.type())); + handleUnexpectedPacket(); + return; } (this->*it.value().second)(); } @@ -455,7 +478,7 @@ void SshConnectionPrivate::handleKeyExchangeInitPacket() // Server-initiated re-exchange. if (m_keyExchangeState == NoKeyExchange) { - m_keyExchange.reset(new SshKeyExchange(m_sendFacility)); + m_keyExchange.reset(new SshKeyExchange(m_connParams, m_sendFacility)); m_keyExchange->sendKexInitPacket(m_serverId); } @@ -503,31 +526,81 @@ void SshConnectionPrivate::handleNewKeysPacket() void SshConnectionPrivate::handleServiceAcceptPacket() { - if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationByPassword) { - m_sendFacility.sendUserAuthByPwdRequestPacket(m_connParams.userName.toUtf8(), - SshCapabilities::SshConnectionService, m_connParams.password.toUtf8()); - } else { - m_sendFacility.sendUserAuthByKeyRequestPacket(m_connParams.userName.toUtf8(), - SshCapabilities::SshConnectionService); + switch (m_connParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods: + m_triedAllPasswordBasedMethods = false; + // Fall-through. + case SshConnectionParameters::AuthenticationTypePassword: + m_sendFacility.sendUserAuthByPasswordRequestPacket(m_connParams.userName().toUtf8(), + SshCapabilities::SshConnectionService, m_connParams.password().toUtf8()); + break; + case SshConnectionParameters::AuthenticationTypeKeyboardInteractive: + m_sendFacility.sendUserAuthByKeyboardInteractiveRequestPacket(m_connParams.userName().toUtf8(), + SshCapabilities::SshConnectionService); + break; + case SshConnectionParameters::AuthenticationTypePublicKey: + authenticateWithPublicKey(); + break; + 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; + return; + } else { + tryAllAgentKeys(); + } + break; } m_state = UserAuthRequested; } void SshConnectionPrivate::handlePasswordExpiredPacket() { - if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationByKey) { - throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, - "Got SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, but did not use password."); + 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. + handleUnexpectedPacket(); + return; } - throw SshClientException(SshAuthenticationError, tr("Password expired.")); } +void SshConnectionPrivate::handleUserAuthInfoRequestPacket() +{ + if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + && !m_triedAllPasswordBasedMethods) { + // This means we just tried to authorize via "password", in which case + // this type of packet is not allowed. + handleUnexpectedPacket(); + return; + } + + 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() +{ + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, + "Unexpected packet.", tr("Unexpected packet of type %1.") + .arg(m_incomingPacket.type())); +} + void SshConnectionPrivate::handleGlobalRequest() { m_sendFacility.sendRequestFailurePacket(); @@ -539,17 +612,75 @@ void SshConnectionPrivate::handleUserAuthSuccessPacket() m_timeoutTimer.stop(); emit connected(); m_lastInvalidMsgSeqNr = InvalidSeqNr; - connect(&m_keepAliveTimer, SIGNAL(timeout()), SLOT(sendKeepAlivePacket())); + 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.")); + } + return; + } + + // TODO: Evaluate "authentications that can continue" field and act on it. + if (m_connParams.authenticationType + == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + && !m_triedAllPasswordBasedMethods) { + m_triedAllPasswordBasedMethods = true; + m_sendFacility.sendUserAuthByKeyboardInteractiveRequestPacket( + m_connParams.userName().toUtf8(), + SshCapabilities::SshConnectionService); + return; + } + m_timeoutTimer.stop(); - const QString errorMsg = m_connParams.authenticationType == SshConnectionParameters::AuthenticationByPassword - ? tr("Server rejected password.") : tr("Server rejected key."); + QString errorMsg; + switch (m_connParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypePublicKey: + case SshConnectionParameters::AuthenticationTypeAgent: + errorMsg = tr("Server rejected key."); + break; + default: + errorMsg = tr("Server rejected password."); + break; + } 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.")); + } + const QByteArray key = m_pendingKeyChecks.dequeue(); + 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); + return; + } + m_agentKeyToUse = key; + qCDebug(sshLog) << "requesting signature from agent"; + SshAgent::requestSignature(key, token); +} + void SshConnectionPrivate::handleDebugPacket() { const SshDebug &msg = m_incomingPacket.extractDebug(); @@ -632,7 +763,15 @@ void SshConnectionPrivate::handleDisconnect() "", 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) { @@ -640,6 +779,11 @@ void SshConnectionPrivate::sendData(const QByteArray &data) m_socket->write(data); } +uint SshConnectionPrivate::tokenForAgent() const +{ + return qHash(m_sendFacility.sessionId()); +} + void SshConnectionPrivate::handleSocketDisconnected() { closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshClosedByServerError, @@ -657,8 +801,10 @@ void SshConnectionPrivate::handleSocketError() void SshConnectionPrivate::handleTimeout() { - closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshTimeoutError, "", - tr("Timeout waiting for reply from server.")); + 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() @@ -675,6 +821,66 @@ void SshConnectionPrivate::sendKeepAlivePacket() m_timeoutTimer.start(); } +void SshConnectionPrivate::handleAgentKeysUpdated() +{ + m_agentKeysUpToDate = true; + if (m_state == WaitingForAgentKeys) { + m_state = UserAuthRequested; + tryAllAgentKeys(); + } +} + +void SshConnectionPrivate::handleSignatureFromAgent(const QByteArray &key, + const QByteArray &signature, uint token) +{ + if (token != tokenForAgent()) { + qCDebug(sshLog) << "signature is for different connection, ignoring"; + return; + } + QSSH_ASSERT(key == m_agentKeyToUse); + m_agentSignature = signature; + authenticateWithPublicKey(); +} + +void SshConnectionPrivate::tryAllAgentKeys() +{ + const QList &keys = SshAgent::publicKeys(); + if (keys.isEmpty()) + throw SshClientException(SshAuthenticationError, tr("ssh-agent has no keys.")); + qCDebug(sshLog) << "trying authentication with" << keys.count() + << "public keys received from agent"; + foreach (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 key; + QByteArray signature; + if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeAgent) { + // Agent is not needed anymore after this point. + disconnect(&SshAgent::instance(), 0, this, 0); + + 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); @@ -687,28 +893,53 @@ void SshConnectionPrivate::connectToHost() m_errorString.clear(); m_serverId.clear(); m_serverHasSentDataBeforeId = false; - - try { - if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationByKey) + m_agentSignature.clear(); + m_agentKeysUpToDate = false; + m_pendingKeyChecks.clear(); + m_agentKeyToUse.clear(); + + switch (m_connParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypePublicKey: + try { createPrivateKey(); - } catch (const SshClientException &ex) { - m_error = ex.error; - m_errorString = ex.errorString; - emit error(m_error); - return; + break; + } catch (const SshClientException &ex) { + m_error = ex.error; + m_errorString = ex.errorString; + emit error(m_error); + return; + } + case SshConnectionParameters::AuthenticationTypeAgent: + if (SshAgent::hasError()) { + setAgentError(); + return; + } + 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); + break; + default: + break; } - connect(m_socket, SIGNAL(connected()), this, SLOT(handleSocketConnected())); - connect(m_socket, SIGNAL(readyRead()), this, SLOT(handleIncomingData())); - connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, - SLOT(handleSocketError())); - connect(m_socket, SIGNAL(disconnected()), this, - SLOT(handleSocketDisconnected())); - connect(&m_timeoutTimer, SIGNAL(timeout()), this, SLOT(handleTimeout())); + connect(m_socket, &QAbstractSocket::connected, + this, &SshConnectionPrivate::handleSocketConnected); + connect(m_socket, &QIODevice::readyRead, + this, &SshConnectionPrivate::handleIncomingData); + connect(m_socket, + static_cast(&QAbstractSocket::error), + this, &SshConnectionPrivate::handleSocketError); + connect(m_socket, &QAbstractSocket::disconnected, + this, &SshConnectionPrivate::handleSocketDisconnected); + connect(&m_timeoutTimer, &QTimer::timeout, this, &SshConnectionPrivate::handleTimeout); m_state = SocketConnecting; m_keyExchangeState = NoKeyExchange; m_timeoutTimer.start(); - m_socket->connectToHost(m_connParams.host, m_connParams.port); + m_socket->connectToHost(m_connParams.host(), m_connParams.port()); } void SshConnectionPrivate::closeConnection(SshErrorCode sshError, @@ -729,7 +960,7 @@ void SshConnectionPrivate::closeConnection(SshErrorCode sshError, try { m_channelManager->closeAllChannels(SshChannelManager::CloseAllAndReset); m_sendFacility.sendDisconnectPacket(sshError, serverErrorString); - } catch (Botan::Exception &) {} // Nothing sensible to be done here. + } catch (...) {} // Nothing sensible to be done here. if (m_error != SshNoError) emit error(userError); if (m_state == ConnectionEstablished) @@ -772,6 +1003,19 @@ QSharedPointer SshConnectionPrivate::createSftpChannel() return m_channelManager->createSftpChannel(); } +SshDirectTcpIpTunnel::Ptr SshConnectionPrivate::createDirectTunnel(const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort) +{ + 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(-1); } // namespace Internal diff --git a/src/libs/ssh/sshconnection.h b/src/libs/ssh/sshconnection.h index 66263c3..fd53c57 100644 --- a/src/libs/ssh/sshconnection.h +++ b/src/libs/ssh/sshconnection.h @@ -32,43 +32,150 @@ #define SSHCONNECTION_H #include "ssherrors.h" +#include "sshhostkeydatabase.h" #include "ssh_global.h" #include +#include +#include #include #include #include #include +#include namespace QSsh { class SftpChannel; +class SshDirectTcpIpTunnel; class SshRemoteProcess; +class SshTcpIpForwardServer; namespace Internal { class SshConnectionPrivate; } // namespace Internal +/*! + * \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 +}; + +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 { public: - enum ProxyType { DefaultProxy, NoProxy }; - enum AuthenticationType { AuthenticationByPassword, AuthenticationByKey }; + + /*! + * \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(); - QString host; - QString userName; - QString password; + /*! + * \brief Returns the hostname or IP set with setHost() + */ + QString host() const { return url.host(); } + + /*! + * \brief Returns the port set with setPort() + */ + quint16 port() const { return url.port(); } + + /*! + * \brief Returns the username set with setUsername() + * \return + */ + 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; - quint16 port; - ProxyType proxyType; + 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 { public: @@ -82,37 +189,99 @@ class QSSH_EXPORT SshConnectionInfo 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 { Q_OBJECT public: + /*! + * \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 = 0); void connectToHost(); void disconnectFromHost(); + + /*! + * \brief Current state of this connection + */ State state() const; + + /*! + * \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 + */ QSharedPointer createRemoteProcess(const QByteArray &command); + + /*! + * \brief Creates a remote interactive session with a shell + */ QSharedPointer createRemoteShell(); QSharedPointer createSftpChannel(); + QSharedPointer createDirectTunnel(const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort); + QSharedPointer createForwardServer(const QString &remoteHost, + quint16 remotePort); // -1 if an error occurred, number of channels closed otherwise. int closeAllChannels(); int channelCount() const; + /*! + * \brief The X11 display name used for X11 forwarding + * \return The name of the X11 display set for this connection + */ + QString x11DisplayName() const; + signals: + /*! + * \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); private: @@ -121,4 +290,6 @@ class QSSH_EXPORT SshConnection : public QObject } // namespace QSsh +Q_DECLARE_METATYPE(QSsh::SshConnectionParameters::AuthenticationType) + #endif // SSHCONNECTION_H diff --git a/src/libs/ssh/sshconnection_p.h b/src/libs/ssh/sshconnection_p.h index 797d736..e329f7f 100644 --- a/src/libs/ssh/sshconnection_p.h +++ b/src/libs/ssh/sshconnection_p.h @@ -34,11 +34,11 @@ #include "sshconnection.h" #include "sshexception_p.h" #include "sshincomingpacket_p.h" -#include "sshremoteprocess.h" #include "sshsendfacility_p.h" #include #include +#include #include #include #include @@ -50,6 +50,9 @@ QT_END_NAMESPACE namespace QSsh { class SftpChannel; +class SshRemoteProcess; +class SshDirectTcpIpTunnel; +class SshTcpIpForwardServer; namespace Internal { class SshChannelManager; @@ -60,6 +63,7 @@ enum SshStateInternal { SocketConnecting, // After connectToHost() SocketConnected, // After socket's connected() signal UserAuthServiceRequested, + WaitingForAgentKeys, UserAuthRequested, ConnectionEstablished // After service has been started // ... @@ -88,8 +92,13 @@ class SshConnectionPrivate : public QObject QSharedPointer createRemoteProcess(const QByteArray &command); QSharedPointer createRemoteShell(); QSharedPointer createSftpChannel(); + QSharedPointer createDirectTunnel(const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort); + QSharedPointer createForwardServer(const QString &remoteHost, + quint16 remotePort); + SshStateInternal state() const { return m_state; } - SshError error() const { return m_error; } + SshError errorState() const { return m_error; } QString errorString() const { return m_errorString; } signals: @@ -99,12 +108,18 @@ class SshConnectionPrivate : public QObject void error(QSsh::SshError); private: - Q_SLOT void handleSocketConnected(); - Q_SLOT void handleIncomingData(); - Q_SLOT void handleSocketError(); - Q_SLOT void handleSocketDisconnected(); - Q_SLOT void handleTimeout(); - Q_SLOT void sendKeepAlivePacket(); + void handleSocketConnected(); + void handleIncomingData(); + void handleSocketError(); + 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(); @@ -114,9 +129,12 @@ class SshConnectionPrivate : public QObject void handleNewKeysPacket(); void handleServiceAcceptPacket(); void handlePasswordExpiredPacket(); + void handleUserAuthInfoRequestPacket(); void handleUserAuthSuccessPacket(); void handleUserAuthFailurePacket(); + void handleUserAuthKeyOkPacket(); void handleUserAuthBannerPacket(); + void handleUnexpectedPacket(); void handleGlobalRequest(); void handleDebugPacket(); void handleUnimplementedPacket(); @@ -132,11 +150,16 @@ class SshConnectionPrivate : public QObject void handleChannelEof(); void handleChannelClose(); void handleDisconnect(); + void handleRequestSuccess(); + void handleRequestFailure(); + bool canUseSocket() const; void createPrivateKey(); void sendData(const QByteArray &data); + uint tokenForAgent() const; + typedef void (SshConnectionPrivate::*PacketHandler)(); typedef QList StateList; void setupPacketHandlers(); @@ -165,7 +188,12 @@ class SshConnectionPrivate : public QObject SshConnection *m_conn; quint64 m_lastInvalidMsgSeqNr; QByteArray m_serverId; + QByteArray m_agentSignature; + QQueue m_pendingKeyChecks; + QByteArray m_agentKeyToUse; bool m_serverHasSentDataBeforeId; + bool m_triedAllPasswordBasedMethods; + bool m_agentKeysUpToDate; }; } // namespace Internal diff --git a/src/libs/ssh/sshconnectionmanager.cpp b/src/libs/ssh/sshconnectionmanager.cpp index 04aa2ef..b13b100 100644 --- a/src/libs/ssh/sshconnectionmanager.cpp +++ b/src/libs/ssh/sshconnectionmanager.cpp @@ -38,33 +38,42 @@ #include #include #include +#include namespace QSsh { namespace Internal { +class UnaquiredConnection { +public: + UnaquiredConnection(SshConnection *conn) : connection(conn), scheduledForRemoval(false) {} + + SshConnection *connection; + bool scheduledForRemoval; +}; +bool operator==(const UnaquiredConnection &c1, const UnaquiredConnection &c2) { + return c1.connection == c2.connection; +} +bool operator!=(const UnaquiredConnection &c1, const UnaquiredConnection &c2) { + return !(c1 == c2); +} -class SshConnectionManagerPrivate : public QObject +class SshConnectionManager : public QObject { Q_OBJECT public: - - static QMutex instanceMutex; - static SshConnectionManager &instance() - { - static SshConnectionManager manager; - return manager; - } - - SshConnectionManagerPrivate() + SshConnectionManager() { moveToThread(QCoreApplication::instance()->thread()); + connect(&m_removalTimer, &QTimer::timeout, + this, &SshConnectionManager::removeInactiveConnections); + m_removalTimer.start(150000); // For a total timeout of five minutes. } - ~SshConnectionManagerPrivate() + ~SshConnectionManager() { - foreach (SshConnection * const connection, m_unacquiredConnections) { - disconnect(connection, 0, this, 0); - delete connection; + foreach (const UnaquiredConnection &connection, m_unacquiredConnections) { + disconnect(connection.connection, 0, this, 0); + delete connection.connection; } QSSH_ASSERT(m_acquiredConnections.isEmpty()); @@ -81,17 +90,18 @@ class SshConnectionManagerPrivate : public QObject continue; if (connection->thread() != QThread::currentThread()) - break; + continue; if (m_deprecatedConnections.contains(connection)) // we were asked to no longer use this one... - break; + continue; m_acquiredConnections.append(connection); return connection; } - // Checked cached open connections: - foreach (SshConnection * const connection, m_unacquiredConnections) { + // Check cached open connections: + foreach (const UnaquiredConnection &c, m_unacquiredConnections) { + SshConnection * const connection = c.connection; if (connection->state() != SshConnection::Connected || connection->connectionParameters() != sshParams) continue; @@ -105,14 +115,15 @@ class SshConnectionManagerPrivate : public QObject Q_ARG(QObject *, QThread::currentThread())); } - m_unacquiredConnections.removeOne(connection); + m_unacquiredConnections.removeOne(c); m_acquiredConnections.append(connection); return connection; } // create a new connection: SshConnection * const connection = new SshConnection(sshParams); - connect(connection, SIGNAL(disconnected()), this, SLOT(cleanup())); + connect(connection, &SshConnection::disconnected, + this, &SshConnectionManager::cleanup); m_acquiredConnections.append(connection); return connection; @@ -133,24 +144,21 @@ class SshConnectionManagerPrivate : public QObject || connection->state() != SshConnection::Connected) { doDelete = true; } else { - QSSH_ASSERT_AND_RETURN(!m_unacquiredConnections.contains(connection)); + 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; - foreach (SshConnection * const conn, m_unacquiredConnections) { - if (conn->connectionParameters() == connection->connectionParameters()) { + foreach (const UnaquiredConnection &c, m_unacquiredConnections) { + if (c.connection->connectionParameters() == connection->connectionParameters()) { haveConnection = true; break; } } if (!haveConnection) { - // Let's nag clients who release connections with open channels. - const int channelCount = connection->closeAllChannels(); - QSSH_ASSERT(channelCount == 0); - - m_unacquiredConnections.append(connection); + connection->closeAllChannels(); // Clean up after neglectful clients. + m_unacquiredConnections.append(UnaquiredConnection(connection)); } else { doDelete = true; } @@ -168,7 +176,7 @@ class SshConnectionManagerPrivate : public QObject QMutexLocker locker(&m_listMutex); for (int i = 0; i < m_unacquiredConnections.count(); ++i) { - SshConnection * const connection = m_unacquiredConnections.at(i); + SshConnection * const connection = m_unacquiredConnections.at(i).connection; if (connection->connectionParameters() == sshParams) { disconnect(connection, 0, this, 0); delete connection; @@ -191,7 +199,6 @@ class SshConnectionManagerPrivate : public QObject connection->moveToThread(qobject_cast(threadObj)); } -private slots: void cleanup() { QMutexLocker locker(&m_listMutex); @@ -200,57 +207,67 @@ private slots: if (!currentConnection) return; - if (m_unacquiredConnections.removeOne(currentConnection)) { + if (m_unacquiredConnections.removeOne(UnaquiredConnection(currentConnection))) { disconnect(currentConnection, 0, this, 0); currentConnection->deleteLater(); } } + void removeInactiveConnections() + { + QMutexLocker locker(&m_listMutex); + for (int i = m_unacquiredConnections.count() - 1; i >= 0; --i) { + UnaquiredConnection &c = m_unacquiredConnections[i]; + if (c.scheduledForRemoval) { + disconnect(c.connection, 0, this, 0); + c.connection->deleteLater(); + m_unacquiredConnections.removeAt(i); + } else { + c.scheduledForRemoval = true; + } + } + } + private: // 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 m_unacquiredConnections; + QList m_unacquiredConnections; // Can contain the same connection more than once; this acts as a reference count. QList m_acquiredConnections; QList m_deprecatedConnections; QMutex m_listMutex; + QTimer m_removalTimer; }; -QMutex SshConnectionManagerPrivate::instanceMutex; - } // namespace Internal -SshConnectionManager &SshConnectionManager::instance() -{ - QMutexLocker locker(&Internal::SshConnectionManagerPrivate::instanceMutex); - return Internal::SshConnectionManagerPrivate::instance(); -} - -SshConnectionManager::SshConnectionManager() - : d(new Internal::SshConnectionManagerPrivate) -{ -} +static QMutex instanceMutex; -SshConnectionManager::~SshConnectionManager() +static Internal::SshConnectionManager &instance() { + static Internal::SshConnectionManager manager; + return manager; } -SshConnection *SshConnectionManager::acquireConnection(const SshConnectionParameters &sshParams) +SshConnection *acquireConnection(const SshConnectionParameters &sshParams) { - return d->acquireConnection(sshParams); + QMutexLocker locker(&instanceMutex); + return instance().acquireConnection(sshParams); } -void SshConnectionManager::releaseConnection(SshConnection *connection) +void releaseConnection(SshConnection *connection) { - d->releaseConnection(connection); + QMutexLocker locker(&instanceMutex); + instance().releaseConnection(connection); } -void SshConnectionManager::forceNewConnection(const SshConnectionParameters &sshParams) +void forceNewConnection(const SshConnectionParameters &sshParams) { - d->forceNewConnection(sshParams); + QMutexLocker locker(&instanceMutex); + instance().forceNewConnection(sshParams); } } // namespace QSsh diff --git a/src/libs/ssh/sshconnectionmanager.h b/src/libs/ssh/sshconnectionmanager.h index 9ea34fc..7815e61 100644 --- a/src/libs/ssh/sshconnectionmanager.h +++ b/src/libs/ssh/sshconnectionmanager.h @@ -33,32 +33,30 @@ #include "ssh_global.h" -#include - namespace QSsh { + class SshConnection; class SshConnectionParameters; -namespace Internal { class SshConnectionManagerPrivate; } - -class QSSH_EXPORT SshConnectionManager -{ - friend class Internal::SshConnectionManagerPrivate; -public: - static SshConnectionManager &instance(); - - SshConnection *acquireConnection(const SshConnectionParameters &sshParams); - void releaseConnection(SshConnection *connection); - // Make sure the next acquireConnection with the given parameters will return a new connection. - void forceNewConnection(const SshConnectionParameters &sshParams); - -private: - explicit SshConnectionManager(); - virtual ~SshConnectionManager(); - SshConnectionManager(const SshConnectionManager &); - SshConnectionManager &operator=(const SshConnectionManager &); - const QScopedPointer d; -}; +/*! + * \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. + * \param sshParams Parameters used during connection + * Make sure the next acquireConnection with the given parameters will return a new connection. + */ +QSSH_EXPORT void forceNewConnection(const SshConnectionParameters &sshParams); } // namespace QSsh diff --git a/src/libs/ssh/sshcryptofacility.cpp b/src/libs/ssh/sshcryptofacility.cpp index 55bc6c4..fa463b8 100644 --- a/src/libs/ssh/sshcryptofacility.cpp +++ b/src/libs/ssh/sshcryptofacility.cpp @@ -30,12 +30,14 @@ #include "sshcryptofacility_p.h" +#include "opensshkeyfilereader_p.h" #include "sshbotanconversions_p.h" #include "sshcapabilities_p.h" #include "sshexception_p.h" #include "sshkeyexchange_p.h" #include "sshkeypasswordretriever_p.h" #include "sshpacket_p.h" +#include "sshlogging_p.h" #include #include @@ -46,6 +48,9 @@ #include #include #include +#include +#include +#include #include #include @@ -73,24 +78,35 @@ void SshAbstractCryptoFacility::clearKeys() m_hMac.reset(0); } +SshAbstractCryptoFacility::Mode SshAbstractCryptoFacility::getMode(const QByteArray &algoName) +{ + if (algoName.endsWith("-ctr")) + return CtrMode; + if (algoName.endsWith("-cbc")) + return CbcMode; + throw SshClientException(SshInternalError, SSH_TR("Unexpected cipher \"%1\"") + .arg(QString::fromLatin1(algoName))); +} + void SshAbstractCryptoFacility::recreateKeys(const SshKeyExchange &kex) { checkInvariant(); if (m_sessionId.isEmpty()) m_sessionId = kex.h(); - const std::string &cryptAlgo = botanCryptAlgoName(cryptAlgoName(kex)); - BlockCipher *const cipher = BlockCipher::create_or_throw(cryptAlgo)->clone(); + const QByteArray &rfcCryptAlgoName = cryptAlgoName(kex); + BlockCipher * const cipher + = BlockCipher::create_or_throw(botanCryptAlgoName(rfcCryptAlgoName))->clone(); - m_cipherBlockSize = cipher->block_size(); + m_cipherBlockSize = static_cast(cipher->block_size()); const QByteArray ivData = generateHash(kex, ivChar(), m_cipherBlockSize); const InitializationVector iv(convertByteArray(ivData), m_cipherBlockSize); - const quint32 keySize = cipher->key_spec().maximum_keylength(); + const quint32 keySize = static_cast(cipher->key_spec().maximum_keylength()); const QByteArray cryptKeyData = generateHash(kex, keyChar(), keySize); SymmetricKey cryptKey(convertByteArray(cryptKeyData), keySize); - - Keyed_Filter * const cipherMode = makeCipherMode(cipher, new Null_Padding, iv, cryptKey); + Keyed_Filter * const cipherMode + = makeCipherMode(cipher, getMode(rfcCryptAlgoName), iv, cryptKey); m_pipe.reset(new Pipe(cipherMode)); m_macLength = botanHMacKeyLen(hMacAlgoName(kex)); @@ -117,9 +133,22 @@ void SshAbstractCryptoFacility::convert(QByteArray &data, quint32 offset, } m_pipe->process_msg(reinterpret_cast(data.constData()) + offset, dataSize); - quint32 bytesRead = m_pipe->read(reinterpret_cast(data.data()) + offset, - dataSize, m_pipe->message_count() - 1); // Can't use Pipe::LAST_MESSAGE because of a VC bug. - Q_ASSERT(bytesRead == dataSize); + // Can't use Pipe::LAST_MESSAGE because of a VC bug. + quint32 bytesRead = static_cast(m_pipe->read( + reinterpret_cast(data.data()) + offset, dataSize, m_pipe->message_count() - 1)); + if (bytesRead != dataSize) { + throw SshClientException(SshInternalError, + QLatin1String("Internal error: Botan::Pipe::read() returned unexpected value")); + } +} + +Keyed_Filter *SshAbstractCryptoFacility::makeCtrCipherMode(BlockCipher *cipher, + const InitializationVector &iv, const SymmetricKey &key) +{ + StreamCipher_Filter *filter = new StreamCipher_Filter(new CTR_BE(cipher)); + filter->set_key(key); + filter->set_iv(iv); + return filter; } QByteArray SshAbstractCryptoFacility::generateMac(const QByteArray &data, @@ -160,6 +189,8 @@ const QByteArray SshEncryptionFacility::PrivKeyFileStartLineRsa("-----BEGIN RSA 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 { @@ -171,11 +202,14 @@ QByteArray SshEncryptionFacility::hMacAlgoName(const SshKeyExchange &kex) const return kex.hMacAlgoClientToServer(); } -Keyed_Filter *SshEncryptionFacility::makeCipherMode(BlockCipher *cipher, - BlockCipherModePaddingMethod *paddingMethod, const InitializationVector &iv, - const SymmetricKey &key) +Keyed_Filter *SshEncryptionFacility::makeCipherMode(BlockCipher *cipher, Mode mode, + const InitializationVector &iv, const SymmetricKey &key) { - CBC_Encryption *cbc = new CBC_Encryption(cipher, paddingMethod); + if (mode == CtrMode) { + return makeCtrCipherMode(cipher, iv, key); + } + + CBC_Encryption *cbc = new CBC_Encryption(cipher, new Null_Padding); Cipher_Mode_Filter *filter = new Cipher_Mode_Filter(cbc); filter->set_iv(iv); filter->set_key(key); @@ -192,19 +226,23 @@ void SshEncryptionFacility::createAuthenticationKey(const QByteArray &privKeyFil if (privKeyFileContents == m_cachedPrivKeyContents) return; -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: Key not cached, reading", Q_FUNC_INFO); -#endif + m_authKeyAlgoName.clear(); + qCDebug(sshLog, "%s: Key not cached, reading", Q_FUNC_INFO); QList pubKeyParams; QList allKeyParams; QString error1; QString error2; - if (!createAuthenticationKeyFromPKCS8(privKeyFileContents, pubKeyParams, allKeyParams, error1) + 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)) { -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: %s\n\t%s\n", Q_FUNC_INFO, qPrintable(error1), qPrintable(error2)); -#endif + 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.")); } @@ -217,8 +255,15 @@ void SshEncryptionFacility::createAuthenticationKey(const QByteArray &privKeyFil } m_authPubKeyBlob = AbstractSshPacket::encodeString(m_authKeyAlgoName); - foreach (const BigInt &b, pubKeyParams) - m_authPubKeyBlob += AbstractSshPacket::encodeMpInt(b); + auto * const ecdsaKey = dynamic_cast(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))); + } else { + foreach (const BigInt &b, pubKeyParams) + m_authPubKeyBlob += AbstractSshPacket::encodeMpInt(b); + } m_cachedPrivKeyContents = privKeyFileContents; } @@ -228,27 +273,30 @@ bool SshEncryptionFacility::createAuthenticationKeyFromPKCS8(const QByteArray &p try { Pipe pipe; pipe.process_msg(convertByteArray(privKeyFileContents), privKeyFileContents.size()); - Private_Key * const key = PKCS8::load_key(pipe, m_rng, SshKeyPasswordRetriever::get_passphrase); - if (DSA_PrivateKey * const dsaKey = dynamic_cast(key)) { + m_authKey.reset(PKCS8::load_key(pipe, m_rng, SshKeyPasswordRetriever::get_passphrase)); + if (auto * const dsaKey = dynamic_cast(m_authKey.data())) { m_authKeyAlgoName = SshCapabilities::PubKeyDss; - m_authKey.reset(dsaKey); pubKeyParams << dsaKey->group_p() << dsaKey->group_q() << dsaKey->group_g() << dsaKey->get_y(); allKeyParams << pubKeyParams << dsaKey->get_x(); - } else if (RSA_PrivateKey * const rsaKey = dynamic_cast(key)) { + } else if (auto * const rsaKey = dynamic_cast(m_authKey.data())) { m_authKeyAlgoName = SshCapabilities::PubKeyRsa; - m_authKey.reset(rsaKey); 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(m_authKey.data())) { + const BigInt value = ecdsaKey->private_value(); + m_authKeyAlgoName = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth( + static_cast(value.bytes())); + pubKeyParams << ecdsaKey->public_point().get_affine_x() + << ecdsaKey->public_point().get_affine_y(); + allKeyParams << pubKeyParams << value; } else { - qWarning("%s: Unexpected code flow, expected success or exception.", Q_FUNC_INFO); + qCWarning(sshLog, "%s: Unexpected code flow, expected success or exception.", + Q_FUNC_INFO); return false; } - } catch (const Botan::Decoding_Error &ex) { - error = QLatin1String(ex.what()); - return false; - } catch (const Botan::Exception &ex) { + } catch (const std::exception &ex) { error = QLatin1String(ex.what()); return false; } @@ -276,6 +324,10 @@ bool SshEncryptionFacility::createAuthenticationKeyFromOpenSSL(const QByteArray syntaxOk = false; else m_authKeyAlgoName = SshCapabilities::PubKeyDss; + } else if (lines.first() == PrivKeyFileStartLineEcdsa) { + if (lines.last() != PrivKeyFileEndLineEcdsa) + syntaxOk = false; + // m_authKeyAlgoName set below, as we don't know the size yet. } else { syntaxOk = false; } @@ -293,8 +345,10 @@ bool SshEncryptionFacility::createAuthenticationKeyFromOpenSSL(const QByteArray BER_Decoder sequence = decoder.start_cons(SEQUENCE); size_t version; sequence.decode (version); - if (version != 0) { - error = SSH_TR("Key encoding has version %1, expected 0.").arg(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); return false; } @@ -305,21 +359,29 @@ bool SshEncryptionFacility::createAuthenticationKeyFromOpenSSL(const QByteArray m_authKey.reset(dsaKey); pubKeyParams << p << q << g << y; allKeyParams << pubKeyParams << x; - } else { + } 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; + } else { + BigInt privKey; + sequence.decode_octet_string_bigint(privKey); + m_authKeyAlgoName = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth( + static_cast(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(); - } catch (const Botan::Decoding_Error &ex) { - error = QLatin1String(ex.what()); - return false; - } catch (const Botan::Exception &ex) { + } catch (const std::exception &ex) { error = QLatin1String(ex.what()); return false; } @@ -343,6 +405,13 @@ QByteArray SshEncryptionFacility::authenticationKeySignature(const QByteArray &d 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); } @@ -368,11 +437,14 @@ QByteArray SshDecryptionFacility::hMacAlgoName(const SshKeyExchange &kex) const return kex.hMacAlgoServerToClient(); } -Keyed_Filter *SshDecryptionFacility::makeCipherMode(BlockCipher *cipher, - BlockCipherModePaddingMethod *paddingMethod, const InitializationVector &iv, +Keyed_Filter *SshDecryptionFacility::makeCipherMode(BlockCipher *cipher, Mode mode, const InitializationVector &iv, const SymmetricKey &key) { - CBC_Decryption *cbc = new CBC_Decryption(cipher, paddingMethod); + if (mode == CtrMode) { + return makeCtrCipherMode(cipher, iv, key); + } + + CBC_Decryption *cbc = new CBC_Decryption(cipher, new Null_Padding); Cipher_Mode_Filter *filter = new Cipher_Mode_Filter(cbc); filter->set_iv(iv); filter->set_key(key); @@ -383,13 +455,11 @@ void SshDecryptionFacility::decrypt(QByteArray &data, quint32 offset, quint32 dataSize) const { convert(data, offset, dataSize); -#ifdef CREATOR_SSH_DEBUG - qDebug("Decrypted data:"); + 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) - qDebug() << "'" << *c << "' (0x" << (static_cast(*c) & 0xff) << ")"; -#endif + qCDebug(sshLog, ) << "'" << *c << "' (0x" << (static_cast(*c) & 0xff) << ")"; } } // namespace Internal diff --git a/src/libs/ssh/sshcryptofacility_p.h b/src/libs/ssh/sshcryptofacility_p.h index 0502a15..52c4915 100644 --- a/src/libs/ssh/sshcryptofacility_p.h +++ b/src/libs/ssh/sshcryptofacility_p.h @@ -33,7 +33,7 @@ #include #include -#include +#include #include #include #include @@ -58,11 +58,15 @@ class SshAbstractCryptoFacility 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; } protected: + enum Mode { CbcMode, CtrMode }; + SshAbstractCryptoFacility(); void convert(QByteArray &data, quint32 offset, quint32 dataSize) const; - QByteArray sessionId() const { return m_sessionId; } + Botan::Keyed_Filter *makeCtrCipherMode(Botan::BlockCipher *cipher, + const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); private: SshAbstractCryptoFacility(const SshAbstractCryptoFacility &); @@ -71,15 +75,14 @@ class SshAbstractCryptoFacility virtual QByteArray cryptAlgoName(const SshKeyExchange &kex) const = 0; virtual QByteArray hMacAlgoName(const SshKeyExchange &kex) const = 0; virtual Botan::Keyed_Filter *makeCipherMode(Botan::BlockCipher *cipher, - Botan::BlockCipherModePaddingMethod *paddingMethod, - const Botan::InitializationVector &iv, - const Botan::SymmetricKey &key) = 0; + Mode mode, const Botan::InitializationVector &iv, const Botan::SymmetricKey &key) = 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; QScopedPointer m_pipe; @@ -105,8 +108,7 @@ class SshEncryptionFacility : public SshAbstractCryptoFacility virtual QByteArray cryptAlgoName(const SshKeyExchange &kex) const; virtual QByteArray hMacAlgoName(const SshKeyExchange &kex) const; virtual Botan::Keyed_Filter *makeCipherMode(Botan::BlockCipher *cipher, - Botan::BlockCipherModePaddingMethod *paddingMethod, - const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); + Mode mode, const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); virtual char ivChar() const { return 'A'; } virtual char keyChar() const { return 'C'; } virtual char macChar() const { return 'E'; } @@ -120,6 +122,8 @@ class SshEncryptionFacility : public SshAbstractCryptoFacility 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; @@ -137,8 +141,7 @@ class SshDecryptionFacility : public SshAbstractCryptoFacility virtual QByteArray cryptAlgoName(const SshKeyExchange &kex) const; virtual QByteArray hMacAlgoName(const SshKeyExchange &kex) const; virtual Botan::Keyed_Filter *makeCipherMode(Botan::BlockCipher *cipher, - Botan::BlockCipherModePaddingMethod *paddingMethod, - const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); + Mode mode, const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); virtual char ivChar() const { return 'B'; } virtual char keyChar() const { return 'D'; } virtual char macChar() const { return 'F'; } diff --git a/src/libs/ssh/sshdirecttcpiptunnel.cpp b/src/libs/ssh/sshdirecttcpiptunnel.cpp new file mode 100644 index 0000000..bd73572 --- /dev/null +++ b/src/libs/ssh/sshdirecttcpiptunnel.cpp @@ -0,0 +1,128 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** +** 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. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +**************************************************************************/ +#include "sshdirecttcpiptunnel.h" +#include "sshdirecttcpiptunnel_p.h" + +#include "sshincomingpacket_p.h" +#include "sshlogging_p.h" +#include "sshsendfacility_p.h" + +#include + +namespace QSsh { +namespace Internal { + +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() +{ + emit initialized(); +} + +} // namespace Internal + +using namespace Internal; + +SshDirectTcpIpTunnel::SshDirectTcpIpTunnel(quint32 channelId, const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort, + SshSendFacility &sendFacility) + : d(new SshDirectTcpIpTunnelPrivate(channelId, originatingHost, originatingPort, remoteHost, + remotePort, sendFacility)) +{ + d->init(this); + connect(d, &SshDirectTcpIpTunnelPrivate::initialized, + this, &SshDirectTcpIpTunnel::initialized, Qt::QueuedConnection); +} + +SshDirectTcpIpTunnel::~SshDirectTcpIpTunnel() +{ + delete d; +} + +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() +{ + d->closeChannel(); + QIODevice::close(); +} + +void SshDirectTcpIpTunnel::initialize() +{ + QSSH_ASSERT_AND_RETURN(d->channelState() == AbstractSshChannel::Inactive); + + try { + QIODevice::open(QIODevice::ReadWrite); + d->m_sendFacility.sendDirectTcpIpPacket(d->localChannelId(), d->initialWindowSize(), + d->maxPacketSize(), d->m_remoteHost.toUtf8(), d->m_remotePort, + d->m_originatingHost.toUtf8(), d->m_originatingPort); + d->setChannelState(AbstractSshChannel::SessionRequested); + d->m_timeoutTimer.start(d->ReplyTimeout); + } catch (const std::exception &e) { // Won't happen, but let's play it safe. + qCWarning(sshLog, "Botan error: %s", e.what()); + d->closeChannel(); + } +} + +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); +} + +} // namespace QSsh diff --git a/src/libs/ssh/sshdirecttcpiptunnel.h b/src/libs/ssh/sshdirecttcpiptunnel.h new file mode 100644 index 0000000..38fbd29 --- /dev/null +++ b/src/libs/ssh/sshdirecttcpiptunnel.h @@ -0,0 +1,89 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** +** 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. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +**************************************************************************/ + +#ifndef SSHDIRECTTCPIPTUNNEL_H +#define SSHDIRECTTCPIPTUNNEL_H + +#include "ssh_global.h" + +#include +#include + +namespace QSsh { + +namespace Internal { +class SshChannelManager; +class SshDirectTcpIpTunnelPrivate; +class SshSendFacility; +class SshTcpIpTunnelPrivate; +} // namespace Internal + +class QSSH_EXPORT SshDirectTcpIpTunnel : public QIODevice +{ + Q_OBJECT + + friend class Internal::SshChannelManager; + friend class Internal::SshTcpIpTunnelPrivate; + +public: + typedef QSharedPointer Ptr; + + ~SshDirectTcpIpTunnel(); + + // QIODevice stuff + bool atEnd() const; + qint64 bytesAvailable() const; + bool canReadLine() const; + void close(); + bool isSequential() const { return true; } + + void initialize(); + +signals: + void initialized(); + void error(const QString &reason); + +private: + SshDirectTcpIpTunnel(quint32 channelId, const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort, + Internal::SshSendFacility &sendFacility); + + // QIODevice stuff + qint64 readData(char *data, qint64 maxlen); + qint64 writeData(const char *data, qint64 len); + + Internal::SshDirectTcpIpTunnelPrivate * const d; +}; + +} // namespace QSsh + +#endif // SSHDIRECTTCPIPTUNNEL_H diff --git a/tests/manual/ssh/remoteprocess/argumentscollector.h b/src/libs/ssh/sshdirecttcpiptunnel_p.h similarity index 55% rename from tests/manual/ssh/remoteprocess/argumentscollector.h rename to src/libs/ssh/sshdirecttcpiptunnel_p.h index 4466cc5..80202ca 100644 --- a/tests/manual/ssh/remoteprocess/argumentscollector.h +++ b/src/libs/ssh/sshdirecttcpiptunnel_p.h @@ -4,7 +4,7 @@ ** ** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). ** -** Contact: http://www.qt-project.org/ +** Contact: Nokia Corporation (qt-info@nokia.com) ** ** ** GNU Lesser General Public License Usage @@ -25,37 +25,44 @@ ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. ** **************************************************************************/ +#ifndef DIRECTTCPIPCHANNEL_P_H +#define DIRECTTCPIPCHANNEL_P_H -#ifndef ARGUMENTSCOLLECTOR_H -#define ARGUMENTSCOLLECTOR_H +#include "sshtcpiptunnel_p.h" -#include +namespace QSsh { +class SshDirectTcpIpTunnel; -#include +namespace Internal { -class ArgumentsCollector +class SshDirectTcpIpTunnelPrivate : public SshTcpIpTunnelPrivate { + Q_OBJECT + + friend class QSsh::SshDirectTcpIpTunnel; + public: - ArgumentsCollector(const QStringList &args); - QSsh::SshConnectionParameters collect(bool &success) const; + explicit SshDirectTcpIpTunnelPrivate(quint32 channelId, const QString &originatingHost, + quint16 originatingPort, const QString &remoteHost, quint16 remotePort, + SshSendFacility &sendFacility); + +signals: + void initialized(); + private: - struct ArgumentErrorException - { - ArgumentErrorException(const QString &error) : error(error) {} - const QString error; - }; - - void printUsage() const; - bool checkAndSetStringArg(int &pos, QString &arg, const char *opt) const; - bool checkAndSetIntArg(int &pos, int &val, bool &alreadyGiven, - const char *opt) const; - bool checkForNoProxy(int &pos, - QSsh::SshConnectionParameters::ProxyType &type, - bool &alreadyGiven) const; - - const QStringList m_arguments; + void handleOpenSuccessInternal(); + + const QString m_originatingHost; + const quint16 m_originatingPort; + const QString m_remoteHost; + const quint16 m_remotePort; }; -#endif // ARGUMENTSCOLLECTOR_H +} // namespace Internal +} // namespace QSsh + +#endif // DIRECTTCPIPCHANNEL_P_H diff --git a/src/libs/ssh/ssherrors.h b/src/libs/ssh/ssherrors.h index f592a24..9673f26 100644 --- a/src/libs/ssh/ssherrors.h +++ b/src/libs/ssh/ssherrors.h @@ -31,14 +31,47 @@ #ifndef SSHERRORS_P_H #define SSHERRORS_P_H +#include + namespace QSsh { +/*! + * \brief SSH specific errors + */ enum SshError { - SshNoError, SshSocketError, SshTimeoutError, SshProtocolError, - SshHostKeyError, SshKeyFileError, SshAuthenticationError, - SshClosedByServerError, SshInternalError + /// 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 }; } // namespace QSsh +Q_DECLARE_METATYPE(QSsh::SshError) + #endif // SSHERRORS_P_H diff --git a/src/libs/ssh/sshexception_p.h b/src/libs/ssh/sshexception_p.h index 75c3ccf..c052bcb 100644 --- a/src/libs/ssh/sshexception_p.h +++ b/src/libs/ssh/sshexception_p.h @@ -37,6 +37,8 @@ #include #include +#include + namespace QSsh { namespace Internal { @@ -63,25 +65,28 @@ enum SshErrorCode { #define SSH_SERVER_EXCEPTION(error, errorString) \ SshServerException((error), (errorString), SSH_TR(errorString)) -struct SshServerException +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 +struct SshClientException : public std::exception { SshClientException(SshError error, const QString &errorString) - : error(error), errorString(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; }; } // namespace Internal diff --git a/src/libs/ssh/sshforwardedtcpiptunnel.cpp b/src/libs/ssh/sshforwardedtcpiptunnel.cpp new file mode 100644 index 0000000..22442f7 --- /dev/null +++ b/src/libs/ssh/sshforwardedtcpiptunnel.cpp @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "sshforwardedtcpiptunnel.h" +#include "sshforwardedtcpiptunnel_p.h" +#include "sshlogging_p.h" +#include "sshsendfacility_p.h" + +namespace QSsh { + +namespace Internal { +SshForwardedTcpIpTunnelPrivate::SshForwardedTcpIpTunnelPrivate(quint32 channelId, + SshSendFacility &sendFacility) : + SshTcpIpTunnelPrivate(channelId, sendFacility) +{ + setChannelState(SessionRequested); +} + +void SshForwardedTcpIpTunnelPrivate::handleOpenSuccessInternal() +{ + QSSH_ASSERT_AND_RETURN(channelState() == AbstractSshChannel::SessionEstablished); + + try { + m_sendFacility.sendChannelOpenConfirmationPacket(remoteChannel(), localChannelId(), + initialWindowSize(), maxPacketSize()); + } catch (const std::exception &e) { // Won't happen, but let's play it safe. + qCWarning(sshLog, "Botan error: %s", e.what()); + closeChannel(); + } +} + +} // namespace Internal + +using namespace Internal; + +SshForwardedTcpIpTunnel::SshForwardedTcpIpTunnel(quint32 channelId, SshSendFacility &sendFacility) : + d(new SshForwardedTcpIpTunnelPrivate(channelId, sendFacility)) +{ + d->init(this); +} + +SshForwardedTcpIpTunnel::~SshForwardedTcpIpTunnel() +{ + delete d; +} + +bool SshForwardedTcpIpTunnel::atEnd() const +{ + return QIODevice::atEnd() && d->m_data.isEmpty(); +} + +qint64 SshForwardedTcpIpTunnel::bytesAvailable() const +{ + return QIODevice::bytesAvailable() + d->m_data.count(); +} + +bool SshForwardedTcpIpTunnel::canReadLine() const +{ + return QIODevice::canReadLine() || d->m_data.contains('\n'); +} + +void SshForwardedTcpIpTunnel::close() +{ + d->closeChannel(); + QIODevice::close(); +} + +qint64 SshForwardedTcpIpTunnel::readData(char *data, qint64 maxlen) +{ + return d->readData(data, maxlen); +} + +qint64 SshForwardedTcpIpTunnel::writeData(const char *data, qint64 len) +{ + return d->writeData(data, len); +} + +} // namespace QSsh diff --git a/src/libs/ssh/sshforwardedtcpiptunnel.h b/src/libs/ssh/sshforwardedtcpiptunnel.h new file mode 100644 index 0000000..cb33c30 --- /dev/null +++ b/src/libs/ssh/sshforwardedtcpiptunnel.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ +#pragma once + +#include "ssh_global.h" +#include +#include + +namespace QSsh { + +namespace Internal { +class SshChannelManager; +class SshForwardedTcpIpTunnelPrivate; +class SshSendFacility; +class SshTcpIpTunnelPrivate; +} // namespace Internal + +class QSSH_EXPORT SshForwardedTcpIpTunnel : public QIODevice +{ + Q_OBJECT + friend class Internal::SshChannelManager; + friend class Internal::SshTcpIpTunnelPrivate; + +public: + typedef QSharedPointer Ptr; + ~SshForwardedTcpIpTunnel() override; + + // QIODevice stuff + bool atEnd() const override; + qint64 bytesAvailable() const override; + bool canReadLine() const override; + void close() override; + bool isSequential() const override { return true; } + +signals: + void error(const QString &reason); + +private: + SshForwardedTcpIpTunnel(quint32 channelId, Internal::SshSendFacility &sendFacility); + + // QIODevice stuff + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + + Internal::SshForwardedTcpIpTunnelPrivate * const d; +}; + +} // namespace QSsh diff --git a/src/libs/ssh/sshforwardedtcpiptunnel_p.h b/src/libs/ssh/sshforwardedtcpiptunnel_p.h new file mode 100644 index 0000000..7b5043d --- /dev/null +++ b/src/libs/ssh/sshforwardedtcpiptunnel_p.h @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include "sshforwardedtcpiptunnel.h" +#include "sshtcpiptunnel_p.h" + +namespace QSsh { +namespace Internal { + +class SshForwardedTcpIpTunnelPrivate : public SshTcpIpTunnelPrivate +{ + Q_OBJECT + friend class QSsh::SshForwardedTcpIpTunnel; +public: + SshForwardedTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility); + void handleOpenSuccessInternal() override; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshhostkeydatabase.cpp b/src/libs/ssh/sshhostkeydatabase.cpp new file mode 100644 index 0000000..163540c --- /dev/null +++ b/src/libs/ssh/sshhostkeydatabase.cpp @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 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. +** +** GNU Lesser General Public License Usage +** 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 +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ +#include "sshhostkeydatabase.h" + +#include "sshlogging_p.h" + +#include +#include +#include +#include +#include +#include + +namespace QSsh { + +class SshHostKeyDatabase::SshHostKeyDatabasePrivate +{ +public: + QHash hostKeys; +}; + +SshHostKeyDatabase::SshHostKeyDatabase() : d(new SshHostKeyDatabasePrivate) +{ +} + +SshHostKeyDatabase::~SshHostKeyDatabase() +{ + delete d; +} + +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()); + } + return false; + } + + d->hostKeys.clear(); + const QByteArray content = file.readAll().trimmed(); + if (content.isEmpty()) + return true; + foreach (const QByteArray &line, content.split('\n')) { + const QList &lineData = line.trimmed().split(' '); + if (lineData.count() != 2) { + qCDebug(Internal::sshLog, "Unexpected line \"%s\" in file \"%s\".", line.constData(), + qPrintable(filePath)); + continue; + } + d->hostKeys.insert(QString::fromUtf8(lineData.first()), + QByteArray::fromHex(lineData.last())); + } + + return true; +} + +bool SshHostKeyDatabase::store(const QString &filePath, QString *error) const +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + if (error) { + *error = QCoreApplication::translate("QSsh::Ssh", + "Failed to open key file \"%1\" for writing: %2") + .arg(QDir::toNativeSeparators(filePath), file.errorString()); + } + return false; + } + + file.resize(0); + for (auto it = d->hostKeys.constBegin(); it != d->hostKeys.constEnd(); ++it) + file.write(it.key().toUtf8() + ' ' + it.value().toHex() + '\n'); + return true; +} + +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); +} + +} // namespace QSsh diff --git a/src/libs/ssh/sshhostkeydatabase.h b/src/libs/ssh/sshhostkeydatabase.h new file mode 100644 index 0000000..a80a084 --- /dev/null +++ b/src/libs/ssh/sshhostkeydatabase.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 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. +** +** GNU Lesser General Public License Usage +** 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 +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ +#ifndef SSHHOSTKEYDATABASE_H +#define SSHHOSTKEYDATABASE_H + +#include "ssh_global.h" + +#include + +QT_BEGIN_NAMESPACE +class QByteArray; +class QString; +QT_END_NAMESPACE + +namespace QSsh { +class SshHostKeyDatabase; + +/// Convenience typedef +typedef QSharedPointer SshHostKeyDatabasePtr; + +class QSSH_EXPORT SshHostKeyDatabase +{ + friend class QSharedPointer; // To give create() access to our constructor. + +public: + enum KeyLookupResult { + KeyLookupMatch, + KeyLookupNoMatch, + KeyLookupMismatch + }; + + ~SshHostKeyDatabase(); + + bool load(const QString &filePath, QString *error = 0); + bool store(const QString &filePath, QString *error = 0) const; + KeyLookupResult matchHostKey(const QString &hostName, const QByteArray &key) const; + void insertHostKey(const QString &hostName, const QByteArray &key); + +private: + SshHostKeyDatabase(); + + class SshHostKeyDatabasePrivate; + SshHostKeyDatabasePrivate * const d; +}; + +} // namespace QSsh + +#endif // Include guard. diff --git a/src/libs/ssh/sshincomingpacket.cpp b/src/libs/ssh/sshincomingpacket.cpp index 96844aa..a6fae06 100644 --- a/src/libs/ssh/sshincomingpacket.cpp +++ b/src/libs/ssh/sshincomingpacket.cpp @@ -30,13 +30,17 @@ #include "sshincomingpacket_p.h" +#include "ssh_global.h" +#include "sshbotanconversions_p.h" #include "sshcapabilities_p.h" +#include "sshlogging_p.h" namespace QSsh { namespace Internal { const QByteArray SshIncomingPacket::ExitStatusType("exit-status"); const QByteArray SshIncomingPacket::ExitSignalType("exit-signal"); +const QByteArray SshIncomingPacket::ForwardedTcpIpType("forwarded-tcpip"); SshIncomingPacket::SshIncomingPacket() : m_serverSeqNr(0) { } @@ -64,10 +68,8 @@ void SshIncomingPacket::reset() void SshIncomingPacket::consumeData(QByteArray &newData) { -#ifdef CREATOR_SSH_DEBUG - qDebug("%s: current data size = %d, new data size = %d", + qCDebug(sshLog, "%s: current data size = %d, new data size = %d", Q_FUNC_INFO, m_data.size(), newData.size()); -#endif if (isComplete() || newData.isEmpty()) return; @@ -81,9 +83,7 @@ void SshIncomingPacket::consumeData(QByteArray &newData) const int bytesToTake = qMin(minSize - currentDataSize(), newData.size()); moveFirstBytes(m_data, newData, bytesToTake); -#ifdef CREATOR_SSH_DEBUG - qDebug("Took %d bytes from new data", bytesToTake); -#endif + qCDebug(sshLog, "Took %d bytes from new data", bytesToTake); if (currentDataSize() < minSize) return; } @@ -95,14 +95,10 @@ void SshIncomingPacket::consumeData(QByteArray &newData) = qMin(length() + 4 + macLength() - currentDataSize(), newData.size()); moveFirstBytes(m_data, newData, bytesToTake); -#ifdef CREATOR_SSH_DEBUG - qDebug("Took %d bytes from new data", bytesToTake); -#endif + qCDebug(sshLog, "Took %d bytes from new data", bytesToTake); if (isComplete()) { -#ifdef CREATOR_SSH_DEBUG - qDebug("Message complete. Overall size: %u, payload size: %u", + qCDebug(sshLog, "Message complete. Overall size: %u, payload size: %u", m_data.size(), m_length - paddingLength() - 1); -#endif decrypt(); ++m_serverSeqNr; } @@ -161,49 +157,87 @@ SshKeyExchangeInit SshIncomingPacket::extractKeyExchangeInitData() const = SshPacketParser::asNameList(m_data, &offset); exchangeData.firstKexPacketFollows = SshPacketParser::asBool(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Key exchange failed: Server sent invalid SSH_MSG_KEXINIT packet."); } return exchangeData; } -SshKeyExchangeReply SshIncomingPacket::extractKeyExchangeReply(const QByteArray &pubKeyAlgo) const +static void getHostKeySpecificReplyData(SshKeyExchangeReply &replyData, + const QByteArray &hostKeyAlgo, const QByteArray &input) +{ + quint32 offset = 0; + if (hostKeyAlgo == SshCapabilities::PubKeyDss || hostKeyAlgo == SshCapabilities::PubKeyRsa) { + // DSS: p and q, RSA: e and n + replyData.hostKeyParameters << SshPacketParser::asBigInt(input, &offset); + replyData.hostKeyParameters << SshPacketParser::asBigInt(input, &offset); + + // g and y + if (hostKeyAlgo == SshCapabilities::PubKeyDss) { + replyData.hostKeyParameters << SshPacketParser::asBigInt(input, &offset); + replyData.hostKeyParameters << SshPacketParser::asBigInt(input, &offset); + } + } else { + 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(isComplete()); Q_ASSERT(type() == SSH_MSG_KEXDH_REPLY); try { SshKeyExchangeReply replyData; - quint32 offset = TypeOffset + 1; - const quint32 k_sLength - = SshPacketParser::asUint32(m_data, &offset); - if (offset + k_sLength > currentDataSize()) - throw SshPacketParseException(); - replyData.k_s = m_data.mid(offset - 4, k_sLength + 4); - if (SshPacketParser::asString(m_data, &offset) != pubKeyAlgo) + 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) throw SshPacketParseException(); - - // DSS: p and q, RSA: e and n - replyData.parameters << SshPacketParser::asBigInt(m_data, &offset); - replyData.parameters << SshPacketParser::asBigInt(m_data, &offset); - - // g and y - if (pubKeyAlgo == SshCapabilities::PubKeyDss) { - replyData.parameters << SshPacketParser::asBigInt(m_data, &offset); - replyData.parameters << SshPacketParser::asBigInt(m_data, &offset); + getHostKeySpecificReplyData(replyData, hostKeyAlgo, replyData.k_s.mid(k_sOffset)); + + if (kexAlgo == SshCapabilities::DiffieHellmanGroup1Sha1 + || kexAlgo == SshCapabilities::DiffieHellmanGroup14Sha1) { + replyData.f = SshPacketParser::asBigInt(m_data, &topLevelOffset); + } else { + QSSH_ASSERT_AND_RETURN_VALUE(kexAlgo.startsWith(SshCapabilities::EcdhKexNamePrefix), + SshKeyExchangeReply()); + replyData.q_s = SshPacketParser::asString(m_data, &topLevelOffset); } - - replyData.f = SshPacketParser::asBigInt(m_data, &offset); - offset += 4; - if (SshPacketParser::asString(m_data, &offset) != pubKeyAlgo) + const QByteArray fullSignature = SshPacketParser::asString(m_data, &topLevelOffset); + quint32 sigOffset = 0; + if (SshPacketParser::asString(fullSignature, &sigOffset) != hostKeyAlgo) throw SshPacketParseException(); - replyData.signatureBlob = SshPacketParser::asString(m_data, &offset); + 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; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Key exchange failed: " - "Server sent invalid SSH_MSG_KEXDH_REPLY packet."); + "Server sent invalid key exchange reply packet."); } } @@ -218,7 +252,7 @@ SshDisconnect SshIncomingPacket::extractDisconnect() const msg.reasonCode = SshPacketParser::asUint32(m_data, &offset); msg.description = SshPacketParser::asUserString(m_data, &offset); msg.language = SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_DISCONNECT."); } @@ -237,12 +271,54 @@ SshUserAuthBanner SshIncomingPacket::extractUserAuthBanner() const msg.message = SshPacketParser::asUserString(m_data, &offset); msg.language = SshPacketParser::asString(m_data, &offset); return msg; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_USERAUTH_BANNER."); } } +SshUserAuthInfoRequestPacket SshIncomingPacket::extractUserAuthInfoRequest() const +{ + Q_ASSERT(isComplete()); + Q_ASSERT(type() == SSH_MSG_USERAUTH_INFO_REQUEST); + + try { + SshUserAuthInfoRequestPacket msg; + quint32 offset = TypeOffset + 1; + 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); + } + return msg; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Invalid SSH_MSG_USERAUTH_INFO_REQUEST."); + } +} + +SshUserAuthPkOkPacket SshIncomingPacket::extractUserAuthPkOk() const +{ + Q_ASSERT(isComplete()); + Q_ASSERT(type() == SSH_MSG_USERAUTH_PK_OK); + + try { + SshUserAuthPkOkPacket msg; + quint32 offset = TypeOffset + 1; + msg.algoName= SshPacketParser::asString(m_data, &offset); + msg.keyBlob = SshPacketParser::asString(m_data, &offset); + return msg; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Invalid SSH_MSG_USERAUTH_PK_OK."); + } +} + SshDebug SshIncomingPacket::extractDebug() const { Q_ASSERT(isComplete()); @@ -255,12 +331,28 @@ SshDebug SshIncomingPacket::extractDebug() const msg.message = SshPacketParser::asUserString(m_data, &offset); msg.language = SshPacketParser::asString(m_data, &offset); return msg; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_DEBUG."); } } +SshRequestSuccess SshIncomingPacket::extractRequestSuccess() const +{ + Q_ASSERT(isComplete()); + Q_ASSERT(type() == SSH_MSG_REQUEST_SUCCESS); + + try { + SshRequestSuccess msg; + quint32 offset = TypeOffset + 1; + msg.bindPort = SshPacketParser::asUint32(m_data, &offset); + return msg; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Invalid SSH_MSG_REQUEST_SUCCESS."); + } +} + SshUnimplemented SshIncomingPacket::extractUnimplemented() const { Q_ASSERT(isComplete()); @@ -271,12 +363,71 @@ SshUnimplemented SshIncomingPacket::extractUnimplemented() const quint32 offset = TypeOffset + 1; msg.invalidMsgSeqNr = SshPacketParser::asUint32(m_data, &offset); return msg; - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_UNIMPLEMENTED."); } } +SshChannelOpenGeneric SshIncomingPacket::extractChannelOpen() const +{ + Q_ASSERT(isComplete()); + Q_ASSERT(type() == SSH_MSG_CHANNEL_OPEN); + + try { + SshChannelOpenGeneric channelOpen; + quint32 offset = TypeOffset + 1; + 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; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Server sent invalid SSH_MSG_CHANNEL_OPEN packet."); + } +} + +SshChannelOpenForwardedTcpIp SshIncomingPacket::extractChannelOpenForwardedTcpIp( + const SshChannelOpenGeneric &genericData) +{ + try { + SshChannelOpenForwardedTcpIp specificData; + specificData.common = genericData.commonData; + quint32 offset = 0; + specificData.remoteAddress = SshPacketParser::asString(genericData.typeSpecificData, + &offset); + specificData.remotePort = SshPacketParser::asUint32(genericData.typeSpecificData, &offset); + specificData.originatorAddress = SshPacketParser::asString(genericData.typeSpecificData, + &offset); + specificData.originatorPort = SshPacketParser::asUint32(genericData.typeSpecificData, + &offset); + return specificData; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Server sent invalid SSH_MSG_CHANNEL_OPEN packet."); + } +} + +SshChannelOpenX11 SshIncomingPacket::extractChannelOpenX11(const SshChannelOpenGeneric &genericData) +{ + try { + SshChannelOpenX11 specificData; + specificData.common = genericData.commonData; + quint32 offset = 0; + specificData.originatorAddress = SshPacketParser::asString(genericData.typeSpecificData, + &offset); + specificData.originatorPort = SshPacketParser::asUint32(genericData.typeSpecificData, + &offset); + return specificData; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Server sent invalid SSH_MSG_CHANNEL_OPEN packet."); + } +} + SshChannelOpenFailure SshIncomingPacket::extractChannelOpenFailure() const { Q_ASSERT(isComplete()); @@ -289,7 +440,7 @@ SshChannelOpenFailure SshIncomingPacket::extractChannelOpenFailure() const openFailure.reasonCode = SshPacketParser::asUint32(m_data, &offset); openFailure.reasonString = QString::fromLocal8Bit(SshPacketParser::asString(m_data, &offset)); openFailure.language = SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Server sent invalid SSH_MSG_CHANNEL_OPEN_FAILURE packet."); } @@ -308,7 +459,7 @@ SshChannelOpenConfirmation SshIncomingPacket::extractChannelOpenConfirmation() c confirmation.remoteChannel = SshPacketParser::asUint32(m_data, &offset); confirmation.remoteWindowSize = SshPacketParser::asUint32(m_data, &offset); confirmation.remoteMaxPacketSize = SshPacketParser::asUint32(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Server sent invalid SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet."); } @@ -325,7 +476,7 @@ SshChannelWindowAdjust SshIncomingPacket::extractWindowAdjust() const quint32 offset = TypeOffset + 1; adjust.localChannel = SshPacketParser::asUint32(m_data, &offset); adjust.bytesToAdd = SshPacketParser::asUint32(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_CHANNEL_WINDOW_ADJUST packet."); } @@ -342,7 +493,7 @@ SshChannelData SshIncomingPacket::extractChannelData() const quint32 offset = TypeOffset + 1; data.localChannel = SshPacketParser::asUint32(m_data, &offset); data.data = SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_CHANNEL_DATA packet."); } @@ -360,7 +511,7 @@ SshChannelExtendedData SshIncomingPacket::extractChannelExtendedData() const data.localChannel = SshPacketParser::asUint32(m_data, &offset); data.type = SshPacketParser::asUint32(m_data, &offset); data.data = SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_CHANNEL_EXTENDED_DATA packet."); } @@ -382,7 +533,7 @@ SshChannelExitStatus SshIncomingPacket::extractChannelExitStatus() const if (SshPacketParser::asBool(m_data, &offset)) throw SshPacketParseException(); exitStatus.exitStatus = SshPacketParser::asUint32(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid exit-status packet."); } @@ -407,7 +558,7 @@ SshChannelExitSignal SshIncomingPacket::extractChannelExitSignal() const exitSignal.coreDumped = SshPacketParser::asBool(m_data, &offset); exitSignal.error = SshPacketParser::asUserString(m_data, &offset); exitSignal.language = SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid exit-signal packet."); } @@ -421,7 +572,7 @@ quint32 SshIncomingPacket::extractRecipientChannel() const try { quint32 offset = TypeOffset + 1; return SshPacketParser::asUint32(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Server sent invalid packet."); } @@ -436,7 +587,7 @@ QByteArray SshIncomingPacket::extractChannelRequestType() const quint32 offset = TypeOffset + 1; SshPacketParser::asUint32(m_data, &offset); return SshPacketParser::asString(m_data, &offset); - } catch (SshPacketParseException &) { + } catch (const SshPacketParseException &) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid SSH_MSG_CHANNEL_REQUEST packet."); } @@ -445,19 +596,13 @@ QByteArray SshIncomingPacket::extractChannelRequestType() const void SshIncomingPacket::calculateLength() const { Q_ASSERT(currentDataSize() >= minPacketSize()); -#ifdef CREATOR_SSH_DEBUG - qDebug("Length field before decryption: %d-%d-%d-%d", m_data.at(0) & 0xff, + 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); -#endif m_decrypter.decrypt(m_data, 0, cipherBlockSize()); -#ifdef CREATOR_SSH_DEBUG - qDebug("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); - qDebug("message type = %d", m_data.at(TypeOffset)); -#endif + 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)); m_length = SshPacketParser::asUint32(m_data, static_cast(0)); -#ifdef CREATOR_SSH_DEBUG - qDebug("decrypted length is %u", m_length); -#endif + qCDebug(sshLog, "decrypted length is %u", m_length); } } // namespace Internal diff --git a/src/libs/ssh/sshincomingpacket_p.h b/src/libs/ssh/sshincomingpacket_p.h index 6da73a7..aaf4169 100644 --- a/src/libs/ssh/sshincomingpacket_p.h +++ b/src/libs/ssh/sshincomingpacket_p.h @@ -36,8 +36,7 @@ #include "sshcryptofacility_p.h" #include "sshpacketparser_p.h" -#include -#include +#include namespace QSsh { namespace Internal { @@ -63,8 +62,10 @@ struct SshKeyExchangeInit struct SshKeyExchangeReply { QByteArray k_s; - QList parameters; // DSS: p, q, g, y. RSA: e, n. - Botan::BigInt f; + QList 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; }; @@ -81,6 +82,21 @@ struct SshUserAuthBanner QByteArray language; }; +struct SshUserAuthPkOkPacket +{ + QByteArray algoName; + QByteArray keyBlob; +}; + +struct SshUserAuthInfoRequestPacket +{ + QString name; + QString instruction; + QByteArray languageTag; + QStringList prompts; + QList echos; +}; + struct SshDebug { bool display; @@ -93,6 +109,41 @@ 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 +{ + SshChannelOpenCommon common; + QByteArray originatorAddress; + quint32 originatorPort; +}; + struct SshChannelOpenFailure { quint32 localChannel; @@ -143,7 +194,6 @@ struct SshChannelExitSignal QByteArray language; }; - class SshIncomingPacket : public AbstractSshPacket { public: @@ -154,12 +204,20 @@ class SshIncomingPacket : public AbstractSshPacket void reset(); SshKeyExchangeInit extractKeyExchangeInitData() const; - SshKeyExchangeReply extractKeyExchangeReply(const QByteArray &pubKeyAlgo) 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; @@ -174,6 +232,7 @@ class SshIncomingPacket : public AbstractSshPacket static const QByteArray ExitStatusType; static const QByteArray ExitSignalType; + static const QByteArray ForwardedTcpIpType; private: virtual quint32 cipherBlockSize() const; diff --git a/src/libs/ssh/sshinit.cpp b/src/libs/ssh/sshinit.cpp new file mode 100644 index 0000000..ad07247 --- /dev/null +++ b/src/libs/ssh/sshinit.cpp @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ +#include "sshinit_p.h" + +#include + +#include +#include + +namespace QSsh { +namespace Internal { + +static bool initialized = false; +static QMutex initMutex; + +void initSsh() +{ + QMutexLocker locker(&initMutex); + if (!initialized) { + Botan::LibraryInitializer::initialize("thread_safe=true"); + initialized = true; + } +} + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshinit_p.h b/src/libs/ssh/sshinit_p.h new file mode 100644 index 0000000..af81268 --- /dev/null +++ b/src/libs/ssh/sshinit_p.h @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +namespace QSsh { +namespace Internal { + +void initSsh(); + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshkeyexchange.cpp b/src/libs/ssh/sshkeyexchange.cpp index 3747c2f..e0a6fba 100644 --- a/src/libs/ssh/sshkeyexchange.cpp +++ b/src/libs/ssh/sshkeyexchange.cpp @@ -30,11 +30,13 @@ #include "sshkeyexchange_p.h" +#include "ssh_global.h" #include "sshbotanconversions_p.h" #include "sshcapabilities_p.h" #include "sshsendfacility_p.h" #include "sshexception_p.h" #include "sshincomingpacket_p.h" +#include "sshlogging_p.h" #include #include @@ -43,6 +45,9 @@ #include #include #include +#include +#include +#include #ifdef CREATOR_SSH_DEBUG #include @@ -59,31 +64,22 @@ namespace { // For debugging void printNameList(const char *listName, const SshNameList &list) { -#ifdef CREATOR_SSH_DEBUG - qDebug("%s:", listName); + qCDebug(sshLog, "%s:", listName); foreach (const QByteArray &name, list.names) - qDebug("%s", name.constData()); -#else - Q_UNUSED(listName); - Q_UNUSED(list); -#endif + qCDebug(sshLog, "%s", name.constData()); } -#ifdef CREATOR_SSH_DEBUG void printData(const char *name, const QByteArray &data) { - std::cerr << std::hex; - qDebug("The client thinks the %s has length %d and is:", name, data.count()); - for (int i = 0; i < data.count(); ++i) - std::cerr << (static_cast(data.at(i)) & 0xff) << ' '; - std::cerr << std::endl; + qCDebug(sshLog, "The client thinks the %s has length %d and is: %s", name, data.count(), + data.toHex().constData()); } -#endif } // anonymous namespace -SshKeyExchange::SshKeyExchange(SshSendFacility &sendFacility) - : m_sendFacility(sendFacility) +SshKeyExchange::SshKeyExchange(const SshConnectionParameters &connParams, + SshSendFacility &sendFacility) + : m_connParams(connParams), m_sendFacility(sendFacility) { } @@ -97,9 +93,7 @@ void SshKeyExchange::sendKexInitPacket(const QByteArray &serverId) bool SshKeyExchange::sendDhInitPacket(const SshIncomingPacket &serverKexInit) { -#ifdef CREATOR_SSH_DEBUG - qDebug("server requests key exchange"); -#endif + qCDebug(sshLog, "server requests key exchange"); serverKexInit.printRawBytes(); SshKeyExchangeInit kexInitParams = serverKexInit.extractKeyExchangeInitData(); @@ -114,48 +108,46 @@ bool SshKeyExchange::sendDhInitPacket(const SshIncomingPacket &serverKexInit) printNameList("Compression algorithms client to server", kexInitParams.compressionAlgorithmsClientToServer); printNameList("Languages client to server", kexInitParams.languagesClientToServer); printNameList("Languages server to client", kexInitParams.languagesServerToClient); -#ifdef CREATOR_SSH_DEBUG - qDebug("First packet follows: %d", kexInitParams.firstKexPacketFollows); -#endif + qCDebug(sshLog, "First packet follows: %d", kexInitParams.firstKexPacketFollows); + + m_kexAlgoName = SshCapabilities::findBestMatch(SshCapabilities::KeyExchangeMethods, + kexInitParams.keyAlgorithms.names); + m_serverHostKeyAlgo = SshCapabilities::findBestMatch(SshCapabilities::PublicKeyAlgorithms, + kexInitParams.serverHostKeyAlgorithms.names); + determineHashingAlgorithm(kexInitParams, true); + determineHashingAlgorithm(kexInitParams, false); - const QByteArray &keyAlgo - = SshCapabilities::findBestMatch(SshCapabilities::KeyExchangeMethods, - kexInitParams.keyAlgorithms.names); - m_serverHostKeyAlgo - = SshCapabilities::findBestMatch(SshCapabilities::PublicKeyAlgorithms, - kexInitParams.serverHostKeyAlgorithms.names); m_encryptionAlgo = SshCapabilities::findBestMatch(SshCapabilities::EncryptionAlgorithms, kexInitParams.encryptionAlgorithmsClientToServer.names); m_decryptionAlgo = SshCapabilities::findBestMatch(SshCapabilities::EncryptionAlgorithms, kexInitParams.encryptionAlgorithmsServerToClient.names); - m_c2sHMacAlgo - = SshCapabilities::findBestMatch(SshCapabilities::MacAlgorithms, - kexInitParams.macAlgorithmsClientToServer.names); - m_s2cHMacAlgo - = SshCapabilities::findBestMatch(SshCapabilities::MacAlgorithms, - kexInitParams.macAlgorithmsServerToClient.names); SshCapabilities::findBestMatch(SshCapabilities::CompressionAlgorithms, kexInitParams.compressionAlgorithmsClientToServer.names); SshCapabilities::findBestMatch(SshCapabilities::CompressionAlgorithms, kexInitParams.compressionAlgorithmsServerToClient.names); AutoSeeded_RNG rng; - m_dhKey.reset(new DH_PrivateKey(rng, - DL_Group(botanKeyExchangeAlgoName(keyAlgo)))); + 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())); + } else { + m_dhKey.reset(new DH_PrivateKey(rng, DL_Group(botanKeyExchangeAlgoName(m_kexAlgoName)))); + m_sendFacility.sendKeyDhInitPacket(m_dhKey->get_y()); + } m_serverKexInitPayload = serverKexInit.payLoad(); - m_sendFacility.sendKeyDhInitPacket(m_dhKey->get_y()); return kexInitParams.firstKexPacketFollows; } void SshKeyExchange::sendNewKeysPacket(const SshIncomingPacket &dhReply, const QByteArray &clientId) { + const SshKeyExchangeReply &reply - = dhReply.extractKeyExchangeReply(m_serverHostKeyAlgo); - if (reply.f <= 0 || reply.f >= m_dhKey->group_p()) { + = dhReply.extractKeyExchangeReply(m_kexAlgoName, m_serverHostKeyAlgo); + if (m_dhKey && (reply.f <= 0 || reply.f >= m_dhKey->group_p())) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Server sent invalid f."); } @@ -165,59 +157,139 @@ void SshKeyExchange::sendNewKeysPacket(const SshIncomingPacket &dhReply, concatenatedData += AbstractSshPacket::encodeString(m_clientKexInitPayload); concatenatedData += AbstractSshPacket::encodeString(m_serverKexInitPayload); concatenatedData += reply.k_s; - concatenatedData += AbstractSshPacket::encodeMpInt(m_dhKey->get_y()); - concatenatedData += AbstractSshPacket::encodeMpInt(reply.f); - const BigInt k = power_mod(reply.f, m_dhKey->get_x(), m_dhKey->get_domain().get_p()); - m_k = AbstractSshPacket::encodeMpInt(k); - concatenatedData += m_k; - - m_hash.reset(HashFunction::create_or_throw(botanSha1Name())->clone()); - const SecureVector &hashResult - = m_hash->process(convertByteArray(concatenatedData), - concatenatedData.size()); - m_h = convertByteArray(hashResult); -#ifdef CREATOR_SSH_DEBUG 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); - printData("y", AbstractSshPacket::encodeMpInt(m_dhKey->get_y())); - printData("f", AbstractSshPacket::encodeMpInt(reply.f)); + + AutoSeeded_RNG rng; + + SecureVector encodedK; + if (m_dhKey) { + concatenatedData += AbstractSshPacket::encodeMpInt(m_dhKey->get_y()); + concatenatedData += AbstractSshPacket::encodeMpInt(reply.f); + + std::unique_ptr dhOp = m_dhKey->create_key_agreement_op(rng, "Raw", "base"); + std::vector encodedF = BigInt::encode(reply.f); + encodedK = dhOp->agree(0, encodedF.data(), encodedF.size(), 0, 0); + printData("y", AbstractSshPacket::encodeMpInt(m_dhKey->get_y())); + printData("f", AbstractSshPacket::encodeMpInt(reply.f)); + m_dhKey.reset(); + } else { + Q_ASSERT(m_ecdhKey); + concatenatedData // Q_C. + += AbstractSshPacket::encodeString(convertByteArray(m_ecdhKey->public_value())); + concatenatedData += AbstractSshPacket::encodeString(reply.q_s); + std::unique_ptr ecdhOp = m_ecdhKey->create_key_agreement_op(rng, "Raw", "base"); + + encodedK = ecdhOp->agree(0, convertByteArray(reply.q_s), reply.q_s.count(), 0, 0); + m_ecdhKey.reset(); + } + + const BigInt k = BigInt::decode(encodedK); + 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 &hashResult = m_hash->process(convertByteArray(concatenatedData), + concatenatedData.size()); + m_h = convertByteArray(hashResult); + printData("H", m_h); -#endif // CREATOR_SSH_DEBUG QScopedPointer sigKey; - QScopedPointer verifier; if (m_serverHostKeyAlgo == SshCapabilities::PubKeyDss) { - const DL_Group group(reply.parameters.at(0), reply.parameters.at(1), - reply.parameters.at(2)); + 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.parameters.at(3)); + = new DSA_PublicKey(group, reply.hostKeyParameters.at(3)); sigKey.reset(dsaKey); - verifier.reset(new PK_Verifier(*dsaKey, botanEmsaAlgoName(SshCapabilities::PubKeyDss))); } else if (m_serverHostKeyAlgo == SshCapabilities::PubKeyRsa) { RSA_PublicKey * const rsaKey - = new RSA_PublicKey(reply.parameters.at(1), reply.parameters.at(0)); + = new RSA_PublicKey(reply.hostKeyParameters.at(1), reply.hostKeyParameters.at(0)); sigKey.reset(rsaKey); - verifier.reset(new PK_Verifier(*rsaKey, botanEmsaAlgoName(SshCapabilities::PubKeyRsa))); } else { - Q_ASSERT(!"Impossible: Neither DSS nor RSA!"); + 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); - if (!verifier->verify_message(botanH, m_h.size(), botanSig, - reply.signatureBlob.size())) { + 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())) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - "Invalid signature in SSH_MSG_KEXDH_REPLY packet."); + "Invalid signature in key exchange reply packet."); } + checkHostKey(reply.k_s); + m_sendFacility.sendNewKeysPacket(); } +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 &serverCapabilities = serverToClient + ? kexInit.macAlgorithmsServerToClient.names + : kexInit.macAlgorithmsClientToServer.names; + *algo = SshCapabilities::findBestMatch(SshCapabilities::MacAlgorithms, serverCapabilities); +} + +void SshKeyExchange::checkHostKey(const QByteArray &hostKey) +{ + if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingNone) { + if (m_connParams.hostKeyDatabase) + m_connParams.hostKeyDatabase->insertHostKey(m_connParams.host(), hostKey); + return; + } + + if (!m_connParams.hostKeyDatabase) { + throw SshClientException(SshInternalError, + 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(); + break; + case SshHostKeyDatabase::KeyLookupNoMatch: + if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingStrict) + throwHostKeyException(); + break; + } + m_connParams.hostKeyDatabase->insertHostKey(m_connParams.host(), hostKey); +} + +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())); +} + } // namespace Internal } // namespace QSsh diff --git a/src/libs/ssh/sshkeyexchange_p.h b/src/libs/ssh/sshkeyexchange_p.h index ea3e599..c1fffa8 100644 --- a/src/libs/ssh/sshkeyexchange_p.h +++ b/src/libs/ssh/sshkeyexchange_p.h @@ -31,24 +31,30 @@ #ifndef SSHKEYEXCHANGE_P_H #define SSHKEYEXCHANGE_P_H +#include "sshconnection.h" + #include #include +#include + namespace Botan { class DH_PrivateKey; +class ECDH_PrivateKey; class HashFunction; } namespace QSsh { namespace Internal { +struct SshKeyExchangeInit; class SshSendFacility; class SshIncomingPacket; class SshKeyExchange { public: - SshKeyExchange(SshSendFacility &sendFacility); + SshKeyExchange(const SshConnectionParameters &connParams, SshSendFacility &sendFacility); ~SshKeyExchange(); void sendKexInitPacket(const QByteArray &serverId); @@ -61,17 +67,24 @@ class SshKeyExchange QByteArray k() const { return m_k; } QByteArray h() const { return m_h; } - Botan::HashFunction *hash() const { return m_hash.data(); } + 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; } private: + QByteArray hashAlgoForKexAlgo() const; + void determineHashingAlgorithm(const SshKeyExchangeInit &kexInit, bool serverToClient); + void checkHostKey(const QByteArray &hostKey); + Q_NORETURN void throwHostKeyException(); + QByteArray m_serverId; QByteArray m_clientKexInitPayload; QByteArray m_serverKexInitPayload; QScopedPointer m_dhKey; + QScopedPointer m_ecdhKey; + QByteArray m_kexAlgoName; QByteArray m_k; QByteArray m_h; QByteArray m_serverHostKeyAlgo; @@ -79,7 +92,8 @@ class SshKeyExchange QByteArray m_decryptionAlgo; QByteArray m_c2sHMacAlgo; QByteArray m_s2cHMacAlgo; - QScopedPointer m_hash; + std::unique_ptr m_hash; + const SshConnectionParameters m_connParams; SshSendFacility &m_sendFacility; }; diff --git a/src/libs/ssh/sshkeygenerator.cpp b/src/libs/ssh/sshkeygenerator.cpp index 54b2535..372e8db 100644 --- a/src/libs/ssh/sshkeygenerator.cpp +++ b/src/libs/ssh/sshkeygenerator.cpp @@ -32,7 +32,10 @@ #include "sshbotanconversions_p.h" #include "sshcapabilities_p.h" +#include "ssh_global.h" +#include "sshinit_p.h" #include "sshpacket_p.h" +#include "sshlogging_p.h" #include #include @@ -42,6 +45,8 @@ #include #include #include +#include +#include #include #include @@ -55,6 +60,7 @@ using namespace Internal; SshKeyGenerator::SshKeyGenerator() : m_type(Rsa) { + initSsh(); } bool SshKeyGenerator::generateKeys(KeyType type, PrivateKeyFormat format, int keySize, @@ -66,10 +72,19 @@ bool SshKeyGenerator::generateKeys(KeyType type, PrivateKeyFormat format, int ke try { AutoSeeded_RNG rng; KeyPtr key; - if (m_type == Rsa) + switch (m_type) { + case Rsa: key = KeyPtr(new RSA_PrivateKey(rng, keySize)); - else + break; + case Dsa: key = KeyPtr(new DSA_PrivateKey(rng, DL_Group(rng, DL_Group::DSA_Kosherizer, keySize))); + break; + case Ecdsa: { + const QByteArray algo = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth(keySize / 8); + key = KeyPtr(new ECDSA_PrivateKey(rng, EC_Group(SshCapabilities::oid(algo)))); + break; + } + } switch (format) { case Pkcs8: generatePkcs8KeyStrings(key, rng); @@ -83,20 +98,20 @@ bool SshKeyGenerator::generateKeys(KeyType type, PrivateKeyFormat format, int ke generateOpenSslPublicKeyString(key); } return true; - } catch (Botan::Exception &e) { - m_error = tr("Error generating key: %1").arg(QString::fromAscii(e.what())); + } catch (const std::exception &e) { + m_error = tr("Error generating key: %1").arg(QString::fromLocal8Bit(e.what())); return false; } } -void SshKeyGenerator::generatePkcs8KeyStrings(const KeyPtr &key, Botan::RandomNumberGenerator &rng) +void SshKeyGenerator::generatePkcs8KeyStrings(const KeyPtr &key, RandomNumberGenerator &rng) { generatePkcs8KeyString(key, false, rng); generatePkcs8KeyString(key, true, rng); } void SshKeyGenerator::generatePkcs8KeyString(const KeyPtr &key, bool privateKey, - Botan::RandomNumberGenerator &rng) + RandomNumberGenerator &rng) { Pipe pipe; pipe.start_msg(); @@ -115,9 +130,12 @@ void SshKeyGenerator::generatePkcs8KeyString(const KeyPtr &key, bool privateKey, keyData = &m_publicKey; } pipe.end_msg(); - keyData->resize(pipe.remaining(pipe.message_count() - 1)); - pipe.read(convertByteArray(*keyData), keyData->size(), + keyData->resize(static_cast(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) @@ -130,19 +148,36 @@ void SshKeyGenerator::generateOpenSslPublicKeyString(const KeyPtr &key) { QList params; QByteArray keyId; - if (m_type == Rsa) { + QByteArray q; + switch (m_type) { + case Rsa: { const QSharedPointer rsaKey = key.dynamicCast(); params << rsaKey->get_e() << rsaKey->get_n(); keyId = SshCapabilities::PubKeyRsa; - } else { + break; + } + case Dsa: { const QSharedPointer dsaKey = key.dynamicCast(); params << dsaKey->group_p() << dsaKey->group_q() << dsaKey->group_g() << dsaKey->get_y(); keyId = SshCapabilities::PubKeyDss; + break; + } + case Ecdsa: { + const auto ecdsaKey = key.dynamicCast(); + q = convertByteArray(ecdsaKey->public_point().encode(PointGFp::UNCOMPRESSED)); + keyId = SshCapabilities::ecdsaPubKeyAlgoForKeyWidth( + static_cast(ecdsaKey->private_value().bytes())); + break; + } } QByteArray publicKeyBlob = AbstractSshPacket::encodeString(keyId); foreach (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(); @@ -152,21 +187,31 @@ void SshKeyGenerator::generateOpenSslPublicKeyString(const KeyPtr &key) void SshKeyGenerator::generateOpenSslPrivateKeyString(const KeyPtr &key) { QList params; - QByteArray keyId; - const char *label; - if (m_type == Rsa) { + const char *label = ""; + switch (m_type) { + case Rsa: { const QSharedPointer rsaKey = key.dynamicCast(); params << rsaKey->get_n() << rsaKey->get_e() << rsaKey->get_d() << rsaKey->get_p() << rsaKey->get_q(); - keyId = SshCapabilities::PubKeyRsa; + 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"; - } else { + break; + } + case Dsa: { const QSharedPointer dsaKey = key.dynamicCast(); params << dsaKey->group_p() << dsaKey->group_q() << dsaKey->group_g() << dsaKey->get_y() << dsaKey->get_x(); - keyId = SshCapabilities::PubKeyDss; label = "DSA PRIVATE KEY"; + break; + } + case Ecdsa: + params << key.dynamicCast()->private_value(); + label = "EC PRIVATE KEY"; + break; } DER_Encoder encoder; diff --git a/src/libs/ssh/sshkeygenerator.h b/src/libs/ssh/sshkeygenerator.h index 67f146a..ca8a8e1 100644 --- a/src/libs/ssh/sshkeygenerator.h +++ b/src/libs/ssh/sshkeygenerator.h @@ -47,7 +47,7 @@ class QSSH_EXPORT SshKeyGenerator { Q_DECLARE_TR_FUNCTIONS(SshKeyGenerator) public: - enum KeyType { Rsa, Dsa }; + enum KeyType { Rsa, Dsa, Ecdsa }; enum PrivateKeyFormat { Pkcs8, OpenSsl, Mixed }; enum EncryptionMode { DoOfferEncryption, DoNotOfferEncryption }; // Only relevant for Pkcs8 format. diff --git a/src/libs/ssh/sshlogging.cpp b/src/libs/ssh/sshlogging.cpp new file mode 100644 index 0000000..ad7c0da --- /dev/null +++ b/src/libs/ssh/sshlogging.cpp @@ -0,0 +1,37 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** 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 +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "sshlogging_p.h" + +namespace QSsh { +namespace Internal { +Q_LOGGING_CATEGORY(sshLog, "qtc.ssh", QtWarningMsg) +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshlogging_p.h b/src/libs/ssh/sshlogging_p.h new file mode 100644 index 0000000..bf41ef2 --- /dev/null +++ b/src/libs/ssh/sshlogging_p.h @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** 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 +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef SSHLOGGING_P_H +#define SSHLOGGING_P_H + +#include + +namespace QSsh { +namespace Internal { +Q_DECLARE_LOGGING_CATEGORY(sshLog) +} // namespace Internal +} // namespace QSsh + +#endif // Include guard diff --git a/src/libs/ssh/sshoutgoingpacket.cpp b/src/libs/ssh/sshoutgoingpacket.cpp index 97b199b..f96f0ee 100644 --- a/src/libs/ssh/sshoutgoingpacket.cpp +++ b/src/libs/ssh/sshoutgoingpacket.cpp @@ -30,8 +30,11 @@ #include "sshoutgoingpacket_p.h" +#include "sshagent_p.h" #include "sshcapabilities_p.h" #include "sshcryptofacility_p.h" +#include "sshlogging_p.h" +#include "sshpacketparser_p.h" #include @@ -89,6 +92,11 @@ 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(); @@ -104,23 +112,70 @@ void SshOutgoingPacket::generateServiceRequest(const QByteArray &service) init(SSH_MSG_SERVICE_REQUEST).appendString(service).finalize(); } -void SshOutgoingPacket::generateUserAuthByPwdRequestPacket(const QByteArray &user, +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 + else + appendString("password").appendBool(false).appendString(pwd); + finalize(); +} + +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("password").appendBool(false).appendString(pwd) - .finalize(); + .appendString("publickey").appendBool(true); + if (!key.isEmpty()) { + appendString(SshPacketParser::asString(key, quint32(0))); + appendString(key); + appendString(signature); + } else { + appendString(m_encrypter.authenticationAlgorithmName()); + appendString(m_encrypter.authenticationPublicKey()); + const QByteArray &dataToSign = m_data.mid(PayloadOffset); + appendString(m_encrypter.authenticationKeySignature(dataToSign)); + } + finalize(); +} + +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())); + init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service) + .appendString("publickey").appendBool(false).appendString(algoName) + .appendString(publicKey).finalize(); } -void SshOutgoingPacket::generateUserAuthByKeyRequestPacket(const QByteArray &user, - const QByteArray &service) +void SshOutgoingPacket::generateUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, + const QByteArray &service) { + // RFC 4256, 3.1 init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service) - .appendString("publickey").appendBool(true) - .appendString(m_encrypter.authenticationAlgorithmName()) - .appendString(m_encrypter.authenticationPublicKey()); - const QByteArray &dataToSign = m_data.mid(PayloadOffset); - appendString(m_encrypter.authenticationKeySignature(dataToSign)); + .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()); + foreach (const QString &response, responses) + appendString(response.toUtf8()); finalize(); } @@ -143,7 +198,29 @@ void SshOutgoingPacket::generateSessionPacket(quint32 channelId, quint32 windowSize, quint32 maxPacketSize) { init(SSH_MSG_CHANNEL_OPEN).appendString("session").appendInt(channelId) - .appendInt(windowSize).appendInt(maxPacketSize).finalize(); + .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) + .appendString(bindAddress).appendInt(bindPort).finalize(); } void SshOutgoingPacket::generateEnvPacket(quint32 remoteChannel, @@ -153,6 +230,14 @@ void SshOutgoingPacket::generateEnvPacket(quint32 remoteChannel, .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) { @@ -222,6 +307,22 @@ 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, const QByteArray &reasonString) { @@ -291,13 +392,9 @@ void SshOutgoingPacket::finalize() setPadding(); setLengthField(m_data); m_length = m_data.size() - 4; -#ifdef CREATOR_SSH_DEBUG - qDebug("Encrypting packet of type %u", m_data.at(TypeOffset)); -#endif + qCDebug(sshLog, "Encrypting packet of type %u", m_data.at(TypeOffset)); encrypt(); -#ifdef CREATOR_SSH_DEBUG - qDebug("Sending packet of size %d", rawData().count()); -#endif + qCDebug(sshLog, "Sending packet of size %d", rawData().count()); Q_ASSERT(isComplete()); } diff --git a/src/libs/ssh/sshoutgoingpacket_p.h b/src/libs/ssh/sshoutgoingpacket_p.h index 60b2d30..804fdf6 100644 --- a/src/libs/ssh/sshoutgoingpacket_p.h +++ b/src/libs/ssh/sshoutgoingpacket_p.h @@ -35,6 +35,8 @@ #include "sshpseudoterminal.h" +#include + namespace QSsh { namespace Internal { @@ -48,22 +50,35 @@ class SshOutgoingPacket : public AbstractSshPacket 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 generateUserAuthByPwdRequestPacket(const QByteArray &user, + void generateUserAuthByPasswordRequestPacket(const QByteArray &user, const QByteArray &service, const QByteArray &pwd); - void generateUserAuthByKeyRequestPacket(const QByteArray &user, + 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, + quint32 maxPacketSize, const QByteArray &remoteHost, quint32 remotePort, + 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); @@ -76,6 +91,10 @@ class SshOutgoingPacket : public AbstractSshPacket 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, + const QByteArray &reasonString); private: virtual quint32 cipherBlockSize() const; diff --git a/src/libs/ssh/sshpacket.cpp b/src/libs/ssh/sshpacket.cpp index 4710c91..94238ff 100644 --- a/src/libs/ssh/sshpacket.cpp +++ b/src/libs/ssh/sshpacket.cpp @@ -33,6 +33,7 @@ #include "sshcapabilities_p.h" #include "sshcryptofacility_p.h" #include "sshexception_p.h" +#include "sshlogging_p.h" #include "sshpacketparser_p.h" #include @@ -47,18 +48,10 @@ const quint32 AbstractSshPacket::PayloadOffset = PaddingLengthOffset + 1; const quint32 AbstractSshPacket::TypeOffset = PayloadOffset; const quint32 AbstractSshPacket::MinPaddingLength = 4; -namespace { - - void printByteArray(const QByteArray &data) - { -#ifdef CREATOR_SSH_DEBUG - for (int i = 0; i < data.count(); ++i) - qDebug() << std::hex << (static_cast(data[i]) & 0xff) << " "; -#else - Q_UNUSED(data); -#endif - } -} // anonymous namespace +static void printByteArray(const QByteArray &data) +{ + qCDebug(sshLog, "%s", data.toHex().constData()); +} AbstractSshPacket::AbstractSshPacket() : m_length(0) { } @@ -108,7 +101,7 @@ QByteArray AbstractSshPacket::encodeMpInt(const Botan::BigInt &number) if (number.is_zero()) return QByteArray(4, 0); - int stringLength = number.bytes(); + int stringLength = static_cast(number.bytes()); const bool positiveAndMsbSet = number.sign() == Botan::BigInt::Positive && (number.byte_at(stringLength - 1) & 0x80); if (positiveAndMsbSet) diff --git a/src/libs/ssh/sshpacket_p.h b/src/libs/ssh/sshpacket_p.h index 883aed4..21b339a 100644 --- a/src/libs/ssh/sshpacket_p.h +++ b/src/libs/ssh/sshpacket_p.h @@ -53,7 +53,9 @@ enum SshPacketType { 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, @@ -61,6 +63,8 @@ enum SshPacketType { 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, @@ -86,6 +90,13 @@ enum SshPacketType { 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; diff --git a/src/libs/ssh/sshpacketparser.cpp b/src/libs/ssh/sshpacketparser.cpp index 2bb0290..0d59c10 100644 --- a/src/libs/ssh/sshpacketparser.cpp +++ b/src/libs/ssh/sshpacketparser.cpp @@ -31,6 +31,7 @@ #include "sshpacketparser_p.h" #include +#include namespace QSsh { namespace Internal { @@ -68,10 +69,7 @@ quint32 SshPacketParser::asUint32(const QByteArray &data, quint32 offset) { if (size(data) < offset + 4) throw SshPacketParseException(); - const quint32 value = ((data.at(offset) & 0xff) << 24) - + ((data.at(offset + 1) & 0xff) << 16) - + ((data.at(offset + 2) & 0xff) << 8) + (data.at(offset + 3) & 0xff); - return value; + return qFromBigEndian(data.constData() + offset); } quint32 SshPacketParser::asUint32(const QByteArray &data, quint32 *offset) @@ -85,15 +83,7 @@ quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 offset) { if (size(data) < offset + 8) throw SshPacketParseException(); - const quint64 value = (static_cast(data.at(offset) & 0xff) << 56) - + (static_cast(data.at(offset + 1) & 0xff) << 48) - + (static_cast(data.at(offset + 2) & 0xff) << 40) - + (static_cast(data.at(offset + 3) & 0xff) << 32) - + ((data.at(offset + 4) & 0xff) << 24) - + ((data.at(offset + 5) & 0xff) << 16) - + ((data.at(offset + 6) & 0xff) << 8) - + (data.at(offset + 7) & 0xff); - return value; + return qFromBigEndian(data.constData() + offset); } quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 *offset) @@ -103,6 +93,11 @@ quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 *offset) 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); diff --git a/src/libs/ssh/sshpacketparser_p.h b/src/libs/ssh/sshpacketparser_p.h index 56cb465..223e069 100644 --- a/src/libs/ssh/sshpacketparser_p.h +++ b/src/libs/ssh/sshpacketparser_p.h @@ -68,6 +68,7 @@ class SshPacketParser 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); diff --git a/src/libs/ssh/sshremoteprocess.cpp b/src/libs/ssh/sshremoteprocess.cpp index 294d1d4..dcbff35 100644 --- a/src/libs/ssh/sshremoteprocess.cpp +++ b/src/libs/ssh/sshremoteprocess.cpp @@ -30,9 +30,12 @@ #include "sshremoteprocess.h" #include "sshremoteprocess_p.h" +#include "sshlogging_p.h" +#include "ssh_global.h" #include "sshincomingpacket_p.h" #include "sshsendfacility_p.h" +#include "sshx11displayinfo_p.h" #include @@ -41,32 +44,19 @@ #include #include -/*! - \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. - */ - namespace QSsh { 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::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, @@ -84,9 +74,8 @@ SshRemoteProcess::SshRemoteProcess(quint32 channelId, Internal::SshSendFacility SshRemoteProcess::~SshRemoteProcess() { - Q_ASSERT(d->channelState() == Internal::SshRemoteProcessPrivate::Inactive - || d->channelState() == Internal::SshRemoteProcessPrivate::CloseRequested - || d->channelState() == Internal::SshRemoteProcessPrivate::Closed); + QSSH_ASSERT(d->channelState() != Internal::AbstractSshChannel::SessionEstablished); + close(); delete d; } @@ -159,14 +148,18 @@ void SshRemoteProcess::setReadChannel(QProcess::ProcessChannel channel) void SshRemoteProcess::init() { - connect(d, SIGNAL(started()), this, SIGNAL(started()), - Qt::QueuedConnection); - connect(d, SIGNAL(readyReadStandardOutput()), this, SIGNAL(readyReadStandardOutput()), - Qt::QueuedConnection); - connect(d, SIGNAL(readyRead()), this, SIGNAL(readyRead()), Qt::QueuedConnection); - connect(d, SIGNAL(readyReadStandardError()), this, - SIGNAL(readyReadStandardError()), Qt::QueuedConnection); - connect(d, SIGNAL(closed(int)), this, SIGNAL(closed(int)), Qt::QueuedConnection); + 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) @@ -175,6 +168,11 @@ void SshRemoteProcess::addToEnvironment(const QByteArray &var, const QByteArray 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); @@ -182,12 +180,16 @@ void SshRemoteProcess::requestTerminal(const SshPseudoTerminal &terminal) d->m_terminal = terminal; } +void SshRemoteProcess::requestX11Forwarding(const QString &displayName) +{ + QSSH_ASSERT_AND_RETURN(d->channelState() == Internal::SshRemoteProcessPrivate::Inactive); + d->m_x11DisplayName = displayName; +} + void SshRemoteProcess::start() { if (d->channelState() == Internal::SshRemoteProcessPrivate::Inactive) { -#ifdef CREATOR_SSH_DEBUG - qDebug("process start requested, channel id = %u", d->localChannelId()); -#endif + qCDebug(Internal::sshLog, "process start requested, channel id = %u", d->localChannelId()); QIODevice::open(QIODevice::ReadWrite); d->requestSessionStart(); } @@ -205,8 +207,8 @@ void SshRemoteProcess::sendSignal(Signal signal) QSSH_ASSERT_AND_RETURN(signalString); d->m_sendFacility.sendChannelSignalPacket(d->remoteChannel(), signalString); } - } catch (Botan::Exception &e) { - setErrorString(QString::fromAscii(e.what())); + } catch (const std::exception &e) { + setErrorString(QString::fromLocal8Bit(e.what())); d->closeChannel(); } } @@ -225,6 +227,14 @@ SshRemoteProcess::Signal SshRemoteProcess::exitSignal() const namespace Internal { +void SshRemoteProcessPrivate::failToStart(const QString &reason) +{ + if (m_procState != NotYetStarted) + return; + m_proc->setErrorString(reason); + setProcState(StartFailed); +} + SshRemoteProcessPrivate::SshRemoteProcessPrivate(const QByteArray &command, quint32 channelId, SshSendFacility &sendFacility, SshRemoteProcess *proc) : AbstractSshChannel(channelId, sendFacility), @@ -257,9 +267,7 @@ void SshRemoteProcessPrivate::init() void SshRemoteProcessPrivate::setProcState(ProcessState newState) { -#ifdef CREATOR_SSH_DEBUG - qDebug("channel: old state = %d,new state = %d", m_procState, newState); -#endif + qCDebug(sshLog, "channel: old state = %d,new state = %d", m_procState, newState); m_procState = newState; if (newState == StartFailed) { emit closed(SshRemoteProcess::FailedToStart); @@ -286,26 +294,41 @@ void SshRemoteProcessPrivate::closeHook() void SshRemoteProcessPrivate::handleOpenSuccessInternal() { - foreach (const EnvVar &envVar, m_env) { - m_sendFacility.sendEnvPacket(remoteChannel(), envVar.first, - envVar.second); - } + if (m_x11DisplayName.isEmpty()) + startProcess(X11DisplayInfo()); + else + emit x11ForwardingRequested(m_x11DisplayName); +} + +void SshRemoteProcessPrivate::startProcess(const X11DisplayInfo &displayInfo) +{ + if (m_procState != NotYetStarted) + return; + + foreach (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_useTerminal) + m_sendFacility.sendPtyRequestPacket(remoteChannel(), m_terminal); - if (m_isShell) - m_sendFacility.sendShellPacket(remoteChannel()); - else - m_sendFacility.sendExecPacket(remoteChannel(), m_command); - setProcState(ExecRequested); - m_timeoutTimer->start(ReplyTimeout); + if (m_isShell) + m_sendFacility.sendShellPacket(remoteChannel()); + else + m_sendFacility.sendExecPacket(remoteChannel(), m_command); + setProcState(ExecRequested); + m_timeoutTimer.start(ReplyTimeout); } void SshRemoteProcessPrivate::handleOpenFailureInternal(const QString &reason) { - setProcState(StartFailed); - m_proc->setErrorString(reason); + failToStart(reason); } void SshRemoteProcessPrivate::handleChannelSuccess() @@ -314,7 +337,7 @@ void SshRemoteProcessPrivate::handleChannelSuccess() throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_SUCCESS message."); } - m_timeoutTimer->stop(); + m_timeoutTimer.stop(); setProcState(Running); } @@ -324,7 +347,7 @@ void SshRemoteProcessPrivate::handleChannelFailure() throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_FAILURE message."); } - m_timeoutTimer->stop(); + m_timeoutTimer.stop(); setProcState(StartFailed); closeChannel(); } @@ -341,7 +364,7 @@ void SshRemoteProcessPrivate::handleChannelExtendedDataInternal(quint32 type, const QByteArray &data) { if (type != SSH_EXTENDED_DATA_STDERR) { - qWarning("Unknown extended data type %u", type); + qCWarning(sshLog, "Unknown extended data type %u", type); } else { m_stderr += data; emit readyReadStandardError(); @@ -352,18 +375,14 @@ void SshRemoteProcessPrivate::handleChannelExtendedDataInternal(quint32 type, void SshRemoteProcessPrivate::handleExitStatus(const SshChannelExitStatus &exitStatus) { -#ifdef CREATOR_SSH_DEBUG - qDebug("Process exiting with exit code %d", exitStatus.exitStatus); -#endif + qCDebug(sshLog, "Process exiting with exit code %d", exitStatus.exitStatus); m_exitCode = exitStatus.exitStatus; m_procState = Exited; } void SshRemoteProcessPrivate::handleExitSignal(const SshChannelExitSignal &signal) { -#ifdef CREATOR_SSH_DEBUG - qDebug("Exit due to signal %s", signal.signal.data()); -#endif + 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) { @@ -375,7 +394,7 @@ void SshRemoteProcessPrivate::handleExitSignal(const SshChannelExitSignal &signa } throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid signal", - tr("Server sent invalid signal '%1'").arg(QString::fromUtf8(signal.signal))); + tr("Server sent invalid signal \"%1\"").arg(QString::fromUtf8(signal.signal))); } } // namespace Internal diff --git a/src/libs/ssh/sshremoteprocess.h b/src/libs/ssh/sshremoteprocess.h index 0ec44a6..5dcd5c6 100644 --- a/src/libs/ssh/sshremoteprocess.h +++ b/src/libs/ssh/sshremoteprocess.h @@ -48,6 +48,19 @@ class SshRemoteProcessPrivate; class SshSendFacility; } // namespace Internal +/*! + \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 { @@ -81,8 +94,10 @@ class QSSH_EXPORT SshRemoteProcess : public QIODevice * 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; diff --git a/src/libs/ssh/sshremoteprocess_p.h b/src/libs/ssh/sshremoteprocess_p.h index 57bf3f9..5f111b6 100644 --- a/src/libs/ssh/sshremoteprocess_p.h +++ b/src/libs/ssh/sshremoteprocess_p.h @@ -44,6 +44,7 @@ class SshRemoteProcess; namespace Internal { class SshSendFacility; +class X11DisplayInfo; class SshRemoteProcessPrivate : public AbstractSshChannel { @@ -54,12 +55,8 @@ class SshRemoteProcessPrivate : public AbstractSshChannel NotYetStarted, ExecRequested, StartFailed, Running, Exited }; - virtual void handleChannelSuccess(); - virtual void handleChannelFailure(); - - virtual void closeHook(); - - QByteArray &data(); + void failToStart(const QString &reason); + void startProcess(const X11DisplayInfo &displayInfo); signals: void started(); @@ -67,6 +64,7 @@ class SshRemoteProcessPrivate : public AbstractSshChannel void readyReadStandardOutput(); void readyReadStandardError(); void closed(int exitStatus); + void x11ForwardingRequested(const QString &display); private: SshRemoteProcessPrivate(const QByteArray &command, quint32 channelId, @@ -74,6 +72,9 @@ class SshRemoteProcessPrivate : public AbstractSshChannel SshRemoteProcessPrivate(quint32 channelId, SshSendFacility &sendFacility, SshRemoteProcess *proc); + virtual void handleChannelSuccess(); + virtual void handleChannelFailure(); + virtual void handleOpenSuccessInternal(); virtual void handleOpenFailureInternal(const QString &reason); virtual void handleChannelDataInternal(const QByteArray &data); @@ -82,8 +83,11 @@ class SshRemoteProcessPrivate : public AbstractSshChannel virtual void handleExitStatus(const SshChannelExitStatus &exitStatus); virtual void handleExitSignal(const SshChannelExitSignal &signal); + virtual void closeHook(); + void init(); void setProcState(ProcessState newState); + QByteArray &data(); QProcess::ProcessChannel m_readChannel; @@ -100,6 +104,8 @@ class SshRemoteProcessPrivate : public AbstractSshChannel bool m_useTerminal; SshPseudoTerminal m_terminal; + QString m_x11DisplayName; + QByteArray m_stdout; QByteArray m_stderr; diff --git a/src/libs/ssh/sshremoteprocessrunner.cpp b/src/libs/ssh/sshremoteprocessrunner.cpp index b1d7631..ba3dcca 100644 --- a/src/libs/ssh/sshremoteprocessrunner.cpp +++ b/src/libs/ssh/sshremoteprocessrunner.cpp @@ -33,13 +33,6 @@ #include "sshconnectionmanager.h" #include "sshpseudoterminal.h" - -/*! - \class QSsh::SshRemoteProcessRunner - - \brief Convenience class for running a remote process over an SSH connection. -*/ - namespace QSsh { namespace Internal { namespace { @@ -111,14 +104,15 @@ void SshRemoteProcessRunner::runInternal(const QByteArray &command, d->m_exitSignal = SshRemoteProcess::NoSignal; d->m_exitCode = -1; d->m_command = command; - d->m_connection = SshConnectionManager::instance().acquireConnection(sshParams); - connect(d->m_connection, SIGNAL(error(QSsh::SshError)), - SLOT(handleConnectionError(QSsh::SshError))); - connect(d->m_connection, SIGNAL(disconnected()), SLOT(handleDisconnected())); + 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(); } else { - connect(d->m_connection, SIGNAL(connected()), SLOT(handleConnected())); + connect(d->m_connection, &SshConnection::connected, this, &SshRemoteProcessRunner::handleConnected); if (d->m_connection->state() == SshConnection::Unconnected) d->m_connection->connectToHost(); } @@ -130,10 +124,14 @@ void SshRemoteProcessRunner::handleConnected() setState(Connected); d->m_process = d->m_connection->createRemoteProcess(d->m_command); - connect(d->m_process.data(), SIGNAL(started()), SLOT(handleProcessStarted())); - connect(d->m_process.data(), SIGNAL(closed(int)), SLOT(handleProcessFinished(int))); - connect(d->m_process.data(), SIGNAL(readyReadStandardOutput()), SLOT(handleStdout())); - connect(d->m_process.data(), SIGNAL(readyReadStandardError()), SLOT(handleStderr())); + 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(); @@ -211,7 +209,7 @@ void SshRemoteProcessRunner::setState(int newState) } if (d->m_connection) { disconnect(d->m_connection, 0, this, 0); - SshConnectionManager::instance().releaseConnection(d->m_connection); + QSsh::releaseConnection(d->m_connection); d->m_connection = 0; } } diff --git a/src/libs/ssh/sshremoteprocessrunner.h b/src/libs/ssh/sshremoteprocessrunner.h index e65c4a6..49522f3 100644 --- a/src/libs/ssh/sshremoteprocessrunner.h +++ b/src/libs/ssh/sshremoteprocessrunner.h @@ -39,6 +39,12 @@ namespace Internal { class SshRemoteProcessRunnerPrivate; } // namespace Internal +/*! + \class QSsh::SshRemoteProcessRunner + + \brief Convenience class for running a remote process over an SSH connection. +*/ + class QSSH_EXPORT SshRemoteProcessRunner : public QObject { Q_OBJECT @@ -73,7 +79,7 @@ class QSSH_EXPORT SshRemoteProcessRunner : public QObject void readyReadStandardError(); void processClosed(int exitStatus); // values are of type SshRemoteProcess::ExitStatus -private slots: +private: void handleConnected(); void handleConnectionError(QSsh::SshError error); void handleDisconnected(); @@ -81,8 +87,6 @@ private slots: void handleProcessFinished(int exitStatus); void handleStdout(); void handleStderr(); - -private: void runInternal(const QByteArray &command, const QSsh::SshConnectionParameters &sshParams); void setState(int newState); diff --git a/src/libs/ssh/sshsendfacility.cpp b/src/libs/ssh/sshsendfacility.cpp index 07f0a37..20a0c95 100644 --- a/src/libs/ssh/sshsendfacility.cpp +++ b/src/libs/ssh/sshsendfacility.cpp @@ -31,6 +31,7 @@ #include "sshsendfacility_p.h" #include "sshkeyexchange_p.h" +#include "sshlogging_p.h" #include "sshoutgoingpacket_p.h" #include @@ -46,9 +47,7 @@ SshSendFacility::SshSendFacility(QTcpSocket *socket) void SshSendFacility::sendPacket() { -#ifdef CREATOR_SSH_DEBUG - qDebug("Sending packet, client seq nr is %u", m_clientSeqNr); -#endif + 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()); @@ -85,6 +84,12 @@ void SshSendFacility::sendKeyDhInitPacket(const Botan::BigInt &e) sendPacket(); } +void SshSendFacility::sendKeyEcdhInitPacket(const QByteArray &clientQ) +{ + m_outgoingPacket.generateKeyEcdhInitPacket(clientQ); + sendPacket(); +} + void SshSendFacility::sendNewKeysPacket() { m_outgoingPacket.generateNewKeysPacket(); @@ -110,17 +115,37 @@ void SshSendFacility::sendUserAuthServiceRequestPacket() sendPacket(); } -void SshSendFacility::sendUserAuthByPwdRequestPacket(const QByteArray &user, +void SshSendFacility::sendUserAuthByPasswordRequestPacket(const QByteArray &user, const QByteArray &service, const QByteArray &pwd) { - m_outgoingPacket.generateUserAuthByPwdRequestPacket(user, service, pwd); + m_outgoingPacket.generateUserAuthByPasswordRequestPacket(user, service, pwd); sendPacket(); } -void SshSendFacility::sendUserAuthByKeyRequestPacket(const QByteArray &user, - const QByteArray &service) +void SshSendFacility::sendUserAuthByPublicKeyRequestPacket(const QByteArray &user, + const QByteArray &service, const QByteArray &key, const QByteArray &signature) { - m_outgoingPacket.generateUserAuthByKeyRequestPacket(user, service); + m_outgoingPacket.generateUserAuthByPublicKeyRequestPacket(user, service, key, signature); + sendPacket(); +} + +void SshSendFacility::sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service, + const QByteArray &publicKey) +{ + m_outgoingPacket.generateQueryPublicKeyPacket(user, service, publicKey); + sendPacket(); +} + +void SshSendFacility::sendUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, + const QByteArray &service) +{ + m_outgoingPacket.generateUserAuthByKeyboardInteractiveRequestPacket(user, service); + sendPacket(); +} + +void SshSendFacility::sendUserAuthInfoResponsePacket(const QStringList &responses) +{ + m_outgoingPacket.generateUserAuthInfoResponsePacket(responses); sendPacket(); } @@ -150,6 +175,27 @@ void SshSendFacility::sendSessionPacket(quint32 channelId, quint32 windowSize, sendPacket(); } +void SshSendFacility::sendDirectTcpIpPacket(quint32 channelId, quint32 windowSize, + quint32 maxPacketSize, const QByteArray &remoteHost, quint32 remotePort, + const QByteArray &localIpAddress, quint32 localPort) +{ + m_outgoingPacket.generateDirectTcpIpPacket(channelId, windowSize, maxPacketSize, remoteHost, + remotePort, localIpAddress, localPort); + sendPacket(); +} + +void SshSendFacility::sendTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort) +{ + m_outgoingPacket.generateTcpIpForwardPacket(bindAddress, bindPort); + sendPacket(); +} + +void SshSendFacility::sendCancelTcpIpForwardPacket(const QByteArray &bindAddress, quint32 bindPort) +{ + m_outgoingPacket.generateCancelTcpIpForwardPacket(bindAddress, bindPort); + sendPacket(); +} + void SshSendFacility::sendPtyRequestPacket(quint32 remoteChannel, const SshPseudoTerminal &terminal) { @@ -164,6 +210,13 @@ void SshSendFacility::sendEnvPacket(quint32 remoteChannel, sendPacket(); } +void SshSendFacility::sendX11ForwardingPacket(quint32 remoteChannel, const QByteArray &protocol, + const QByteArray &cookie, quint32 screenNumber) +{ + m_outgoingPacket.generateX11ForwardingPacket(remoteChannel, protocol, cookie, screenNumber); + sendPacket(); +} + void SshSendFacility::sendExecPacket(quint32 remoteChannel, const QByteArray &command) { @@ -216,5 +269,20 @@ void SshSendFacility::sendChannelClosePacket(quint32 remoteChannel) sendPacket(); } +void SshSendFacility::sendChannelOpenConfirmationPacket(quint32 remoteChannel, quint32 localChannel, + quint32 localWindowSize, quint32 maxPacketSize) +{ + m_outgoingPacket.generateChannelOpenConfirmationPacket(remoteChannel, localChannel, + localWindowSize, maxPacketSize); + sendPacket(); +} + +void SshSendFacility::sendChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason, + const QByteArray &reasonString) +{ + m_outgoingPacket.generateChannelOpenFailurePacket(remoteChannel, reason, reasonString); + sendPacket(); +} + } // namespace Internal } // namespace QSsh diff --git a/src/libs/ssh/sshsendfacility_p.h b/src/libs/ssh/sshsendfacility_p.h index d7d20d9..03690d8 100644 --- a/src/libs/ssh/sshsendfacility_p.h +++ b/src/libs/ssh/sshsendfacility_p.h @@ -34,6 +34,8 @@ #include "sshcryptofacility_p.h" #include "sshoutgoingpacket_p.h" +#include + QT_BEGIN_NAMESPACE class QTcpSocket; QT_END_NAMESPACE @@ -53,26 +55,41 @@ class SshSendFacility void recreateKeys(const SshKeyExchange &keyExchange); void createAuthenticationKey(const QByteArray &privKeyFileContents); + 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, const QByteArray &reasonString); void sendMsgUnimplementedPacket(quint32 serverSeqNr); void sendUserAuthServiceRequestPacket(); - void sendUserAuthByPwdRequestPacket(const QByteArray &user, + void sendUserAuthByPasswordRequestPacket(const QByteArray &user, const QByteArray &service, const QByteArray &pwd); - void sendUserAuthByKeyRequestPacket(const QByteArray &user, + void sendUserAuthByPublicKeyRequestPacket(const QByteArray &user, + const QByteArray &service, const QByteArray &key, const QByteArray &signature); + void sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service, + const QByteArray &publicKey); + void sendUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, const QByteArray &service); + void sendUserAuthInfoResponsePacket(const QStringList &responses); void sendRequestFailurePacket(); void sendIgnorePacket(); void sendInvalidPacket(); void sendSessionPacket(quint32 channelId, quint32 windowSize, quint32 maxPacketSize); + 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, const SshPseudoTerminal &terminal); void sendEnvPacket(quint32 remoteChannel, const QByteArray &var, const QByteArray &value); + void sendX11ForwardingPacket(quint32 remoteChannel, const QByteArray &protocol, + const QByteArray &cookie, quint32 screenNumber); void sendExecPacket(quint32 remoteChannel, const QByteArray &command); void sendShellPacket(quint32 remoteChannel); void sendSftpPacket(quint32 remoteChannel); @@ -82,6 +99,10 @@ class SshSendFacility const QByteArray &signalName); void sendChannelEofPacket(quint32 remoteChannel); void sendChannelClosePacket(quint32 remoteChannel); + void sendChannelOpenConfirmationPacket(quint32 remoteChannel, quint32 localChannel, + quint32 localWindowSize, quint32 maxPackeSize); + void sendChannelOpenFailurePacket(quint32 remoteChannel, quint32 reason, + const QByteArray &reasonString); quint32 nextClientSeqNr() const { return m_clientSeqNr; } private: diff --git a/src/libs/ssh/sshtcpipforwardserver.cpp b/src/libs/ssh/sshtcpipforwardserver.cpp new file mode 100644 index 0000000..5501ea9 --- /dev/null +++ b/src/libs/ssh/sshtcpipforwardserver.cpp @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "sshtcpipforwardserver.h" +#include "sshtcpipforwardserver_p.h" + +#include "sshlogging_p.h" +#include "sshsendfacility_p.h" + +namespace QSsh { +namespace Internal { + +SshTcpIpForwardServerPrivate::SshTcpIpForwardServerPrivate(const QString &bindAddress, + quint16 bindPort, SshSendFacility &sendFacility) + : m_sendFacility(sendFacility), + m_bindAddress(bindAddress), + m_bindPort(bindPort), + m_state(SshTcpIpForwardServer::Inactive) +{ +} + +} // namespace Internal + +using namespace Internal; + +SshTcpIpForwardServer::SshTcpIpForwardServer(const QString &bindAddress, quint16 bindPort, + SshSendFacility &sendFacility) + : 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() +{ + delete d; +} + +void SshTcpIpForwardServer::initialize() +{ + if (d->m_state == Inactive || d->m_state == Closing) { + try { + d->m_state = Initializing; + emit stateChanged(Initializing); + d->m_sendFacility.sendTcpIpForwardPacket(d->m_bindAddress.toUtf8(), d->m_bindPort); + d->m_timeoutTimer.start(d->ReplyTimeout); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); + d->m_timeoutTimer.stop(); + setClosed(); + } + } +} + +void SshTcpIpForwardServer::close() +{ + d->m_timeoutTimer.stop(); + + if (d->m_state == Initializing || d->m_state == Listening) { + try { + d->m_state = Closing; + emit stateChanged(Closing); + d->m_sendFacility.sendCancelTcpIpForwardPacket(d->m_bindAddress.toUtf8(), + d->m_bindPort); + d->m_timeoutTimer.start(d->ReplyTimeout); + } catch (const std::exception &e) { + qCWarning(sshLog, "Botan error: %s", e.what()); + d->m_timeoutTimer.stop(); + setClosed(); + } + } +} + +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(); +} + +} // namespace QSsh diff --git a/src/libs/ssh/sshtcpipforwardserver.h b/src/libs/ssh/sshtcpipforwardserver.h new file mode 100644 index 0000000..2a0ed7e --- /dev/null +++ b/src/libs/ssh/sshtcpipforwardserver.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include "ssh_global.h" +#include "sshforwardedtcpiptunnel.h" +#include + +namespace QSsh { + +namespace Internal { +class SshChannelManager; +class SshTcpIpForwardServerPrivate; +class SshSendFacility; +class SshConnectionPrivate; +} // namespace Internal + +class QSSH_EXPORT SshTcpIpForwardServer : public QObject +{ + Q_OBJECT + friend class Internal::SshChannelManager; + friend class Internal::SshConnectionPrivate; + +public: + enum State { + Inactive, + Initializing, + Listening, + Closing + }; + + typedef QSharedPointer Ptr; + ~SshTcpIpForwardServer(); + + const QString &bindAddress() const; + quint16 port() const; + State state() const; + void initialize(); + void close(); + + SshForwardedTcpIpTunnel::Ptr nextPendingConnection(); + +signals: + void error(const QString &reason); + void newConnection(); + void stateChanged(State state); + +private: + SshTcpIpForwardServer(const QString &bindAddress, quint16 bindPort, + Internal::SshSendFacility &sendFacility); + void setListening(quint16 port); + void setClosed(); + void setNewConnection(const SshForwardedTcpIpTunnel::Ptr &connection); + + Internal::SshTcpIpForwardServerPrivate * const d; +}; + +} // namespace QSsh diff --git a/src/libs/ssh/sshtcpipforwardserver_p.h b/src/libs/ssh/sshtcpipforwardserver_p.h new file mode 100644 index 0000000..9f4775b --- /dev/null +++ b/src/libs/ssh/sshtcpipforwardserver_p.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include "sshtcpipforwardserver.h" +#include +#include + +namespace QSsh { +namespace Internal { + +class SshTcpIpForwardServerPrivate +{ +public: + static const int ReplyTimeout = 10000; // milli seconds + + SshTcpIpForwardServerPrivate(const QString &bindAddress, quint16 bindPort, + SshSendFacility &sendFacility); + + SshSendFacility &m_sendFacility; + QTimer m_timeoutTimer; + + const QString m_bindAddress; + quint16 m_bindPort; + SshTcpIpForwardServer::State m_state; + + QList m_pendingConnections; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshtcpiptunnel.cpp b/src/libs/ssh/sshtcpiptunnel.cpp new file mode 100644 index 0000000..ac7bf52 --- /dev/null +++ b/src/libs/ssh/sshtcpiptunnel.cpp @@ -0,0 +1,123 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "sshsendfacility_p.h" +#include "sshtcpiptunnel_p.h" + +#include "sshincomingpacket_p.h" +#include "sshexception_p.h" +#include "sshlogging_p.h" + +namespace QSsh { + +namespace Internal { +SshTcpIpTunnelPrivate::SshTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility) + : AbstractSshChannel(channelId, sendFacility) +{ + connect(this, &AbstractSshChannel::eof, this, &SshTcpIpTunnelPrivate::handleEof); +} + +SshTcpIpTunnelPrivate::~SshTcpIpTunnelPrivate() +{ + closeChannel(); +} + + + +void SshTcpIpTunnelPrivate::handleChannelSuccess() +{ + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Unexpected SSH_MSG_CHANNEL_SUCCESS message."); +} + +void SshTcpIpTunnelPrivate::handleChannelFailure() +{ + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Unexpected SSH_MSG_CHANNEL_FAILURE message."); +} + +void SshTcpIpTunnelPrivate::handleOpenFailureInternal(const QString &reason) +{ + emit error(reason); + closeChannel(); +} + +void SshTcpIpTunnelPrivate::handleChannelDataInternal(const QByteArray &data) +{ + m_data += data; + emit readyRead(); +} + +void SshTcpIpTunnelPrivate::handleChannelExtendedDataInternal(quint32 type, + const QByteArray &data) +{ + 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() +{ + emit closed(); +} + +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. + */ + closeChannel(); +} + +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); + return bytesRead; +} + +qint64 SshTcpIpTunnelPrivate::writeData(const char *data, qint64 len) +{ + QSSH_ASSERT_AND_RETURN_VALUE(channelState() == AbstractSshChannel::SessionEstablished, 0); + + sendData(QByteArray(data, len)); + return len; +} + +} // namespace Internal + +} // namespace QSsh diff --git a/src/libs/ssh/sshtcpiptunnel_p.h b/src/libs/ssh/sshtcpiptunnel_p.h new file mode 100644 index 0000000..a9f2844 --- /dev/null +++ b/src/libs/ssh/sshtcpiptunnel_p.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include "sshchannel_p.h" +#include +#include + +namespace QSsh { +namespace Internal { + +class SshTcpIpTunnelPrivate : public AbstractSshChannel +{ + Q_OBJECT + +public: + SshTcpIpTunnelPrivate(quint32 channelId, SshSendFacility &sendFacility); + ~SshTcpIpTunnelPrivate(); + + template + 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; + + qint64 readData(char *data, qint64 maxlen); + qint64 writeData(const char *data, qint64 len); + +signals: + void readyRead(); + void error(const QString &reason); + void closed(); + +protected: + 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; + + QByteArray m_data; + +private: + void handleEof(); +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshx11channel.cpp b/src/libs/ssh/sshx11channel.cpp new file mode 100644 index 0000000..d531142 --- /dev/null +++ b/src/libs/ssh/sshx11channel.cpp @@ -0,0 +1,220 @@ +/**************************************************************************** +** +** 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 "sshx11channel_p.h" + +#include "sshincomingpacket_p.h" +#include "sshlogging_p.h" +#include "sshsendfacility_p.h" + +#include +#include +#include + +namespace QSsh { +namespace Internal { + +class X11Socket : public QObject +{ + Q_OBJECT +public: + X11Socket(QObject *parent) : QObject(parent) { } + + void establishConnection(const X11DisplayInfo &displayInfo) + { + const bool hostNameIsPath = displayInfo.hostName.startsWith('/'); // macOS + const bool hasActualHostName = !displayInfo.hostName.isEmpty() + && displayInfo.hostName != "unix" && !displayInfo.hostName.endsWith("/unix") + && !hostNameIsPath; + if (hasActualHostName) { + QTcpSocket * const socket = new QTcpSocket(this); + connect(socket, &QTcpSocket::connected, this, &X11Socket::connected); + connect(socket, + static_cast(&QTcpSocket::error), + [this, socket] { + emit error(socket->errorString()); + }); + socket->connectToHost(displayInfo.hostName, 6000 + displayInfo.display); + m_socket = socket; + } else { + const QString serverBasePath = hostNameIsPath ? QString(displayInfo.hostName + ':') + : "/tmp/.X11-unix/X"; + QLocalSocket * const socket = new QLocalSocket(this); + connect(socket, &QLocalSocket::connected, this, &X11Socket::connected); + connect(socket, + static_cast(&QLocalSocket::error), + [this, socket] { + emit error(socket->errorString()); + }); + socket->connectToServer(serverBasePath + QString::number(displayInfo.display)); + m_socket = socket; + } + connect(m_socket, &QIODevice::readyRead, + [this] { emit dataAvailable(m_socket->readAll()); }); + } + + void closeConnection() + { + m_socket->disconnect(); + if (localSocket()) + localSocket()->disconnectFromServer(); + else + tcpSocket()->disconnectFromHost(); + } + + void write(const QByteArray &data) + { + m_socket->write(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); + } + +signals: + void connected(); + void error(const QString &message); + void dataAvailable(const QByteArray &data); + +private: + QLocalSocket *localSocket() const { return qobject_cast(m_socket); } + QTcpSocket *tcpSocket() const { return qobject_cast(m_socket); } + + QIODevice *m_socket = nullptr; +}; + +SshX11Channel::SshX11Channel(const X11DisplayInfo &displayInfo, quint32 channelId, + SshSendFacility &sendFacility) + : 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() +{ + m_sendFacility.sendChannelOpenConfirmationPacket(remoteChannel(), localChannelId(), + initialWindowSize(), maxPacketSize()); + connect(m_x11Socket, &X11Socket::connected, [this] { + qCDebug(sshLog) << "x11 socket connected for channel" << localChannelId(); + if (!m_queuedRemoteData.isEmpty()) + handleRemoteData(QByteArray()); + }); + connect(m_x11Socket, &X11Socket::error, + [this](const QString &msg) { emit error(tr("X11 socket error: %1").arg(msg)); }); + connect(m_x11Socket, &X11Socket::dataAvailable, [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; + closeChannel(); +} + +void SshX11Channel::handleExitSignal(const SshChannelExitSignal &signal) +{ + qCWarning(sshLog) << "unexpected exit signal message on X11 channel" << signal.error; + closeChannel(); +} + +void SshX11Channel::closeHook() +{ + m_x11Socket->disconnect(); + m_x11Socket->closeConnection(); +} + +void SshX11Channel::handleRemoteData(const QByteArray &data) +{ + if (m_x11Socket->hasError()) + return; + 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; + return; + } + if (m_haveReplacedRandomCookie) { + qCDebug(sshLog) << "forwarding data to x11 socket"; + m_x11Socket->write(data); + return; + } + m_queuedRemoteData += 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"; + return; + } + 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; +} + +} // namespace Internal +} // namespace QSsh + +#include diff --git a/src/libs/ssh/sshx11channel_p.h b/src/libs/ssh/sshx11channel_p.h new file mode 100644 index 0000000..9181291 --- /dev/null +++ b/src/libs/ssh/sshx11channel_p.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "sshchannel_p.h" + +#include "sshx11displayinfo_p.h" + +#include + +namespace QSsh { +namespace Internal { +class SshChannelManager; +class SshSendFacility; +class X11Socket; + +class SshX11Channel : public AbstractSshChannel +{ + Q_OBJECT + + friend class Internal::SshChannelManager; + +signals: + void error(const QString &message); + +private: + SshX11Channel(const X11DisplayInfo &displayInfo, quint32 channelId, + SshSendFacility &sendFacility); + + void handleChannelSuccess() override; + void handleChannelFailure() override; + + void handleOpenSuccessInternal() 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 handleRemoteData(const QByteArray &data); + + X11Socket * const m_x11Socket; + const X11DisplayInfo m_displayInfo; + QByteArray m_queuedRemoteData; + bool m_haveReplacedRandomCookie = false; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshx11displayinfo_p.h b/src/libs/ssh/sshx11displayinfo_p.h new file mode 100644 index 0000000..65fa408 --- /dev/null +++ b/src/libs/ssh/sshx11displayinfo_p.h @@ -0,0 +1,49 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "ssh_global.h" + +#include +#include + +namespace QSsh { +namespace Internal { + +class QSSH_EXPORT X11DisplayInfo +{ +public: + QString displayName; + QString hostName; + QByteArray protocol; + QByteArray cookie; + QByteArray randomCookie; + int display = 0; + int screen = 0; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshx11inforetriever.cpp b/src/libs/ssh/sshx11inforetriever.cpp new file mode 100644 index 0000000..819ad42 --- /dev/null +++ b/src/libs/ssh/sshx11inforetriever.cpp @@ -0,0 +1,153 @@ +/**************************************************************************** +** +** 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 "sshx11inforetriever_p.h" + +#include "sshlogging_p.h" +#include "sshx11displayinfo_p.h" + +#include +#include +#include +#include + +#include + +namespace QSsh { +namespace Internal { + +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(&QProcess::finished), + [this] { + if (m_xauthProc->exitStatus() != QProcess::NormalExit) { + emitFailure(tr("xauth crashed: %1").arg(m_xauthProc->errorString())); + return; + } + if (m_xauthProc->exitCode() != 0) { + emitFailure(tr("xauth failed with exit code %1.").arg(m_xauthProc->exitCode())); + return; + } + switch (m_state) { + case State::RunningGenerate: + m_state = State::RunningList; + m_xauthProc->start("xauth", QStringList{"-f", m_xauthFile->fileName(), "list", + m_displayName}); + break; + case State::RunningList: { + const QByteArrayList outputLines = m_xauthProc->readAllStandardOutput().split('\n'); + if (outputLines.empty()) { + emitFailure(tr("Unexpected xauth output.")); + return; + } + const QByteArrayList data = outputLines.first().simplified().split(' '); + if (data.size() < 3 || data.at(1) != xauthProtocol() || data.at(2).isEmpty()) { + emitFailure(tr("Unexpected xauth output.")); + return; + } + X11DisplayInfo displayInfo; + displayInfo.displayName = m_displayName; + const int colonIndex = m_displayName.indexOf(':'); + if (colonIndex == -1) { + emitFailure(tr("Invalid display name \"%1\"").arg(m_displayName)); + return; + } + displayInfo.hostName = m_displayName.mid(0, colonIndex); + const int dotIndex = m_displayName.indexOf('.', colonIndex + 1); + const QString display = m_displayName.mid(colonIndex + 1, + dotIndex == -1 ? -1 + : dotIndex - colonIndex - 1); + if (display.isEmpty()) { + emitFailure(tr("Invalid display name \"%1\"").arg(m_displayName)); + return; + } + bool ok; + displayInfo.display = display.toInt(&ok); + if (!ok) { + emitFailure(tr("Invalid display name \"%1\"").arg(m_displayName)); + return; + } + if (dotIndex != -1) { + displayInfo.screen = m_displayName.mid(dotIndex + 1).toInt(&ok); + if (!ok) { + emitFailure(tr("Invalid display name \"%1\"").arg(m_displayName)); + return; + } + } + displayInfo.protocol = data.at(1); + displayInfo.cookie = QByteArray::fromHex(data.at(2)); + displayInfo.randomCookie.resize(displayInfo.cookie.size()); + try { + Botan::AutoSeeded_RNG rng; + rng.randomize(reinterpret_cast(displayInfo.randomCookie.data()), + displayInfo.randomCookie.size()); + } catch (const std::exception &ex) { + emitFailure(tr("Failed to generate random cookie: %1") + .arg(QLatin1String(ex.what()))); + return; + } + emit success(displayInfo); + deleteLater(); + break; + } + default: + emitFailure(tr("Internal error")); + } + }); +} + +void SshX11InfoRetriever::start() +{ + if (!m_xauthFile->open()) { + emitFailure(tr("Could not create temporary file: %1").arg(m_xauthFile->errorString())); + return; + } + m_state = State::RunningGenerate; + m_xauthProc->start("xauth", QStringList{"-f", m_xauthFile->fileName(), "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)); + deleteLater(); + }); +} + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshx11inforetriever_p.h b/src/libs/ssh/sshx11inforetriever_p.h new file mode 100644 index 0000000..056d774 --- /dev/null +++ b/src/libs/ssh/sshx11inforetriever_p.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "ssh_global.h" + +#include +#include + +QT_BEGIN_NAMESPACE +class QByteArray; +class QProcess; +class QTemporaryFile; +QT_END_NAMESPACE + +namespace QSsh { +namespace Internal { +class X11DisplayInfo; + +class QSSH_EXPORT SshX11InfoRetriever : public QObject +{ + Q_OBJECT +public: + SshX11InfoRetriever(const QString &displayName, QObject *parent = nullptr); + void start(); + +signals: + void failure(const QString &message); + void success(const X11DisplayInfo &displayInfo); + +private: + 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; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/private_headers.pri b/src/private_headers.pri deleted file mode 100644 index b00401a..0000000 --- a/src/private_headers.pri +++ /dev/null @@ -1,11 +0,0 @@ -# Try to find location of Qt private headers (see README) -isEmpty(QT_PRIVATE_HEADERS) { - QT_PRIVATE_HEADERS = $$[QT_INSTALL_HEADERS] -} else { - INCLUDEPATH += \ - $${QT_PRIVATE_HEADERS} \ - $${QT_PRIVATE_HEADERS}/QtCore \ - $${QT_PRIVATE_HEADERS}/QtGui \ - $${QT_PRIVATE_HEADERS}/QtScript \ - $${QT_PRIVATE_HEADERS}/QtDeclarative -} diff --git a/src/qtcreatorlibrary.pri b/src/qtcreatorlibrary.pri index 0fa9ac6..8b13789 100644 --- a/src/qtcreatorlibrary.pri +++ b/src/qtcreatorlibrary.pri @@ -1,24 +1 @@ -include(../qssh.pri) -win32 { - DLLDESTDIR = $$IDE_APP_PATH -} - -DESTDIR = $$IDE_LIBRARY_PATH - -include(rpath.pri) - -TARGET = $$qtLibraryName($$TARGET) - -CONFIG += shared dll - -contains(QT_CONFIG, reduce_exports):CONFIG += hide_symbols - -!macx { - win32 { - target.path = $$QTC_PREFIX/bin - } else { - target.path = $$QTC_PREFIX/$$IDE_LIBRARY_BASENAME/qtcreator - } - INSTALLS += target -} diff --git a/src/rpath.pri b/src/rpath.pri index c352a82..e69de29 100644 --- a/src/rpath.pri +++ b/src/rpath.pri @@ -1,18 +0,0 @@ -macx { - !isEmpty(TIGER_COMPAT_MODE) { - QMAKE_LFLAGS_SONAME = -Wl,-install_name,@executable_path/../PlugIns/ - } else { - QMAKE_LFLAGS_SONAME = -Wl,-install_name,@rpath/PlugIns/ - QMAKE_LFLAGS += -Wl,-rpath,@loader_path/../,-rpath,@executable_path/../ - } -} else:linux-* { - #do the rpath by hand since it's not possible to use ORIGIN in QMAKE_RPATHDIR - # this expands to $ORIGIN (after qmake and make), it does NOT read a qmake var - QMAKE_RPATHDIR += \$\$ORIGIN - QMAKE_RPATHDIR += \$\$ORIGIN/.. - QMAKE_RPATHDIR += \$\$ORIGIN/../$$IDE_LIBRARY_BASENAME/qtcreator - IDE_PLUGIN_RPATH = $$join(QMAKE_RPATHDIR, ":") - - QMAKE_LFLAGS += -Wl,-z,origin \'-Wl,-rpath,$${IDE_PLUGIN_RPATH}\' - QMAKE_RPATHDIR = -} diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro new file mode 100644 index 0000000..09d59a5 --- /dev/null +++ b/tests/auto/auto.pro @@ -0,0 +1,5 @@ +TEMPLATE = subdirs + +SUBDIRS += \ + ssh \ + diff --git a/tests/auto/qttest.pri b/tests/auto/qttest.pri new file mode 100644 index 0000000..6268873 --- /dev/null +++ b/tests/auto/qttest.pri @@ -0,0 +1,16 @@ +include(../../qssh.pri) +include(qttestrpath.pri) + +TEMPLATE=app +QT += testlib +CONFIG += qt warn_on console depend_includepath testcase no_testcase_installs +CONFIG -= app_bundle + +DEFINES -= QT_RESTRICTED_CAST_FROM_ASCII + +win32 { + lib = $$IDE_LIBRARY_PATH;$$IDE_PLUGIN_PATH + lib ~= s,/,\\,g + # the below gets added to later by testcase.prf + check.commands = cd . & set PATH=$$lib;%PATH%& cmd /c +} diff --git a/tests/auto/qttestrpath.pri b/tests/auto/qttestrpath.pri new file mode 100644 index 0000000..e0c320f --- /dev/null +++ b/tests/auto/qttestrpath.pri @@ -0,0 +1,11 @@ +linux-* { + QMAKE_RPATHDIR += $$IDE_BUILD_TREE/$$IDE_LIBRARY_BASENAME/ + QMAKE_RPATHDIR += $$IDE_PLUGIN_PATH + + IDE_PLUGIN_RPATH = $$join(QMAKE_RPATHDIR, ":") + + QMAKE_LFLAGS += -Wl,-z,origin \'-Wl,-rpath,$${IDE_PLUGIN_RPATH}\' +} else:macx { + QMAKE_LFLAGS += -Wl,-rpath,\"$$IDE_BIN_PATH/../\" +} + diff --git a/tests/auto/ssh/README.md b/tests/auto/ssh/README.md new file mode 100644 index 0000000..237e34b --- /dev/null +++ b/tests/auto/ssh/README.md @@ -0,0 +1,19 @@ +ssh/sftp auto test +================== + +This example needs or uses the following environment variables to know where +and how to connect to run the tests. I've just tested with an openssh server, +but in theory any should work. + + * `QTC_SSH_TEST_HOST`: The hostname of the server to connect to. + * `QTC_SSH_TEST_PORT`: The port to connect to. + * `QTC_SSH_TEST_USER`: The username to use when connecting. + * `QTC_SSH_TEST_PASSWORD`: The password to use when connecting. + * `QTC_SSH_TEST_KEYFILE`: The key file to use when connecting. + * `QTC_SSH_TEST_KEYFILE`: The key file to use when connecting. + +For the tunnel tests (uses `localhost` instead of `QTC_SSH_TEST_HOST`): + * `QTC_SSH_TEST_PORT_TUNNEL` + * `QTC_SSH_TEST_USER_TUNNEL` + * `QTC_SSH_TEST_PASSWORD_TUNNEL` + * `QTC_SSH_TEST_KEYFILE_TUNNEL` diff --git a/tests/auto/ssh/ssh.pro b/tests/auto/ssh/ssh.pro new file mode 100644 index 0000000..c66ac10 --- /dev/null +++ b/tests/auto/ssh/ssh.pro @@ -0,0 +1,5 @@ +QT = core network +LIBS += -lQSsh +include(../qttest.pri) + +SOURCES += tst_ssh.cpp diff --git a/tests/auto/ssh/tst_ssh.cpp b/tests/auto/ssh/tst_ssh.cpp new file mode 100644 index 0000000..47e6975 --- /dev/null +++ b/tests/auto/ssh/tst_ssh.cpp @@ -0,0 +1,1088 @@ +/**************************************************************************** +** +** 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace QSsh; + +static QString getHostFromEnvironment() +{ + return QString::fromLocal8Bit(qgetenv("QTC_SSH_TEST_HOST")); +} + +enum class TestType { Normal, Tunnel }; +static const char *portVar(TestType testType) +{ + return testType == TestType::Normal ? "QTC_SSH_TEST_PORT" : "QTC_SSH_TEST_PORT_TUNNEL"; +} +static const char *userVar(TestType testType) +{ + return testType == TestType::Normal ? "QTC_SSH_TEST_USER" : "QTC_SSH_TEST_USER_TUNNEL"; +} +static const char *pwdVar(TestType testType) +{ + return testType == TestType::Normal ? "QTC_SSH_TEST_PASSWORD" : "QTC_SSH_TEST_PASSWORD_TUNNEL"; +} +static const char *keyFileVar(TestType testType) +{ + return testType == TestType::Normal ? "QTC_SSH_TEST_KEYFILE" : "QTC_SSH_TEST_KEYFILE_TUNNEL"; +} + +static bool canUseFallbackValue(TestType testType) +{ + return testType == TestType::Tunnel && getHostFromEnvironment() == "localhost"; +} + +static quint16 getPortFromEnvironment(TestType testType) +{ + const int port = qEnvironmentVariableIntValue(portVar(testType)); + if (port != 0) + return port; + if (canUseFallbackValue(testType)) + return getPortFromEnvironment(TestType::Normal); + return 22; +} + +static QString getUserFromEnvironment(TestType testType) +{ + const QString user = QString::fromLocal8Bit(qgetenv(userVar(testType))); + if (user.isEmpty() && canUseFallbackValue(testType)) + return getUserFromEnvironment(TestType::Normal); + return user; +} + +static QString getPasswordFromEnvironment(TestType testType) +{ + const QString pwd = QString::fromLocal8Bit(qgetenv(pwdVar(testType))); + if (pwd.isEmpty() && canUseFallbackValue(testType)) + return getPasswordFromEnvironment(TestType::Normal); + return pwd; +} + +static QString getKeyFileFromEnvironment(TestType testType) +{ + const QString keyFile = QString::fromLocal8Bit(qgetenv(keyFileVar(testType))); + if (keyFile.isEmpty() && canUseFallbackValue(testType)) + return getKeyFileFromEnvironment(TestType::Normal); + return keyFile; +} + +static SshConnectionParameters getParameters(TestType testType) +{ + SshConnectionParameters params; + params.setHost(testType == TestType::Tunnel ? QString("localhost") + : getHostFromEnvironment()); + params.setPort(getPortFromEnvironment(testType)); + params.setUserName(getUserFromEnvironment(testType)); + params.setPassword(getPasswordFromEnvironment(testType)); + params.timeout = 10; + params.privateKeyFile = getKeyFileFromEnvironment(testType); + params.authenticationType = !params.password().isEmpty() + ? SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + : SshConnectionParameters::AuthenticationTypePublicKey; + return params; +} + +#define CHECK_PARAMS(params, testType) \ + do { \ + if (params.host().isEmpty()) { \ + Q_ASSERT(testType == TestType::Normal); \ + QSKIP("No hostname provided. Set QTC_SSH_TEST_HOST."); \ + } \ + if (params.userName().isEmpty()) \ + QSKIP(qPrintable(QString::fromLatin1("No user name provided. Set %1.") \ + .arg(userVar(testType)))); \ + if (params.password().isEmpty() && params.privateKeyFile.isEmpty()) \ + QSKIP(qPrintable(QString::fromLatin1("No authentication data provided. " \ + "Set %1 or %2.").arg(pwdVar(testType), keyFileVar(testType)))); \ + } while (false) + +class tst_Ssh : public QObject +{ + Q_OBJECT + +private slots: + void directTunnel(); + void errorHandling_data(); + void errorHandling(); + void forwardTunnel(); + void pristineConnectionObject(); + void remoteProcess_data(); + void remoteProcess(); + void remoteProcessChannels(); + void remoteProcessInput(); + void sftp(); + void x11InfoRetriever_data(); + void x11InfoRetriever(); + +private: + bool waitForConnection(SshConnection &connection); +}; + +void tst_Ssh::directTunnel() +{ + // Establish SSH connection + const SshConnectionParameters params = getParameters(TestType::Tunnel); + CHECK_PARAMS(params, TestType::Tunnel); + SshConnection connection(params); + QVERIFY(waitForConnection(connection)); + + // Set up the tunnel + QTcpServer targetServer; + QTcpSocket *targetSocket = nullptr; + bool tunnelInitialized = false; + QVERIFY2(targetServer.listen(QHostAddress::LocalHost), qPrintable(targetServer.errorString())); + const quint16 targetPort = targetServer.serverPort(); + const SshDirectTcpIpTunnel::Ptr tunnel + = connection.createDirectTunnel("localhost", 1024, "localhost", targetPort); + QEventLoop loop; + const auto connectionHandler = [&targetServer, &targetSocket, &loop, &tunnelInitialized] { + targetSocket = targetServer.nextPendingConnection(); + targetServer.close(); + if (tunnelInitialized) + loop.quit(); + }; + connect(&targetServer, &QTcpServer::newConnection, connectionHandler); + connect(tunnel.data(), &SshDirectTcpIpTunnel::error, &loop, &QEventLoop::quit); + connect(tunnel.data(), &SshDirectTcpIpTunnel::initialized, + [&tunnelInitialized, &targetSocket, &loop] { + tunnelInitialized = true; + if (targetSocket) + loop.quit(); + }); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + QVERIFY(!tunnel->isOpen()); + tunnel->initialize(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(tunnel->isOpen()); + QVERIFY(targetSocket); + QVERIFY(tunnelInitialized); + + // Send data through the tunnel and check that it is received by the "remote" side + static const QByteArray testData("Urgsblubb?"); + QByteArray clientDataReceivedByServer; + connect(targetSocket, + static_cast(&QAbstractSocket::error), + &loop, &QEventLoop::quit); + const auto socketDataHandler = [targetSocket, &clientDataReceivedByServer, &loop] { + clientDataReceivedByServer += targetSocket->readAll(); + if (clientDataReceivedByServer == testData) + loop.quit(); + }; + connect(targetSocket, &QIODevice::readyRead, socketDataHandler); + timer.start(); + tunnel->write(testData); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(tunnel->isOpen()); + QVERIFY2(targetSocket->error() == QAbstractSocket::UnknownSocketError, + qPrintable(targetSocket->errorString())); + QCOMPARE(clientDataReceivedByServer, testData); + + // Send data back and check that it is received by the "local" side + QByteArray serverDataReceivedByClient; + connect(tunnel.data(), &QIODevice::readyRead, [tunnel, &serverDataReceivedByClient, &loop] { + serverDataReceivedByClient += tunnel->readAll(); + if (serverDataReceivedByClient == testData) + loop.quit(); + }); + timer.start(); + targetSocket->write(clientDataReceivedByServer); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(tunnel->isOpen()); + QVERIFY2(targetSocket->error() == QAbstractSocket::UnknownSocketError, + qPrintable(targetSocket->errorString())); + QCOMPARE(serverDataReceivedByClient, testData); + + // Close tunnel by closing the "remote" socket + connect(tunnel.data(), &QIODevice::aboutToClose, &loop, &QEventLoop::quit); + timer.start(); + targetSocket->close(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(!tunnel->isOpen()); + QVERIFY2(targetSocket->error() == QAbstractSocket::UnknownSocketError, + qPrintable(targetSocket->errorString())); +} + +using ErrorList = QList; +void tst_Ssh::errorHandling_data() +{ + QTest::addColumn("host"); + QTest::addColumn("port"); + QTest::addColumn("authType"); + QTest::addColumn("user"); + QTest::addColumn("password"); + QTest::addColumn("keyFile"); + QTest::addColumn("expectedErrors"); + + QTest::newRow("no host") + << QString("hgdfxgfhgxfhxgfchxgcf") << quint16(12345) + << SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + << QString() << QString() << QString() << ErrorList{SshSocketError, SshTimeoutError}; + const QString theHost = getHostFromEnvironment(); + if (theHost.isEmpty()) + return; + const quint16 thePort = getPortFromEnvironment(TestType::Normal); + QTest::newRow("no user") + << theHost << thePort + << SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + << QString("dumdidumpuffpuff") << QString("whatever") << QString() + << ErrorList{SshAuthenticationError}; + QTest::newRow("wrong password") + << theHost << thePort + << SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + << QString("root") << QString("thiscantpossiblybeapasswordcanit") << QString() + << ErrorList{SshAuthenticationError}; + QTest::newRow("non-existing key file") + << theHost << thePort + << SshConnectionParameters::AuthenticationTypePublicKey + << QString("root") << QString() + << QString("somefilenamethatwedontexpecttocontainavalidkey") + << ErrorList{SshKeyFileError}; + + // TODO: Valid key file not known to the server +} + +void tst_Ssh::errorHandling() +{ + QFETCH(QString, host); + QFETCH(quint16, port); + QFETCH(SshConnectionParameters::AuthenticationType, authType); + QFETCH(QString, user); + QFETCH(QString, password); + QFETCH(QString, keyFile); + QFETCH(ErrorList, expectedErrors); + SshConnectionParameters params; + params.setHost(host); + params.setPort(port); + params.setUserName(user); + params.setPassword(password); + params.timeout = 10; + params.authenticationType = authType; + params.privateKeyFile = keyFile; + SshConnection connection(params); + QEventLoop loop; + bool disconnected = false; + QString dataReceived; + QObject::connect(&connection, &SshConnection::connected, &loop, &QEventLoop::quit); + QObject::connect(&connection, &SshConnection::error, &loop, &QEventLoop::quit); + QObject::connect(&connection, &SshConnection::disconnected, + [&disconnected] { disconnected = true; }); + QObject::connect(&connection, &SshConnection::dataAvailable, + [&dataReceived](const QString &data) { dataReceived = data; }); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.start((params.timeout + 5) * 1000); + connection.connectToHost(); + loop.exec(); + QVERIFY(timer.isActive()); + QCOMPARE(connection.state(), SshConnection::Unconnected); + QVERIFY2(expectedErrors.contains(connection.errorState()), + qPrintable(connection.errorString())); + QVERIFY(!disconnected); + QVERIFY2(dataReceived.isEmpty(), qPrintable(dataReceived)); +} + +void tst_Ssh::forwardTunnel() +{ + // Set up SSH connection + const SshConnectionParameters params = getParameters(TestType::Tunnel); + CHECK_PARAMS(params, TestType::Tunnel); + SshConnection connection(params); + QVERIFY(waitForConnection(connection)); + + // Find a free port on the "remote" side and listen on it + quint16 targetPort; + { + QTcpServer server; + QVERIFY2(server.listen(QHostAddress::LocalHost), qPrintable(server.errorString())); + targetPort = server.serverPort(); + } + SshTcpIpForwardServer::Ptr server = connection.createForwardServer(QLatin1String("localhost"), + targetPort); + QEventLoop loop; + QTimer timer; + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(server.data(), &SshTcpIpForwardServer::stateChanged, &loop, &QEventLoop::quit); + connect(server.data(), &SshTcpIpForwardServer::error, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + QCOMPARE(server->state(), SshTcpIpForwardServer::Inactive); + server->initialize(); + QCOMPARE(server->state(), SshTcpIpForwardServer::Initializing); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(server->state() == SshTcpIpForwardServer::Listening); + + // Establish a tunnel + connect(server.data(), &QSsh::SshTcpIpForwardServer::newConnection, &loop, &QEventLoop::quit); + QTcpSocket targetSocket; + targetSocket.connectToHost("localhost", targetPort); + timer.start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + const SshForwardedTcpIpTunnel::Ptr tunnel = server->nextPendingConnection(); + QVERIFY(!tunnel.isNull()); + QVERIFY(tunnel->isOpen()); + + // Send data through the socket and check that we receive it through the tunnel + static const QByteArray testData("Urgsblubb?"); + QByteArray dataReceivedOnTunnel; + QString tunnelError; + const auto tunnelErrorHandler = [&loop, &tunnelError](const QString &error) { + tunnelError = error; + loop.quit(); + }; + connect(tunnel.data(), &SshForwardedTcpIpTunnel::error, tunnelErrorHandler); + connect(tunnel.data(), &QIODevice::readyRead, [tunnel, &dataReceivedOnTunnel, &loop] { + dataReceivedOnTunnel += tunnel->readAll(); + if (dataReceivedOnTunnel == testData) + loop.quit(); + }); + timer.start(); + targetSocket.write(testData); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(tunnel->isOpen()); + QVERIFY2(tunnelError.isEmpty(), qPrintable(tunnelError)); + QCOMPARE(dataReceivedOnTunnel, testData); + + // Send data though the tunnel and check that we receive it on the socket + QByteArray dataReceivedOnSocket; + connect(&targetSocket, &QTcpSocket::readyRead, [&targetSocket, &dataReceivedOnSocket, &loop] { + dataReceivedOnSocket += targetSocket.readAll(); + if (dataReceivedOnSocket == testData) + loop.quit(); + }); + timer.start(); + tunnel->write(dataReceivedOnTunnel); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(tunnel->isOpen()); + QCOMPARE(dataReceivedOnSocket, testData); + QVERIFY2(tunnelError.isEmpty(), qPrintable(tunnelError)); + + // Close the tunnel via the socket + connect(tunnel.data(), &SshForwardedTcpIpTunnel::aboutToClose, &loop, &QEventLoop::quit); + timer.start(); + targetSocket.close(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(!tunnel->isOpen()); + QVERIFY2(tunnelError.isEmpty(), qPrintable(tunnelError)); + QCOMPARE(server->state(), SshTcpIpForwardServer::Listening); + + // Close the server + timer.start(); + server->close(); + QCOMPARE(server->state(), SshTcpIpForwardServer::Closing); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QCOMPARE(server->state(), SshTcpIpForwardServer::Inactive); +} + +void tst_Ssh::pristineConnectionObject() +{ + QSsh::SshConnection connection((SshConnectionParameters())); + QCOMPARE(connection.state(), SshConnection::Unconnected); + QVERIFY(connection.createRemoteProcess("").isNull()); + QVERIFY(connection.createSftpChannel().isNull()); +} + +void tst_Ssh::remoteProcess_data() +{ + QTest::addColumn("commandLine"); + QTest::addColumn("useTerminal"); + QTest::addColumn("isBlocking"); + QTest::addColumn("successExpected"); + QTest::addColumn("stdoutExpected"); + QTest::addColumn("stderrExpected"); + + QTest::newRow("normal command") + << QByteArray("ls -a /tmp") << false << false << true << true << false; + QTest::newRow("failing command") + << QByteArray("top -n 1") << false << false << false << false << true; + QTest::newRow("blocking command") + << QByteArray("/bin/sleep 100") << false << true << false << false << false; + QTest::newRow("terminal command") + << QByteArray("top -n 1") << true << false << true << true << false; +} + +void tst_Ssh::remoteProcess() +{ + const SshConnectionParameters params = getParameters(TestType::Normal); + CHECK_PARAMS(params, TestType::Normal); + + QFETCH(QByteArray, commandLine); + QFETCH(bool, useTerminal); + QFETCH(bool, isBlocking); + QFETCH(bool, successExpected); + QFETCH(bool, stdoutExpected); + QFETCH(bool, stderrExpected); + + QByteArray remoteStdout; + QByteArray remoteStderr; + SshRemoteProcessRunner runner; + QEventLoop loop; + connect(&runner, &SshRemoteProcessRunner::connectionError, &loop, &QEventLoop::quit); + connect(&runner, &SshRemoteProcessRunner::processStarted, &loop, &QEventLoop::quit); + connect(&runner, &SshRemoteProcessRunner::processClosed, &loop, &QEventLoop::quit); + connect(&runner, &SshRemoteProcessRunner::readyReadStandardOutput, + [&remoteStdout, &runner] { remoteStdout += runner.readAllStandardOutput(); }); + connect(&runner, &SshRemoteProcessRunner::readyReadStandardError, + [&remoteStderr, &runner] { remoteStderr += runner.readAllStandardError(); }); + if (useTerminal) + runner.runInTerminal(commandLine, SshPseudoTerminal(), params); + else + runner.run(commandLine, params); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(runner.isProcessRunning()); // Event loop exit should have been triggered by started(). + QVERIFY2(remoteStdout.isEmpty(), remoteStdout.constData()); + QVERIFY2(remoteStderr.isEmpty(), remoteStderr.constData()); + + SshRemoteProcessRunner killer; + if (isBlocking) + killer.run("pkill -f -9 \"" + commandLine + '"', params); + + timer.start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(!runner.isProcessRunning()); + if (isBlocking) { + // Some shells (e.g. mksh) do not report a crash exit. + if (runner.processExitStatus() == SshRemoteProcess::CrashExit) + QCOMPARE(runner.processExitSignal(), SshRemoteProcess::KillSignal); + else + QVERIFY(runner.processExitCode() != 0); + } else { + QCOMPARE(successExpected, runner.processExitCode() == 0); + } + QCOMPARE(stdoutExpected, !remoteStdout.isEmpty()); + QCOMPARE(stderrExpected, !remoteStderr.isEmpty()); +} + +void tst_Ssh::remoteProcessChannels() +{ + const SshConnectionParameters params = getParameters(TestType::Normal); + CHECK_PARAMS(params, TestType::Normal); + SshConnection connection(params); + QVERIFY(waitForConnection(connection)); + + static const QByteArray testString("ChannelTest"); + QByteArray remoteStdout; + QByteArray remoteStderr; + QByteArray remoteData; + SshRemoteProcess::Ptr echoProcess + = connection.createRemoteProcess("printf " + testString + " >&2"); + echoProcess->setReadChannel(QProcess::StandardError); + QEventLoop loop; + connect(echoProcess.data(), &SshRemoteProcess::closed, &loop, &QEventLoop::quit); + connect(echoProcess.data(), &QIODevice::readyRead, + [&remoteData, echoProcess] { remoteData += echoProcess->readAll(); }); + connect(echoProcess.data(), &SshRemoteProcess::readyReadStandardOutput, + [&remoteStdout, echoProcess] { remoteStdout += echoProcess->readAllStandardOutput(); }); + connect(echoProcess.data(), &SshRemoteProcess::readyReadStandardError, + [&remoteStderr, echoProcess] { remoteStderr = testString; }); + echoProcess->start(); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(!echoProcess->isRunning()); + QCOMPARE(echoProcess->exitSignal(), SshRemoteProcess::NoSignal); + QCOMPARE(echoProcess->exitCode(), 0); + QVERIFY(remoteStdout.isEmpty()); + QCOMPARE(remoteData, testString); + QCOMPARE(remoteData, remoteStderr); +} + +void tst_Ssh::remoteProcessInput() +{ + const SshConnectionParameters params = getParameters(TestType::Normal); + CHECK_PARAMS(params, TestType::Normal); + SshConnection connection(params); + QVERIFY(waitForConnection(connection)); + + SshRemoteProcess::Ptr catProcess + = connection.createRemoteProcess(QString::fromLatin1("/bin/cat").toUtf8()); + QEventLoop loop; + connect(catProcess.data(), &SshRemoteProcess::started, &loop, &QEventLoop::quit); + connect(catProcess.data(), &SshRemoteProcess::closed, &loop, &QEventLoop::quit); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + catProcess->start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(catProcess->isRunning()); + + static QString testString = "x\r\n"; + connect(catProcess.data(), &QIODevice::readyRead, &loop, &QEventLoop::quit); + QTextStream stream(catProcess.data()); + stream << testString; + stream.flush(); + timer.start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(catProcess->isRunning()); + + const QString data = QString::fromUtf8(catProcess->readAll()); + QCOMPARE(data, testString); + SshRemoteProcessRunner * const killer = new SshRemoteProcessRunner(this); + killer->run("pkill -9 cat", params); + timer.start(); + loop.exec(); + QVERIFY(!catProcess->isRunning()); + QVERIFY(catProcess->exitCode() != 0 + || catProcess->exitSignal() == SshRemoteProcess::KillSignal); +} + +void tst_Ssh::sftp() +{ + // Connect to server + const SshConnectionParameters params = getParameters(TestType::Normal); + CHECK_PARAMS(params, TestType::Normal); + SshConnection connection(params); + QVERIFY(waitForConnection(connection)); + + // Establish SFTP channel + SftpChannel::Ptr sftpChannel = connection.createSftpChannel(); + QList jobs; + bool invalidFinishedSignal = false; + QString jobError; + QEventLoop loop; + connect(sftpChannel.data(), &SftpChannel::initialized, &loop, &QEventLoop::quit); + connect(sftpChannel.data(), &SftpChannel::channelError, &loop, &QEventLoop::quit); + connect(sftpChannel.data(), &SftpChannel::closed, &loop, &QEventLoop::quit); + connect(sftpChannel.data(), &SftpChannel::finished, + [&loop, &jobs, &invalidFinishedSignal, &jobError](SftpJobId job, const SftpError errorType, const QString &error) { + Q_UNUSED(errorType); + if (!jobs.removeOne(job)) { + invalidFinishedSignal = true; + loop.quit(); + return; + } + if (!error.isEmpty()) { + jobError = error; + loop.quit(); + return; + } + if (jobs.empty()) + loop.quit(); + }); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval((params.timeout + 5) * 1000); + timer.start(); + sftpChannel->initialize(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + QVERIFY(!invalidFinishedSignal); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + + // Create and upload 1000 small files and one big file + QTemporaryDir dirForFilesToUpload; + QTemporaryDir dirForFilesToDownload; + QVERIFY2(dirForFilesToUpload.isValid(), qPrintable(dirForFilesToUpload.errorString())); + QVERIFY2(dirForFilesToDownload.isValid(), qPrintable(dirForFilesToDownload.errorString())); + static const auto getRemoteFilePath = [](const QString &localFileName) { + return QString("/tmp/").append(localFileName).append(".upload"); + }; + const auto getDownloadFilePath = [&dirForFilesToDownload](const QString &localFileName) { + return QString(dirForFilesToDownload.path()).append('/').append(localFileName); + }; + std::srand(QDateTime::currentDateTime().toSecsSinceEpoch()); + for (int i = 0; i < 1000; ++i) { + const QString fileName = "sftptestfile" + QString::number(i + 1); + QFile file(dirForFilesToUpload.path() + '/' + fileName); + QVERIFY2(file.open(QIODevice::WriteOnly), qPrintable(file.errorString())); + int content[1024 / sizeof(int)]; + for (size_t j = 0; j < sizeof content / sizeof content[0]; ++j) + content[j] = qrand(); + file.write(reinterpret_cast(content), sizeof content); + file.close(); + QVERIFY2(file.error() == QFile::NoError, qPrintable(file.errorString())); + const QString remoteFilePath = getRemoteFilePath(fileName); + const SftpJobId uploadJob = sftpChannel->uploadFile(file.fileName(), remoteFilePath, + SftpOverwriteExisting); + QVERIFY(uploadJob != SftpInvalidJob); + jobs << uploadJob; + } + const QString bigFileName("sftpbigfile"); + QFile bigFile(dirForFilesToUpload.path() + '/' + bigFileName); + QVERIFY2(bigFile.open(QIODevice::WriteOnly), qPrintable(bigFile.errorString())); + const int bigFileSize = 100 * 1024 * 1024; + const int blockSize = 8192; + const int blockCount = bigFileSize / blockSize; + for (int block = 0; block < blockCount; ++block) { + int content[blockSize / sizeof(int)]; + for (size_t j = 0; j < sizeof content / sizeof content[0]; ++j) + content[j] = qrand(); + bigFile.write(reinterpret_cast(content), sizeof content); + } + bigFile.close(); + QVERIFY2(bigFile.error() == QFile::NoError, qPrintable(bigFile.errorString())); + const SftpJobId uploadJob = sftpChannel->uploadFile(bigFile.fileName(), + getRemoteFilePath(bigFileName), SftpOverwriteExisting); + QVERIFY(uploadJob != SftpInvalidJob); + jobs << uploadJob; + QCOMPARE(jobs.size(), 1001); + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + + // Download the uploaded files to a different location + const QStringList allUploadedFileNames + = QDir(dirForFilesToUpload.path()).entryList(QDir::Files); + QCOMPARE(allUploadedFileNames.size(), 1001); + for (const QString &fileName : allUploadedFileNames) { + const QString localFilePath = dirForFilesToUpload.path() + '/' + fileName; + const QString remoteFilePath = getRemoteFilePath(fileName); + const QString downloadFilePath = getDownloadFilePath(fileName); + const SftpJobId downloadJob = sftpChannel->downloadFile(remoteFilePath, downloadFilePath, + SftpOverwriteExisting); + QVERIFY(downloadJob != SftpInvalidJob); + jobs << downloadJob; + } + QCOMPARE(jobs.size(), 1001); + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + + // Compare contents of uploaded and downloaded files + for (const QString &fileName : allUploadedFileNames) { + QFile originalFile(dirForFilesToUpload.path() + '/' + fileName); + QVERIFY2(originalFile.open(QIODevice::ReadOnly), qPrintable(originalFile.errorString())); + QFile downloadedFile(dirForFilesToDownload.path() + '/' + fileName); + QVERIFY2(downloadedFile.open(QIODevice::ReadOnly), + qPrintable(downloadedFile.errorString())); + QVERIFY(originalFile.fileName() != downloadedFile.fileName()); + QCOMPARE(originalFile.size(), downloadedFile.size()); + qint64 bytesLeft = originalFile.size(); + while (bytesLeft > 0) { + const qint64 bytesToRead = qMin(bytesLeft, Q_INT64_C(1024 * 1024)); + const QByteArray origBlock = originalFile.read(bytesToRead); + const QByteArray copyBlock = downloadedFile.read(bytesToRead); + QCOMPARE(origBlock.size(), bytesToRead); + QCOMPARE(origBlock, copyBlock); + bytesLeft -= bytesToRead; + } + } + + // Remove the uploaded files on the remote system + for (const QString &fileName : allUploadedFileNames) { + const QString remoteFilePath = getRemoteFilePath(fileName); + const SftpJobId removeJob = sftpChannel->removeFile(remoteFilePath); + QVERIFY(removeJob != SftpInvalidJob); + jobs << removeJob; + } + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + + // Create a directory on the remote system + const QString remoteDirPath = "/tmp/sftptest-" + QDateTime::currentDateTime().toString(); + const SftpJobId mkdirJob = sftpChannel->createDirectory(remoteDirPath); + QVERIFY(mkdirJob != SftpInvalidJob); + jobs << mkdirJob; + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + + // Retrieve and check the attributes of the remote directory + QList remoteFileInfo; + const auto fileInfoHandler + = [&remoteFileInfo](SftpJobId, const QList &fileInfoList) { + remoteFileInfo << fileInfoList; + }; + connect(sftpChannel.data(), &SftpChannel::fileInfoAvailable, fileInfoHandler); + const SftpJobId statDirJob = sftpChannel->statFile(remoteDirPath); + QVERIFY(statDirJob != SftpInvalidJob); + jobs << statDirJob; + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + QCOMPARE(remoteFileInfo.size(), 1); + const SftpFileInfo remoteDirInfo = remoteFileInfo.takeFirst(); + QCOMPARE(remoteDirInfo.type, FileTypeDirectory); + QCOMPARE(remoteDirInfo.name, QFileInfo(remoteDirPath).fileName()); + + // Retrieve and check the contents of the remote directory + const SftpJobId lsDirJob = sftpChannel->listDirectory(remoteDirPath); + QVERIFY(lsDirJob != SftpInvalidJob); + jobs << lsDirJob; + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + QCOMPARE(remoteFileInfo.size(), 2); + for (const SftpFileInfo &fi : remoteFileInfo) { + QCOMPARE(fi.type, FileTypeDirectory); + QVERIFY2(fi.name == "." || fi.name == "..", qPrintable(fi.name)); + } + QVERIFY(remoteFileInfo.first().name != remoteFileInfo.last().name); + + // Remove the remote directory. + const SftpJobId rmDirJob = sftpChannel->removeDirectory(remoteDirPath); + QVERIFY(rmDirJob != SftpInvalidJob); + jobs << rmDirJob; + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Initialized); + QVERIFY(jobs.empty()); + + // Closing down + sftpChannel->closeChannel(); + QCOMPARE(sftpChannel->state(), SftpChannel::Closing); + loop.exec(); + QVERIFY(!invalidFinishedSignal); + QVERIFY2(jobError.isEmpty(), qPrintable(jobError)); + QCOMPARE(sftpChannel->state(), SftpChannel::Closed); +} + +static QStringList appendExeExtensions(const QString &executable) +{ + QStringList execs(executable); + const QFileInfo fi(executable); + +#ifdef Q_OS_WIN + // Check all the executable extensions on windows: + // PATHEXT is only used if the executable has no extension + if (fi.suffix().isEmpty()) { + const QStringList extensions = value("PATHEXT").split(';'); + + for (const QString &ext : extensions) + execs << executable + ext.toLower(); + } +#endif + + return execs; +} + +/** Expand environment variables in a string. + * + * Environment variables are accepted in the following forms: + * $SOMEVAR, ${SOMEVAR} on Unix and %SOMEVAR% on Windows. + * No escapes and quoting are supported. + * If a variable is not found, it is not substituted. + */ +static QString expandVariables(const QString &input) +{ + QString result = input; + +#ifdef Q_OS_WIN + for (int vStart = -1, i = 0; i < result.length(); ) { + if (result.at(i++) == '%') { + if (vStart > 0) { + const QByteArray varName = result.mid(vStart, i - vStart - 1).toLocal8Bit(); + if (qEnvironmentVariableIsSet(varName.constData())) { + const QByteArray varValue = qgetenv(varName.constData()); + result.replace(vStart - 1, i - vStart + 1, varValue); + i = vStart - 1 + varValue.length(); + vStart = -1; + } else { + vStart = i; + } + } else { + vStart = i; + } + } + } +#else//Q_OS_WIN + enum { BASE, OPTIONALVARIABLEBRACE, VARIABLE, BRACEDVARIABLE } state = BASE; + int vStart = -1; + + for (int i = 0; i < result.length();) { + QChar c = result.at(i++); + if (state == BASE) { + if (c == '$') + state = OPTIONALVARIABLEBRACE; + } else if (state == OPTIONALVARIABLEBRACE) { + if (c == '{') { + state = BRACEDVARIABLE; + vStart = i; + } else if (c.isLetterOrNumber() || c == '_') { + state = VARIABLE; + vStart = i - 1; + } else { + state = BASE; + } + } else if (state == BRACEDVARIABLE) { + if (c == '}') { + const QByteArray varName = result.mid(vStart, i - 1 - vStart).toLocal8Bit(); + if (qEnvironmentVariableIsSet(varName.constData())) { + const QByteArray varValue = qgetenv(varName.constData()); + result.replace(vStart - 2, i - vStart + 2, varValue); + i = vStart - 2 + varValue.length(); + } + state = BASE; + } + } else if (state == VARIABLE) { + if (!c.isLetterOrNumber() && c != '_') { + const QByteArray varName = result.mid(vStart, i - vStart - 1).toLocal8Bit(); + if (qEnvironmentVariableIsSet(varName.constData())) { + const QByteArray varValue = qgetenv(varName.constData()); + result.replace(vStart - 1, i - vStart, varValue); + i = vStart - 1 + varValue.length(); + } + state = BASE; + } + } + } + + if (state == VARIABLE) { + const QByteArray varName = result.mid(vStart).toLocal8Bit(); + if (qEnvironmentVariableIsSet(varName.constData())) { + result.replace(vStart - 1, result.length() - vStart + 1, qgetenv(varName.constData())); + } + } +#endif//Q_OS_WIN + + return result; +} + +/// Constructs a FileName from \a fileName +/// \a fileName is only passed through QDir::cleanPath +static QFileInfo fromUserInput(const QString &filename) +{ + QString clean = QDir::cleanPath(filename); + if (clean.startsWith(QLatin1String("~/"))) + clean = QDir::homePath() + clean.mid(1); + return QFileInfo(clean); +} + +static QFileInfoList systemPath() +{ +#ifdef Q_OS_WIN + const QChar separator = ';'; +#else + const QChar separator = ':'; +#endif + + const QStringList pathComponents = QString::fromLocal8Bit(qgetenv("PATH")) + .split(separator, QString::SkipEmptyParts); + + QFileInfoList ret; + for (const QString &component : pathComponents) { + ret.append(fromUserInput(component)); + } + + return ret; +} + + +static QFileInfo searchInDirectory(const QStringList &execs, const QFileInfo &directory, + QSet &alreadyChecked) +{ + const int checkedCount = alreadyChecked.count(); + alreadyChecked.insert(directory.canonicalPath()); + + if (!directory.isDir() || alreadyChecked.count() == checkedCount) + return QFileInfo(); + + const QString dir = directory.canonicalPath(); + + QFileInfo fi; + for (const QString &exec : execs) { + fi.setFile(dir, exec); + if (fi.isFile() && fi.isExecutable()) + return fi.absoluteFilePath(); + } + return QFileInfo(); +} + + +static QFileInfo searchInPath(const QString &executable) +{ + if (executable.isEmpty()) + return QFileInfo(); + + const QString exec = QDir::cleanPath(expandVariables(executable)); + const QFileInfo fi(exec); + + const QStringList execs = appendExeExtensions(exec); + + if (fi.isAbsolute()) { + for (const QString &path : execs) { + QFileInfo pfi = QFileInfo(path); + if (pfi.isFile() && pfi.isExecutable()) + return QFileInfo(path); + } + return QFileInfo(exec); + } + + QSet alreadyChecked; + + if (executable.contains('/')) + return QFileInfo(); + + for (const QFileInfo &p : systemPath()) { + QFileInfo tmp = searchInDirectory(execs, p, alreadyChecked); + if (tmp.exists()) { + return tmp; + } + } + return QFileInfo(); +} + +void tst_Ssh::x11InfoRetriever_data() +{ + QTest::addColumn("displayName"); + QTest::addColumn("successExpected"); + + const QFileInfo xauthCommand = searchInPath("xauth"); + const QString displayName = QLatin1String(qgetenv("DISPLAY")); + const bool canSucceed = xauthCommand.exists() && !displayName.isEmpty(); + QTest::newRow(canSucceed ? "suitable host" : "unsuitable host") << displayName << canSucceed; + QTest::newRow("invalid display name") << QString("dummy") << false; +} + +void tst_Ssh::x11InfoRetriever() +{ + QFETCH(QString, displayName); + QFETCH(bool, successExpected); + using namespace QSsh::Internal; + auto * const x11InfoRetriever = new SshX11InfoRetriever(displayName); + QEventLoop loop; + bool success; + X11DisplayInfo displayInfo; + QString errorMessage; + const auto successHandler = [&loop, &success, &displayInfo](const X11DisplayInfo &di) { + success = true; + displayInfo = di; + loop.quit(); + }; + connect(x11InfoRetriever, &SshX11InfoRetriever::success, successHandler); + const auto failureHandler = [&loop, &success, &errorMessage](const QString &error) { + success = false; + errorMessage = error; + loop.quit(); + }; + connect(x11InfoRetriever, &SshX11InfoRetriever::failure, failureHandler); + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.setSingleShot(true); + timer.setInterval(40000); + timer.start(); + x11InfoRetriever->start(); + loop.exec(); + QVERIFY(timer.isActive()); + timer.stop(); + if (successExpected) { + QVERIFY2(success, qPrintable(errorMessage)); + QVERIFY(!displayInfo.protocol.isEmpty()); + QVERIFY(!displayInfo.cookie.isEmpty()); + QCOMPARE(displayInfo.cookie.size(), displayInfo.randomCookie.size()); + QCOMPARE(displayInfo.displayName, displayName); + } else { + QVERIFY(!success); + QVERIFY(!errorMessage.isEmpty()); + } +} + +bool tst_Ssh::waitForConnection(SshConnection &connection) +{ + QEventLoop loop; + QObject::connect(&connection, &SshConnection::connected, &loop, &QEventLoop::quit); + QObject::connect(&connection, &SshConnection::error, &loop, &QEventLoop::quit); + connection.connectToHost(); + loop.exec(); + if (connection.errorState() != SshNoError) + qDebug() << connection.errorString(); + return connection.state() == SshConnection::Connected && connection.errorState() == SshNoError; +} + +QTEST_MAIN(tst_Ssh) + +#include diff --git a/tests/manual/ssh/errorhandling/errorhandling.pro b/tests/manual/ssh/errorhandling/errorhandling.pro deleted file mode 100644 index 1161d46..0000000 --- a/tests/manual/ssh/errorhandling/errorhandling.pro +++ /dev/null @@ -1,4 +0,0 @@ -include(../ssh.pri) - -TARGET=errorhandling -SOURCES=main.cpp diff --git a/tests/manual/ssh/errorhandling/main.cpp b/tests/manual/ssh/errorhandling/main.cpp deleted file mode 100644 index 69ae030..0000000 --- a/tests/manual/ssh/errorhandling/main.cpp +++ /dev/null @@ -1,207 +0,0 @@ -/************************************************************************** -** -** 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 -#include -#include - -#include -#include -#include -#include -#include - -using namespace QSsh; - -class Test : public QObject { - Q_OBJECT -public: - Test() - { - m_timeoutTimer.setSingleShot(true); - m_connection = new SshConnection(SshConnectionParameters()); - if (m_connection->state() != SshConnection::Unconnected) { - qDebug("Error: Newly created SSH connection has state %d.", - m_connection->state()); - } - - if (m_connection->createRemoteProcess("")) - qDebug("Error: Unconnected SSH connection creates remote process."); - if (m_connection->createSftpChannel()) - qDebug("Error: Unconnected SSH connection creates SFTP channel."); - - SshConnectionParameters noHost; - noHost.host = QLatin1String("hgdfxgfhgxfhxgfchxgcf"); - noHost.port = 12345; - noHost.timeout = 10; - noHost.authenticationType = SshConnectionParameters::AuthenticationByPassword; - - SshConnectionParameters noUser; - noUser.host = QLatin1String("localhost"); - noUser.port = 22; - noUser.timeout = 30; - noUser.authenticationType = SshConnectionParameters::AuthenticationByPassword; - noUser.userName = QLatin1String("dumdidumpuffpuff"); - noUser.password = QLatin1String("whatever"); - - SshConnectionParameters wrongPwd; - wrongPwd.host = QLatin1String("localhost"); - wrongPwd.port = 22; - wrongPwd.timeout = 30; - wrongPwd.authenticationType = SshConnectionParameters::AuthenticationByPassword; - wrongPwd.userName = QLatin1String("root"); - noUser.password = QLatin1String("thiscantpossiblybeapasswordcanit"); - - SshConnectionParameters invalidKeyFile; - invalidKeyFile.host = QLatin1String("localhost"); - invalidKeyFile.port = 22; - invalidKeyFile.timeout = 30; - invalidKeyFile.authenticationType = SshConnectionParameters::AuthenticationByKey; - invalidKeyFile.userName = QLatin1String("root"); - invalidKeyFile.privateKeyFile - = QLatin1String("somefilenamethatwedontexpecttocontainavalidkey"); - - // TODO: Create a valid key file and check for authentication error. - - m_testSet << TestItem("Behavior with non-existing host", - noHost, ErrorList() << SshSocketError); - m_testSet << TestItem("Behavior with non-existing user", noUser, - ErrorList() << SshSocketError << SshTimeoutError - << SshAuthenticationError); - m_testSet << TestItem("Behavior with wrong password", wrongPwd, - ErrorList() << SshSocketError << SshTimeoutError - << SshAuthenticationError); - m_testSet << TestItem("Behavior with invalid key file", invalidKeyFile, - ErrorList() << SshSocketError << SshTimeoutError - << SshKeyFileError); - - runNextTest(); - } - - ~Test() - { - delete m_connection; - } - -private slots: - void handleConnected() - { - qDebug("Error: Received unexpected connected() signal."); - qApp->quit(); - } - - void handleDisconnected() - { - qDebug("Error: Received unexpected disconnected() signal."); - qApp->quit(); - } - - void handleDataAvailable(const QString &msg) - { - qDebug("Error: Received unexpected dataAvailable() signal. " - "Message was: '%s'.", qPrintable(msg)); - qApp->quit(); - } - - void handleError(QSsh::SshError error) - { - if (m_testSet.isEmpty()) { - qDebug("Error: Received error %d, but no test was running.", error); - qApp->quit(); - } - - const TestItem testItem = m_testSet.takeFirst(); - if (testItem.allowedErrors.contains(error)) { - qDebug("Received error %d, as expected.", error); - if (m_testSet.isEmpty()) { - qDebug("All tests finished successfully."); - qApp->quit(); - } else { - runNextTest(); - } - } else { - qDebug("Received unexpected error %d.", error); - qApp->quit(); - } - } - - void handleTimeout() - { - if (m_testSet.isEmpty()) { - qDebug("Error: timeout, but no test was running."); - qApp->quit(); - } - const TestItem testItem = m_testSet.takeFirst(); - qDebug("Error: The following test timed out: %s", testItem.description); - } - -private: - void runNextTest() - { - if (m_connection) { - disconnect(m_connection, 0, this, 0); - delete m_connection; - } - m_connection = new SshConnection(m_testSet.first().params); - connect(m_connection, SIGNAL(connected()), SLOT(handleConnected())); - connect(m_connection, SIGNAL(disconnected()), SLOT(handleDisconnected())); - connect(m_connection, SIGNAL(dataAvailable(QString)), SLOT(handleDataAvailable(QString))); - connect(m_connection, SIGNAL(error(QSsh::SshError)), SLOT(handleError(QSsh::SshError))); - const TestItem &nextItem = m_testSet.first(); - m_timeoutTimer.stop(); - m_timeoutTimer.setInterval(qMax(10000, nextItem.params.timeout * 1000)); - qDebug("Testing: %s", nextItem.description); - m_connection->connectToHost(); - } - - SshConnection *m_connection; - typedef QList ErrorList; - struct TestItem { - TestItem(const char *d, const SshConnectionParameters &p, - const ErrorList &e) : description(d), params(p), allowedErrors(e) {} - - const char *description; - SshConnectionParameters params; - ErrorList allowedErrors; - }; - QList m_testSet; - QTimer m_timeoutTimer; -}; - -int main(int argc, char *argv[]) -{ - QCoreApplication a(argc, argv); - Test t; - - return a.exec(); -} - - -#include "main.moc" diff --git a/tests/manual/ssh/remoteprocess/main.cpp b/tests/manual/ssh/remoteprocess/main.cpp deleted file mode 100644 index 01fd72f..0000000 --- a/tests/manual/ssh/remoteprocess/main.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/************************************************************************** -** -** 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 "argumentscollector.h" -#include "remoteprocesstest.h" - -#include - -#include -#include -#include - -#include -#include - -int main(int argc, char *argv[]) -{ - QCoreApplication app(argc, argv); - bool parseSuccess; - const QSsh::SshConnectionParameters ¶meters - = ArgumentsCollector(app.arguments()).collect(parseSuccess); - if (!parseSuccess) - return EXIT_FAILURE; - RemoteProcessTest remoteProcessTest(parameters); - remoteProcessTest.run(); - return app.exec(); -} diff --git a/tests/manual/ssh/remoteprocess/remoteprocess.pro b/tests/manual/ssh/remoteprocess/remoteprocess.pro deleted file mode 100644 index 11c31a0..0000000 --- a/tests/manual/ssh/remoteprocess/remoteprocess.pro +++ /dev/null @@ -1,5 +0,0 @@ -include(../ssh.pri) - -TARGET=remoteprocess -SOURCES=main.cpp remoteprocesstest.cpp argumentscollector.cpp -HEADERS=remoteprocesstest.h argumentscollector.h diff --git a/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp b/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp deleted file mode 100644 index e147bd0..0000000 --- a/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp +++ /dev/null @@ -1,363 +0,0 @@ -/************************************************************************** -** -** 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 "remoteprocesstest.h" - -#include - -#include -#include -#include - -#include - -using namespace QSsh; - -const QByteArray StderrOutput("ChannelTest"); - -RemoteProcessTest::RemoteProcessTest(const SshConnectionParameters ¶ms) - : m_sshParams(params), - m_timeoutTimer(new QTimer(this)), - m_sshConnection(0), - m_remoteRunner(new SshRemoteProcessRunner(this)), - m_state(Inactive) -{ - m_timeoutTimer->setInterval(5000); - connect(m_timeoutTimer, SIGNAL(timeout()), SLOT(handleTimeout())); -} - -RemoteProcessTest::~RemoteProcessTest() -{ - delete m_sshConnection; -} - -void RemoteProcessTest::run() -{ - connect(m_remoteRunner, SIGNAL(connectionError()), - SLOT(handleConnectionError())); - connect(m_remoteRunner, SIGNAL(processStarted()), - SLOT(handleProcessStarted())); - connect(m_remoteRunner, SIGNAL(readyReadStandardOutput()), SLOT(handleProcessStdout())); - connect(m_remoteRunner, SIGNAL(readyReadStandardError()), SLOT(handleProcessStderr())); - connect(m_remoteRunner, SIGNAL(processClosed(int)), - SLOT(handleProcessClosed(int))); - - std::cout << "Testing successful remote process... " << std::flush; - m_state = TestingSuccess; - m_started = false; - m_timeoutTimer->start(); - m_remoteRunner->run("ls -a /tmp", m_sshParams); -} - -void RemoteProcessTest::handleConnectionError() -{ - const QString error = m_state == TestingIoDevice || m_state == TestingProcessChannels - ? m_sshConnection->errorString() : m_remoteRunner->lastConnectionErrorString(); - - std::cerr << "Error: Connection failure (" << qPrintable(error) << ")." << std::endl; - qApp->quit(); -} - -void RemoteProcessTest::handleProcessStarted() -{ - if (m_started) { - std::cerr << "Error: Received started() signal again." << std::endl; - qApp->quit(); - } else { - m_started = true; - if (m_state == TestingCrash) { - QSsh::SshRemoteProcessRunner * const killer - = new QSsh::SshRemoteProcessRunner(this); - killer->run("pkill -9 sleep", m_sshParams); - } else if (m_state == TestingIoDevice) { - connect(m_catProcess.data(), SIGNAL(readyRead()), SLOT(handleReadyRead())); - m_textStream = new QTextStream(m_catProcess.data()); - *m_textStream << testString(); - m_textStream->flush(); - } - } -} - -void RemoteProcessTest::handleProcessStdout() -{ - if (!m_started) { - std::cerr << "Error: Remote output from non-started process." - << std::endl; - qApp->quit(); - } else if (m_state != TestingSuccess && m_state != TestingTerminal) { - std::cerr << "Error: Got remote standard output in state " << m_state - << "." << std::endl; - qApp->quit(); - } else { - m_remoteStdout += m_remoteRunner->readAllStandardOutput(); - } -} - -void RemoteProcessTest::handleProcessStderr() -{ - if (!m_started) { - std::cerr << "Error: Remote error output from non-started process." - << std::endl; - qApp->quit(); - } else if (m_state == TestingSuccess) { - std::cerr << "Error: Unexpected remote standard error output." - << std::endl; - qApp->quit(); - } else { - m_remoteStderr += m_remoteRunner->readAllStandardError(); - } -} - -void RemoteProcessTest::handleProcessClosed(int exitStatus) -{ - switch (exitStatus) { - case SshRemoteProcess::NormalExit: - if (!m_started) { - std::cerr << "Error: Process exited without starting." << std::endl; - qApp->quit(); - return; - } - switch (m_state) { - case TestingSuccess: { - const int exitCode = m_remoteRunner->processExitCode(); - if (exitCode != 0) { - std::cerr << "Error: exit code is " << exitCode - << ", expected zero." << std::endl; - qApp->quit(); - return; - } - if (m_remoteStdout.isEmpty()) { - std::cerr << "Error: Command did not produce output." - << std::endl; - qApp->quit(); - return; - } - - std::cout << "Ok.\nTesting unsuccessful remote process... " << std::flush; - m_state = TestingFailure; - m_started = false; - m_timeoutTimer->start(); - m_remoteRunner->run("top -n 1", m_sshParams); // Does not succeed without terminal. - break; - } - case TestingFailure: { - const int exitCode = m_remoteRunner->processExitCode(); - if (exitCode == 0) { - std::cerr << "Error: exit code is zero, expected non-zero." - << std::endl; - qApp->quit(); - return; - } - if (m_remoteStderr.isEmpty()) { - std::cerr << "Error: Command did not produce error output." << std::flush; - qApp->quit(); - return; - } - - std::cout << "Ok.\nTesting crashing remote process... " << std::flush; - m_state = TestingCrash; - m_started = false; - m_timeoutTimer->start(); - m_remoteRunner->run("/bin/sleep 100", m_sshParams); - break; - } - case TestingCrash: - if (m_remoteRunner->processExitCode() == 0) { - std::cerr << "Error: Successful exit from process that was " - "supposed to crash." << std::endl; - qApp->quit(); - } else { - // Some shells (e.g. mksh) don't report "killed", but just a non-zero exit code. - handleSuccessfulCrashTest(); - } - break; - case TestingTerminal: { - const int exitCode = m_remoteRunner->processExitCode(); - if (exitCode != 0) { - std::cerr << "Error: exit code is " << exitCode - << ", expected zero." << std::endl; - qApp->quit(); - return; - } - if (m_remoteStdout.isEmpty()) { - std::cerr << "Error: Command did not produce output." - << std::endl; - qApp->quit(); - return; - } - std::cout << "Ok.\nTesting I/O device functionality... " << std::flush; - m_state = TestingIoDevice; - m_sshConnection = new QSsh::SshConnection(m_sshParams); - connect(m_sshConnection, SIGNAL(connected()), SLOT(handleConnected())); - connect(m_sshConnection, SIGNAL(error(QSsh::SshError)), - SLOT(handleConnectionError())); - m_sshConnection->connectToHost(); - m_timeoutTimer->start(); - break; - } - case TestingIoDevice: - if (m_catProcess->exitCode() == 0) { - std::cerr << "Error: Successful exit from process that was supposed to crash." - << std::endl; - qApp->exit(EXIT_FAILURE); - } else { - handleSuccessfulIoTest(); - } - break; - case TestingProcessChannels: - if (m_remoteStderr.isEmpty()) { - std::cerr << "Error: Did not receive readyReadStderr()." << std::endl; - qApp->exit(EXIT_FAILURE); - return; - } - if (m_remoteData != StderrOutput) { - std::cerr << "Error: Expected output '" << StderrOutput.data() << "', received '" - << m_remoteData.data() << "'." << std::endl; - qApp->exit(EXIT_FAILURE); - return; - } - std::cout << "Ok.\nAll tests succeeded." << std::endl; - qApp->quit(); - break; - case Inactive: - Q_ASSERT(false); - } - break; - case SshRemoteProcess::FailedToStart: - if (m_started) { - std::cerr << "Error: Got 'failed to start' signal for process " - "that has not started yet." << std::endl; - } else { - std::cerr << "Error: Process failed to start." << std::endl; - } - qApp->quit(); - break; - case SshRemoteProcess::CrashExit: - switch (m_state) { - case TestingCrash: - handleSuccessfulCrashTest(); - break; - case TestingIoDevice: - handleSuccessfulIoTest(); - break; - default: - std::cerr << "Error: Unexpected crash." << std::endl; - qApp->quit(); - return; - } - } -} - -void RemoteProcessTest::handleTimeout() -{ - std::cerr << "Error: Timeout waiting for progress." << std::endl; - qApp->quit(); -} - -void RemoteProcessTest::handleConnected() -{ - Q_ASSERT(m_state == TestingIoDevice); - - m_catProcess = m_sshConnection->createRemoteProcess(QString::fromLocal8Bit("/bin/cat").toUtf8()); - connect(m_catProcess.data(), SIGNAL(started()), SLOT(handleProcessStarted())); - connect(m_catProcess.data(), SIGNAL(closed(int)), SLOT(handleProcessClosed(int))); - m_started = false; - m_timeoutTimer->start(); - m_catProcess->start(); -} - -QString RemoteProcessTest::testString() const -{ - return QLatin1String("x\r\n"); -} - -void RemoteProcessTest::handleReadyRead() -{ - switch (m_state) { - case TestingIoDevice: { - const QString &data = QString::fromUtf8(m_catProcess->readAll()); - if (data != testString()) { - std::cerr << "Testing of QIODevice functionality failed: Expected '" - << qPrintable(testString()) << "', got '" << qPrintable(data) << "'." << std::endl; - qApp->exit(1); - } - QSsh::SshRemoteProcessRunner * const killer = new QSsh::SshRemoteProcessRunner(this); - killer->run("pkill -9 cat", m_sshParams); - break; - } - case TestingProcessChannels: - m_remoteData += m_echoProcess->readAll(); - break; - default: - qFatal("%s: Unexpected state %d.", Q_FUNC_INFO, m_state); - } - -} - -void RemoteProcessTest::handleReadyReadStdout() -{ - Q_ASSERT(m_state == TestingProcessChannels); - - std::cerr << "Error: Received unexpected stdout data." << std::endl; - qApp->exit(EXIT_FAILURE); -} - -void RemoteProcessTest::handleReadyReadStderr() -{ - Q_ASSERT(m_state == TestingProcessChannels); - - m_remoteStderr = "dummy"; -} - -void RemoteProcessTest::handleSuccessfulCrashTest() -{ - std::cout << "Ok.\nTesting remote process with terminal... " << std::flush; - m_state = TestingTerminal; - m_started = false; - m_timeoutTimer->start(); - m_remoteRunner->runInTerminal("top -n 1", SshPseudoTerminal(), m_sshParams); -} - -void RemoteProcessTest::handleSuccessfulIoTest() -{ - std::cout << "Ok\nTesting process channels... " << std::flush; - m_state = TestingProcessChannels; - m_started = false; - m_remoteStderr.clear(); - m_echoProcess = m_sshConnection->createRemoteProcess("printf " + StderrOutput + " >&2"); - m_echoProcess->setReadChannel(QProcess::StandardError); - connect(m_echoProcess.data(), SIGNAL(started()), SLOT(handleProcessStarted())); - connect(m_echoProcess.data(), SIGNAL(closed(int)), SLOT(handleProcessClosed(int))); - connect(m_echoProcess.data(), SIGNAL(readyRead()), SLOT(handleReadyRead())); - connect(m_echoProcess.data(), SIGNAL(readyReadStandardError()), - SLOT(handleReadyReadStderr())); - m_echoProcess->start(); - m_timeoutTimer->start(); -} diff --git a/tests/manual/ssh/remoteprocess/remoteprocesstest.h b/tests/manual/ssh/remoteprocess/remoteprocesstest.h deleted file mode 100644 index 71fef7f..0000000 --- a/tests/manual/ssh/remoteprocess/remoteprocesstest.h +++ /dev/null @@ -1,86 +0,0 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ - -#ifndef REMOTEPROCESSTEST_H -#define REMOTEPROCESSTEST_H - -#include - -#include - -QT_FORWARD_DECLARE_CLASS(QTextStream) -QT_FORWARD_DECLARE_CLASS(QTimer) - -class RemoteProcessTest : public QObject -{ - Q_OBJECT -public: - RemoteProcessTest(const QSsh::SshConnectionParameters ¶ms); - ~RemoteProcessTest(); - void run(); - -private slots: - void handleConnectionError(); - void handleProcessStarted(); - void handleProcessStdout(); - void handleProcessStderr(); - void handleProcessClosed(int exitStatus); - void handleTimeout(); - void handleReadyRead(); - void handleReadyReadStdout(); - void handleReadyReadStderr(); - void handleConnected(); - -private: - enum State { - Inactive, TestingSuccess, TestingFailure, TestingCrash, TestingTerminal, TestingIoDevice, - TestingProcessChannels - }; - - QString testString() const; - void handleSuccessfulCrashTest(); - void handleSuccessfulIoTest(); - - const QSsh::SshConnectionParameters m_sshParams; - QTimer * const m_timeoutTimer; - QTextStream *m_textStream; - QSsh::SshConnection *m_sshConnection; - QSsh::SshRemoteProcessRunner * const m_remoteRunner; - QSsh::SshRemoteProcess::Ptr m_catProcess; - QSsh::SshRemoteProcess::Ptr m_echoProcess; - QByteArray m_remoteStdout; - QByteArray m_remoteStderr; - QByteArray m_remoteData; - State m_state; - bool m_started; -}; - - -#endif // REMOTEPROCESSTEST_H diff --git a/tests/manual/ssh/sftp/argumentscollector.cpp b/tests/manual/ssh/sftp/argumentscollector.cpp deleted file mode 100644 index 9ba7d22..0000000 --- a/tests/manual/ssh/sftp/argumentscollector.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/************************************************************************** -** -** 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 "argumentscollector.h" - -#include - -using namespace QSsh; - -using namespace std; - -ArgumentsCollector::ArgumentsCollector(const QStringList &args) - : m_arguments(args) -{ -} - -Parameters ArgumentsCollector::collect(bool &success) const -{ - Parameters parameters; - try { - bool authTypeGiven = false; - bool portGiven = false; - bool timeoutGiven = false; - bool smallFileCountGiven = false; - bool bigFileSizeGiven = false; - bool proxySettingGiven = false; - int pos; - int port; - for (pos = 1; pos < m_arguments.count() - 1; ++pos) { - if (checkAndSetStringArg(pos, parameters.sshParams.host, "-h") - || checkAndSetStringArg(pos, parameters.sshParams.userName, "-u")) - continue; - if (checkAndSetIntArg(pos, port, portGiven, "-p") - || checkAndSetIntArg(pos, parameters.sshParams.timeout, timeoutGiven, "-t") - || checkAndSetIntArg(pos, parameters.smallFileCount, smallFileCountGiven, "-c") - || checkAndSetIntArg(pos, parameters.bigFileSize, bigFileSizeGiven, "-s")) - continue; - if (checkAndSetStringArg(pos, parameters.sshParams.password, "-pwd")) { - if (!parameters.sshParams.privateKeyFile.isEmpty()) - throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive.")); - parameters.sshParams.authenticationType - = SshConnectionParameters::AuthenticationByPassword; - authTypeGiven = true; - continue; - } - if (checkAndSetStringArg(pos, parameters.sshParams.privateKeyFile, "-k")) { - if (!parameters.sshParams.password.isEmpty()) - throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive.")); - parameters.sshParams.authenticationType - = SshConnectionParameters::AuthenticationByKey; - authTypeGiven = true; - continue; - } - if (!checkForNoProxy(pos, parameters.sshParams.proxyType, proxySettingGiven)) - throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos)); - } - - Q_ASSERT(pos <= m_arguments.count()); - if (pos == m_arguments.count() - 1) { - if (!checkForNoProxy(pos, parameters.sshParams.proxyType, proxySettingGiven)) - throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos)); - } - - if (!authTypeGiven) - throw ArgumentErrorException(QLatin1String("No authentication argument given.")); - if (parameters.sshParams.host.isEmpty()) - throw ArgumentErrorException(QLatin1String("No host given.")); - if (parameters.sshParams.userName.isEmpty()) - throw ArgumentErrorException(QLatin1String("No user name given.")); - - parameters.sshParams.port = portGiven ? port : 22; - if (!timeoutGiven) - parameters.sshParams.timeout = 30; - if (!smallFileCountGiven) - parameters.smallFileCount = 1000; - if (!bigFileSizeGiven) - parameters.bigFileSize = 1024; - success = true; - } catch (ArgumentErrorException &ex) { - cerr << "Error: " << qPrintable(ex.error) << endl; - printUsage(); - success = false; - } - return parameters; -} - -void ArgumentsCollector::printUsage() const -{ - cerr << "Usage: " << qPrintable(m_arguments.first()) - << " -h -u " - << "-pwd | -k [ -p ] " - << "[ -t ] [ -c ] " - << "[ -s ] [ -no-proxy ]" << endl; -} - -bool ArgumentsCollector::checkAndSetStringArg(int &pos, QString &arg, const char *opt) const -{ - if (m_arguments.at(pos) == QLatin1String(opt)) { - if (!arg.isEmpty()) { - throw ArgumentErrorException(QLatin1String("option ") + opt - + QLatin1String(" was given twice.")); - } - arg = m_arguments.at(++pos); - if (arg.isEmpty() && QLatin1String(opt) != QLatin1String("-pwd")) - throw ArgumentErrorException(QLatin1String("empty argument not allowed here.")); - return true; - } - return false; -} - -bool ArgumentsCollector::checkAndSetIntArg(int &pos, int &val, - bool &alreadyGiven, const char *opt) const -{ - if (m_arguments.at(pos) == QLatin1String(opt)) { - if (alreadyGiven) { - throw ArgumentErrorException(QLatin1String("option ") + opt - + QLatin1String(" was given twice.")); - } - bool isNumber; - val = m_arguments.at(++pos).toInt(&isNumber); - if (!isNumber) { - throw ArgumentErrorException(QLatin1String("option ") + opt - + QLatin1String(" needs integer argument")); - } - alreadyGiven = true; - return true; - } - return false; -} - -bool ArgumentsCollector::checkForNoProxy(int &pos, - SshConnectionParameters::ProxyType &type, bool &alreadyGiven) const -{ - if (m_arguments.at(pos) == QLatin1String("-no-proxy")) { - if (alreadyGiven) - throw ArgumentErrorException(QLatin1String("proxy setting given twice.")); - type = SshConnectionParameters::NoProxy; - alreadyGiven = true; - return true; - } - return false; -} diff --git a/tests/manual/ssh/sftp/argumentscollector.h b/tests/manual/ssh/sftp/argumentscollector.h deleted file mode 100644 index cc7c7cf..0000000 --- a/tests/manual/ssh/sftp/argumentscollector.h +++ /dev/null @@ -1,60 +0,0 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ - -#ifndef ARGUMENTSCOLLECTOR_H -#define ARGUMENTSCOLLECTOR_H - -#include "parameters.h" - -#include - -class ArgumentsCollector -{ -public: - ArgumentsCollector(const QStringList &args); - Parameters collect(bool &success) const; -private: - struct ArgumentErrorException - { - ArgumentErrorException(const QString &error) : error(error) {} - const QString error; - }; - - void printUsage() const; - bool checkAndSetStringArg(int &pos, QString &arg, const char *opt) const; - bool checkAndSetIntArg(int &pos, int &val, bool &alreadyGiven, - const char *opt) const; - bool checkForNoProxy(int &pos, QSsh::SshConnectionParameters::ProxyType &type, - bool &alreadyGiven) const; - - const QStringList m_arguments; -}; - -#endif // ARGUMENTSCOLLECTOR_H diff --git a/tests/manual/ssh/sftp/main.cpp b/tests/manual/ssh/sftp/main.cpp deleted file mode 100644 index 71d8dec..0000000 --- a/tests/manual/ssh/sftp/main.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/************************************************************************** -** -** 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 "argumentscollector.h" -#include "sftptest.h" - -#include -#include - -#include -#include -#include - -#include -#include - -int main(int argc, char *argv[]) -{ - QCoreApplication app(argc, argv); - bool parseSuccess; - const Parameters parameters = ArgumentsCollector(app.arguments()).collect(parseSuccess); - if (!parseSuccess) - return EXIT_FAILURE; - SftpTest sftpTest(parameters); - sftpTest.run(); - return app.exec(); -} diff --git a/tests/manual/ssh/sftp/parameters.h b/tests/manual/ssh/sftp/parameters.h deleted file mode 100644 index 7e0a5b3..0000000 --- a/tests/manual/ssh/sftp/parameters.h +++ /dev/null @@ -1,42 +0,0 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ - -#ifndef PARAMETERS_H -#define PARAMETERS_H - -#include - -struct Parameters { - QSsh::SshConnectionParameters sshParams; - int smallFileCount; - int bigFileSize; -}; - -#endif // PARAMETERS_H diff --git a/tests/manual/ssh/sftp/sftp.pro b/tests/manual/ssh/sftp/sftp.pro deleted file mode 100644 index eb5b7d8..0000000 --- a/tests/manual/ssh/sftp/sftp.pro +++ /dev/null @@ -1,6 +0,0 @@ -include(../ssh.pri) - -TARGET=sftp -SOURCES=main.cpp sftptest.cpp argumentscollector.cpp -HEADERS=sftptest.h argumentscollector.h parameters.h - diff --git a/tests/manual/ssh/sftp/sftptest.cpp b/tests/manual/ssh/sftp/sftptest.cpp deleted file mode 100644 index e8b08a3..0000000 --- a/tests/manual/ssh/sftp/sftptest.cpp +++ /dev/null @@ -1,634 +0,0 @@ -/************************************************************************** -** -** 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 "sftptest.h" - -#include -#include -#include -#include -#include - -#include - -using namespace QSsh; - -SftpTest::SftpTest(const Parameters ¶ms) - : m_parameters(params), m_state(Inactive), m_error(false), m_connection(0), - m_bigFileUploadJob(SftpInvalidJob), - m_bigFileDownloadJob(SftpInvalidJob), - m_bigFileRemovalJob(SftpInvalidJob), - m_mkdirJob(SftpInvalidJob), - m_statDirJob(SftpInvalidJob), - m_lsDirJob(SftpInvalidJob), - m_rmDirJob(SftpInvalidJob) -{ -} - -SftpTest::~SftpTest() -{ - removeFiles(true); - delete m_connection; -} - -void SftpTest::run() -{ - m_connection = new SshConnection(m_parameters.sshParams); - connect(m_connection, SIGNAL(connected()), SLOT(handleConnected())); - connect(m_connection, SIGNAL(error(QSsh::SshError)), SLOT(handleError())); - connect(m_connection, SIGNAL(disconnected()), SLOT(handleDisconnected())); - std::cout << "Connecting to host '" - << qPrintable(m_parameters.sshParams.host) << "'..." << std::endl; - m_state = Connecting; - m_connection->connectToHost(); -} - -void SftpTest::handleConnected() -{ - if (m_state != Connecting) { - std::cerr << "Unexpected state " << m_state << " in function " - << Q_FUNC_INFO << "." << std::endl; - earlyDisconnectFromHost(); - } else { - std::cout << "Connected. Initializing SFTP channel..." << std::endl; - m_channel = m_connection->createSftpChannel(); - connect(m_channel.data(), SIGNAL(initialized()), this, - SLOT(handleChannelInitialized())); - connect(m_channel.data(), SIGNAL(initializationFailed(QString)), this, - SLOT(handleChannelInitializationFailure(QString))); - connect(m_channel.data(), SIGNAL(finished(QSsh::SftpJobId,QString)), - this, SLOT(handleJobFinished(QSsh::SftpJobId,QString))); - connect(m_channel.data(), - SIGNAL(fileInfoAvailable(QSsh::SftpJobId,QList)), - SLOT(handleFileInfo(QSsh::SftpJobId,QList))); - connect(m_channel.data(), SIGNAL(closed()), this, - SLOT(handleChannelClosed())); - m_state = InitializingChannel; - m_channel->initialize(); - } -} - -void SftpTest::handleDisconnected() -{ - if (m_state != Disconnecting) { - std::cerr << "Unexpected state " << m_state << " in function " - << Q_FUNC_INFO << std::endl; - m_error = true; - } else { - std::cout << "Connection closed." << std::endl; - } - std::cout << "Test finished. "; - if (m_error) - std::cout << "There were errors."; - else - std::cout << "No errors encountered."; - std::cout << std::endl; - qApp->exit(m_error ? EXIT_FAILURE : EXIT_SUCCESS); -} - -void SftpTest::handleError() -{ - std::cerr << "Encountered SSH error: " - << qPrintable(m_connection->errorString()) << "." << std::endl; - m_error = true; - m_state = Disconnecting; - qApp->exit(EXIT_FAILURE); -} - -void SftpTest::handleChannelInitialized() -{ - if (m_state != InitializingChannel) { - std::cerr << "Unexpected state " << m_state << "in function " - << Q_FUNC_INFO << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - - std::cout << "Creating " << m_parameters.smallFileCount - << " files of 1 KB each ..." << std::endl; - qsrand(QDateTime::currentDateTime().toTime_t()); - for (int i = 0; i < m_parameters.smallFileCount; ++i) { - const QString fileName - = QLatin1String("sftptestfile") + QString::number(i + 1); - const FilePtr file(new QFile(QDir::tempPath() + QLatin1Char('/') - + fileName)); - bool success = true; - if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) - success = false; - if (success) { - int content[1024/sizeof(int)]; - for (size_t j = 0; j < sizeof content / sizeof content[0]; ++j) - content[j] = qrand(); - file->write(reinterpret_cast(content), sizeof content); - file->close(); - } - success = success && file->error() == QFile::NoError; - if (!success) { - std::cerr << "Error creating local file " - << qPrintable(file->fileName()) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_localSmallFiles << file; - } - std::cout << "Files created. Now uploading..." << std::endl; - foreach (const FilePtr &file, m_localSmallFiles) { - const QString localFilePath = file->fileName(); - const QString remoteFp - = remoteFilePath(QFileInfo(localFilePath).fileName()); - const SftpJobId uploadJob = m_channel->uploadFile(file->fileName(), - remoteFp, SftpOverwriteExisting); - if (uploadJob == SftpInvalidJob) { - std::cerr << "Error uploading local file " - << qPrintable(localFilePath) << " to remote file " - << qPrintable(remoteFp) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_smallFilesUploadJobs.insert(uploadJob, remoteFp); - } - m_state = UploadingSmall; -} - -void SftpTest::handleChannelInitializationFailure(const QString &reason) -{ - std::cerr << "Could not initialize SFTP channel: " << qPrintable(reason) - << "." << std::endl; - earlyDisconnectFromHost(); -} - -void SftpTest::handleChannelClosed() -{ - if (m_state != ChannelClosing) { - std::cerr << "Unexpected state " << m_state << " in function " - << Q_FUNC_INFO << "." << std::endl; - } else { - std::cout << "SFTP channel closed. Now disconnecting..." << std::endl; - } - m_state = Disconnecting; - m_connection->disconnectFromHost(); -} - -void SftpTest::handleJobFinished(QSsh::SftpJobId job, const QString &error) -{ - switch (m_state) { - case UploadingSmall: - if (!handleJobFinished(job, m_smallFilesUploadJobs, error, "uploading")) - return; - if (m_smallFilesUploadJobs.isEmpty()) { - std::cout << "Uploading finished, now downloading for comparison..." - << std::endl; - foreach (const FilePtr &file, m_localSmallFiles) { - const QString localFilePath = file->fileName(); - const QString remoteFp - = remoteFilePath(QFileInfo(localFilePath).fileName()); - const QString downloadFilePath = cmpFileName(localFilePath); - const SftpJobId downloadJob = m_channel->downloadFile(remoteFp, - downloadFilePath, SftpOverwriteExisting); - if (downloadJob == SftpInvalidJob) { - std::cerr << "Error downloading remote file " - << qPrintable(remoteFp) << " to local file " - << qPrintable(downloadFilePath) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_smallFilesDownloadJobs.insert(downloadJob, remoteFp); - } - m_state = DownloadingSmall; - } - break; - case DownloadingSmall: - if (!handleJobFinished(job, m_smallFilesDownloadJobs, error, "downloading")) - return; - if (m_smallFilesDownloadJobs.isEmpty()) { - std::cout << "Downloading finished, now comparing..." << std::endl; - foreach (const FilePtr &ptr, m_localSmallFiles) { - if (!ptr->open(QIODevice::ReadOnly)) { - std::cerr << "Error opening local file " - << qPrintable(ptr->fileName()) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - const QString downloadedFilePath = cmpFileName(ptr->fileName()); - QFile downloadedFile(downloadedFilePath); - if (!downloadedFile.open(QIODevice::ReadOnly)) { - std::cerr << "Error opening downloaded file " - << qPrintable(downloadedFilePath) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - if (!compareFiles(ptr.data(), &downloadedFile)) - return; - } - - std::cout << "Comparisons successful, now removing files..." - << std::endl; - QList remoteFilePaths; - foreach (const FilePtr &ptr, m_localSmallFiles) { - const QString downloadedFilePath = cmpFileName(ptr->fileName()); - remoteFilePaths - << remoteFilePath(QFileInfo(ptr->fileName()).fileName()); - if (!ptr->remove()) { - std::cerr << "Error: Failed to remove local file '" - << qPrintable(ptr->fileName()) << "'." << std::endl; - earlyDisconnectFromHost(); - } - if (!QFile::remove(downloadedFilePath)) { - std::cerr << "Error: Failed to remove downloaded file '" - << qPrintable(downloadedFilePath) << "'." << std::endl; - earlyDisconnectFromHost(); - } - } - m_localSmallFiles.clear(); - foreach (const QString &remoteFp, remoteFilePaths) { - m_smallFilesRemovalJobs.insert(m_channel->removeFile(remoteFp), - remoteFp); - } - m_state = RemovingSmall; - } - break; - case RemovingSmall: - if (!handleJobFinished(job, m_smallFilesRemovalJobs, error, "removing")) - return; - if (m_smallFilesRemovalJobs.isEmpty()) { - std::cout << "Small files successfully removed. " - << "Now creating big file..." << std::endl; - const QLatin1String bigFileName("sftpbigfile"); - m_localBigFile = FilePtr(new QFile(QDir::tempPath() - + QLatin1Char('/') + bigFileName)); - bool success = m_localBigFile->open(QIODevice::WriteOnly); - const int blockSize = 8192; - const quint64 blockCount - = static_cast(m_parameters.bigFileSize)*1024*1024/blockSize; - for (quint64 block = 0; block < blockCount; ++block) { - int content[blockSize/sizeof(int)]; - for (size_t j = 0; j < sizeof content / sizeof content[0]; ++j) - content[j] = qrand(); - m_localBigFile->write(reinterpret_cast(content), - sizeof content); - } - m_localBigFile->close(); - success = success && m_localBigFile->error() == QFile::NoError; - if (!success) { - std::cerr << "Error trying to create big file '" - << qPrintable(m_localBigFile->fileName()) << "'." - << std::endl; - earlyDisconnectFromHost(); - return; - } - - std::cout << "Big file created. Now uploading ..." << std::endl; - m_bigJobTimer.start(); - m_bigFileUploadJob - = m_channel->uploadFile(m_localBigFile->fileName(), - remoteFilePath(bigFileName), SftpOverwriteExisting); - if (m_bigFileUploadJob == SftpInvalidJob) { - std::cerr << "Error uploading file '" << bigFileName.latin1() - << "'." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_state = UploadingBig; - } - break; - case UploadingBig: { - if (!handleBigJobFinished(job, m_bigFileUploadJob, error, "uploading")) - return; - const qint64 msecs = m_bigJobTimer.elapsed(); - std::cout << "Successfully uploaded big file. Took " << (msecs/1000) - << " seconds for " << m_parameters.bigFileSize << " MB." - << std::endl; - const QString localFilePath = m_localBigFile->fileName(); - const QString downloadedFilePath = cmpFileName(localFilePath); - const QString remoteFp - = remoteFilePath(QFileInfo(localFilePath).fileName()); - std::cout << "Now downloading big file for comparison..." << std::endl; - m_bigJobTimer.start(); - m_bigFileDownloadJob = m_channel->downloadFile(remoteFp, - downloadedFilePath, SftpOverwriteExisting); - if (m_bigFileDownloadJob == SftpInvalidJob) { - std::cerr << "Error downloading remote file '" - << qPrintable(remoteFp) << "' to local file '" - << qPrintable(downloadedFilePath) << "'." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_state = DownloadingBig; - break; - } - case DownloadingBig: { - if (!handleBigJobFinished(job, m_bigFileDownloadJob, error, "downloading")) - return; - const qint64 msecs = m_bigJobTimer.elapsed(); - std::cout << "Successfully downloaded big file. Took " << (msecs/1000) - << " seconds for " << m_parameters.bigFileSize << " MB." - << std::endl; - std::cout << "Now comparing big files..." << std::endl; - QFile downloadedFile(cmpFileName(m_localBigFile->fileName())); - if (!downloadedFile.open(QIODevice::ReadOnly)) { - std::cerr << "Error opening downloaded file '" - << qPrintable(downloadedFile.fileName()) << "': " - << qPrintable(downloadedFile.errorString()) << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - if (!m_localBigFile->open(QIODevice::ReadOnly)) { - std::cerr << "Error opening big file '" - << qPrintable(m_localBigFile->fileName()) << "': " - << qPrintable(m_localBigFile->errorString()) << "." - << std::endl; - earlyDisconnectFromHost(); - return; - } - if (!compareFiles(m_localBigFile.data(), &downloadedFile)) - return; - std::cout << "Comparison successful. Now removing big files..." - << std::endl; - if (!m_localBigFile->remove()) { - std::cerr << "Error: Could not remove file '" - << qPrintable(m_localBigFile->fileName()) << "'." << std::endl; - earlyDisconnectFromHost(); - return; - } - if (!downloadedFile.remove()) { - std::cerr << "Error: Could not remove file '" - << qPrintable(downloadedFile.fileName()) << "'." << std::endl; - earlyDisconnectFromHost(); - return; - } - const QString remoteFp - = remoteFilePath(QFileInfo(m_localBigFile->fileName()).fileName()); - m_bigFileRemovalJob = m_channel->removeFile(remoteFp); - m_state = RemovingBig; - break; - } - case RemovingBig: - if (!handleBigJobFinished(job, m_bigFileRemovalJob, error, "removing")) - return; - std::cout << "Big files successfully removed. " - << "Now creating remote directory..." << std::endl; - m_remoteDirPath = QLatin1String("/tmp/sftptest-") + QDateTime::currentDateTime().toString(); - m_mkdirJob = m_channel->createDirectory(m_remoteDirPath); - m_state = CreatingDir; - break; - case CreatingDir: - if (!handleJobFinished(job, m_mkdirJob, error, "creating remote directory")) - return; - std::cout << "Directory successfully created. Now checking directory attributes..." - << std::endl; - m_statDirJob = m_channel->statFile(m_remoteDirPath); - m_state = CheckingDirAttributes; - break; - case CheckingDirAttributes: { - if (!handleJobFinished(job, m_statDirJob, error, "checking directory attributes")) - return; - if (m_dirInfo.type != FileTypeDirectory) { - std::cerr << "Error: Newly created directory has file type " << m_dirInfo.type - << ", expected was " << FileTypeDirectory << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - const QString fileName = QFileInfo(m_remoteDirPath).fileName(); - if (m_dirInfo.name != fileName) { - std::cerr << "Error: Remote directory reports file name '" - << qPrintable(m_dirInfo.name) << "', expected '" << qPrintable(fileName) << "'." - << std::endl; - earlyDisconnectFromHost(); - return; - } - std::cout << "Directory attributes ok. Now checking directory contents..." << std::endl; - m_lsDirJob = m_channel->listDirectory(m_remoteDirPath); - m_state = CheckingDirContents; - break; - } - case CheckingDirContents: - if (!handleJobFinished(job, m_lsDirJob, error, "checking directory contents")) - return; - if (m_dirContents.count() != 2) { - std::cerr << "Error: Remote directory has " << m_dirContents.count() - << " entries, expected 2." << std::endl; - earlyDisconnectFromHost(); - return; - } - foreach (const SftpFileInfo &fi, m_dirContents) { - if (fi.type != FileTypeDirectory) { - std::cerr << "Error: Remote directory has entry of type " << fi.type - << ", expected " << FileTypeDirectory << "." << std::endl; - earlyDisconnectFromHost(); - return; - } - if (fi.name != QLatin1String(".") && fi.name != QLatin1String("..")) { - std::cerr << "Error: Remote directory has entry '" << qPrintable(fi.name) - << "', expected '.' or '..'." << std::endl; - earlyDisconnectFromHost(); - return; - } - } - if (m_dirContents.first().name == m_dirContents.last().name) { - std::cerr << "Error: Remote directory has two entries of the same name." << std::endl; - earlyDisconnectFromHost(); - return; - } - std::cout << "Directory contents ok. Now removing directory..." << std::endl; - m_rmDirJob = m_channel->removeDirectory(m_remoteDirPath); - m_state = RemovingDir; - break; - case RemovingDir: - if (!handleJobFinished(job, m_rmDirJob, error, "removing directory")) - return; - std::cout << "Directory successfully removed. Now closing the SFTP channel..." << std::endl; - m_state = ChannelClosing; - m_channel->closeChannel(); - break; - case Disconnecting: - break; - default: - if (!m_error) { - std::cerr << "Unexpected state " << m_state << " in function " - << Q_FUNC_INFO << "." << std::endl; - earlyDisconnectFromHost(); - } - } -} - -void SftpTest::handleFileInfo(SftpJobId job, const QList &fileInfoList) -{ - switch (m_state) { - case CheckingDirAttributes: { - static int count = 0; - if (!checkJobId(job, m_statDirJob, "checking directory attributes")) - return; - if (++count > 1) { - std::cerr << "Error: More than one reply for directory attributes check." << std::endl; - earlyDisconnectFromHost(); - return; - } - m_dirInfo = fileInfoList.first(); - break; - } - case CheckingDirContents: - if (!checkJobId(job, m_lsDirJob, "checking directory contents")) - return; - m_dirContents << fileInfoList; - break; - default: - std::cerr << "Error: Unexpected file info in state " << m_state << "." << std::endl; - earlyDisconnectFromHost(); - } -} - -void SftpTest::removeFile(const FilePtr &file, bool remoteToo) -{ - if (!file) - return; - const QString localFilePath = file->fileName(); - file->remove(); - QFile::remove(cmpFileName(localFilePath)); - if (remoteToo && m_channel - && m_channel->state() == SftpChannel::Initialized) - m_channel->removeFile(remoteFilePath(QFileInfo(localFilePath).fileName())); -} - -QString SftpTest::cmpFileName(const QString &fileName) const -{ - return fileName + QLatin1String(".cmp"); -} - -QString SftpTest::remoteFilePath(const QString &localFileName) const -{ - return QLatin1String("/tmp/") + localFileName + QLatin1String(".upload"); -} - -void SftpTest::earlyDisconnectFromHost() -{ - m_error = true; - removeFiles(true); - if (m_channel) - disconnect(m_channel.data(), 0, this, 0); - m_state = Disconnecting; - m_connection->disconnectFromHost(); -} - -bool SftpTest::checkJobId(SftpJobId job, SftpJobId expectedJob, const char *activity) -{ - if (job != expectedJob) { - std::cerr << "Error " << activity << ": Expected job id " << expectedJob - << ", got job id " << job << '.' << std::endl; - earlyDisconnectFromHost(); - return false; - } - return true; -} - -void SftpTest::removeFiles(bool remoteToo) -{ - foreach (const FilePtr &file, m_localSmallFiles) - removeFile(file, remoteToo); - removeFile(m_localBigFile, remoteToo); -} - -bool SftpTest::handleJobFinished(SftpJobId job, JobMap &jobMap, - const QString &error, const char *activity) -{ - JobMap::Iterator it = jobMap.find(job); - if (it == jobMap.end()) { - std::cerr << "Error: Unknown job " << job << "finished." - << std::endl; - earlyDisconnectFromHost(); - return false; - } - if (!error.isEmpty()) { - std::cerr << "Error " << activity << " file " << qPrintable(it.value()) - << ": " << qPrintable(error) << "." << std::endl; - earlyDisconnectFromHost(); - return false; - } - jobMap.erase(it); - return true; -} - -bool SftpTest::handleJobFinished(SftpJobId job, SftpJobId expectedJob, const QString &error, - const char *activity) -{ - if (!checkJobId(job, expectedJob, activity)) - return false; - if (!error.isEmpty()) { - std::cerr << "Error " << activity << ": " << qPrintable(error) << "." << std::endl; - earlyDisconnectFromHost(); - return false; - } - return true; -} - -bool SftpTest::handleBigJobFinished(SftpJobId job, SftpJobId expectedJob, - const QString &error, const char *activity) -{ - if (job != expectedJob) { - std::cerr << "Error " << activity << " file '" - << qPrintable(m_localBigFile->fileName()) - << "': Expected job id " << expectedJob - << ", got job id " << job << '.' << std::endl; - earlyDisconnectFromHost(); - return false; - } - if (!error.isEmpty()) { - std::cerr << "Error " << activity << " file '" - << qPrintable(m_localBigFile->fileName()) << "': " - << qPrintable(error) << std::endl; - earlyDisconnectFromHost(); - return false; - } - return true; -} - -bool SftpTest::compareFiles(QFile *orig, QFile *copy) -{ - bool success = orig->size() == copy->size(); - qint64 bytesLeft = orig->size(); - orig->seek(0); - while (success && bytesLeft > 0) { - const qint64 bytesToRead = qMin(bytesLeft, Q_INT64_C(1024*1024)); - const QByteArray origBlock = orig->read(bytesToRead); - const QByteArray copyBlock = copy->read(bytesToRead); - if (origBlock.size() != bytesToRead || origBlock != copyBlock) - success = false; - bytesLeft -= bytesToRead; - } - orig->close(); - success = success && orig->error() == QFile::NoError - && copy->error() == QFile::NoError; - if (!success) { - std::cerr << "Error: Original file '" << qPrintable(orig->fileName()) - << "'' differs from downloaded file '" - << qPrintable(copy->fileName()) << "'." << std::endl; - earlyDisconnectFromHost(); - } - return success; -} diff --git a/tests/manual/ssh/sftp/sftptest.h b/tests/manual/ssh/sftp/sftptest.h deleted file mode 100644 index 2ae48d0..0000000 --- a/tests/manual/ssh/sftp/sftptest.h +++ /dev/null @@ -1,112 +0,0 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ - -#ifndef SFTPTEST_H -#define SFTPTEST_H - -#include "parameters.h" - -#include -#include - -#include -#include -#include -#include -#include - -QT_FORWARD_DECLARE_CLASS(QFile); - -class SftpTest : public QObject -{ - Q_OBJECT -public: - SftpTest(const Parameters ¶ms); - ~SftpTest(); - void run(); - -private slots: - void handleConnected(); - void handleError(); - void handleDisconnected(); - void handleChannelInitialized(); - void handleChannelInitializationFailure(const QString &reason); - void handleJobFinished(QSsh::SftpJobId job, const QString &error); - void handleFileInfo(QSsh::SftpJobId job, const QList &fileInfoList); - void handleChannelClosed(); - -private: - typedef QHash JobMap; - typedef QSharedPointer FilePtr; - enum State { - Inactive, Connecting, InitializingChannel, UploadingSmall, DownloadingSmall, - RemovingSmall, UploadingBig, DownloadingBig, RemovingBig, CreatingDir, - CheckingDirAttributes, CheckingDirContents, RemovingDir, ChannelClosing, Disconnecting - }; - - void removeFile(const FilePtr &filePtr, bool remoteToo); - void removeFiles(bool remoteToo); - QString cmpFileName(const QString &localFileName) const; - QString remoteFilePath(const QString &localFileName) const; - void earlyDisconnectFromHost(); - bool checkJobId(QSsh::SftpJobId job, QSsh::SftpJobId expectedJob, const char *activity); - bool handleJobFinished(QSsh::SftpJobId job, JobMap &jobMap, - const QString &error, const char *activity); - bool handleJobFinished(QSsh::SftpJobId job, QSsh::SftpJobId expectedJob, const QString &error, - const char *activity); - bool handleBigJobFinished(QSsh::SftpJobId job, QSsh::SftpJobId expectedJob, - const QString &error, const char *activity); - bool compareFiles(QFile *orig, QFile *copy); - - const Parameters m_parameters; - State m_state; - bool m_error; - QSsh::SshConnection *m_connection; - QSsh::SftpChannel::Ptr m_channel; - QList m_localSmallFiles; - JobMap m_smallFilesUploadJobs; - JobMap m_smallFilesDownloadJobs; - JobMap m_smallFilesRemovalJobs; - FilePtr m_localBigFile; - QSsh::SftpJobId m_bigFileUploadJob; - QSsh::SftpJobId m_bigFileDownloadJob; - QSsh::SftpJobId m_bigFileRemovalJob; - QSsh::SftpJobId m_mkdirJob; - QSsh::SftpJobId m_statDirJob; - QSsh::SftpJobId m_lsDirJob; - QSsh::SftpJobId m_rmDirJob; - QElapsedTimer m_bigJobTimer; - QString m_remoteDirPath; - QSsh::SftpFileInfo m_dirInfo; - QList m_dirContents; -}; - - -#endif // SFTPTEST_H diff --git a/tests/manual/ssh/sftpfsmodel/main.cpp b/tests/manual/ssh/sftpfsmodel/main.cpp index 76022a9..8289480 100644 --- a/tests/manual/ssh/sftpfsmodel/main.cpp +++ b/tests/manual/ssh/sftpfsmodel/main.cpp @@ -1,32 +1,28 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ +/**************************************************************************** +** +** Copyright (C) 2016 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 "window.h" #include diff --git a/tests/manual/ssh/sftpfsmodel/window.cpp b/tests/manual/ssh/sftpfsmodel/window.cpp index 9b9868e..ed038da 100644 --- a/tests/manual/ssh/sftpfsmodel/window.cpp +++ b/tests/manual/ssh/sftpfsmodel/window.cpp @@ -1,32 +1,28 @@ -/************************************************************************** +/**************************************************************************** ** -** This file is part of Qt Creator +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ ** -** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** This file is part of Qt Creator. ** -** Contact: http://www.qt-project.org/ +** 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. ** -** 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 "window.h" #include "ui_window.h" @@ -46,8 +42,8 @@ using namespace QSsh; SftpFsWindow::SftpFsWindow(QWidget *parent) : QDialog(parent), m_ui(new Ui::Window) { m_ui->setupUi(this); - connect(m_ui->connectButton, SIGNAL(clicked()), SLOT(connectToHost())); - connect(m_ui->downloadButton, SIGNAL(clicked()), SLOT(downloadFile())); + connect(m_ui->connectButton, &QAbstractButton::clicked, this, &SftpFsWindow::connectToHost); + connect(m_ui->downloadButton, &QAbstractButton::clicked, this, &SftpFsWindow::downloadFile); } SftpFsWindow::~SftpFsWindow() @@ -59,18 +55,20 @@ void SftpFsWindow::connectToHost() { m_ui->connectButton->setEnabled(false); SshConnectionParameters sshParams; - sshParams.host = m_ui->hostLineEdit->text(); - sshParams.userName = m_ui->userLineEdit->text(); - sshParams.authenticationType = SshConnectionParameters::AuthenticationByPassword; - sshParams.password = m_ui->passwordLineEdit->text(); - sshParams.port = m_ui->portSpinBox->value(); + sshParams.setHost(m_ui->hostLineEdit->text()); + sshParams.setUserName(m_ui->userLineEdit->text()); + sshParams.authenticationType + = SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods; + sshParams.setPassword(m_ui->passwordLineEdit->text()); + sshParams.setPort(m_ui->portSpinBox->value()); sshParams.timeout = 10; m_fsModel = new SftpFileSystemModel(this); - connect(m_fsModel, SIGNAL(sftpOperationFailed(QString)), - SLOT(handleSftpOperationFailed(QString))); - connect(m_fsModel, SIGNAL(connectionError(QString)), SLOT(handleConnectionError(QString))); - connect(m_fsModel, SIGNAL(sftpOperationFinished(QSsh::SftpJobId,QString)), - SLOT(handleSftpOperationFinished(QSsh::SftpJobId,QString))); + connect(m_fsModel, &SftpFileSystemModel::sftpOperationFailed, + this, &SftpFsWindow::handleSftpOperationFailed); + connect(m_fsModel, &SftpFileSystemModel::connectionError, + this, &SftpFsWindow::handleConnectionError); + connect(m_fsModel, &SftpFileSystemModel::sftpOperationFinished, + this, &SftpFsWindow::handleSftpOperationFinished); m_fsModel->setSshConnection(sshParams); m_ui->fsView->setModel(m_fsModel); } @@ -80,7 +78,7 @@ void SftpFsWindow::downloadFile() const QModelIndexList selectedIndexes = m_ui->fsView->selectionModel()->selectedIndexes(); if (selectedIndexes.count() != 2) return; - const QString targetFilePath = QFileDialog::getSaveFileName(this, tr("Choose target file"), + const QString targetFilePath = QFileDialog::getSaveFileName(this, tr("Choose Target File"), QDir::tempPath()); if (targetFilePath.isEmpty()) return; @@ -112,5 +110,5 @@ void SftpFsWindow::handleConnectionError(const QString &errorMessage) { QMessageBox::warning(this, tr("Connection Error"), tr("Fatal SSH error: %1").arg(errorMessage)); - qApp->quit(); + QCoreApplication::quit(); } diff --git a/tests/manual/ssh/sftpfsmodel/window.h b/tests/manual/ssh/sftpfsmodel/window.h index 69cee9a..d3ba03c 100644 --- a/tests/manual/ssh/sftpfsmodel/window.h +++ b/tests/manual/ssh/sftpfsmodel/window.h @@ -1,32 +1,28 @@ -/************************************************************************** -** -** 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. -** -** -**************************************************************************/ +/**************************************************************************** +** +** Copyright (C) 2016 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 #include @@ -44,14 +40,13 @@ class SftpFsWindow : public QDialog SftpFsWindow(QWidget *parent = 0); ~SftpFsWindow(); -private slots: +private: void connectToHost(); void downloadFile(); void handleConnectionError(const QString &errorMessage); void handleSftpOperationFailed(const QString &errorMessage); void handleSftpOperationFinished(QSsh::SftpJobId jobId, const QString &error); -private: QSsh::SftpFileSystemModel *m_fsModel; Ui::Window *m_ui; }; diff --git a/tests/manual/ssh/remoteprocess/argumentscollector.cpp b/tests/manual/ssh/shell/argumentscollector.cpp similarity index 61% rename from tests/manual/ssh/remoteprocess/argumentscollector.cpp rename to tests/manual/ssh/shell/argumentscollector.cpp index 285805b..cc98393 100644 --- a/tests/manual/ssh/remoteprocess/argumentscollector.cpp +++ b/tests/manual/ssh/shell/argumentscollector.cpp @@ -1,32 +1,28 @@ -/************************************************************************** +/**************************************************************************** ** -** This file is part of Qt Creator +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ ** -** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** This file is part of Qt Creator. ** -** Contact: http://www.qt-project.org/ +** 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. ** -** 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 "argumentscollector.h" #include @@ -43,66 +39,71 @@ ArgumentsCollector::ArgumentsCollector(const QStringList &args) { } -QSsh::SshConnectionParameters ArgumentsCollector::collect(bool &success) const +SshConnectionParameters ArgumentsCollector::collect(bool &success) const { SshConnectionParameters parameters; + parameters.options &= ~SshIgnoreDefaultProxy; try { bool authTypeGiven = false; bool portGiven = false; bool timeoutGiven = false; bool proxySettingGiven = false; int pos; - int port; + int port = 22; for (pos = 1; pos < m_arguments.count() - 1; ++pos) { - if (checkAndSetStringArg(pos, parameters.host, "-h") - || checkAndSetStringArg(pos, parameters.userName, "-u")) + QString host; + QString user; + if (checkAndSetStringArg(pos, host, "-h") || checkAndSetStringArg(pos, user, "-u")) { + parameters.setHost(host); + parameters.setUserName(user); continue; + } if (checkAndSetIntArg(pos, port, portGiven, "-p") || checkAndSetIntArg(pos, parameters.timeout, timeoutGiven, "-t")) continue; - if (checkAndSetStringArg(pos, parameters.password, "-pwd")) { + QString pass; + if (checkAndSetStringArg(pos, pass, "-pwd")) { + parameters.setPassword(pass); if (!parameters.privateKeyFile.isEmpty()) throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive.")); parameters.authenticationType - = SshConnectionParameters::AuthenticationByPassword; + = SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods; authTypeGiven = true; continue; } if (checkAndSetStringArg(pos, parameters.privateKeyFile, "-k")) { - if (!parameters.password.isEmpty()) + if (!parameters.password().isEmpty()) throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive.")); parameters.authenticationType - = SshConnectionParameters::AuthenticationByKey; + = SshConnectionParameters::AuthenticationTypePublicKey; authTypeGiven = true; continue; } - if (!checkForNoProxy(pos, parameters.proxyType, proxySettingGiven)) + if (!checkForNoProxy(pos, parameters.options, proxySettingGiven)) throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos)); } Q_ASSERT(pos <= m_arguments.count()); if (pos == m_arguments.count() - 1) { - if (!checkForNoProxy(pos, parameters.proxyType, proxySettingGiven)) + if (!checkForNoProxy(pos, parameters.options, proxySettingGiven)) throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos)); } if (!authTypeGiven) { - parameters.authenticationType = SshConnectionParameters::AuthenticationByKey; + parameters.authenticationType = SshConnectionParameters::AuthenticationTypePublicKey; parameters.privateKeyFile = QDir::homePath() + QLatin1String("/.ssh/id_rsa"); } - if (parameters.userName.isEmpty()) { - parameters.userName - = QProcessEnvironment::systemEnvironment().value(QLatin1String("USER")); - } - if (parameters.userName.isEmpty()) + if (parameters.userName().isEmpty()) + parameters.setUserName(QProcessEnvironment::systemEnvironment().value("USER")); + if (parameters.userName().isEmpty()) throw ArgumentErrorException(QLatin1String("No user name given.")); - if (parameters.host.isEmpty()) + if (parameters.host().isEmpty()) throw ArgumentErrorException(QLatin1String("No host given.")); - parameters.port = portGiven ? port : 22; + parameters.setPort(portGiven ? port : 22); if (!timeoutGiven) parameters.timeout = 30; success = true; @@ -126,7 +127,7 @@ bool ArgumentsCollector::checkAndSetStringArg(int &pos, QString &arg, const char { if (m_arguments.at(pos) == QLatin1String(opt)) { if (!arg.isEmpty()) { - throw ArgumentErrorException(QLatin1String("option ") + opt + throw ArgumentErrorException(QLatin1String("option ") + QLatin1String(opt) + QLatin1String(" was given twice.")); } arg = m_arguments.at(++pos); @@ -142,13 +143,13 @@ bool ArgumentsCollector::checkAndSetIntArg(int &pos, int &val, { if (m_arguments.at(pos) == QLatin1String(opt)) { if (alreadyGiven) { - throw ArgumentErrorException(QLatin1String("option ") + opt + throw ArgumentErrorException(QLatin1String("option ") + QLatin1String(opt) + QLatin1String(" was given twice.")); } bool isNumber; val = m_arguments.at(++pos).toInt(&isNumber); if (!isNumber) { - throw ArgumentErrorException(QLatin1String("option ") + opt + throw ArgumentErrorException(QLatin1String("option ") + QLatin1String(opt) + QLatin1String(" needs integer argument")); } alreadyGiven = true; @@ -157,13 +158,13 @@ bool ArgumentsCollector::checkAndSetIntArg(int &pos, int &val, return false; } -bool ArgumentsCollector::checkForNoProxy(int &pos, - SshConnectionParameters::ProxyType &type, bool &alreadyGiven) const +bool ArgumentsCollector::checkForNoProxy(int &pos, SshConnectionOptions &options, + bool &alreadyGiven) const { if (m_arguments.at(pos) == QLatin1String("-no-proxy")) { if (alreadyGiven) throw ArgumentErrorException(QLatin1String("proxy setting given twice.")); - type = SshConnectionParameters::NoProxy; + options |= SshIgnoreDefaultProxy; alreadyGiven = true; return true; } diff --git a/tests/manual/ssh/shell/argumentscollector.h b/tests/manual/ssh/shell/argumentscollector.h new file mode 100644 index 0000000..5f195cf --- /dev/null +++ b/tests/manual/ssh/shell/argumentscollector.h @@ -0,0 +1,51 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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. +** +****************************************************************************/ + +#pragma once + +#include + +#include + +class ArgumentsCollector +{ +public: + ArgumentsCollector(const QStringList &args); + QSsh::SshConnectionParameters collect(bool &success) const; +private: + struct ArgumentErrorException + { + ArgumentErrorException(const QString &error) : error(error) {} + const QString error; + }; + + void printUsage() const; + bool checkAndSetStringArg(int &pos, QString &arg, const char *opt) const; + bool checkAndSetIntArg(int &pos, int &val, bool &alreadyGiven, + const char *opt) const; + bool checkForNoProxy(int &pos, QSsh::SshConnectionOptions &options, bool &alreadyGiven) const; + + const QStringList m_arguments; +}; diff --git a/tests/manual/ssh/shell/main.cpp b/tests/manual/ssh/shell/main.cpp index 7540881..8890342 100644 --- a/tests/manual/ssh/shell/main.cpp +++ b/tests/manual/ssh/shell/main.cpp @@ -1,33 +1,29 @@ -/************************************************************************** -** -** 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 "../remoteprocess/argumentscollector.h" +/**************************************************************************** +** +** Copyright (C) 2016 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 "argumentscollector.h" #include "shell.h" #include diff --git a/tests/manual/ssh/shell/shell.cpp b/tests/manual/ssh/shell/shell.cpp index b187582..0bc3021 100644 --- a/tests/manual/ssh/shell/shell.cpp +++ b/tests/manual/ssh/shell/shell.cpp @@ -1,32 +1,28 @@ -/************************************************************************** +/**************************************************************************** ** -** This file is part of Qt Creator +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ ** -** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** This file is part of Qt Creator. ** -** Contact: http://www.qt-project.org/ +** 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. ** -** 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 "shell.h" #include @@ -41,14 +37,14 @@ using namespace QSsh; -Shell::Shell(const QSsh::SshConnectionParameters ¶meters, QObject *parent) +Shell::Shell(const SshConnectionParameters ¶meters, QObject *parent) : QObject(parent), m_connection(new SshConnection(parameters)), m_stdin(new QFile(this)) { - connect(m_connection, SIGNAL(connected()), SLOT(handleConnected())); - connect(m_connection, SIGNAL(dataAvailable(QString)), SLOT(handleShellMessage(QString))); - connect(m_connection, SIGNAL(error(QSsh::SshError)), SLOT(handleConnectionError())); + connect(m_connection, &SshConnection::connected, this, &Shell::handleConnected); + connect(m_connection, &SshConnection::dataAvailable, this, &Shell::handleShellMessage); + connect(m_connection, &SshConnection::error, this, &Shell::handleConnectionError); } Shell::~Shell() @@ -60,7 +56,7 @@ void Shell::run() { if (!m_stdin->open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered)) { std::cerr << "Error: Cannot read from standard input." << std::endl; - qApp->exit(EXIT_FAILURE); + QCoreApplication::exit(EXIT_FAILURE); return; } @@ -70,7 +66,7 @@ void Shell::run() void Shell::handleConnectionError() { std::cerr << "SSH connection error: " << qPrintable(m_connection->errorString()) << std::endl; - qApp->exit(EXIT_FAILURE); + QCoreApplication::exit(EXIT_FAILURE); } void Shell::handleShellMessage(const QString &message) @@ -81,17 +77,19 @@ void Shell::handleShellMessage(const QString &message) void Shell::handleConnected() { m_shell = m_connection->createRemoteShell(); - connect(m_shell.data(), SIGNAL(started()), SLOT(handleShellStarted())); - connect(m_shell.data(), SIGNAL(readyReadStandardOutput()), SLOT(handleRemoteStdout())); - connect(m_shell.data(), SIGNAL(readyReadStandardError()), SLOT(handleRemoteStderr())); - connect(m_shell.data(), SIGNAL(closed(int)), SLOT(handleChannelClosed(int))); + connect(m_shell.data(), &SshRemoteProcess::started, this, &Shell::handleShellStarted); + connect(m_shell.data(), &SshRemoteProcess::readyReadStandardOutput, + this, &Shell::handleRemoteStdout); + connect(m_shell.data(), &SshRemoteProcess::readyReadStandardError, + this, &Shell::handleRemoteStderr); + connect(m_shell.data(), &SshRemoteProcess::closed, this, &Shell::handleChannelClosed); m_shell->start(); } void Shell::handleShellStarted() { QSocketNotifier * const notifier = new QSocketNotifier(0, QSocketNotifier::Read, this); - connect(notifier, SIGNAL(activated(int)), SLOT(handleStdin())); + connect(notifier, &QSocketNotifier::activated, this, &Shell::handleStdin); } void Shell::handleRemoteStdout() @@ -108,7 +106,7 @@ void Shell::handleChannelClosed(int exitStatus) { std::cerr << "Shell closed. Exit status was " << exitStatus << ", exit code was " << m_shell->exitCode() << "." << std::endl; - qApp->exit(exitStatus == SshRemoteProcess::NormalExit && m_shell->exitCode() == 0 + QCoreApplication::exit(exitStatus == SshRemoteProcess::NormalExit && m_shell->exitCode() == 0 ? EXIT_SUCCESS : EXIT_FAILURE); } diff --git a/tests/manual/ssh/shell/shell.h b/tests/manual/ssh/shell/shell.h index e7a8ad5..ed5f4d8 100644 --- a/tests/manual/ssh/shell/shell.h +++ b/tests/manual/ssh/shell/shell.h @@ -1,34 +1,29 @@ -/************************************************************************** +/**************************************************************************** ** -** This file is part of Qt Creator +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ ** -** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** This file is part of Qt Creator. ** -** Contact: http://www.qt-project.org/ +** 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. ** -** 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. -** -** -**************************************************************************/ -#ifndef SHELL_H -#define SHELL_H +****************************************************************************/ + +#pragma once #include #include @@ -54,7 +49,7 @@ class Shell : public QObject void run(); -private slots: +private: void handleConnected(); void handleConnectionError(); void handleRemoteStdout(); @@ -64,10 +59,7 @@ private slots: void handleShellStarted(); void handleStdin(); -private: QSsh::SshConnection *m_connection; QSharedPointer m_shell; QFile * const m_stdin; }; - -#endif // SHELL_H diff --git a/tests/manual/ssh/shell/shell.pro b/tests/manual/ssh/shell/shell.pro index ed3b0d9..f7d0b17 100644 --- a/tests/manual/ssh/shell/shell.pro +++ b/tests/manual/ssh/shell/shell.pro @@ -2,5 +2,5 @@ include(../ssh.pri) QT += network TARGET=shell -SOURCES=main.cpp shell.cpp ../remoteprocess/argumentscollector.cpp -HEADERS=shell.h ../remoteprocess/argumentscollector.h +SOURCES=main.cpp shell.cpp argumentscollector.cpp +HEADERS=shell.h argumentscollector.h diff --git a/tests/manual/ssh/ssh.pro b/tests/manual/ssh/ssh.pro index 6284229..1801bd1 100644 --- a/tests/manual/ssh/ssh.pro +++ b/tests/manual/ssh/ssh.pro @@ -5,4 +5,4 @@ #------------------------------------------------- TEMPLATE = subdirs -SUBDIRS = errorhandling sftp remoteprocess shell sftpfsmodel +SUBDIRS = shell sftpfsmodel diff --git a/tests/tests.pro b/tests/tests.pro index 036ccc3..54b65dc 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,2 +1,2 @@ TEMPLATE=subdirs -SUBDIRS += manual +SUBDIRS += manual auto