File indexing completed on 2024-05-12 09:00:23

0001 /*
0002  * SPDX-FileCopyrightText: 2001 Lucas Fisher <ljfisher@purdue.edu>
0003  * SPDX-FileCopyrightText: 2009 Andreas Schneider <mail@cynapses.org>
0004  * SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "kio_sftp.h"
0010 #include <config-runtime.h>
0011 
0012 #include <array>
0013 #include <cerrno>
0014 #include <cstring>
0015 #include <filesystem>
0016 #include <memory>
0017 #include <ranges>
0018 #include <span>
0019 
0020 #include <QCoreApplication>
0021 #include <QDateTime>
0022 #include <QDir>
0023 #include <QFile>
0024 #include <QMimeDatabase>
0025 #include <QMimeType>
0026 #include <QScopeGuard>
0027 #include <QScopedPointer>
0028 #include <QVarLengthArray>
0029 
0030 #include <KMessageBox>
0031 #include <KUser>
0032 
0033 #include <KConfigGroup>
0034 #include <KIO/AuthInfo>
0035 #include <KLocalizedString>
0036 #include <kio/ioworker_defaults.h>
0037 
0038 #ifdef Q_OS_WIN
0039 #include <qplatformdefs.h>
0040 #endif
0041 
0042 #include "kio_sftp_debug.h"
0043 #include "kio_sftp_trace_debug.h"
0044 
0045 // For MinGW compatibility
0046 #ifndef QT_STAT_LNK
0047 #define QT_STAT_LNK 0120000
0048 #endif // QT_STAT_LNK
0049 
0050 using namespace KIO;
0051 using namespace std::filesystem;
0052 
0053 namespace std
0054 {
0055 template<>
0056 struct default_delete<struct sftp_file_struct> {
0057     void operator()(struct sftp_file_struct *ptr) const
0058     {
0059         if (ptr) {
0060             sftp_close(ptr);
0061         }
0062     }
0063 };
0064 } // namespace std
0065 
0066 using UniqueSFTPFilePtr = std::unique_ptr<struct sftp_file_struct>;
0067 
0068 namespace
0069 {
0070 constexpr auto KIO_SFTP_SPECIAL_TIMEOUT_MS = 30;
0071 
0072 // How big should each data packet be? Definitely not bigger than 64kb or
0073 // you will overflow the 2 byte size variable in a sftp packet.
0074 // TODO: investigate what size we should have and consider changing.
0075 // this seems too large...
0076 // from the RFC:
0077 //   The maximum size of a packet is in practice determined by the client
0078 //   (the maximum size of read or write requests that it sends, plus a few
0079 //   bytes of packet overhead).  All servers SHOULD support packets of at
0080 //   least 34000 bytes (where the packet size refers to the full length,
0081 //   including the header above).  This should allow for reads and writes of
0082 //   at most 32768 bytes.
0083 // In practice that means we can assume that the server supports 32kb,
0084 // it may be more or it could be less. Since there's not really a system in place to
0085 // figure out the maximum (and at least openssh arbitrarily resets the entire
0086 // session if it finds a packet that is too large
0087 // [https://bugs.kde.org/show_bug.cgi?id=404890]) we ought to be more conservative!
0088 // At the same time there's no bug reports about the 60k requests being too large so
0089 // perhaps all popular servers effectively support at least 64k.
0090 constexpr auto MAX_XFER_BUF_SIZE = (60ULL * 1024);
0091 
0092 inline bool KSFTP_ISDIR(SFTPAttributesPtr &sb)
0093 {
0094     return sb->type == SSH_FILEXFER_TYPE_DIRECTORY;
0095 }
0096 
0097 // Converts SSH error into KIO error. Only must be called for error handling
0098 // as this will always return an error state and never NoError.
0099 int toKIOError(const int err)
0100 {
0101     switch (err) {
0102     case SSH_FX_NO_SUCH_FILE:
0103     case SSH_FX_NO_SUCH_PATH:
0104         return KIO::ERR_DOES_NOT_EXIST;
0105     case SSH_FX_PERMISSION_DENIED:
0106         return KIO::ERR_ACCESS_DENIED;
0107     case SSH_FX_FILE_ALREADY_EXISTS:
0108         return KIO::ERR_FILE_ALREADY_EXIST;
0109     case SSH_FX_INVALID_HANDLE:
0110         return KIO::ERR_MALFORMED_URL;
0111     case SSH_FX_OP_UNSUPPORTED:
0112         return KIO::ERR_UNSUPPORTED_ACTION;
0113     case SSH_FX_BAD_MESSAGE:
0114         return KIO::ERR_UNKNOWN;
0115     default:
0116         return KIO::ERR_INTERNAL;
0117     }
0118     // We should not get here. When this function gets called we've
0119     // encountered an error on the libssh side, this needs to be mapped to *any*
0120     // KIO error. Not mapping is not an option at this point, even if the ssh err
0121     // is wrong or 'ok'.
0122     Q_UNREACHABLE();
0123     return KIO::ERR_UNKNOWN;
0124 }
0125 
0126 // Writes buf into fd.
0127 int writeToFile(int fd, const std::span<const char> &buf)
0128 {
0129     size_t offset = 0;
0130     while (offset != buf.size()) {
0131         const auto written = write(fd, &buf[offset], buf.size() - offset);
0132 
0133         if (written >= 0) {
0134             offset += written;
0135             continue;
0136         }
0137 
0138         switch (errno) {
0139         case EINTR:
0140         case EAGAIN:
0141             continue;
0142         case EPIPE:
0143             return ERR_CONNECTION_BROKEN;
0144         case ENOSPC:
0145             return ERR_DISK_FULL;
0146         default:
0147             return ERR_CANNOT_WRITE;
0148         }
0149     }
0150     return 0;
0151 }
0152 
0153 bool wasUsernameChanged(const QString &username, const KIO::AuthInfo &info)
0154 {
0155     QString loginName(username);
0156     // If username is empty, assume the current logged in username. Why ?
0157     // Because libssh's SSH_OPTIONS_USER will default to that when it is not
0158     // set and it won't be set unless the user explicitly typed a user user
0159     // name as part of the request URL.
0160     if (loginName.isEmpty()) {
0161         KUser u;
0162         loginName = u.loginName();
0163     }
0164     return (loginName != info.username);
0165 }
0166 
0167 // The callback function for libssh
0168 int auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata)
0169 {
0170     if (userdata == nullptr) {
0171         return -1;
0172     }
0173 
0174     auto *worker = static_cast<SFTPWorker *>(userdata);
0175     if (worker->auth_callback(prompt, buf, len, echo, verify, userdata) < 0) {
0176         return -1;
0177     }
0178 
0179     return 0;
0180 }
0181 
0182 void log_callback(int priority, const char *function, const char *buffer, void *userdata)
0183 {
0184     if (userdata == nullptr) {
0185         return;
0186     }
0187 
0188     auto *worker = static_cast<SFTPWorker *>(userdata);
0189     worker->log_callback(priority, function, buffer, userdata);
0190 }
0191 
0192 inline std::optional<perms> posixToOptionalPerms(uint32_t mode) noexcept
0193 {
0194     if (mode < 0) {
0195         return {};
0196     }
0197     return static_cast<perms>(mode) & perms::mask;
0198 }
0199 
0200 inline mode_t permsToPosix(perms mode) noexcept
0201 {
0202     return static_cast<mode_t>(mode);
0203 }
0204 } // namespace
0205 
0206 // Pseudo plugin class to embed meta data
0207 class KIOPluginForMetaData : public QObject
0208 {
0209     Q_OBJECT
0210     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.sftp" FILE "sftp.json")
0211 };
0212 
0213 extern "C" {
0214 int Q_DECL_EXPORT kdemain(int argc, char **argv)
0215 {
0216     QCoreApplication app(argc, argv);
0217     app.setApplicationName("kio_sftp");
0218 
0219     qCDebug(KIO_SFTP_LOG) << "*** Starting kio_sftp ";
0220 
0221     if (argc != 4) {
0222         qCWarning(KIO_SFTP_LOG) << "Usage: kio_sftp protocol domain-socket1 domain-socket2";
0223         exit(-1);
0224     }
0225 
0226     std::span args{argv, std::make_unsigned_t<decltype(argc)>(argc)};
0227     SFTPWorker worker(args[2], args[3]);
0228     worker.dispatchLoop();
0229 
0230     qCDebug(KIO_SFTP_LOG) << "*** kio_sftp Done";
0231     return 0;
0232 }
0233 }
0234 
0235 SFTPWorker::SFTPWorker(const QByteArray &poolSocket, const QByteArray &appSocket)
0236     : WorkerBase(QByteArrayLiteral("kio_sftp"), poolSocket, appSocket)
0237 {
0238     const auto result = init();
0239     Q_ASSERT(result.success());
0240 }
0241 
0242 SFTPWorker::~SFTPWorker()
0243 {
0244     qCDebug(KIO_SFTP_LOG) << "pid = " << qApp->applicationPid();
0245     closeConnection();
0246 
0247     delete mCallbacks;
0248     delete mPublicKeyAuthInfo; // for precaution
0249 
0250     /* cleanup and shut down crypto stuff */
0251     ssh_finalize();
0252 }
0253 
0254 int SFTPWorker::auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata)
0255 {
0256     Q_UNUSED(echo)
0257     Q_UNUSED(verify)
0258     Q_UNUSED(userdata)
0259 
0260     QString errMsg;
0261     if (!mPublicKeyAuthInfo) {
0262         mPublicKeyAuthInfo = new KIO::AuthInfo;
0263     } else {
0264         errMsg = i18n("Incorrect or invalid passphrase");
0265     }
0266 
0267     mPublicKeyAuthInfo->url.setScheme(QLatin1String("sftp"));
0268     mPublicKeyAuthInfo->url.setHost(mHost);
0269     if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
0270         mPublicKeyAuthInfo->url.setPort(mPort);
0271     }
0272     mPublicKeyAuthInfo->url.setUserName(mUsername);
0273 
0274     QUrl u(mPublicKeyAuthInfo->url);
0275     u.setPath(QString());
0276     mPublicKeyAuthInfo->comment = u.url();
0277     mPublicKeyAuthInfo->readOnly = true;
0278     mPublicKeyAuthInfo->prompt = QString::fromUtf8(prompt);
0279     mPublicKeyAuthInfo->keepPassword = false; // don't save passwords for public key,
0280     // that's the task of ssh-agent.
0281     mPublicKeyAuthInfo->setExtraField(QLatin1String("hide-username-line"), true);
0282     mPublicKeyAuthInfo->setModified(false);
0283 
0284     qCDebug(KIO_SFTP_LOG) << "Entering authentication callback, prompt=" << mPublicKeyAuthInfo->prompt;
0285 
0286     if (openPasswordDialog(*mPublicKeyAuthInfo, errMsg) != 0) {
0287         qCDebug(KIO_SFTP_LOG) << "User canceled public key password dialog";
0288         return -1;
0289     }
0290 
0291     strncpy(buf, mPublicKeyAuthInfo->password.toUtf8().constData(), len - 1);
0292 
0293     mPublicKeyAuthInfo->password.fill('x');
0294     mPublicKeyAuthInfo->password.clear();
0295 
0296     return 0;
0297 }
0298 
0299 void SFTPWorker::log_callback(int priority, const char *function, const char *buffer, void *userdata)
0300 {
0301     Q_UNUSED(userdata)
0302     qCDebug(KIO_SFTP_LOG) << "[" << function << "] (" << priority << ") " << buffer;
0303 }
0304 
0305 Result SFTPWorker::init()
0306 {
0307     qCDebug(KIO_SFTP_LOG) << "pid = " << qApp->applicationPid();
0308     qCDebug(KIO_SFTP_LOG) << "debug = " << getenv("KIO_SFTP_LOG_VERBOSITY");
0309 
0310     // Members are 'value initialized' to zero because of non-user defined ()!
0311     mCallbacks = new struct ssh_callbacks_struct();
0312     if (mCallbacks == nullptr) {
0313         return Result::fail(KIO::ERR_OUT_OF_MEMORY, i18n("Could not allocate callbacks"));
0314     }
0315 
0316     mCallbacks->userdata = this;
0317     mCallbacks->auth_function = ::auth_callback;
0318 
0319     ssh_callbacks_init(mCallbacks);
0320 
0321     bool ok = false;
0322     int level = qEnvironmentVariableIntValue("KIO_SFTP_LOG_VERBOSITY", &ok);
0323     if (ok) {
0324         int rc = ssh_set_log_level(level);
0325         if (rc != SSH_OK) {
0326             return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log verbosity."));
0327         }
0328 
0329         rc = ssh_set_log_userdata(this);
0330         if (rc != SSH_OK) {
0331             return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log userdata."));
0332         }
0333 
0334         rc = ssh_set_log_callback(::log_callback);
0335         if (rc != SSH_OK) {
0336             return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set log callback."));
0337         }
0338     }
0339 
0340     return Result::pass();
0341 }
0342 
0343 int SFTPWorker::authenticateKeyboardInteractive(AuthInfo &info)
0344 {
0345     int err = ssh_userauth_kbdint(mSession, nullptr, nullptr);
0346 
0347     while (err == SSH_AUTH_INFO) {
0348         const QString name = QString::fromUtf8(ssh_userauth_kbdint_getname(mSession));
0349         const QString instruction = QString::fromUtf8(ssh_userauth_kbdint_getinstruction(mSession));
0350         const int n = ssh_userauth_kbdint_getnprompts(mSession);
0351 
0352         qCDebug(KIO_SFTP_LOG) << "name=" << name << " instruction=" << instruction << " prompts=" << n;
0353 
0354         for (int iInt = 0; iInt < n; ++iInt) {
0355             const auto i = static_cast<unsigned int>(iInt); // can only be >0
0356             char echo = 0;
0357             const char *answer = "";
0358 
0359             const QString prompt = QString::fromUtf8(ssh_userauth_kbdint_getprompt(mSession, i, &echo));
0360             qCDebug(KIO_SFTP_LOG) << "prompt=" << prompt << " echo=" << QString::number(echo);
0361             if (echo) {
0362                 // See RFC4256 Section 3.3 User Interface
0363                 KIO::AuthInfo infoKbdInt;
0364 
0365                 infoKbdInt.url.setScheme("sftp");
0366                 infoKbdInt.url.setHost(mHost);
0367                 if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
0368                     infoKbdInt.url.setPort(mPort);
0369                 }
0370 
0371                 if (!name.isEmpty()) {
0372                     infoKbdInt.caption = QString(i18n("SFTP Login") + " - " + name);
0373                 } else {
0374                     infoKbdInt.caption = i18n("SFTP Login");
0375                 }
0376 
0377                 infoKbdInt.comment = "sftp://" + mUsername + "@" + mHost;
0378 
0379                 QString newPrompt;
0380                 if (!instruction.isEmpty()) {
0381                     newPrompt = instruction + "<br /><br />";
0382                 }
0383                 newPrompt.append(prompt);
0384                 infoKbdInt.prompt = newPrompt;
0385 
0386                 infoKbdInt.readOnly = false;
0387                 infoKbdInt.keepPassword = false;
0388 
0389                 if (openPasswordDialog(infoKbdInt, i18n("Use the username input field to answer this question.")) == KJob::NoError) {
0390                     qCDebug(KIO_SFTP_LOG) << "Got the answer from the password dialog";
0391                     answer = info.username.toUtf8().constData();
0392                 }
0393 
0394                 if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
0395                     qCDebug(KIO_SFTP_LOG) << "An error occurred setting the answer: " << ssh_get_error(mSession);
0396                     return SSH_AUTH_ERROR;
0397                 }
0398                 break;
0399             }
0400 
0401             if (prompt.startsWith(QLatin1String("password:"), Qt::CaseInsensitive)) {
0402                 info.prompt = i18n("Please enter your password.");
0403             } else {
0404                 info.prompt = prompt;
0405             }
0406             info.comment = info.url.url();
0407             info.commentLabel = i18n("Site:");
0408             info.setExtraField(QLatin1String("hide-username-line"), true);
0409 
0410             if (openPasswordDialog(info) == KJob::NoError) {
0411                 qCDebug(KIO_SFTP_LOG) << "Got the answer from the password dialog";
0412                 answer = info.password.toUtf8().constData();
0413             }
0414 
0415             if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
0416                 qCDebug(KIO_SFTP_LOG) << "An error occurred setting the answer: " << ssh_get_error(mSession);
0417                 return SSH_AUTH_ERROR;
0418             }
0419         }
0420         err = ssh_userauth_kbdint(mSession, nullptr, nullptr);
0421     }
0422 
0423     return err;
0424 }
0425 
0426 Result SFTPWorker::reportError(const QUrl &url, const int err)
0427 {
0428     qCDebug(KIO_SFTP_LOG) << "url = " << url << " - err=" << err;
0429 
0430     const int kioError = toKIOError(err);
0431     Q_ASSERT(kioError != KJob::NoError);
0432 
0433     return Result::fail(kioError, url.toDisplayString());
0434 }
0435 
0436 Result SFTPWorker::createUDSEntry(SFTPAttributesPtr sb, UDSEntry &entry, const QByteArray &path, const QString &name, int details)
0437 {
0438     // caller should make sure it is not null. behavior between stat on a file and listing a dir is different!
0439     // Also we consume the pointer, hence why it is a SFTPAttributesPtr
0440     Q_ASSERT(sb.get());
0441 
0442     entry.clear();
0443     entry.reserve(10);
0444     entry.fastInsert(KIO::UDSEntry::UDS_NAME, name /* awkwardly sftp_lstat doesn't fill the attributes name correctly, calculate from path */);
0445 
0446     bool isBrokenLink = false;
0447     if (sb->type == SSH_FILEXFER_TYPE_SYMLINK) {
0448         std::unique_ptr<char, decltype(&free)> link(sftp_readlink(mSftp, path.constData()), free);
0449         if (!link) {
0450             return Result::fail(KIO::ERR_INTERNAL,
0451                                 i18nc("error message. %1 is a path, %2 is a numeric error code",
0452                                       "Could not read link: %1 [%2]",
0453                                       QString::fromUtf8(path),
0454                                       QString::number(sftp_get_error(mSftp))));
0455         }
0456         entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(link.get()));
0457         // A symlink -> follow it only if details > 1
0458         if (details > 1) {
0459             sftp_attributes sb2 = sftp_stat(mSftp, path.constData());
0460             if (sb2 == nullptr) {
0461                 isBrokenLink = true;
0462             } else {
0463                 sb.reset(sb2);
0464             }
0465         }
0466     }
0467 
0468     perms access = perms::none;
0469     long long fileType = QT_STAT_REG;
0470     uint64_t size = 0U;
0471     if (isBrokenLink) {
0472         // It is a link pointing to nowhere
0473         fileType = QT_STAT_MASK - 1;
0474         access = perms::all;
0475         size = 0LL;
0476     } else {
0477         switch (sb->type) {
0478         case SSH_FILEXFER_TYPE_REGULAR:
0479             fileType = QT_STAT_REG;
0480             break;
0481         case SSH_FILEXFER_TYPE_DIRECTORY:
0482             fileType = QT_STAT_DIR;
0483             break;
0484         case SSH_FILEXFER_TYPE_SYMLINK:
0485             fileType = QT_STAT_LNK;
0486             break;
0487         case SSH_FILEXFER_TYPE_SPECIAL:
0488         case SSH_FILEXFER_TYPE_UNKNOWN:
0489             fileType = QT_STAT_MASK - 1;
0490             break;
0491         }
0492         access = posixToOptionalPerms(sb->permissions).value_or(perms::none);
0493         size = sb->size;
0494     }
0495     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, fileType);
0496     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, permsToPosix(access));
0497     entry.fastInsert(KIO::UDSEntry::UDS_SIZE, size);
0498 
0499     if (details > 0) {
0500         if (sb->owner) {
0501             entry.fastInsert(KIO::UDSEntry::UDS_USER, QString::fromUtf8(sb->owner));
0502         } else {
0503             entry.fastInsert(KIO::UDSEntry::UDS_USER, QString::number(sb->uid));
0504         }
0505 
0506         if (sb->group) {
0507             entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QString::fromUtf8(sb->group));
0508         } else {
0509             entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QString::number(sb->gid));
0510         }
0511 
0512         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, sb->atime);
0513         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, sb->mtime);
0514 
0515         if (sb->flags & SSH_FILEXFER_ATTR_CREATETIME) {
0516             // Availability depends on outside factors.
0517             // https://bugs.kde.org/show_bug.cgi?id=375305
0518             entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, sb->createtime);
0519         }
0520     }
0521 
0522     return Result::pass();
0523 }
0524 
0525 QString SFTPWorker::canonicalizePath(const QString &path)
0526 {
0527     qCDebug(KIO_SFTP_LOG) << "Path to canonicalize: " << path;
0528     QString cPath;
0529     char *sPath = nullptr;
0530 
0531     if (path.isEmpty()) {
0532         return cPath;
0533     }
0534 
0535     sPath = sftp_canonicalize_path(mSftp, path.toUtf8().constData());
0536     if (sPath == nullptr) {
0537         qCDebug(KIO_SFTP_LOG) << "Could not canonicalize path: " << path;
0538         return cPath;
0539     }
0540 
0541     cPath = QFile::decodeName(sPath);
0542     ssh_string_free_char(sPath);
0543 
0544     qCDebug(KIO_SFTP_LOG) << "Canonicalized path: " << cPath;
0545 
0546     return cPath;
0547 }
0548 
0549 void SFTPWorker::setHost(const QString &host, quint16 port, const QString &user, const QString &pass)
0550 {
0551     qCDebug(KIO_SFTP_LOG) << user << "@" << host << ":" << port;
0552 
0553     // Close connection if the request is to another server...
0554     if (host != mHost || port != mPort || user != mUsername || pass != mPassword) {
0555         closeConnection();
0556     }
0557 
0558     mHost = host;
0559     mPort = port;
0560     mUsername = user;
0561     mPassword = pass;
0562 }
0563 
0564 Result SFTPWorker::sftpOpenConnection(const AuthInfo &info)
0565 {
0566     closeConnection(); // make sure a potential previous connection is closed. this function may get called to re-connect
0567 
0568     mSession = ssh_new();
0569     if (mSession == nullptr) {
0570         return Result::fail(KIO::ERR_OUT_OF_MEMORY, i18n("Could not create a new SSH session."));
0571     }
0572 
0573     const long timeout_sec = 30;
0574     const long timeout_usec = 0;
0575 
0576     qCDebug(KIO_SFTP_LOG) << "Creating the SSH session and setting options";
0577 
0578     // Set timeout
0579     int rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT, &timeout_sec);
0580     if (rc < 0) {
0581         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
0582     }
0583     rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT_USEC, &timeout_usec);
0584     if (rc < 0) {
0585         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
0586     }
0587 
0588     // Disable Nagle's Algorithm (TCP_NODELAY). Usually faster for sftp.
0589     bool nodelay = true;
0590     rc = ssh_options_set(mSession, SSH_OPTIONS_NODELAY, &nodelay);
0591     if (rc < 0) {
0592         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not disable Nagle's Algorithm."));
0593     }
0594 
0595     // Prefer not to use compression
0596     rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_C_S, "none,zlib@openssh.com,zlib");
0597     if (rc < 0) {
0598         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set compression."));
0599     }
0600 
0601     rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_S_C, "none,zlib@openssh.com,zlib");
0602     if (rc < 0) {
0603         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set compression."));
0604     }
0605 
0606     // Set host and port
0607     rc = ssh_options_set(mSession, SSH_OPTIONS_HOST, mHost.toUtf8().constData());
0608     if (rc < 0) {
0609         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set host."));
0610     }
0611 
0612     if (mPort > 0) {
0613         rc = ssh_options_set(mSession, SSH_OPTIONS_PORT, &mPort);
0614         if (rc < 0) {
0615             return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set port."));
0616         }
0617     }
0618 
0619     // Set the username
0620     if (!info.username.isEmpty()) {
0621         rc = ssh_options_set(mSession, SSH_OPTIONS_USER, info.username.toUtf8().constData());
0622         if (rc < 0) {
0623             return Result::fail(KIO::ERR_INTERNAL, i18n("Could not set username."));
0624         }
0625     }
0626 
0627     // Read ~/.ssh/config
0628     rc = ssh_options_parse_config(mSession, nullptr);
0629     if (rc < 0) {
0630         return Result::fail(KIO::ERR_INTERNAL, i18n("Could not parse the config file."));
0631     }
0632 
0633     ssh_set_callbacks(mSession, mCallbacks);
0634 
0635     qCDebug(KIO_SFTP_LOG) << "Trying to connect to the SSH server";
0636 
0637     unsigned int effectivePort = mPort;
0638     if (effectivePort <= 0) {
0639         effectivePort = DEFAULT_SFTP_PORT;
0640         ssh_options_get_port(mSession, &effectivePort);
0641     }
0642 
0643     qCDebug(KIO_SFTP_LOG) << "username=" << mUsername << ", host=" << mHost << ", port=" << effectivePort;
0644 
0645     infoMessage(xi18n("Opening SFTP connection to host %1:%2", mHost, QString::number(effectivePort)));
0646 
0647     /* try to connect */
0648     rc = ssh_connect(mSession);
0649     if (rc < 0) {
0650         const QString errorString = QString::fromUtf8(ssh_get_error(mSession));
0651         closeConnection();
0652         return Result::fail(KIO::ERR_WORKER_DEFINED, errorString);
0653     }
0654 
0655     return Result::pass();
0656 }
0657 
0658 struct ServerKeyInspection {
0659     QByteArray serverPublicKeyType;
0660     QByteArray fingerprint;
0661     Result result = Result::pass();
0662 
0663     ServerKeyInspection &withResult(const Result &result)
0664     {
0665         this->result = result;
0666         return *this;
0667     }
0668 };
0669 
0670 Q_REQUIRED_RESULT ServerKeyInspection fingerprint(ssh_session session)
0671 {
0672     ServerKeyInspection inspection;
0673 
0674     ssh_key srv_pubkey = nullptr;
0675     const auto freeKey = qScopeGuard([srv_pubkey] {
0676         ssh_key_free(srv_pubkey);
0677     });
0678     int rc = ssh_get_server_publickey(session, &srv_pubkey);
0679     if (rc < 0) {
0680         return inspection.withResult(Result::fail(KIO::ERR_WORKER_DEFINED, QString::fromUtf8(ssh_get_error(session))));
0681     }
0682 
0683     const char *srv_pubkey_type = ssh_key_type_to_char(ssh_key_type(srv_pubkey));
0684     if (srv_pubkey_type == nullptr) {
0685         return inspection.withResult(Result::fail(KIO::ERR_WORKER_DEFINED, i18n("Could not get server public key type name")));
0686     }
0687     inspection.serverPublicKeyType = QByteArray(srv_pubkey_type);
0688 
0689     unsigned char *hash = nullptr; // the server hash
0690     const auto freeHash = qScopeGuard([&hash] {
0691         ssh_clean_pubkey_hash(&hash);
0692     });
0693 
0694     size_t hlen = 0;
0695     rc = ssh_get_publickey_hash(srv_pubkey, SSH_PUBLICKEY_HASH_SHA256, &hash, &hlen);
0696     if (rc != SSH_OK) {
0697         return inspection.withResult(Result::fail(KIO::ERR_WORKER_DEFINED, i18n("Could not create hash from server public key")));
0698     }
0699 
0700     char *fingerprint = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, hlen);
0701     const auto freeFingerprint = qScopeGuard([fingerprint] {
0702         ssh_string_free_char(fingerprint);
0703     });
0704 
0705     if (fingerprint == nullptr) {
0706         return inspection.withResult(Result::fail(KIO::ERR_WORKER_DEFINED, i18n("Could not create fingerprint for server public key")));
0707     }
0708 
0709     inspection.fingerprint = fingerprint;
0710 
0711     return inspection.withResult(Result::pass());
0712 }
0713 
0714 Result SFTPWorker::openConnectionWithoutCloseOnError()
0715 {
0716     if (mConnected) {
0717         return Result::pass();
0718     }
0719 
0720     if (mHost.isEmpty()) {
0721         qCDebug(KIO_SFTP_LOG) << "openConnection(): Need hostname...";
0722         return Result::fail(KIO::ERR_UNKNOWN_HOST, QString());
0723     }
0724 
0725     AuthInfo info;
0726     info.url.setScheme("sftp");
0727     info.url.setHost(mHost);
0728     if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
0729         info.url.setPort(mPort);
0730     }
0731     info.url.setUserName(mUsername);
0732     info.username = mUsername;
0733 
0734     // Check for cached authentication info if no password is specified...
0735     if (mPassword.isEmpty()) {
0736         qCDebug(KIO_SFTP_LOG) << "checking cache: info.username =" << info.username << ", info.url =" << info.url.toDisplayString();
0737         checkCachedAuthentication(info);
0738     } else {
0739         info.password = mPassword;
0740     }
0741 
0742     // Start the ssh connection.
0743 
0744     // Attempt to start a ssh session and establish a connection with the server.
0745     const Result openResult = sftpOpenConnection(info);
0746     if (!openResult.success()) {
0747         return openResult;
0748     }
0749 
0750     // get the hash
0751     qCDebug(KIO_SFTP_LOG) << "Getting the SSH server hash";
0752     const ServerKeyInspection inspection = fingerprint(mSession);
0753     if (!inspection.result.success()) {
0754         return inspection.result;
0755     }
0756     const QString fingerprint = QString::fromUtf8(inspection.fingerprint);
0757     const QString serverPublicKeyType = QString::fromUtf8(inspection.serverPublicKeyType);
0758 
0759     qCDebug(KIO_SFTP_LOG) << "Checking if the SSH server is known";
0760 
0761     /* check the server public key hash */
0762     enum ssh_known_hosts_e state = ssh_session_is_known_server(mSession);
0763     switch (state) {
0764     case SSH_KNOWN_HOSTS_OTHER: {
0765         const QString errorString = i18n(
0766             "An %1 host key for this server was "
0767             "not found, but another type of key exists.\n"
0768             "An attacker might change the default server key to confuse your "
0769             "client into thinking the key does not exist.\n"
0770             "Please contact your system administrator.\n"
0771             "%2",
0772             serverPublicKeyType,
0773             QString::fromUtf8(ssh_get_error(mSession)));
0774         return Result::fail(KIO::ERR_WORKER_DEFINED, errorString);
0775     }
0776     case SSH_KNOWN_HOSTS_CHANGED:
0777     case SSH_KNOWN_HOSTS_NOT_FOUND:
0778     case SSH_KNOWN_HOSTS_UNKNOWN: {
0779         QString caption;
0780         QString msg;
0781 
0782         if (state == SSH_KNOWN_HOSTS_CHANGED) {
0783             caption = i18nc("@title:window", "Host Identity Change");
0784             msg = xi18nc("@info",
0785                          "<para>The host key for the server <emphasis>%1</emphasis> has changed.</para>"
0786                          "<para>This could either mean that DNS spoofing is happening or the IP "
0787                          "address for the host and its host key have changed at the same time.</para>"
0788                          "<para>The %2 key fingerprint sent by the remote host is:"
0789                          "<bcode>%3</bcode>"
0790                          "Are you sure you want to continue connecting?</para>",
0791                          mHost,
0792                          serverPublicKeyType,
0793                          fingerprint);
0794         } else {
0795             caption = i18nc("@title:window", "Host Verification Failure");
0796             msg = xi18nc("@info",
0797                          "<para>The authenticity of host <emphasis>%1</emphasis> cannot be established.</para>"
0798                          "<para>The %2 key fingerprint is:"
0799                          "<bcode>%3</bcode>"
0800                          "Are you sure you want to continue connecting?</para>",
0801                          mHost,
0802                          serverPublicKeyType,
0803                          fingerprint);
0804         }
0805 
0806         if (KMessageBox::Continue != messageBox(WorkerBase::WarningContinueCancel, msg, caption, i18nc("@action:button", "Connect Anyway"))) {
0807             return Result::fail(KIO::ERR_USER_CANCELED);
0808         }
0809 
0810         /* write the known_hosts file */
0811         qCDebug(KIO_SFTP_LOG) << "Adding server to known_hosts file.";
0812         int rc = ssh_session_update_known_hosts(mSession);
0813         if (rc != SSH_OK) {
0814             return Result::fail(KIO::ERR_USER_CANCELED, QString::fromUtf8(ssh_get_error(mSession)));
0815         }
0816         break;
0817     }
0818     case SSH_KNOWN_HOSTS_ERROR:
0819         return Result::fail(KIO::ERR_WORKER_DEFINED, QString::fromUtf8(ssh_get_error(mSession)));
0820     case SSH_KNOWN_HOSTS_OK:
0821         break;
0822     }
0823 
0824     qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with the server";
0825 
0826     // Try to login without authentication
0827     int rc = ssh_userauth_none(mSession, nullptr);
0828     if (rc == SSH_AUTH_ERROR) {
0829         return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0830     }
0831 
0832     // This NEEDS to be called after ssh_userauth_none() !!!
0833     int method = ssh_auth_list(mSession);
0834     if (rc != SSH_AUTH_SUCCESS && method == 0) {
0835         return Result::fail(KIO::ERR_CANNOT_LOGIN,
0836                             i18n("Authentication failed. The server "
0837                                  "didn't send any authentication methods"));
0838     }
0839 
0840     // Try to authenticate with public key first
0841     if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PUBLICKEY)) {
0842         qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with public key";
0843         for (;;) {
0844             rc = ssh_userauth_publickey_auto(mSession, nullptr, nullptr);
0845             if (rc == SSH_AUTH_ERROR) {
0846                 qCDebug(KIO_SFTP_LOG) << "Public key authentication failed:" << QString::fromUtf8(ssh_get_error(mSession));
0847                 clearPubKeyAuthInfo();
0848                 return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0849             }
0850             if (rc != SSH_AUTH_DENIED || !mPublicKeyAuthInfo || !mPublicKeyAuthInfo->isModified()) {
0851                 clearPubKeyAuthInfo();
0852                 break;
0853             }
0854         }
0855     }
0856 
0857     // Try to authenticate with GSSAPI
0858     if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_GSSAPI_MIC)) {
0859         qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with GSSAPI";
0860         rc = ssh_userauth_gssapi(mSession);
0861         if (rc == SSH_AUTH_ERROR) {
0862             qCDebug(KIO_SFTP_LOG) << "Public key authentication failed:" << QString::fromUtf8(ssh_get_error(mSession));
0863             return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0864         }
0865     }
0866 
0867     // Try to authenticate with keyboard interactive
0868     if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_INTERACTIVE)) {
0869         qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with keyboard interactive";
0870         AuthInfo info2(info);
0871         rc = authenticateKeyboardInteractive(info2);
0872         if (rc == SSH_AUTH_SUCCESS) {
0873             info = info2;
0874         } else if (rc == SSH_AUTH_ERROR) {
0875             qCDebug(KIO_SFTP_LOG) << "Keyboard interactive authentication failed:" << QString::fromUtf8(ssh_get_error(mSession));
0876             return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0877         }
0878     }
0879 
0880     // Try to authenticate with password
0881     if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PASSWORD)) {
0882         qCDebug(KIO_SFTP_LOG) << "Trying to authenticate with password";
0883 
0884         info.caption = i18n("SFTP Login");
0885         info.prompt = i18n("Please enter your username and password.");
0886         info.comment = info.url.url();
0887         info.commentLabel = i18n("Site:");
0888         bool isFirstLoginAttempt = true;
0889 
0890         for (;;) {
0891             if (!isFirstLoginAttempt || info.password.isEmpty()) {
0892                 info.keepPassword = true; // make the "keep Password" check box visible to the user.
0893                 info.setModified(false);
0894 
0895                 QString username(info.username);
0896                 const QString errMsg(isFirstLoginAttempt ? QString() : i18n("Incorrect username or password"));
0897 
0898                 qCDebug(KIO_SFTP_LOG) << "Username:" << username << "first attempt?" << isFirstLoginAttempt << "error:" << errMsg;
0899 
0900                 // Handle user canceled or dialog failed to open...
0901 
0902                 int errCode = openPasswordDialog(info, errMsg);
0903                 if (errCode != 0) {
0904                     qCDebug(KIO_SFTP_LOG) << "User canceled password/retry dialog";
0905                     return Result::fail(errCode, QString());
0906                 }
0907 
0908                 // If the user name changes, we have to re-establish connection again
0909                 // since the user name must always be set before calling ssh_connect.
0910                 if (wasUsernameChanged(username, info)) {
0911                     qCDebug(KIO_SFTP_LOG) << "Username changed to" << info.username;
0912                     if (!info.url.userName().isEmpty()) {
0913                         info.url.setUserName(info.username);
0914                     }
0915                     const auto result = sftpOpenConnection(info);
0916                     if (!result.success()) {
0917                         return result;
0918                     }
0919                 }
0920             }
0921 
0922             rc = ssh_userauth_password(mSession, info.username.toUtf8().constData(), info.password.toUtf8().constData());
0923             if (rc == SSH_AUTH_SUCCESS) {
0924                 break;
0925             }
0926             if (rc == SSH_AUTH_ERROR) {
0927                 qCDebug(KIO_SFTP_LOG) << "Password authentication failed:" << QString::fromUtf8(ssh_get_error(mSession));
0928                 return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0929             }
0930 
0931             isFirstLoginAttempt = false; // failed attempt to login.
0932             info.password.clear(); // clear the password after failed attempts.
0933         }
0934     }
0935 
0936     // If we're still not authenticated then we need to leave.
0937     if (rc != SSH_AUTH_SUCCESS) {
0938         return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Authentication failed."));
0939     }
0940 
0941     // start sftp session
0942     qCDebug(KIO_SFTP_LOG) << "Trying to request the sftp session";
0943     mSftp = sftp_new(mSession);
0944     if (mSftp == nullptr) {
0945         return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Unable to request the SFTP subsystem. Make sure SFTP is enabled on the server."));
0946     }
0947 
0948     qCDebug(KIO_SFTP_LOG) << "Trying to initialize the sftp session";
0949     if (sftp_init(mSftp) < 0) {
0950         return Result::fail(KIO::ERR_CANNOT_LOGIN, i18n("Could not initialize the SFTP session."));
0951     }
0952 
0953     // Login succeeded!
0954     infoMessage(i18n("Successfully connected to %1", mHost));
0955     if (info.keepPassword) {
0956         qCDebug(KIO_SFTP_LOG) << "Caching info.username = " << info.username << ", info.url = " << info.url.toDisplayString();
0957         cacheAuthentication(info);
0958     }
0959 
0960     // Update the original username in case it was changed!
0961     if (!mUsername.isEmpty()) {
0962         mUsername = info.username;
0963     }
0964 
0965     setTimeoutSpecialCommand(KIO_SFTP_SPECIAL_TIMEOUT_MS);
0966 
0967     mConnected = true;
0968 
0969     info.password.fill('x');
0970     info.password.clear();
0971 
0972     return Result::pass();
0973 }
0974 
0975 Result SFTPWorker::openConnection()
0976 {
0977     const Result result = openConnectionWithoutCloseOnError();
0978     if (!result.success()) {
0979         closeConnection();
0980     }
0981     return result;
0982 }
0983 
0984 void SFTPWorker::closeConnection()
0985 {
0986     qCDebug(KIO_SFTP_LOG);
0987 
0988     if (mSftp) {
0989         sftp_free(mSftp);
0990         mSftp = nullptr;
0991     }
0992 
0993     if (mSession) {
0994         ssh_disconnect(mSession);
0995         ssh_free(mSession);
0996         mSession = nullptr;
0997     }
0998 
0999     mConnected = false;
1000 }
1001 
1002 Result SFTPWorker::special(const QByteArray &)
1003 {
1004     qCDebug(KIO_SFTP_LOG) << "special(): polling";
1005 
1006     if (!mSftp) {
1007         return Result::fail(KIO::ERR_INTERNAL, i18n("Invalid sftp context"));
1008     }
1009 
1010     /*
1011      * ssh_channel_poll() returns the number of bytes that may be read on the
1012      * channel. It does so by checking the input buffer and eventually the
1013      * network socket for data to read. If the input buffer is not empty, it
1014      * will not probe the network (and such not read packets nor reply to
1015      * keepalives).
1016      *
1017      * As ssh_channel_poll can act on two specific buffers (a channel has two
1018      * different stream: stdio and stderr), polling for data on the stderr
1019      * stream has more chance of not being in the problematic case (data left
1020      * in the buffer). Checking the return value (for >0) would be a good idea
1021      * to debug the problem.
1022      */
1023     int rc = ssh_channel_poll(mSftp->channel, 0);
1024     if (rc > 0) {
1025         rc = ssh_channel_poll(mSftp->channel, 1);
1026     }
1027 
1028     if (rc < 0) { // first or second poll failed
1029         qCDebug(KIO_SFTP_LOG) << "ssh_channel_poll failed: "
1030                               << "- SFTP error:" << sftp_get_error(mSftp) << "- SSH error:" << ssh_get_error_code(mSession)
1031                               << "- SSH errorString:" << ssh_get_error(mSession);
1032     }
1033 
1034     setTimeoutSpecialCommand(KIO_SFTP_SPECIAL_TIMEOUT_MS);
1035 
1036     return Result::pass();
1037 }
1038 
1039 Result SFTPWorker::open(const QUrl &url, QIODevice::OpenMode mode)
1040 {
1041     qCDebug(KIO_SFTP_LOG) << "open: " << url;
1042 
1043     const auto loginResult = sftpLogin();
1044     if (!loginResult.success()) {
1045         return loginResult;
1046     }
1047 
1048     const QString path = url.path();
1049     const QByteArray path_c = path.toUtf8();
1050 
1051     SFTPAttributesPtr sb(sftp_lstat(mSftp, path_c.constData()));
1052     if (sb == nullptr) {
1053         return reportError(url, sftp_get_error(mSftp));
1054     }
1055 
1056     switch (sb->type) {
1057     case SSH_FILEXFER_TYPE_DIRECTORY:
1058         return Result::fail(KIO::ERR_IS_DIRECTORY, url.toDisplayString());
1059     case SSH_FILEXFER_TYPE_SPECIAL:
1060     case SSH_FILEXFER_TYPE_UNKNOWN:
1061         return Result::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, url.toDisplayString());
1062     case SSH_FILEXFER_TYPE_SYMLINK:
1063     case SSH_FILEXFER_TYPE_REGULAR:
1064         break;
1065     }
1066 
1067     KIO::filesize_t fileSize = sb->size;
1068 
1069     int flags = 0;
1070 
1071     if (mode & QIODevice::ReadOnly) {
1072         if (mode & QIODevice::WriteOnly) {
1073             flags = O_RDWR | O_CREAT;
1074         } else {
1075             flags = O_RDONLY;
1076         }
1077     } else if (mode & QIODevice::WriteOnly) {
1078         flags = O_WRONLY | O_CREAT;
1079     }
1080 
1081     if (mode & QIODevice::Append) {
1082         flags |= O_APPEND;
1083     } else if (mode & QIODevice::Truncate) {
1084         flags |= O_TRUNC;
1085     }
1086 
1087     if (flags & O_CREAT) {
1088         mOpenFile = sftp_open(mSftp, path_c.constData(), flags, permsToPosix(perms::owner_read | perms::owner_write | perms::group_read | perms::others_read));
1089     } else {
1090         mOpenFile = sftp_open(mSftp, path_c.constData(), flags, 0);
1091     }
1092 
1093     if (mOpenFile == nullptr) {
1094         return Result::fail(toKIOError(sftp_get_error(mSftp)), path);
1095     }
1096 
1097     // Determine the mimetype of the file to be retrieved, and emit it.
1098     // This is mandatory in all workers (for KRun/BrowserRun to work).
1099     // If we're not opening the file ReadOnly or ReadWrite, don't attempt to
1100     // read the file and send the mimetype.
1101     if (mode & QIODevice::ReadOnly) {
1102         if (const Result result = sftpSendMimetype(mOpenFile, mOpenUrl); !result.success()) {
1103             (void)close();
1104             return result;
1105         }
1106     }
1107 
1108     mOpenUrl = url;
1109 
1110     openOffset = 0;
1111     totalSize(fileSize);
1112     position(0);
1113 
1114     return Result::pass();
1115 }
1116 
1117 Result SFTPWorker::read(KIO::filesize_t bytes)
1118 {
1119     qCDebug(KIO_SFTP_LOG) << "read, offset = " << openOffset << ", bytes = " << bytes;
1120 
1121     Q_ASSERT(mOpenFile != nullptr);
1122 
1123     QVarLengthArray<char> buffer(bytes);
1124 
1125     ssize_t bytesRead = sftp_read(mOpenFile, buffer.data(), bytes);
1126     Q_ASSERT(bytesRead <= static_cast<ssize_t>(bytes));
1127 
1128     if (bytesRead < 0) {
1129         qCDebug(KIO_SFTP_LOG) << "Could not read" << mOpenUrl << sftp_get_error(mSftp) << ssh_get_error_code(mSession) << ssh_get_error(mSession);
1130         (void)close();
1131         return Result::fail(KIO::ERR_CANNOT_READ, mOpenUrl.toDisplayString());
1132     }
1133 
1134     const QByteArray fileData = QByteArray::fromRawData(buffer.data(), bytesRead);
1135     data(fileData);
1136 
1137     return Result::pass();
1138 }
1139 
1140 Result SFTPWorker::write(const QByteArray &data)
1141 {
1142     qCDebug(KIO_SFTP_LOG) << "write, offset = " << openOffset << ", bytes = " << data.size();
1143 
1144     Q_ASSERT(mOpenFile != nullptr);
1145 
1146     for (const auto &response : asyncWrite(mOpenFile, [data]() -> QCoro::Generator<ReadResponse> {
1147              co_yield ReadResponse(data);
1148          }())) {
1149         if (response.error != KJob::NoError) {
1150             qCDebug(KIO_SFTP_LOG) << "Could not write to " << mOpenUrl;
1151             (void)close();
1152             return Result::fail(KIO::ERR_CANNOT_WRITE, mOpenUrl.toDisplayString());
1153         }
1154         // We don't care about progress in this function. We'll emit a single written() once done.
1155     }
1156 
1157     written(data.size());
1158 
1159     return Result::pass();
1160 }
1161 
1162 Result SFTPWorker::seek(KIO::filesize_t offset)
1163 {
1164     qCDebug(KIO_SFTP_LOG) << "seek, offset = " << offset;
1165 
1166     Q_ASSERT(mOpenFile != nullptr);
1167 
1168     if (sftp_seek64(mOpenFile, static_cast<uint64_t>(offset)) < 0) {
1169         (void)close();
1170         return Result::fail(KIO::ERR_CANNOT_SEEK, mOpenUrl.path());
1171     }
1172 
1173     position(sftp_tell64(mOpenFile));
1174 
1175     return Result::pass();
1176 }
1177 
1178 Result SFTPWorker::truncate(KIO::filesize_t length)
1179 {
1180     qCDebug(KIO_SFTP_LOG) << "truncate, length =" << length;
1181 
1182     Q_ASSERT(mOpenFile);
1183 
1184     int errorCode = KJob::NoError;
1185     SFTPAttributesPtr attr(sftp_fstat(mOpenFile));
1186     if (attr) {
1187         attr->size = length;
1188         if (sftp_setstat(mSftp, mOpenUrl.path().toUtf8().constData(), attr.get()) == 0) {
1189             truncated(length);
1190         } else {
1191             errorCode = toKIOError(sftp_get_error(mSftp));
1192         }
1193     } else {
1194         errorCode = toKIOError(sftp_get_error(mSftp));
1195     }
1196 
1197     if (errorCode) {
1198         (void)close();
1199         return Result::fail(errorCode == KIO::ERR_INTERNAL ? KIO::ERR_CANNOT_TRUNCATE : errorCode, mOpenUrl.path());
1200     }
1201 
1202     return Result::pass();
1203 }
1204 
1205 Result SFTPWorker::close()
1206 {
1207     if (mOpenFile) {
1208         sftp_close(mOpenFile);
1209     }
1210     mOpenFile = nullptr;
1211     return Result::pass();
1212 }
1213 
1214 Result SFTPWorker::get(const QUrl &url)
1215 {
1216     qCDebug(KIO_SFTP_LOG) << url;
1217 
1218     const auto result = sftpGet(url);
1219     if (!result.success()) {
1220         return Result::fail(result.error(), url.toDisplayString());
1221     }
1222 
1223     return Result::pass();
1224 }
1225 
1226 Result SFTPWorker::sftpSendMimetype(sftp_file file, const QUrl &url)
1227 {
1228     constexpr int readLimit = 1024; // entirely arbitrary
1229     std::array<char, readLimit> mimeTypeBuf{};
1230     const ssize_t bytesRead = sftp_read(file, mimeTypeBuf.data(), readLimit);
1231     if (bytesRead < 0) {
1232         return Result::fail(KIO::ERR_CANNOT_READ, url.toString());
1233     }
1234 
1235     QMimeDatabase db;
1236     const QMimeType mime = db.mimeTypeForFileNameAndData(url.fileName(), QByteArray(mimeTypeBuf.data(), bytesRead));
1237     if (!mime.isDefault()) {
1238         mimeType(mime.name());
1239     } else {
1240         mimeType(db.mimeTypeForUrl(url).name());
1241     }
1242     sftp_rewind(file);
1243 
1244     return Result::pass();
1245 }
1246 
1247 Result SFTPWorker::sftpGet(const QUrl &url, KIO::fileoffset_t offset, int fd)
1248 {
1249     qCDebug(KIO_SFTP_LOG) << url;
1250 
1251     const auto loginResult = sftpLogin();
1252     if (!loginResult.success()) {
1253         return loginResult;
1254     }
1255 
1256     QByteArray path = url.path().toUtf8();
1257 
1258     KIO::filesize_t totalbytesread = 0;
1259 
1260     SFTPAttributesPtr sb(sftp_lstat(mSftp, path.constData()));
1261     if (sb == nullptr) {
1262         return Result::fail(toKIOError(sftp_get_error(mSftp)), url.toString());
1263     }
1264 
1265     switch (sb->type) {
1266     case SSH_FILEXFER_TYPE_DIRECTORY:
1267         return Result::fail(KIO::ERR_IS_DIRECTORY, url.toString());
1268     case SSH_FILEXFER_TYPE_SPECIAL:
1269     case SSH_FILEXFER_TYPE_UNKNOWN:
1270         return Result::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, url.toString());
1271     case SSH_FILEXFER_TYPE_SYMLINK:
1272     case SSH_FILEXFER_TYPE_REGULAR:
1273         break;
1274     }
1275 
1276     // Open file
1277     const UniqueSFTPFilePtr file(sftp_open(mSftp, path.constData(), O_RDONLY, 0));
1278     if (!file) {
1279         return Result::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, url.toString());
1280     }
1281 
1282     if (const Result result = sftpSendMimetype(file.get(), url); !result.success()) {
1283         return result;
1284     }
1285 
1286     // Set the total size
1287     totalSize(sb->size);
1288 
1289     // If offset is not specified, check the "resume" meta-data.
1290     if (offset < 0) {
1291         const QString resumeOffsetStr = metaData(QLatin1String("resume"));
1292         if (!resumeOffsetStr.isEmpty()) {
1293             bool ok = false;
1294             qlonglong resumeOffset = resumeOffsetStr.toLongLong(&ok);
1295             if (ok) {
1296                 offset = resumeOffset;
1297             }
1298         }
1299     }
1300 
1301     // If we can resume, offset the buffer properly.
1302     if (offset > 0 && ((unsigned long long)offset < sb->size)) {
1303         if (sftp_seek64(file.get(), offset) == 0) {
1304             canResume();
1305             totalbytesread = offset;
1306             qCDebug(KIO_SFTP_LOG) << "Resume offset: " << QString::number(offset);
1307         }
1308     }
1309 
1310     auto reader = asyncRead(file.get(), sb->size);
1311     for (const auto &response : reader) {
1312         if (response.error != KJob::NoError) {
1313             return Result::fail(response.error, url.toString());
1314         }
1315 
1316         if (fd == -1) {
1317             data(response.filedata);
1318         } else if (int error = writeToFile(fd, response.filedata); error != KJob::NoError) {
1319             return Result::fail(error, url.toString());
1320         }
1321 
1322         // increment total bytes read
1323         totalbytesread += response.filedata.length();
1324         processedSize(totalbytesread);
1325     }
1326 
1327     if (fd == -1) {
1328         data(QByteArray());
1329     }
1330 
1331     processedSize(static_cast<KIO::filesize_t>(sb->size));
1332     return Result::pass();
1333 }
1334 
1335 Result SFTPWorker::put(const QUrl &url, int permissions, KIO::JobFlags flags)
1336 {
1337     qCDebug(KIO_SFTP_LOG) << url << ", permissions =" << permissions << ", overwrite =" << (flags & KIO::Overwrite) << ", resume =" << (flags & KIO::Resume);
1338 
1339     qCDebug(KIO_SFTP_LOG) << url;
1340 
1341     return sftpPut(url, permissions, flags);
1342 }
1343 
1344 Result SFTPWorker::sftpPut(const QUrl &url, int permissionsMode, JobFlags flags, int fd)
1345 {
1346     qCDebug(KIO_SFTP_LOG) << url << ", permissions =" << permissionsMode << ", overwrite =" << (flags & KIO::Overwrite)
1347                           << ", resume =" << (flags & KIO::Resume);
1348 
1349     auto permissions(posixToOptionalPerms(permissionsMode));
1350 
1351     const auto loginResult = sftpLogin();
1352     if (!loginResult.success()) {
1353         return loginResult;
1354     }
1355 
1356     const QString dest_orig = url.path();
1357     const QByteArray dest_orig_c = dest_orig.toUtf8();
1358     const QString dest_part = dest_orig + ".part";
1359     const QByteArray dest_part_c = dest_part.toUtf8();
1360     uid_t owner = 0;
1361     gid_t group = 0;
1362 
1363     SFTPAttributesPtr sb(sftp_lstat(mSftp, dest_orig_c.constData()));
1364     const bool bOrigExists = (sb != nullptr);
1365     bool bPartExists = false;
1366     const bool bMarkPartial = configValue(QStringLiteral("MarkPartial"), true);
1367 
1368     // Don't change permissions of the original file
1369     if (bOrigExists) {
1370         permissions = posixToOptionalPerms(sb->permissions);
1371         owner = sb->uid;
1372         group = sb->gid;
1373     }
1374 
1375     if (bMarkPartial) {
1376         SFTPAttributesPtr sbPart(sftp_lstat(mSftp, dest_part_c.constData()));
1377         bPartExists = (sbPart != nullptr);
1378 
1379         if (bPartExists && !(flags & KIO::Resume) && !(flags & KIO::Overwrite) && sbPart->size > 0 && sbPart->type == SSH_FILEXFER_TYPE_REGULAR) {
1380             if (fd == -1) {
1381                 // Maybe we can use this partial file for resuming
1382                 // Tell about the size we have, and the app will tell us
1383                 // if it's ok to resume or not.
1384                 qCDebug(KIO_SFTP_LOG) << "calling canResume with " << sbPart->size;
1385                 flags |= canResume(sbPart->size) ? KIO::Resume : KIO::DefaultFlags;
1386                 qCDebug(KIO_SFTP_LOG) << "put got answer " << (flags & KIO::Resume);
1387 
1388             } else {
1389                 KIO::filesize_t pos = QT_LSEEK(fd, sbPart->size, SEEK_SET);
1390                 if (pos != sbPart->size) {
1391                     qCDebug(KIO_SFTP_LOG) << "Failed to seek to" << sbPart->size << "bytes in source file. Reason given:" << strerror(errno);
1392                     return Result::fail(ERR_CANNOT_SEEK, url.toString());
1393                 }
1394                 flags |= KIO::Resume;
1395             }
1396             qCDebug(KIO_SFTP_LOG) << "Resuming at" << sbPart->size;
1397         }
1398     }
1399 
1400     if (bOrigExists && !(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
1401         const int error = KSFTP_ISDIR(sb) ? KIO::ERR_DIR_ALREADY_EXIST : KIO::ERR_FILE_ALREADY_EXIST;
1402         return Result::fail(error, url.toString());
1403     }
1404 
1405     QByteArray dest;
1406     if (bMarkPartial) {
1407         qCDebug(KIO_SFTP_LOG) << "Appending .part extension to" << dest_orig;
1408         dest = dest_part_c;
1409         if (bPartExists && !(flags & KIO::Resume)) {
1410             qCDebug(KIO_SFTP_LOG) << "Deleting partial file" << dest_part;
1411             sftp_unlink(mSftp, dest_part_c.constData());
1412             // Catch errors when we try to open the file.
1413         }
1414     } else {
1415         dest = dest_orig_c; // Will be automatically truncated below...
1416     } // bMarkPartial
1417 
1418     UniqueSFTPFilePtr destFile{};
1419     KIO::fileoffset_t totalBytesSent = 0;
1420     if ((flags & KIO::Resume)) {
1421         qCDebug(KIO_SFTP_LOG) << "Trying to append: " << dest;
1422         destFile.reset(sftp_open(mSftp, dest.constData(), O_RDWR, 0)); // append if resuming
1423         if (destFile) {
1424             SFTPAttributesPtr fstat(sftp_fstat(destFile.get()));
1425             if (fstat) {
1426                 sftp_seek64(destFile.get(), fstat->size); // Seek to end TODO
1427                 totalBytesSent += fstat->size;
1428             }
1429         }
1430     } else {
1431         perms initialMode = perms::owner_read | perms::owner_write | perms::group_read | perms::others_read;
1432         if (permissions.has_value()) {
1433             initialMode = permissions.value() | perms::owner_write | perms::owner_read;
1434         }
1435 
1436         qCDebug(KIO_SFTP_LOG) << "Trying to open:" << QString(dest) << ", mode=" << QString::number(permsToPosix(initialMode));
1437         destFile.reset(sftp_open(mSftp, dest.constData(), O_CREAT | O_TRUNC | O_WRONLY, permsToPosix(initialMode)));
1438     } // flags & KIO::Resume
1439 
1440     auto closeOnError = [&destFile, bMarkPartial, this, dest, url](int errorCode) -> Result {
1441         qCDebug(KIO_SFTP_LOG) << "Error during 'put'. Aborting.";
1442 
1443         if (destFile != nullptr) {
1444             destFile = nullptr; // close to force a data flush before we stat
1445 
1446             SFTPAttributesPtr attr(sftp_stat(mSftp, dest.constData()));
1447             if (bMarkPartial && attr != nullptr) {
1448                 size_t size = configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE);
1449                 if (attr->size < size) {
1450                     sftp_unlink(mSftp, dest.constData());
1451                 }
1452             }
1453         }
1454 
1455         return errorCode == KJob::NoError ? Result::pass() : Result::fail(errorCode, url.toString());
1456     };
1457 
1458     if (destFile == nullptr) {
1459         qCDebug(KIO_SFTP_LOG) << "COULD NOT WRITE " << QString(dest) << ", permissions=" << permsToPosix(permissions.value_or(perms::none))
1460                               << ", error=" << ssh_get_error(mSession);
1461         if (sftp_get_error(mSftp) == SSH_FX_PERMISSION_DENIED) {
1462             return closeOnError(KIO::ERR_WRITE_ACCESS_DENIED);
1463         }
1464         return closeOnError(KIO::ERR_CANNOT_OPEN_FOR_WRITING);
1465     } // file
1466 
1467     auto reader = [fd, this]() -> QCoro::Generator<ReadResponse> {
1468         for (int result = 1; result > 0;) {
1469             ReadResponse response;
1470             if (fd == -1) {
1471                 dataReq(); // Request for data
1472                 result = readData(response.filedata);
1473                 if (result < 0) {
1474                     qCDebug(KIO_SFTP_LOG) << "unexpected error during readData";
1475                 }
1476             } else {
1477                 std::array<char, MAX_XFER_BUF_SIZE> buf{};
1478                 result = ::read(fd, buf.data(), buf.size());
1479                 if (result < 0) {
1480                     qCDebug(KIO_SFTP_LOG) << "failed to read" << errno;
1481                     response.error = ERR_CANNOT_READ;
1482                 } else {
1483                     response.filedata = QByteArray(buf.data(), result);
1484                 }
1485             }
1486 
1487             if (result == 0) {
1488                 // proftpd stumbles over zero size writes.
1489                 // https://bugs.kde.org/show_bug.cgi?id=419999
1490                 // http://bugs.proftpd.org/show_bug.cgi?id=4398
1491                 // At this point we'll have opened the file and thus created it.
1492                 // It's safe to break here as even in the ideal scenario that the server
1493                 // doesn't fall over, the write code is pointless because zero size writes
1494                 // do absolutely nothing.
1495                 break;
1496             }
1497 
1498             co_yield response;
1499         }
1500     };
1501 
1502     for (const auto &response : asyncWrite(destFile.get(), reader())) {
1503         if (response.error != KJob::NoError) {
1504             qCDebug(KIO_SFTP_LOG) << "totalBytesSent at error:" << totalBytesSent;
1505             return closeOnError(KIO::ERR_CANNOT_WRITE);
1506         }
1507 
1508         totalBytesSent += response.bytes;
1509         processedSize(totalBytesSent);
1510     }
1511 
1512     if (destFile == nullptr) { // we got nothing to write out, so we never opened the file
1513         return Result::pass();
1514     }
1515 
1516     if (sftp_close(destFile.release()) < 0) {
1517         qCWarning(KIO_SFTP_LOG) << "Error when closing file descriptor";
1518         return Result::fail(KIO::ERR_CANNOT_WRITE, url.toString());
1519     }
1520 
1521     // after full download rename the file back to original name
1522     if (bMarkPartial) {
1523         // If the original URL is a symlink and we were asked to overwrite it,
1524         // remove the symlink first. This ensures that we do not overwrite the
1525         // current source if the symlink points to it.
1526         if ((flags & KIO::Overwrite)) {
1527             sftp_unlink(mSftp, dest_orig_c.constData());
1528         }
1529 
1530         if (sftp_rename(mSftp, dest.constData(), dest_orig_c.constData()) < 0) {
1531             qCWarning(KIO_SFTP_LOG) << " Couldn't rename " << dest << " to " << dest_orig;
1532             return Result::fail(ERR_CANNOT_RENAME_PARTIAL, url.toString());
1533         }
1534     }
1535 
1536     // set final permissions
1537     if (permissions.has_value() && !(flags & KIO::Resume)) {
1538         qCDebug(KIO_SFTP_LOG) << "Trying to set final permissions of " << dest_orig << " to " << QString::number(permsToPosix(permissions.value()));
1539         if (sftp_chmod(mSftp, dest_orig_c.constData(), permsToPosix(permissions.value())) < 0) {
1540             warning(i18n("Could not change permissions for\n%1", url.toString()));
1541             return Result::pass();
1542         }
1543     }
1544 
1545     // set original owner and group
1546     if (bOrigExists) {
1547         qCDebug(KIO_SFTP_LOG) << "Trying to restore original owner and group of " << dest_orig;
1548         if (sftp_chown(mSftp, dest_orig_c.constData(), owner, group) < 0) {
1549             qCWarning(KIO_SFTP_LOG) << "Could not change owner and group for" << dest_orig;
1550             // warning(i18n( "Could not change owner and group for\n%1", dest_orig));
1551         }
1552     }
1553 
1554     // set modification time
1555     const QString mtimeStr = metaData("modified");
1556     if (!mtimeStr.isEmpty()) {
1557         QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
1558         if (dt.isValid()) {
1559             std::array<struct timeval, 2> times{};
1560 
1561             SFTPAttributesPtr attr(sftp_lstat(mSftp, dest_orig_c.constData()));
1562             if (attr != nullptr) {
1563                 times[0].tv_sec = attr->atime; //// access time, unchanged
1564                 times[1].tv_sec = dt.toSecsSinceEpoch(); // modification time
1565                 times[0].tv_usec = times[1].tv_usec = 0;
1566 
1567                 qCDebug(KIO_SFTP_LOG) << "Trying to restore mtime for " << dest_orig << " to: " << mtimeStr;
1568                 int result = sftp_utimes(mSftp, dest_orig_c.constData(), times.data());
1569                 if (result < 0) {
1570                     qCWarning(KIO_SFTP_LOG) << "Failed to set mtime for" << dest_orig;
1571                 }
1572             }
1573         }
1574     }
1575 
1576     return Result::pass();
1577 }
1578 
1579 Result SFTPWorker::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags)
1580 {
1581     qCDebug(KIO_SFTP_LOG) << src << " -> " << dest << " , permissions = " << QString::number(permissions) << ", overwrite = " << (flags & KIO::Overwrite)
1582                           << ", resume = " << (flags & KIO::Resume);
1583 
1584     const bool isSourceLocal = src.isLocalFile();
1585     const bool isDestinationLocal = dest.isLocalFile();
1586 
1587     if (!isSourceLocal && isDestinationLocal) { // sftp -> file
1588         return sftpCopyGet(src, dest.toLocalFile(), permissions, flags);
1589     }
1590     if (isSourceLocal && !isDestinationLocal) { // file -> sftp
1591         return sftpCopyPut(dest, src.toLocalFile(), permissions, flags);
1592     }
1593 
1594     return Result::fail(ERR_UNSUPPORTED_ACTION);
1595 }
1596 
1597 Result SFTPWorker::sftpCopyGet(const QUrl &url, const QString &sCopyFile, int permissionsMode, KIO::JobFlags flags)
1598 {
1599     qCDebug(KIO_SFTP_LOG) << url << "->" << sCopyFile << ", permissions=" << permissionsMode;
1600 
1601     auto permissions = posixToOptionalPerms(permissionsMode);
1602 
1603     // check if destination is ok ...
1604     QFileInfo copyFile(sCopyFile);
1605     const bool bDestExists = copyFile.exists();
1606     if (bDestExists) {
1607         if (copyFile.isDir()) {
1608             return Result::fail(ERR_IS_DIRECTORY, sCopyFile);
1609         }
1610 
1611         if (!(flags & KIO::Overwrite)) {
1612             return Result::fail(ERR_FILE_ALREADY_EXIST, sCopyFile);
1613         }
1614     }
1615 
1616     bool bResume = false;
1617     const QString sPart = sCopyFile + QLatin1String(".part"); // do we have a ".part" file?
1618     QFileInfo partFile(sPart);
1619     const bool bPartExists = partFile.exists();
1620     const bool bMarkPartial = configValue(QStringLiteral("MarkPartial"), true);
1621     const QString dest = (bMarkPartial ? sPart : sCopyFile);
1622 
1623     if (bMarkPartial && bPartExists) {
1624         if (partFile.isDir()) {
1625             return Result::fail(ERR_FILE_ALREADY_EXIST, sCopyFile);
1626         }
1627         if (partFile.size() > 0) {
1628             bResume = canResume(copyFile.size());
1629         }
1630     }
1631 
1632     if (bPartExists && !bResume) { // get rid of an unwanted ".part" file
1633         QFile::remove(sPart);
1634     }
1635 
1636     // WABA: Make sure that we keep writing permissions ourselves,
1637     // otherwise we can be in for a surprise on NFS.
1638     perms initialMode = perms::owner_read | perms::owner_write | perms::group_read | perms::group_write | perms::others_read | perms::others_write;
1639     if (permissions.has_value()) {
1640         initialMode = permissions.value() | perms::owner_write;
1641     }
1642 
1643     // open the output file ...
1644     int fd = -1;
1645     KIO::fileoffset_t offset = 0;
1646     if (bResume) {
1647         fd = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming
1648         offset = QT_LSEEK(fd, partFile.size(), SEEK_SET);
1649         if (offset != partFile.size()) {
1650             qCDebug(KIO_SFTP_LOG) << "Failed to seek to" << partFile.size() << "bytes in target file. Reason given:" << strerror(errno);
1651             ::close(fd);
1652             return Result::fail(ERR_CANNOT_RESUME, sCopyFile);
1653         }
1654         qCDebug(KIO_SFTP_LOG) << "resuming at" << offset;
1655     } else {
1656         fd = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, permsToPosix(initialMode));
1657     }
1658 
1659     if (fd == -1) {
1660         qCDebug(KIO_SFTP_LOG) << "could not write to" << sCopyFile;
1661         return Result::fail((errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING, sCopyFile);
1662     }
1663 
1664     const auto getResult = sftpGet(url, offset, fd);
1665 
1666     // The following is a bit awkward, we'll override the internal error
1667     // in cleanup conditions, so these are subject to change until we return.
1668     int errorCode = getResult.error();
1669     QString errorString = getResult.errorString();
1670 
1671     if (::close(fd) && getResult.success()) {
1672         errorCode = ERR_CANNOT_WRITE;
1673         errorString = sCopyFile;
1674     }
1675 
1676     // handle renaming or deletion of a partial file ...
1677     if (bMarkPartial) {
1678         if (getResult.success()) { // rename ".part" on success
1679             if (!QFile::rename(sPart, sCopyFile)) {
1680                 // If rename fails, try removing the destination first if it exists.
1681                 if (!bDestExists || !QFile::remove(sCopyFile) || !QFile::rename(sPart, sCopyFile)) {
1682                     qCDebug(KIO_SFTP_LOG) << "cannot rename " << sPart << " to " << sCopyFile;
1683                     errorCode = ERR_CANNOT_RENAME_PARTIAL;
1684                     errorString = sCopyFile;
1685                 }
1686             }
1687         } else {
1688             partFile.refresh();
1689             const int size = configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE);
1690             if (partFile.exists() && partFile.size() < size) { // should a very small ".part" be deleted?
1691                 QFile::remove(sPart);
1692             }
1693         }
1694     }
1695 
1696     const QString mtimeStr = metaData("modified");
1697     if (!mtimeStr.isEmpty()) {
1698         QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
1699         if (dt.isValid()) {
1700             QFile receivedFile(sCopyFile);
1701             if (receivedFile.exists()) {
1702                 if (!receivedFile.open(QIODevice::ReadWrite | QIODevice::Text)) {
1703                     QString error_msg = receivedFile.errorString();
1704                     qCDebug(KIO_SFTP_LOG) << "Couldn't update modified time : " << error_msg;
1705                 } else {
1706                     receivedFile.setFileTime(dt, QFileDevice::FileModificationTime);
1707                 }
1708             }
1709         }
1710     }
1711 
1712     return errorCode == KJob::NoError ? Result::pass() : Result::fail(errorCode, errorString);
1713 }
1714 
1715 Result SFTPWorker::sftpCopyPut(const QUrl &url, const QString &sCopyFile, int permissions, JobFlags flags)
1716 {
1717     qCDebug(KIO_SFTP_LOG) << sCopyFile << "->" << url << ", permissions=" << permissions << ", flags" << flags;
1718 
1719     // check if source is ok ...
1720     QFileInfo copyFile(sCopyFile);
1721     bool bSrcExists = copyFile.exists();
1722     if (bSrcExists) {
1723         if (copyFile.isDir()) {
1724             return Result::fail(ERR_IS_DIRECTORY, sCopyFile);
1725         }
1726     } else {
1727         return Result::fail(ERR_DOES_NOT_EXIST, sCopyFile);
1728     }
1729 
1730     const int fd = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY);
1731     if (fd == -1) {
1732         return Result::fail(ERR_CANNOT_OPEN_FOR_READING, sCopyFile);
1733     }
1734 
1735     totalSize(copyFile.size());
1736 
1737     // delegate the real work (errorCode gets status) ...
1738     const auto result = sftpPut(url, permissions, flags, fd);
1739     ::close(fd);
1740     return result;
1741 }
1742 
1743 Result SFTPWorker::stat(const QUrl &url)
1744 {
1745     qCDebug(KIO_SFTP_LOG) << url;
1746 
1747     const auto loginResult = sftpLogin();
1748     if (!loginResult.success()) {
1749         return loginResult;
1750     }
1751 
1752     if (url.path().isEmpty() || QDir::isRelativePath(url.path()) || url.path().contains("/./") || url.path().contains("/../")) {
1753         QString cPath;
1754 
1755         if (!url.path().isEmpty()) {
1756             cPath = canonicalizePath(url.path());
1757         } else {
1758             cPath = canonicalizePath(QLatin1String("."));
1759         }
1760 
1761         if (cPath.isEmpty()) {
1762             return Result::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
1763         }
1764         QUrl redir(url);
1765         redir.setPath(cPath);
1766         redirection(redir);
1767 
1768         qCDebug(KIO_SFTP_LOG) << "redirecting to " << redir.url();
1769 
1770         return Result::pass();
1771     }
1772 
1773     QByteArray path = url.path().toUtf8();
1774 
1775     const QString sDetails = metaData(QLatin1String("details"));
1776     const int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
1777 
1778     sftp_attributes_struct *attributes = sftp_lstat(mSftp, path.constData());
1779     if (attributes == nullptr) {
1780         return Result::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
1781     }
1782 
1783     UDSEntry entry;
1784     const Result result = createUDSEntry(SFTPAttributesPtr(attributes), entry, path, QFileInfo(path).fileName(), details);
1785     if (!result.success()) {
1786         return result;
1787     }
1788     statEntry(entry);
1789 
1790     return Result::pass();
1791 }
1792 
1793 Result SFTPWorker::mimetype(const QUrl &url)
1794 {
1795     qCDebug(KIO_SFTP_LOG) << url;
1796 
1797     const auto loginResult = sftpLogin();
1798     if (!loginResult.success()) {
1799         return loginResult;
1800     }
1801 
1802     // open() feeds the mimetype
1803     const auto result = open(url, QIODevice::ReadOnly);
1804     (void)close();
1805 
1806     return result;
1807 }
1808 
1809 Result SFTPWorker::listDir(const QUrl &url)
1810 {
1811     qCDebug(KIO_SFTP_LOG) << "list directory: " << url;
1812 
1813     const auto loginResult = sftpLogin();
1814     if (!loginResult.success()) {
1815         return loginResult;
1816     }
1817 
1818     if (url.path().isEmpty() || QDir::isRelativePath(url.path()) || url.path().contains("/./") || url.path().contains("/../")) {
1819         QString cPath;
1820 
1821         if (!url.path().isEmpty()) {
1822             cPath = canonicalizePath(url.path());
1823         } else {
1824             cPath = canonicalizePath(QStringLiteral("."));
1825         }
1826 
1827         if (cPath.isEmpty()) {
1828             return Result::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
1829         }
1830         QUrl redir(url);
1831         redir.setPath(cPath);
1832         redirection(redir);
1833 
1834         qCDebug(KIO_SFTP_LOG) << "redirecting to " << redir.url();
1835 
1836         return Result::pass();
1837     }
1838 
1839     QByteArray path = url.path().toUtf8();
1840 
1841     sftp_dir dp = sftp_opendir(mSftp, path.constData());
1842     if (dp == nullptr) {
1843         return reportError(url, sftp_get_error(mSftp));
1844     }
1845 
1846     const QString sDetails = metaData(QLatin1String("details"));
1847     const int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
1848 
1849     qCDebug(KIO_SFTP_LOG) << "readdir: " << path << ", details: " << QString::number(details);
1850 
1851     UDSEntry entry; // internally this is backed by a heap'd Private, no need allocating that repeatedly
1852     for (;;) {
1853         sftp_attributes_struct *attributes = sftp_readdir(mSftp, dp);
1854         if (attributes == nullptr) {
1855             break;
1856         }
1857 
1858         const QByteArray name = QFile::decodeName(attributes->name).toUtf8();
1859         const QByteArray filePath = path + '/' + name;
1860         const Result result = createUDSEntry(SFTPAttributesPtr(attributes), entry, filePath, QString::fromUtf8(name), details);
1861         if (!result.success()) {
1862             // Failing to list one entry in a directory is not a fatal problem. Log the problem and move on.
1863             qCWarning(KIO_SFTP_LOG) << result.error() << result.errorString();
1864             continue;
1865         }
1866 
1867         listEntry(entry);
1868     } // for ever
1869     sftp_closedir(dp);
1870     return Result::pass();
1871 }
1872 
1873 Result SFTPWorker::mkdir(const QUrl &url, int permissions)
1874 {
1875     qCDebug(KIO_SFTP_LOG) << "create directory: " << url;
1876 
1877     const auto loginResult = sftpLogin();
1878     if (!loginResult.success()) {
1879         return loginResult;
1880     }
1881 
1882     if (url.path().isEmpty()) {
1883         return Result::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
1884     }
1885     const QString path = url.path();
1886     const QByteArray path_c = path.toUtf8();
1887 
1888     // Remove existing file or symlink, if requested.
1889     if (metaData(QLatin1String("overwrite")) == QLatin1String("true")) {
1890         qCDebug(KIO_SFTP_LOG) << "overwrite set, remove existing file or symlink: " << url;
1891         sftp_unlink(mSftp, path_c.constData());
1892     }
1893 
1894     qCDebug(KIO_SFTP_LOG) << "Trying to create directory: " << path;
1895     SFTPAttributesPtr sb(sftp_lstat(mSftp, path_c.constData()));
1896     if (sb == nullptr) {
1897         if (sftp_mkdir(mSftp, path_c.constData(), 0777) < 0) {
1898             return reportError(url, sftp_get_error(mSftp));
1899         }
1900 
1901         qCDebug(KIO_SFTP_LOG) << "Successfully created directory: " << url;
1902         if (permissions != -1) {
1903             const auto result = chmod(url, permissions);
1904             if (!result.success()) {
1905                 return result;
1906             }
1907         }
1908 
1909         return Result::pass();
1910     }
1911 
1912     auto err = KSFTP_ISDIR(sb) ? KIO::ERR_DIR_ALREADY_EXIST : KIO::ERR_FILE_ALREADY_EXIST;
1913     return Result::fail(err, path);
1914 }
1915 
1916 Result SFTPWorker::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
1917 {
1918     qCDebug(KIO_SFTP_LOG) << "rename " << src << " to " << dest << flags;
1919 
1920     const auto loginResult = sftpLogin();
1921     if (!loginResult.success()) {
1922         return loginResult;
1923     }
1924 
1925     QByteArray qsrc = src.path().toUtf8();
1926     QByteArray qdest = dest.path().toUtf8();
1927 
1928     SFTPAttributesPtr sb(sftp_lstat(mSftp, qdest.constData()));
1929     if (sb != nullptr) {
1930         const bool isDir = KSFTP_ISDIR(sb);
1931         if (!(flags & KIO::Overwrite)) {
1932             return Result::fail(isDir ? KIO::ERR_DIR_ALREADY_EXIST : KIO::ERR_FILE_ALREADY_EXIST, dest.url());
1933         }
1934 
1935         // Delete the existing destination file/dir...
1936         if (isDir) {
1937             if (sftp_rmdir(mSftp, qdest.constData()) < 0) {
1938                 return reportError(dest, sftp_get_error(mSftp));
1939             }
1940         } else {
1941             if (sftp_unlink(mSftp, qdest.constData()) < 0) {
1942                 return reportError(dest, sftp_get_error(mSftp));
1943             }
1944         }
1945     }
1946 
1947     if (sftp_rename(mSftp, qsrc.constData(), qdest.constData()) < 0) {
1948         return reportError(dest, sftp_get_error(mSftp));
1949     }
1950 
1951     return Result::pass();
1952 }
1953 
1954 Result SFTPWorker::symlink(const QString &target, const QUrl &dest, KIO::JobFlags flags)
1955 {
1956     qCDebug(KIO_SFTP_LOG) << "link " << target << "->" << dest << ", overwrite = " << (flags & KIO::Overwrite) << ", resume = " << (flags & KIO::Resume);
1957 
1958     const auto loginResult = sftpLogin();
1959     if (!loginResult.success()) {
1960         return loginResult;
1961     }
1962 
1963     QByteArray t = target.toUtf8();
1964     QByteArray d = dest.path().toUtf8();
1965 
1966     bool failed = false;
1967     if (sftp_symlink(mSftp, t.constData(), d.constData()) < 0) {
1968         if (flags == KIO::Overwrite) {
1969             SFTPAttributesPtr sb(sftp_lstat(mSftp, d.constData()));
1970             if (sb == nullptr) {
1971                 failed = true;
1972             } else {
1973                 if (sftp_unlink(mSftp, d.constData()) < 0) {
1974                     failed = true;
1975                 } else {
1976                     if (sftp_symlink(mSftp, t.constData(), d.constData()) < 0) {
1977                         failed = true;
1978                     }
1979                 }
1980             }
1981         }
1982     }
1983 
1984     if (failed) {
1985         return reportError(dest, sftp_get_error(mSftp));
1986     }
1987 
1988     return Result::pass();
1989 }
1990 
1991 Result SFTPWorker::chmod(const QUrl &url, int permissions)
1992 {
1993     qCDebug(KIO_SFTP_LOG) << "change permission of " << url << " to " << QString::number(permissions);
1994 
1995     const auto loginResult = sftpLogin();
1996     if (!loginResult.success()) {
1997         return loginResult;
1998     }
1999 
2000     QByteArray path = url.path().toUtf8();
2001 
2002     if (sftp_chmod(mSftp, path.constData(), permissions) < 0) {
2003         return reportError(url, sftp_get_error(mSftp));
2004     }
2005 
2006     return Result::pass();
2007 }
2008 
2009 Result SFTPWorker::del(const QUrl &url, bool isfile)
2010 {
2011     qCDebug(KIO_SFTP_LOG) << "deleting " << (isfile ? "file: " : "directory: ") << url;
2012 
2013     const auto loginResult = sftpLogin();
2014     if (!loginResult.success()) {
2015         return loginResult;
2016     }
2017 
2018     QByteArray path = url.path().toUtf8();
2019 
2020     if (isfile) {
2021         if (sftp_unlink(mSftp, path.constData()) < 0) {
2022             return reportError(url, sftp_get_error(mSftp));
2023         }
2024     } else {
2025         if (sftp_rmdir(mSftp, path.constData()) < 0) {
2026             return reportError(url, sftp_get_error(mSftp));
2027         }
2028     }
2029 
2030     return Result::pass();
2031 }
2032 
2033 void SFTPWorker::worker_status()
2034 {
2035     qCDebug(KIO_SFTP_LOG) << "connected to " << mHost << "?: " << mConnected;
2036     workerStatus((mConnected ? mHost : QString()), mConnected);
2037 }
2038 
2039 SFTPWorker::GetRequest::GetRequest(sftp_file file, uint64_t size, ushort maxPendingRequests)
2040     : m_file(file)
2041     , m_size(size)
2042     , m_maxPendingRequests(maxPendingRequests)
2043 {
2044 }
2045 
2046 bool SFTPWorker::GetRequest::enqueueChunks()
2047 {
2048     SFTPWorker::GetRequest::Request request;
2049 
2050     qCDebug(KIO_SFTP_TRACE_LOG) << "enqueueChunks";
2051 
2052     while (m_pendingRequests.count() < m_maxPendingRequests) {
2053         request.expectedLength = MAX_XFER_BUF_SIZE;
2054         request.startOffset = m_file->offset;
2055         request.id = sftp_async_read_begin(m_file, request.expectedLength);
2056         if (request.id < 0) {
2057             if (m_pendingRequests.isEmpty()) {
2058                 return false;
2059             }
2060             break;
2061         }
2062 
2063         m_pendingRequests.enqueue(request);
2064 
2065         if (m_file->offset >= m_size) {
2066             // Do not add any more chunks if the offset is larger than the given file size.
2067             // However this is done after adding a request as the remote file size may
2068             // have changed in the meantime.
2069             break;
2070         }
2071     }
2072 
2073     qCDebug(KIO_SFTP_TRACE_LOG) << "enqueueChunks done" << QString::number(m_pendingRequests.size());
2074 
2075     return true;
2076 }
2077 
2078 int SFTPWorker::GetRequest::readChunks(QByteArray &data)
2079 {
2080     int totalRead = 0;
2081     ssize_t bytesread = 0;
2082     const uint64_t initialOffset = m_file->offset;
2083 
2084     while (!m_pendingRequests.isEmpty()) {
2085         SFTPWorker::GetRequest::Request &request = m_pendingRequests.head();
2086         int dataSize = data.size() + request.expectedLength;
2087 
2088         data.resize(dataSize);
2089         if (data.size() < dataSize) {
2090             // Could not allocate enough memory - skip current chunk
2091             data.resize(dataSize - request.expectedLength);
2092             break;
2093         }
2094 
2095         bytesread = sftp_async_read(m_file, data.data() + totalRead, request.expectedLength, request.id);
2096 
2097         // qCDebug(KIO_SFTP_LOG) << "bytesread=" << QString::number(bytesread);
2098 
2099         if (bytesread == 0 || bytesread == SSH_AGAIN) {
2100             // Done reading or timeout
2101             data.resize(data.size() - request.expectedLength);
2102 
2103             if (bytesread == 0) {
2104                 m_pendingRequests.dequeue(); // This frees QByteArray &data!
2105             }
2106 
2107             break;
2108         }
2109         if (bytesread == SSH_ERROR) {
2110             return -1;
2111         }
2112 
2113         totalRead += bytesread;
2114 
2115         if (bytesread < request.expectedLength) {
2116             int rc = -1;
2117 
2118             // If less data is read than expected - requeue the request
2119             data.resize(data.size() - (request.expectedLength - bytesread));
2120 
2121             // Modify current request
2122             request.expectedLength -= bytesread;
2123             request.startOffset += bytesread;
2124 
2125             rc = sftp_seek64(m_file, request.startOffset);
2126             if (rc < 0) {
2127                 // Failed to continue reading
2128                 return -1;
2129             }
2130 
2131             request.id = sftp_async_read_begin(m_file, request.expectedLength);
2132 
2133             if (request.id < 0) {
2134                 // Failed to dispatch re-request
2135                 return -1;
2136             }
2137 
2138             // Move the offset back to where it was before the read.
2139             // The way this works is that originally the offset is at the maximum of all pending requests,
2140             // read then reduces that by the amount that it came up short, we then seek to where the short request
2141             // left off and make another request for the remaining data. After that we need to move the offset
2142             // back to the original value - without the reduction because we re-requested the missing data!
2143             rc = sftp_seek64(m_file, initialOffset);
2144             if (rc < 0) {
2145                 // Failed to continue reading
2146                 return -1;
2147             }
2148 
2149             return totalRead;
2150         }
2151 
2152         m_pendingRequests.dequeue();
2153     }
2154 
2155     return totalRead;
2156 }
2157 
2158 SFTPWorker::GetRequest::~GetRequest()
2159 {
2160     SFTPWorker::GetRequest::Request request{};
2161     std::array<char, MAX_XFER_BUF_SIZE> buf{};
2162 
2163     // Remove pending reads to avoid memory leaks
2164     while (!m_pendingRequests.isEmpty()) {
2165         request = m_pendingRequests.dequeue();
2166         sftp_async_read(m_file, buf.data(), request.expectedLength, request.id);
2167     }
2168 }
2169 
2170 void SFTPWorker::requiresUserNameRedirection()
2171 {
2172     QUrl redirectUrl;
2173     redirectUrl.setScheme(QLatin1String("sftp"));
2174     redirectUrl.setUserName(mUsername);
2175     redirectUrl.setPassword(mPassword);
2176     redirectUrl.setHost(mHost);
2177     if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
2178         redirectUrl.setPort(mPort);
2179     }
2180     qCDebug(KIO_SFTP_LOG) << "redirecting to" << redirectUrl;
2181     redirection(redirectUrl);
2182 }
2183 
2184 Result SFTPWorker::sftpLogin()
2185 {
2186     const QString origUsername = mUsername;
2187     const auto openResult = openConnection();
2188     if (!openResult.success()) {
2189         return openResult;
2190     }
2191     qCDebug(KIO_SFTP_LOG) << "connected ?" << mConnected << "username: old=" << origUsername << "new=" << mUsername;
2192     if (!origUsername.isEmpty() && origUsername != mUsername) {
2193         requiresUserNameRedirection();
2194         return Result::fail();
2195     }
2196     return mConnected ? Result::pass() : Result::fail();
2197 }
2198 
2199 void SFTPWorker::clearPubKeyAuthInfo()
2200 {
2201     if (mPublicKeyAuthInfo) {
2202         delete mPublicKeyAuthInfo;
2203         mPublicKeyAuthInfo = nullptr;
2204     }
2205 }
2206 
2207 Result SFTPWorker::fileSystemFreeSpace(const QUrl &url)
2208 {
2209     qCDebug(KIO_SFTP_LOG) << "file system free space of" << url;
2210 
2211     const auto loginResult = sftpLogin();
2212     if (!loginResult.success()) {
2213         return loginResult;
2214     }
2215 
2216     if (sftp_extension_supported(mSftp, "statvfs@openssh.com", "2") == 0) {
2217         return Result::fail(ERR_UNSUPPORTED_ACTION, QString());
2218     }
2219 
2220     const QByteArray path = url.path().isEmpty() ? QByteArrayLiteral("/") : url.path().toUtf8();
2221 
2222     sftp_statvfs_t statvfs = sftp_statvfs(mSftp, path.constData());
2223     if (statvfs == nullptr) {
2224         return reportError(url, sftp_get_error(mSftp));
2225     }
2226 
2227     setMetaData(QString::fromLatin1("total"), QString::number(statvfs->f_frsize * statvfs->f_blocks));
2228     setMetaData(QString::fromLatin1("available"), QString::number(statvfs->f_frsize * statvfs->f_bavail));
2229 
2230     sftp_statvfs_free(statvfs);
2231 
2232     return Result::pass();
2233 }
2234 
2235 QCoro::Generator<SFTPWorker::ReadResponse> SFTPWorker::asyncRead(sftp_file file, size_t size)
2236 {
2237     SFTPWorker::GetRequest request(file, size);
2238 
2239     while (true) {
2240         // Enqueue get requests
2241         if (!request.enqueueChunks()) {
2242             co_yield SFTPWorker::ReadResponse(KIO::ERR_CANNOT_READ);
2243             break;
2244         }
2245 
2246         QByteArray filedata;
2247         const auto bytesread = request.readChunks(filedata);
2248         // Read pending get requests
2249         if (bytesread == -1) {
2250             co_yield SFTPWorker::ReadResponse(KIO::ERR_CANNOT_READ);
2251             break;
2252         }
2253         if (bytesread == 0) {
2254             if (file->eof) {
2255                 break;
2256             }
2257             continue;
2258         }
2259 
2260         co_yield SFTPWorker::ReadResponse(filedata);
2261     }
2262 }
2263 
2264 QCoro::Generator<SFTPWorker::WriteResponse> SFTPWorker::asyncWrite(sftp_file file, QCoro::Generator<ReadResponse> reader)
2265 {
2266     // TODO: enqueue requests.
2267     // Similarly to reading we may enqueue a number of requests to mitigate the
2268     // network overhead and speed up things. Serial iteration gives super poor
2269     // performance.
2270 
2271     for (const auto &response : reader) {
2272         if (response.error) {
2273             co_yield {.error = response.error};
2274             break;
2275         }
2276 
2277         // Break up into multiple requests in case a single request would be too large.
2278         // Servers can impose an arbitrary size limit that we don't want to hit.
2279         // https://bugs.kde.org/show_bug.cgi?id=404890
2280         off_t offset = 0;
2281         while (offset < response.filedata.size()) {
2282             const auto length = qMin<int>(MAX_XFER_BUF_SIZE, response.filedata.size() - offset);
2283             auto bytesWritten = sftp_write(file, response.filedata.data() + offset, length);
2284             if (bytesWritten < 0) {
2285                 qCDebug(KIO_SFTP_LOG) << "Failed to sftp_write" << length << "bytes."
2286                                       << "- Already written (for this call):" << offset << "- Return of sftp_write:" << bytesWritten
2287                                       << "- SFTP error:" << sftp_get_error(mSftp) << "- SSH error:" << ssh_get_error_code(mSession)
2288                                       << "- SSH errorString:" << ssh_get_error(mSession);
2289                 co_yield {.error = KIO::ERR_CANNOT_WRITE};
2290                 break;
2291             }
2292             co_yield {.bytes = std::make_unsigned_t<size_t>(bytesWritten)};
2293             offset += bytesWritten;
2294         }
2295     }
2296 }
2297 
2298 #include "kio_sftp.moc"