summaryrefslogtreecommitdiffstats
path: root/coreplugin/ssh/sshconnection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'coreplugin/ssh/sshconnection.cpp')
-rw-r--r--coreplugin/ssh/sshconnection.cpp561
1 files changed, 561 insertions, 0 deletions
diff --git a/coreplugin/ssh/sshconnection.cpp b/coreplugin/ssh/sshconnection.cpp
new file mode 100644
index 0000000..fbf63d7
--- /dev/null
+++ b/coreplugin/ssh/sshconnection.cpp
@@ -0,0 +1,561 @@
+/**************************************************************************
+**
+** This file is part of Qt Creator
+**
+** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
+**
+** Contact: Nokia Corporation (qt-info@nokia.com)
+**
+** Commercial Usage
+**
+** Licensees holding valid Qt Commercial licenses may use this file in
+** accordance with the Qt Commercial License Agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Nokia.
+**
+** 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.
+**
+** If you are unsure which license is appropriate for your use, please
+** contact the sales department at http://qt.nokia.com/contact.
+**
+**************************************************************************/
+
+#include "sshconnection.h"
+#include "sshconnection_p.h"
+
+#include "sftpchannel.h"
+#include "sshcapabilities_p.h"
+#include "sshchannelmanager_p.h"
+#include "sshcryptofacility_p.h"
+#include "sshexception_p.h"
+#include "sshkeyexchange_p.h"
+
+#include <botan/exceptn.h>
+#include <botan/init.h>
+
+#include <QtCore/QFile>
+#include <QtCore/QMutex>
+#include <QtNetwork/QTcpSocket>
+
+namespace Core {
+
+namespace {
+ const QByteArray ClientId("SSH-2.0-QtCreator\r\n");
+
+ bool staticInitializationsDone = false;
+ QMutex staticInitMutex;
+
+ void doStaticInitializationsIfNecessary()
+ {
+ if (!staticInitializationsDone) {
+ staticInitMutex.lock();
+ if (!staticInitializationsDone) {
+ Botan::LibraryInitializer::initialize("thread_safe=true");
+ qRegisterMetaType<SshError>("SshError");
+ staticInitializationsDone = true;
+ }
+ staticInitMutex.unlock();
+ }
+ }
+}
+
+// TODO: Mechanism for checking the host key. First connection to host: save, later: compare
+
+SshConnection::Ptr SshConnection::create()
+{
+ doStaticInitializationsIfNecessary();
+ return Ptr(new SshConnection);
+}
+
+SshConnection::SshConnection() : d(new Internal::SshConnectionPrivate(this))
+{
+ connect(d, SIGNAL(connected()), this, SIGNAL(connected()),
+ Qt::QueuedConnection);
+ connect(d, SIGNAL(dataAvailable(QString)), this,
+ SIGNAL(dataAvailable(QString)), Qt::QueuedConnection);
+ connect(d, SIGNAL(disconnected()), this, SIGNAL(disconnected()),
+ Qt::QueuedConnection);
+ connect(d, SIGNAL(error(SshError)), this, SIGNAL(error(SshError)),
+ Qt::QueuedConnection);
+}
+
+void SshConnection::connectToHost(const SshConnectionParameters &serverInfo)
+{
+ d->connectToHost(serverInfo);
+}
+
+void SshConnection::disconnectFromHost()
+{
+ d->closeConnection(Internal::SSH_DISCONNECT_BY_APPLICATION, SshNoError, "",
+ QString());
+}
+
+SshConnection::State SshConnection::state() const
+{
+ switch (d->state()) {
+ case Internal::SocketUnconnected:
+ return Unconnected;
+ case Internal::ConnectionEstablished:
+ return Connected;
+ default:
+ return Connecting;
+ }
+}
+
+SshError SshConnection::errorState() const
+{
+ return d->error();
+}
+
+QString SshConnection::errorString() const
+{
+ return d->errorString();
+}
+
+SshConnectionParameters SshConnection::connectionParameters() const
+{
+ return d->m_connParams;
+}
+
+SshConnection::~SshConnection()
+{
+ disconnect();
+ disconnectFromHost();
+ delete d;
+}
+
+QSharedPointer<SshRemoteProcess> SshConnection::createRemoteProcess(const QByteArray &command)
+{
+ return state() == Connected
+ ? d->createRemoteProcess(command) : QSharedPointer<SshRemoteProcess>();
+}
+
+QSharedPointer<SftpChannel> SshConnection::createSftpChannel()
+{
+ return state() == Connected
+ ? d->createSftpChannel() : QSharedPointer<SftpChannel>();
+}
+
+
+namespace Internal {
+
+SshConnectionPrivate::SshConnectionPrivate(SshConnection *conn)
+ : m_socket(new QTcpSocket(this)), m_state(SocketUnconnected),
+ m_sendFacility(m_socket),
+ m_channelManager(new SshChannelManager(m_sendFacility)),
+ m_error(SshNoError), m_ignoreNextPacket(false), m_conn(conn)
+{
+ setupPacketHandlers();
+ connect(&m_timeoutTimer, SIGNAL(timeout()), this, SLOT(handleTimeout()));
+}
+
+SshConnectionPrivate::~SshConnectionPrivate()
+{
+ disconnect();
+}
+
+void SshConnectionPrivate::setupPacketHandlers()
+{
+ typedef SshConnectionPrivate This;
+
+ setupPacketHandler(SSH_MSG_KEXINIT, StateList() << SocketConnected,
+ &This::handleKeyExchangeInitPacket);
+ setupPacketHandler(SSH_MSG_KEXDH_REPLY, StateList() << KeyExchangeStarted,
+ &This::handleKeyExchangeReplyPacket);
+
+ setupPacketHandler(SSH_MSG_NEWKEYS, StateList() << KeyExchangeSuccess,
+ &This::handleNewKeysPacket);
+ setupPacketHandler(SSH_MSG_SERVICE_ACCEPT,
+ StateList() << UserAuthServiceRequested,
+ &This::handleServiceAcceptPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_PASSWD_CHANGEREQ,
+ StateList() << UserAuthRequested, &This::handlePasswordExpiredPacket);
+ setupPacketHandler(SSH_MSG_GLOBAL_REQUEST,
+ StateList() << ConnectionEstablished, &This::handleGlobalRequest);
+
+ const StateList authReqList = StateList() << UserAuthRequested;
+ setupPacketHandler(SSH_MSG_USERAUTH_BANNER, authReqList,
+ &This::handleUserAuthBannerPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_SUCCESS, authReqList,
+ &This::handleUserAuthSuccessPacket);
+ setupPacketHandler(SSH_MSG_USERAUTH_FAILURE, authReqList,
+ &This::handleUserAuthFailurePacket);
+
+ const StateList connectedList
+ = StateList() << ConnectionEstablished;
+ setupPacketHandler(SSH_MSG_CHANNEL_REQUEST, connectedList,
+ &This::handleChannelRequest);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN, connectedList,
+ &This::handleChannelOpen);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN_FAILURE, connectedList,
+ &This::handleChannelOpenFailure);
+ setupPacketHandler(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, connectedList,
+ &This::handleChannelOpenConfirmation);
+ setupPacketHandler(SSH_MSG_CHANNEL_SUCCESS, connectedList,
+ &This::handleChannelSuccess);
+ setupPacketHandler(SSH_MSG_CHANNEL_FAILURE, connectedList,
+ &This::handleChannelFailure);
+ setupPacketHandler(SSH_MSG_CHANNEL_WINDOW_ADJUST, connectedList,
+ &This::handleChannelWindowAdjust);
+ setupPacketHandler(SSH_MSG_CHANNEL_DATA, connectedList,
+ &This::handleChannelData);
+ setupPacketHandler(SSH_MSG_CHANNEL_EXTENDED_DATA, connectedList,
+ &This::handleChannelExtendedData);
+
+ const StateList connectedOrClosedList
+ = StateList() << SocketUnconnected << ConnectionEstablished;
+ setupPacketHandler(SSH_MSG_CHANNEL_EOF, connectedOrClosedList,
+ &This::handleChannelEof);
+ setupPacketHandler(SSH_MSG_CHANNEL_CLOSE, connectedOrClosedList,
+ &This::handleChannelClose);
+
+ setupPacketHandler(SSH_MSG_DISCONNECT, StateList() << SocketConnected
+ << KeyExchangeStarted << KeyExchangeSuccess
+ << UserAuthServiceRequested << UserAuthRequested
+ << ConnectionEstablished, &This::handleDisconnect);
+}
+
+void SshConnectionPrivate::setupPacketHandler(SshPacketType type,
+ const SshConnectionPrivate::StateList &states,
+ SshConnectionPrivate::PacketHandler handler)
+{
+ m_packetHandlers.insert(type, HandlerInStates(states, handler));
+}
+
+void SshConnectionPrivate::handleSocketConnected()
+{
+ m_state = SocketConnected;
+ sendData(ClientId);
+}
+
+void SshConnectionPrivate::handleIncomingData()
+{
+ if (m_state == SocketUnconnected)
+ return; // For stuff queued in the event loop after we've called closeConnection();
+
+ try {
+ m_incomingData += m_socket->readAll();
+#ifdef CREATOR_SSH_DEBUG
+ qDebug("state = %d, remote data size = %d", m_state,
+ m_incomingData.count());
+#endif
+ if (m_state == SocketConnected)
+ handleServerId();
+ handlePackets();
+ } catch (SshServerException &e) {
+ closeConnection(e.error, SshProtocolError, e.errorStringServer,
+ tr("SSH Protocol error: %1").arg(e.errorStringUser));
+ } catch (SshClientException &e) {
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, e.error, "",
+ e.errorString);
+ } catch (Botan::Exception &e) {
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshInternalError, "",
+ tr("Botan exception: %1").arg(e.what()));
+ }
+}
+
+void SshConnectionPrivate::handleServerId()
+{
+ const int idOffset = m_incomingData.indexOf("SSH-");
+ if (idOffset == -1)
+ return;
+ m_incomingData.remove(0, idOffset);
+ if (m_incomingData.size() < 7)
+ return;
+ const QByteArray &version = m_incomingData.mid(4, 3);
+ if (version != "2.0") {
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED,
+ "Invalid protocol version.",
+ tr("Invalid protocol version: Expected '2.0', got '%1'.")
+ .arg(SshPacketParser::asUserString(version)));
+ }
+ const int endOffset = m_incomingData.indexOf("\r\n");
+ if (endOffset == -1)
+ return;
+ if (m_incomingData.at(7) != '-') {
+ throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
+ "Invalid server id.", tr("Invalid server id '%1'.")
+ .arg(SshPacketParser::asUserString(m_incomingData)));
+ }
+
+ m_keyExchange.reset(new SshKeyExchange(m_sendFacility));
+ m_keyExchange->sendKexInitPacket(m_incomingData.left(endOffset));
+ m_incomingData.remove(0, endOffset + 2);
+}
+
+void SshConnectionPrivate::handlePackets()
+{
+ m_incomingPacket.consumeData(m_incomingData);
+ while (m_incomingPacket.isComplete()) {
+ handleCurrentPacket();
+ m_incomingPacket.clear();
+ m_incomingPacket.consumeData(m_incomingData);
+ }
+}
+
+void SshConnectionPrivate::handleCurrentPacket()
+{
+ Q_ASSERT(m_incomingPacket.isComplete());
+ Q_ASSERT(m_state == KeyExchangeStarted || !m_ignoreNextPacket);
+
+ if (m_ignoreNextPacket) {
+ m_ignoreNextPacket = false;
+ return;
+ }
+
+ QHash<SshPacketType, HandlerInStates>::ConstIterator it
+ = m_packetHandlers.find(m_incomingPacket.type());
+ if (it == m_packetHandlers.end()) {
+ 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()));
+ }
+ (this->*it.value().second)();
+}
+
+void SshConnectionPrivate::handleKeyExchangeInitPacket()
+{
+ // If the server sends a guessed packet, the guess must be wrong,
+ // because the algorithms we support requires us to initiate the
+ // key exchange.
+ if (m_keyExchange->sendDhInitPacket(m_incomingPacket))
+ m_ignoreNextPacket = true;
+ m_state = KeyExchangeStarted;
+}
+
+void SshConnectionPrivate::handleKeyExchangeReplyPacket()
+{
+ m_keyExchange->sendNewKeysPacket(m_incomingPacket,
+ ClientId.left(ClientId.size() - 2));
+ m_sendFacility.recreateKeys(*m_keyExchange);
+ m_state = KeyExchangeSuccess;
+}
+
+void SshConnectionPrivate::handleNewKeysPacket()
+{
+ m_incomingPacket.recreateKeys(*m_keyExchange);
+ m_keyExchange.reset();
+ m_sendFacility.sendUserAuthServiceRequestPacket();
+ m_state = UserAuthServiceRequested;
+}
+
+void SshConnectionPrivate::handleServiceAcceptPacket()
+{
+ if (m_connParams.authType == SshConnectionParameters::AuthByPwd) {
+ m_sendFacility.sendUserAuthByPwdRequestPacket(m_connParams.uname.toUtf8(),
+ SshCapabilities::SshConnectionService, m_connParams.pwd.toUtf8());
+ } else {
+ QFile privKeyFile(m_connParams.privateKeyFile);
+ bool couldOpen = privKeyFile.open(QIODevice::ReadOnly);
+ QByteArray contents;
+ if (couldOpen)
+ contents = privKeyFile.readAll();
+ if (!couldOpen || privKeyFile.error() != QFile::NoError) {
+ throw SshClientException(SshKeyFileError,
+ tr("Could not read private key file: %1")
+ .arg(privKeyFile.errorString()));
+ }
+
+ m_sendFacility.createAuthenticationKey(contents);
+ m_sendFacility.sendUserAuthByKeyRequestPacket(m_connParams.uname.toUtf8(),
+ SshCapabilities::SshConnectionService);
+ }
+ m_state = UserAuthRequested;
+}
+
+void SshConnectionPrivate::handlePasswordExpiredPacket()
+{
+ if (m_connParams.authType == SshConnectionParameters::AuthByKey) {
+ throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR,
+ "Got SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, but did not use password.");
+ }
+
+ throw SshClientException(SshAuthenticationError, tr("Password expired."));
+}
+
+void SshConnectionPrivate::handleUserAuthBannerPacket()
+{
+ emit dataAvailable(m_incomingPacket.extractUserAuthBanner().message);
+}
+
+void SshConnectionPrivate::handleGlobalRequest()
+{
+ m_sendFacility.sendRequestFailurePacket();
+}
+
+void SshConnectionPrivate::handleUserAuthSuccessPacket()
+{
+ m_state = ConnectionEstablished;
+ m_timeoutTimer.stop();
+ emit connected();
+}
+
+void SshConnectionPrivate::handleUserAuthFailurePacket()
+{
+ const QString errorMsg = m_connParams.authType == SshConnectionParameters::AuthByPwd
+ ? tr("Server rejected password.") : tr("Server rejected key.");
+ throw SshClientException(SshAuthenticationError, errorMsg);
+}
+void SshConnectionPrivate::handleDebugPacket()
+{
+ const SshDebug &msg = m_incomingPacket.extractDebug();
+ if (msg.display)
+ emit dataAvailable(msg.message);
+}
+
+void SshConnectionPrivate::handleChannelRequest()
+{
+ m_channelManager->handleChannelRequest(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelOpen()
+{
+ m_channelManager->handleChannelOpen(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelOpenFailure()
+{
+ m_channelManager->handleChannelOpenFailure(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelOpenConfirmation()
+{
+ m_channelManager->handleChannelOpenConfirmation(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelSuccess()
+{
+ m_channelManager->handleChannelSuccess(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelFailure()
+{
+ m_channelManager->handleChannelFailure(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelWindowAdjust()
+{
+ m_channelManager->handleChannelWindowAdjust(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelData()
+{
+ m_channelManager->handleChannelData(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelExtendedData()
+{
+ m_channelManager->handleChannelExtendedData(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelEof()
+{
+ m_channelManager->handleChannelEof(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleChannelClose()
+{
+ m_channelManager->handleChannelClose(m_incomingPacket);
+}
+
+void SshConnectionPrivate::handleDisconnect()
+{
+ const SshDisconnect msg = m_incomingPacket.extractDisconnect();
+ throw SshServerException(SSH_DISCONNECT_CONNECTION_LOST,
+ "", tr("Server closed connection: %1").arg(msg.description));
+}
+
+void SshConnectionPrivate::sendData(const QByteArray &data)
+{
+ m_socket->write(data);
+}
+
+void SshConnectionPrivate::handleSocketDisconnected()
+{
+ closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshClosedByServerError,
+ "Connection closed unexpectedly.",
+ tr("Connection closed unexpectedly."));
+}
+
+void SshConnectionPrivate::handleSocketError()
+{
+ if (m_error == SshNoError) {
+ closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshSocketError,
+ "Network error", m_socket->errorString());
+ }
+}
+
+void SshConnectionPrivate::handleTimeout()
+{
+ if (m_state != ConnectionEstablished)
+ closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshTimeoutError, "",
+ tr("Connection timed out."));
+}
+
+void SshConnectionPrivate::connectToHost(const SshConnectionParameters &serverInfo)
+{
+ m_incomingData.clear();
+ m_incomingPacket.reset();
+ m_sendFacility.reset();
+ m_error = SshNoError;
+ m_ignoreNextPacket = false;
+ m_errorString.clear();
+ 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()));
+ this->m_connParams = serverInfo;
+ m_state = SocketConnecting;
+ m_timeoutTimer.start(m_connParams.timeout * 1000);
+ m_socket->connectToHost(serverInfo.host, serverInfo.port);
+}
+
+void SshConnectionPrivate::closeConnection(SshErrorCode sshError,
+ SshError userError, const QByteArray &serverErrorString,
+ const QString &userErrorString)
+{
+ // Prevent endless loops by recursive exceptions.
+ if (m_state == SocketUnconnected || m_error != SshNoError)
+ return;
+
+ m_error = userError;
+ m_errorString = userErrorString;
+ m_timeoutTimer.stop();
+ disconnect(m_socket, 0, this, 0);
+ try {
+ m_channelManager->closeAllChannels();
+ m_sendFacility.sendDisconnectPacket(sshError, serverErrorString);
+ } catch (Botan::Exception &) {} // Nothing sensible to be done here.
+ if (m_error != SshNoError)
+ emit error(userError);
+ if (m_state == ConnectionEstablished)
+ emit disconnected();
+ m_socket->disconnectFromHost();
+ m_state = SocketUnconnected;
+}
+
+QSharedPointer<SshRemoteProcess> SshConnectionPrivate::createRemoteProcess(const QByteArray &command)
+{
+ return m_channelManager->createRemoteProcess(command);
+}
+
+QSharedPointer<SftpChannel> SshConnectionPrivate::createSftpChannel()
+{
+ return m_channelManager->createSftpChannel();
+}
+
+} // namespace Internal
+} // namespace Core