1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtNetwork module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qnetworkaccessftpbackend_p.h"
41 #include "qnetworkaccessmanager_p.h"
42 #include "QtNetwork/qauthenticator.h"
43 #include "private/qnoncontiguousbytedevice_p.h"
44 #include <QStringList>
45 
46 QT_BEGIN_NAMESPACE
47 
48 enum {
49     DefaultFtpPort = 21
50 };
51 
makeCacheKey(const QUrl & url)52 static QByteArray makeCacheKey(const QUrl &url)
53 {
54     QUrl copy = url;
55     copy.setPort(url.port(DefaultFtpPort));
56     return "ftp-connection:" +
57         copy.toEncoded(QUrl::RemovePassword | QUrl::RemovePath | QUrl::RemoveQuery |
58                        QUrl::RemoveFragment);
59 }
60 
supportedSchemes() const61 QStringList QNetworkAccessFtpBackendFactory::supportedSchemes() const
62 {
63     return QStringList(QStringLiteral("ftp"));
64 }
65 
66 QNetworkAccessBackend *
create(QNetworkAccessManager::Operation op,const QNetworkRequest & request) const67 QNetworkAccessFtpBackendFactory::create(QNetworkAccessManager::Operation op,
68                                         const QNetworkRequest &request) const
69 {
70     // is it an operation we know of?
71     switch (op) {
72     case QNetworkAccessManager::GetOperation:
73     case QNetworkAccessManager::PutOperation:
74         break;
75 
76     default:
77         // no, we can't handle this operation
78         return nullptr;
79     }
80 
81     QUrl url = request.url();
82     if (url.scheme().compare(QLatin1String("ftp"), Qt::CaseInsensitive) == 0)
83         return new QNetworkAccessFtpBackend;
84     return nullptr;
85 }
86 
87 class QNetworkAccessCachedFtpConnection: public QFtp, public QNetworkAccessCache::CacheableObject
88 {
89     // Q_OBJECT
90 public:
QNetworkAccessCachedFtpConnection()91     QNetworkAccessCachedFtpConnection()
92     {
93         setExpires(true);
94         setShareable(false);
95     }
96 
dispose()97     void dispose() override
98     {
99         connect(this, SIGNAL(done(bool)), this, SLOT(deleteLater()));
100         close();
101     }
102 
103     using QFtp::clearError;
104 };
105 
QNetworkAccessFtpBackend()106 QNetworkAccessFtpBackend::QNetworkAccessFtpBackend()
107     : ftp(nullptr), uploadDevice(nullptr), totalBytes(0), helpId(-1), sizeId(-1), mdtmId(-1), pwdId(-1),
108     supportsSize(false), supportsMdtm(false), supportsPwd(false), state(Idle)
109 {
110 }
111 
~QNetworkAccessFtpBackend()112 QNetworkAccessFtpBackend::~QNetworkAccessFtpBackend()
113 {
114     //if backend destroyed while in use, then abort (this is the code path from QNetworkReply::abort)
115     if (ftp && state != Disconnecting)
116         ftp->abort();
117     disconnectFromFtp(RemoveCachedConnection);
118 }
119 
open()120 void QNetworkAccessFtpBackend::open()
121 {
122 #ifndef QT_NO_NETWORKPROXY
123     QNetworkProxy proxy;
124     const auto proxies = proxyList();
125     for (const QNetworkProxy &p : proxies) {
126         // use the first FTP proxy
127         // or no proxy at all
128         if (p.type() == QNetworkProxy::FtpCachingProxy
129             || p.type() == QNetworkProxy::NoProxy) {
130             proxy = p;
131             break;
132         }
133     }
134 
135     // did we find an FTP proxy or a NoProxy?
136     if (proxy.type() == QNetworkProxy::DefaultProxy) {
137         // unsuitable proxies
138         error(QNetworkReply::ProxyNotFoundError,
139               tr("No suitable proxy found"));
140         finished();
141         return;
142     }
143 
144 #endif
145 
146     QUrl url = this->url();
147     if (url.path().isEmpty()) {
148         url.setPath(QLatin1String("/"));
149         setUrl(url);
150     }
151     if (url.path().endsWith(QLatin1Char('/'))) {
152         error(QNetworkReply::ContentOperationNotPermittedError,
153               tr("Cannot open %1: is a directory").arg(url.toString()));
154         finished();
155         return;
156     }
157     state = LoggingIn;
158 
159     QNetworkAccessCache* objectCache = QNetworkAccessManagerPrivate::getObjectCache(this);
160     QByteArray cacheKey = makeCacheKey(url);
161     if (!objectCache->requestEntry(cacheKey, this,
162                              SLOT(ftpConnectionReady(QNetworkAccessCache::CacheableObject*)))) {
163         ftp = new QNetworkAccessCachedFtpConnection;
164 #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section
165         //copy network session down to the QFtp
166         ftp->setProperty("_q_networksession", property("_q_networksession"));
167 #endif
168 #ifndef QT_NO_NETWORKPROXY
169         if (proxy.type() == QNetworkProxy::FtpCachingProxy)
170             ftp->setProxy(proxy.hostName(), proxy.port());
171 #endif
172         ftp->connectToHost(url.host(), url.port(DefaultFtpPort));
173         ftp->login(url.userName(), url.password());
174 
175         objectCache->addEntry(cacheKey, ftp);
176         ftpConnectionReady(ftp);
177     }
178 
179     // Put operation
180     if (operation() == QNetworkAccessManager::PutOperation) {
181         uploadDevice = QNonContiguousByteDeviceFactory::wrap(createUploadByteDevice());
182         uploadDevice->setParent(this);
183     }
184 }
185 
closeDownstreamChannel()186 void QNetworkAccessFtpBackend::closeDownstreamChannel()
187 {
188     state = Disconnecting;
189     if (operation() == QNetworkAccessManager::GetOperation)
190         ftp->abort();
191 }
192 
downstreamReadyWrite()193 void QNetworkAccessFtpBackend::downstreamReadyWrite()
194 {
195     if (state == Transferring && ftp && ftp->bytesAvailable())
196         ftpReadyRead();
197 }
198 
ftpConnectionReady(QNetworkAccessCache::CacheableObject * o)199 void QNetworkAccessFtpBackend::ftpConnectionReady(QNetworkAccessCache::CacheableObject *o)
200 {
201     ftp = static_cast<QNetworkAccessCachedFtpConnection *>(o);
202     connect(ftp, SIGNAL(done(bool)), SLOT(ftpDone()));
203     connect(ftp, SIGNAL(rawCommandReply(int,QString)), SLOT(ftpRawCommandReply(int,QString)));
204     connect(ftp, SIGNAL(readyRead()), SLOT(ftpReadyRead()));
205 
206     // is the login process done already?
207     if (ftp->state() == QFtp::LoggedIn)
208         ftpDone();
209 
210     // no, defer the actual operation until after we've logged in
211 }
212 
disconnectFromFtp(CacheCleanupMode mode)213 void QNetworkAccessFtpBackend::disconnectFromFtp(CacheCleanupMode mode)
214 {
215     state = Disconnecting;
216 
217     if (ftp) {
218         disconnect(ftp, nullptr, this, nullptr);
219 
220         QByteArray key = makeCacheKey(url());
221         if (mode == RemoveCachedConnection) {
222             QNetworkAccessManagerPrivate::getObjectCache(this)->removeEntry(key);
223             ftp->dispose();
224         } else {
225             QNetworkAccessManagerPrivate::getObjectCache(this)->releaseEntry(key);
226         }
227 
228         ftp = nullptr;
229     }
230 }
231 
ftpDone()232 void QNetworkAccessFtpBackend::ftpDone()
233 {
234     // the last command we sent is done
235     if (state == LoggingIn && ftp->state() != QFtp::LoggedIn) {
236         if (ftp->state() == QFtp::Connected) {
237             // the login did not succeed
238             QUrl newUrl = url();
239             QString userInfo = newUrl.userInfo();
240             newUrl.setUserInfo(QString());
241             setUrl(newUrl);
242 
243             QAuthenticator auth;
244             authenticationRequired(&auth);
245 
246             if (!auth.isNull()) {
247                 // try again:
248                 newUrl.setUserName(auth.user());
249                 ftp->login(auth.user(), auth.password());
250                 return;
251             }
252 
253             // Re insert the user info so that we can remove the cache entry.
254             newUrl.setUserInfo(userInfo);
255             setUrl(newUrl);
256 
257             error(QNetworkReply::AuthenticationRequiredError,
258                   tr("Logging in to %1 failed: authentication required")
259                   .arg(url().host()));
260         } else {
261             // we did not connect
262             QNetworkReply::NetworkError code;
263             switch (ftp->error()) {
264             case QFtp::HostNotFound:
265                 code = QNetworkReply::HostNotFoundError;
266                 break;
267 
268             case QFtp::ConnectionRefused:
269                 code = QNetworkReply::ConnectionRefusedError;
270                 break;
271 
272             default:
273                 code = QNetworkReply::ProtocolFailure;
274                 break;
275             }
276 
277             error(code, ftp->errorString());
278         }
279 
280         // we're not connected, so remove the cache entry:
281         disconnectFromFtp(RemoveCachedConnection);
282         finished();
283         return;
284     }
285 
286     // check for errors:
287     if (state == CheckingFeatures && ftp->error() == QFtp::UnknownError) {
288         qWarning("QNetworkAccessFtpBackend: HELP command failed, ignoring it");
289         ftp->clearError();
290     } else if (ftp->error() != QFtp::NoError) {
291         QString msg;
292         if (operation() == QNetworkAccessManager::GetOperation)
293             msg = tr("Error while downloading %1: %2");
294         else
295             msg = tr("Error while uploading %1: %2");
296         msg = msg.arg(url().toString(), ftp->errorString());
297 
298         if (state == Statting)
299             // file probably doesn't exist
300             error(QNetworkReply::ContentNotFoundError,  msg);
301         else
302             error(QNetworkReply::ContentAccessDenied, msg);
303 
304         disconnectFromFtp(RemoveCachedConnection);
305         finished();
306     }
307 
308     if (state == LoggingIn) {
309         state = CheckingFeatures;
310         // send help command to find out if server supports SIZE, MDTM, and PWD
311         if (operation() == QNetworkAccessManager::GetOperation
312             || operation() == QNetworkAccessManager::PutOperation) {
313             helpId = ftp->rawCommand(QLatin1String("HELP")); // get supported commands
314         } else {
315             ftpDone();
316         }
317     } else if (state == CheckingFeatures) {
318         // If a URL path starts with // prefix (/%2F decoded), the resource will
319         // be retrieved by an absolute path starting with the root directory.
320         // For the other URLs, the working directory is retrieved by PWD command
321         // and prepended to the resource path as an absolute path starting with
322         // the working directory.
323         state = ResolvingPath;
324         QString path = url().path();
325         if (path.startsWith(QLatin1String("//")) || supportsPwd == false) {
326             ftpDone(); // no commands sent, move to the next state
327         } else {
328             // If a path starts with /~/ prefix, its prefix will be replaced by
329             // the working directory as an absolute path starting with working
330             // directory.
331             if (path.startsWith(QLatin1String("/~/"))) {
332                 // Remove leading /~ symbols
333                 QUrl newUrl = url();
334                 newUrl.setPath(path.mid(2));
335                 setUrl(newUrl);
336             }
337 
338             // send PWD command to retrieve the working directory
339             pwdId = ftp->rawCommand(QLatin1String("PWD"));
340         }
341     } else if (state == ResolvingPath) {
342         state = Statting;
343         if (operation() == QNetworkAccessManager::GetOperation) {
344             // logged in successfully, send the stat requests (if supported)
345             const QString path = url().path();
346             if (supportsSize) {
347                 ftp->rawCommand(QLatin1String("TYPE I"));
348                 sizeId = ftp->rawCommand(QLatin1String("SIZE ") + path); // get size
349             }
350             if (supportsMdtm)
351                 mdtmId = ftp->rawCommand(QLatin1String("MDTM ") + path); // get modified time
352             if (!supportsSize && !supportsMdtm)
353                 ftpDone();      // no commands sent, move to the next state
354         } else {
355             ftpDone();
356         }
357     } else if (state == Statting) {
358         // statted successfully, send the actual request
359         metaDataChanged();
360         state = Transferring;
361 
362         QFtp::TransferType type = QFtp::Binary;
363         if (operation() == QNetworkAccessManager::GetOperation) {
364             setCachingEnabled(true);
365             ftp->get(url().path(), nullptr, type);
366         } else {
367             ftp->put(uploadDevice, url().path(), type);
368         }
369 
370     } else if (state == Transferring) {
371         // upload or download finished
372         disconnectFromFtp();
373         finished();
374     }
375 }
376 
ftpReadyRead()377 void QNetworkAccessFtpBackend::ftpReadyRead()
378 {
379     QByteArray data = ftp->readAll();
380     QByteDataBuffer list;
381     list.append(data);
382     data.clear(); // important because of implicit sharing!
383     writeDownstreamData(list);
384 }
385 
ftpRawCommandReply(int code,const QString & text)386 void QNetworkAccessFtpBackend::ftpRawCommandReply(int code, const QString &text)
387 {
388     //qDebug() << "FTP reply:" << code << text;
389     int id = ftp->currentId();
390 
391     if ((id == helpId) && ((code == 200) || (code == 214))) {     // supported commands
392         // the "FEAT" ftp command would be nice here, but it is not part of the
393         // initial FTP RFC 959, neither ar "SIZE" nor "MDTM" (they are all specified
394         // in RFC 3659)
395         if (text.contains(QLatin1String("SIZE"), Qt::CaseSensitive))
396             supportsSize = true;
397         if (text.contains(QLatin1String("MDTM"), Qt::CaseSensitive))
398             supportsMdtm = true;
399         if (text.contains(QLatin1String("PWD"), Qt::CaseSensitive))
400             supportsPwd = true;
401     } else if (id == pwdId && code == 257) {
402         QString pwdPath;
403         int startIndex = text.indexOf('"');
404         int stopIndex = text.lastIndexOf('"');
405         if (stopIndex - startIndex) {
406             // The working directory is a substring between \" symbols.
407             startIndex++; // skip the first \" symbol
408             pwdPath = text.mid(startIndex, stopIndex - startIndex);
409         } else {
410             // If there is no or only one \" symbol, use all the characters of
411             // text.
412             pwdPath = text;
413         }
414 
415         // If a URL path starts with the working directory prefix, its resource
416         // will be retrieved from the working directory. Otherwise, the path of
417         // the working directory is prepended to the resource path.
418         QString urlPath = url().path();
419         if (!urlPath.startsWith(pwdPath)) {
420             if (pwdPath.endsWith(QLatin1Char('/')))
421                 pwdPath.chop(1);
422             // Prepend working directory to the URL path
423             QUrl newUrl = url();
424             newUrl.setPath(pwdPath % urlPath);
425             setUrl(newUrl);
426         }
427     } else if (code == 213) {          // file status
428         if (id == sizeId) {
429             // reply to the size command
430             setHeader(QNetworkRequest::ContentLengthHeader, text.toLongLong());
431 #if QT_CONFIG(datestring)
432         } else if (id == mdtmId) {
433             QDateTime dt = QDateTime::fromString(text, QLatin1String("yyyyMMddHHmmss"));
434             setHeader(QNetworkRequest::LastModifiedHeader, dt);
435 #endif
436         }
437     }
438 }
439 
440 QT_END_NAMESPACE
441