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
46QT_BEGIN_NAMESPACE
47
48enum {
49 DefaultFtpPort = 21
50};
51
52static QByteArray makeCacheKey(const QUrl &url)
53{
54 QUrl copy = url;
55 copy.setPort(url.port(defaultPort: DefaultFtpPort));
56 return "ftp-connection:" +
57 copy.toEncoded(options: QUrl::RemovePassword | QUrl::RemovePath | QUrl::RemoveQuery |
58 QUrl::RemoveFragment);
59}
60
61QStringList QNetworkAccessFtpBackendFactory::supportedSchemes() const
62{
63 return QStringList(QStringLiteral("ftp"));
64}
65
66QNetworkAccessBackend *
67QNetworkAccessFtpBackendFactory::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(other: QLatin1String("ftp"), cs: Qt::CaseInsensitive) == 0)
83 return new QNetworkAccessFtpBackend;
84 return nullptr;
85}
86
87class QNetworkAccessCachedFtpConnection: public QFtp, public QNetworkAccessCache::CacheableObject
88{
89 // Q_OBJECT
90public:
91 QNetworkAccessCachedFtpConnection()
92 {
93 setExpires(true);
94 setShareable(false);
95 }
96
97 void dispose() override
98 {
99 connect(sender: this, SIGNAL(done(bool)), receiver: this, SLOT(deleteLater()));
100 close();
101 }
102
103 using QFtp::clearError;
104};
105
106QNetworkAccessFtpBackend::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
112QNetworkAccessFtpBackend::~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(mode: RemoveCachedConnection);
118}
119
120void 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(code: QNetworkReply::ProxyNotFoundError,
139 errorString: tr(s: "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(path: QLatin1String("/"));
149 setUrl(url);
150 }
151 if (url.path().endsWith(c: QLatin1Char('/'))) {
152 error(code: QNetworkReply::ContentOperationNotPermittedError,
153 errorString: tr(s: "Cannot open %1: is a directory").arg(a: url.toString()));
154 finished();
155 return;
156 }
157 state = LoggingIn;
158
159 QNetworkAccessCache* objectCache = QNetworkAccessManagerPrivate::getObjectCache(backend: this);
160 QByteArray cacheKey = makeCacheKey(url);
161 if (!objectCache->requestEntry(key: cacheKey, target: 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(name: "_q_networksession", value: property(name: "_q_networksession"));
167#endif
168#ifndef QT_NO_NETWORKPROXY
169 if (proxy.type() == QNetworkProxy::FtpCachingProxy)
170 ftp->setProxy(host: proxy.hostName(), port: proxy.port());
171#endif
172 ftp->connectToHost(host: url.host(), port: url.port(defaultPort: DefaultFtpPort));
173 ftp->login(user: url.userName(), password: url.password());
174
175 objectCache->addEntry(key: cacheKey, entry: ftp);
176 ftpConnectionReady(object: ftp);
177 }
178
179 // Put operation
180 if (operation() == QNetworkAccessManager::PutOperation) {
181 uploadDevice = QNonContiguousByteDeviceFactory::wrap(byteDevice: createUploadByteDevice());
182 uploadDevice->setParent(this);
183 }
184}
185
186void QNetworkAccessFtpBackend::closeDownstreamChannel()
187{
188 state = Disconnecting;
189 if (operation() == QNetworkAccessManager::GetOperation)
190 ftp->abort();
191}
192
193void QNetworkAccessFtpBackend::downstreamReadyWrite()
194{
195 if (state == Transferring && ftp && ftp->bytesAvailable())
196 ftpReadyRead();
197}
198
199void QNetworkAccessFtpBackend::ftpConnectionReady(QNetworkAccessCache::CacheableObject *o)
200{
201 ftp = static_cast<QNetworkAccessCachedFtpConnection *>(o);
202 connect(asender: ftp, SIGNAL(done(bool)), SLOT(ftpDone()));
203 connect(asender: ftp, SIGNAL(rawCommandReply(int,QString)), SLOT(ftpRawCommandReply(int,QString)));
204 connect(asender: 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
213void QNetworkAccessFtpBackend::disconnectFromFtp(CacheCleanupMode mode)
214{
215 state = Disconnecting;
216
217 if (ftp) {
218 disconnect(sender: ftp, signal: nullptr, receiver: this, member: nullptr);
219
220 QByteArray key = makeCacheKey(url: url());
221 if (mode == RemoveCachedConnection) {
222 QNetworkAccessManagerPrivate::getObjectCache(backend: this)->removeEntry(key);
223 ftp->dispose();
224 } else {
225 QNetworkAccessManagerPrivate::getObjectCache(backend: this)->releaseEntry(key);
226 }
227
228 ftp = nullptr;
229 }
230}
231
232void 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(userInfo: QString());
241 setUrl(newUrl);
242
243 QAuthenticator auth;
244 authenticationRequired(auth: &auth);
245
246 if (!auth.isNull()) {
247 // try again:
248 newUrl.setUserName(userName: auth.user());
249 ftp->login(user: auth.user(), password: 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(code: QNetworkReply::AuthenticationRequiredError,
258 errorString: tr(s: "Logging in to %1 failed: authentication required")
259 .arg(a: 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, errorString: ftp->errorString());
278 }
279
280 // we're not connected, so remove the cache entry:
281 disconnectFromFtp(mode: RemoveCachedConnection);
282 finished();
283 return;
284 }
285
286 // check for errors:
287 if (state == CheckingFeatures && ftp->error() == QFtp::UnknownError) {
288 qWarning(msg: "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(s: "Error while downloading %1: %2");
294 else
295 msg = tr(s: "Error while uploading %1: %2");
296 msg = msg.arg(args: url().toString(), args: ftp->errorString());
297
298 if (state == Statting)
299 // file probably doesn't exist
300 error(code: QNetworkReply::ContentNotFoundError, errorString: msg);
301 else
302 error(code: QNetworkReply::ContentAccessDenied, errorString: msg);
303
304 disconnectFromFtp(mode: 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(command: 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(s: 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(s: QLatin1String("/~/"))) {
332 // Remove leading /~ symbols
333 QUrl newUrl = url();
334 newUrl.setPath(path: path.mid(position: 2));
335 setUrl(newUrl);
336 }
337
338 // send PWD command to retrieve the working directory
339 pwdId = ftp->rawCommand(command: 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(command: QLatin1String("TYPE I"));
348 sizeId = ftp->rawCommand(command: QLatin1String("SIZE ") + path); // get size
349 }
350 if (supportsMdtm)
351 mdtmId = ftp->rawCommand(command: 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(file: url().path(), dev: nullptr, type);
366 } else {
367 ftp->put(dev: uploadDevice, file: url().path(), type);
368 }
369
370 } else if (state == Transferring) {
371 // upload or download finished
372 disconnectFromFtp();
373 finished();
374 }
375}
376
377void QNetworkAccessFtpBackend::ftpReadyRead()
378{
379 QByteArray data = ftp->readAll();
380 QByteDataBuffer list;
381 list.append(bd: data);
382 data.clear(); // important because of implicit sharing!
383 writeDownstreamData(list);
384}
385
386void 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(s: QLatin1String("SIZE"), cs: Qt::CaseSensitive))
396 supportsSize = true;
397 if (text.contains(s: QLatin1String("MDTM"), cs: Qt::CaseSensitive))
398 supportsMdtm = true;
399 if (text.contains(s: QLatin1String("PWD"), cs: Qt::CaseSensitive))
400 supportsPwd = true;
401 } else if (id == pwdId && code == 257) {
402 QString pwdPath;
403 int startIndex = text.indexOf(c: '"');
404 int stopIndex = text.lastIndexOf(c: '"');
405 if (stopIndex - startIndex) {
406 // The working directory is a substring between \" symbols.
407 startIndex++; // skip the first \" symbol
408 pwdPath = text.mid(position: startIndex, n: 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(s: pwdPath)) {
420 if (pwdPath.endsWith(c: QLatin1Char('/')))
421 pwdPath.chop(n: 1);
422 // Prepend working directory to the URL path
423 QUrl newUrl = url();
424 newUrl.setPath(path: 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(header: QNetworkRequest::ContentLengthHeader, value: text.toLongLong());
431#if QT_CONFIG(datestring)
432 } else if (id == mdtmId) {
433 QDateTime dt = QDateTime::fromString(s: text, format: QLatin1String("yyyyMMddHHmmss"));
434 setHeader(header: QNetworkRequest::LastModifiedHeader, value: dt);
435#endif
436 }
437 }
438}
439
440QT_END_NAMESPACE
441

source code of qtbase/src/network/access/qnetworkaccessftpbackend.cpp