kio_sftp.cpp 94.6 KB
Newer Older
Lucas Fisher's avatar
Lucas Fisher committed
1
/*
2
3
4
 * SPDX-FileCopyrightText: 2001 Lucas Fisher <ljfisher@purdue.edu>
 * SPDX-FileCopyrightText: 2009 Andreas Schneider <mail@cynapses.org>
 * SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
5
 *
6
 * SPDX-License-Identifier: LGPL-2.0-or-later
7
 */
Lucas Fisher's avatar
Lucas Fisher committed
8

9
10
#include "kio_sftp.h"

11
#include <config-runtime.h>
12
#include "kio_sftp_debug.h"
13
#include "kio_sftp_trace_debug.h"
14
15
#include <cerrno>
#include <cstring>
16

17
18
19
20
21
22
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QVarLengthArray>
#include <QMimeType>
#include <QMimeDatabase>
23
#include <QDateTime>
24
25
#include <QScopedPointer>
#include <QScopeGuard>
Lucas Fisher's avatar
Lucas Fisher committed
26

27
#include <kuser.h>
Lucas Fisher's avatar
Lucas Fisher committed
28
#include <kmessagebox.h>
29

30
#include <klocalizedstring.h>
31
#include <kconfiggroup.h>
32
#include <kio/ioslave_defaults.h>
Lucas Fisher's avatar
Lucas Fisher committed
33

34
#ifdef Q_OS_WIN
35
36
#include <filesystem>  // for permissions
using namespace std::filesystem;
37
38
39
40
41
#include <qplatformdefs.h>
#else
#include <utime.h>
#endif

42
#define KIO_SFTP_SPECIAL_TIMEOUT 30
43

44
45
// How big should each data packet be? Definitely not bigger than 64kb or
// you will overflow the 2 byte size variable in a sftp packet.
46
47
48
// TODO: investigate what size we should have and consider changing.
// this seems too large...
// from the RFC:
Harald Sitter's avatar
en_us++    
Harald Sitter committed
49
//   The maximum size of a packet is in practice determined by the client
50
51
52
53
54
//   (the maximum size of read or write requests that it sends, plus a few
//   bytes of packet overhead).  All servers SHOULD support packets of at
//   least 34000 bytes (where the packet size refers to the full length,
//   including the header above).  This should allow for reads and writes of
//   at most 32768 bytes.
Harald Sitter's avatar
en_us++    
Harald Sitter committed
55
// In practice that means we can assume that the server supports 32kb,
56
57
58
59
60
61
// it may be more or it could be less. Since there's not really a system in place to
// figure out the maximum (and at least openssh arbitrarily resets the entire
// session if it finds a packet that is too large
// [https://bugs.kde.org/show_bug.cgi?id=404890]) we ought to be more conservative!
// At the same time there's no bug reports about the 60k requests being too large so
// perhaps all popular servers effectively support at least 64k.
62
63
#define MAX_XFER_BUF_SIZE (60 * 1024)

Harald Sitter's avatar
Harald Sitter committed
64
65
#define KSFTP_ISDIR(sb) (sb->type == SSH_FILEXFER_TYPE_DIRECTORY)

66
67
68
69
70
71
72
73
74
75
// sftp_attributes must be freed. Use this ScopedPtr to ensure they always are!
struct ScopedPointerCustomDeleter
{
    static inline void cleanup(sftp_attributes attr)
    {
        sftp_attributes_free(attr);
    }
};
typedef QScopedPointer<sftp_attributes_struct, ScopedPointerCustomDeleter> SFTPAttributesPtr;

76
77
78
79
80
81
82
// Pseudo plugin class to embed meta data
class KIOPluginForMetaData : public QObject
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.kio.slave.sftp" FILE "sftp.json")
};

Lucas Fisher's avatar
Lucas Fisher committed
83
84
85
using namespace KIO;
extern "C"
{
86
87
88
89
    int Q_DECL_EXPORT kdemain( int argc, char **argv )
    {
        QCoreApplication app(argc, argv);
        app.setApplicationName("kio_sftp");
90

91
        qCDebug(KIO_SFTP_LOG) << "*** Starting kio_sftp ";
Lucas Fisher's avatar
Lucas Fisher committed
92

93
94
95
96
        if (argc != 4) {
            qCDebug(KIO_SFTP_LOG) << "Usage: kio_sftp protocol domain-socket1 domain-socket2";
            exit(-1);
        }
Lucas Fisher's avatar
Lucas Fisher committed
97

98
99
        SFTPSlave slave(argv[2], argv[3]);
        slave.dispatchLoop();
Lucas Fisher's avatar
Lucas Fisher committed
100

101
102
103
        qCDebug(KIO_SFTP_LOG) << "*** kio_sftp Done";
        return 0;
    }
Lucas Fisher's avatar
Lucas Fisher committed
104
105
}

106
107
// Converts SSH error into KIO error. Only must be called for error handling
// as this will always return an error state and never NoError.
108
static int toKIOError(const int err)
109
{
110
    switch (err) {
111
112
    case SSH_FX_NO_SUCH_FILE:
    case SSH_FX_NO_SUCH_PATH:
113
        return KIO::ERR_DOES_NOT_EXIST;
114
    case SSH_FX_PERMISSION_DENIED:
115
        return KIO::ERR_ACCESS_DENIED;
116
    case SSH_FX_FILE_ALREADY_EXISTS:
117
        return KIO::ERR_FILE_ALREADY_EXIST;
118
    case SSH_FX_INVALID_HANDLE:
119
        return KIO::ERR_MALFORMED_URL;
120
    case SSH_FX_OP_UNSUPPORTED:
121
        return KIO::ERR_UNSUPPORTED_ACTION;
122
    case SSH_FX_BAD_MESSAGE:
123
        return KIO::ERR_UNKNOWN;
124
    default:
125
126
127
128
129
130
131
132
        return KIO::ERR_INTERNAL;
    }
    // We should not get here. When this function gets called we've
    // encountered an error on the libssh side, this needs to be mapped to *any*
    // KIO error. Not mapping is not an option at this point, even if the ssh err
    // is wrong or 'ok'.
    Q_UNREACHABLE();
    return KIO::ERR_UNKNOWN;
133
134
135
}

// Writes 'len' bytes from 'buf' to the file handle 'fd'.
136
static int writeToFile(int fd, const char *buf, int len)
137
{
138
    while (len > 0)  {
139
140
        size_t lenSize = static_cast<size_t>(len); // len is always >> 0 and, being an int, always << size_t
        ssize_t written = write(fd, buf, lenSize);
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

        if (written >= 0) {
            buf += written;
            len -= written;
            continue;
        }

        switch(errno) {
        case EINTR:
        case EAGAIN:
            continue;
        case EPIPE:
            return ERR_CONNECTION_BROKEN;
        case ENOSPC:
            return ERR_DISK_FULL;
        default:
Laurent Montel's avatar
Laurent Montel committed
157
            return ERR_CANNOT_WRITE;
158
159
160
        }
    }
    return 0;
161
162
}

163
static bool wasUsernameChanged(const QString &username, const KIO::AuthInfo &info)
164
165
166
167
168
169
170
171
172
173
174
175
176
{
    QString loginName (username);
    // If username is empty, assume the current logged in username. Why ?
    // Because libssh's SSH_OPTIONS_USER will default to that when it is not
    // set and it won't be set unless the user explicitly typed a user user
    // name as part of the request URL.
    if (loginName.isEmpty()) {
        KUser u;
        loginName = u.loginName();
    }
    return (loginName != info.username);
}

177
// The callback function for libssh
178
179
180
static int auth_callback(const char *prompt, char *buf, size_t len,
                         int echo, int verify, void *userdata)
{
181
182
183
    if (userdata == nullptr) {
        return -1;
    }
184

185
    SFTPInternal *slave = static_cast<SFTPInternal *>(userdata);
186

187
188
189
    if (slave->auth_callback(prompt, buf, len, echo, verify, userdata) < 0) {
        return -1;
    }
190

191
    return 0;
192
193
}

194
static void log_callback(int priority, const char *function, const char *buffer,
195
196
                         void *userdata)
{
Kevin Funk's avatar
Kevin Funk committed
197
    if (userdata == nullptr) {
198
199
        return;
    }
200

201
    SFTPInternal *slave = static_cast<SFTPInternal *>(userdata);
202

203
    slave->log_callback(priority, function, buffer, userdata);
204
205
}

206
207
208
209
210
211
int SFTPInternal::auth_callback(const char *prompt, char *buf, size_t len,
                                int echo, int verify, void *userdata)
{
    Q_UNUSED(echo)
    Q_UNUSED(verify)
    Q_UNUSED(userdata)
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238

    QString errMsg;
    if (!mPublicKeyAuthInfo) {
        mPublicKeyAuthInfo = new KIO::AuthInfo;
    } else {
        errMsg = i18n("Incorrect or invalid passphrase");
    }

    mPublicKeyAuthInfo->url.setScheme(QLatin1String("sftp"));
    mPublicKeyAuthInfo->url.setHost(mHost);
    if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
        mPublicKeyAuthInfo->url.setPort(mPort);
    }
    mPublicKeyAuthInfo->url.setUserName(mUsername);

    QUrl u (mPublicKeyAuthInfo->url);
    u.setPath(QString());
    mPublicKeyAuthInfo->comment = u.url();
    mPublicKeyAuthInfo->readOnly = true;
    mPublicKeyAuthInfo->prompt = QString::fromUtf8(prompt);
    mPublicKeyAuthInfo->keepPassword = false; // don't save passwords for public key,
    // that's the task of ssh-agent.
    mPublicKeyAuthInfo->setExtraField(QLatin1String("hide-username-line"), true);
    mPublicKeyAuthInfo->setModified(false);

    qCDebug(KIO_SFTP_LOG) << "Entering authentication callback, prompt=" << mPublicKeyAuthInfo->prompt;

239
    if (q->openPasswordDialogV2(*mPublicKeyAuthInfo, errMsg) != 0) {
Harald Sitter's avatar
typo--    
Harald Sitter committed
240
        qCDebug(KIO_SFTP_LOG) << "User canceled public key password dialog";
241
242
243
244
245
246
247
248
249
        return -1;
    }

    strncpy(buf, mPublicKeyAuthInfo->password.toUtf8().constData(), len - 1);

    mPublicKeyAuthInfo->password.fill('x');
    mPublicKeyAuthInfo->password.clear();

    return 0;
Lucas Fisher's avatar
Lucas Fisher committed
250
251
}

252
void SFTPInternal::log_callback(int priority, const char *function, const char *buffer,
253
                                void *userdata)
254
{
255
    Q_UNUSED(userdata)
256
    qCDebug(KIO_SFTP_LOG) << "[" << function << "] (" << priority << ") " << buffer;
257
258
}

259
260
Result SFTPInternal::init()
{
261
    qCDebug(KIO_SFTP_LOG) << "pid = " << qApp->applicationPid();
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
    qCDebug(KIO_SFTP_LOG) << "debug = " << getenv("KIO_SFTP_LOG_VERBOSITY");

    // Members are 'value initialized' to zero because of non-user defined ()!
    mCallbacks = new struct ssh_callbacks_struct();
    if (mCallbacks == nullptr) {
        return Result::fail(KIO::ERR_OUT_OF_MEMORY, i18n("Could not allocate callbacks"));
    }

    mCallbacks->userdata = this;
    mCallbacks->auth_function = ::auth_callback;

    ssh_callbacks_init(mCallbacks)

    bool ok;
    int level = qEnvironmentVariableIntValue("KIO_SFTP_LOG_VERBOSITY", &ok);
    if (ok) {
        int rc = ssh_set_log_level(level);
        if (rc != SSH_OK) {
            return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log verbosity."));
        }

        rc = ssh_set_log_userdata(this);
        if (rc != SSH_OK) {
            return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log userdata."));
        }

        rc = ssh_set_log_callback(::log_callback);
        if (rc != SSH_OK) {
            return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log callback."));
        }
    }

    return Result::pass();
}

void SFTPSlave::virtual_hook(int id, void *data)
298
{
299
300
301
    switch(id) {
    case SlaveBase::GetFileSystemFreeSpace: {
        QUrl *url = static_cast<QUrl *>(data);
302
303
304
        finalize(d->fileSystemFreeSpace(*url));
        return;
    }
305
306
    case SlaveBase::Truncate: {
        auto length = static_cast<KIO::filesize_t *>(data);
307
308
309
        maybeError(d->truncate(*length));
        return;
    }
310
    }
311
    SlaveBase::virtual_hook(id, data);
312
313
}

314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
void SFTPSlave::finalize(const Result &result)
{
    if (!result.success) {
        error(result.error, result.errorString);
        return;
    }
    finished();
}

void SFTPSlave::maybeError(const Result &result)
{
    if (!result.success) {
        error(result.error, result.errorString);
    }
}
329

330
331
int SFTPInternal::authenticateKeyboardInteractive(AuthInfo &info)
{
332
333
334
335
336
337
338
339
340
    int err = ssh_userauth_kbdint(mSession, nullptr, nullptr);

    while (err == SSH_AUTH_INFO) {
        const QString name = QString::fromUtf8(ssh_userauth_kbdint_getname(mSession));
        const QString instruction = QString::fromUtf8(ssh_userauth_kbdint_getinstruction(mSession));
        const int n = ssh_userauth_kbdint_getnprompts(mSession);

        qCDebug(KIO_SFTP_LOG) << "name=" << name << " instruction=" << instruction << " prompts=" << n;

341
342
        for (int iInt = 0; iInt < n; ++iInt) {
            unsigned int i = static_cast<unsigned int>(iInt); // can only be >0
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
            char echo;
            const char *answer = "";

            const QString prompt = QString::fromUtf8(ssh_userauth_kbdint_getprompt(mSession, i, &echo));
            qCDebug(KIO_SFTP_LOG) << "prompt=" << prompt << " echo=" << QString::number(echo);
            if (echo) {
                // See RFC4256 Section 3.3 User Interface
                KIO::AuthInfo infoKbdInt;

                infoKbdInt.url.setScheme("sftp");
                infoKbdInt.url.setHost(mHost);
                if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
                    infoKbdInt.url.setPort(mPort);
                }

                if (!name.isEmpty()) {
                    infoKbdInt.caption = QString(i18n("SFTP Login") + " - " + name);
                } else {
                    infoKbdInt.caption = i18n("SFTP Login");
                }

                infoKbdInt.comment = "sftp://" + mUsername + "@"  + mHost;

                QString newPrompt;
                if (!instruction.isEmpty()) {
                    newPrompt = instruction + "<br /><br />";
                }
                newPrompt.append(prompt);
                infoKbdInt.prompt = newPrompt;

                infoKbdInt.readOnly = false;
                infoKbdInt.keepPassword = false;

376
                if (q->openPasswordDialogV2(infoKbdInt, i18n("Use the username input field to answer this question.")) == KJob::NoError) {
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
                    qCDebug(KIO_SFTP_LOG) << "Got the answer from the password dialog";
                    answer = info.username.toUtf8().constData();
                }

                if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
                    qCDebug(KIO_SFTP_LOG) << "An error occurred setting the answer: "
                                          << ssh_get_error(mSession);
                    return SSH_AUTH_ERROR;
                }
                break;
            } else {
                if (prompt.startsWith(QLatin1String("password:"), Qt::CaseInsensitive)) {
                    info.prompt = i18n("Please enter your password.");
                } else {
                    info.prompt = prompt;
                }
                info.comment = info.url.url();
                info.commentLabel = i18n("Site:");
                info.setExtraField(QLatin1String("hide-username-line"), true);

397
                if (q->openPasswordDialogV2(info) == KJob::NoError) {
398
399
400
401
402
403
404
405
406
407
408
409
410
                    qCDebug(KIO_SFTP_LOG) << "Got the answer from the password dialog";
                    answer = info.password.toUtf8().constData();
                }

                if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
                    qCDebug(KIO_SFTP_LOG) << "An error occurred setting the answer: "
                                          << ssh_get_error(mSession);
                    return SSH_AUTH_ERROR;
                }
            }
        }
        err = ssh_userauth_kbdint(mSession, nullptr, nullptr);
    }
411

412
413
    return err;
}
414

415
416
Result SFTPInternal::reportError(const QUrl &url, const int err)
{
417
    qCDebug(KIO_SFTP_LOG) << "url = " << url << " - err=" << err;
418

419
    const int kioError = toKIOError(err);
420
    Q_ASSERT(kioError != KJob::NoError);
421

422
    return Result::fail(kioError, url.toDisplayString());
423
}
424

425
426
427
bool SFTPInternal::createUDSEntry(const QString &filename, const QByteArray &path,
                                  UDSEntry &entry, short int details)
{
428
429
430
    mode_t access;
    char *link;
    bool isBrokenLink = false;
431
    long long fileType = QT_STAT_REG;
432
    uint64_t size = 0U;
433

434
    Q_ASSERT(entry.count() == 0);
435
    entry.reserve(10);
436

437
    SFTPAttributesPtr sb(sftp_lstat(mSftp, path.constData()));
438
    if (sb == nullptr) {
439
        qCDebug(KIO_SFTP_LOG) << "Failed to stat" << path << sftp_get_error(mSftp);
440
441
        return false;
    }
442

443
    entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
444

445
446
447
    if (sb->type == SSH_FILEXFER_TYPE_SYMLINK) {
        link = sftp_readlink(mSftp, path.constData());
        if (link == nullptr) {
448
            qCDebug(KIO_SFTP_LOG) << "Failed to readlink despite this being a link!" << path << sftp_get_error(mSftp);
449
            return false;
450
        }
451
452
453
454
455
456
457
458
        entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(link));
        free(link);
        // A symlink -> follow it only if details > 1
        if (details > 1) {
            sftp_attributes sb2 = sftp_stat(mSftp, path.constData());
            if (sb2 == nullptr) {
                isBrokenLink = true;
            } else {
459
                sb.reset(sb2);
460
            }
461
        }
462
    }
463

464
465
    if (isBrokenLink) {
        // It is a link pointing to nowhere
466
467
468
469
        fileType = QT_STAT_MASK - 1;
#ifdef Q_OS_WIN
        access = static_cast<mode_t>(perms::owner_all | perms::group_all | perms::others_all);
#else
470
        access = S_IRWXU | S_IRWXG | S_IRWXO;
471
#endif
472
473
474
475
        size = 0LL;
    } else {
        switch (sb->type) {
        case SSH_FILEXFER_TYPE_REGULAR:
476
            fileType = QT_STAT_REG;
477
478
            break;
        case SSH_FILEXFER_TYPE_DIRECTORY:
479
            fileType = QT_STAT_DIR;
480
481
            break;
        case SSH_FILEXFER_TYPE_SYMLINK:
482
            fileType = QT_STAT_LNK;
483
484
485
            break;
        case SSH_FILEXFER_TYPE_SPECIAL:
        case SSH_FILEXFER_TYPE_UNKNOWN:
486
            fileType = QT_STAT_MASK - 1;
487
            break;
488
        }
489
490
491
492
493
494
495
496
497
498
        access = sb->permissions & 07777;
        size = sb->size;
    }
    entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, fileType);
    entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access);
    entry.fastInsert(KIO::UDSEntry::UDS_SIZE, size);

    if (details > 0) {
        if (sb->owner) {
            entry.fastInsert(KIO::UDSEntry::UDS_USER, QString::fromUtf8(sb->owner));
499
        } else {
500
            entry.fastInsert(KIO::UDSEntry::UDS_USER, QString::number(sb->uid));
501
        }
502

503
504
505
506
        if (sb->group) {
            entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QString::fromUtf8(sb->group));
        } else {
            entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QString::number(sb->gid));
507
        }
508

509
510
        entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, sb->atime);
        entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, sb->mtime);
511
512
513
514
515
516

        if (sb->flags & SSH_FILEXFER_ATTR_CREATETIME) {
            // Availability depends on outside factors.
            // https://bugs.kde.org/show_bug.cgi?id=375305
            entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, sb->createtime);
        }
517
    }
Lucas Fisher's avatar
Lucas Fisher committed
518

519
    return true;
520
}
521

522
523
QString SFTPInternal::canonicalizePath(const QString &path)
{
524
525
526
    qCDebug(KIO_SFTP_LOG) << "Path to canonicalize: " << path;
    QString cPath;
    char *sPath = nullptr;
527

528
529
    if (path.isEmpty()) {
        return cPath;
Lucas Fisher's avatar
Lucas Fisher committed
530
531
    }

532
533
534
535
    sPath = sftp_canonicalize_path(mSftp, path.toUtf8().constData());
    if (sPath == nullptr) {
        qCDebug(KIO_SFTP_LOG) << "Could not canonicalize path: " << path;
        return cPath;
Lucas Fisher's avatar
Lucas Fisher committed
536
    }
537

538
    cPath = QFile::decodeName(sPath);
David Hallas's avatar
David Hallas committed
539
    ssh_string_free_char(sPath);
540

541
    qCDebug(KIO_SFTP_LOG) << "Canonicalized path: " << cPath;
542

543
    return cPath;
Lucas Fisher's avatar
Lucas Fisher committed
544
545
}

546
547
548
SFTPInternal::SFTPInternal(SFTPSlave *qptr)
    : q(qptr)
{
549
}
Lucas Fisher's avatar
Lucas Fisher committed
550

551
SFTPInternal::~SFTPInternal() {
552
    qCDebug(KIO_SFTP_LOG) << "pid = " << qApp->applicationPid();
553
    closeConnection();
Lucas Fisher's avatar
Lucas Fisher committed
554

555
556
    delete mCallbacks;
    delete mPublicKeyAuthInfo; // for precaution
557

558
559
    /* cleanup and shut down cryto stuff */
    ssh_finalize();
560
}
561

562
void SFTPInternal::setHost(const QString& host, quint16 port, const QString& user, const QString& pass) {
563
    qCDebug(KIO_SFTP_LOG) << user << "@" << host << ":" << port;
564

565
566
567
568
569
    // Close connection if the request is to another server...
    if (host != mHost || port != mPort ||
            user != mUsername || pass != mPassword) {
        closeConnection();
    }
570

571
572
573
574
    mHost = host;
    mPort = port;
    mUsername = user;
    mPassword = pass;
575
}
576

577
Result SFTPInternal::sftpOpenConnection(const AuthInfo &info)
578
{
579
580
    mSession = ssh_new();
    if (mSession == nullptr) {
581
        return Result::fail(KIO::ERR_OUT_OF_MEMORY, i18n("Could not create a new SSH session."));
582
583
584
585
586
587
588
589
590
    }

    long timeout_sec = 30, timeout_usec = 0;

    qCDebug(KIO_SFTP_LOG) << "Creating the SSH session and setting options";

    // Set timeout
    int rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT, &timeout_sec);
    if (rc < 0) {
591
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
592
593
594
    }
    rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT_USEC, &timeout_usec);
    if (rc < 0) {
595
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
596
    }
597

598
#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 8, 0)
599
600
601
602
    // Disable Nagle's Algorithm (TCP_NODELAY). Usually faster for sftp.
    bool nodelay = true;
    rc = ssh_options_set(mSession, SSH_OPTIONS_NODELAY, &nodelay);
    if (rc < 0) {
603
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not disable Nagle's Algorithm."));
604
    }
605
#endif // 0.8.0
Andreas Schneider's avatar
Andreas Schneider committed
606

607
608
609
    // Don't use any compression
    rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_C_S, "none");
    if (rc < 0) {
610
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set compression."));
611
612
613
    }

    rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_S_C, "none");
614
    if (rc < 0) {
615
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set compression."));
616
    }
617

618
619
    // Set host and port
    rc = ssh_options_set(mSession, SSH_OPTIONS_HOST, mHost.toUtf8().constData());
620
    if (rc < 0) {
621
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set host."));
622
623
624
625
626
    }

    if (mPort > 0) {
        rc = ssh_options_set(mSession, SSH_OPTIONS_PORT, &mPort);
        if (rc < 0) {
627
            return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set port."));
628
629
630
631
632
633
634
        }
    }

    // Set the username
    if (!info.username.isEmpty()) {
        rc = ssh_options_set(mSession, SSH_OPTIONS_USER, info.username.toUtf8().constData());
        if (rc < 0) {
635
            return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set username."));
636
        }
637
    }
638

639
640
641
    // Read ~/.ssh/config
    rc = ssh_options_parse_config(mSession, nullptr);
    if (rc < 0) {
642
        return Result::fail(KIO::ERR_INTERNAL, i18n("Could not parse the config file."));
643
    }
644

645
    ssh_set_callbacks(mSession, mCallbacks);
646

647
    qCDebug(KIO_SFTP_LOG) << "Trying to connect to the SSH server";
648

649
650
651
652
653
654
655
    unsigned int effectivePort;
    if (mPort > 0) {
        effectivePort = mPort;
    } else {
        effectivePort = DEFAULT_SFTP_PORT;
        ssh_options_get_port(mSession, &effectivePort);
    }
656

657
    qCDebug(KIO_SFTP_LOG) << "username=" << mUsername << ", host=" << mHost << ", port=" << effectivePort;
658

659
    q->infoMessage(xi18n("Opening SFTP connection to host %1:%2", mHost, QString::number(effectivePort)));
660

661
662
663
    /* try to connect */
    rc = ssh_connect(mSession);
    if (rc < 0) {
664
        const QString errorString = QString::fromUtf8(ssh_get_error(mSession));
665
        closeConnection();
666
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
667
    }
668

669
    return Result::pass();
670
671
}

672
#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 8, 3)
673
Result SFTPInternal::openConnection()
674
675
{
    if (mConnected) {
676
        return Result::pass();
677
678
679
680
    }

    if (mHost.isEmpty()) {
        qCDebug(KIO_SFTP_LOG) << "openConnection(): Need hostname...";
681
        return Result::fail(KIO::ERR_UNKNOWN_HOST, QString());
682
683
684
685
686
687
688
689
690
691
692
693
694
695
    }

    AuthInfo info;
    info.url.setScheme("sftp");
    info.url.setHost(mHost);
    if ( mPort > 0 && mPort != DEFAULT_SFTP_PORT ) {
        info.url.setPort(mPort);
    }
    info.url.setUserName(mUsername);
    info.username = mUsername;

    // Check for cached authentication info if no password is specified...
    if (mPassword.isEmpty()) {
        qCDebug(KIO_SFTP_LOG) << "checking cache: info.username =" << info.username
696
                              << ", info.url =" << info.url.toDisplayString();
697
        q->checkCachedAuthentication(info);
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
    } else {
        info.password = mPassword;
    }

    // Start the ssh connection.
    QString msg;     // msg for dialog box
    QString caption; // dialog box caption
    unsigned char *hash = nullptr; // the server hash
    size_t hlen;
    ssh_key srv_pubkey = nullptr;
    const char *srv_pubkey_type = nullptr;
    char *fingerprint = nullptr;
    enum ssh_known_hosts_e state;
    int rc;

    // Attempt to start a ssh session and establish a connection with the server.
714
715
716
    const Result openResult = sftpOpenConnection(info);
    if (!openResult.success) {
        return openResult;
717
718
719
720
721
722
723
    }

    qCDebug(KIO_SFTP_LOG) << "Getting the SSH server hash";

    /* get the hash */
    rc = ssh_get_server_publickey(mSession, &srv_pubkey);
    if (rc < 0) {
724
        const QString errorString = QString::fromUtf8(ssh_get_error(mSession));
725
        closeConnection();
726
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
727
728
729
730
731
732
    }

    srv_pubkey_type = ssh_key_type_to_char(ssh_key_type(srv_pubkey));
    if (srv_pubkey_type == nullptr) {
        ssh_key_free(srv_pubkey);
        closeConnection();
733
734
        return Result::fail(KIO::ERR_SLAVE_DEFINED,
                            i18n("Could not get server public key type name"));
735
736
737
738
739
740
741
742
743
    }

    rc = ssh_get_publickey_hash(srv_pubkey,
                                SSH_PUBLICKEY_HASH_SHA256,
                                &hash,
                                &hlen);
    ssh_key_free(srv_pubkey);
    if (rc != SSH_OK) {
        closeConnection();
744
745
        return Result::fail(KIO::ERR_SLAVE_DEFINED,
                            i18n("Could not create hash from server public key"));
746
    }
747

748
749
750
751
752
753
    fingerprint = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256,
                                           hash,
                                           hlen);
    ssh_string_free_char((char *)hash);
    if (fingerprint == nullptr) {
        closeConnection();
754
755
        return Result::fail(KIO::ERR_SLAVE_DEFINED,
                            i18n("Could not create fingerprint for server public key"));
756
757
758
759
760
761
762
    }

    qCDebug(KIO_SFTP_LOG) << "Checking if the SSH server is known";

    /* check the server public key hash */
    state = ssh_session_is_known_server(mSession);
    switch (state) {
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
    case SSH_KNOWN_HOSTS_OTHER: {
        ssh_string_free_char(fingerprint);
        const QString errorString = i18n("An %1 host key for this server was "
                                         "not found, but another type of key exists.\n"
                                         "An attacker might change the default server key to confuse your "
                                         "client into thinking the key does not exist.\n"
                                         "Please contact your system administrator.\n"
                                         "%2",
                                         QString::fromUtf8(srv_pubkey_type),
                                         QString::fromUtf8(ssh_get_error(mSession)));
        closeConnection();
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
    }
    case SSH_KNOWN_HOSTS_CHANGED: {
        const QString errorString = i18n("The host key for the server %1 has changed.\n"
                                         "This could either mean that DNS SPOOFING is happening or the IP "
                                         "address for the host and its host key have changed at the same time.\n"
                                         "The fingerprint for the %2 key sent by the remote host is:\n"
                                         "  SHA256:%3\n"
                                         "Please contact your system administrator.\n%4",
                                         mHost,
                                         QString::fromUtf8(srv_pubkey_type),
                                         QString::fromUtf8(fingerprint),
                                         QString::fromUtf8(ssh_get_error(mSession)));
        ssh_string_free_char(fingerprint);
        closeConnection();
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
    }
    case SSH_KNOWN_HOSTS_NOT_FOUND:
    case SSH_KNOWN_HOSTS_UNKNOWN: {
        caption = i18n("Warning: Cannot verify host's identity.");
        msg = i18n("The authenticity of host %1 cannot be established.\n"
                   "The %2 key fingerprint is: %3\n"
                   "Are you sure you want to continue connecting?",
                   mHost,
                   QString::fromUtf8(srv_pubkey_type),
                   QString::fromUtf8(fingerprint));
        ssh_string_free_char(fingerprint);

        if (KMessageBox::Yes != q->messageBox(SlaveBase::WarningYesNo, msg, caption)) {
803
            closeConnection();
804
            return Result::fail(KIO::ERR_USER_CANCELED);
805
        }
806

807
808
809
810
811
812
813
        /* write the known_hosts file */
        qCDebug(KIO_SFTP_LOG) << "Adding server to known_hosts file.";
        rc = ssh_session_update_known_hosts(mSession);
        if (rc != SSH_OK) {
            const QString errorString = QString::fromUtf8(ssh_get_error(mSession));
            closeConnection();
            return Result::fail(KIO::ERR_USER_CANCELED,errorString);
814
        }
815
816
817
818
819
820
821
822
        break;
    }
    case SSH_KNOWN_HOSTS_ERROR:
        ssh_string_free_char(fingerprint);
        return Result::fail(KIO::ERR_SLAVE_DEFINED,
                            QString::fromUtf8(ssh_get_error(mSession)));
    case SSH_KNOWN_HOSTS_OK:
        break;
823
824
825
826
827
828
829
830
    }

    qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with the server";

    // Try to login without authentication
    rc = ssh_userauth_none(mSession, nullptr);
    if (rc == SSH_AUTH_ERROR) {
        closeConnection();
831
        return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
832
833
834
835
836
837
    }

    // This NEEDS to be called after ssh_userauth_none() !!!
    int method = ssh_auth_list(mSession);
    if (rc != SSH_AUTH_SUCCESS && method == 0) {
        closeConnection();
838
        return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed. The server "
839
                            "didn't send any authentication methods"));
840
841
842
843
844
845
846
847
848
    }

    // Try to authenticate with public key first
    if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PUBLICKEY)) {
        qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with public key";
        for(;;) {
            rc = ssh_userauth_publickey_auto(mSession, nullptr, nullptr);
            if (rc == SSH_AUTH_ERROR) {
                qCDebug(KIO_SFTP_LOG) << "Public key authentication failed:" <<
849
                                      QString::fromUtf8(ssh_get_error(mSession));
850
851
                closeConnection();
                clearPubKeyAuthInfo();
852
                return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
853
854
855
856
857
858
859
860
861
862
863
864
865
            } else if (rc != SSH_AUTH_DENIED || !mPublicKeyAuthInfo || !mPublicKeyAuthInfo->isModified()) {
                clearPubKeyAuthInfo();
                break;
            }
        }
    }

    // Try to authenticate with GSSAPI
    if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_GSSAPI_MIC)) {
        qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with GSSAPI";
        rc = ssh_userauth_gssapi(mSession);
        if (rc == SSH_AUTH_ERROR) {
            qCDebug(KIO_SFTP_LOG) << "Public key authentication failed:" <<
866
                                  QString::fromUtf8(ssh_get_error(mSession));
867
            closeConnection();
868
            return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
869
870
871
872
873
874
875
876
877
878
879
880
        }
    }

    // Try to authenticate with keyboard interactive
    if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_INTERACTIVE)) {
        qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with keyboard interactive";
        AuthInfo info2 (info);
        rc = authenticateKeyboardInteractive(info2);
        if (rc == SSH_AUTH_SUCCESS) {
            info = info2;
        } else if (rc == SSH_AUTH_ERROR) {
            qCDebug(KIO_SFTP_LOG) << "Keyboard interactive authentication failed:"
881
                                  << QString::fromUtf8(ssh_get_error(mSession));
882
            closeConnection();
883
            return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
        }
    }

    // Try to authenticate with password
    if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PASSWORD)) {
        qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with password";

        info.caption = i18n("SFTP Login");
        info.prompt = i18n("Please enter your username and password.");
        info.comment = info.url.url();
        info.commentLabel = i18n("Site:");
        bool isFirstLoginAttempt = true;

        for(;;) {
            if (!isFirstLoginAttempt || info.password.isEmpty()) {
                info.keepPassword = true; // make the "keep Password" check box visible to the user.
                info.setModified(false);

                QString username (info.username);
                const QString errMsg(isFirstLoginAttempt ? QString() : i18n("Incorrect username or password"));

                qCDebug(KIO_SFTP_LOG) << "Username:" << username << "first attempt?"
906
                                      << isFirstLoginAttempt << "error:" << errMsg;
907
908
909

                // Handle user canceled or dialog failed to open...

910
                int errCode = q->openPasswordDialogV2(info, errMsg);
911
912
913
                if (errCode != 0) {
                    qCDebug(KIO_SFTP_LOG) << "User canceled password/retry dialog";
                    closeConnection();
914
                    return Result::fail(errCode, QString());
915
916
                }

Yuri Chornoivan's avatar
Yuri Chornoivan committed
917
                // If the user name changes, we have to re-establish connection again
918
919
920
921
922
923
924
                // since the user name must always be set before calling ssh_connect.
                if (wasUsernameChanged(username, info)) {
                    qCDebug(KIO_SFTP_LOG) << "Username changed to" << info.username;
                    if (!info.url.userName().isEmpty()) {
                        info.url.setUserName(info.username);
                    }
                    closeConnection();
925
926
927
                    const auto result = sftpOpenConnection(info);
                    if (!result.success) {
                        return result;
928
929
930
931
932
933
934
935
936
                    }
                }
            }

            rc = ssh_userauth_password(mSession, info.username.toUtf8().constData(), info.password.toUtf8().constData());
            if (rc == SSH_AUTH_SUCCESS) {
                break;
            } else if (rc == SSH_AUTH_ERROR) {
                qCDebug(KIO_SFTP_LOG) << "Password authentication failed:"
937
                                      << QString::fromUtf8(ssh_get_error(mSession));
938
                closeConnection();
939
                return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
940
941
942
943
944
945
946
947
948
            }

            isFirstLoginAttempt = false; // failed attempt to login.
            info.password.clear();       // clear the password after failed attempts.
        }
    }

    // If we're still not authenticated then we need to leave.
    if (rc != SSH_AUTH_SUCCESS) {
949
        return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
950
951
952
953
954
955
956
    }

    // start sftp session
    qCDebug(KIO_SFTP_LOG) << "Trying to request the sftp session";
    mSftp = sftp_new(mSession);
    if (mSftp == nullptr) {
        closeConnection();
957
        return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Unable to request the SFTP subsystem. "
958
                            "Make sure SFTP is enabled on the server."));
959
960
961
962
963
    }

    qCDebug(KIO_SFTP_LOG) << "Trying to initialize the sftp session";
    if (sftp_init(mSftp) < 0) {
        closeConnection();
964
        return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Could not initialize the SFTP session."));
965
966
967
    }

    // Login succeeded!
968
    q->infoMessage(i18n("Successfully connected to %1", mHost));
969
970
    if (info.keepPassword) {
        qCDebug(KIO_SFTP_LOG) << "Caching info.username = " << info.username
971
                              << ", info.url = " << info.url.toDisplayString();
972
        q->cacheAuthentication(info);
973
974
975
976
977
978
979
    }

    // Update the original username in case it was changed!
    if (!mUsername.isEmpty()) {
        mUsername = info.username;
    }

980
    q->setTimeoutSpecialCommand(KIO_SFTP_SPECIAL_TIMEOUT);
981
982

    mConnected = true;
983
    q->connected();
984
985
986

    info.password.fill('x');
    info.password.clear();
987
988

    return Result::pass();
989
990
}
#else // < 0.8.0
991
Result SFTPInternal::openConnection()
992
{
993
    if (mConnected) {
994
        return Result::pass();
995
996
997
998
    }

    if (mHost.isEmpty()) {
        qCDebug(KIO_SFTP_LOG) << "openConnection(): Need hostname...";
999
        return Result::fail(KIO::ERR_UNKNOWN_HOST);
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
    }

    AuthInfo info;
    info.url.setScheme("sftp");
    info.url.setHost(mHost);
    if ( mPort > 0 && mPort != DEFAULT_SFTP_PORT ) {
        info.url.setPort(mPort);
    }
    info.url.setUserName(mUsername);
    info.username = mUsername;

    // Check for cached authentication info if no password is specified...
    if (mPassword.isEmpty()) {
        qCDebug(KIO_SFTP_LOG) << "checking cache: info.username =" << info.username
                              << ", info.url =" << info.url.toDisplayString();
1015
        q->checkCachedAuthentication(info);
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
    } else {
        info.password = mPassword;
    }

    // Start the ssh connection.
    QString msg;     // msg for dialog box
    QString caption; // dialog box caption
    unsigned char *hash = nullptr; // the server hash
    ssh_key srv_pubkey;
    char *hexa;
    size_t hlen;
    int rc, state;

    // Attempt to start a ssh session and establish a connection with the server.
1030
1031
1032
    const auto openResult = sftpOpenConnection(info);
    if (!openResult.success) {
        return openResult;
1033
1034
1035
1036
1037
1038
1039
    }

    qCDebug(KIO_SFTP_LOG) << "Getting the SSH server hash";

    /* get the hash */
    rc = ssh_get_publickey(mSession, &srv_pubkey);
    if (rc < 0) {
1040
        const auto result = Result::fail(KIO::ERR_SLAVE_DEFINED, QString::fromUtf8(ssh_get_error(mSession)));
1041
        closeConnection();
1042
        return result;
1043
    }
1044

1045
1046
1047
1048
1049
1050
1051
    rc = ssh_get_publickey_hash(srv_pubkey,
                                SSH_PUBLICKEY_HASH_SHA1,
                                &hash,
                                &hlen);
    ssh_key_free(srv_pubkey);
    if (rc < 0) {
        closeConnection();
1052
1053
        return Result::fail(KIO::ERR_SLAVE_DEFINED,
                            i18n("Could not create hash from server public key"));
1054
    }
1055

1056
1057
1058
1059
1060
    qCDebug(KIO_SFTP_LOG) << "Checking if the SSH server is known";

    /* check the server public key hash */
    state = ssh_is_server_known(mSession);
    switch (state) {
1061
    case SSH_SERVER_KNOWN_OK:
1062
        break;
1063
    case SSH_SERVER_FOUND_OTHER: {
1064
        ssh_string_free_char((char *)hash);
1065
1066
1067
1068
1069
1070
        const QString errorString = i18n("The host key for this server was "
                                         "not found, but another type of key exists.\n"
                                         "An attacker might change the default server key to confuse your "
                                         "client into thinking the key does not exist.\n"
                                         "Please contact your system administrator.\n%1",
                                         QString::fromUtf8(ssh_get_error(mSession)));
1071
        closeConnection();
1072
1073
1074
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
    }
    case SSH_SERVER_KNOWN_CHANGED: {
1075
1076
1077
        hexa = ssh_get_hexa(hash, hlen);
        ssh_string_free_char((char *)hash);
        /* TODO print known_hosts file, port? */
1078
1079
1080
1081
1082
1083
1084
1085
        const QString errorString = i18n("The host key for the server %1 has changed.\n"
                                         "This could either mean that DNS SPOOFING is happening or the IP "
                                         "address for the host and its host key have changed at the same time.\n"
                                         "The fingerprint for the key sent by the remote host is:\n %2\n"
                                         "Please contact your system administrator.\n%3",
                                         mHost,
                                         QString::fromUtf8(hexa),
                                         QString::fromUtf8(ssh_get_error(mSession)));
1086
        ssh_string_free_char(hexa);
1087
        closeConnection();
1088
1089
        return Result::fail(KIO::ERR_SLAVE_DEFINED, errorString);
    }
1090
    case SSH_SERVER_FILE_NOT_FOUND:
1091
    case SSH_SERVER_NOT_KNOWN: {
1092
1093
1094
1095
1096
1097
1098
1099
        hexa = ssh_get