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