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