1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org>
4     SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 /*
10     Recommended reading explaining FTP details and quirks:
11       http://cr.yp.to/ftp.html  (by D.J. Bernstein)
12 
13     RFC:
14       RFC  959 "File Transfer Protocol (FTP)"
15       RFC 1635 "How to Use Anonymous FTP"
16       RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV)
17 */
18 
19 #include <config-kioslave-ftp.h>
20 
21 #include "ftp.h"
22 
23 #ifdef Q_OS_WIN
24 #include <sys/utime.h>
25 #else
26 #include <utime.h>
27 #endif
28 
29 #include <cctype>
30 #include <cerrno>
31 #include <cstdlib>
32 #include <cstring>
33 
34 #include <QAuthenticator>
35 #include <QCoreApplication>
36 #include <QDir>
37 #include <QHostAddress>
38 #include <QMimeDatabase>
39 #include <QNetworkProxy>
40 #include <QSslSocket>
41 #include <QTcpServer>
42 #include <QTcpSocket>
43 
44 #include <KConfigGroup>
45 #include <KLocalizedString>
46 #include <QDebug>
47 #include <ioslave_defaults.h>
48 #include <kremoteencoding.h>
49 
50 #include "kioglobal_p.h"
51 
52 #include <QLoggingCategory>
53 Q_DECLARE_LOGGING_CATEGORY(KIO_FTP)
54 Q_LOGGING_CATEGORY(KIO_FTP, "kf.kio.slaves.ftp", QtWarningMsg)
55 
56 #if HAVE_STRTOLL
57 #define charToLongLong(a) strtoll(a, nullptr, 10)
58 #else
59 #define charToLongLong(a) strtol(a, nullptr, 10)
60 #endif
61 
62 static constexpr char s_ftpLogin[] = "anonymous";
63 static constexpr char s_ftpPasswd[] = "anonymous@";
64 
65 static constexpr bool s_enableCanResume = true;
66 
67 // Pseudo plugin class to embed meta data
68 class KIOPluginForMetaData : public QObject
69 {
70     Q_OBJECT
71     Q_PLUGIN_METADATA(IID "org.kde.kio.slave.ftp" FILE "ftp.json")
72 };
73 
ftpCleanPath(const QString & path)74 static QString ftpCleanPath(const QString &path)
75 {
76     if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive)
77         || path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) {
78         return path.left((path.length() - qstrlen(";type=X")));
79     }
80 
81     return path;
82 }
83 
ftpModeFromPath(const QString & path,char defaultMode='\\0')84 static char ftpModeFromPath(const QString &path, char defaultMode = '\0')
85 {
86     const int index = path.lastIndexOf(QLatin1String(";type="));
87 
88     if (index > -1 && (index + 6) < path.size()) {
89         const QChar mode = path.at(index + 6);
90         // kio_ftp supports only A (ASCII) and I(BINARY) modes.
91         if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) {
92             return mode.toUpper().toLatin1();
93         }
94     }
95 
96     return defaultMode;
97 }
98 
supportedProxyScheme(const QString & scheme)99 static bool supportedProxyScheme(const QString &scheme)
100 {
101     return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks"));
102 }
103 
104 // JPF: somebody should find a better solution for this or move this to KIO
105 namespace KIO
106 {
107 enum buffersizes {
108     /**
109      * largest buffer size that should be used to transfer data between
110      * KIO slaves using the data() function
111      */
112     maximumIpcSize = 32 * 1024,
113     /**
114      * this is a reasonable value for an initial read() that a KIO slave
115      * can do to obtain data via a slow network connection.
116      */
117     initialIpcSize = 2 * 1024,
118     /**
119      * recommended size of a data block passed to findBufferFileType()
120      */
121     minimumMimeSize = 1024,
122 };
123 
124 // JPF: this helper was derived from write_all in file.cc (FileProtocol).
125 static // JPF: in ftp.cc we make it static
126     /**
127      * This helper handles some special issues (blocking and interrupted
128      * system call) when writing to a file handle.
129      *
130      * @return 0 on success or an error code on failure (ERR_CANNOT_WRITE,
131      * ERR_DISK_FULL, ERR_CONNECTION_BROKEN).
132      */
133     int
WriteToFile(int fd,const char * buf,size_t len)134     WriteToFile(int fd, const char *buf, size_t len)
135 {
136     while (len > 0) {
137         // JPF: shouldn't there be a KDE_write?
138         ssize_t written = write(fd, buf, len);
139         if (written >= 0) {
140             buf += written;
141             len -= written;
142             continue;
143         }
144         switch (errno) {
145         case EINTR:
146             continue;
147         case EPIPE:
148             return ERR_CONNECTION_BROKEN;
149         case ENOSPC:
150             return ERR_DISK_FULL;
151         default:
152             return ERR_CANNOT_WRITE;
153         }
154     }
155     return 0;
156 }
157 }
158 
159 const KIO::filesize_t FtpInternal::UnknownSize = (KIO::filesize_t)-1;
160 
161 using namespace KIO;
162 
kdemain(int argc,char ** argv)163 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
164 {
165     QCoreApplication app(argc, argv);
166     app.setApplicationName(QStringLiteral("kio_ftp"));
167 
168     qCDebug(KIO_FTP) << "Starting";
169 
170     if (argc != 4) {
171         fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n");
172         exit(-1);
173     }
174 
175     Ftp slave(argv[2], argv[3]);
176     slave.dispatchLoop();
177 
178     qCDebug(KIO_FTP) << "Done";
179     return 0;
180 }
181 
182 //===============================================================================
183 // FtpInternal
184 //===============================================================================
185 
186 /**
187  * This closes a data connection opened by ftpOpenDataConnection().
188  */
ftpCloseDataConnection()189 void FtpInternal::ftpCloseDataConnection()
190 {
191     delete m_data;
192     m_data = nullptr;
193     delete m_server;
194     m_server = nullptr;
195 }
196 
197 /**
198  * This closes a control connection opened by ftpOpenControlConnection() and reinits the
199  * related states.  This method gets called from the constructor with m_control = nullptr.
200  */
ftpCloseControlConnection()201 void FtpInternal::ftpCloseControlConnection()
202 {
203     m_extControl = 0;
204     delete m_control;
205     m_control = nullptr;
206     m_cDataMode = 0;
207     m_bLoggedOn = false; // logon needs control connection
208     m_bTextMode = false;
209     m_bBusy = false;
210 }
211 
212 /**
213  * Returns the last response from the server (iOffset >= 0)  -or-  reads a new response
214  * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0).
215  */
ftpResponse(int iOffset)216 const char *FtpInternal::ftpResponse(int iOffset)
217 {
218     Q_ASSERT(m_control); // must have control connection socket
219     const char *pTxt = m_lastControlLine.data();
220 
221     // read the next line ...
222     if (iOffset < 0) {
223         int iMore = 0;
224         m_iRespCode = 0;
225 
226         if (!pTxt) {
227             return nullptr; // avoid using a nullptr when calling atoi.
228         }
229 
230         // If the server sends a multiline response starting with
231         // "nnn-text" we loop here until a final "nnn text" line is
232         // reached. Only data from the final line will be stored.
233         do {
234             while (!m_control->canReadLine() && m_control->waitForReadyRead((q->readTimeout() * 1000))) { }
235             m_lastControlLine = m_control->readLine();
236             pTxt = m_lastControlLine.data();
237             int iCode = atoi(pTxt);
238             if (iMore == 0) {
239                 // first line
240                 qCDebug(KIO_FTP) << "    > " << pTxt;
241                 if (iCode >= 100) {
242                     m_iRespCode = iCode;
243                     if (pTxt[3] == '-') {
244                         // marker for a multiple line response
245                         iMore = iCode;
246                     }
247                 } else {
248                     qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt;
249                 }
250             } else {
251                 // multi-line
252                 qCDebug(KIO_FTP) << "    > " << pTxt;
253                 if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') {
254                     iMore = 0;
255                 }
256             }
257         } while (iMore != 0);
258         qCDebug(KIO_FTP) << "resp> " << pTxt;
259 
260         m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0;
261     }
262 
263     // return text with offset ...
264     while (iOffset-- > 0 && pTxt[0]) {
265         pTxt++;
266     }
267     return pTxt;
268 }
269 
closeConnection()270 void FtpInternal::closeConnection()
271 {
272     if (m_control || m_data) {
273         qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy;
274     }
275 
276     if (m_bBusy) { // ftpCloseCommand not called
277         qCWarning(KIO_FTP) << "Abandoned data stream";
278         ftpCloseDataConnection();
279     }
280 
281     if (m_bLoggedOn) { // send quit
282         if (!ftpSendCmd(QByteArrayLiteral("quit"), 0) || (m_iRespType != 2)) {
283             qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode;
284         }
285     }
286 
287     // close the data and control connections ...
288     ftpCloseDataConnection();
289     ftpCloseControlConnection();
290 }
291 
FtpInternal(Ftp * qptr)292 FtpInternal::FtpInternal(Ftp *qptr)
293     : QObject()
294     , q(qptr)
295 {
296     ftpCloseControlConnection();
297 }
298 
~FtpInternal()299 FtpInternal::~FtpInternal()
300 {
301     qCDebug(KIO_FTP);
302     closeConnection();
303 }
304 
setHost(const QString & _host,quint16 _port,const QString & _user,const QString & _pass)305 void FtpInternal::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass)
306 {
307     qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user;
308 
309     m_proxyURL.clear();
310     m_proxyUrls = q->mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
311 
312     qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls;
313 
314     if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) {
315         closeConnection();
316     }
317 
318     m_host = _host;
319     m_port = _port;
320     m_user = _user;
321     m_pass = _pass;
322 }
323 
openConnection()324 Result FtpInternal::openConnection()
325 {
326     return ftpOpenConnection(LoginMode::Explicit);
327 }
328 
ftpOpenConnection(LoginMode loginMode)329 Result FtpInternal::ftpOpenConnection(LoginMode loginMode)
330 {
331     // check for implicit login if we are already logged on ...
332     if (loginMode == LoginMode::Implicit && m_bLoggedOn) {
333         Q_ASSERT(m_control); // must have control connection socket
334         return Result::pass();
335     }
336 
337     qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]";
338 
339     q->infoMessage(i18n("Opening connection to host %1", m_host));
340 
341     if (m_host.isEmpty()) {
342         return Result::fail(ERR_UNKNOWN_HOST);
343     }
344 
345     Q_ASSERT(!m_bLoggedOn);
346 
347     m_initialPath.clear();
348     m_currentPath.clear();
349 
350     const Result result = ftpOpenControlConnection();
351     if (!result.success) {
352         return result;
353     }
354     q->infoMessage(i18n("Connected to host %1", m_host));
355 
356     bool userNameChanged = false;
357     if (loginMode != LoginMode::Deferred) {
358         const Result result = ftpLogin(&userNameChanged);
359         m_bLoggedOn = result.success;
360         if (!m_bLoggedOn) {
361             return result;
362         }
363     }
364 
365     m_bTextMode = q->configValue(QStringLiteral("textmode"), false);
366     q->connected();
367 
368     // Redirected due to credential change...
369     if (userNameChanged && m_bLoggedOn) {
370         QUrl realURL;
371         realURL.setScheme(QStringLiteral("ftp"));
372         if (m_user != QLatin1String(s_ftpLogin)) {
373             realURL.setUserName(m_user);
374         }
375         if (m_pass != QLatin1String(s_ftpPasswd)) {
376             realURL.setPassword(m_pass);
377         }
378         realURL.setHost(m_host);
379         if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
380             realURL.setPort(m_port);
381         }
382         if (m_initialPath.isEmpty()) {
383             m_initialPath = QStringLiteral("/");
384         }
385         realURL.setPath(m_initialPath);
386         qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL;
387         q->redirection(realURL);
388         return Result::fail();
389     }
390 
391     return Result::pass();
392 }
393 
394 /**
395  * Called by @ref openConnection. It opens the control connection to the ftp server.
396  *
397  * @return true on success.
398  */
ftpOpenControlConnection()399 Result FtpInternal::ftpOpenControlConnection()
400 {
401     if (m_proxyUrls.isEmpty()) {
402         return ftpOpenControlConnection(m_host, m_port);
403     }
404 
405     Result result = Result::fail();
406 
407     for (const QString &proxyUrl : std::as_const(m_proxyUrls)) {
408         const QUrl url(proxyUrl);
409         const QString scheme(url.scheme());
410 
411         if (!supportedProxyScheme(scheme)) {
412             // TODO: Need a new error code to indicate unsupported URL scheme.
413             result = Result::fail(ERR_CANNOT_CONNECT, url.toString());
414             continue;
415         }
416 
417         if (!isSocksProxyScheme(scheme)) {
418             const Result result = ftpOpenControlConnection(url.host(), url.port());
419             if (result.success) {
420                 return Result::pass();
421             }
422             continue;
423         }
424 
425         qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url;
426         m_proxyURL = url;
427         result = ftpOpenControlConnection(m_host, m_port);
428         if (result.success) {
429             return result;
430         }
431         m_proxyURL.clear();
432     }
433 
434     return result;
435 }
436 
ftpOpenControlConnection(const QString & host,int port)437 Result FtpInternal::ftpOpenControlConnection(const QString &host, int port)
438 {
439     // implicitly close, then try to open a new connection ...
440     closeConnection();
441     QString sErrorMsg;
442 
443     // now connect to the server and read the login message ...
444     if (port == 0) {
445         port = 21; // default FTP port
446     }
447     const auto connectionResult = synchronousConnectToHost(host, port);
448     m_control = connectionResult.socket;
449 
450     int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT;
451     if (!connectionResult.result.success) {
452         qDebug() << "overriding error code!!1" << connectionResult.result.error;
453         iErrorCode = connectionResult.result.error;
454         sErrorMsg = connectionResult.result.errorString;
455     }
456 
457     // on connect success try to read the server message...
458     if (iErrorCode == 0) {
459         const char *psz = ftpResponse(-1);
460         if (m_iRespType != 2) {
461             // login not successful, do we have an message text?
462             if (psz[0]) {
463                 sErrorMsg = i18n("%1 (Error %2)", host, q->remoteEncoding()->decode(psz).trimmed());
464             }
465             iErrorCode = ERR_CANNOT_CONNECT;
466         }
467     } else {
468         const auto socketError = m_control->error();
469         if (socketError == QAbstractSocket::HostNotFoundError) {
470             iErrorCode = ERR_UNKNOWN_HOST;
471         }
472 
473         sErrorMsg = QStringLiteral("%1: %2").arg(host, m_control->errorString());
474     }
475 
476     // if there was a problem - report it ...
477     if (iErrorCode == 0) { // OK, return success
478         return Result::pass();
479     }
480     closeConnection(); // clean-up on error
481     return Result::fail(iErrorCode, sErrorMsg);
482 }
483 
484 /**
485  * Called by @ref openConnection. It logs us in.
486  * @ref m_initialPath is set to the current working directory
487  * if logging on was successful.
488  *
489  * @return true on success.
490  */
ftpLogin(bool * userChanged)491 Result FtpInternal::ftpLogin(bool *userChanged)
492 {
493     q->infoMessage(i18n("Sending login information"));
494 
495     Q_ASSERT(!m_bLoggedOn);
496 
497     QString user(m_user);
498     QString pass(m_pass);
499 
500     if (q->configValue(QStringLiteral("EnableAutoLogin"), false)) {
501         QString au = q->configValue(QStringLiteral("autoLoginUser"));
502         if (!au.isEmpty()) {
503             user = au;
504             pass = q->configValue(QStringLiteral("autoLoginPass"));
505         }
506     }
507 
508     AuthInfo info;
509     info.url.setScheme(QStringLiteral("ftp"));
510     info.url.setHost(m_host);
511     if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
512         info.url.setPort(m_port);
513     }
514     if (!user.isEmpty()) {
515         info.url.setUserName(user);
516     }
517 
518     // Check for cached authentication first and fallback to
519     // anonymous login when no stored credentials are found.
520     if (!q->configValue(QStringLiteral("TryAnonymousLoginFirst"), false) && pass.isEmpty() && q->checkCachedAuthentication(info)) {
521         user = info.username;
522         pass = info.password;
523     }
524 
525     // Try anonymous login if both username/password
526     // information is blank.
527     if (user.isEmpty() && pass.isEmpty()) {
528         user = QString::fromLatin1(s_ftpLogin);
529         pass = QString::fromLatin1(s_ftpPasswd);
530     }
531 
532     QByteArray tempbuf;
533     QString lastServerResponse;
534     int failedAuth = 0;
535     bool promptForRetry = false;
536 
537     // Give the user the option to login anonymously...
538     info.setExtraField(QStringLiteral("anonymous"), false);
539 
540     do {
541         // Check the cache and/or prompt user for password if 1st
542         // login attempt failed OR the user supplied a login name,
543         // but no password.
544         if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) {
545             QString errorMsg;
546             qCDebug(KIO_FTP) << "Prompting user for login info...";
547 
548             // Ask user if we should retry after when login fails!
549             if (failedAuth > 0 && promptForRetry) {
550                 errorMsg = i18n(
551                     "Message sent:\nLogin using username=%1 and "
552                     "password=[hidden]\n\nServer replied:\n%2\n\n",
553                     user,
554                     lastServerResponse);
555             }
556 
557             if (user != QLatin1String(s_ftpLogin)) {
558                 info.username = user;
559             }
560 
561             info.prompt = i18n(
562                 "You need to supply a username and a password "
563                 "to access this site.");
564             info.commentLabel = i18n("Site:");
565             info.comment = i18n("<b>%1</b>", m_host);
566             info.keepPassword = true; // Prompt the user for persistence as well.
567             info.setModified(false); // Default the modified flag since we reuse authinfo.
568 
569             const bool disablePassDlg = q->configValue(QStringLiteral("DisablePassDlg"), false);
570             if (disablePassDlg) {
571                 return Result::fail(ERR_USER_CANCELED, m_host);
572             }
573             const int errorCode = q->openPasswordDialogV2(info, errorMsg);
574             if (errorCode) {
575                 return Result::fail(errorCode);
576             } else {
577                 // User can decide go anonymous using checkbox
578                 if (info.getExtraField(QStringLiteral("anonymous")).toBool()) {
579                     user = QString::fromLatin1(s_ftpLogin);
580                     pass = QString::fromLatin1(s_ftpPasswd);
581                 } else {
582                     user = info.username;
583                     pass = info.password;
584                 }
585                 promptForRetry = true;
586             }
587         }
588 
589         tempbuf = "USER " + user.toLatin1();
590         if (m_proxyURL.isValid()) {
591             tempbuf += '@' + m_host.toLatin1();
592             if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
593                 tempbuf += ':' + QByteArray::number(m_port);
594             }
595         }
596 
597         qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf;
598 
599         bool loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230));
600         bool needPass = (m_iRespCode == 331);
601         // Prompt user for login info if we do not
602         // get back a "230" or "331".
603         if (!loggedIn && !needPass) {
604             lastServerResponse = QString::fromUtf8(ftpResponse(0));
605             qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse;
606             ++failedAuth;
607             continue; // Well we failed, prompt the user please!!
608         }
609 
610         if (needPass) {
611             tempbuf = "PASS " + pass.toLatin1();
612             qCDebug(KIO_FTP) << "Sending Login password: "
613                              << "[protected]";
614             loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230));
615         }
616 
617         if (loggedIn) {
618             // Make sure the user name changed flag is properly set.
619             if (userChanged) {
620                 *userChanged = (!m_user.isEmpty() && (m_user != user));
621             }
622 
623             // Do not cache the default login!!
624             if (user != QLatin1String(s_ftpLogin) && pass != QLatin1String(s_ftpPasswd)) {
625                 // Update the username in case it was changed during login.
626                 if (!m_user.isEmpty()) {
627                     info.url.setUserName(user);
628                     m_user = user;
629                 }
630 
631                 // Cache the password if the user requested it.
632                 if (info.keepPassword) {
633                     q->cacheAuthentication(info);
634                 }
635             }
636             failedAuth = -1;
637         } else {
638             // some servers don't let you login anymore
639             // if you fail login once, so restart the connection here
640             lastServerResponse = QString::fromUtf8(ftpResponse(0));
641             const Result result = ftpOpenControlConnection();
642             if (!result.success) {
643                 return result;
644             }
645         }
646     } while (++failedAuth);
647 
648     qCDebug(KIO_FTP) << "Login OK";
649     q->infoMessage(i18n("Login OK"));
650 
651     // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix:
652     // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint
653     if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) {
654         if (!qstrncmp(ftpResponse(0), "215 Windows_NT", 14)) { // should do for any version
655             (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
656             // Check if it was already in Unix style
657             // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk>
658             if (!qstrncmp(ftpResponse(0), "200 MSDOS-like directory output is on", 37))
659             // It was in Unix style already!
660             {
661                 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
662             }
663             // windows won't support chmod before KDE konquers their desktop...
664             m_extControl |= chmodUnknown;
665         }
666     } else {
667         qCWarning(KIO_FTP) << "SYST failed";
668     }
669 
670     if (q->configValue(QStringLiteral("EnableAutoLoginMacro"), false)) {
671         ftpAutoLoginMacro();
672     }
673 
674     // Get the current working directory
675     qCDebug(KIO_FTP) << "Searching for pwd";
676     if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) {
677         qCDebug(KIO_FTP) << "Couldn't issue pwd command";
678         return Result::fail(ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ?
679     }
680 
681     QString sTmp = q->remoteEncoding()->decode(ftpResponse(3));
682     const int iBeg = sTmp.indexOf(QLatin1Char('"'));
683     const int iEnd = sTmp.lastIndexOf(QLatin1Char('"'));
684     if (iBeg > 0 && iBeg < iEnd) {
685         m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1);
686         if (!m_initialPath.startsWith(QLatin1Char('/'))) {
687             m_initialPath.prepend(QLatin1Char('/'));
688         }
689         qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath;
690         m_currentPath = m_initialPath;
691     }
692 
693     return Result::pass();
694 }
695 
ftpAutoLoginMacro()696 void FtpInternal::ftpAutoLoginMacro()
697 {
698     QString macro = q->metaData(QStringLiteral("autoLoginMacro"));
699 
700     if (macro.isEmpty()) {
701         return;
702     }
703 
704     QStringList list = macro.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
705     auto initIt = std::find_if(list.cbegin(), list.cend(), [](const QString &s) {
706         return s.startsWith(QLatin1String("init"));
707     });
708 
709     if (initIt != list.cend()) {
710         list = macro.split(QLatin1Char('\\'), Qt::SkipEmptyParts);
711         // Ignore the macro name, so start from list.cbegin() + 1
712         for (auto it = list.cbegin() + 1; it != list.cend(); ++it) {
713             // TODO: Add support for arbitrary commands besides simply changing directory!!
714             if ((*it).startsWith(QLatin1String("cwd"))) {
715                 (void)ftpFolder((*it).mid(4));
716             }
717         }
718     }
719 }
720 
721 /**
722  * ftpSendCmd - send a command (@p cmd) and read response
723  *
724  * @param maxretries number of time it should retry. Since it recursively
725  * calls itself if it can't read the answer (this happens especially after
726  * timeouts), we need to limit the recursiveness ;-)
727  *
728  * return true if any response received, false on error
729  */
ftpSendCmd(const QByteArray & cmd,int maxretries)730 bool FtpInternal::ftpSendCmd(const QByteArray &cmd, int maxretries)
731 {
732     Q_ASSERT(m_control); // must have control connection socket
733 
734     if (cmd.indexOf('\r') != -1 || cmd.indexOf('\n') != -1) {
735         qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data();
736         return false;
737     }
738 
739     // Don't print out the password...
740     bool isPassCmd = (cmd.left(4).toLower() == "pass");
741 
742     // Send the message...
743     const QByteArray buf = cmd + "\r\n"; // Yes, must use CR/LF - see http://cr.yp.to/ftp/request.html
744     int num = m_control->write(buf);
745     while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) { }
746 
747     // If we were able to successfully send the command, then we will
748     // attempt to read the response. Otherwise, take action to re-attempt
749     // the login based on the maximum number of retries specified...
750     if (num > 0) {
751         ftpResponse(-1);
752     } else {
753         m_iRespType = m_iRespCode = 0;
754     }
755 
756     // If respCh is NULL or the response is 421 (Timed-out), we try to re-send
757     // the command based on the value of maxretries.
758     if ((m_iRespType <= 0) || (m_iRespCode == 421)) {
759         // We have not yet logged on...
760         if (!m_bLoggedOn) {
761             // The command was sent from the ftpLogin function, i.e. we are actually
762             // attempting to login in. NOTE: If we already sent the username, we
763             // return false and let the user decide whether (s)he wants to start from
764             // the beginning...
765             if (maxretries > 0 && !isPassCmd) {
766                 closeConnection();
767                 const auto result = ftpOpenConnection(LoginMode::Deferred);
768                 if (result.success && ftpSendCmd(cmd, maxretries - 1)) {
769                     return true;
770                 }
771             }
772 
773             return false;
774         } else {
775             if (maxretries < 1) {
776                 return false;
777             } else {
778                 qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection.";
779 
780                 closeConnection(); // Close the old connection...
781                 const Result openResult = openConnection(); // Attempt to re-establish a new connection...
782 
783                 if (!openResult.success) {
784                     if (m_control) { // if openConnection succeeded ...
785                         qCDebug(KIO_FTP) << "Login failure, aborting";
786                         closeConnection();
787                     }
788                     return false;
789                 }
790 
791                 qCDebug(KIO_FTP) << "Logged back in, re-issuing command";
792 
793                 // If we were able to login, resend the command...
794                 if (maxretries) {
795                     maxretries--;
796                 }
797 
798                 return ftpSendCmd(cmd, maxretries);
799             }
800         }
801     }
802 
803     return true;
804 }
805 
806 /*
807  * ftpOpenPASVDataConnection - set up data connection, using PASV mode
808  *
809  * return 0 if successful, ERR_INTERNAL otherwise
810  * doesn't set error message, since non-pasv mode will always be tried if
811  * this one fails
812  */
ftpOpenPASVDataConnection()813 int FtpInternal::ftpOpenPASVDataConnection()
814 {
815     Q_ASSERT(m_control); // must have control connection socket
816     Q_ASSERT(!m_data); // ... but no data connection
817 
818     // Check that we can do PASV
819     QHostAddress address = m_control->peerAddress();
820     if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) {
821         return ERR_INTERNAL; // no PASV for non-PF_INET connections
822     }
823 
824     if (m_extControl & pasvUnknown) {
825         return ERR_INTERNAL; // already tried and got "unknown command"
826     }
827 
828     m_bPasv = true;
829 
830     /* Let's PASsiVe*/
831     if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) {
832         qCDebug(KIO_FTP) << "PASV attempt failed";
833         // unknown command?
834         if (m_iRespType == 5) {
835             qCDebug(KIO_FTP) << "disabling use of PASV";
836             m_extControl |= pasvUnknown;
837         }
838         return ERR_INTERNAL;
839     }
840 
841     // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)'
842     // but anonftpd gives '227 =160,39,200,55,6,245'
843     int i[6];
844     const char *start = strchr(ftpResponse(3), '(');
845     if (!start) {
846         start = strchr(ftpResponse(3), '=');
847     }
848     if (!start
849         || (sscanf(start, "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6
850             && sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) {
851         qCritical() << "parsing IP and port numbers failed. String parsed: " << start;
852         return ERR_INTERNAL;
853     }
854 
855     // we ignore the host part on purpose for two reasons
856     // a) it might be wrong anyway
857     // b) it would make us being susceptible to a port scanning attack
858 
859     // now connect the data socket ...
860     quint16 port = i[4] << 8 | i[5];
861     const QString host = (isSocksProxy() ? m_host : address.toString());
862     const auto connectionResult = synchronousConnectToHost(host, port);
863     m_data = connectionResult.socket;
864     if (!connectionResult.result.success) {
865         return connectionResult.result.error;
866     }
867 
868     return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
869 }
870 
871 /*
872  * ftpOpenEPSVDataConnection - opens a data connection via EPSV
873  */
ftpOpenEPSVDataConnection()874 int FtpInternal::ftpOpenEPSVDataConnection()
875 {
876     Q_ASSERT(m_control); // must have control connection socket
877     Q_ASSERT(!m_data); // ... but no data connection
878 
879     QHostAddress address = m_control->peerAddress();
880     int portnum;
881 
882     if (m_extControl & epsvUnknown) {
883         return ERR_INTERNAL;
884     }
885 
886     m_bPasv = true;
887     if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) {
888         // unknown command?
889         if (m_iRespType == 5) {
890             qCDebug(KIO_FTP) << "disabling use of EPSV";
891             m_extControl |= epsvUnknown;
892         }
893         return ERR_INTERNAL;
894     }
895 
896     const char *start = strchr(ftpResponse(3), '|');
897     if (!start || sscanf(start, "|||%d|", &portnum) != 1) {
898         return ERR_INTERNAL;
899     }
900     Q_ASSERT(portnum > 0);
901 
902     const QString host = (isSocksProxy() ? m_host : address.toString());
903     const auto connectionResult = synchronousConnectToHost(host, static_cast<quint16>(portnum));
904     m_data = connectionResult.socket;
905     if (!connectionResult.result.success) {
906         return connectionResult.result.error;
907     }
908     return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
909 }
910 
911 /*
912  * ftpOpenDataConnection - set up data connection
913  *
914  * The routine calls several ftpOpenXxxxConnection() helpers to find
915  * the best connection mode. If a helper cannot connect if returns
916  * ERR_INTERNAL - so this is not really an error! All other error
917  * codes are treated as fatal, e.g. they are passed back to the caller
918  * who is responsible for calling error(). ftpOpenPortDataConnection
919  * can be called as last try and it does never return ERR_INTERNAL.
920  *
921  * @return 0 if successful, err code otherwise
922  */
ftpOpenDataConnection()923 int FtpInternal::ftpOpenDataConnection()
924 {
925     // make sure that we are logged on and have no data connection...
926     Q_ASSERT(m_bLoggedOn);
927     ftpCloseDataConnection();
928 
929     int iErrCode = 0;
930     int iErrCodePASV = 0; // Remember error code from PASV
931 
932     // First try passive (EPSV & PASV) modes
933     if (!q->configValue(QStringLiteral("DisablePassiveMode"), false)) {
934         iErrCode = ftpOpenPASVDataConnection();
935         if (iErrCode == 0) {
936             return 0; // success
937         }
938         iErrCodePASV = iErrCode;
939         ftpCloseDataConnection();
940 
941         if (!q->configValue(QStringLiteral("DisableEPSV"), false)) {
942             iErrCode = ftpOpenEPSVDataConnection();
943             if (iErrCode == 0) {
944                 return 0; // success
945             }
946             ftpCloseDataConnection();
947         }
948 
949         // if we sent EPSV ALL already and it was accepted, then we can't
950         // use active connections any more
951         if (m_extControl & epsvAllSent) {
952             return iErrCodePASV;
953         }
954     }
955 
956     // fall back to port mode
957     iErrCode = ftpOpenPortDataConnection();
958     if (iErrCode == 0) {
959         return 0; // success
960     }
961 
962     ftpCloseDataConnection();
963     // prefer to return the error code from PASV if any, since that's what should have worked in the first place
964     return iErrCodePASV ? iErrCodePASV : iErrCode;
965 }
966 
967 /*
968  * ftpOpenPortDataConnection - set up data connection
969  *
970  * @return 0 if successful, err code otherwise (but never ERR_INTERNAL
971  *         because this is the last connection mode that is tried)
972  */
ftpOpenPortDataConnection()973 int FtpInternal::ftpOpenPortDataConnection()
974 {
975     Q_ASSERT(m_control); // must have control connection socket
976     Q_ASSERT(!m_data); // ... but no data connection
977 
978     m_bPasv = false;
979     if (m_extControl & eprtUnknown) {
980         return ERR_INTERNAL;
981     }
982 
983     if (!m_server) {
984         m_server = new QTcpServer;
985         m_server->listen(QHostAddress::Any, 0);
986     }
987 
988     if (!m_server->isListening()) {
989         delete m_server;
990         m_server = nullptr;
991         return ERR_CANNOT_LISTEN;
992     }
993 
994     m_server->setMaxPendingConnections(1);
995 
996     QString command;
997     QHostAddress localAddress = m_control->localAddress();
998     if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) {
999         struct {
1000             quint32 ip4;
1001             quint16 port;
1002         } data;
1003         data.ip4 = localAddress.toIPv4Address();
1004         data.port = m_server->serverPort();
1005 
1006         unsigned char *pData = reinterpret_cast<unsigned char *>(&data);
1007         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]);
1008     } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) {
1009         command = QStringLiteral("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort());
1010     }
1011 
1012     if (ftpSendCmd(command.toLatin1()) && (m_iRespType == 2)) {
1013         return 0;
1014     }
1015 
1016     delete m_server;
1017     m_server = nullptr;
1018     return ERR_INTERNAL;
1019 }
1020 
ftpOpenCommand(const char * _command,const QString & _path,char _mode,int errorcode,KIO::fileoffset_t _offset)1021 Result FtpInternal::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset)
1022 {
1023     int errCode = 0;
1024     if (!ftpDataMode(ftpModeFromPath(_path, _mode))) {
1025         errCode = ERR_CANNOT_CONNECT;
1026     } else {
1027         errCode = ftpOpenDataConnection();
1028     }
1029 
1030     if (errCode != 0) {
1031         return Result::fail(errCode, m_host);
1032     }
1033 
1034     if (_offset > 0) {
1035         // send rest command if offset > 0, this applies to retr and stor commands
1036         char buf[100];
1037         sprintf(buf, "rest %lld", _offset);
1038         if (!ftpSendCmd(buf)) {
1039             return Result::fail();
1040         }
1041         if (m_iRespType != 3) {
1042             return Result::fail(ERR_CANNOT_RESUME, _path); // should never happen
1043         }
1044     }
1045 
1046     QByteArray tmp = _command;
1047     QString errormessage;
1048 
1049     if (!_path.isEmpty()) {
1050         tmp += ' ' + q->remoteEncoding()->encode(ftpCleanPath(_path));
1051     }
1052 
1053     if (!ftpSendCmd(tmp) || (m_iRespType != 1)) {
1054         if (_offset > 0 && qstrcmp(_command, "retr") == 0 && (m_iRespType == 4)) {
1055             errorcode = ERR_CANNOT_RESUME;
1056         }
1057         // The error code here depends on the command
1058         errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed());
1059     }
1060 
1061     else {
1062         // Only now we know for sure that we can resume
1063         if (_offset > 0 && qstrcmp(_command, "retr") == 0) {
1064             q->canResume();
1065         }
1066 
1067         if (m_server && !m_data) {
1068             qCDebug(KIO_FTP) << "waiting for connection from remote.";
1069             m_server->waitForNewConnection(q->connectTimeout() * 1000);
1070             m_data = m_server->nextPendingConnection();
1071         }
1072 
1073         if (m_data) {
1074             qCDebug(KIO_FTP) << "connected with remote.";
1075             m_bBusy = true; // cleared in ftpCloseCommand
1076             return Result::pass();
1077         }
1078 
1079         qCDebug(KIO_FTP) << "no connection received from remote.";
1080         errorcode = ERR_CANNOT_ACCEPT;
1081         errormessage = m_host;
1082     }
1083 
1084     if (errorcode != KJob::NoError) {
1085         return Result::fail(errorcode, errormessage);
1086     }
1087     return Result::fail();
1088 }
1089 
ftpCloseCommand()1090 bool FtpInternal::ftpCloseCommand()
1091 {
1092     // first close data sockets (if opened), then read response that
1093     // we got for whatever was used in ftpOpenCommand ( should be 226 )
1094     ftpCloseDataConnection();
1095 
1096     if (!m_bBusy) {
1097         return true;
1098     }
1099 
1100     qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result";
1101     m_bBusy = false;
1102 
1103     if (!ftpResponse(-1) || (m_iRespType != 2)) {
1104         qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message";
1105         return false;
1106     }
1107     return true;
1108 }
1109 
mkdir(const QUrl & url,int permissions)1110 Result FtpInternal::mkdir(const QUrl &url, int permissions)
1111 {
1112     auto result = ftpOpenConnection(LoginMode::Implicit);
1113     if (!result.success) {
1114         return result;
1115     }
1116 
1117     const QByteArray encodedPath(q->remoteEncoding()->encode(url));
1118     const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size());
1119 
1120     if (!ftpSendCmd((QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) {
1121         QString currentPath(m_currentPath);
1122 
1123         // Check whether or not mkdir failed because
1124         // the directory already exists...
1125         if (ftpFolder(path)) {
1126             const QString &failedPath = path;
1127             // Change the directory back to what it was...
1128             (void)ftpFolder(currentPath);
1129             return Result::fail(ERR_DIR_ALREADY_EXIST, failedPath);
1130         }
1131 
1132         return Result::fail(ERR_CANNOT_MKDIR, path);
1133     }
1134 
1135     if (permissions != -1) {
1136         // chmod the dir we just created, ignoring errors.
1137         (void)ftpChmod(path, permissions);
1138     }
1139 
1140     return Result::pass();
1141 }
1142 
rename(const QUrl & src,const QUrl & dst,KIO::JobFlags flags)1143 Result FtpInternal::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags)
1144 {
1145     const auto result = ftpOpenConnection(LoginMode::Implicit);
1146     if (!result.success) {
1147         return result;
1148     }
1149 
1150     // The actual functionality is in ftpRename because put needs it
1151     return ftpRename(src.path(), dst.path(), flags);
1152 }
1153 
ftpRename(const QString & src,const QString & dst,KIO::JobFlags jobFlags)1154 Result FtpInternal::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags)
1155 {
1156     Q_ASSERT(m_bLoggedOn);
1157 
1158     // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793).
1159     if (!(jobFlags & KIO::Overwrite)) {
1160         if (ftpFileExists(dst)) {
1161             return Result::fail(ERR_FILE_ALREADY_EXIST, dst);
1162         }
1163     }
1164 
1165     if (ftpFolder(dst)) {
1166         return Result::fail(ERR_DIR_ALREADY_EXIST, dst);
1167     }
1168 
1169     // CD into parent folder
1170     const int pos = src.lastIndexOf(QLatin1Char('/'));
1171     if (pos >= 0) {
1172         if (!ftpFolder(src.left(pos + 1))) {
1173             return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, src);
1174         }
1175     }
1176 
1177     const QByteArray from_cmd = "RNFR " + q->remoteEncoding()->encode(src.mid(pos + 1));
1178     if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) {
1179         return Result::fail(ERR_CANNOT_RENAME, src);
1180     }
1181 
1182     const QByteArray to_cmd = "RNTO " + q->remoteEncoding()->encode(dst);
1183     if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) {
1184         return Result::fail(ERR_CANNOT_RENAME, src);
1185     }
1186 
1187     return Result::pass();
1188 }
1189 
del(const QUrl & url,bool isfile)1190 Result FtpInternal::del(const QUrl &url, bool isfile)
1191 {
1192     auto result = ftpOpenConnection(LoginMode::Implicit);
1193     if (!result.success) {
1194         return result;
1195     }
1196 
1197     // When deleting a directory, we must exit from it first
1198     // The last command probably went into it (to stat it)
1199     if (!isfile) {
1200         (void)ftpFolder(q->remoteEncoding()->decode(q->remoteEncoding()->directory(url))); // ignore errors
1201     }
1202 
1203     const QByteArray cmd = (isfile ? "DELE " : "RMD ") + q->remoteEncoding()->encode(url);
1204 
1205     if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
1206         return Result::fail(ERR_CANNOT_DELETE, url.path());
1207     }
1208 
1209     return Result::pass();
1210 }
1211 
ftpChmod(const QString & path,int permissions)1212 bool FtpInternal::ftpChmod(const QString &path, int permissions)
1213 {
1214     Q_ASSERT(m_bLoggedOn);
1215 
1216     if (m_extControl & chmodUnknown) { // previous errors?
1217         return false;
1218     }
1219 
1220     // we need to do bit AND 777 to get permissions, in case
1221     // we were sent a full mode (unlikely)
1222     const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777 /*octal*/, 8 /*octal*/) + ' ' + q->remoteEncoding()->encode(path);
1223 
1224     if (ftpSendCmd(cmd)) {
1225         qCDebug(KIO_FTP) << "ftpChmod: Failed to issue chmod";
1226         return false;
1227     }
1228 
1229     if (m_iRespType == 2) {
1230         return true;
1231     }
1232 
1233     if (m_iRespCode == 500) {
1234         m_extControl |= chmodUnknown;
1235         qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling";
1236     }
1237     return false;
1238 }
1239 
chmod(const QUrl & url,int permissions)1240 Result FtpInternal::chmod(const QUrl &url, int permissions)
1241 {
1242     const auto result = ftpOpenConnection(LoginMode::Implicit);
1243     if (!result.success) {
1244         return result;
1245     }
1246 
1247     if (!ftpChmod(url.path(), permissions)) {
1248         return Result::fail(ERR_CANNOT_CHMOD, url.path());
1249     }
1250 
1251     return Result::pass();
1252 }
1253 
ftpCreateUDSEntry(const QString & filename,const FtpEntry & ftpEnt,UDSEntry & entry,bool isDir)1254 void FtpInternal::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir)
1255 {
1256     Q_ASSERT(entry.count() == 0); // by contract :-)
1257 
1258     entry.reserve(9);
1259     entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1260     entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ftpEnt.size);
1261     entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date.toSecsSinceEpoch());
1262     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, ftpEnt.access);
1263     entry.fastInsert(KIO::UDSEntry::UDS_USER, ftpEnt.owner);
1264     if (!ftpEnt.group.isEmpty()) {
1265         entry.fastInsert(KIO::UDSEntry::UDS_GROUP, ftpEnt.group);
1266     }
1267 
1268     if (!ftpEnt.link.isEmpty()) {
1269         entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link);
1270 
1271         QMimeDatabase db;
1272         QMimeType mime = db.mimeTypeForUrl(QUrl(QLatin1String("ftp://host/") + filename));
1273         // Links on ftp sites are often links to dirs, and we have no way to check
1274         // that. Let's do like Netscape : assume dirs generally.
1275         // But we do this only when the MIME type can't be known from the filename.
1276         // --> we do better than Netscape :-)
1277         if (mime.isDefault()) {
1278             qCDebug(KIO_FTP) << "Setting guessed MIME type to inode/directory for " << filename;
1279             entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory"));
1280             isDir = true;
1281         }
1282     }
1283 
1284     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type);
1285     // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime);
1286     // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime);
1287 }
1288 
ftpShortStatAnswer(const QString & filename,bool isDir)1289 void FtpInternal::ftpShortStatAnswer(const QString &filename, bool isDir)
1290 {
1291     UDSEntry entry;
1292 
1293     entry.reserve(4);
1294     entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1295     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG);
1296     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1297     if (isDir) {
1298         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1299     }
1300     // No details about size, ownership, group, etc.
1301 
1302     q->statEntry(entry);
1303 }
1304 
ftpStatAnswerNotFound(const QString & path,const QString & filename)1305 Result FtpInternal::ftpStatAnswerNotFound(const QString &path, const QString &filename)
1306 {
1307     // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source")
1308     // When e.g. uploading a file, we still need stat() to return "not found"
1309     // when the file doesn't exist.
1310     QString statSide = q->metaData(QStringLiteral("statSide"));
1311     qCDebug(KIO_FTP) << "statSide=" << statSide;
1312     if (statSide == QLatin1String("source")) {
1313         qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing";
1314         // MS Server is incapable of handling "list <blah>" in a case insensitive way
1315         // But "retr <blah>" works. So lie in stat(), to get going...
1316         //
1317         // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run
1318         // where listing permissions are denied, but downloading is still possible.
1319         ftpShortStatAnswer(filename, false /*file, not dir*/);
1320 
1321         return Result::pass();
1322     }
1323 
1324     return Result::fail(ERR_DOES_NOT_EXIST, path);
1325 }
1326 
stat(const QUrl & url)1327 Result FtpInternal::stat(const QUrl &url)
1328 {
1329     qCDebug(KIO_FTP) << "path=" << url.path();
1330     auto result = ftpOpenConnection(LoginMode::Implicit);
1331     if (!result.success) {
1332         return result;
1333     }
1334 
1335     const QString path = ftpCleanPath(QDir::cleanPath(url.path()));
1336     qCDebug(KIO_FTP) << "cleaned path=" << path;
1337 
1338     // We can't stat root, but we know it's a dir.
1339     if (path.isEmpty() || path == QLatin1String("/")) {
1340         UDSEntry entry;
1341         entry.reserve(6);
1342         // entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) );
1343         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
1344         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1345         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1346         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1347         entry.fastInsert(KIO::UDSEntry::UDS_USER, QStringLiteral("root"));
1348         entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QStringLiteral("root"));
1349         // no size
1350 
1351         q->statEntry(entry);
1352         return Result::pass();
1353     }
1354 
1355     QUrl tempurl(url);
1356     tempurl.setPath(path); // take the clean one
1357     QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash);
1358     QString parentDir;
1359     const QString filename = tempurl.fileName();
1360     Q_ASSERT(!filename.isEmpty());
1361 
1362     // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info)
1363     // if it doesn't work, it's a file (and then we'll use dir filename)
1364     bool isDir = ftpFolder(path);
1365 
1366     // if we're only interested in "file or directory", we should stop here
1367     QString sDetails = q->metaData(QStringLiteral("details"));
1368     int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
1369     qCDebug(KIO_FTP) << "details=" << details;
1370     if (details == 0) {
1371         if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ?
1372             // no -> it doesn't exist at all
1373             return ftpStatAnswerNotFound(path, filename);
1374         }
1375         ftpShortStatAnswer(filename, isDir);
1376         return Result::pass(); // successfully found a dir or a file -> done
1377     }
1378 
1379     if (!isDir) {
1380         // It is a file or it doesn't exist, try going to parent directory
1381         parentDir = tempurl.adjusted(QUrl::RemoveFilename).path();
1382         // With files we can do "LIST <filename>" to avoid listing the whole dir
1383         listarg = filename;
1384     } else {
1385         // --- New implementation:
1386         // Don't list the parent dir. Too slow, might not show it, etc.
1387         // Just return that it's a dir.
1388         UDSEntry entry;
1389         entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1390         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1391         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1392         // No clue about size, ownership, group, etc.
1393 
1394         q->statEntry(entry);
1395         return Result::pass();
1396     }
1397 
1398     // Now cwd the parent dir, to prepare for listing
1399     if (!ftpFolder(parentDir)) {
1400         return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, parentDir);
1401     }
1402 
1403     result = ftpOpenCommand("list", listarg, 'I', ERR_DOES_NOT_EXIST);
1404     if (!result.success) {
1405         qCritical() << "COULD NOT LIST";
1406         return result;
1407     }
1408     qCDebug(KIO_FTP) << "Starting of list was ok";
1409 
1410     Q_ASSERT(!filename.isEmpty() && filename != QLatin1String("/"));
1411 
1412     bool bFound = false;
1413     QUrl linkURL;
1414     FtpEntry ftpEnt;
1415     QList<FtpEntry> ftpValidateEntList;
1416     while (ftpReadDir(ftpEnt)) {
1417         if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(0).isSpace()) {
1418             ftpValidateEntList.append(ftpEnt);
1419             continue;
1420         }
1421 
1422         // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at)
1423         // return only the filename when doing "dir /full/path/to/file"
1424         if (!bFound) {
1425             bFound = maybeEmitStatEntry(ftpEnt, filename, isDir);
1426         }
1427         qCDebug(KIO_FTP) << ftpEnt.name;
1428     }
1429 
1430     for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1431         FtpEntry &ftpEnt = ftpValidateEntList[i];
1432         fixupEntryName(&ftpEnt);
1433         if (maybeEmitStatEntry(ftpEnt, filename, isDir)) {
1434             break;
1435         }
1436     }
1437 
1438     ftpCloseCommand(); // closes the data connection only
1439 
1440     if (!bFound) {
1441         return ftpStatAnswerNotFound(path, filename);
1442     }
1443 
1444     if (!linkURL.isEmpty()) {
1445         if (linkURL == url || linkURL == tempurl) {
1446             return Result::fail(ERR_CYCLIC_LINK, linkURL.toString());
1447         }
1448         return FtpInternal::stat(linkURL);
1449     }
1450 
1451     qCDebug(KIO_FTP) << "stat : finished successfully";
1452     ;
1453     return Result::pass();
1454 }
1455 
maybeEmitStatEntry(FtpEntry & ftpEnt,const QString & filename,bool isDir)1456 bool FtpInternal::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &filename, bool isDir)
1457 {
1458     if (filename == ftpEnt.name && !filename.isEmpty()) {
1459         UDSEntry entry;
1460         ftpCreateUDSEntry(filename, ftpEnt, entry, isDir);
1461         q->statEntry(entry);
1462         return true;
1463     }
1464 
1465     return false;
1466 }
1467 
listDir(const QUrl & url)1468 Result FtpInternal::listDir(const QUrl &url)
1469 {
1470     qCDebug(KIO_FTP) << url;
1471     auto result = ftpOpenConnection(LoginMode::Implicit);
1472     if (!result.success) {
1473         return result;
1474     }
1475 
1476     // No path specified ?
1477     QString path = url.path();
1478     if (path.isEmpty()) {
1479         QUrl realURL;
1480         realURL.setScheme(QStringLiteral("ftp"));
1481         realURL.setUserName(m_user);
1482         realURL.setPassword(m_pass);
1483         realURL.setHost(m_host);
1484         if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
1485             realURL.setPort(m_port);
1486         }
1487         if (m_initialPath.isEmpty()) {
1488             m_initialPath = QStringLiteral("/");
1489         }
1490         realURL.setPath(m_initialPath);
1491         qCDebug(KIO_FTP) << "REDIRECTION to " << realURL;
1492         q->redirection(realURL);
1493         return Result::pass();
1494     }
1495 
1496     qCDebug(KIO_FTP) << "hunting for path" << path;
1497 
1498     result = ftpOpenDir(path);
1499     if (!result.success) {
1500         if (ftpFileExists(path)) {
1501             return Result::fail(ERR_IS_FILE, path);
1502         }
1503         // not sure which to emit
1504         // error( ERR_DOES_NOT_EXIST, path );
1505         return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, path);
1506     }
1507 
1508     UDSEntry entry;
1509     FtpEntry ftpEnt;
1510     QList<FtpEntry> ftpValidateEntList;
1511     while (ftpReadDir(ftpEnt)) {
1512         qCDebug(KIO_FTP) << ftpEnt.name;
1513         // Q_ASSERT( !ftpEnt.name.isEmpty() );
1514         if (!ftpEnt.name.isEmpty()) {
1515             if (ftpEnt.name.at(0).isSpace()) {
1516                 ftpValidateEntList.append(ftpEnt);
1517                 continue;
1518             }
1519 
1520             // if ( S_ISDIR( (mode_t)ftpEnt.type ) )
1521             //   qDebug() << "is a dir";
1522             // if ( !ftpEnt.link.isEmpty() )
1523             //   qDebug() << "is a link to " << ftpEnt.link;
1524             ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false);
1525             q->listEntry(entry);
1526             entry.clear();
1527         }
1528     }
1529 
1530     for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1531         FtpEntry &ftpEnt = ftpValidateEntList[i];
1532         fixupEntryName(&ftpEnt);
1533         ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false);
1534         q->listEntry(entry);
1535         entry.clear();
1536     }
1537 
1538     ftpCloseCommand(); // closes the data connection only
1539     return Result::pass();
1540 }
1541 
slave_status()1542 void FtpInternal::slave_status()
1543 {
1544     qCDebug(KIO_FTP) << "Got slave_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " ["
1545                      << (m_bLoggedOn ? "Connected" : "Not connected") << "]";
1546     q->slaveStatus(m_host, m_bLoggedOn);
1547 }
1548 
ftpOpenDir(const QString & path)1549 Result FtpInternal::ftpOpenDir(const QString &path)
1550 {
1551     // QString path( _url.path(QUrl::RemoveTrailingSlash) );
1552 
1553     // We try to change to this directory first to see whether it really is a directory.
1554     // (And also to follow symlinks)
1555     QString tmp = path.isEmpty() ? QStringLiteral("/") : path;
1556 
1557     // We get '550', whether it's a file or doesn't exist...
1558     if (!ftpFolder(tmp)) {
1559         return Result::fail();
1560     }
1561 
1562     // Don't use the path in the list command:
1563     // We changed into this directory anyway - so it's enough just to send "list".
1564     // We use '-a' because the application MAY be interested in dot files.
1565     // The only way to really know would be to have a metadata flag for this...
1566     // Since some windows ftp server seems not to support the -a argument, we use a fallback here.
1567     // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com)
1568     // Pass KJob::NoError first because we don't want to emit error before we
1569     // have tried all commands.
1570     auto result = ftpOpenCommand("list -la", QString(), 'I', KJob::NoError);
1571     if (!result.success) {
1572         result = ftpOpenCommand("list", QString(), 'I', KJob::NoError);
1573     }
1574     if (!result.success) {
1575         // Servers running with Turkish locale having problems converting 'i' letter to upper case.
1576         // So we send correct upper case command as last resort.
1577         result = ftpOpenCommand("LIST -la", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY);
1578     }
1579 
1580     if (!result.success) {
1581         qCWarning(KIO_FTP) << "Can't open for listing";
1582         return result;
1583     }
1584 
1585     qCDebug(KIO_FTP) << "Starting of list was ok";
1586     return Result::pass();
1587 }
1588 
ftpReadDir(FtpEntry & de)1589 bool FtpInternal::ftpReadDir(FtpEntry &de)
1590 {
1591     Q_ASSERT(m_data);
1592 
1593     // get a line from the data connection ...
1594     while (true) {
1595         while (!m_data->canReadLine() && m_data->waitForReadyRead((q->readTimeout() * 1000))) { }
1596         QByteArray data = m_data->readLine();
1597         if (data.size() == 0) {
1598             break;
1599         }
1600 
1601         const char *buffer = data.data();
1602         qCDebug(KIO_FTP) << "dir > " << buffer;
1603 
1604         // Normally the listing looks like
1605         // -rw-r--r--   1 dfaure   dfaure        102 Nov  9 12:30 log
1606         // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442)
1607         // d [RWCEAFMS] Admin                     512 Oct 13  2004 PSI
1608 
1609         // we should always get the following 5 fields ...
1610         const char *p_access;
1611         const char *p_junk;
1612         const char *p_owner;
1613         const char *p_group;
1614         const char *p_size;
1615         if ((p_access = strtok((char *)buffer, " ")) == nullptr) {
1616             continue;
1617         }
1618         if ((p_junk = strtok(nullptr, " ")) == nullptr) {
1619             continue;
1620         }
1621         if ((p_owner = strtok(nullptr, " ")) == nullptr) {
1622             continue;
1623         }
1624         if ((p_group = strtok(nullptr, " ")) == nullptr) {
1625             continue;
1626         }
1627         if ((p_size = strtok(nullptr, " ")) == nullptr) {
1628             continue;
1629         }
1630 
1631         qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size;
1632 
1633         de.access = 0;
1634         if (qstrlen(p_access) == 1 && p_junk[0] == '[') { // Netware
1635             de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions
1636         }
1637 
1638         const char *p_date_1;
1639         const char *p_date_2;
1640         const char *p_date_3;
1641         const char *p_name;
1642 
1643         // A special hack for "/dev". A listing may look like this:
1644         // crw-rw-rw-   1 root     root       1,   5 Jun 29  1997 zero
1645         // So we just ignore the number in front of the ",". Ok, it is a hack :-)
1646         if (strchr(p_size, ',') != nullptr) {
1647             qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)";
1648             if ((p_size = strtok(nullptr, " ")) == nullptr) {
1649                 continue;
1650             }
1651         }
1652 
1653         // This is needed for ftp servers with a directory listing like this (#375610):
1654         // drwxr-xr-x               folder        0 Mar 15 15:50 directory_name
1655         if (strcmp(p_junk, "folder") == 0) {
1656             p_date_1 = p_group;
1657             p_date_2 = p_size;
1658             p_size = p_owner;
1659             p_group = nullptr;
1660             p_owner = nullptr;
1661         }
1662         // Check whether the size we just read was really the size
1663         // or a month (this happens when the server lists no group)
1664         // Used to be the case on sunsite.uio.no, but not anymore
1665         // This is needed for the Netware case, too.
1666         else if (!isdigit(*p_size)) {
1667             p_date_1 = p_size;
1668             p_date_2 = strtok(nullptr, " ");
1669             p_size = p_group;
1670             p_group = nullptr;
1671             qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1;
1672         } else {
1673             p_date_1 = strtok(nullptr, " ");
1674             p_date_2 = strtok(nullptr, " ");
1675             qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1;
1676         }
1677 
1678         if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(nullptr, " ")) != nullptr && (p_name = strtok(nullptr, "\r\n")) != nullptr) {
1679             {
1680                 QByteArray tmp(p_name);
1681                 if (p_access[0] == 'l') {
1682                     int i = tmp.lastIndexOf(" -> ");
1683                     if (i != -1) {
1684                         de.link = q->remoteEncoding()->decode(p_name + i + 4);
1685                         tmp.truncate(i);
1686                     } else {
1687                         de.link.clear();
1688                     }
1689                 } else {
1690                     de.link.clear();
1691                 }
1692 
1693                 if (tmp.startsWith('/')) { // listing on ftp://ftp.gnupg.org/ starts with '/'
1694                     tmp.remove(0, 1);
1695                 }
1696 
1697                 if (tmp.indexOf('/') != -1) {
1698                     continue; // Don't trick us!
1699                 }
1700 
1701                 de.name = q->remoteEncoding()->decode(tmp);
1702             }
1703 
1704             de.type = S_IFREG;
1705             switch (p_access[0]) {
1706             case 'd':
1707                 de.type = S_IFDIR;
1708                 break;
1709             case 's':
1710                 de.type = S_IFSOCK;
1711                 break;
1712             case 'b':
1713                 de.type = S_IFBLK;
1714                 break;
1715             case 'c':
1716                 de.type = S_IFCHR;
1717                 break;
1718             case 'l':
1719                 de.type = S_IFREG;
1720                 // we don't set S_IFLNK here.  de.link says it.
1721                 break;
1722             default:
1723                 break;
1724             }
1725 
1726             if (p_access[1] == 'r') {
1727                 de.access |= S_IRUSR;
1728             }
1729             if (p_access[2] == 'w') {
1730                 de.access |= S_IWUSR;
1731             }
1732             if (p_access[3] == 'x' || p_access[3] == 's') {
1733                 de.access |= S_IXUSR;
1734             }
1735             if (p_access[4] == 'r') {
1736                 de.access |= S_IRGRP;
1737             }
1738             if (p_access[5] == 'w') {
1739                 de.access |= S_IWGRP;
1740             }
1741             if (p_access[6] == 'x' || p_access[6] == 's') {
1742                 de.access |= S_IXGRP;
1743             }
1744             if (p_access[7] == 'r') {
1745                 de.access |= S_IROTH;
1746             }
1747             if (p_access[8] == 'w') {
1748                 de.access |= S_IWOTH;
1749             }
1750             if (p_access[9] == 'x' || p_access[9] == 't') {
1751                 de.access |= S_IXOTH;
1752             }
1753             if (p_access[3] == 's' || p_access[3] == 'S') {
1754                 de.access |= S_ISUID;
1755             }
1756             if (p_access[6] == 's' || p_access[6] == 'S') {
1757                 de.access |= S_ISGID;
1758             }
1759             if (p_access[9] == 't' || p_access[9] == 'T') {
1760                 de.access |= S_ISVTX;
1761             }
1762 
1763             de.owner = q->remoteEncoding()->decode(p_owner);
1764             de.group = q->remoteEncoding()->decode(p_group);
1765             de.size = charToLongLong(p_size);
1766 
1767             // Parsing the date is somewhat tricky
1768             // Examples : "Oct  6 22:49", "May 13  1999"
1769 
1770             // First get current date - we need the current month and year
1771             QDate currentDate(QDate::currentDate());
1772             int currentMonth = currentDate.month();
1773             int day = currentDate.day();
1774             int month = currentDate.month();
1775             int year = currentDate.year();
1776             int minute = 0;
1777             int hour = 0;
1778             // Get day number (always second field)
1779             if (p_date_2) {
1780                 day = atoi(p_date_2);
1781             }
1782             // Get month from first field
1783             // NOTE : no, we don't want to use KLocale here
1784             // It seems all FTP servers use the English way
1785             qCDebug(KIO_FTP) << "Looking for month " << p_date_1;
1786             static const char s_months[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
1787             for (int c = 0; c < 12; c++) {
1788                 if (!qstrcmp(p_date_1, s_months[c])) {
1789                     qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1;
1790                     month = c + 1;
1791                     break;
1792                 }
1793             }
1794 
1795             // Parse third field
1796             if (p_date_3 && !strchr(p_date_3, ':')) { // No colon, looks like a year
1797                 year = atoi(p_date_3);
1798             } else {
1799                 // otherwise, the year is implicit
1800                 // according to man ls, this happens when it is between than 6 months
1801                 // old and 1 hour in the future.
1802                 // So the year is : current year if tm_mon <= currentMonth+1
1803                 // otherwise current year minus one
1804                 // (The +1 is a security for the "+1 hour" at the end of the month issue)
1805                 if (month > currentMonth + 1) {
1806                     year--;
1807                 }
1808 
1809                 // and p_date_3 contains probably a time
1810                 char *semicolon;
1811                 if (p_date_3 && (semicolon = (char *)strchr(p_date_3, ':'))) {
1812                     *semicolon = '\0';
1813                     minute = atoi(semicolon + 1);
1814                     hour = atoi(p_date_3);
1815                 } else {
1816                     qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3;
1817                 }
1818             }
1819 
1820             de.date = QDateTime(QDate(year, month, day), QTime(hour, minute));
1821             qCDebug(KIO_FTP) << de.date;
1822             return true;
1823         }
1824     } // line invalid, loop to get another line
1825     return false;
1826 }
1827 
1828 //===============================================================================
1829 // public: get           download file from server
1830 // helper: ftpGet        called from get() and copy()
1831 //===============================================================================
get(const QUrl & url)1832 Result FtpInternal::get(const QUrl &url)
1833 {
1834     qCDebug(KIO_FTP) << url;
1835     const Result result = ftpGet(-1, QString(), url, 0);
1836     ftpCloseCommand(); // must close command!
1837     return result;
1838 }
1839 
ftpGet(int iCopyFile,const QString & sCopyFile,const QUrl & url,KIO::fileoffset_t llOffset)1840 Result FtpInternal::ftpGet(int iCopyFile, const QString &sCopyFile, const QUrl &url, KIO::fileoffset_t llOffset)
1841 {
1842     auto result = ftpOpenConnection(LoginMode::Implicit);
1843     if (!result.success) {
1844         return result;
1845     }
1846 
1847     // Try to find the size of the file (and check that it exists at
1848     // the same time). If we get back a 550, "File does not exist"
1849     // or "not a plain file", check if it is a directory. If it is a
1850     // directory, return an error; otherwise simply try to retrieve
1851     // the request...
1852     if (!ftpSize(url.path(), '?') && (m_iRespCode == 550) && ftpFolder(url.path())) {
1853         // Ok it's a dir in fact
1854         qCDebug(KIO_FTP) << "it is a directory in fact";
1855         return Result::fail(ERR_IS_DIRECTORY);
1856     }
1857 
1858     QString resumeOffset = q->metaData(QStringLiteral("range-start"));
1859     if (resumeOffset.isEmpty()) {
1860         resumeOffset = q->metaData(QStringLiteral("resume")); // old name
1861     }
1862     if (!resumeOffset.isEmpty()) {
1863         llOffset = resumeOffset.toLongLong();
1864         qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset;
1865     }
1866 
1867     result = ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset);
1868     if (!result.success) {
1869         qCWarning(KIO_FTP) << "Can't open for reading";
1870         return result;
1871     }
1872 
1873     // Read the size from the response string
1874     if (m_size == UnknownSize) {
1875         const char *psz = strrchr(ftpResponse(4), '(');
1876         if (psz) {
1877             m_size = charToLongLong(psz + 1);
1878         }
1879         if (!m_size) {
1880             m_size = UnknownSize;
1881         }
1882     }
1883 
1884     // Send the MIME type...
1885     if (iCopyFile == -1) {
1886         const auto result = ftpSendMimeType(url);
1887         if (!result.success) {
1888             return result;
1889         }
1890     }
1891 
1892     KIO::filesize_t bytesLeft = 0;
1893     if (m_size != UnknownSize) {
1894         bytesLeft = m_size - llOffset;
1895         q->totalSize(m_size); // emit the total size...
1896     }
1897 
1898     qCDebug(KIO_FTP) << "starting with offset=" << llOffset;
1899     KIO::fileoffset_t processed_size = llOffset;
1900 
1901     QByteArray array;
1902     char buffer[maximumIpcSize];
1903     // start with small data chunks in case of a slow data source (modem)
1904     // - unfortunately this has a negative impact on performance for large
1905     // - files - so we will increase the block size after a while ...
1906     int iBlockSize = initialIpcSize;
1907     int iBufferCur = 0;
1908 
1909     while (m_size == UnknownSize || bytesLeft > 0) {
1910         // let the buffer size grow if the file is larger 64kByte ...
1911         if (processed_size - llOffset > 1024 * 64) {
1912             iBlockSize = maximumIpcSize;
1913         }
1914 
1915         // read the data and detect EOF or error ...
1916         if (iBlockSize + iBufferCur > (int)sizeof(buffer)) {
1917             iBlockSize = sizeof(buffer) - iBufferCur;
1918         }
1919         if (m_data->bytesAvailable() == 0) {
1920             m_data->waitForReadyRead((q->readTimeout() * 1000));
1921         }
1922         int n = m_data->read(buffer + iBufferCur, iBlockSize);
1923         if (n <= 0) {
1924             // this is how we detect EOF in case of unknown size
1925             if (m_size == UnknownSize && n == 0) {
1926                 break;
1927             }
1928             // unexpected eof. Happens when the daemon gets killed.
1929             return Result::fail(ERR_CANNOT_READ);
1930         }
1931         processed_size += n;
1932 
1933         // collect very small data chunks in buffer before processing ...
1934         if (m_size != UnknownSize) {
1935             bytesLeft -= n;
1936             iBufferCur += n;
1937             if (iBufferCur < minimumMimeSize && bytesLeft > 0) {
1938                 q->processedSize(processed_size);
1939                 continue;
1940             }
1941             n = iBufferCur;
1942             iBufferCur = 0;
1943         }
1944 
1945         // write output file or pass to data pump ...
1946         int writeError = 0;
1947         if (iCopyFile == -1) {
1948             array = QByteArray::fromRawData(buffer, n);
1949             q->data(array);
1950             array.clear();
1951         } else if ((writeError = WriteToFile(iCopyFile, buffer, n)) != 0) {
1952             return Result::fail(writeError, sCopyFile);
1953         }
1954 
1955         Q_ASSERT(processed_size >= 0);
1956         q->processedSize(static_cast<KIO::filesize_t>(processed_size));
1957     }
1958 
1959     qCDebug(KIO_FTP) << "done";
1960     if (iCopyFile == -1) { // must signal EOF to data pump ...
1961         q->data(array); // array is empty and must be empty!
1962     }
1963 
1964     q->processedSize(m_size == UnknownSize ? processed_size : m_size);
1965     return Result::pass();
1966 }
1967 
1968 //===============================================================================
1969 // public: put           upload file to server
1970 // helper: ftpPut        called from put() and copy()
1971 //===============================================================================
put(const QUrl & url,int permissions,KIO::JobFlags flags)1972 Result FtpInternal::put(const QUrl &url, int permissions, KIO::JobFlags flags)
1973 {
1974     qCDebug(KIO_FTP) << url;
1975     const auto result = ftpPut(-1, url, permissions, flags);
1976     ftpCloseCommand(); // must close command!
1977     return result;
1978 }
1979 
ftpPut(int iCopyFile,const QUrl & dest_url,int permissions,KIO::JobFlags flags)1980 Result FtpInternal::ftpPut(int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags)
1981 {
1982     const auto openResult = ftpOpenConnection(LoginMode::Implicit);
1983     if (!openResult.success) {
1984         return openResult;
1985     }
1986 
1987     // Don't use mark partial over anonymous FTP.
1988     // My incoming dir allows put but not rename...
1989     bool bMarkPartial;
1990     if (m_user.isEmpty() || m_user == QLatin1String(s_ftpLogin)) {
1991         bMarkPartial = false;
1992     } else {
1993         bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true);
1994     }
1995 
1996     QString dest_orig = dest_url.path();
1997     const QString dest_part = dest_orig + QLatin1String(".part");
1998 
1999     if (ftpSize(dest_orig, 'I')) {
2000         if (m_size == 0) {
2001             // delete files with zero size
2002             const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_orig);
2003             if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
2004                 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString());
2005             }
2006         } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
2007             return Result::fail(ERR_FILE_ALREADY_EXIST, QString());
2008         } else if (bMarkPartial) {
2009             // when using mark partial, append .part extension
2010             const auto result = ftpRename(dest_orig, dest_part, KIO::Overwrite);
2011             if (!result.success) {
2012                 return Result::fail(ERR_CANNOT_RENAME_PARTIAL, QString());
2013             }
2014         }
2015         // Don't chmod an existing file
2016         permissions = -1;
2017     } else if (bMarkPartial && ftpSize(dest_part, 'I')) {
2018         // file with extension .part exists
2019         if (m_size == 0) {
2020             // delete files with zero size
2021             const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_part);
2022             if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
2023                 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString());
2024             }
2025         } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
2026             flags |= q->canResume(m_size) ? KIO::Resume : KIO::DefaultFlags;
2027             if (!(flags & KIO::Resume)) {
2028                 return Result::fail(ERR_FILE_ALREADY_EXIST, QString());
2029             }
2030         }
2031     } else {
2032         m_size = 0;
2033     }
2034 
2035     QString dest;
2036 
2037     // if we are using marking of partial downloads -> add .part extension
2038     if (bMarkPartial) {
2039         qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig;
2040         dest = dest_part;
2041     } else {
2042         dest = dest_orig;
2043     }
2044 
2045     KIO::fileoffset_t offset = 0;
2046 
2047     // set the mode according to offset
2048     if ((flags & KIO::Resume) && m_size > 0) {
2049         offset = m_size;
2050         if (iCopyFile != -1) {
2051             if (QT_LSEEK(iCopyFile, offset, SEEK_SET) < 0) {
2052                 return Result::fail(ERR_CANNOT_RESUME, QString());
2053             }
2054         }
2055     }
2056 
2057     const auto storResult = ftpOpenCommand("stor", dest, '?', ERR_CANNOT_WRITE, offset);
2058     if (!storResult.success) {
2059         return storResult;
2060     }
2061 
2062     qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset;
2063     KIO::fileoffset_t processed_size = offset;
2064 
2065     QByteArray buffer;
2066     int result;
2067     int iBlockSize = initialIpcSize;
2068     int writeError = 0;
2069     // Loop until we got 'dataEnd'
2070     do {
2071         if (iCopyFile == -1) {
2072             q->dataReq(); // Request for data
2073             result = q->readData(buffer);
2074         } else {
2075             // let the buffer size grow if the file is larger 64kByte ...
2076             if (processed_size - offset > 1024 * 64) {
2077                 iBlockSize = maximumIpcSize;
2078             }
2079             buffer.resize(iBlockSize);
2080             result = QT_READ(iCopyFile, buffer.data(), buffer.size());
2081             if (result < 0) {
2082                 writeError = ERR_CANNOT_READ;
2083             } else {
2084                 buffer.resize(result);
2085             }
2086         }
2087 
2088         if (result > 0) {
2089             m_data->write(buffer);
2090             while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) { }
2091             processed_size += result;
2092             q->processedSize(processed_size);
2093         }
2094     } while (result > 0);
2095 
2096     if (result != 0) { // error
2097         ftpCloseCommand(); // don't care about errors
2098         qCDebug(KIO_FTP) << "Error during 'put'. Aborting.";
2099         if (bMarkPartial) {
2100             // Remove if smaller than minimum size
2101             if (ftpSize(dest, 'I') && (processed_size < q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE))) {
2102                 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest);
2103                 (void)ftpSendCmd(cmd);
2104             }
2105         }
2106         return Result::fail(writeError, dest_url.toString());
2107     }
2108 
2109     if (!ftpCloseCommand()) {
2110         return Result::fail(ERR_CANNOT_WRITE);
2111     }
2112 
2113     // after full download rename the file back to original name
2114     if (bMarkPartial) {
2115         qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")";
2116         const auto result = ftpRename(dest, dest_orig, KIO::Overwrite);
2117         if (!result.success) {
2118             return Result::fail(ERR_CANNOT_RENAME_PARTIAL);
2119         }
2120     }
2121 
2122     // set final permissions
2123     if (permissions != -1) {
2124         if (m_user == QLatin1String(s_ftpLogin)) {
2125             qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???";
2126         }
2127         // chmod the file we just put
2128         if (!ftpChmod(dest_orig, permissions)) {
2129             // To be tested
2130             // if ( m_user != s_ftpLogin )
2131             //    warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) );
2132         }
2133     }
2134 
2135     return Result::pass();
2136 }
2137 
2138 /** Use the SIZE command to get the file size.
2139     Warning : the size depends on the transfer mode, hence the second arg. */
ftpSize(const QString & path,char mode)2140 bool FtpInternal::ftpSize(const QString &path, char mode)
2141 {
2142     m_size = UnknownSize;
2143     if (!ftpDataMode(mode)) {
2144         return false;
2145     }
2146 
2147     const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path);
2148     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2149         return false;
2150     }
2151 
2152     // skip leading "213 " (response code)
2153     QByteArray psz(ftpResponse(4));
2154     if (psz.isEmpty()) {
2155         return false;
2156     }
2157     bool ok = false;
2158     m_size = psz.trimmed().toLongLong(&ok);
2159     if (!ok) {
2160         m_size = UnknownSize;
2161     }
2162     return true;
2163 }
2164 
ftpFileExists(const QString & path)2165 bool FtpInternal::ftpFileExists(const QString &path)
2166 {
2167     const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path);
2168     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2169         return false;
2170     }
2171 
2172     // skip leading "213 " (response code)
2173     const char *psz = ftpResponse(4);
2174     return psz != nullptr;
2175 }
2176 
2177 // Today the differences between ASCII and BINARY are limited to
2178 // CR or CR/LF line terminators. Many servers ignore ASCII (like
2179 // win2003 -or- vsftp with default config). In the early days of
2180 // computing, when even text-files had structure, this stuff was
2181 // more important.
2182 // Theoretically "list" could return different results in ASCII
2183 // and BINARY mode. But again, most servers ignore ASCII here.
ftpDataMode(char cMode)2184 bool FtpInternal::ftpDataMode(char cMode)
2185 {
2186     if (cMode == '?') {
2187         cMode = m_bTextMode ? 'A' : 'I';
2188     } else if (cMode == 'a') {
2189         cMode = 'A';
2190     } else if (cMode != 'A') {
2191         cMode = 'I';
2192     }
2193 
2194     qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode;
2195     if (m_cDataMode == cMode) {
2196         return true;
2197     }
2198 
2199     const QByteArray buf = QByteArrayLiteral("TYPE ") + cMode;
2200     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2201         return false;
2202     }
2203     m_cDataMode = cMode;
2204     return true;
2205 }
2206 
ftpFolder(const QString & path)2207 bool FtpInternal::ftpFolder(const QString &path)
2208 {
2209     QString newPath = path;
2210     int iLen = newPath.length();
2211     if (iLen > 1 && newPath[iLen - 1] == QLatin1Char('/')) {
2212         newPath.chop(1);
2213     }
2214 
2215     qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath;
2216     if (m_currentPath == newPath) {
2217         return true;
2218     }
2219 
2220     const QByteArray tmp = "cwd " + q->remoteEncoding()->encode(newPath);
2221     if (!ftpSendCmd(tmp)) {
2222         return false; // connection failure
2223     }
2224     if (m_iRespType != 2) {
2225         return false; // not a folder
2226     }
2227     m_currentPath = newPath;
2228     return true;
2229 }
2230 
2231 //===============================================================================
2232 // public: copy          don't use kio data pump if one side is a local file
2233 // helper: ftpCopyPut    called from copy() on upload
2234 // helper: ftpCopyGet    called from copy() on download
2235 //===============================================================================
copy(const QUrl & src,const QUrl & dest,int permissions,KIO::JobFlags flags)2236 Result FtpInternal::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags)
2237 {
2238     int iCopyFile = -1;
2239     bool bSrcLocal = src.isLocalFile();
2240     bool bDestLocal = dest.isLocalFile();
2241     QString sCopyFile;
2242 
2243     Result result = Result::pass();
2244     if (bSrcLocal && !bDestLocal) { // File -> Ftp
2245         sCopyFile = src.toLocalFile();
2246         qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path();
2247         result = ftpCopyPut(iCopyFile, sCopyFile, dest, permissions, flags);
2248     } else if (!bSrcLocal && bDestLocal) { // Ftp -> File
2249         sCopyFile = dest.toLocalFile();
2250         qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile;
2251         result = ftpCopyGet(iCopyFile, sCopyFile, src, permissions, flags);
2252     } else {
2253         return Result::fail(ERR_UNSUPPORTED_ACTION, QString());
2254     }
2255 
2256     // perform clean-ups and report error (if any)
2257     if (iCopyFile != -1) {
2258         QT_CLOSE(iCopyFile);
2259     }
2260     ftpCloseCommand(); // must close command!
2261 
2262     return result;
2263 }
2264 
isSocksProxyScheme(const QString & scheme)2265 bool FtpInternal::isSocksProxyScheme(const QString &scheme)
2266 {
2267     return scheme == QLatin1String("socks") || scheme == QLatin1String("socks5");
2268 }
2269 
isSocksProxy() const2270 bool FtpInternal::isSocksProxy() const
2271 {
2272     return isSocksProxyScheme(m_proxyURL.scheme());
2273 }
2274 
ftpCopyPut(int & iCopyFile,const QString & sCopyFile,const QUrl & url,int permissions,KIO::JobFlags flags)2275 Result FtpInternal::ftpCopyPut(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2276 {
2277     // check if source is ok ...
2278     QFileInfo info(sCopyFile);
2279     bool bSrcExists = info.exists();
2280     if (bSrcExists) {
2281         if (info.isDir()) {
2282             return Result::fail(ERR_IS_DIRECTORY);
2283         }
2284     } else {
2285         return Result::fail(ERR_DOES_NOT_EXIST);
2286     }
2287 
2288     iCopyFile = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY);
2289     if (iCopyFile == -1) {
2290         return Result::fail(ERR_CANNOT_OPEN_FOR_READING);
2291     }
2292 
2293     // delegate the real work (iError gets status) ...
2294     q->totalSize(info.size());
2295     if (s_enableCanResume) {
2296         return ftpPut(iCopyFile, url, permissions, flags & ~KIO::Resume);
2297     } else {
2298         return ftpPut(iCopyFile, url, permissions, flags | KIO::Resume);
2299     }
2300 }
2301 
ftpCopyGet(int & iCopyFile,const QString & sCopyFile,const QUrl & url,int permissions,KIO::JobFlags flags)2302 Result FtpInternal::ftpCopyGet(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2303 {
2304     // check if destination is ok ...
2305     QFileInfo info(sCopyFile);
2306     const bool bDestExists = info.exists();
2307     if (bDestExists) {
2308         if (info.isDir()) {
2309             return Result::fail(ERR_IS_DIRECTORY);
2310         }
2311         if (!(flags & KIO::Overwrite)) {
2312             return Result::fail(ERR_FILE_ALREADY_EXIST);
2313         }
2314     }
2315 
2316     // do we have a ".part" file?
2317     const QString sPart = sCopyFile + QLatin1String(".part");
2318     bool bResume = false;
2319     QFileInfo sPartInfo(sPart);
2320     const bool bPartExists = sPartInfo.exists();
2321     const bool bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true);
2322     const QString dest = bMarkPartial ? sPart : sCopyFile;
2323     if (bMarkPartial && bPartExists && sPartInfo.size() > 0) {
2324         // must not be a folder! please fix a similar bug in kio_file!!
2325         if (sPartInfo.isDir()) {
2326             return Result::fail(ERR_DIR_ALREADY_EXIST);
2327         }
2328         // doesn't work for copy? -> design flaw?
2329         bResume = s_enableCanResume ? q->canResume(sPartInfo.size()) : true;
2330     }
2331 
2332     if (bPartExists && !bResume) { // get rid of an unwanted ".part" file
2333         QFile::remove(sPart);
2334     }
2335 
2336     // WABA: Make sure that we keep writing permissions ourselves,
2337     // otherwise we can be in for a surprise on NFS.
2338     mode_t initialMode;
2339     if (permissions >= 0) {
2340         initialMode = static_cast<mode_t>(permissions | S_IWUSR);
2341     } else {
2342         initialMode = 0666;
2343     }
2344 
2345     // open the output file ...
2346     KIO::fileoffset_t hCopyOffset = 0;
2347     if (bResume) {
2348         iCopyFile = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming
2349         hCopyOffset = QT_LSEEK(iCopyFile, 0, SEEK_END);
2350         if (hCopyOffset < 0) {
2351             return Result::fail(ERR_CANNOT_RESUME);
2352         }
2353         qCDebug(KIO_FTP) << "resuming at " << hCopyOffset;
2354     } else {
2355         iCopyFile = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode);
2356     }
2357 
2358     if (iCopyFile == -1) {
2359         qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile;
2360         const int error = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING;
2361         return Result::fail(error);
2362     }
2363 
2364     // delegate the real work (iError gets status) ...
2365     auto result = ftpGet(iCopyFile, sCopyFile, url, hCopyOffset);
2366 
2367     if (QT_CLOSE(iCopyFile) == 0 && !result.success) {
2368         // If closing the file failed but there isn't an error yet, switch
2369         // into an error!
2370         result = Result::fail(ERR_CANNOT_WRITE);
2371     }
2372     iCopyFile = -1;
2373 
2374     // handle renaming or deletion of a partial file ...
2375     if (bMarkPartial) {
2376         if (result.success) {
2377             // rename ".part" on success
2378             if (!QFile::rename(sPart, sCopyFile)) {
2379                 // If rename fails, try removing the destination first if it exists.
2380                 if (!bDestExists || !(QFile::remove(sCopyFile) && QFile::rename(sPart, sCopyFile))) {
2381                     qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile;
2382                     result = Result::fail(ERR_CANNOT_RENAME_PARTIAL);
2383                 }
2384             }
2385         } else {
2386             sPartInfo.refresh();
2387             if (sPartInfo.exists()) { // should a very small ".part" be deleted?
2388                 int size = q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE);
2389                 if (sPartInfo.size() < size) {
2390                     QFile::remove(sPart);
2391                 }
2392             }
2393         }
2394     }
2395 
2396     if (result.success) {
2397         const QString mtimeStr = q->metaData(QStringLiteral("modified"));
2398         if (!mtimeStr.isEmpty()) {
2399             QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
2400             if (dt.isValid()) {
2401                 qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr;
2402                 struct utimbuf utbuf;
2403                 info.refresh();
2404                 utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged
2405                 utbuf.modtime = dt.toSecsSinceEpoch(); // modification time
2406                 ::utime(QFile::encodeName(sCopyFile).constData(), &utbuf);
2407             }
2408         }
2409     }
2410 
2411     return result;
2412 }
2413 
ftpSendMimeType(const QUrl & url)2414 Result FtpInternal::ftpSendMimeType(const QUrl &url)
2415 {
2416     const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : static_cast<int>(m_size));
2417     QByteArray buffer(totalSize, '\0');
2418 
2419     while (true) {
2420         // Wait for content to be available...
2421         if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((q->readTimeout() * 1000))) {
2422             return Result::fail(ERR_CANNOT_READ, url.toString());
2423         }
2424 
2425         const qint64 bytesRead = m_data->peek(buffer.data(), totalSize);
2426 
2427         // If we got a -1, it must be an error so return an error.
2428         if (bytesRead == -1) {
2429             return Result::fail(ERR_CANNOT_READ, url.toString());
2430         }
2431 
2432         // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size
2433         // equal to the size we want, then break.
2434         if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) {
2435             break;
2436         }
2437     }
2438 
2439     if (!buffer.isEmpty()) {
2440         QMimeDatabase db;
2441         QMimeType mime = db.mimeTypeForFileNameAndData(url.path(), buffer);
2442         qCDebug(KIO_FTP) << "Emitting MIME type" << mime.name();
2443         q->mimeType(mime.name()); // emit the MIME type...
2444     }
2445 
2446     return Result::pass();
2447 }
2448 
fixupEntryName(FtpEntry * e)2449 void FtpInternal::fixupEntryName(FtpEntry *e)
2450 {
2451     Q_ASSERT(e);
2452     if (e->type == S_IFDIR) {
2453         if (!ftpFolder(e->name)) {
2454             QString name(e->name.trimmed());
2455             if (ftpFolder(name)) {
2456                 e->name = name;
2457                 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2458             } else {
2459                 int index = 0;
2460                 while (e->name.at(index).isSpace()) {
2461                     index++;
2462                     name = e->name.mid(index);
2463                     if (ftpFolder(name)) {
2464                         qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2465                         e->name = name;
2466                         break;
2467                     }
2468                 }
2469             }
2470         }
2471     } else {
2472         if (!ftpFileExists(e->name)) {
2473             QString name(e->name.trimmed());
2474             if (ftpFileExists(name)) {
2475                 e->name = name;
2476                 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2477             } else {
2478                 int index = 0;
2479                 while (e->name.at(index).isSpace()) {
2480                     index++;
2481                     name = e->name.mid(index);
2482                     if (ftpFileExists(name)) {
2483                         qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2484                         e->name = name;
2485                         break;
2486                     }
2487                 }
2488             }
2489         }
2490     }
2491 }
2492 
synchronousConnectToHost(const QString & host,quint16 port)2493 ConnectionResult FtpInternal::synchronousConnectToHost(const QString &host, quint16 port)
2494 {
2495     const QUrl proxyUrl = m_proxyURL;
2496     QNetworkProxy proxy;
2497     if (!proxyUrl.isEmpty()) {
2498         proxy = QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyUrl.host(), static_cast<quint16>(proxyUrl.port(0)), proxyUrl.userName(), proxyUrl.password());
2499     }
2500 
2501     QTcpSocket *socket = new QSslSocket;
2502     socket->setProxy(proxy);
2503     socket->connectToHost(host, port);
2504     socket->waitForConnected(q->connectTimeout() * 1000);
2505     const auto socketError = socket->error();
2506     if (socketError == QAbstractSocket::ProxyAuthenticationRequiredError) {
2507         AuthInfo info;
2508         info.url = proxyUrl;
2509         info.verifyPath = true; //### whatever
2510 
2511         if (!q->checkCachedAuthentication(info)) {
2512             info.prompt = i18n(
2513                 "You need to supply a username and a password for "
2514                 "the proxy server listed below before you are allowed "
2515                 "to access any sites.");
2516             info.keepPassword = true;
2517             info.commentLabel = i18n("Proxy:");
2518             info.comment = i18n("<b>%1</b>", proxy.hostName());
2519 
2520             const int errorCode = q->openPasswordDialogV2(info, i18n("Proxy Authentication Failed."));
2521             if (errorCode != KJob::NoError) {
2522                 qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error." << errorCode;
2523                 return ConnectionResult{socket, Result::fail(errorCode, proxyUrl.toString())};
2524             }
2525         }
2526 
2527         proxy.setUser(info.username);
2528         proxy.setPassword(info.password);
2529 
2530         delete socket;
2531         socket = new QSslSocket;
2532         socket->setProxy(proxy);
2533         socket->connectToHost(host, port);
2534         socket->waitForConnected(q->connectTimeout() * 1000);
2535 
2536         if (socket->state() == QAbstractSocket::ConnectedState) {
2537             // reconnect with credentials was successful -> save data
2538             q->cacheAuthentication(info);
2539 
2540             m_proxyURL.setUserName(info.username);
2541             m_proxyURL.setPassword(info.password);
2542         }
2543     }
2544 
2545     return ConnectionResult{socket, Result::pass()};
2546 }
2547 
2548 //===============================================================================
2549 // Ftp
2550 //===============================================================================
2551 
Ftp(const QByteArray & pool,const QByteArray & app)2552 Ftp::Ftp(const QByteArray &pool, const QByteArray &app)
2553     : SlaveBase(QByteArrayLiteral("ftp"), pool, app)
2554     , d(new FtpInternal(this))
2555 {
2556 }
2557 
2558 Ftp::~Ftp() = default;
2559 
setHost(const QString & host,quint16 port,const QString & user,const QString & pass)2560 void Ftp::setHost(const QString &host, quint16 port, const QString &user, const QString &pass)
2561 {
2562     d->setHost(host, port, user, pass);
2563 }
2564 
openConnection()2565 void Ftp::openConnection()
2566 {
2567     const auto result = d->openConnection();
2568     if (!result.success) {
2569         error(result.error, result.errorString);
2570         return;
2571     }
2572     opened();
2573 }
2574 
closeConnection()2575 void Ftp::closeConnection()
2576 {
2577     d->closeConnection();
2578 }
2579 
stat(const QUrl & url)2580 void Ftp::stat(const QUrl &url)
2581 {
2582     finalize(d->stat(url));
2583 }
2584 
listDir(const QUrl & url)2585 void Ftp::listDir(const QUrl &url)
2586 {
2587     finalize(d->listDir(url));
2588 }
2589 
mkdir(const QUrl & url,int permissions)2590 void Ftp::mkdir(const QUrl &url, int permissions)
2591 {
2592     finalize(d->mkdir(url, permissions));
2593 }
2594 
rename(const QUrl & src,const QUrl & dst,JobFlags flags)2595 void Ftp::rename(const QUrl &src, const QUrl &dst, JobFlags flags)
2596 {
2597     finalize(d->rename(src, dst, flags));
2598 }
2599 
del(const QUrl & url,bool isfile)2600 void Ftp::del(const QUrl &url, bool isfile)
2601 {
2602     finalize(d->del(url, isfile));
2603 }
2604 
chmod(const QUrl & url,int permissions)2605 void Ftp::chmod(const QUrl &url, int permissions)
2606 {
2607     finalize(d->chmod(url, permissions));
2608 }
2609 
get(const QUrl & url)2610 void Ftp::get(const QUrl &url)
2611 {
2612     finalize(d->get(url));
2613 }
2614 
put(const QUrl & url,int permissions,JobFlags flags)2615 void Ftp::put(const QUrl &url, int permissions, JobFlags flags)
2616 {
2617     finalize(d->put(url, permissions, flags));
2618 }
2619 
slave_status()2620 void Ftp::slave_status()
2621 {
2622     d->slave_status();
2623 }
2624 
copy(const QUrl & src,const QUrl & dest,int permissions,JobFlags flags)2625 void Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
2626 {
2627     finalize(d->copy(src, dest, permissions, flags));
2628 }
2629 
finalize(const Result & result)2630 void Ftp::finalize(const Result &result)
2631 {
2632     if (!result.success) {
2633         error(result.error, result.errorString);
2634         return;
2635     }
2636     finished();
2637 }
2638 
operator <<(QDebug dbg,const Result & r)2639 QDebug operator<<(QDebug dbg, const Result &r)
2640 
2641 {
2642     QDebugStateSaver saver(dbg);
2643     dbg.nospace() << "Result("
2644                   << "success=" << r.success << ", err=" << r.error << ", str=" << r.errorString << ')';
2645     return dbg;
2646 }
2647 
2648 // needed for JSON file embedding
2649 #include "ftp.moc"
2650