File indexing completed on 2024-05-19 15:15:44
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org> 0004 SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 /* 0010 Recommended reading explaining FTP details and quirks: 0011 https://cr.yp.to/ftp.html (by D.J. Bernstein) 0012 0013 RFC: 0014 RFC 959 "File Transfer Protocol (FTP)" 0015 RFC 1635 "How to Use Anonymous FTP" 0016 RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV) 0017 */ 0018 0019 #include <config-kioworker-ftp.h> 0020 0021 #include "ftp.h" 0022 0023 #ifdef Q_OS_WIN 0024 #include <sys/utime.h> 0025 #else 0026 #include <utime.h> 0027 #endif 0028 0029 #include <cctype> 0030 #include <cerrno> 0031 #include <cstdlib> 0032 #include <cstring> 0033 0034 #include <QAuthenticator> 0035 #include <QCoreApplication> 0036 #include <QDir> 0037 #include <QHostAddress> 0038 #include <QMimeDatabase> 0039 #include <QNetworkProxy> 0040 #include <QSslSocket> 0041 #include <QTcpServer> 0042 #include <QTcpSocket> 0043 0044 #include <KConfigGroup> 0045 #include <KLocalizedString> 0046 #include <QDebug> 0047 #include <ioworker_defaults.h> 0048 #include <kremoteencoding.h> 0049 0050 #include "kioglobal_p.h" 0051 0052 #include <QLoggingCategory> 0053 Q_DECLARE_LOGGING_CATEGORY(KIO_FTP) 0054 Q_LOGGING_CATEGORY(KIO_FTP, "kf.kio.workers.ftp", QtWarningMsg) 0055 0056 #if HAVE_STRTOLL 0057 #define charToLongLong(a) strtoll(a, nullptr, 10) 0058 #else 0059 #define charToLongLong(a) strtol(a, nullptr, 10) 0060 #endif 0061 0062 static constexpr char s_ftpLogin[] = "anonymous"; 0063 static constexpr char s_ftpPasswd[] = "anonymous@"; 0064 0065 static constexpr bool s_enableCanResume = true; 0066 0067 // Pseudo plugin class to embed meta data 0068 class KIOPluginForMetaData : public QObject 0069 { 0070 Q_OBJECT 0071 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.ftp" FILE "ftp.json") 0072 }; 0073 0074 static QString ftpCleanPath(const QString &path) 0075 { 0076 if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive) 0077 || path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) { 0078 return path.left((path.length() - qstrlen(";type=X"))); 0079 } 0080 0081 return path; 0082 } 0083 0084 static char ftpModeFromPath(const QString &path, char defaultMode = '\0') 0085 { 0086 const int index = path.lastIndexOf(QLatin1String(";type=")); 0087 0088 if (index > -1 && (index + 6) < path.size()) { 0089 const QChar mode = path.at(index + 6); 0090 // kio_ftp supports only A (ASCII) and I(BINARY) modes. 0091 if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) { 0092 return mode.toUpper().toLatin1(); 0093 } 0094 } 0095 0096 return defaultMode; 0097 } 0098 0099 static bool supportedProxyScheme(const QString &scheme) 0100 { 0101 return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks")); 0102 } 0103 0104 // JPF: somebody should find a better solution for this or move this to KIO 0105 namespace KIO 0106 { 0107 enum buffersizes { 0108 /** 0109 * largest buffer size that should be used to transfer data between 0110 * KIO workers using the data() function 0111 */ 0112 maximumIpcSize = 32 * 1024, 0113 /** 0114 * this is a reasonable value for an initial read() that a KIO worker 0115 * can do to obtain data via a slow network connection. 0116 */ 0117 initialIpcSize = 2 * 1024, 0118 /** 0119 * recommended size of a data block passed to findBufferFileType() 0120 */ 0121 minimumMimeSize = 1024, 0122 }; 0123 0124 // JPF: this helper was derived from write_all in file.cc (FileProtocol). 0125 static // JPF: in ftp.cc we make it static 0126 /** 0127 * This helper handles some special issues (blocking and interrupted 0128 * system call) when writing to a file handle. 0129 * 0130 * @return 0 on success or an error code on failure (ERR_CANNOT_WRITE, 0131 * ERR_DISK_FULL, ERR_CONNECTION_BROKEN). 0132 */ 0133 int 0134 WriteToFile(int fd, const char *buf, size_t len) 0135 { 0136 while (len > 0) { 0137 // JPF: shouldn't there be a KDE_write? 0138 ssize_t written = write(fd, buf, len); 0139 if (written >= 0) { 0140 buf += written; 0141 len -= written; 0142 continue; 0143 } 0144 switch (errno) { 0145 case EINTR: 0146 continue; 0147 case EPIPE: 0148 return ERR_CONNECTION_BROKEN; 0149 case ENOSPC: 0150 return ERR_DISK_FULL; 0151 default: 0152 return ERR_CANNOT_WRITE; 0153 } 0154 } 0155 return 0; 0156 } 0157 } 0158 0159 const KIO::filesize_t FtpInternal::UnknownSize = (KIO::filesize_t)-1; 0160 0161 using namespace KIO; 0162 0163 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) 0164 { 0165 QCoreApplication app(argc, argv); 0166 app.setApplicationName(QStringLiteral("kio_ftp")); 0167 0168 qCDebug(KIO_FTP) << "Starting"; 0169 0170 if (argc != 4) { 0171 fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n"); 0172 exit(-1); 0173 } 0174 0175 Ftp worker(argv[2], argv[3]); 0176 worker.dispatchLoop(); 0177 0178 qCDebug(KIO_FTP) << "Done"; 0179 return 0; 0180 } 0181 0182 //=============================================================================== 0183 // FtpInternal 0184 //=============================================================================== 0185 0186 /** 0187 * This closes a data connection opened by ftpOpenDataConnection(). 0188 */ 0189 void FtpInternal::ftpCloseDataConnection() 0190 { 0191 delete m_data; 0192 m_data = nullptr; 0193 delete m_server; 0194 m_server = nullptr; 0195 } 0196 0197 /** 0198 * This closes a control connection opened by ftpOpenControlConnection() and reinits the 0199 * related states. This method gets called from the constructor with m_control = nullptr. 0200 */ 0201 void FtpInternal::ftpCloseControlConnection() 0202 { 0203 m_extControl = 0; 0204 delete m_control; 0205 m_control = nullptr; 0206 m_cDataMode = 0; 0207 m_bLoggedOn = false; // logon needs control connection 0208 m_bTextMode = false; 0209 m_bBusy = false; 0210 } 0211 0212 /** 0213 * Returns the last response from the server (iOffset >= 0) -or- reads a new response 0214 * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0). 0215 */ 0216 const char *FtpInternal::ftpResponse(int iOffset) 0217 { 0218 Q_ASSERT(m_control); // must have control connection socket 0219 const char *pTxt = m_lastControlLine.data(); 0220 0221 // read the next line ... 0222 if (iOffset < 0) { 0223 int iMore = 0; 0224 m_iRespCode = 0; 0225 0226 if (!pTxt) { 0227 return nullptr; // avoid using a nullptr when calling atoi. 0228 } 0229 0230 // If the server sends a multiline response starting with 0231 // "nnn-text" we loop here until a final "nnn text" line is 0232 // reached. Only data from the final line will be stored. 0233 do { 0234 while (!m_control->canReadLine() && m_control->waitForReadyRead((q->readTimeout() * 1000))) { } 0235 m_lastControlLine = m_control->readLine(); 0236 pTxt = m_lastControlLine.data(); 0237 int iCode = atoi(pTxt); 0238 if (iMore == 0) { 0239 // first line 0240 qCDebug(KIO_FTP) << " > " << pTxt; 0241 if (iCode >= 100) { 0242 m_iRespCode = iCode; 0243 if (pTxt[3] == '-') { 0244 // marker for a multiple line response 0245 iMore = iCode; 0246 } 0247 } else { 0248 qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt; 0249 } 0250 } else { 0251 // multi-line 0252 qCDebug(KIO_FTP) << " > " << pTxt; 0253 if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') { 0254 iMore = 0; 0255 } 0256 } 0257 } while (iMore != 0); 0258 qCDebug(KIO_FTP) << "resp> " << pTxt; 0259 0260 m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0; 0261 } 0262 0263 // return text with offset ... 0264 while (iOffset-- > 0 && pTxt[0]) { 0265 pTxt++; 0266 } 0267 return pTxt; 0268 } 0269 0270 void FtpInternal::closeConnection() 0271 { 0272 if (m_control || m_data) { 0273 qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy; 0274 } 0275 0276 if (m_bBusy) { // ftpCloseCommand not called 0277 qCWarning(KIO_FTP) << "Abandoned data stream"; 0278 ftpCloseDataConnection(); 0279 } 0280 0281 if (m_bLoggedOn) { // send quit 0282 if (!ftpSendCmd(QByteArrayLiteral("quit"), 0) || (m_iRespType != 2)) { 0283 qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode; 0284 } 0285 } 0286 0287 // close the data and control connections ... 0288 ftpCloseDataConnection(); 0289 ftpCloseControlConnection(); 0290 } 0291 0292 FtpInternal::FtpInternal(Ftp *qptr) 0293 : QObject() 0294 , q(qptr) 0295 { 0296 ftpCloseControlConnection(); 0297 } 0298 0299 FtpInternal::~FtpInternal() 0300 { 0301 qCDebug(KIO_FTP); 0302 closeConnection(); 0303 } 0304 0305 void FtpInternal::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass) 0306 { 0307 qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user; 0308 0309 m_proxyURL.clear(); 0310 m_proxyUrls = q->mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), Qt::SkipEmptyParts); 0311 0312 qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls; 0313 0314 if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) { 0315 closeConnection(); 0316 } 0317 0318 m_host = _host; 0319 m_port = _port; 0320 m_user = _user; 0321 m_pass = _pass; 0322 } 0323 0324 Result FtpInternal::openConnection() 0325 { 0326 return ftpOpenConnection(LoginMode::Explicit); 0327 } 0328 0329 Result FtpInternal::ftpOpenConnection(LoginMode loginMode) 0330 { 0331 // check for implicit login if we are already logged on ... 0332 if (loginMode == LoginMode::Implicit && m_bLoggedOn) { 0333 Q_ASSERT(m_control); // must have control connection socket 0334 return Result::pass(); 0335 } 0336 0337 qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]"; 0338 0339 q->infoMessage(i18n("Opening connection to host %1", m_host)); 0340 0341 if (m_host.isEmpty()) { 0342 return Result::fail(ERR_UNKNOWN_HOST); 0343 } 0344 0345 Q_ASSERT(!m_bLoggedOn); 0346 0347 m_initialPath.clear(); 0348 m_currentPath.clear(); 0349 0350 const Result result = ftpOpenControlConnection(); 0351 if (!result.success()) { 0352 return result; 0353 } 0354 q->infoMessage(i18n("Connected to host %1", m_host)); 0355 0356 bool userNameChanged = false; 0357 if (loginMode != LoginMode::Deferred) { 0358 const Result result = ftpLogin(&userNameChanged); 0359 m_bLoggedOn = result.success(); 0360 if (!m_bLoggedOn) { 0361 return result; 0362 } 0363 } 0364 0365 m_bTextMode = q->configValue(QStringLiteral("textmode"), false); 0366 0367 // Redirected due to credential change... 0368 if (userNameChanged && m_bLoggedOn) { 0369 QUrl realURL; 0370 realURL.setScheme(QStringLiteral("ftp")); 0371 if (m_user != QLatin1String(s_ftpLogin)) { 0372 realURL.setUserName(m_user); 0373 } 0374 if (m_pass != QLatin1String(s_ftpPasswd)) { 0375 realURL.setPassword(m_pass); 0376 } 0377 realURL.setHost(m_host); 0378 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { 0379 realURL.setPort(m_port); 0380 } 0381 if (m_initialPath.isEmpty()) { 0382 m_initialPath = QStringLiteral("/"); 0383 } 0384 realURL.setPath(m_initialPath); 0385 qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL; 0386 q->redirection(realURL); 0387 return Result::fail(); 0388 } 0389 0390 return Result::pass(); 0391 } 0392 0393 /** 0394 * Called by @ref openConnection. It opens the control connection to the ftp server. 0395 * 0396 * @return true on success. 0397 */ 0398 Result FtpInternal::ftpOpenControlConnection() 0399 { 0400 if (m_proxyUrls.isEmpty()) { 0401 return ftpOpenControlConnection(m_host, m_port); 0402 } 0403 0404 Result result = Result::fail(); 0405 0406 for (const QString &proxyUrl : std::as_const(m_proxyUrls)) { 0407 const QUrl url(proxyUrl); 0408 const QString scheme(url.scheme()); 0409 0410 if (!supportedProxyScheme(scheme)) { 0411 // TODO: Need a new error code to indicate unsupported URL scheme. 0412 result = Result::fail(ERR_CANNOT_CONNECT, url.toString()); 0413 continue; 0414 } 0415 0416 if (!isSocksProxyScheme(scheme)) { 0417 const Result result = ftpOpenControlConnection(url.host(), url.port()); 0418 if (result.success()) { 0419 return Result::pass(); 0420 } 0421 continue; 0422 } 0423 0424 qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url; 0425 m_proxyURL = url; 0426 result = ftpOpenControlConnection(m_host, m_port); 0427 if (result.success()) { 0428 return result; 0429 } 0430 m_proxyURL.clear(); 0431 } 0432 0433 return result; 0434 } 0435 0436 Result FtpInternal::ftpOpenControlConnection(const QString &host, int port) 0437 { 0438 // implicitly close, then try to open a new connection ... 0439 closeConnection(); 0440 QString sErrorMsg; 0441 0442 // now connect to the server and read the login message ... 0443 if (port == 0) { 0444 port = 21; // default FTP port 0445 } 0446 const auto connectionResult = synchronousConnectToHost(host, port); 0447 m_control = connectionResult.socket; 0448 0449 int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT; 0450 if (!connectionResult.result.success()) { 0451 qDebug() << "overriding error code!!1" << connectionResult.result.error(); 0452 iErrorCode = connectionResult.result.error(); 0453 sErrorMsg = connectionResult.result.errorString(); 0454 } 0455 0456 // on connect success try to read the server message... 0457 if (iErrorCode == 0) { 0458 const char *psz = ftpResponse(-1); 0459 if (m_iRespType != 2) { 0460 // login not successful, do we have an message text? 0461 if (psz[0]) { 0462 sErrorMsg = i18n("%1 (Error %2)", host, q->remoteEncoding()->decode(psz).trimmed()); 0463 } 0464 iErrorCode = ERR_CANNOT_CONNECT; 0465 } 0466 } else { 0467 const auto socketError = m_control->error(); 0468 if (socketError == QAbstractSocket::HostNotFoundError) { 0469 iErrorCode = ERR_UNKNOWN_HOST; 0470 } 0471 0472 sErrorMsg = QStringLiteral("%1: %2").arg(host, m_control->errorString()); 0473 } 0474 0475 // if there was a problem - report it ... 0476 if (iErrorCode == 0) { // OK, return success 0477 return Result::pass(); 0478 } 0479 closeConnection(); // clean-up on error 0480 return Result::fail(iErrorCode, sErrorMsg); 0481 } 0482 0483 /** 0484 * Called by @ref openConnection. It logs us in. 0485 * @ref m_initialPath is set to the current working directory 0486 * if logging on was successful. 0487 * 0488 * @return true on success. 0489 */ 0490 Result FtpInternal::ftpLogin(bool *userChanged) 0491 { 0492 q->infoMessage(i18n("Sending login information")); 0493 0494 Q_ASSERT(!m_bLoggedOn); 0495 0496 QString user(m_user); 0497 QString pass(m_pass); 0498 0499 if (q->configValue(QStringLiteral("EnableAutoLogin"), false)) { 0500 QString au = q->configValue(QStringLiteral("autoLoginUser")); 0501 if (!au.isEmpty()) { 0502 user = au; 0503 pass = q->configValue(QStringLiteral("autoLoginPass")); 0504 } 0505 } 0506 0507 AuthInfo info; 0508 info.url.setScheme(QStringLiteral("ftp")); 0509 info.url.setHost(m_host); 0510 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { 0511 info.url.setPort(m_port); 0512 } 0513 if (!user.isEmpty()) { 0514 info.url.setUserName(user); 0515 } 0516 0517 // Check for cached authentication first and fallback to 0518 // anonymous login when no stored credentials are found. 0519 if (!q->configValue(QStringLiteral("TryAnonymousLoginFirst"), false) && pass.isEmpty() && q->checkCachedAuthentication(info)) { 0520 user = info.username; 0521 pass = info.password; 0522 } 0523 0524 // Try anonymous login if both username/password 0525 // information is blank. 0526 if (user.isEmpty() && pass.isEmpty()) { 0527 user = QString::fromLatin1(s_ftpLogin); 0528 pass = QString::fromLatin1(s_ftpPasswd); 0529 } 0530 0531 QByteArray tempbuf; 0532 QString lastServerResponse; 0533 int failedAuth = 0; 0534 bool promptForRetry = false; 0535 0536 // Give the user the option to login anonymously... 0537 info.setExtraField(QStringLiteral("anonymous"), false); 0538 0539 do { 0540 // Check the cache and/or prompt user for password if 1st 0541 // login attempt failed OR the user supplied a login name, 0542 // but no password. 0543 if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) { 0544 QString errorMsg; 0545 qCDebug(KIO_FTP) << "Prompting user for login info..."; 0546 0547 // Ask user if we should retry after when login fails! 0548 if (failedAuth > 0 && promptForRetry) { 0549 errorMsg = i18n( 0550 "Message sent:\nLogin using username=%1 and " 0551 "password=[hidden]\n\nServer replied:\n%2\n\n", 0552 user, 0553 lastServerResponse); 0554 } 0555 0556 if (user != QLatin1String(s_ftpLogin)) { 0557 info.username = user; 0558 } 0559 0560 info.prompt = i18n( 0561 "You need to supply a username and a password " 0562 "to access this site."); 0563 info.commentLabel = i18n("Site:"); 0564 info.comment = i18n("<b>%1</b>", m_host); 0565 info.keepPassword = true; // Prompt the user for persistence as well. 0566 info.setModified(false); // Default the modified flag since we reuse authinfo. 0567 0568 const bool disablePassDlg = q->configValue(QStringLiteral("DisablePassDlg"), false); 0569 if (disablePassDlg) { 0570 return Result::fail(ERR_USER_CANCELED, m_host); 0571 } 0572 const int errorCode = q->openPasswordDialog(info, errorMsg); 0573 if (errorCode) { 0574 return Result::fail(errorCode); 0575 } else { 0576 // User can decide go anonymous using checkbox 0577 if (info.getExtraField(QStringLiteral("anonymous")).toBool()) { 0578 user = QString::fromLatin1(s_ftpLogin); 0579 pass = QString::fromLatin1(s_ftpPasswd); 0580 } else { 0581 user = info.username; 0582 pass = info.password; 0583 } 0584 promptForRetry = true; 0585 } 0586 } 0587 0588 tempbuf = "USER " + user.toLatin1(); 0589 if (m_proxyURL.isValid()) { 0590 tempbuf += '@' + m_host.toLatin1(); 0591 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { 0592 tempbuf += ':' + QByteArray::number(m_port); 0593 } 0594 } 0595 0596 qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf; 0597 0598 bool loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); 0599 bool needPass = (m_iRespCode == 331); 0600 // Prompt user for login info if we do not 0601 // get back a "230" or "331". 0602 if (!loggedIn && !needPass) { 0603 lastServerResponse = QString::fromUtf8(ftpResponse(0)); 0604 qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse; 0605 ++failedAuth; 0606 continue; // Well we failed, prompt the user please!! 0607 } 0608 0609 if (needPass) { 0610 tempbuf = "PASS " + pass.toLatin1(); 0611 qCDebug(KIO_FTP) << "Sending Login password: " 0612 << "[protected]"; 0613 loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); 0614 } 0615 0616 if (loggedIn) { 0617 // Make sure the user name changed flag is properly set. 0618 if (userChanged) { 0619 *userChanged = (!m_user.isEmpty() && (m_user != user)); 0620 } 0621 0622 // Do not cache the default login!! 0623 if (user != QLatin1String(s_ftpLogin) && pass != QLatin1String(s_ftpPasswd)) { 0624 // Update the username in case it was changed during login. 0625 if (!m_user.isEmpty()) { 0626 info.url.setUserName(user); 0627 m_user = user; 0628 } 0629 0630 // Cache the password if the user requested it. 0631 if (info.keepPassword) { 0632 q->cacheAuthentication(info); 0633 } 0634 } 0635 failedAuth = -1; 0636 } else { 0637 // some servers don't let you login anymore 0638 // if you fail login once, so restart the connection here 0639 lastServerResponse = QString::fromUtf8(ftpResponse(0)); 0640 const Result result = ftpOpenControlConnection(); 0641 if (!result.success()) { 0642 return result; 0643 } 0644 } 0645 } while (++failedAuth); 0646 0647 qCDebug(KIO_FTP) << "Login OK"; 0648 q->infoMessage(i18n("Login OK")); 0649 0650 // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix: 0651 // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint 0652 if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) { 0653 if (!qstrncmp(ftpResponse(0), "215 Windows_NT", 14)) { // should do for any version 0654 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle")); 0655 // Check if it was already in Unix style 0656 // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk> 0657 if (!qstrncmp(ftpResponse(0), "200 MSDOS-like directory output is on", 37)) 0658 // It was in Unix style already! 0659 { 0660 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle")); 0661 } 0662 // windows won't support chmod before KDE konquers their desktop... 0663 m_extControl |= chmodUnknown; 0664 } 0665 } else { 0666 qCWarning(KIO_FTP) << "SYST failed"; 0667 } 0668 0669 if (q->configValue(QStringLiteral("EnableAutoLoginMacro"), false)) { 0670 ftpAutoLoginMacro(); 0671 } 0672 0673 // Get the current working directory 0674 qCDebug(KIO_FTP) << "Searching for pwd"; 0675 if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) { 0676 qCDebug(KIO_FTP) << "Couldn't issue pwd command"; 0677 return Result::fail(ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ? 0678 } 0679 0680 QString sTmp = q->remoteEncoding()->decode(ftpResponse(3)); 0681 const int iBeg = sTmp.indexOf(QLatin1Char('"')); 0682 const int iEnd = sTmp.lastIndexOf(QLatin1Char('"')); 0683 if (iBeg > 0 && iBeg < iEnd) { 0684 m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1); 0685 if (!m_initialPath.startsWith(QLatin1Char('/'))) { 0686 m_initialPath.prepend(QLatin1Char('/')); 0687 } 0688 qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath; 0689 m_currentPath = m_initialPath; 0690 } 0691 0692 return Result::pass(); 0693 } 0694 0695 void FtpInternal::ftpAutoLoginMacro() 0696 { 0697 QString macro = q->metaData(QStringLiteral("autoLoginMacro")); 0698 0699 if (macro.isEmpty()) { 0700 return; 0701 } 0702 0703 QStringList list = macro.split(QLatin1Char('\n'), Qt::SkipEmptyParts); 0704 auto initIt = std::find_if(list.cbegin(), list.cend(), [](const QString &s) { 0705 return s.startsWith(QLatin1String("init")); 0706 }); 0707 0708 if (initIt != list.cend()) { 0709 list = macro.split(QLatin1Char('\\'), Qt::SkipEmptyParts); 0710 // Ignore the macro name, so start from list.cbegin() + 1 0711 for (auto it = list.cbegin() + 1; it != list.cend(); ++it) { 0712 // TODO: Add support for arbitrary commands besides simply changing directory!! 0713 if ((*it).startsWith(QLatin1String("cwd"))) { 0714 (void)ftpFolder((*it).mid(4)); 0715 } 0716 } 0717 } 0718 } 0719 0720 /** 0721 * ftpSendCmd - send a command (@p cmd) and read response 0722 * 0723 * @param maxretries number of time it should retry. Since it recursively 0724 * calls itself if it can't read the answer (this happens especially after 0725 * timeouts), we need to limit the recursiveness ;-) 0726 * 0727 * return true if any response received, false on error 0728 */ 0729 bool FtpInternal::ftpSendCmd(const QByteArray &cmd, int maxretries) 0730 { 0731 Q_ASSERT(m_control); // must have control connection socket 0732 0733 if (cmd.indexOf('\r') != -1 || cmd.indexOf('\n') != -1) { 0734 qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data(); 0735 return false; 0736 } 0737 0738 // Don't print out the password... 0739 bool isPassCmd = (cmd.left(4).toLower() == "pass"); 0740 0741 // Send the message... 0742 const QByteArray buf = cmd + "\r\n"; // Yes, must use CR/LF - see https://cr.yp.to/ftp/request.html 0743 int num = m_control->write(buf); 0744 while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) { } 0745 0746 // If we were able to successfully send the command, then we will 0747 // attempt to read the response. Otherwise, take action to re-attempt 0748 // the login based on the maximum number of retries specified... 0749 if (num > 0) { 0750 ftpResponse(-1); 0751 } else { 0752 m_iRespType = m_iRespCode = 0; 0753 } 0754 0755 // If respCh is NULL or the response is 421 (Timed-out), we try to re-send 0756 // the command based on the value of maxretries. 0757 if ((m_iRespType <= 0) || (m_iRespCode == 421)) { 0758 // We have not yet logged on... 0759 if (!m_bLoggedOn) { 0760 // The command was sent from the ftpLogin function, i.e. we are actually 0761 // attempting to login in. NOTE: If we already sent the username, we 0762 // return false and let the user decide whether (s)he wants to start from 0763 // the beginning... 0764 if (maxretries > 0 && !isPassCmd) { 0765 closeConnection(); 0766 const auto result = ftpOpenConnection(LoginMode::Deferred); 0767 if (result.success() && ftpSendCmd(cmd, maxretries - 1)) { 0768 return true; 0769 } 0770 } 0771 0772 return false; 0773 } else { 0774 if (maxretries < 1) { 0775 return false; 0776 } else { 0777 qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection."; 0778 0779 closeConnection(); // Close the old connection... 0780 const Result openResult = openConnection(); // Attempt to re-establish a new connection... 0781 0782 if (!openResult.success()) { 0783 if (m_control) { // if openConnection succeeded ... 0784 qCDebug(KIO_FTP) << "Login failure, aborting"; 0785 closeConnection(); 0786 } 0787 return false; 0788 } 0789 0790 qCDebug(KIO_FTP) << "Logged back in, re-issuing command"; 0791 0792 // If we were able to login, resend the command... 0793 if (maxretries) { 0794 maxretries--; 0795 } 0796 0797 return ftpSendCmd(cmd, maxretries); 0798 } 0799 } 0800 } 0801 0802 return true; 0803 } 0804 0805 /* 0806 * ftpOpenPASVDataConnection - set up data connection, using PASV mode 0807 * 0808 * return 0 if successful, ERR_INTERNAL otherwise 0809 * doesn't set error message, since non-pasv mode will always be tried if 0810 * this one fails 0811 */ 0812 int FtpInternal::ftpOpenPASVDataConnection() 0813 { 0814 Q_ASSERT(m_control); // must have control connection socket 0815 Q_ASSERT(!m_data); // ... but no data connection 0816 0817 // Check that we can do PASV 0818 QHostAddress address = m_control->peerAddress(); 0819 if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) { 0820 return ERR_INTERNAL; // no PASV for non-PF_INET connections 0821 } 0822 0823 if (m_extControl & pasvUnknown) { 0824 return ERR_INTERNAL; // already tried and got "unknown command" 0825 } 0826 0827 m_bPasv = true; 0828 0829 /* Let's PASsiVe*/ 0830 if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) { 0831 qCDebug(KIO_FTP) << "PASV attempt failed"; 0832 // unknown command? 0833 if (m_iRespType == 5) { 0834 qCDebug(KIO_FTP) << "disabling use of PASV"; 0835 m_extControl |= pasvUnknown; 0836 } 0837 return ERR_INTERNAL; 0838 } 0839 0840 // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)' 0841 // but anonftpd gives '227 =160,39,200,55,6,245' 0842 int i[6]; 0843 const char *start = strchr(ftpResponse(3), '('); 0844 if (!start) { 0845 start = strchr(ftpResponse(3), '='); 0846 } 0847 if (!start 0848 || (sscanf(start, "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 0849 && sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) { 0850 qCritical() << "parsing IP and port numbers failed. String parsed: " << start; 0851 return ERR_INTERNAL; 0852 } 0853 0854 // we ignore the host part on purpose for two reasons 0855 // a) it might be wrong anyway 0856 // b) it would make us being susceptible to a port scanning attack 0857 0858 // now connect the data socket ... 0859 quint16 port = i[4] << 8 | i[5]; 0860 const QString host = (isSocksProxy() ? m_host : address.toString()); 0861 const auto connectionResult = synchronousConnectToHost(host, port); 0862 m_data = connectionResult.socket; 0863 if (!connectionResult.result.success()) { 0864 return connectionResult.result.error(); 0865 } 0866 0867 return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL; 0868 } 0869 0870 /* 0871 * ftpOpenEPSVDataConnection - opens a data connection via EPSV 0872 */ 0873 int FtpInternal::ftpOpenEPSVDataConnection() 0874 { 0875 Q_ASSERT(m_control); // must have control connection socket 0876 Q_ASSERT(!m_data); // ... but no data connection 0877 0878 QHostAddress address = m_control->peerAddress(); 0879 int portnum; 0880 0881 if (m_extControl & epsvUnknown) { 0882 return ERR_INTERNAL; 0883 } 0884 0885 m_bPasv = true; 0886 if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) { 0887 // unknown command? 0888 if (m_iRespType == 5) { 0889 qCDebug(KIO_FTP) << "disabling use of EPSV"; 0890 m_extControl |= epsvUnknown; 0891 } 0892 return ERR_INTERNAL; 0893 } 0894 0895 const char *start = strchr(ftpResponse(3), '|'); 0896 if (!start || sscanf(start, "|||%d|", &portnum) != 1) { 0897 return ERR_INTERNAL; 0898 } 0899 Q_ASSERT(portnum > 0); 0900 0901 const QString host = (isSocksProxy() ? m_host : address.toString()); 0902 const auto connectionResult = synchronousConnectToHost(host, static_cast<quint16>(portnum)); 0903 m_data = connectionResult.socket; 0904 if (!connectionResult.result.success()) { 0905 return connectionResult.result.error(); 0906 } 0907 return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL; 0908 } 0909 0910 /* 0911 * ftpOpenDataConnection - set up data connection 0912 * 0913 * The routine calls several ftpOpenXxxxConnection() helpers to find 0914 * the best connection mode. If a helper cannot connect if returns 0915 * ERR_INTERNAL - so this is not really an error! All other error 0916 * codes are treated as fatal, e.g. they are passed back to the caller 0917 * who is responsible for calling error(). ftpOpenPortDataConnection 0918 * can be called as last try and it does never return ERR_INTERNAL. 0919 * 0920 * @return 0 if successful, err code otherwise 0921 */ 0922 int FtpInternal::ftpOpenDataConnection() 0923 { 0924 // make sure that we are logged on and have no data connection... 0925 Q_ASSERT(m_bLoggedOn); 0926 ftpCloseDataConnection(); 0927 0928 int iErrCode = 0; 0929 int iErrCodePASV = 0; // Remember error code from PASV 0930 0931 // First try passive (EPSV & PASV) modes 0932 if (!q->configValue(QStringLiteral("DisablePassiveMode"), false)) { 0933 iErrCode = ftpOpenPASVDataConnection(); 0934 if (iErrCode == 0) { 0935 return 0; // success 0936 } 0937 iErrCodePASV = iErrCode; 0938 ftpCloseDataConnection(); 0939 0940 if (!q->configValue(QStringLiteral("DisableEPSV"), false)) { 0941 iErrCode = ftpOpenEPSVDataConnection(); 0942 if (iErrCode == 0) { 0943 return 0; // success 0944 } 0945 ftpCloseDataConnection(); 0946 } 0947 0948 // if we sent EPSV ALL already and it was accepted, then we can't 0949 // use active connections any more 0950 if (m_extControl & epsvAllSent) { 0951 return iErrCodePASV; 0952 } 0953 } 0954 0955 // fall back to port mode 0956 iErrCode = ftpOpenPortDataConnection(); 0957 if (iErrCode == 0) { 0958 return 0; // success 0959 } 0960 0961 ftpCloseDataConnection(); 0962 // prefer to return the error code from PASV if any, since that's what should have worked in the first place 0963 return iErrCodePASV ? iErrCodePASV : iErrCode; 0964 } 0965 0966 /* 0967 * ftpOpenPortDataConnection - set up data connection 0968 * 0969 * @return 0 if successful, err code otherwise (but never ERR_INTERNAL 0970 * because this is the last connection mode that is tried) 0971 */ 0972 int FtpInternal::ftpOpenPortDataConnection() 0973 { 0974 Q_ASSERT(m_control); // must have control connection socket 0975 Q_ASSERT(!m_data); // ... but no data connection 0976 0977 m_bPasv = false; 0978 if (m_extControl & eprtUnknown) { 0979 return ERR_INTERNAL; 0980 } 0981 0982 if (!m_server) { 0983 m_server = new QTcpServer; 0984 m_server->listen(QHostAddress::Any, 0); 0985 } 0986 0987 if (!m_server->isListening()) { 0988 delete m_server; 0989 m_server = nullptr; 0990 return ERR_CANNOT_LISTEN; 0991 } 0992 0993 m_server->setMaxPendingConnections(1); 0994 0995 QString command; 0996 QHostAddress localAddress = m_control->localAddress(); 0997 if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) { 0998 struct { 0999 quint32 ip4; 1000 quint16 port; 1001 } data; 1002 data.ip4 = localAddress.toIPv4Address(); 1003 data.port = m_server->serverPort(); 1004 1005 unsigned char *pData = reinterpret_cast<unsigned char *>(&data); 1006 command = QStringLiteral("PORT %1,%2,%3,%4,%5,%6").arg(pData[3]).arg(pData[2]).arg(pData[1]).arg(pData[0]).arg(pData[5]).arg(pData[4]); 1007 } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) { 1008 command = QStringLiteral("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort()); 1009 } 1010 1011 if (ftpSendCmd(command.toLatin1()) && (m_iRespType == 2)) { 1012 return 0; 1013 } 1014 1015 delete m_server; 1016 m_server = nullptr; 1017 return ERR_INTERNAL; 1018 } 1019 1020 Result FtpInternal::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset) 1021 { 1022 if (!ftpDataMode(ftpModeFromPath(_path, _mode))) { 1023 return Result::fail(ERR_CANNOT_CONNECT, m_host); 1024 } 1025 1026 if (int error = ftpOpenDataConnection()) { 1027 return Result::fail(error, m_host); 1028 } 1029 1030 if (_offset > 0) { 1031 // send rest command if offset > 0, this applies to retr and stor commands 1032 char buf[100]; 1033 sprintf(buf, "rest %lld", _offset); 1034 if (!ftpSendCmd(buf)) { 1035 return Result::fail(); 1036 } 1037 if (m_iRespType != 3) { 1038 return Result::fail(ERR_CANNOT_RESUME, _path); // should never happen 1039 } 1040 } 1041 1042 QByteArray tmp = _command; 1043 QString errormessage; 1044 1045 if (!_path.isEmpty()) { 1046 tmp += ' ' + q->remoteEncoding()->encode(ftpCleanPath(_path)); 1047 } 1048 1049 if (!ftpSendCmd(tmp) || (m_iRespType != 1)) { 1050 if (_offset > 0 && qstrcmp(_command, "retr") == 0 && (m_iRespType == 4)) { 1051 errorcode = ERR_CANNOT_RESUME; 1052 } 1053 // The error code here depends on the command 1054 errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed()); 1055 } 1056 1057 else { 1058 // Only now we know for sure that we can resume 1059 if (_offset > 0 && qstrcmp(_command, "retr") == 0) { 1060 q->canResume(); 1061 } 1062 1063 if (m_server && !m_data) { 1064 qCDebug(KIO_FTP) << "waiting for connection from remote."; 1065 m_server->waitForNewConnection(q->connectTimeout() * 1000); 1066 m_data = m_server->nextPendingConnection(); 1067 } 1068 1069 if (m_data) { 1070 qCDebug(KIO_FTP) << "connected with remote."; 1071 m_bBusy = true; // cleared in ftpCloseCommand 1072 return Result::pass(); 1073 } 1074 1075 qCDebug(KIO_FTP) << "no connection received from remote."; 1076 errorcode = ERR_CANNOT_ACCEPT; 1077 errormessage = m_host; 1078 } 1079 1080 if (errorcode != KJob::NoError) { 1081 return Result::fail(errorcode, errormessage); 1082 } 1083 return Result::fail(); 1084 } 1085 1086 bool FtpInternal::ftpCloseCommand() 1087 { 1088 // first close data sockets (if opened), then read response that 1089 // we got for whatever was used in ftpOpenCommand ( should be 226 ) 1090 ftpCloseDataConnection(); 1091 1092 if (!m_bBusy) { 1093 return true; 1094 } 1095 1096 qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result"; 1097 m_bBusy = false; 1098 1099 if (!ftpResponse(-1) || (m_iRespType != 2)) { 1100 qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message"; 1101 return false; 1102 } 1103 return true; 1104 } 1105 1106 Result FtpInternal::mkdir(const QUrl &url, int permissions) 1107 { 1108 auto result = ftpOpenConnection(LoginMode::Implicit); 1109 if (!result.success()) { 1110 return result; 1111 } 1112 1113 const QByteArray encodedPath(q->remoteEncoding()->encode(url)); 1114 const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size()); 1115 1116 if (!ftpSendCmd((QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) { 1117 QString currentPath(m_currentPath); 1118 1119 // Check whether or not mkdir failed because 1120 // the directory already exists... 1121 if (ftpFolder(path)) { 1122 const QString &failedPath = path; 1123 // Change the directory back to what it was... 1124 (void)ftpFolder(currentPath); 1125 return Result::fail(ERR_DIR_ALREADY_EXIST, failedPath); 1126 } 1127 1128 return Result::fail(ERR_CANNOT_MKDIR, path); 1129 } 1130 1131 if (permissions != -1) { 1132 // chmod the dir we just created, ignoring errors. 1133 (void)ftpChmod(path, permissions); 1134 } 1135 1136 return Result::pass(); 1137 } 1138 1139 Result FtpInternal::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags) 1140 { 1141 const auto result = ftpOpenConnection(LoginMode::Implicit); 1142 if (!result.success()) { 1143 return result; 1144 } 1145 1146 // The actual functionality is in ftpRename because put needs it 1147 return ftpRename(src.path(), dst.path(), flags); 1148 } 1149 1150 Result FtpInternal::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags) 1151 { 1152 Q_ASSERT(m_bLoggedOn); 1153 1154 // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793). 1155 if (!(jobFlags & KIO::Overwrite)) { 1156 if (ftpFileExists(dst)) { 1157 return Result::fail(ERR_FILE_ALREADY_EXIST, dst); 1158 } 1159 } 1160 1161 if (ftpFolder(dst)) { 1162 return Result::fail(ERR_DIR_ALREADY_EXIST, dst); 1163 } 1164 1165 // CD into parent folder 1166 const int pos = src.lastIndexOf(QLatin1Char('/')); 1167 if (pos >= 0) { 1168 if (!ftpFolder(src.left(pos + 1))) { 1169 return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, src); 1170 } 1171 } 1172 1173 const QByteArray from_cmd = "RNFR " + q->remoteEncoding()->encode(src.mid(pos + 1)); 1174 if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) { 1175 return Result::fail(ERR_CANNOT_RENAME, src); 1176 } 1177 1178 const QByteArray to_cmd = "RNTO " + q->remoteEncoding()->encode(dst); 1179 if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) { 1180 return Result::fail(ERR_CANNOT_RENAME, src); 1181 } 1182 1183 return Result::pass(); 1184 } 1185 1186 Result FtpInternal::del(const QUrl &url, bool isfile) 1187 { 1188 auto result = ftpOpenConnection(LoginMode::Implicit); 1189 if (!result.success()) { 1190 return result; 1191 } 1192 1193 // When deleting a directory, we must exit from it first 1194 // The last command probably went into it (to stat it) 1195 if (!isfile) { 1196 (void)ftpFolder(q->remoteEncoding()->decode(q->remoteEncoding()->directory(url))); // ignore errors 1197 } 1198 1199 const QByteArray cmd = (isfile ? "DELE " : "RMD ") + q->remoteEncoding()->encode(url); 1200 1201 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { 1202 return Result::fail(ERR_CANNOT_DELETE, url.path()); 1203 } 1204 1205 return Result::pass(); 1206 } 1207 1208 bool FtpInternal::ftpChmod(const QString &path, int permissions) 1209 { 1210 Q_ASSERT(m_bLoggedOn); 1211 1212 if (m_extControl & chmodUnknown) { // previous errors? 1213 return false; 1214 } 1215 1216 // we need to do bit AND 777 to get permissions, in case 1217 // we were sent a full mode (unlikely) 1218 const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777 /*octal*/, 8 /*octal*/) + ' ' + q->remoteEncoding()->encode(path); 1219 1220 if (ftpSendCmd(cmd)) { 1221 qCDebug(KIO_FTP) << "ftpChmod: Failed to issue chmod"; 1222 return false; 1223 } 1224 1225 if (m_iRespType == 2) { 1226 return true; 1227 } 1228 1229 if (m_iRespCode == 500) { 1230 m_extControl |= chmodUnknown; 1231 qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling"; 1232 } 1233 return false; 1234 } 1235 1236 Result FtpInternal::chmod(const QUrl &url, int permissions) 1237 { 1238 const auto result = ftpOpenConnection(LoginMode::Implicit); 1239 if (!result.success()) { 1240 return result; 1241 } 1242 1243 if (!ftpChmod(url.path(), permissions)) { 1244 return Result::fail(ERR_CANNOT_CHMOD, url.path()); 1245 } 1246 1247 return Result::pass(); 1248 } 1249 1250 void FtpInternal::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir) 1251 { 1252 Q_ASSERT(entry.count() == 0); // by contract :-) 1253 1254 entry.reserve(9); 1255 entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); 1256 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ftpEnt.size); 1257 entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date.toSecsSinceEpoch()); 1258 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, ftpEnt.access); 1259 entry.fastInsert(KIO::UDSEntry::UDS_USER, ftpEnt.owner); 1260 if (!ftpEnt.group.isEmpty()) { 1261 entry.fastInsert(KIO::UDSEntry::UDS_GROUP, ftpEnt.group); 1262 } 1263 1264 if (!ftpEnt.link.isEmpty()) { 1265 entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link); 1266 1267 QMimeDatabase db; 1268 QMimeType mime = db.mimeTypeForUrl(QUrl(QLatin1String("ftp://host/") + filename)); 1269 // Links on ftp sites are often links to dirs, and we have no way to check 1270 // that. Let's do like Netscape : assume dirs generally. 1271 // But we do this only when the MIME type can't be known from the filename. 1272 // --> we do better than Netscape :-) 1273 if (mime.isDefault()) { 1274 qCDebug(KIO_FTP) << "Setting guessed MIME type to inode/directory for " << filename; 1275 entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory")); 1276 isDir = true; 1277 } 1278 } 1279 1280 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type); 1281 // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime); 1282 // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime); 1283 } 1284 1285 void FtpInternal::ftpShortStatAnswer(const QString &filename, bool isDir) 1286 { 1287 UDSEntry entry; 1288 1289 entry.reserve(4); 1290 entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); 1291 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG); 1292 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 1293 if (isDir) { 1294 entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 1295 } 1296 // No details about size, ownership, group, etc. 1297 1298 q->statEntry(entry); 1299 } 1300 1301 Result FtpInternal::ftpStatAnswerNotFound(const QString &path, const QString &filename) 1302 { 1303 // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source") 1304 // When e.g. uploading a file, we still need stat() to return "not found" 1305 // when the file doesn't exist. 1306 QString statSide = q->metaData(QStringLiteral("statSide")); 1307 qCDebug(KIO_FTP) << "statSide=" << statSide; 1308 if (statSide == QLatin1String("source")) { 1309 qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing"; 1310 // MS Server is incapable of handling "list <blah>" in a case insensitive way 1311 // But "retr <blah>" works. So lie in stat(), to get going... 1312 // 1313 // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run 1314 // where listing permissions are denied, but downloading is still possible. 1315 ftpShortStatAnswer(filename, false /*file, not dir*/); 1316 1317 return Result::pass(); 1318 } 1319 1320 return Result::fail(ERR_DOES_NOT_EXIST, path); 1321 } 1322 1323 Result FtpInternal::stat(const QUrl &url) 1324 { 1325 qCDebug(KIO_FTP) << "path=" << url.path(); 1326 auto result = ftpOpenConnection(LoginMode::Implicit); 1327 if (!result.success()) { 1328 return result; 1329 } 1330 1331 const QString path = ftpCleanPath(QDir::cleanPath(url.path())); 1332 qCDebug(KIO_FTP) << "cleaned path=" << path; 1333 1334 // We can't stat root, but we know it's a dir. 1335 if (path.isEmpty() || path == QLatin1String("/")) { 1336 UDSEntry entry; 1337 entry.reserve(6); 1338 // entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) ); 1339 entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); 1340 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 1341 entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 1342 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 1343 entry.fastInsert(KIO::UDSEntry::UDS_USER, QStringLiteral("root")); 1344 entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QStringLiteral("root")); 1345 // no size 1346 1347 q->statEntry(entry); 1348 return Result::pass(); 1349 } 1350 1351 QUrl tempurl(url); 1352 tempurl.setPath(path); // take the clean one 1353 QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash); 1354 QString parentDir; 1355 const QString filename = tempurl.fileName(); 1356 Q_ASSERT(!filename.isEmpty()); 1357 1358 // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info) 1359 // if it doesn't work, it's a file (and then we'll use dir filename) 1360 bool isDir = ftpFolder(path); 1361 1362 // if we're only interested in "file or directory", we should stop here 1363 QString sDetails = q->metaData(QStringLiteral("details")); 1364 int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); 1365 qCDebug(KIO_FTP) << "details=" << details; 1366 if (details == 0) { 1367 if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ? 1368 // no -> it doesn't exist at all 1369 return ftpStatAnswerNotFound(path, filename); 1370 } 1371 ftpShortStatAnswer(filename, isDir); 1372 return Result::pass(); // successfully found a dir or a file -> done 1373 } 1374 1375 if (!isDir) { 1376 // It is a file or it doesn't exist, try going to parent directory 1377 parentDir = tempurl.adjusted(QUrl::RemoveFilename).path(); 1378 // With files we can do "LIST <filename>" to avoid listing the whole dir 1379 listarg = filename; 1380 } else { 1381 // --- New implementation: 1382 // Don't list the parent dir. Too slow, might not show it, etc. 1383 // Just return that it's a dir. 1384 UDSEntry entry; 1385 entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); 1386 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 1387 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 1388 // No clue about size, ownership, group, etc. 1389 1390 q->statEntry(entry); 1391 return Result::pass(); 1392 } 1393 1394 // Now cwd the parent dir, to prepare for listing 1395 if (!ftpFolder(parentDir)) { 1396 return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, parentDir); 1397 } 1398 1399 result = ftpOpenCommand("list", listarg, 'I', ERR_DOES_NOT_EXIST); 1400 if (!result.success()) { 1401 qCritical() << "COULD NOT LIST"; 1402 return result; 1403 } 1404 qCDebug(KIO_FTP) << "Starting of list was ok"; 1405 1406 Q_ASSERT(!filename.isEmpty() && filename != QLatin1String("/")); 1407 1408 bool bFound = false; 1409 QUrl linkURL; 1410 FtpEntry ftpEnt; 1411 QList<FtpEntry> ftpValidateEntList; 1412 while (ftpReadDir(ftpEnt)) { 1413 if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(0).isSpace()) { 1414 ftpValidateEntList.append(ftpEnt); 1415 continue; 1416 } 1417 1418 // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at) 1419 // return only the filename when doing "dir /full/path/to/file" 1420 if (!bFound) { 1421 bFound = maybeEmitStatEntry(ftpEnt, filename, isDir); 1422 } 1423 qCDebug(KIO_FTP) << ftpEnt.name; 1424 } 1425 1426 for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { 1427 FtpEntry &ftpEnt = ftpValidateEntList[i]; 1428 fixupEntryName(&ftpEnt); 1429 if (maybeEmitStatEntry(ftpEnt, filename, isDir)) { 1430 break; 1431 } 1432 } 1433 1434 ftpCloseCommand(); // closes the data connection only 1435 1436 if (!bFound) { 1437 return ftpStatAnswerNotFound(path, filename); 1438 } 1439 1440 if (!linkURL.isEmpty()) { 1441 if (linkURL == url || linkURL == tempurl) { 1442 return Result::fail(ERR_CYCLIC_LINK, linkURL.toString()); 1443 } 1444 return FtpInternal::stat(linkURL); 1445 } 1446 1447 qCDebug(KIO_FTP) << "stat : finished successfully"; 1448 ; 1449 return Result::pass(); 1450 } 1451 1452 bool FtpInternal::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &filename, bool isDir) 1453 { 1454 if (filename == ftpEnt.name && !filename.isEmpty()) { 1455 UDSEntry entry; 1456 ftpCreateUDSEntry(filename, ftpEnt, entry, isDir); 1457 q->statEntry(entry); 1458 return true; 1459 } 1460 1461 return false; 1462 } 1463 1464 Result FtpInternal::listDir(const QUrl &url) 1465 { 1466 qCDebug(KIO_FTP) << url; 1467 auto result = ftpOpenConnection(LoginMode::Implicit); 1468 if (!result.success()) { 1469 return result; 1470 } 1471 1472 // No path specified ? 1473 QString path = url.path(); 1474 if (path.isEmpty()) { 1475 QUrl realURL; 1476 realURL.setScheme(QStringLiteral("ftp")); 1477 realURL.setUserName(m_user); 1478 realURL.setPassword(m_pass); 1479 realURL.setHost(m_host); 1480 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { 1481 realURL.setPort(m_port); 1482 } 1483 if (m_initialPath.isEmpty()) { 1484 m_initialPath = QStringLiteral("/"); 1485 } 1486 realURL.setPath(m_initialPath); 1487 qCDebug(KIO_FTP) << "REDIRECTION to " << realURL; 1488 q->redirection(realURL); 1489 return Result::pass(); 1490 } 1491 1492 qCDebug(KIO_FTP) << "hunting for path" << path; 1493 1494 result = ftpOpenDir(path); 1495 if (!result.success()) { 1496 if (ftpFileExists(path)) { 1497 return Result::fail(ERR_IS_FILE, path); 1498 } 1499 // not sure which to emit 1500 // error( ERR_DOES_NOT_EXIST, path ); 1501 return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, path); 1502 } 1503 1504 UDSEntry entry; 1505 FtpEntry ftpEnt; 1506 QList<FtpEntry> ftpValidateEntList; 1507 while (ftpReadDir(ftpEnt)) { 1508 qCDebug(KIO_FTP) << ftpEnt.name; 1509 // Q_ASSERT( !ftpEnt.name.isEmpty() ); 1510 if (!ftpEnt.name.isEmpty()) { 1511 if (ftpEnt.name.at(0).isSpace()) { 1512 ftpValidateEntList.append(ftpEnt); 1513 continue; 1514 } 1515 1516 // if ( S_ISDIR( (mode_t)ftpEnt.type ) ) 1517 // qDebug() << "is a dir"; 1518 // if ( !ftpEnt.link.isEmpty() ) 1519 // qDebug() << "is a link to " << ftpEnt.link; 1520 ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); 1521 q->listEntry(entry); 1522 entry.clear(); 1523 } 1524 } 1525 1526 for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { 1527 FtpEntry &ftpEnt = ftpValidateEntList[i]; 1528 fixupEntryName(&ftpEnt); 1529 ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); 1530 q->listEntry(entry); 1531 entry.clear(); 1532 } 1533 1534 ftpCloseCommand(); // closes the data connection only 1535 return Result::pass(); 1536 } 1537 1538 void FtpInternal::worker_status() 1539 { 1540 qCDebug(KIO_FTP) << "Got worker_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " [" 1541 << (m_bLoggedOn ? "Connected" : "Not connected") << "]"; 1542 q->workerStatus(m_host, m_bLoggedOn); 1543 } 1544 1545 Result FtpInternal::ftpOpenDir(const QString &path) 1546 { 1547 // QString path( _url.path(QUrl::RemoveTrailingSlash) ); 1548 1549 // We try to change to this directory first to see whether it really is a directory. 1550 // (And also to follow symlinks) 1551 QString tmp = path.isEmpty() ? QStringLiteral("/") : path; 1552 1553 // We get '550', whether it's a file or doesn't exist... 1554 if (!ftpFolder(tmp)) { 1555 return Result::fail(); 1556 } 1557 1558 // Don't use the path in the list command: 1559 // We changed into this directory anyway - so it's enough just to send "list". 1560 // We use '-a' because the application MAY be interested in dot files. 1561 // The only way to really know would be to have a metadata flag for this... 1562 // Since some windows ftp server seems not to support the -a argument, we use a fallback here. 1563 // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com) 1564 // Pass KJob::NoError first because we don't want to emit error before we 1565 // have tried all commands. 1566 auto result = ftpOpenCommand("list -la", QString(), 'I', KJob::NoError); 1567 if (!result.success()) { 1568 result = ftpOpenCommand("list", QString(), 'I', KJob::NoError); 1569 } 1570 if (!result.success()) { 1571 // Servers running with Turkish locale having problems converting 'i' letter to upper case. 1572 // So we send correct upper case command as last resort. 1573 result = ftpOpenCommand("LIST -la", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY); 1574 } 1575 1576 if (!result.success()) { 1577 qCWarning(KIO_FTP) << "Can't open for listing"; 1578 return result; 1579 } 1580 1581 qCDebug(KIO_FTP) << "Starting of list was ok"; 1582 return Result::pass(); 1583 } 1584 1585 bool FtpInternal::ftpReadDir(FtpEntry &de) 1586 { 1587 Q_ASSERT(m_data); 1588 1589 // get a line from the data connection ... 1590 while (true) { 1591 while (!m_data->canReadLine() && m_data->waitForReadyRead((q->readTimeout() * 1000))) { } 1592 QByteArray data = m_data->readLine(); 1593 if (data.size() == 0) { 1594 break; 1595 } 1596 1597 const char *buffer = data.data(); 1598 qCDebug(KIO_FTP) << "dir > " << buffer; 1599 1600 // Normally the listing looks like 1601 // -rw-r--r-- 1 dfaure dfaure 102 Nov 9 12:30 log 1602 // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442) 1603 // d [RWCEAFMS] Admin 512 Oct 13 2004 PSI 1604 1605 // we should always get the following 5 fields ... 1606 const char *p_access; 1607 const char *p_junk; 1608 const char *p_owner; 1609 const char *p_group; 1610 const char *p_size; 1611 if ((p_access = strtok((char *)buffer, " ")) == nullptr) { 1612 continue; 1613 } 1614 if ((p_junk = strtok(nullptr, " ")) == nullptr) { 1615 continue; 1616 } 1617 if ((p_owner = strtok(nullptr, " ")) == nullptr) { 1618 continue; 1619 } 1620 if ((p_group = strtok(nullptr, " ")) == nullptr) { 1621 continue; 1622 } 1623 if ((p_size = strtok(nullptr, " ")) == nullptr) { 1624 continue; 1625 } 1626 1627 qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size; 1628 1629 de.access = 0; 1630 if (qstrlen(p_access) == 1 && p_junk[0] == '[') { // Netware 1631 de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions 1632 } 1633 1634 const char *p_date_1; 1635 const char *p_date_2; 1636 const char *p_date_3; 1637 const char *p_name; 1638 1639 // A special hack for "/dev". A listing may look like this: 1640 // crw-rw-rw- 1 root root 1, 5 Jun 29 1997 zero 1641 // So we just ignore the number in front of the ",". Ok, it is a hack :-) 1642 if (strchr(p_size, ',') != nullptr) { 1643 qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)"; 1644 if ((p_size = strtok(nullptr, " ")) == nullptr) { 1645 continue; 1646 } 1647 } 1648 1649 // This is needed for ftp servers with a directory listing like this (#375610): 1650 // drwxr-xr-x folder 0 Mar 15 15:50 directory_name 1651 if (strcmp(p_junk, "folder") == 0) { 1652 p_date_1 = p_group; 1653 p_date_2 = p_size; 1654 p_size = p_owner; 1655 p_group = nullptr; 1656 p_owner = nullptr; 1657 } 1658 // Check whether the size we just read was really the size 1659 // or a month (this happens when the server lists no group) 1660 // Used to be the case on sunsite.uio.no, but not anymore 1661 // This is needed for the Netware case, too. 1662 else if (!isdigit(*p_size)) { 1663 p_date_1 = p_size; 1664 p_date_2 = strtok(nullptr, " "); 1665 p_size = p_group; 1666 p_group = nullptr; 1667 qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1; 1668 } else { 1669 p_date_1 = strtok(nullptr, " "); 1670 p_date_2 = strtok(nullptr, " "); 1671 qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1; 1672 } 1673 1674 if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(nullptr, " ")) != nullptr && (p_name = strtok(nullptr, "\r\n")) != nullptr) { 1675 { 1676 QByteArray tmp(p_name); 1677 if (p_access[0] == 'l') { 1678 int i = tmp.lastIndexOf(" -> "); 1679 if (i != -1) { 1680 de.link = q->remoteEncoding()->decode(p_name + i + 4); 1681 tmp.truncate(i); 1682 } else { 1683 de.link.clear(); 1684 } 1685 } else { 1686 de.link.clear(); 1687 } 1688 1689 if (tmp.startsWith('/')) { // listing on ftp://ftp.gnupg.org/ starts with '/' 1690 tmp.remove(0, 1); 1691 } 1692 1693 if (tmp.indexOf('/') != -1) { 1694 continue; // Don't trick us! 1695 } 1696 1697 de.name = q->remoteEncoding()->decode(tmp); 1698 } 1699 1700 de.type = S_IFREG; 1701 switch (p_access[0]) { 1702 case 'd': 1703 de.type = S_IFDIR; 1704 break; 1705 case 's': 1706 de.type = S_IFSOCK; 1707 break; 1708 case 'b': 1709 de.type = S_IFBLK; 1710 break; 1711 case 'c': 1712 de.type = S_IFCHR; 1713 break; 1714 case 'l': 1715 de.type = S_IFREG; 1716 // we don't set S_IFLNK here. de.link says it. 1717 break; 1718 default: 1719 break; 1720 } 1721 1722 if (p_access[1] == 'r') { 1723 de.access |= S_IRUSR; 1724 } 1725 if (p_access[2] == 'w') { 1726 de.access |= S_IWUSR; 1727 } 1728 if (p_access[3] == 'x' || p_access[3] == 's') { 1729 de.access |= S_IXUSR; 1730 } 1731 if (p_access[4] == 'r') { 1732 de.access |= S_IRGRP; 1733 } 1734 if (p_access[5] == 'w') { 1735 de.access |= S_IWGRP; 1736 } 1737 if (p_access[6] == 'x' || p_access[6] == 's') { 1738 de.access |= S_IXGRP; 1739 } 1740 if (p_access[7] == 'r') { 1741 de.access |= S_IROTH; 1742 } 1743 if (p_access[8] == 'w') { 1744 de.access |= S_IWOTH; 1745 } 1746 if (p_access[9] == 'x' || p_access[9] == 't') { 1747 de.access |= S_IXOTH; 1748 } 1749 if (p_access[3] == 's' || p_access[3] == 'S') { 1750 de.access |= S_ISUID; 1751 } 1752 if (p_access[6] == 's' || p_access[6] == 'S') { 1753 de.access |= S_ISGID; 1754 } 1755 if (p_access[9] == 't' || p_access[9] == 'T') { 1756 de.access |= S_ISVTX; 1757 } 1758 1759 de.owner = q->remoteEncoding()->decode(p_owner); 1760 de.group = q->remoteEncoding()->decode(p_group); 1761 de.size = charToLongLong(p_size); 1762 1763 // Parsing the date is somewhat tricky 1764 // Examples : "Oct 6 22:49", "May 13 1999" 1765 1766 // First get current date - we need the current month and year 1767 QDate currentDate(QDate::currentDate()); 1768 int currentMonth = currentDate.month(); 1769 int day = currentDate.day(); 1770 int month = currentDate.month(); 1771 int year = currentDate.year(); 1772 int minute = 0; 1773 int hour = 0; 1774 // Get day number (always second field) 1775 if (p_date_2) { 1776 day = atoi(p_date_2); 1777 } 1778 // Get month from first field 1779 // NOTE : no, we don't want to use KLocale here 1780 // It seems all FTP servers use the English way 1781 qCDebug(KIO_FTP) << "Looking for month " << p_date_1; 1782 static const char s_months[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 1783 for (int c = 0; c < 12; c++) { 1784 if (!qstrcmp(p_date_1, s_months[c])) { 1785 qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1; 1786 month = c + 1; 1787 break; 1788 } 1789 } 1790 1791 // Parse third field 1792 if (p_date_3 && !strchr(p_date_3, ':')) { // No colon, looks like a year 1793 year = atoi(p_date_3); 1794 } else { 1795 // otherwise, the year is implicit 1796 // according to man ls, this happens when it is between than 6 months 1797 // old and 1 hour in the future. 1798 // So the year is : current year if tm_mon <= currentMonth+1 1799 // otherwise current year minus one 1800 // (The +1 is a security for the "+1 hour" at the end of the month issue) 1801 if (month > currentMonth + 1) { 1802 year--; 1803 } 1804 1805 // and p_date_3 contains probably a time 1806 char *semicolon; 1807 if (p_date_3 && (semicolon = (char *)strchr(p_date_3, ':'))) { 1808 *semicolon = '\0'; 1809 minute = atoi(semicolon + 1); 1810 hour = atoi(p_date_3); 1811 } else { 1812 qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3; 1813 } 1814 } 1815 1816 de.date = QDateTime(QDate(year, month, day), QTime(hour, minute)); 1817 qCDebug(KIO_FTP) << de.date; 1818 return true; 1819 } 1820 } // line invalid, loop to get another line 1821 return false; 1822 } 1823 1824 //=============================================================================== 1825 // public: get download file from server 1826 // helper: ftpGet called from get() and copy() 1827 //=============================================================================== 1828 Result FtpInternal::get(const QUrl &url) 1829 { 1830 qCDebug(KIO_FTP) << url; 1831 const Result result = ftpGet(-1, QString(), url, 0); 1832 ftpCloseCommand(); // must close command! 1833 return result; 1834 } 1835 1836 Result FtpInternal::ftpGet(int iCopyFile, const QString &sCopyFile, const QUrl &url, KIO::fileoffset_t llOffset) 1837 { 1838 auto result = ftpOpenConnection(LoginMode::Implicit); 1839 if (!result.success()) { 1840 return result; 1841 } 1842 1843 // Try to find the size of the file (and check that it exists at 1844 // the same time). If we get back a 550, "File does not exist" 1845 // or "not a plain file", check if it is a directory. If it is a 1846 // directory, return an error; otherwise simply try to retrieve 1847 // the request... 1848 if (!ftpSize(url.path(), '?') && (m_iRespCode == 550) && ftpFolder(url.path())) { 1849 // Ok it's a dir in fact 1850 qCDebug(KIO_FTP) << "it is a directory in fact"; 1851 return Result::fail(ERR_IS_DIRECTORY); 1852 } 1853 1854 QString resumeOffset = q->metaData(QStringLiteral("range-start")); 1855 if (resumeOffset.isEmpty()) { 1856 resumeOffset = q->metaData(QStringLiteral("resume")); // old name 1857 } 1858 if (!resumeOffset.isEmpty()) { 1859 llOffset = resumeOffset.toLongLong(); 1860 qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset; 1861 } 1862 1863 result = ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset); 1864 if (!result.success()) { 1865 qCWarning(KIO_FTP) << "Can't open for reading"; 1866 return result; 1867 } 1868 1869 // Read the size from the response string 1870 if (m_size == UnknownSize) { 1871 const char *psz = strrchr(ftpResponse(4), '('); 1872 if (psz) { 1873 m_size = charToLongLong(psz + 1); 1874 } 1875 if (!m_size) { 1876 m_size = UnknownSize; 1877 } 1878 } 1879 1880 // Send the MIME type... 1881 if (iCopyFile == -1) { 1882 const auto result = ftpSendMimeType(url); 1883 if (!result.success()) { 1884 return result; 1885 } 1886 } 1887 1888 KIO::filesize_t bytesLeft = 0; 1889 if (m_size != UnknownSize) { 1890 bytesLeft = m_size - llOffset; 1891 q->totalSize(m_size); // emit the total size... 1892 } 1893 1894 qCDebug(KIO_FTP) << "starting with offset=" << llOffset; 1895 KIO::fileoffset_t processed_size = llOffset; 1896 1897 QByteArray array; 1898 char buffer[maximumIpcSize]; 1899 // start with small data chunks in case of a slow data source (modem) 1900 // - unfortunately this has a negative impact on performance for large 1901 // - files - so we will increase the block size after a while ... 1902 int iBlockSize = initialIpcSize; 1903 int iBufferCur = 0; 1904 1905 while (m_size == UnknownSize || bytesLeft > 0) { 1906 // let the buffer size grow if the file is larger 64kByte ... 1907 if (processed_size - llOffset > 1024 * 64) { 1908 iBlockSize = maximumIpcSize; 1909 } 1910 1911 // read the data and detect EOF or error ... 1912 if (iBlockSize + iBufferCur > (int)sizeof(buffer)) { 1913 iBlockSize = sizeof(buffer) - iBufferCur; 1914 } 1915 if (m_data->bytesAvailable() == 0) { 1916 m_data->waitForReadyRead((q->readTimeout() * 1000)); 1917 } 1918 int n = m_data->read(buffer + iBufferCur, iBlockSize); 1919 if (n <= 0) { 1920 // this is how we detect EOF in case of unknown size 1921 if (m_size == UnknownSize && n == 0) { 1922 break; 1923 } 1924 // unexpected eof. Happens when the daemon gets killed. 1925 return Result::fail(ERR_CANNOT_READ); 1926 } 1927 processed_size += n; 1928 1929 // collect very small data chunks in buffer before processing ... 1930 if (m_size != UnknownSize) { 1931 bytesLeft -= n; 1932 iBufferCur += n; 1933 if (iBufferCur < minimumMimeSize && bytesLeft > 0) { 1934 q->processedSize(processed_size); 1935 continue; 1936 } 1937 n = iBufferCur; 1938 iBufferCur = 0; 1939 } 1940 1941 // write output file or pass to data pump ... 1942 int writeError = 0; 1943 if (iCopyFile == -1) { 1944 array = QByteArray::fromRawData(buffer, n); 1945 q->data(array); 1946 array.clear(); 1947 } else if ((writeError = WriteToFile(iCopyFile, buffer, n)) != 0) { 1948 return Result::fail(writeError, sCopyFile); 1949 } 1950 1951 Q_ASSERT(processed_size >= 0); 1952 q->processedSize(static_cast<KIO::filesize_t>(processed_size)); 1953 } 1954 1955 qCDebug(KIO_FTP) << "done"; 1956 if (iCopyFile == -1) { // must signal EOF to data pump ... 1957 q->data(array); // array is empty and must be empty! 1958 } 1959 1960 q->processedSize(m_size == UnknownSize ? processed_size : m_size); 1961 return Result::pass(); 1962 } 1963 1964 //=============================================================================== 1965 // public: put upload file to server 1966 // helper: ftpPut called from put() and copy() 1967 //=============================================================================== 1968 Result FtpInternal::put(const QUrl &url, int permissions, KIO::JobFlags flags) 1969 { 1970 qCDebug(KIO_FTP) << url; 1971 const auto result = ftpPut(-1, url, permissions, flags); 1972 ftpCloseCommand(); // must close command! 1973 return result; 1974 } 1975 1976 Result FtpInternal::ftpPut(int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags) 1977 { 1978 const auto openResult = ftpOpenConnection(LoginMode::Implicit); 1979 if (!openResult.success()) { 1980 return openResult; 1981 } 1982 1983 // Don't use mark partial over anonymous FTP. 1984 // My incoming dir allows put but not rename... 1985 bool bMarkPartial; 1986 if (m_user.isEmpty() || m_user == QLatin1String(s_ftpLogin)) { 1987 bMarkPartial = false; 1988 } else { 1989 bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true); 1990 } 1991 1992 QString dest_orig = dest_url.path(); 1993 const QString dest_part = dest_orig + QLatin1String(".part"); 1994 1995 if (ftpSize(dest_orig, 'I')) { 1996 if (m_size == 0) { 1997 // delete files with zero size 1998 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_orig); 1999 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { 2000 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString()); 2001 } 2002 } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { 2003 return Result::fail(ERR_FILE_ALREADY_EXIST, QString()); 2004 } else if (bMarkPartial) { 2005 // when using mark partial, append .part extension 2006 const auto result = ftpRename(dest_orig, dest_part, KIO::Overwrite); 2007 if (!result.success()) { 2008 return Result::fail(ERR_CANNOT_RENAME_PARTIAL, QString()); 2009 } 2010 } 2011 // Don't chmod an existing file 2012 permissions = -1; 2013 } else if (bMarkPartial && ftpSize(dest_part, 'I')) { 2014 // file with extension .part exists 2015 if (m_size == 0) { 2016 // delete files with zero size 2017 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_part); 2018 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { 2019 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString()); 2020 } 2021 } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { 2022 flags |= q->canResume(m_size) ? KIO::Resume : KIO::DefaultFlags; 2023 if (!(flags & KIO::Resume)) { 2024 return Result::fail(ERR_FILE_ALREADY_EXIST, QString()); 2025 } 2026 } 2027 } else { 2028 m_size = 0; 2029 } 2030 2031 QString dest; 2032 2033 // if we are using marking of partial downloads -> add .part extension 2034 if (bMarkPartial) { 2035 qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig; 2036 dest = dest_part; 2037 } else { 2038 dest = dest_orig; 2039 } 2040 2041 KIO::fileoffset_t offset = 0; 2042 2043 // set the mode according to offset 2044 if ((flags & KIO::Resume) && m_size > 0) { 2045 offset = m_size; 2046 if (iCopyFile != -1) { 2047 if (QT_LSEEK(iCopyFile, offset, SEEK_SET) < 0) { 2048 return Result::fail(ERR_CANNOT_RESUME, QString()); 2049 } 2050 } 2051 } 2052 2053 const auto storResult = ftpOpenCommand("stor", dest, '?', ERR_CANNOT_WRITE, offset); 2054 if (!storResult.success()) { 2055 return storResult; 2056 } 2057 2058 qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset; 2059 KIO::fileoffset_t processed_size = offset; 2060 2061 QByteArray buffer; 2062 int result; 2063 int iBlockSize = initialIpcSize; 2064 int writeError = 0; 2065 // Loop until we got 'dataEnd' 2066 do { 2067 if (iCopyFile == -1) { 2068 q->dataReq(); // Request for data 2069 result = q->readData(buffer); 2070 } else { 2071 // let the buffer size grow if the file is larger 64kByte ... 2072 if (processed_size - offset > 1024 * 64) { 2073 iBlockSize = maximumIpcSize; 2074 } 2075 buffer.resize(iBlockSize); 2076 result = QT_READ(iCopyFile, buffer.data(), buffer.size()); 2077 if (result < 0) { 2078 writeError = ERR_CANNOT_READ; 2079 } else { 2080 buffer.resize(result); 2081 } 2082 } 2083 2084 if (result > 0) { 2085 m_data->write(buffer); 2086 while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) { } 2087 processed_size += result; 2088 q->processedSize(processed_size); 2089 } 2090 } while (result > 0); 2091 2092 if (result != 0) { // error 2093 ftpCloseCommand(); // don't care about errors 2094 qCDebug(KIO_FTP) << "Error during 'put'. Aborting."; 2095 if (bMarkPartial) { 2096 // Remove if smaller than minimum size 2097 if (ftpSize(dest, 'I') && (processed_size < q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE))) { 2098 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest); 2099 (void)ftpSendCmd(cmd); 2100 } 2101 } 2102 return Result::fail(writeError, dest_url.toString()); 2103 } 2104 2105 if (!ftpCloseCommand()) { 2106 return Result::fail(ERR_CANNOT_WRITE); 2107 } 2108 2109 // after full download rename the file back to original name 2110 if (bMarkPartial) { 2111 qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")"; 2112 const auto result = ftpRename(dest, dest_orig, KIO::Overwrite); 2113 if (!result.success()) { 2114 return Result::fail(ERR_CANNOT_RENAME_PARTIAL); 2115 } 2116 } 2117 2118 // set final permissions 2119 if (permissions != -1) { 2120 if (m_user == QLatin1String(s_ftpLogin)) { 2121 qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???"; 2122 } 2123 // chmod the file we just put 2124 if (!ftpChmod(dest_orig, permissions)) { 2125 // To be tested 2126 // if ( m_user != s_ftpLogin ) 2127 // warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) ); 2128 } 2129 } 2130 2131 return Result::pass(); 2132 } 2133 2134 /** Use the SIZE command to get the file size. 2135 Warning : the size depends on the transfer mode, hence the second arg. */ 2136 bool FtpInternal::ftpSize(const QString &path, char mode) 2137 { 2138 m_size = UnknownSize; 2139 if (!ftpDataMode(mode)) { 2140 return false; 2141 } 2142 2143 const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path); 2144 if (!ftpSendCmd(buf) || (m_iRespType != 2)) { 2145 return false; 2146 } 2147 2148 // skip leading "213 " (response code) 2149 QByteArray psz(ftpResponse(4)); 2150 if (psz.isEmpty()) { 2151 return false; 2152 } 2153 bool ok = false; 2154 m_size = psz.trimmed().toLongLong(&ok); 2155 if (!ok) { 2156 m_size = UnknownSize; 2157 } 2158 return true; 2159 } 2160 2161 bool FtpInternal::ftpFileExists(const QString &path) 2162 { 2163 const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path); 2164 if (!ftpSendCmd(buf) || (m_iRespType != 2)) { 2165 return false; 2166 } 2167 2168 // skip leading "213 " (response code) 2169 const char *psz = ftpResponse(4); 2170 return psz != nullptr; 2171 } 2172 2173 // Today the differences between ASCII and BINARY are limited to 2174 // CR or CR/LF line terminators. Many servers ignore ASCII (like 2175 // win2003 -or- vsftp with default config). In the early days of 2176 // computing, when even text-files had structure, this stuff was 2177 // more important. 2178 // Theoretically "list" could return different results in ASCII 2179 // and BINARY mode. But again, most servers ignore ASCII here. 2180 bool FtpInternal::ftpDataMode(char cMode) 2181 { 2182 if (cMode == '?') { 2183 cMode = m_bTextMode ? 'A' : 'I'; 2184 } else if (cMode == 'a') { 2185 cMode = 'A'; 2186 } else if (cMode != 'A') { 2187 cMode = 'I'; 2188 } 2189 2190 qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode; 2191 if (m_cDataMode == cMode) { 2192 return true; 2193 } 2194 2195 const QByteArray buf = QByteArrayLiteral("TYPE ") + cMode; 2196 if (!ftpSendCmd(buf) || (m_iRespType != 2)) { 2197 return false; 2198 } 2199 m_cDataMode = cMode; 2200 return true; 2201 } 2202 2203 bool FtpInternal::ftpFolder(const QString &path) 2204 { 2205 QString newPath = path; 2206 int iLen = newPath.length(); 2207 if (iLen > 1 && newPath[iLen - 1] == QLatin1Char('/')) { 2208 newPath.chop(1); 2209 } 2210 2211 qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath; 2212 if (m_currentPath == newPath) { 2213 return true; 2214 } 2215 2216 const QByteArray tmp = "cwd " + q->remoteEncoding()->encode(newPath); 2217 if (!ftpSendCmd(tmp)) { 2218 return false; // connection failure 2219 } 2220 if (m_iRespType != 2) { 2221 return false; // not a folder 2222 } 2223 m_currentPath = newPath; 2224 return true; 2225 } 2226 2227 //=============================================================================== 2228 // public: copy don't use kio data pump if one side is a local file 2229 // helper: ftpCopyPut called from copy() on upload 2230 // helper: ftpCopyGet called from copy() on download 2231 //=============================================================================== 2232 Result FtpInternal::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags) 2233 { 2234 int iCopyFile = -1; 2235 bool bSrcLocal = src.isLocalFile(); 2236 bool bDestLocal = dest.isLocalFile(); 2237 QString sCopyFile; 2238 2239 Result result = Result::pass(); 2240 if (bSrcLocal && !bDestLocal) { // File -> Ftp 2241 sCopyFile = src.toLocalFile(); 2242 qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path(); 2243 result = ftpCopyPut(iCopyFile, sCopyFile, dest, permissions, flags); 2244 } else if (!bSrcLocal && bDestLocal) { // Ftp -> File 2245 sCopyFile = dest.toLocalFile(); 2246 qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile; 2247 result = ftpCopyGet(iCopyFile, sCopyFile, src, permissions, flags); 2248 } else { 2249 return Result::fail(ERR_UNSUPPORTED_ACTION, QString()); 2250 } 2251 2252 // perform clean-ups and report error (if any) 2253 if (iCopyFile != -1) { 2254 QT_CLOSE(iCopyFile); 2255 } 2256 ftpCloseCommand(); // must close command! 2257 2258 return result; 2259 } 2260 2261 bool FtpInternal::isSocksProxyScheme(const QString &scheme) 2262 { 2263 return scheme == QLatin1String("socks") || scheme == QLatin1String("socks5"); 2264 } 2265 2266 bool FtpInternal::isSocksProxy() const 2267 { 2268 return isSocksProxyScheme(m_proxyURL.scheme()); 2269 } 2270 2271 Result FtpInternal::ftpCopyPut(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) 2272 { 2273 // check if source is ok ... 2274 QFileInfo info(sCopyFile); 2275 bool bSrcExists = info.exists(); 2276 if (bSrcExists) { 2277 if (info.isDir()) { 2278 return Result::fail(ERR_IS_DIRECTORY); 2279 } 2280 } else { 2281 return Result::fail(ERR_DOES_NOT_EXIST); 2282 } 2283 2284 iCopyFile = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY); 2285 if (iCopyFile == -1) { 2286 return Result::fail(ERR_CANNOT_OPEN_FOR_READING); 2287 } 2288 2289 // delegate the real work (iError gets status) ... 2290 q->totalSize(info.size()); 2291 if (s_enableCanResume) { 2292 return ftpPut(iCopyFile, url, permissions, flags & ~KIO::Resume); 2293 } else { 2294 return ftpPut(iCopyFile, url, permissions, flags | KIO::Resume); 2295 } 2296 } 2297 2298 Result FtpInternal::ftpCopyGet(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) 2299 { 2300 // check if destination is ok ... 2301 QFileInfo info(sCopyFile); 2302 const bool bDestExists = info.exists(); 2303 if (bDestExists) { 2304 if (info.isDir()) { 2305 return Result::fail(ERR_IS_DIRECTORY); 2306 } 2307 if (!(flags & KIO::Overwrite)) { 2308 return Result::fail(ERR_FILE_ALREADY_EXIST); 2309 } 2310 } 2311 2312 // do we have a ".part" file? 2313 const QString sPart = sCopyFile + QLatin1String(".part"); 2314 bool bResume = false; 2315 QFileInfo sPartInfo(sPart); 2316 const bool bPartExists = sPartInfo.exists(); 2317 const bool bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true); 2318 const QString dest = bMarkPartial ? sPart : sCopyFile; 2319 if (bMarkPartial && bPartExists && sPartInfo.size() > 0) { 2320 // must not be a folder! please fix a similar bug in kio_file!! 2321 if (sPartInfo.isDir()) { 2322 return Result::fail(ERR_DIR_ALREADY_EXIST); 2323 } 2324 // doesn't work for copy? -> design flaw? 2325 bResume = s_enableCanResume ? q->canResume(sPartInfo.size()) : true; 2326 } 2327 2328 if (bPartExists && !bResume) { // get rid of an unwanted ".part" file 2329 QFile::remove(sPart); 2330 } 2331 2332 // WABA: Make sure that we keep writing permissions ourselves, 2333 // otherwise we can be in for a surprise on NFS. 2334 mode_t initialMode; 2335 if (permissions >= 0) { 2336 initialMode = static_cast<mode_t>(permissions | S_IWUSR); 2337 } else { 2338 initialMode = 0666; 2339 } 2340 2341 // open the output file ... 2342 KIO::fileoffset_t hCopyOffset = 0; 2343 if (bResume) { 2344 iCopyFile = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming 2345 hCopyOffset = QT_LSEEK(iCopyFile, 0, SEEK_END); 2346 if (hCopyOffset < 0) { 2347 return Result::fail(ERR_CANNOT_RESUME); 2348 } 2349 qCDebug(KIO_FTP) << "resuming at " << hCopyOffset; 2350 } else { 2351 iCopyFile = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); 2352 } 2353 2354 if (iCopyFile == -1) { 2355 qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile; 2356 const int error = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING; 2357 return Result::fail(error); 2358 } 2359 2360 // delegate the real work (iError gets status) ... 2361 auto result = ftpGet(iCopyFile, sCopyFile, url, hCopyOffset); 2362 2363 if (QT_CLOSE(iCopyFile) == 0 && !result.success()) { 2364 // If closing the file failed but there isn't an error yet, switch 2365 // into an error! 2366 result = Result::fail(ERR_CANNOT_WRITE); 2367 } 2368 iCopyFile = -1; 2369 2370 // handle renaming or deletion of a partial file ... 2371 if (bMarkPartial) { 2372 if (result.success()) { 2373 // rename ".part" on success 2374 if (!QFile::rename(sPart, sCopyFile)) { 2375 // If rename fails, try removing the destination first if it exists. 2376 if (!bDestExists || !(QFile::remove(sCopyFile) && QFile::rename(sPart, sCopyFile))) { 2377 qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile; 2378 result = Result::fail(ERR_CANNOT_RENAME_PARTIAL); 2379 } 2380 } 2381 } else { 2382 sPartInfo.refresh(); 2383 if (sPartInfo.exists()) { // should a very small ".part" be deleted? 2384 int size = q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE); 2385 if (sPartInfo.size() < size) { 2386 QFile::remove(sPart); 2387 } 2388 } 2389 } 2390 } 2391 2392 if (result.success()) { 2393 const QString mtimeStr = q->metaData(QStringLiteral("modified")); 2394 if (!mtimeStr.isEmpty()) { 2395 QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); 2396 if (dt.isValid()) { 2397 qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr; 2398 struct utimbuf utbuf; 2399 info.refresh(); 2400 utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged 2401 utbuf.modtime = dt.toSecsSinceEpoch(); // modification time 2402 ::utime(QFile::encodeName(sCopyFile).constData(), &utbuf); 2403 } 2404 } 2405 } 2406 2407 return result; 2408 } 2409 2410 Result FtpInternal::ftpSendMimeType(const QUrl &url) 2411 { 2412 const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : static_cast<int>(m_size)); 2413 QByteArray buffer(totalSize, '\0'); 2414 2415 while (true) { 2416 // Wait for content to be available... 2417 if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((q->readTimeout() * 1000))) { 2418 return Result::fail(ERR_CANNOT_READ, url.toString()); 2419 } 2420 2421 const qint64 bytesRead = m_data->peek(buffer.data(), totalSize); 2422 2423 // If we got a -1, it must be an error so return an error. 2424 if (bytesRead == -1) { 2425 return Result::fail(ERR_CANNOT_READ, url.toString()); 2426 } 2427 2428 // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size 2429 // equal to the size we want, then break. 2430 if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) { 2431 break; 2432 } 2433 } 2434 2435 if (!buffer.isEmpty()) { 2436 QMimeDatabase db; 2437 QMimeType mime = db.mimeTypeForFileNameAndData(url.path(), buffer); 2438 qCDebug(KIO_FTP) << "Emitting MIME type" << mime.name(); 2439 q->mimeType(mime.name()); // emit the MIME type... 2440 } 2441 2442 return Result::pass(); 2443 } 2444 2445 void FtpInternal::fixupEntryName(FtpEntry *e) 2446 { 2447 Q_ASSERT(e); 2448 if (e->type == S_IFDIR) { 2449 if (!ftpFolder(e->name)) { 2450 QString name(e->name.trimmed()); 2451 if (ftpFolder(name)) { 2452 e->name = name; 2453 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; 2454 } else { 2455 int index = 0; 2456 while (e->name.at(index).isSpace()) { 2457 index++; 2458 name = e->name.mid(index); 2459 if (ftpFolder(name)) { 2460 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; 2461 e->name = name; 2462 break; 2463 } 2464 } 2465 } 2466 } 2467 } else { 2468 if (!ftpFileExists(e->name)) { 2469 QString name(e->name.trimmed()); 2470 if (ftpFileExists(name)) { 2471 e->name = name; 2472 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; 2473 } else { 2474 int index = 0; 2475 while (e->name.at(index).isSpace()) { 2476 index++; 2477 name = e->name.mid(index); 2478 if (ftpFileExists(name)) { 2479 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; 2480 e->name = name; 2481 break; 2482 } 2483 } 2484 } 2485 } 2486 } 2487 } 2488 2489 ConnectionResult FtpInternal::synchronousConnectToHost(const QString &host, quint16 port) 2490 { 2491 const QUrl proxyUrl = m_proxyURL; 2492 QNetworkProxy proxy; 2493 if (!proxyUrl.isEmpty()) { 2494 proxy = QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyUrl.host(), static_cast<quint16>(proxyUrl.port(0)), proxyUrl.userName(), proxyUrl.password()); 2495 } 2496 2497 QTcpSocket *socket = new QSslSocket; 2498 socket->setProxy(proxy); 2499 socket->connectToHost(host, port); 2500 socket->waitForConnected(q->connectTimeout() * 1000); 2501 const auto socketError = socket->error(); 2502 if (socketError == QAbstractSocket::ProxyAuthenticationRequiredError) { 2503 AuthInfo info; 2504 info.url = proxyUrl; 2505 info.verifyPath = true; // ### whatever 2506 2507 if (!q->checkCachedAuthentication(info)) { 2508 info.prompt = i18n( 2509 "You need to supply a username and a password for " 2510 "the proxy server listed below before you are allowed " 2511 "to access any sites."); 2512 info.keepPassword = true; 2513 info.commentLabel = i18n("Proxy:"); 2514 info.comment = i18n("<b>%1</b>", proxy.hostName()); 2515 2516 const int errorCode = q->openPasswordDialog(info, i18n("Proxy Authentication Failed.")); 2517 if (errorCode != KJob::NoError) { 2518 qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error." << errorCode; 2519 return ConnectionResult{socket, Result::fail(errorCode, proxyUrl.toString())}; 2520 } 2521 } 2522 2523 proxy.setUser(info.username); 2524 proxy.setPassword(info.password); 2525 2526 delete socket; 2527 socket = new QSslSocket; 2528 socket->setProxy(proxy); 2529 socket->connectToHost(host, port); 2530 socket->waitForConnected(q->connectTimeout() * 1000); 2531 2532 if (socket->state() == QAbstractSocket::ConnectedState) { 2533 // reconnect with credentials was successful -> save data 2534 q->cacheAuthentication(info); 2535 2536 m_proxyURL.setUserName(info.username); 2537 m_proxyURL.setPassword(info.password); 2538 } 2539 } 2540 2541 return ConnectionResult{socket, Result::pass()}; 2542 } 2543 2544 //=============================================================================== 2545 // Ftp 2546 //=============================================================================== 2547 2548 Ftp::Ftp(const QByteArray &pool, const QByteArray &app) 2549 : WorkerBase(QByteArrayLiteral("ftp"), pool, app) 2550 , d(new FtpInternal(this)) 2551 { 2552 } 2553 2554 Ftp::~Ftp() = default; 2555 2556 void Ftp::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) 2557 { 2558 d->setHost(host, port, user, pass); 2559 } 2560 2561 KIO::WorkerResult Ftp::openConnection() 2562 { 2563 return d->openConnection(); 2564 } 2565 2566 void Ftp::closeConnection() 2567 { 2568 d->closeConnection(); 2569 } 2570 2571 KIO::WorkerResult Ftp::stat(const QUrl &url) 2572 { 2573 return d->stat(url); 2574 } 2575 2576 KIO::WorkerResult Ftp::listDir(const QUrl &url) 2577 { 2578 return d->listDir(url); 2579 } 2580 2581 KIO::WorkerResult Ftp::mkdir(const QUrl &url, int permissions) 2582 { 2583 return d->mkdir(url, permissions); 2584 } 2585 2586 KIO::WorkerResult Ftp::rename(const QUrl &src, const QUrl &dst, JobFlags flags) 2587 { 2588 return d->rename(src, dst, flags); 2589 } 2590 2591 KIO::WorkerResult Ftp::del(const QUrl &url, bool isfile) 2592 { 2593 return d->del(url, isfile); 2594 } 2595 2596 KIO::WorkerResult Ftp::chmod(const QUrl &url, int permissions) 2597 { 2598 return d->chmod(url, permissions); 2599 } 2600 2601 KIO::WorkerResult Ftp::get(const QUrl &url) 2602 { 2603 return d->get(url); 2604 } 2605 2606 KIO::WorkerResult Ftp::put(const QUrl &url, int permissions, JobFlags flags) 2607 { 2608 return d->put(url, permissions, flags); 2609 } 2610 2611 void Ftp::worker_status() 2612 { 2613 d->worker_status(); 2614 } 2615 2616 KIO::WorkerResult Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags) 2617 { 2618 return d->copy(src, dest, permissions, flags); 2619 } 2620 2621 QDebug operator<<(QDebug dbg, const Result &r) 2622 2623 { 2624 QDebugStateSaver saver(dbg); 2625 dbg.nospace() << "Result(" 2626 << "success=" << r.success() << ", err=" << r.error() << ", str=" << r.errorString() << ')'; 2627 return dbg; 2628 } 2629 2630 // needed for JSON file embedding 2631 #include "ftp.moc" 2632 2633 #include "moc_ftp.cpp"