File indexing completed on 2024-05-05 04:59:44

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