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

source code of kio/src/kioworkers/ftp/ftp.cpp