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"