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