SSH: Implement X11 forwarding

Change-Id: Ia7b15e784cb098bc7c6c6be2748d772192187e97
Reviewed-by: hjk <hjk@qt.io>
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
Christian Kandeler
2017-06-14 16:31:33 +02:00
parent 3d1d9aae2e
commit 424923817c
24 changed files with 811 additions and 17 deletions

View File

@@ -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

View File

@@ -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"] : []

View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -142,6 +142,8 @@ public:
int channelCount() const;
QString x11DisplayName() const;
signals:
void connected();
void disconnected();

View File

@@ -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());

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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);
}
if (m_useTerminal)
m_sendFacility.sendPtyRequestPacket(remoteChannel(), m_terminal);
void SshRemoteProcessPrivate::startProcess(const X11DisplayInfo &displayInfo)
{
if (m_procState != NotYetStarted)
return;
if (m_isShell)
m_sendFacility.sendShellPacket(remoteChannel());
else
m_sendFacility.sendExecPacket(remoteChannel(), m_command);
setProcState(ExecRequested);
m_timeoutTimer.start(ReplyTimeout);
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_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()

View File

@@ -78,6 +78,7 @@ public:
void clearEnvironment();
void requestTerminal(const SshPseudoTerminal &terminal);
void requestX11Forwarding(const QString &displayName);
void start();
bool isRunning() const;

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,5 @@
QT = core network
QTC_LIB_DEPENDS += ssh
QTC_LIB_DEPENDS += ssh utils
include(../qttest.pri)
SOURCES += tst_ssh.cpp

View File

@@ -3,5 +3,6 @@ import qbs
QtcAutotest {
name: "SSH autotest"
Depends { name: "QtcSsh" }
Depends { name: "Utils" }
files: "tst_ssh.cpp"
}

View File

@@ -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;