diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2017-06-14 16:31:33 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2018-11-19 09:12:31 +0000 |
commit | 424923817cd018767671e0e88b90deef3fd4917a (patch) | |
tree | 12f060fee0a92018b318c881636b2722bad48857 | |
parent | 3d1d9aae2e977ef599582074e0f4faaef454a595 (diff) |
SSH: Implement X11 forwarding
Change-Id: Ia7b15e784cb098bc7c6c6be2748d772192187e97
Reviewed-by: hjk <hjk@qt.io>
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
24 files changed, 811 insertions, 17 deletions
diff --git a/src/libs/ssh/ssh.pro b/src/libs/ssh/ssh.pro index 40efcaf96b..27407b7f2e 100644 --- a/src/libs/ssh/ssh.pro +++ b/src/libs/ssh/ssh.pro @@ -33,7 +33,9 @@ SOURCES = $$PWD/sshsendfacility.cpp \ $$PWD/sshtcpipforwardserver.cpp \ $$PWD/sshtcpiptunnel.cpp \ $$PWD/sshforwardedtcpiptunnel.cpp \ - $$PWD/sshagent.cpp + $$PWD/sshagent.cpp \ + $$PWD/sshx11channel.cpp \ + $$PWD/sshx11inforetriever.cpp HEADERS = $$PWD/sshsendfacility_p.h \ $$PWD/sshremoteprocess.h \ @@ -76,7 +78,10 @@ HEADERS = $$PWD/sshsendfacility_p.h \ $$PWD/sshtcpiptunnel_p.h \ $$PWD/sshforwardedtcpiptunnel.h \ $$PWD/sshforwardedtcpiptunnel_p.h \ - $$PWD/sshagent_p.h + $$PWD/sshagent_p.h \ + $$PWD/sshx11channel_p.h \ + $$PWD/sshx11displayinfo_p.h \ + $$PWD/sshx11inforetriever_p.h FORMS = $$PWD/sshkeycreationdialog.ui diff --git a/src/libs/ssh/ssh.qbs b/src/libs/ssh/ssh.qbs index c93e5d3a1c..f433fdea56 100644 --- a/src/libs/ssh/ssh.qbs +++ b/src/libs/ssh/ssh.qbs @@ -95,6 +95,11 @@ Project { "sshtcpipforwardserver_p.h", "sshtcpiptunnel.cpp", "sshtcpiptunnel_p.h", + "sshx11channel.cpp", + "sshx11channel_p.h", + "sshx11displayinfo_p.h", + "sshx11inforetriever.cpp", + "sshx11inforetriever_p.h", ] property var botanIncludes: qtc.useSystemBotan ? ["/usr/include/botan-2"] : [] diff --git a/src/libs/ssh/ssh_global.h b/src/libs/ssh/ssh_global.h index 35fed0fc0d..3941ffbfcd 100644 --- a/src/libs/ssh/ssh_global.h +++ b/src/libs/ssh/ssh_global.h @@ -33,6 +33,12 @@ # define QSSH_EXPORT Q_DECL_IMPORT #endif +#ifdef WITH_TESTS +# define QSSH_AUTOTEST_EXPORT QSSH_EXPORT +#else +# define QSSH_AUTOTEST_EXPORT +#endif + #define QSSH_PRINT_WARNING qWarning("Soft assert at %s:%d", __FILE__, __LINE__) #define QSSH_ASSERT(cond) do { if (!(cond)) { QSSH_PRINT_WARNING; } } while (false) #define QSSH_ASSERT_AND_RETURN(cond) do { if (!(cond)) { QSSH_PRINT_WARNING; return; } } while (false) diff --git a/src/libs/ssh/sshchannelmanager.cpp b/src/libs/ssh/sshchannelmanager.cpp index 78672d2549..f4bfb8cf12 100644 --- a/src/libs/ssh/sshchannelmanager.cpp +++ b/src/libs/ssh/sshchannelmanager.cpp @@ -38,6 +38,8 @@ #include "sshsendfacility_p.h" #include "sshtcpipforwardserver.h" #include "sshtcpipforwardserver_p.h" +#include "sshx11channel_p.h" +#include "sshx11inforetriever_p.h" #include <QList> @@ -63,6 +65,10 @@ void SshChannelManager::handleChannelOpen(const SshIncomingPacket &packet) handleChannelOpenForwardedTcpIp(channelOpen); return; } + if (channelOpen.channelType == "x11") { + handleChannelOpenX11(channelOpen); + return; + } try { m_sendFacility.sendChannelOpenFailurePacket(channelOpen.commonData.remoteChannel, SSH_OPEN_UNKNOWN_CHANNEL_TYPE, QByteArray()); @@ -195,6 +201,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<SshRemoteProcessPrivate *>(sender())); + }); + connect(proc->d, &SshRemoteProcessPrivate::x11ForwardingRequested, this, + [this, proc = proc->d](const QString &displayName) { + if (!x11DisplayName().isEmpty()) { + if (x11DisplayName() != displayName) { + proc->failToStart(tr("Cannot forward to display %1 on SSH connection that is " + "already forwarding to display %2.") + .arg(displayName, x11DisplayName())); + 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; } @@ -303,6 +346,25 @@ void SshChannelManager::handleChannelOpenForwardedTcpIp( 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<QObject>()); +} + int SshChannelManager::closeAllChannels(CloseAllMode mode) { int count = 0; diff --git a/src/libs/ssh/sshchannelmanager_p.h b/src/libs/ssh/sshchannelmanager_p.h index 33f7af9ef0..10e31e7d9e 100644 --- a/src/libs/ssh/sshchannelmanager_p.h +++ b/src/libs/ssh/sshchannelmanager_p.h @@ -25,6 +25,8 @@ #pragma once +#include "sshx11displayinfo_p.h" + #include <QHash> #include <QObject> #include <QSharedPointer> @@ -41,6 +43,7 @@ class AbstractSshChannel; struct SshChannelOpenGeneric; class SshIncomingPacket; class SshSendFacility; +class SshRemoteProcessPrivate; class SshChannelManager : public QObject { @@ -59,6 +62,7 @@ public: 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); @@ -89,12 +93,16 @@ private: const QSharedPointer<QObject> &pub); void handleChannelOpenForwardedTcpIp(const SshChannelOpenGeneric &channelOpenGeneric); + void handleChannelOpenX11(const SshChannelOpenGeneric &channelOpenGeneric); + SshSendFacility &m_sendFacility; QHash<quint32, AbstractSshChannel *> m_channels; QHash<AbstractSshChannel *, QSharedPointer<QObject> > m_sessions; quint32 m_nextLocalChannelId; QList<QSharedPointer<SshTcpIpForwardServer>> m_waitingForwardServers; QList<QSharedPointer<SshTcpIpForwardServer>> m_listeningForwardServers; + QList<SshRemoteProcessPrivate *> m_x11ForwardingRequests; + X11DisplayInfo m_x11DisplayInfo; }; } // namespace Internal diff --git a/src/libs/ssh/sshconnection.cpp b/src/libs/ssh/sshconnection.cpp index 7981a65cb9..8495da64c4 100644 --- a/src/libs/ssh/sshconnection.cpp +++ b/src/libs/ssh/sshconnection.cpp @@ -207,6 +207,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, diff --git a/src/libs/ssh/sshconnection.h b/src/libs/ssh/sshconnection.h index 78f7692c61..ca1e3a461a 100644 --- a/src/libs/ssh/sshconnection.h +++ b/src/libs/ssh/sshconnection.h @@ -142,6 +142,8 @@ public: int channelCount() const; + QString x11DisplayName() const; + signals: void connected(); void disconnected(); diff --git a/src/libs/ssh/sshincomingpacket.cpp b/src/libs/ssh/sshincomingpacket.cpp index c97e18c857..aa3beedf6b 100644 --- a/src/libs/ssh/sshincomingpacket.cpp +++ b/src/libs/ssh/sshincomingpacket.cpp @@ -406,6 +406,23 @@ SshChannelOpenForwardedTcpIp SshIncomingPacket::extractChannelOpenForwardedTcpIp } } +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()); diff --git a/src/libs/ssh/sshincomingpacket_p.h b/src/libs/ssh/sshincomingpacket_p.h index 6b3a54abf3..d495f9ca95 100644 --- a/src/libs/ssh/sshincomingpacket_p.h +++ b/src/libs/ssh/sshincomingpacket_p.h @@ -131,6 +131,13 @@ struct SshChannelOpenForwardedTcpIp quint32 originatorPort; }; +struct SshChannelOpenX11 +{ + SshChannelOpenCommon common; + QByteArray originatorAddress; + quint32 originatorPort; +}; + struct SshChannelOpenFailure { quint32 localChannel; @@ -204,6 +211,7 @@ public: SshChannelOpenGeneric extractChannelOpen() const; static SshChannelOpenForwardedTcpIp extractChannelOpenForwardedTcpIp( const SshChannelOpenGeneric &genericData); + static SshChannelOpenX11 extractChannelOpenX11(const SshChannelOpenGeneric &genericData); SshChannelOpenFailure extractChannelOpenFailure() const; SshChannelOpenConfirmation extractChannelOpenConfirmation() const; SshChannelWindowAdjust extractWindowAdjust() const; diff --git a/src/libs/ssh/sshoutgoingpacket.cpp b/src/libs/ssh/sshoutgoingpacket.cpp index ae505700e0..ac25e0c980 100644 --- a/src/libs/ssh/sshoutgoingpacket.cpp +++ b/src/libs/ssh/sshoutgoingpacket.cpp @@ -225,6 +225,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) { diff --git a/src/libs/ssh/sshoutgoingpacket_p.h b/src/libs/ssh/sshoutgoingpacket_p.h index 8e3da5aef6..b9392279aa 100644 --- a/src/libs/ssh/sshoutgoingpacket_p.h +++ b/src/libs/ssh/sshoutgoingpacket_p.h @@ -71,6 +71,8 @@ public: 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); diff --git a/src/libs/ssh/sshremoteprocess.cpp b/src/libs/ssh/sshremoteprocess.cpp index 3b81e0409c..f4c43bdce2 100644 --- a/src/libs/ssh/sshremoteprocess.cpp +++ b/src/libs/ssh/sshremoteprocess.cpp @@ -30,6 +30,7 @@ #include "sshincomingpacket_p.h" #include "sshlogging_p.h" #include "sshsendfacility_p.h" +#include "sshx11displayinfo_p.h" #include <QTimer> @@ -186,6 +187,12 @@ 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) { @@ -227,6 +234,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), @@ -286,26 +301,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() diff --git a/src/libs/ssh/sshremoteprocess.h b/src/libs/ssh/sshremoteprocess.h index cd54a03ede..85f8018227 100644 --- a/src/libs/ssh/sshremoteprocess.h +++ b/src/libs/ssh/sshremoteprocess.h @@ -78,6 +78,7 @@ public: 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 edf0648329..459da8c593 100644 --- a/src/libs/ssh/sshremoteprocess_p.h +++ b/src/libs/ssh/sshremoteprocess_p.h @@ -38,6 +38,7 @@ class SshRemoteProcess; namespace Internal { class SshSendFacility; +class X11DisplayInfo; class SshRemoteProcessPrivate : public AbstractSshChannel { @@ -48,12 +49,16 @@ public: NotYetStarted, ExecRequested, StartFailed, Running, Exited }; + void failToStart(const QString &reason); + void startProcess(const X11DisplayInfo &displayInfo); + signals: void started(); void readyRead(); void readyReadStandardOutput(); void readyReadStandardError(); void closed(int exitStatus); + void x11ForwardingRequested(const QString &display); private: SshRemoteProcessPrivate(const QByteArray &command, quint32 channelId, @@ -93,6 +98,8 @@ private: bool m_useTerminal; SshPseudoTerminal m_terminal; + QString m_x11DisplayName; + QByteArray m_stdout; QByteArray m_stderr; diff --git a/src/libs/ssh/sshsendfacility.cpp b/src/libs/ssh/sshsendfacility.cpp index 9552d3e020..f92a1220fb 100644 --- a/src/libs/ssh/sshsendfacility.cpp +++ b/src/libs/ssh/sshsendfacility.cpp @@ -205,6 +205,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) { diff --git a/src/libs/ssh/sshsendfacility_p.h b/src/libs/ssh/sshsendfacility_p.h index f54a2a9463..b3e0e44e1f 100644 --- a/src/libs/ssh/sshsendfacility_p.h +++ b/src/libs/ssh/sshsendfacility_p.h @@ -82,6 +82,8 @@ public: 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); diff --git a/src/libs/ssh/sshx11channel.cpp b/src/libs/ssh/sshx11channel.cpp new file mode 100644 index 0000000000..d531142ceb --- /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 <QFileInfo> +#include <QLocalSocket> +#include <QTcpSocket> + +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<void(QTcpSocket::*)(QTcpSocket::SocketError)>(&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<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&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<QLocalSocket *>(m_socket); } + QTcpSocket *tcpSocket() const { return qobject_cast<QTcpSocket *>(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 <sshx11channel.moc> diff --git a/src/libs/ssh/sshx11channel_p.h b/src/libs/ssh/sshx11channel_p.h new file mode 100644 index 0000000000..918129172e --- /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 <QByteArray> + +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 0000000000..48ba05218a --- /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 <QString> +#include <QByteArray> + +namespace QSsh { +namespace Internal { + +class QSSH_AUTOTEST_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 0000000000..f329a6b8cd --- /dev/null +++ b/src/libs/ssh/sshx11inforetriever.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** 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 <QByteArrayList> +#include <QProcess> +#include <QTemporaryFile> + +#include <botan/auto_rng.h> + +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<void (QProcess::*)(int,QProcess::ExitStatus)>(&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<Botan::uint8_t *>(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) +{ + 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 0000000000..fc27672f4f --- /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 <QObject> +#include <QString> + +QT_BEGIN_NAMESPACE +class QByteArray; +class QProcess; +class QTemporaryFile; +QT_END_NAMESPACE + +namespace QSsh { +namespace Internal { +class X11DisplayInfo; + +class QSSH_AUTOTEST_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/tests/auto/ssh/ssh.pro b/tests/auto/ssh/ssh.pro index 31469ad3ad..8ba850177d 100644 --- a/tests/auto/ssh/ssh.pro +++ b/tests/auto/ssh/ssh.pro @@ -1,5 +1,5 @@ QT = core network -QTC_LIB_DEPENDS += ssh +QTC_LIB_DEPENDS += ssh utils include(../qttest.pri) SOURCES += tst_ssh.cpp diff --git a/tests/auto/ssh/ssh.qbs b/tests/auto/ssh/ssh.qbs index b580f05433..c2b7072b55 100644 --- a/tests/auto/ssh/ssh.qbs +++ b/tests/auto/ssh/ssh.qbs @@ -3,5 +3,6 @@ import qbs QtcAutotest { name: "SSH autotest" Depends { name: "QtcSsh" } + Depends { name: "Utils" } files: "tst_ssh.cpp" } diff --git a/tests/auto/ssh/tst_ssh.cpp b/tests/auto/ssh/tst_ssh.cpp index b15e411066..93b8a85c13 100644 --- a/tests/auto/ssh/tst_ssh.cpp +++ b/tests/auto/ssh/tst_ssh.cpp @@ -30,6 +30,9 @@ #include <ssh/sshpseudoterminal.h> #include <ssh/sshremoteprocessrunner.h> #include <ssh/sshtcpipforwardserver.h> +#include <ssh/sshx11displayinfo_p.h> +#include <ssh/sshx11inforetriever_p.h> +#include <utils/environment.h> #include <QDateTime> #include <QDir> @@ -152,6 +155,8 @@ private slots: void remoteProcessChannels(); void remoteProcessInput(); void sftp(); + void x11InfoRetriever_data(); + void x11InfoRetriever(); private: bool waitForConnection(SshConnection &connection); @@ -826,6 +831,62 @@ void tst_Ssh::sftp() QCOMPARE(sftpChannel->state(), SftpChannel::Closed); } +void tst_Ssh::x11InfoRetriever_data() +{ + QTest::addColumn<QString>("displayName"); + QTest::addColumn<bool>("successExpected"); + + const Utils::FileName xauthCommand + = Utils::Environment::systemEnvironment().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; + SshX11InfoRetriever x11InfoRetriever(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; |