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