1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "sshconnection.h"
27 
28 #include "sftpsession.h"
29 #include "sftptransfer.h"
30 #include "sshlogging_p.h"
31 #include "sshprocess.h"
32 #include "sshremoteprocess.h"
33 #include "sshsettings.h"
34 
35 #include <utils/filesystemwatcher.h>
36 #include <utils/fileutils.h>
37 #include <utils/hostosinfo.h>
38 #include <utils/qtcassert.h>
39 
40 #include <QByteArrayList>
41 #include <QDir>
42 #include <QFileInfo>
43 #include <QTemporaryDir>
44 #include <QTimer>
45 
46 #include <memory>
47 
48 /*!
49     \class QSsh::SshConnection
50 
51     \brief The SshConnection class provides an SSH connection via an OpenSSH client
52            running in master mode.
53 
54     It operates asynchronously (non-blocking) and is not thread-safe.
55 
56     If connection sharing is turned off, the class operates as a simple factory
57     for processes etc and "connecting" always succeeds. The actual connection
58     is then established later, e.g. when starting the remote process.
59 
60 */
61 
62 namespace QSsh {
63 using namespace Internal;
64 using namespace Utils;
65 
SshConnectionParameters()66 SshConnectionParameters::SshConnectionParameters()
67 {
68     url.setPort(0);
69 }
70 
equals(const SshConnectionParameters & p1,const SshConnectionParameters & p2)71 static inline bool equals(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
72 {
73     return p1.url == p2.url
74             && p1.authenticationType == p2.authenticationType
75             && p1.privateKeyFile == p2.privateKeyFile
76             && p1.hostKeyCheckingMode == p2.hostKeyCheckingMode
77             && p1.x11DisplayName == p2.x11DisplayName
78             && p1.timeout == p2.timeout;
79 }
80 
operator ==(const SshConnectionParameters & p1,const SshConnectionParameters & p2)81 bool operator==(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
82 {
83     return equals(p1, p2);
84 }
85 
operator !=(const SshConnectionParameters & p1,const SshConnectionParameters & p2)86 bool operator!=(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
87 {
88     return !equals(p1, p2);
89 }
90 
91 struct SshConnection::SshConnectionPrivate
92 {
fullProcessErrorQSsh::SshConnection::SshConnectionPrivate93     QString fullProcessError()
94     {
95         QString error;
96         if (masterProcess.exitStatus() != QProcess::NormalExit)
97             error = masterProcess.errorString();
98         const QByteArray stdErr = masterProcess.readAllStandardError();
99         if (!stdErr.isEmpty()) {
100             if (!error.isEmpty())
101                 error.append('\n');
102             error.append(QString::fromLocal8Bit(stdErr));
103         }
104         return error;
105     }
106 
socketFilePathQSsh::SshConnection::SshConnectionPrivate107     QString socketFilePath() const
108     {
109         QTC_ASSERT(masterSocketDir, return QString());
110         return masterSocketDir->path() + "/cs";
111     }
112 
connectionOptionsQSsh::SshConnection::SshConnectionPrivate113     QStringList connectionOptions(const FilePath &binary) const
114     {
115         QString hostKeyCheckingString;
116         switch (connParams.hostKeyCheckingMode) {
117         case SshHostKeyCheckingNone:
118         case SshHostKeyCheckingAllowNoMatch:
119             // There is "accept-new" as well, but only since 7.6.
120             hostKeyCheckingString = "no";
121             break;
122         case SshHostKeyCheckingStrict:
123             hostKeyCheckingString = "yes";
124             break;
125         }
126         QStringList args{"-o", "StrictHostKeyChecking=" + hostKeyCheckingString,
127                     "-o", "User=" + connParams.userName(),
128                     "-o", "Port=" + QString::number(connParams.port())};
129         const bool keyOnly = connParams.authenticationType ==
130                 SshConnectionParameters::AuthenticationTypeSpecificKey;
131         if (keyOnly) {
132             args << "-o" << "IdentitiesOnly=yes";
133             args << "-i" << connParams.privateKeyFile;
134         }
135         if (keyOnly || SshSettings::askpassFilePath().isEmpty())
136             args << "-o" << "BatchMode=yes";
137         if (sharingEnabled)
138             args << "-o" << ("ControlPath=" + socketFilePath());
139         bool useTimeout = connParams.timeout != 0;
140         if (useTimeout && HostOsInfo::isWindowsHost()
141                 && binary.toString().toLower().contains("/system32/")) {
142             useTimeout = false;
143         }
144         if (useTimeout)
145             args << "-o" << ("ConnectTimeout=" + QString::number(connParams.timeout));
146         return args;
147     }
148 
connectionArgsQSsh::SshConnection::SshConnectionPrivate149     QStringList connectionArgs(const FilePath &binary) const
150     {
151         return connectionOptions(binary) << connParams.host();
152     }
153 
154     SshConnectionParameters connParams;
155     SshConnectionInfo connInfo;
156     SshProcess masterProcess;
157     QString errorString;
158     std::unique_ptr<QTemporaryDir> masterSocketDir;
159     State state = Unconnected;
160     const bool sharingEnabled = SshSettings::connectionSharingEnabled();
161 };
162 
163 
SshConnection(const SshConnectionParameters & serverInfo,QObject * parent)164 SshConnection::SshConnection(const SshConnectionParameters &serverInfo, QObject *parent)
165     : QObject(parent), d(new SshConnectionPrivate)
166 {
167     qRegisterMetaType<QSsh::SftpFileInfo>("QSsh::SftpFileInfo");
168     qRegisterMetaType<QList <QSsh::SftpFileInfo> >("QList<QSsh::SftpFileInfo>");
169     d->connParams = serverInfo;
170     connect(&d->masterProcess, &QProcess::started, [this] {
171         QFileInfo socketInfo(d->socketFilePath());
172         if (socketInfo.exists()) {
173             emitConnected();
174             return;
175         }
176         auto * const socketWatcher = new FileSystemWatcher(this);
177         auto * const socketWatcherTimer = new QTimer(this);
178         const auto socketFileChecker = [this, socketWatcher, socketWatcherTimer] {
179             if (!QFileInfo::exists(d->socketFilePath()))
180                 return;
181             socketWatcher->disconnect();
182             socketWatcher->deleteLater();
183             socketWatcherTimer->disconnect();
184             socketWatcherTimer->stop();
185             socketWatcherTimer->deleteLater();
186             emitConnected();
187         };
188         connect(socketWatcher, &FileSystemWatcher::directoryChanged, socketFileChecker);
189         socketWatcher->addDirectory(socketInfo.path(), FileSystemWatcher::WatchAllChanges);
190         if (HostOsInfo::isMacHost()) {
191             // QTBUG-72455
192             socketWatcherTimer->setInterval(1000);
193             connect(socketWatcherTimer, &QTimer::timeout, socketFileChecker);
194             socketWatcherTimer->start();
195         }
196     });
197     connect(&d->masterProcess, &QProcess::errorOccurred, [this] (QProcess::ProcessError error) {
198         switch (error) {
199         case QProcess::FailedToStart:
200             emitError(tr("Cannot establish SSH connection: Control process failed to start: %1")
201                       .arg(d->fullProcessError()));
202             break;
203         case QProcess::Crashed: // Handled by finished() handler.
204         case QProcess::Timedout:
205         case QProcess::ReadError:
206         case QProcess::WriteError:
207         case QProcess::UnknownError:
208             break; // Cannot happen.
209         }
210     });
211     connect(&d->masterProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), [this] {
212         if (d->state == Disconnecting) {
213             emitDisconnected();
214             return;
215         }
216         const QString procError = d->fullProcessError();
217         QString errorMsg = tr("SSH connection failure.");
218         if (!procError.isEmpty())
219             errorMsg.append('\n').append(procError);
220         emitError(errorMsg);
221     });
222     if (!d->connParams.x11DisplayName.isEmpty()) {
223         QProcessEnvironment env = d->masterProcess.processEnvironment();
224         env.insert("DISPLAY", d->connParams.x11DisplayName);
225         d->masterProcess.setProcessEnvironment(env);
226     }
227 }
228 
connectToHost()229 void SshConnection::connectToHost()
230 {
231     d->state = Connecting;
232     QTimer::singleShot(0, this, &SshConnection::doConnectToHost);
233 }
234 
disconnectFromHost()235 void SshConnection::disconnectFromHost()
236 {
237     switch (d->state) {
238     case Connecting:
239     case Connected:
240         if (!d->sharingEnabled) {
241             QTimer::singleShot(0, this, &SshConnection::emitDisconnected);
242             return;
243         }
244         d->state = Disconnecting;
245         if (HostOsInfo::isWindowsHost())
246             d->masterProcess.kill();
247         else
248             d->masterProcess.terminate();
249         break;
250     case Unconnected:
251     case Disconnecting:
252         break;
253     }
254 }
255 
state() const256 SshConnection::State SshConnection::state() const
257 {
258     return d->state;
259 }
260 
errorString() const261 QString SshConnection::errorString() const
262 {
263     return d->errorString;
264 }
265 
connectionParameters() const266 SshConnectionParameters SshConnection::connectionParameters() const
267 {
268     return d->connParams;
269 }
270 
connectionInfo() const271 SshConnectionInfo SshConnection::connectionInfo() const
272 {
273     QTC_ASSERT(state() == Connected, return SshConnectionInfo());
274     if (d->connInfo.isValid())
275         return d->connInfo;
276     QProcess p;
277     p.start(SshSettings::sshFilePath().toString(), d->connectionArgs(SshSettings::sshFilePath())
278             << "echo" << "-n" << "$SSH_CLIENT");
279     if (!p.waitForStarted() || !p.waitForFinished()) {
280         qCWarning(Internal::sshLog) << "failed to retrieve connection info:" << p.errorString();
281         return SshConnectionInfo();
282     }
283     const QByteArrayList data = p.readAllStandardOutput().split(' ');
284     if (data.size() != 3) {
285         qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output";
286         return SshConnectionInfo();
287     }
288     d->connInfo.localPort = data.at(1).toInt();
289     if (d->connInfo.localPort == 0) {
290         qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output";
291         return SshConnectionInfo();
292     }
293     if (!d->connInfo.localAddress.setAddress(QString::fromLatin1(data.first()))) {
294         qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output";
295         return SshConnectionInfo();
296     }
297     d->connInfo.peerPort = d->connParams.port();
298     d->connInfo.peerAddress.setAddress(d->connParams.host());
299     return d->connInfo;
300 }
301 
connectionOptions(const FilePath & binary) const302 QStringList SshConnection::connectionOptions(const FilePath &binary) const
303 {
304     return d->connectionOptions(binary);
305 }
306 
sharingEnabled() const307 bool SshConnection::sharingEnabled() const
308 {
309     return d->sharingEnabled;
310 }
311 
~SshConnection()312 SshConnection::~SshConnection()
313 {
314     disconnect();
315     disconnectFromHost();
316     delete d;
317 }
318 
createRemoteProcess(const QString & command)319 SshRemoteProcessPtr SshConnection::createRemoteProcess(const QString &command)
320 {
321     QTC_ASSERT(state() == Connected, return SshRemoteProcessPtr());
322     return SshRemoteProcessPtr(new SshRemoteProcess(command,
323                                                     d->connectionArgs(SshSettings::sshFilePath())));
324 }
325 
createRemoteShell()326 SshRemoteProcessPtr SshConnection::createRemoteShell()
327 {
328     return createRemoteProcess({});
329 }
330 
createUpload(const FilesToTransfer & files,FileTransferErrorHandling errorHandlingMode)331 SftpTransferPtr SshConnection::createUpload(const FilesToTransfer &files,
332                                             FileTransferErrorHandling errorHandlingMode)
333 {
334     return setupTransfer(files, Internal::FileTransferType::Upload, errorHandlingMode);
335 }
336 
createDownload(const FilesToTransfer & files,FileTransferErrorHandling errorHandlingMode)337 SftpTransferPtr SshConnection::createDownload(const FilesToTransfer &files,
338                                               FileTransferErrorHandling errorHandlingMode)
339 {
340     return setupTransfer(files, Internal::FileTransferType::Download, errorHandlingMode);
341 }
342 
createSftpSession()343 SftpSessionPtr SshConnection::createSftpSession()
344 {
345     QTC_ASSERT(state() == Connected, return SftpSessionPtr());
346     return SftpSessionPtr(new SftpSession(d->connectionArgs(SshSettings::sftpFilePath())));
347 }
348 
doConnectToHost()349 void SshConnection::doConnectToHost()
350 {
351     if (d->state != Connecting)
352         return;
353     const FilePath sshBinary = SshSettings::sshFilePath();
354     if (!sshBinary.exists()) {
355         emitError(tr("Cannot establish SSH connection: ssh binary \"%1\" does not exist.")
356                   .arg(sshBinary.toUserOutput()));
357         return;
358     }
359     if (!d->sharingEnabled) {
360         emitConnected();
361         return;
362     }
363     d->masterSocketDir.reset(new QTemporaryDir);
364     if (!d->masterSocketDir->isValid()) {
365         emitError(tr("Cannot establish SSH connection: Failed to create temporary "
366                      "directory for control socket: %1")
367                   .arg(d->masterSocketDir->errorString()));
368         return;
369     }
370     QStringList args = QStringList{"-M", "-N", "-o", "ControlPersist=no"}
371             << d->connectionArgs(sshBinary);
372     if (!d->connParams.x11DisplayName.isEmpty())
373         args.prepend("-X");
374     qCDebug(sshLog) << "establishing connection:" << sshBinary.toUserOutput() << args;
375     d->masterProcess.start(sshBinary.toString(), args);
376 }
377 
emitError(const QString & reason)378 void SshConnection::emitError(const QString &reason)
379 {
380     const State oldState = d->state;
381     d->state = Unconnected;
382     d->errorString = reason;
383     emit errorOccurred();
384     if (oldState == Connected)
385         emitDisconnected();
386 }
387 
emitConnected()388 void SshConnection::emitConnected()
389 {
390     d->state = Connected;
391     emit connected();
392 }
393 
emitDisconnected()394 void SshConnection::emitDisconnected()
395 {
396     d->state = Unconnected;
397     emit disconnected();
398 }
399 
setupTransfer(const FilesToTransfer & files,Internal::FileTransferType type,FileTransferErrorHandling errorHandlingMode)400 SftpTransferPtr SshConnection::setupTransfer(
401         const FilesToTransfer &files, Internal::FileTransferType type,
402         FileTransferErrorHandling errorHandlingMode)
403 {
404     QTC_ASSERT(state() == Connected, return SftpTransferPtr());
405     return SftpTransferPtr(new SftpTransfer(files, type, errorHandlingMode,
406                                             d->connectionArgs(SshSettings::sftpFilePath())));
407 }
408 
409 } // namespace QSsh
410