1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:network-protocol
4
5//#define QNETWORKACCESSHTTPBACKEND_DEBUG
6
7#include "qnetworkreplyhttpimpl_p.h"
8#include "qnetworkaccessmanager_p.h"
9#include "qnetworkaccesscache_p.h"
10#include "qabstractnetworkcache.h"
11#include "qnetworkrequest.h"
12#include "qnetworkreply.h"
13#include "qnetworkrequest_p.h"
14#include "qnetworkcookie.h"
15#include "qnetworkcookie_p.h"
16#include "QtCore/qdatetime.h"
17#include "QtCore/qelapsedtimer.h"
18#include "QtNetwork/qsslconfiguration.h"
19#include "qhttpthreaddelegate_p.h"
20#include "qhsts_p.h"
21#include "qthread.h"
22#include "QtCore/qcoreapplication.h"
23
24#include <QtCore/private/qthread_p.h>
25#include <QtCore/private/qtools_p.h>
26
27#include "qnetworkcookiejar.h"
28#include "qnetconmonitor_p.h"
29
30#include "qnetworkreplyimpl_p.h"
31
32#include <string.h> // for strchr
33
34QT_BEGIN_NAMESPACE
35
36using namespace Qt::StringLiterals;
37using namespace QtMiscUtils;
38using namespace std::chrono_literals;
39
40class QNetworkProxy;
41
42static inline QByteArray rangeName() { return "Range"_ba; }
43static inline QByteArray cacheControlName() { return "Cache-Control"_ba; }
44static constexpr QByteArrayView bytesEqualPrefix() noexcept { return "bytes="; }
45
46// ### merge with nextField in cookiejar.cpp
47static QHash<QByteArray, QByteArray> parseHttpOptionHeader(QByteArrayView header)
48{
49 // The HTTP header is of the form:
50 // header = #1(directives)
51 // directives = token | value-directive
52 // value-directive = token "=" (token | quoted-string)
53 QHash<QByteArray, QByteArray> result;
54
55 int pos = 0;
56 while (true) {
57 // skip spaces
58 pos = nextNonWhitespace(text: header, from: pos);
59 if (pos == header.size())
60 return result; // end of parsing
61
62 // pos points to a non-whitespace
63 int comma = header.indexOf(ch: ',', from: pos);
64 int equal = header.indexOf(ch: '=', from: pos);
65 if (comma == pos || equal == pos)
66 // huh? Broken header.
67 return result;
68
69 // The key name is delimited by either a comma, an equal sign or the end
70 // of the header, whichever comes first
71 int end = comma;
72 if (end == -1)
73 end = header.size();
74 if (equal != -1 && end > equal)
75 end = equal; // equal sign comes before comma/end
76 const auto key = header.sliced(pos, n: end - pos).trimmed();
77 pos = end + 1;
78
79 if (uint(equal) < uint(comma)) {
80 // case: token "=" (token | quoted-string)
81 // skip spaces
82 pos = nextNonWhitespace(text: header, from: pos);
83 if (pos == header.size())
84 // huh? Broken header
85 return result;
86
87 QByteArray value;
88 value.reserve(asize: header.size() - pos);
89 if (header.at(n: pos) == '"') {
90 // case: quoted-string
91 // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
92 // qdtext = <any TEXT except <">>
93 // quoted-pair = "\" CHAR
94 ++pos;
95 while (pos < header.size()) {
96 char c = header.at(n: pos);
97 if (c == '"') {
98 // end of quoted text
99 break;
100 } else if (c == '\\') {
101 ++pos;
102 if (pos >= header.size())
103 // broken header
104 return result;
105 c = header.at(n: pos);
106 }
107
108 value += c;
109 ++pos;
110 }
111 } else {
112 const auto isSeparator = [](char c) {
113 static const char separators[] = "()<>@,;:\\\"/[]?={}";
114 return isLWS(c) || strchr(s: separators, c: c) != nullptr;
115 };
116
117 // case: token
118 while (pos < header.size()) {
119 char c = header.at(n: pos);
120 if (isSeparator(c))
121 break;
122 value += c;
123 ++pos;
124 }
125 }
126
127 result.insert(key: key.toByteArray().toLower(), value);
128
129 // find the comma now:
130 comma = header.indexOf(ch: ',', from: pos);
131 if (comma == -1)
132 return result; // end of parsing
133 pos = comma + 1;
134 } else {
135 // case: token
136 // key is already set
137 result.insert(key: key.toByteArray().toLower(), value: QByteArray());
138 }
139 }
140}
141
142QNetworkReplyHttpImpl::QNetworkReplyHttpImpl(QNetworkAccessManager* const manager,
143 const QNetworkRequest& request,
144 QNetworkAccessManager::Operation& operation,
145 QIODevice* outgoingData)
146 : QNetworkReply(*new QNetworkReplyHttpImplPrivate, manager)
147{
148 Q_D(QNetworkReplyHttpImpl);
149 Q_ASSERT(manager);
150 d->manager = manager;
151 d->managerPrivate = manager->d_func();
152 d->request = request;
153 d->originalRequest = request;
154 d->operation = operation;
155 d->outgoingData = outgoingData;
156 d->url = request.url();
157#ifndef QT_NO_SSL
158 if (request.url().scheme() == "https"_L1)
159 d->sslConfiguration.reset(other: new QSslConfiguration(request.sslConfiguration()));
160#endif
161
162 QObjectPrivate::connect(sender: this, signal: &QNetworkReplyHttpImpl::redirectAllowed, receiverPrivate: d,
163 slot: &QNetworkReplyHttpImplPrivate::followRedirect, type: Qt::QueuedConnection);
164
165 // FIXME Later maybe set to Unbuffered, especially if it is zerocopy or from cache?
166 QIODevice::open(mode: QIODevice::ReadOnly);
167
168
169 // Internal code that does a HTTP reply for the synchronous Ajax
170 // in Qt WebKit.
171 QVariant synchronousHttpAttribute = request.attribute(
172 code: static_cast<QNetworkRequest::Attribute>(QNetworkRequest::SynchronousRequestAttribute));
173 if (synchronousHttpAttribute.isValid()) {
174 d->synchronous = synchronousHttpAttribute.toBool();
175 if (d->synchronous && outgoingData) {
176 // The synchronous HTTP is a corner case, we will put all upload data in one big QByteArray in the outgoingDataBuffer.
177 // Yes, this is not the most efficient thing to do, but on the other hand synchronous XHR needs to die anyway.
178 d->outgoingDataBuffer = std::make_shared<QRingBuffer>();
179 qint64 previousDataSize = 0;
180 do {
181 previousDataSize = d->outgoingDataBuffer->size();
182 d->outgoingDataBuffer->append(qba: d->outgoingData->readAll());
183 } while (d->outgoingDataBuffer->size() != previousDataSize);
184 d->_q_startOperation();
185 return;
186 }
187 }
188
189
190 if (outgoingData) {
191 // there is data to be uploaded, e.g. HTTP POST.
192
193 if (!d->outgoingData->isSequential()) {
194 // fixed size non-sequential (random-access)
195 // just start the operation
196 QMetaObject::invokeMethod(obj: this, member: "_q_startOperation", c: Qt::QueuedConnection);
197 // FIXME make direct call?
198 } else {
199 bool bufferingDisallowed =
200 request.attribute(code: QNetworkRequest::DoNotBufferUploadDataAttribute,
201 defaultValue: false).toBool();
202
203 if (bufferingDisallowed) {
204 // if a valid content-length header for the request was supplied, we can disable buffering
205 // if not, we will buffer anyway
206
207 const auto sizeOpt = QNetworkHeadersPrivate::toInt(
208 value: request.headers().value(name: QHttpHeaders::WellKnownHeader::ContentLength));
209 if (sizeOpt) {
210 QMetaObject::invokeMethod(obj: this, member: "_q_startOperation", c: Qt::QueuedConnection);
211 // FIXME make direct call?
212 } else {
213 d->state = d->Buffering;
214 QMetaObject::invokeMethod(obj: this, member: "_q_bufferOutgoingData", c: Qt::QueuedConnection);
215 }
216 } else {
217 // _q_startOperation will be called when the buffering has finished.
218 d->state = d->Buffering;
219 QMetaObject::invokeMethod(obj: this, member: "_q_bufferOutgoingData", c: Qt::QueuedConnection);
220 }
221 }
222 } else {
223 // No outgoing data (POST, ..)
224 d->_q_startOperation();
225 }
226}
227
228QNetworkReplyHttpImpl::~QNetworkReplyHttpImpl()
229{
230 // This will do nothing if the request was already finished or aborted
231 emit abortHttpRequest();
232}
233
234void QNetworkReplyHttpImpl::close()
235{
236 Q_D(QNetworkReplyHttpImpl);
237
238 if (d->state == QNetworkReplyPrivate::Aborted ||
239 d->state == QNetworkReplyPrivate::Finished)
240 return;
241
242 // According to the documentation close only stops the download
243 // by closing we can ignore the download part and continue uploading.
244 QNetworkReply::close();
245
246 // call finished which will emit signals
247 // FIXME shouldn't this be emitted Queued?
248 d->error(code: OperationCanceledError, errorString: tr(s: "Operation canceled"));
249 d->finished();
250}
251
252void QNetworkReplyHttpImpl::abort()
253{
254 Q_D(QNetworkReplyHttpImpl);
255 // FIXME
256 if (d->state == QNetworkReplyPrivate::Finished || d->state == QNetworkReplyPrivate::Aborted)
257 return;
258
259 QNetworkReply::close();
260
261 if (d->state != QNetworkReplyPrivate::Finished) {
262 // call finished which will emit signals
263 // FIXME shouldn't this be emitted Queued?
264 d->error(code: OperationCanceledError, errorString: tr(s: "Operation canceled"));
265 d->finished();
266 }
267
268 d->state = QNetworkReplyPrivate::Aborted;
269
270 emit abortHttpRequest();
271}
272
273qint64 QNetworkReplyHttpImpl::bytesAvailable() const
274{
275 Q_D(const QNetworkReplyHttpImpl);
276
277 // if we load from cache device
278 if (d->cacheLoadDevice) {
279 return QNetworkReply::bytesAvailable() + d->cacheLoadDevice->bytesAvailable();
280 }
281
282 // zerocopy buffer
283 if (d->downloadZerocopyBuffer) {
284 return QNetworkReply::bytesAvailable() + d->downloadBufferCurrentSize - d->downloadBufferReadPosition;
285 }
286
287 if (d->decompressHelper.isValid()) {
288 if (d->decompressHelper.isCountingBytes())
289 return QNetworkReply::bytesAvailable() + d->decompressHelper.uncompressedSize();
290 if (d->decompressHelper.hasData())
291 return QNetworkReply::bytesAvailable() + 1;
292 }
293
294 // normal buffer
295 return QNetworkReply::bytesAvailable();
296}
297
298bool QNetworkReplyHttpImpl::isSequential () const
299{
300 // FIXME In the cache of a cached load or the zero-copy buffer we could actually be non-sequential.
301 // FIXME however this requires us to implement stuff like seek() too.
302 return true;
303}
304
305qint64 QNetworkReplyHttpImpl::size() const
306{
307 // FIXME At some point, this could return a proper value, e.g. if we're non-sequential.
308 return QNetworkReply::size();
309}
310
311qint64 QNetworkReplyHttpImpl::readData(char* data, qint64 maxlen)
312{
313 Q_D(QNetworkReplyHttpImpl);
314
315 // cacheload device
316 if (d->cacheLoadDevice) {
317 // FIXME bytesdownloaded, position etc?
318
319 qint64 ret = d->cacheLoadDevice->read(data, maxlen);
320 return ret;
321 }
322
323 // zerocopy buffer
324 if (d->downloadZerocopyBuffer) {
325 // FIXME bytesdownloaded, position etc?
326
327 qint64 howMuch = qMin(a: maxlen, b: (d->downloadBufferCurrentSize - d->downloadBufferReadPosition));
328 memcpy(dest: data, src: d->downloadZerocopyBuffer + d->downloadBufferReadPosition, n: howMuch);
329 d->downloadBufferReadPosition += howMuch;
330 return howMuch;
331
332 }
333
334 if (d->decompressHelper.isValid() && (d->decompressHelper.hasData() || !isFinished())) {
335 if (maxlen == 0 || !d->decompressHelper.hasData())
336 return 0;
337 const qint64 bytesRead = d->decompressHelper.read(data, maxSize: maxlen);
338 if (!d->decompressHelper.isValid()) {
339 d->error(code: QNetworkReplyImpl::NetworkError::UnknownContentError,
340 errorString: QCoreApplication::translate(context: "QHttp", key: "Decompression failed: %1")
341 .arg(a: d->decompressHelper.errorString()));
342 d->decompressHelper.clear();
343 return -1;
344 }
345 if (d->cacheSaveDevice) {
346 // Need to write to the cache now that we have the data
347 d->cacheSaveDevice->write(data, len: bytesRead);
348 // ... and if we've read everything then the cache can be closed.
349 if (isFinished() && !d->decompressHelper.hasData())
350 d->completeCacheSave();
351 }
352 // In case of buffer size restriction we need to emit that it has been emptied
353 qint64 wasBuffered = d->bytesBuffered;
354 d->bytesBuffered = 0;
355 if (readBufferSize())
356 emit readBufferFreed(size: wasBuffered);
357 return bytesRead;
358 }
359
360 // normal buffer
361 if (d->state == d->Finished || d->state == d->Aborted)
362 return -1;
363
364 qint64 wasBuffered = d->bytesBuffered;
365 d->bytesBuffered = 0;
366 if (readBufferSize())
367 emit readBufferFreed(size: wasBuffered);
368 return 0;
369}
370
371void QNetworkReplyHttpImpl::setReadBufferSize(qint64 size)
372{
373 QNetworkReply::setReadBufferSize(size);
374 emit readBufferSizeChanged(size);
375 return;
376}
377
378bool QNetworkReplyHttpImpl::canReadLine () const
379{
380 Q_D(const QNetworkReplyHttpImpl);
381
382 if (QNetworkReply::canReadLine())
383 return true;
384
385 if (d->cacheLoadDevice)
386 return d->cacheLoadDevice->canReadLine();
387
388 if (d->downloadZerocopyBuffer)
389 return memchr(s: d->downloadZerocopyBuffer + d->downloadBufferReadPosition, c: '\n', n: d->downloadBufferCurrentSize - d->downloadBufferReadPosition);
390
391 return false;
392}
393
394#ifndef QT_NO_SSL
395void QNetworkReplyHttpImpl::ignoreSslErrors()
396{
397 Q_D(QNetworkReplyHttpImpl);
398 Q_ASSERT(d->managerPrivate);
399
400 if (d->managerPrivate->stsEnabled && d->managerPrivate->stsCache.isKnownHost(url: url())) {
401 // We cannot ignore any Security Transport-related errors for this host.
402 return;
403 }
404
405 d->pendingIgnoreAllSslErrors = true;
406}
407
408void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError> &errors)
409{
410 Q_D(QNetworkReplyHttpImpl);
411 Q_ASSERT(d->managerPrivate);
412
413 if (d->managerPrivate->stsEnabled && d->managerPrivate->stsCache.isKnownHost(url: url())) {
414 // We cannot ignore any Security Transport-related errors for this host.
415 return;
416 }
417
418 // the pending list is set if QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
419 // is called before QNetworkAccessManager::get() (or post(), etc.)
420 d->pendingIgnoreSslErrorsList = errors;
421}
422
423void QNetworkReplyHttpImpl::setSslConfigurationImplementation(const QSslConfiguration &newconfig)
424{
425 // Setting a SSL configuration on a reply is not supported. The user needs to set
426 // her/his QSslConfiguration on the QNetworkRequest.
427 Q_UNUSED(newconfig);
428}
429
430void QNetworkReplyHttpImpl::sslConfigurationImplementation(QSslConfiguration &configuration) const
431{
432 Q_D(const QNetworkReplyHttpImpl);
433 if (d->sslConfiguration.data())
434 configuration = *d->sslConfiguration;
435 else
436 configuration = request().sslConfiguration();
437}
438#endif
439
440QNetworkReplyHttpImplPrivate::QNetworkReplyHttpImplPrivate()
441 : QNetworkReplyPrivate()
442 , manager(nullptr)
443 , managerPrivate(nullptr)
444 , synchronous(false)
445 , state(Idle)
446 , statusCode(0)
447 , uploadByteDevicePosition(false)
448 , uploadDeviceChoking(false)
449 , outgoingData(nullptr)
450 , bytesUploaded(-1)
451 , cacheLoadDevice(nullptr)
452 , loadingFromCache(false)
453 , cacheSaveDevice(nullptr)
454 , cacheEnabled(false)
455 , resumeOffset(0)
456 , bytesDownloaded(0)
457 , bytesBuffered(0)
458 , transferTimeout(nullptr)
459 , downloadBufferReadPosition(0)
460 , downloadBufferCurrentSize(0)
461 , downloadZerocopyBuffer(nullptr)
462 , pendingDownloadDataEmissions(std::make_shared<QAtomicInt>())
463 , pendingDownloadProgressEmissions(std::make_shared<QAtomicInt>())
464 #ifndef QT_NO_SSL
465 , pendingIgnoreAllSslErrors(false)
466 #endif
467
468{
469}
470
471QNetworkReplyHttpImplPrivate::~QNetworkReplyHttpImplPrivate()
472{
473 if (cacheSaveDevice)
474 managerPrivate->networkCache->remove(url);
475}
476
477/*
478 For a given httpRequest
479 1) If AlwaysNetwork, return
480 2) If we have a cache entry for this url populate headers so the server can return 304
481 3) Calculate if response_is_fresh and if so send the cache and set loadedFromCache to true
482 */
483bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &httpRequest)
484{
485 QNetworkRequest::CacheLoadControl CacheLoadControlAttribute =
486 (QNetworkRequest::CacheLoadControl)request.attribute(code: QNetworkRequest::CacheLoadControlAttribute, defaultValue: QNetworkRequest::PreferNetwork).toInt();
487
488 auto requestHeaders = request.headers();
489 if (CacheLoadControlAttribute == QNetworkRequest::AlwaysNetwork) {
490 // If the request does not already specify preferred cache-control
491 // force reload from the network and tell any caching proxy servers to reload too
492 if (!requestHeaders.contains(name: QHttpHeaders::WellKnownHeader::CacheControl)) {
493 const auto noCache = "no-cache"_ba;
494 httpRequest.setHeaderField(name: cacheControlName(), data: noCache);
495 httpRequest.setHeaderField(name: "Pragma"_ba, data: noCache);
496 }
497 return false;
498 }
499
500 // The disk cache API does not currently support partial content retrieval.
501 // That is why we don't use the disk cache for any such requests.
502 if (requestHeaders.contains(name: QHttpHeaders::WellKnownHeader::Range))
503 return false;
504
505 QAbstractNetworkCache *nc = managerPrivate->networkCache;
506 if (!nc)
507 return false; // no local cache
508
509 QNetworkCacheMetaData metaData = nc->metaData(url: httpRequest.url());
510 if (!metaData.isValid())
511 return false; // not in cache
512
513 if (!metaData.saveToDisk())
514 return false;
515
516 QHttpHeaders cacheHeaders = metaData.headers();
517
518 const auto sizeOpt = QNetworkHeadersPrivate::toInt(
519 value: cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::ContentLength));
520 if (sizeOpt) {
521 std::unique_ptr<QIODevice> data(nc->data(url: httpRequest.url()));
522 if (!data || data->size() < sizeOpt.value())
523 return false; // The data is smaller than the content-length specified
524 }
525
526 auto value = cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::ETag);
527 if (!value.empty())
528 httpRequest.setHeaderField(name: "If-None-Match"_ba, data: value.toByteArray());
529
530 QDateTime lastModified = metaData.lastModified();
531 if (lastModified.isValid())
532 httpRequest.setHeaderField(name: "If-Modified-Since"_ba, data: QNetworkHeadersPrivate::toHttpDate(dt: lastModified));
533
534 value = cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::CacheControl);
535 if (!value.empty()) {
536 QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(header: value);
537 if (cacheControl.contains(key: "no-cache"_ba))
538 return false;
539 }
540
541 QDateTime currentDateTime = QDateTime::currentDateTimeUtc();
542 QDateTime expirationDate = metaData.expirationDate();
543
544 bool response_is_fresh;
545 if (!expirationDate.isValid()) {
546 /*
547 * age_value
548 * is the value of Age: header received by the cache with
549 * this response.
550 * date_value
551 * is the value of the origin server's Date: header
552 * request_time
553 * is the (local) time when the cache made the request
554 * that resulted in this cached response
555 * response_time
556 * is the (local) time when the cache received the
557 * response
558 * now
559 * is the current (local) time
560 */
561 const auto ageOpt = QNetworkHeadersPrivate::toInt(
562 value: cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::Age));
563 const qint64 age_value = ageOpt.value_or(u: 0);
564
565 QDateTime dateHeader;
566 qint64 date_value = 0;
567 value = cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::Date);
568 if (!value.empty()) {
569 dateHeader = QNetworkHeadersPrivate::fromHttpDate(value);
570 date_value = dateHeader.toSecsSinceEpoch();
571 }
572
573 qint64 now = currentDateTime.toSecsSinceEpoch();
574 qint64 request_time = now;
575 qint64 response_time = now;
576
577 // Algorithm from RFC 2616 section 13.2.3
578 qint64 apparent_age = qMax<qint64>(a: 0, b: response_time - date_value);
579 qint64 corrected_received_age = qMax(a: apparent_age, b: age_value);
580 qint64 response_delay = response_time - request_time;
581 qint64 corrected_initial_age = corrected_received_age + response_delay;
582 qint64 resident_time = now - response_time;
583 qint64 current_age = corrected_initial_age + resident_time;
584
585 qint64 freshness_lifetime = 0;
586
587 // RFC 2616 13.2.4 Expiration Calculations
588 if (lastModified.isValid() && dateHeader.isValid()) {
589 qint64 diff = lastModified.secsTo(dateHeader);
590 freshness_lifetime = diff / 10;
591 const auto warningHeader = "Warning"_ba;
592 if (httpRequest.headerField(name: warningHeader).isEmpty()) {
593 QDateTime dt = currentDateTime.addSecs(secs: current_age);
594 if (currentDateTime.daysTo(dt) > 1)
595 httpRequest.setHeaderField(name: warningHeader, data: "113"_ba);
596 }
597 }
598
599 // the cache-saving code below sets the freshness_lifetime with (dateHeader - last_modified) / 10
600 // if "last-modified" is present, or to Expires otherwise
601 response_is_fresh = (freshness_lifetime > current_age);
602 } else {
603 // expiration date was calculated earlier (e.g. when storing object to the cache)
604 response_is_fresh = currentDateTime.secsTo(expirationDate) >= 0;
605 }
606
607 if (!response_is_fresh)
608 return false;
609
610#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
611 qDebug() << "response_is_fresh" << CacheLoadControlAttribute;
612#endif
613 return sendCacheContents(metaData);
614}
615
616QHttpNetworkRequest::Priority QNetworkReplyHttpImplPrivate::convert(QNetworkRequest::Priority prio)
617{
618 switch (prio) {
619 case QNetworkRequest::LowPriority:
620 return QHttpNetworkRequest::LowPriority;
621 case QNetworkRequest::HighPriority:
622 return QHttpNetworkRequest::HighPriority;
623 case QNetworkRequest::NormalPriority:
624 return QHttpNetworkRequest::NormalPriority;
625 }
626 Q_UNREACHABLE_RETURN(QHttpNetworkRequest::NormalPriority);
627}
628
629void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpRequest)
630{
631 Q_Q(QNetworkReplyHttpImpl);
632
633 QThread *thread = nullptr;
634 if (synchronous) {
635 // A synchronous HTTP request uses its own thread
636 thread = new QThread();
637 thread->setObjectName(QStringLiteral("Qt HTTP synchronous thread"));
638 QObject::connect(sender: thread, SIGNAL(finished()), receiver: thread, SLOT(deleteLater()));
639 thread->start();
640 } else {
641 // We use the manager-global thread.
642 // At some point we could switch to having multiple threads if it makes sense.
643 thread = managerPrivate->createThread();
644 }
645
646 QUrl url = newHttpRequest.url();
647 httpRequest.setUrl(url);
648 httpRequest.setRedirectCount(newHttpRequest.maximumRedirectsAllowed());
649
650 QString scheme = url.scheme();
651 bool ssl = (scheme == "https"_L1 || scheme == "preconnect-https"_L1);
652 q->setAttribute(code: QNetworkRequest::ConnectionEncryptedAttribute, value: ssl);
653 httpRequest.setSsl(ssl);
654
655 bool preConnect = (scheme == "preconnect-http"_L1 || scheme == "preconnect-https"_L1);
656 httpRequest.setPreConnect(preConnect);
657
658#ifndef QT_NO_NETWORKPROXY
659 QNetworkProxy transparentProxy, cacheProxy;
660
661 // FIXME the proxy stuff should be done in the HTTP thread
662 const auto proxies = managerPrivate->queryProxy(query: QNetworkProxyQuery(newHttpRequest.url()));
663 for (const QNetworkProxy &p : proxies) {
664 // use the first proxy that works
665 // for non-encrypted connections, any transparent or HTTP proxy
666 // for encrypted, only transparent proxies
667 if (!ssl
668 && (p.capabilities() & QNetworkProxy::CachingCapability)
669 && (p.type() == QNetworkProxy::HttpProxy ||
670 p.type() == QNetworkProxy::HttpCachingProxy)) {
671 cacheProxy = p;
672 transparentProxy = QNetworkProxy::NoProxy;
673 break;
674 }
675 if (p.isTransparentProxy()) {
676 transparentProxy = p;
677 cacheProxy = QNetworkProxy::NoProxy;
678 break;
679 }
680 }
681
682 // check if at least one of the proxies
683 if (transparentProxy.type() == QNetworkProxy::DefaultProxy &&
684 cacheProxy.type() == QNetworkProxy::DefaultProxy) {
685 // unsuitable proxies
686 QMetaObject::invokeMethod(obj: q, member: "_q_error", c: synchronous ? Qt::DirectConnection : Qt::QueuedConnection,
687 Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ProxyNotFoundError),
688 Q_ARG(QString, QNetworkReplyHttpImpl::tr("No suitable proxy found")));
689 QMetaObject::invokeMethod(obj: q, member: "_q_finished", c: synchronous ? Qt::DirectConnection : Qt::QueuedConnection);
690 return;
691 }
692#endif
693
694 auto redirectPolicy = QNetworkRequest::NoLessSafeRedirectPolicy;
695 const QVariant value = newHttpRequest.attribute(code: QNetworkRequest::RedirectPolicyAttribute);
696 if (value.isValid())
697 redirectPolicy = qvariant_cast<QNetworkRequest::RedirectPolicy>(v: value);
698
699 httpRequest.setRedirectPolicy(redirectPolicy);
700
701 httpRequest.setPriority(convert(prio: newHttpRequest.priority()));
702 loadingFromCache = false;
703
704 switch (operation) {
705 case QNetworkAccessManager::GetOperation:
706 httpRequest.setOperation(QHttpNetworkRequest::Get);
707 // If the request has a body, createUploadByteDevice() and don't use caching
708 if (outgoingData) {
709 invalidateCache();
710 createUploadByteDevice();
711 } else if (loadFromCacheIfAllowed(httpRequest)) {
712 return; // no need to send the request! :)
713 }
714 break;
715
716 case QNetworkAccessManager::HeadOperation:
717 httpRequest.setOperation(QHttpNetworkRequest::Head);
718 if (loadFromCacheIfAllowed(httpRequest))
719 return; // no need to send the request! :)
720 break;
721
722 case QNetworkAccessManager::PostOperation:
723 invalidateCache();
724 httpRequest.setOperation(QHttpNetworkRequest::Post);
725 createUploadByteDevice();
726 break;
727
728 case QNetworkAccessManager::PutOperation:
729 invalidateCache();
730 httpRequest.setOperation(QHttpNetworkRequest::Put);
731 createUploadByteDevice();
732 break;
733
734 case QNetworkAccessManager::DeleteOperation:
735 invalidateCache();
736 httpRequest.setOperation(QHttpNetworkRequest::Delete);
737 break;
738
739 case QNetworkAccessManager::CustomOperation:
740 invalidateCache(); // for safety reasons, we don't know what the operation does
741 httpRequest.setOperation(QHttpNetworkRequest::Custom);
742 createUploadByteDevice();
743 httpRequest.setCustomVerb(newHttpRequest.attribute(
744 code: QNetworkRequest::CustomVerbAttribute).toByteArray());
745 break;
746
747 default:
748 break; // can't happen
749 }
750
751 QHttpHeaders newRequestHeaders = newHttpRequest.headers();
752 if (resumeOffset != 0) {
753 if (newRequestHeaders.contains(name: QHttpHeaders::WellKnownHeader::Range)) {
754 // Need to adjust resume offset for user specified range
755
756 // We've already verified that requestRange starts with "bytes=", see canResume.
757 const auto rangeHeader = newRequestHeaders.value(name: QHttpHeaders::WellKnownHeader::Range);
758 const auto requestRange = QByteArrayView(rangeHeader).mid(pos: bytesEqualPrefix().size());
759
760 newRequestHeaders.removeAll(name: QHttpHeaders::WellKnownHeader::Range);
761
762 int index = requestRange.indexOf(ch: '-');
763
764 quint64 requestStartOffset = requestRange.left(n: index).toULongLong();
765 quint64 requestEndOffset = requestRange.mid(pos: index + 1).toULongLong();
766
767 // In case an end offset is not given it is skipped from the request range
768 QByteArray newRange = bytesEqualPrefix() + QByteArray::number(resumeOffset + requestStartOffset) +
769 '-' + (requestEndOffset ? QByteArray::number(requestEndOffset) : QByteArray());
770
771 httpRequest.setHeaderField(name: rangeName(), data: newRange);
772 } else {
773 httpRequest.setHeaderField(name: rangeName(), data: bytesEqualPrefix() + QByteArray::number(resumeOffset) + '-');
774 }
775 }
776
777 for (int i = 0; i < newRequestHeaders.size(); i++) {
778 const auto name = newRequestHeaders.nameAt(i);
779 const auto value = newRequestHeaders.valueAt(i);
780 httpRequest.setHeaderField(name: QByteArray(name.data(), name.size()), data: value.toByteArray());
781 }
782
783 if (newHttpRequest.attribute(code: QNetworkRequest::HttpPipeliningAllowedAttribute).toBool())
784 httpRequest.setPipeliningAllowed(true);
785
786 if (auto allowed = request.attribute(code: QNetworkRequest::Http2AllowedAttribute);
787 allowed.isValid() && allowed.canConvert<bool>()) {
788 httpRequest.setHTTP2Allowed(allowed.value<bool>());
789 }
790 auto h2cAttribute = request.attribute(code: QNetworkRequest::Http2CleartextAllowedAttribute);
791 // ### Qt7: Stop checking the environment variable
792 if (h2cAttribute.toBool()
793 || (!h2cAttribute.isValid() && qEnvironmentVariableIsSet(varName: "QT_NETWORK_H2C_ALLOWED"))) {
794 httpRequest.setH2cAllowed(true);
795 }
796
797 if (request.attribute(code: QNetworkRequest::Http2DirectAttribute).toBool()) {
798 // Intentionally mutually exclusive - cannot be both direct and 'allowed'
799 httpRequest.setHTTP2Direct(true);
800 httpRequest.setHTTP2Allowed(false);
801 }
802
803 if (static_cast<QNetworkRequest::LoadControl>
804 (newHttpRequest.attribute(code: QNetworkRequest::AuthenticationReuseAttribute,
805 defaultValue: QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual)
806 httpRequest.setWithCredentials(false);
807
808 if (request.attribute(code: QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool())
809 emitAllUploadProgressSignals = true;
810
811 httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName());
812
813 if (scheme.startsWith(s: ("unix"_L1))) {
814 if (QVariant path = newHttpRequest.attribute(code: QNetworkRequest::FullLocalServerNameAttribute);
815 path.isValid() && path.canConvert<QString>()) {
816 httpRequest.setFullLocalServerName(path.toString());
817 }
818 }
819
820 // Create the HTTP thread delegate
821 QHttpThreadDelegate *delegate = new QHttpThreadDelegate;
822 // Propagate Http/2 settings:
823 delegate->http2Parameters = request.http2Configuration();
824 delegate->http1Parameters = request.http1Configuration();
825
826 if (request.attribute(code: QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).isValid())
827 delegate->connectionCacheExpiryTimeoutSeconds = request.attribute(code: QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).toInt();
828
829 // For the synchronous HTTP, this is the normal way the delegate gets deleted
830 // For the asynchronous HTTP this is a safety measure, the delegate deletes itself when HTTP is finished
831 QMetaObject::Connection threadFinishedConnection =
832 QObject::connect(sender: thread, SIGNAL(finished()), receiver: delegate, SLOT(deleteLater()));
833
834 // QTBUG-88063: When 'delegate' is deleted the connection will be added to 'thread''s orphaned
835 // connections list. This orphaned list will be cleaned up next time 'thread' emits a signal,
836 // unfortunately that's the finished signal. It leads to a soft-leak so we do this to disconnect
837 // it on deletion so that it cleans up the orphan immediately.
838 QObject::connect(sender: delegate, signal: &QObject::destroyed, context: delegate, slot: [threadFinishedConnection]() {
839 if (bool(threadFinishedConnection))
840 QObject::disconnect(threadFinishedConnection);
841 });
842
843 // Set the properties it needs
844 delegate->httpRequest = httpRequest;
845#ifndef QT_NO_NETWORKPROXY
846 delegate->cacheProxy = cacheProxy;
847 delegate->transparentProxy = transparentProxy;
848#endif
849 delegate->ssl = ssl;
850#ifndef QT_NO_SSL
851 if (ssl)
852 delegate->incomingSslConfiguration.reset(other: new QSslConfiguration(newHttpRequest.sslConfiguration()));
853#endif
854
855 // Do we use synchronous HTTP?
856 delegate->synchronous = synchronous;
857
858 // The authentication manager is used to avoid the BlockingQueuedConnection communication
859 // from HTTP thread to user thread in some cases.
860 delegate->authenticationManager = managerPrivate->authenticationManager;
861
862 if (!synchronous) {
863 // Tell our zerocopy policy to the delegate
864 QVariant downloadBufferMaximumSizeAttribute = newHttpRequest.attribute(code: QNetworkRequest::MaximumDownloadBufferSizeAttribute);
865 if (downloadBufferMaximumSizeAttribute.isValid()) {
866 delegate->downloadBufferMaximumSize = downloadBufferMaximumSizeAttribute.toLongLong();
867 } else {
868 // If there is no MaximumDownloadBufferSizeAttribute set (which is for the majority
869 // of QNetworkRequest) then we can assume we'll do it anyway for small HTTP replies.
870 // This helps with performance and memory fragmentation.
871 delegate->downloadBufferMaximumSize = 128*1024;
872 }
873
874
875 // These atomic integers are used for signal compression
876 delegate->pendingDownloadData = pendingDownloadDataEmissions;
877 delegate->pendingDownloadProgress = pendingDownloadProgressEmissions;
878
879 // Connect the signals of the delegate to us
880 QObject::connect(sender: delegate, SIGNAL(downloadData(QByteArray)),
881 receiver: q, SLOT(replyDownloadData(QByteArray)),
882 Qt::QueuedConnection);
883 QObject::connect(sender: delegate, SIGNAL(downloadFinished()),
884 receiver: q, SLOT(replyFinished()),
885 Qt::QueuedConnection);
886 QObject::connect(sender: delegate, signal: &QHttpThreadDelegate::socketStartedConnecting,
887 context: q, slot: &QNetworkReply::socketStartedConnecting, type: Qt::QueuedConnection);
888 QObject::connect(sender: delegate, signal: &QHttpThreadDelegate::requestSent,
889 context: q, slot: &QNetworkReply::requestSent, type: Qt::QueuedConnection);
890 connect(sender: delegate, signal: &QHttpThreadDelegate::downloadMetaData, receiverPrivate: this,
891 slot: &QNetworkReplyHttpImplPrivate::replyDownloadMetaData, type: Qt::QueuedConnection);
892 QObject::connect(sender: delegate, SIGNAL(downloadProgress(qint64,qint64)),
893 receiver: q, SLOT(replyDownloadProgressSlot(qint64,qint64)),
894 Qt::QueuedConnection);
895 QObject::connect(sender: delegate, SIGNAL(error(QNetworkReply::NetworkError,QString)),
896 receiver: q, SLOT(httpError(QNetworkReply::NetworkError,QString)),
897 Qt::QueuedConnection);
898 QObject::connect(sender: delegate, SIGNAL(redirected(QUrl,int,int)),
899 receiver: q, SLOT(onRedirected(QUrl,int,int)),
900 Qt::QueuedConnection);
901
902#ifndef QT_NO_SSL
903 QObject::connect(sender: delegate, SIGNAL(sslConfigurationChanged(QSslConfiguration)),
904 receiver: q, SLOT(replySslConfigurationChanged(QSslConfiguration)),
905 Qt::QueuedConnection);
906#endif
907 // Those need to report back, therefore BlockingQueuedConnection
908 QObject::connect(sender: delegate, SIGNAL(authenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
909 receiver: q, SLOT(httpAuthenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
910 Qt::BlockingQueuedConnection);
911#ifndef QT_NO_NETWORKPROXY
912 QObject::connect(sender: delegate, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
913 receiver: q, SLOT(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
914 Qt::BlockingQueuedConnection);
915#endif
916#ifndef QT_NO_SSL
917 QObject::connect(sender: delegate, SIGNAL(encrypted()), receiver: q, SLOT(replyEncrypted()),
918 Qt::BlockingQueuedConnection);
919 QObject::connect(sender: delegate, SIGNAL(sslErrors(QList<QSslError>,bool*,QList<QSslError>*)),
920 receiver: q, SLOT(replySslErrors(QList<QSslError>,bool*,QList<QSslError>*)),
921 Qt::BlockingQueuedConnection);
922 QObject::connect(sender: delegate, SIGNAL(preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)),
923 receiver: q, SLOT(replyPreSharedKeyAuthenticationRequiredSlot(QSslPreSharedKeyAuthenticator*)),
924 Qt::BlockingQueuedConnection);
925#endif
926 // This signal we will use to start the request.
927 QObject::connect(sender: q, SIGNAL(startHttpRequest()), receiver: delegate, SLOT(startRequest()));
928 QObject::connect(sender: q, SIGNAL(abortHttpRequest()), receiver: delegate, SLOT(abortRequest()));
929
930 // To throttle the connection.
931 QObject::connect(sender: q, SIGNAL(readBufferSizeChanged(qint64)), receiver: delegate, SLOT(readBufferSizeChanged(qint64)));
932 QObject::connect(sender: q, SIGNAL(readBufferFreed(qint64)), receiver: delegate, SLOT(readBufferFreed(qint64)));
933
934 if (uploadByteDevice) {
935 QNonContiguousByteDeviceThreadForwardImpl *forwardUploadDevice =
936 new QNonContiguousByteDeviceThreadForwardImpl(uploadByteDevice->atEnd(), uploadByteDevice->size());
937 forwardUploadDevice->setParent(delegate); // needed to make sure it is moved on moveToThread()
938 delegate->httpRequest.setUploadByteDevice(forwardUploadDevice);
939
940 // If the device in the user thread claims it has more data, keep the flow to HTTP thread going
941 QObject::connect(sender: uploadByteDevice.get(), SIGNAL(readyRead()),
942 receiver: q, SLOT(uploadByteDeviceReadyReadSlot()),
943 Qt::QueuedConnection);
944
945 // From user thread to http thread:
946 QObject::connect(sender: q, SIGNAL(haveUploadData(qint64,QByteArray,bool,qint64)),
947 receiver: forwardUploadDevice, SLOT(haveDataSlot(qint64,QByteArray,bool,qint64)), Qt::QueuedConnection);
948 QObject::connect(sender: uploadByteDevice.get(), SIGNAL(readyRead()),
949 receiver: forwardUploadDevice, SIGNAL(readyRead()),
950 Qt::QueuedConnection);
951
952 // From http thread to user thread:
953 QObject::connect(sender: forwardUploadDevice, SIGNAL(wantData(qint64)),
954 receiver: q, SLOT(wantUploadDataSlot(qint64)));
955 QObject::connect(sender: forwardUploadDevice,SIGNAL(processedData(qint64,qint64)),
956 receiver: q, SLOT(sentUploadDataSlot(qint64,qint64)));
957 QObject::connect(sender: forwardUploadDevice, SIGNAL(resetData(bool*)),
958 receiver: q, SLOT(resetUploadDataSlot(bool*)),
959 Qt::BlockingQueuedConnection); // this is the only one with BlockingQueued!
960 }
961 } else if (synchronous) {
962 QObject::connect(sender: q, SIGNAL(startHttpRequestSynchronously()), receiver: delegate, SLOT(startRequestSynchronously()), Qt::BlockingQueuedConnection);
963
964 if (uploadByteDevice) {
965 // For the synchronous HTTP use case the use thread (this one here) is blocked
966 // so we cannot use the asynchronous upload architecture.
967 // We therefore won't use the QNonContiguousByteDeviceThreadForwardImpl but directly
968 // use the uploadByteDevice provided to us by the QNetworkReplyImpl.
969 // The code that is in start() makes sure it is safe to use from a thread
970 // since it only wraps a QRingBuffer
971 delegate->httpRequest.setUploadByteDevice(uploadByteDevice.get());
972 }
973 }
974
975
976 // Move the delegate to the http thread
977 delegate->moveToThread(thread);
978 // This call automatically moves the uploadDevice too for the asynchronous case.
979
980 // Prepare timers for progress notifications
981 downloadProgressSignalChoke.start();
982 uploadProgressSignalChoke.invalidate();
983
984 // Send an signal to the delegate so it starts working in the other thread
985 if (synchronous) {
986 emit q->startHttpRequestSynchronously(); // This one is BlockingQueuedConnection, so it will return when all work is done
987
988 replyDownloadMetaData
989 (delegate->incomingHeaders,
990 delegate->incomingStatusCode,
991 delegate->incomingReasonPhrase,
992 delegate->isPipeliningUsed,
993 QSharedPointer<char>(),
994 delegate->incomingContentLength,
995 delegate->removedContentLength,
996 delegate->isHttp2Used,
997 delegate->isCompressed);
998 replyDownloadData(delegate->synchronousDownloadData);
999
1000 if (delegate->incomingErrorCode != QNetworkReply::NoError)
1001 httpError(error: delegate->incomingErrorCode, errorString: delegate->incomingErrorDetail);
1002
1003 thread->quit();
1004 thread->wait(deadline: QDeadlineTimer(5000));
1005 if (thread->isFinished())
1006 delete thread;
1007 else
1008 QObject::connect(sender: thread, SIGNAL(finished()), receiver: thread, SLOT(deleteLater()));
1009
1010 finished();
1011 } else {
1012 emit q->startHttpRequest(); // Signal to the HTTP thread and go back to user.
1013 }
1014}
1015
1016void QNetworkReplyHttpImplPrivate::invalidateCache()
1017{
1018 QAbstractNetworkCache *nc = managerPrivate->networkCache;
1019 if (nc)
1020 nc->remove(url: httpRequest.url());
1021}
1022
1023void QNetworkReplyHttpImplPrivate::initCacheSaveDevice()
1024{
1025 Q_Q(QNetworkReplyHttpImpl);
1026
1027 // The disk cache does not support partial content, so don't even try to
1028 // save any such content into the cache.
1029 if (q->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt() == 206) {
1030 cacheEnabled = false;
1031 return;
1032 }
1033
1034 // save the meta data
1035 QNetworkCacheMetaData metaData;
1036 metaData.setUrl(url);
1037 metaData = fetchCacheMetaData(metaData);
1038
1039 // save the redirect request also in the cache
1040 QVariant redirectionTarget = q->attribute(code: QNetworkRequest::RedirectionTargetAttribute);
1041 if (redirectionTarget.isValid()) {
1042 QNetworkCacheMetaData::AttributesMap attributes = metaData.attributes();
1043 attributes.insert(key: QNetworkRequest::RedirectionTargetAttribute, value: redirectionTarget);
1044 metaData.setAttributes(attributes);
1045 }
1046
1047 cacheSaveDevice = managerPrivate->networkCache->prepare(metaData);
1048
1049 if (cacheSaveDevice)
1050 q->connect(asender: cacheSaveDevice, SIGNAL(aboutToClose()), SLOT(_q_cacheSaveDeviceAboutToClose()));
1051
1052 if (!cacheSaveDevice || !cacheSaveDevice->isOpen()) {
1053 if (Q_UNLIKELY(cacheSaveDevice && !cacheSaveDevice->isOpen()))
1054 qCritical(msg: "QNetworkReplyImpl: network cache returned a device that is not open -- "
1055 "class %s probably needs to be fixed",
1056 managerPrivate->networkCache->metaObject()->className());
1057
1058 managerPrivate->networkCache->remove(url);
1059 cacheSaveDevice = nullptr;
1060 cacheEnabled = false;
1061 }
1062}
1063
1064void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d)
1065{
1066 Q_Q(QNetworkReplyHttpImpl);
1067
1068 // If we're closed just ignore this data
1069 if (!q->isOpen())
1070 return;
1071
1072 // cache this, we need it later and it's invalidated when dealing with compressed data
1073 auto dataSize = d.size();
1074
1075 if (cacheEnabled && isCachingAllowed() && !cacheSaveDevice)
1076 initCacheSaveDevice();
1077
1078 if (decompressHelper.isValid()) {
1079 qint64 uncompressedBefore = -1;
1080 if (decompressHelper.isCountingBytes())
1081 uncompressedBefore = decompressHelper.uncompressedSize();
1082
1083 decompressHelper.feed(data: std::move(d));
1084
1085 if (!decompressHelper.isValid()) {
1086 error(code: QNetworkReplyImpl::NetworkError::UnknownContentError,
1087 errorString: QCoreApplication::translate(context: "QHttp", key: "Decompression failed: %1")
1088 .arg(a: decompressHelper.errorString()));
1089 decompressHelper.clear();
1090 return;
1091 }
1092
1093 if (!isHttpRedirectResponse()) {
1094 if (decompressHelper.isCountingBytes())
1095 bytesDownloaded += (decompressHelper.uncompressedSize() - uncompressedBefore);
1096 setupTransferTimeout();
1097 }
1098
1099 if (synchronous) {
1100 d = QByteArray();
1101 const qsizetype increments = 16 * 1024;
1102 qint64 bytesRead = 0;
1103 while (decompressHelper.hasData()) {
1104 quint64 nextSize = quint64(d.size()) + quint64(increments);
1105 if (nextSize > quint64(std::numeric_limits<QByteArray::size_type>::max())) {
1106 error(code: QNetworkReplyImpl::NetworkError::UnknownContentError,
1107 errorString: QCoreApplication::translate(context: "QHttp",
1108 key: "Data downloaded is too large to store"));
1109 decompressHelper.clear();
1110 return;
1111 }
1112 d.resize(size: nextSize);
1113 bytesRead += decompressHelper.read(data: d.data() + bytesRead, maxSize: increments);
1114 if (!decompressHelper.isValid()) {
1115 error(code: QNetworkReplyImpl::NetworkError::UnknownContentError,
1116 errorString: QCoreApplication::translate(context: "QHttp", key: "Decompression failed: %1")
1117 .arg(a: decompressHelper.errorString()));
1118 decompressHelper.clear();
1119 return;
1120 }
1121 }
1122 d.resize(size: bytesRead);
1123 // we're synchronous so we're not calling this function again; reset the decompressHelper
1124 decompressHelper.clear();
1125 }
1126 }
1127
1128 // This is going to look a little strange. When downloading data while a
1129 // HTTP redirect is happening (and enabled), we write the redirect
1130 // response to the cache. However, we do not append it to our internal
1131 // buffer as that will contain the response data only for the final
1132 // response
1133 // Note: For compressed data this is done in readData()
1134 if (cacheSaveDevice && !decompressHelper.isValid()) {
1135 cacheSaveDevice->write(data: d);
1136 }
1137
1138 // if decompressHelper is valid then we have compressed data, and this is handled above
1139 if (!decompressHelper.isValid() && !isHttpRedirectResponse()) {
1140 buffer.append(qba: d);
1141 bytesDownloaded += dataSize;
1142 setupTransferTimeout();
1143 }
1144 bytesBuffered += dataSize;
1145
1146 int pendingSignals = pendingDownloadDataEmissions->fetchAndSubAcquire(valueToAdd: 1) - 1;
1147 if (pendingSignals > 0) {
1148 // Some more signal emissions to this slot are pending.
1149 // Instead of writing the downstream data, we wait
1150 // and do it in the next call we get
1151 // (signal comppression)
1152 return;
1153 }
1154
1155 if (isHttpRedirectResponse())
1156 return;
1157
1158 // This can occur when downloading compressed data as some of the data may be the content
1159 // encoding's header. Don't emit anything for this.
1160 if (lastReadyReadEmittedSize == bytesDownloaded) {
1161 if (readBufferMaxSize)
1162 emit q->readBufferFreed(size: dataSize);
1163 return;
1164 }
1165 lastReadyReadEmittedSize = bytesDownloaded;
1166
1167 const auto totalSizeOpt = QNetworkHeadersPrivate::toInt(
1168 value: headers().value(name: QHttpHeaders::WellKnownHeader::ContentLength));
1169
1170 emit q->readyRead();
1171 // emit readyRead before downloadProgress in case this will cause events to be
1172 // processed and we get into a recursive call (as in QProgressDialog).
1173 if (downloadProgressSignalChoke.isValid() &&
1174 downloadProgressSignalChoke.elapsed() >= progressSignalInterval
1175 && (!decompressHelper.isValid() || decompressHelper.isCountingBytes())) {
1176 downloadProgressSignalChoke.start();
1177 emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: totalSizeOpt.value_or(u: -1));
1178 }
1179}
1180
1181void QNetworkReplyHttpImplPrivate::replyFinished()
1182{
1183 // We are already loading from cache, we still however
1184 // got this signal because it was posted already
1185 if (loadingFromCache)
1186 return;
1187
1188 finished();
1189}
1190
1191QNetworkAccessManager::Operation QNetworkReplyHttpImplPrivate::getRedirectOperation(QNetworkAccessManager::Operation currentOp, int httpStatus)
1192{
1193 // HTTP status code can be used to decide if we can redirect with a GET
1194 // operation or not. See http://www.ietf.org/rfc/rfc2616.txt [Sec 10.3] for
1195 // more details
1196
1197 // We MUST keep using the verb that was used originally when being redirected with 307 or 308.
1198 if (httpStatus == 307 || httpStatus == 308)
1199 return currentOp;
1200
1201 switch (currentOp) {
1202 case QNetworkAccessManager::HeadOperation:
1203 return QNetworkAccessManager::HeadOperation;
1204 default:
1205 break;
1206 }
1207 // Use GET for everything else.
1208 return QNetworkAccessManager::GetOperation;
1209}
1210
1211bool QNetworkReplyHttpImplPrivate::isHttpRedirectResponse() const
1212{
1213 return httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(statusCode);
1214}
1215
1216QNetworkRequest QNetworkReplyHttpImplPrivate::createRedirectRequest(const QNetworkRequest &originalRequest,
1217 const QUrl &url,
1218 int maxRedirectsRemaining)
1219{
1220 QNetworkRequest newRequest(originalRequest);
1221 newRequest.setUrl(url);
1222 newRequest.setMaximumRedirectsAllowed(maxRedirectsRemaining);
1223
1224 return newRequest;
1225}
1226
1227void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int httpStatus, int maxRedirectsRemaining)
1228{
1229 Q_Q(QNetworkReplyHttpImpl);
1230 Q_ASSERT(manager);
1231 Q_ASSERT(managerPrivate);
1232
1233 if (isFinished)
1234 return;
1235
1236 const QString schemeBefore(url.scheme());
1237 if (httpRequest.isFollowRedirects()) // update the reply's url as it could've changed
1238 url = redirectUrl;
1239
1240 const bool wasLocalSocket = schemeBefore.startsWith(s: "unix"_L1);
1241 if (!wasLocalSocket && managerPrivate->stsEnabled && managerPrivate->stsCache.isKnownHost(url)) {
1242 // RFC6797, 8.3:
1243 // The UA MUST replace the URI scheme with "https" [RFC2818],
1244 // and if the URI contains an explicit port component of "80",
1245 // then the UA MUST convert the port component to be "443", or
1246 // if the URI contains an explicit port component that is not
1247 // equal to "80", the port component value MUST be preserved;
1248 // otherwise, if the URI does not contain an explicit port
1249 // component, the UA MUST NOT add one.
1250 url.setScheme("https"_L1);
1251 if (url.port() == 80)
1252 url.setPort(443);
1253 }
1254
1255 // Just to be on the safe side for local sockets, any changes to the scheme
1256 // are considered less safe
1257 const bool changingLocalScheme = wasLocalSocket && url.scheme() != schemeBefore;
1258 const bool isLessSafe = changingLocalScheme
1259 || (schemeBefore == "https"_L1 && url.scheme() == "http"_L1);
1260 if (httpRequest.redirectPolicy() == QNetworkRequest::NoLessSafeRedirectPolicy && isLessSafe) {
1261 error(code: QNetworkReply::InsecureRedirectError,
1262 errorString: QCoreApplication::translate(context: "QHttp", key: "Insecure redirect"));
1263 return;
1264 }
1265
1266 // If the original operation was a GET with a body and the status code is
1267 // 308 then keep the message body
1268 const bool getOperationKeepsBody = (operation == QNetworkAccessManager::GetOperation)
1269 && httpStatus == 308;
1270
1271 redirectRequest = createRedirectRequest(originalRequest, url, maxRedirectsRemaining);
1272 operation = getRedirectOperation(currentOp: operation, httpStatus);
1273
1274 // Clear stale headers, the relevant ones get set again later
1275 httpRequest.clearHeaders();
1276 auto newHeaders = redirectRequest.headers();
1277 if ((operation == QNetworkAccessManager::GetOperation
1278 || operation == QNetworkAccessManager::HeadOperation) && !getOperationKeepsBody) {
1279 // possibly changed from not-GET/HEAD to GET/HEAD, make sure to get rid of upload device
1280 uploadByteDevice.reset();
1281 uploadByteDevicePosition = 0;
1282 if (outgoingData) {
1283 QObject::disconnect(sender: outgoingData, SIGNAL(readyRead()), receiver: q,
1284 SLOT(_q_bufferOutgoingData()));
1285 QObject::disconnect(sender: outgoingData, SIGNAL(readChannelFinished()), receiver: q,
1286 SLOT(_q_bufferOutgoingDataFinished()));
1287 }
1288 outgoingData = nullptr;
1289 outgoingDataBuffer.reset();
1290 // We need to explicitly unset these headers so they're not reapplied to the httpRequest
1291 newHeaders.removeAll(name: QHttpHeaders::WellKnownHeader::ContentLength);
1292 newHeaders.removeAll(name: QHttpHeaders::WellKnownHeader::ContentType);
1293 }
1294
1295 if (const QNetworkCookieJar *const cookieJar = manager->cookieJar()) {
1296 auto cookies = cookieJar->cookiesForUrl(url);
1297 if (!cookies.empty()) {
1298 auto cookieHeader = QNetworkHeadersPrivate::fromCookieList(cookies);
1299 newHeaders.replaceOrAppend(name: QHttpHeaders::WellKnownHeader::Cookie, newValue: cookieHeader);
1300 }
1301 }
1302
1303 redirectRequest.setHeaders(std::move(newHeaders));
1304
1305 if (httpRequest.redirectPolicy() != QNetworkRequest::UserVerifiedRedirectPolicy)
1306 followRedirect();
1307
1308 emit q->redirected(url);
1309}
1310
1311void QNetworkReplyHttpImplPrivate::followRedirect()
1312{
1313 Q_Q(QNetworkReplyHttpImpl);
1314 Q_ASSERT(managerPrivate);
1315
1316 decompressHelper.clear();
1317 clearHeaders();
1318
1319 if (managerPrivate->thread)
1320 managerPrivate->thread->disconnect();
1321
1322 QMetaObject::invokeMethod(
1323 object: q, function: [this]() { postRequest(newHttpRequest: redirectRequest); }, type: Qt::QueuedConnection);
1324}
1325
1326static constexpr QLatin1StringView locationHeader() noexcept { return "location"_L1; }
1327
1328void QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode)
1329{
1330 Q_Q(QNetworkReplyHttpImpl);
1331 switch (statusCode) {
1332 case 301: // Moved Permanently
1333 case 302: // Found
1334 case 303: // See Other
1335 case 307: // Temporary Redirect
1336 case 308: // Permanent Redirect
1337 // What do we do about the caching of the HTML note?
1338 // The response to a 303 MUST NOT be cached, while the response to
1339 // all of the others is cacheable if the headers indicate it to be
1340 QByteArrayView header = q->headers().value(name: locationHeader());
1341 QUrl url = QUrl(QString::fromUtf8(utf8: header));
1342 if (!url.isValid())
1343 url = QUrl(QLatin1StringView(header));
1344 q->setAttribute(code: QNetworkRequest::RedirectionTargetAttribute, value: url);
1345 }
1346}
1347
1348void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QHttpHeaders &hm,
1349 int sc, const QString &rp, bool pu,
1350 QSharedPointer<char> db,
1351 qint64 contentLength,
1352 qint64 removedContentLength,
1353 bool h2Used, bool isCompressed)
1354{
1355 Q_Q(QNetworkReplyHttpImpl);
1356 Q_UNUSED(contentLength);
1357
1358 statusCode = sc;
1359 reasonPhrase = rp;
1360
1361#ifndef QT_NO_SSL
1362 // We parse this header only if we're using secure transport:
1363 //
1364 // RFC6797, 8.1
1365 // If an HTTP response is received over insecure transport, the UA MUST
1366 // ignore any present STS header field(s).
1367 if (url.scheme() == "https"_L1 && managerPrivate->stsEnabled)
1368 managerPrivate->stsCache.updateFromHeaders(headers: hm, url);
1369#endif
1370 // Download buffer
1371 if (!db.isNull()) {
1372 downloadBufferPointer = db;
1373 downloadZerocopyBuffer = downloadBufferPointer.data();
1374 downloadBufferCurrentSize = 0;
1375 q->setAttribute(code: QNetworkRequest::DownloadBufferAttribute, value: QVariant::fromValue<QSharedPointer<char> > (value: downloadBufferPointer));
1376 }
1377
1378 q->setAttribute(code: QNetworkRequest::HttpPipeliningWasUsedAttribute, value: pu);
1379 q->setAttribute(code: QNetworkRequest::Http2WasUsedAttribute, value: h2Used);
1380
1381 // A user having manually defined which encodings they accept is, for
1382 // somwehat unknown (presumed legacy compatibility) reasons treated as
1383 // disabling our decompression:
1384 const bool autoDecompress = !request.headers().contains(name: QHttpHeaders::WellKnownHeader::AcceptEncoding);
1385 const bool shouldDecompress = isCompressed && autoDecompress;
1386 // reconstruct the HTTP header
1387 auto h = q->headers();
1388 for (qsizetype i = 0; i < hm.size(); ++i) {
1389 const auto key = hm.nameAt(i);
1390 const auto originValue = hm.valueAt(i);
1391
1392 // Reset any previous "location" header set in the reply. In case of
1393 // redirects, we don't want to 'append' multiple location header values,
1394 // rather we keep only the latest one
1395 if (key.compare(other: locationHeader(), cs: Qt::CaseInsensitive) == 0)
1396 h.removeAll(name: key);
1397
1398 if (shouldDecompress && !decompressHelper.isValid() && key == "content-encoding"_L1) {
1399 if (!synchronous) // with synchronous all the data is expected to be handled at once
1400 decompressHelper.setCountingBytesEnabled(true);
1401
1402 if (!decompressHelper.setEncoding(originValue)) {
1403 error(code: QNetworkReplyImpl::NetworkError::UnknownContentError,
1404 errorString: QCoreApplication::translate(context: "QHttp", key: "Failed to initialize decompression: %1")
1405 .arg(a: decompressHelper.errorString()));
1406 return;
1407 }
1408 decompressHelper.setDecompressedSafetyCheckThreshold(
1409 request.decompressedSafetyCheckThreshold());
1410 }
1411
1412 h.append(name: key, value: originValue);
1413 }
1414 q->setHeaders(std::move(h));
1415
1416 q->setAttribute(code: QNetworkRequest::HttpStatusCodeAttribute, value: statusCode);
1417 q->setAttribute(code: QNetworkRequest::HttpReasonPhraseAttribute, value: reasonPhrase);
1418 if (removedContentLength != -1)
1419 q->setAttribute(code: QNetworkRequest::OriginalContentLengthAttribute, value: removedContentLength);
1420
1421 // is it a redirection?
1422 if (!isHttpRedirectResponse())
1423 checkForRedirect(statusCode);
1424
1425 if (statusCode >= 500 && statusCode < 600) {
1426 QAbstractNetworkCache *nc = managerPrivate->networkCache;
1427 if (nc) {
1428 QNetworkCacheMetaData metaData = nc->metaData(url: httpRequest.url());
1429 auto value = metaData.headers().value(name: QHttpHeaders::WellKnownHeader::CacheControl);
1430 bool mustReValidate = false;
1431 if (!value.empty()) {
1432 QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(header: value);
1433 if (cacheControl.contains(key: "must-revalidate"_ba))
1434 mustReValidate = true;
1435 }
1436 if (!mustReValidate && sendCacheContents(metaData))
1437 return;
1438 }
1439 }
1440
1441 if (statusCode == 304) {
1442#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
1443 qDebug() << "Received a 304 from" << request.url();
1444#endif
1445 QAbstractNetworkCache *nc = managerPrivate->networkCache;
1446 if (nc) {
1447 QNetworkCacheMetaData oldMetaData = nc->metaData(url: httpRequest.url());
1448 QNetworkCacheMetaData metaData = fetchCacheMetaData(metaData: oldMetaData);
1449 if (oldMetaData != metaData)
1450 nc->updateMetaData(metaData);
1451 if (sendCacheContents(metaData))
1452 return;
1453 }
1454 }
1455
1456
1457 if (statusCode != 304 && statusCode != 303) {
1458 if (!isCachingEnabled())
1459 setCachingEnabled(true);
1460 }
1461
1462 _q_metaDataChanged();
1463}
1464
1465void QNetworkReplyHttpImplPrivate::replyDownloadProgressSlot(qint64 bytesReceived, qint64 bytesTotal)
1466{
1467 Q_Q(QNetworkReplyHttpImpl);
1468
1469 // If we're closed just ignore this data
1470 if (!q->isOpen())
1471 return;
1472
1473 // we can be sure here that there is a download buffer
1474
1475 int pendingSignals = (int)pendingDownloadProgressEmissions->fetchAndAddAcquire(valueToAdd: -1) - 1;
1476 if (pendingSignals > 0) {
1477 // Let's ignore this signal and look at the next one coming in
1478 // (signal comppression)
1479 return;
1480 }
1481
1482 if (!q->isOpen())
1483 return;
1484
1485 if (cacheEnabled && isCachingAllowed() && bytesReceived == bytesTotal) {
1486 // Write everything in one go if we use a download buffer. might be more performant.
1487 initCacheSaveDevice();
1488 // need to check again if cache enabled and device exists
1489 if (cacheSaveDevice && cacheEnabled)
1490 cacheSaveDevice->write(data: downloadZerocopyBuffer, len: bytesTotal);
1491 // FIXME where is it closed?
1492 }
1493
1494 if (isHttpRedirectResponse())
1495 return;
1496
1497 bytesDownloaded = bytesReceived;
1498 setupTransferTimeout();
1499
1500 downloadBufferCurrentSize = bytesReceived;
1501
1502 // Only emit readyRead when actual data is there
1503 // emit readyRead before downloadProgress in case this will cause events to be
1504 // processed and we get into a recursive call (as in QProgressDialog).
1505 if (bytesDownloaded > 0)
1506 emit q->readyRead();
1507 if (downloadProgressSignalChoke.isValid() &&
1508 downloadProgressSignalChoke.elapsed() >= progressSignalInterval) {
1509 downloadProgressSignalChoke.start();
1510 emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal);
1511 }
1512}
1513
1514void QNetworkReplyHttpImplPrivate::httpAuthenticationRequired(const QHttpNetworkRequest &request,
1515 QAuthenticator *auth)
1516{
1517 managerPrivate->authenticationRequired(authenticator: auth, reply: q_func(), synchronous, url, urlForLastAuthentication: &urlForLastAuthentication, allowAuthenticationReuse: request.withCredentials());
1518}
1519
1520#ifndef QT_NO_NETWORKPROXY
1521void QNetworkReplyHttpImplPrivate::proxyAuthenticationRequired(const QNetworkProxy &proxy,
1522 QAuthenticator *authenticator)
1523{
1524 managerPrivate->proxyAuthenticationRequired(url: request.url(), proxy, synchronous, authenticator, lastProxyAuthentication: &lastProxyAuthentication);
1525}
1526#endif
1527
1528void QNetworkReplyHttpImplPrivate::httpError(QNetworkReply::NetworkError errorCode,
1529 const QString &errorString)
1530{
1531#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
1532 qDebug() << "http error!" << errorCode << errorString;
1533#endif
1534
1535 // FIXME?
1536 error(code: errorCode, errorString);
1537}
1538
1539#ifndef QT_NO_SSL
1540void QNetworkReplyHttpImplPrivate::replyEncrypted()
1541{
1542 Q_Q(QNetworkReplyHttpImpl);
1543 emit q->encrypted();
1544}
1545
1546void QNetworkReplyHttpImplPrivate::replySslErrors(
1547 const QList<QSslError> &list, bool *ignoreAll, QList<QSslError> *toBeIgnored)
1548{
1549 Q_Q(QNetworkReplyHttpImpl);
1550 emit q->sslErrors(errors: list);
1551 // Check if the callback set any ignore and return this here to http thread
1552 if (pendingIgnoreAllSslErrors)
1553 *ignoreAll = true;
1554 if (!pendingIgnoreSslErrorsList.isEmpty())
1555 *toBeIgnored = pendingIgnoreSslErrorsList;
1556}
1557
1558void QNetworkReplyHttpImplPrivate::replySslConfigurationChanged(const QSslConfiguration &newSslConfiguration)
1559{
1560 // Receiving the used SSL configuration from the HTTP thread
1561 if (sslConfiguration.data())
1562 *sslConfiguration = newSslConfiguration;
1563 else
1564 sslConfiguration.reset(other: new QSslConfiguration(newSslConfiguration));
1565}
1566
1567void QNetworkReplyHttpImplPrivate::replyPreSharedKeyAuthenticationRequiredSlot(QSslPreSharedKeyAuthenticator *authenticator)
1568{
1569 Q_Q(QNetworkReplyHttpImpl);
1570 emit q->preSharedKeyAuthenticationRequired(authenticator);
1571}
1572#endif
1573
1574// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
1575void QNetworkReplyHttpImplPrivate::resetUploadDataSlot(bool *r)
1576{
1577 *r = uploadByteDevice->reset();
1578 if (*r) {
1579 // reset our own position which is used for the inter-thread communication
1580 uploadByteDevicePosition = 0;
1581 }
1582}
1583
1584// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
1585void QNetworkReplyHttpImplPrivate::sentUploadDataSlot(qint64 pos, qint64 amount)
1586{
1587 if (!uploadByteDevice) // uploadByteDevice is no longer available
1588 return;
1589
1590 if (uploadByteDevicePosition + amount != pos) {
1591 // Sanity check, should not happen.
1592 error(code: QNetworkReply::UnknownNetworkError, errorString: QString());
1593 return;
1594 }
1595 uploadByteDevice->advanceReadPointer(amount);
1596 uploadByteDevicePosition += amount;
1597}
1598
1599// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
1600void QNetworkReplyHttpImplPrivate::wantUploadDataSlot(qint64 maxSize)
1601{
1602 Q_Q(QNetworkReplyHttpImpl);
1603
1604 if (!uploadByteDevice) // uploadByteDevice is no longer available
1605 return;
1606
1607 // call readPointer
1608 qint64 currentUploadDataLength = 0;
1609 char *data = const_cast<char*>(uploadByteDevice->readPointer(maximumLength: maxSize, len&: currentUploadDataLength));
1610
1611 if (currentUploadDataLength == 0) {
1612 uploadDeviceChoking = true;
1613 // No bytes from upload byte device. There will be bytes later, it will emit readyRead()
1614 // and our uploadByteDeviceReadyReadSlot() is called.
1615 return;
1616 } else {
1617 uploadDeviceChoking = false;
1618 }
1619
1620 // Let's make a copy of this data
1621 QByteArray dataArray(data, currentUploadDataLength);
1622
1623 // Communicate back to HTTP thread
1624 emit q->haveUploadData(pos: uploadByteDevicePosition, dataArray, dataAtEnd: uploadByteDevice->atEnd(), dataSize: uploadByteDevice->size());
1625}
1626
1627void QNetworkReplyHttpImplPrivate::uploadByteDeviceReadyReadSlot()
1628{
1629 // Start the flow between this thread and the HTTP thread again by triggering a upload.
1630 // However only do this when we were choking before, else the state in
1631 // QNonContiguousByteDeviceThreadForwardImpl gets messed up.
1632 if (uploadDeviceChoking) {
1633 uploadDeviceChoking = false;
1634 wantUploadDataSlot(maxSize: 1024);
1635 }
1636}
1637
1638
1639/*
1640 A simple web page that can be used to test us: http://www.procata.com/cachetest/
1641 */
1642bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData &metaData)
1643{
1644 Q_Q(QNetworkReplyHttpImpl);
1645
1646 setCachingEnabled(false);
1647 if (!metaData.isValid())
1648 return false;
1649
1650 QAbstractNetworkCache *nc = managerPrivate->networkCache;
1651 Q_ASSERT(nc);
1652 QIODevice *contents = nc->data(url);
1653 if (!contents) {
1654#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
1655 qDebug() << "Cannot send cache, the contents are 0" << url;
1656#endif
1657 return false;
1658 }
1659 contents->setParent(q);
1660
1661 QNetworkCacheMetaData::AttributesMap attributes = metaData.attributes();
1662 int status = attributes.value(key: QNetworkRequest::HttpStatusCodeAttribute).toInt();
1663 if (status < 100)
1664 status = 200; // fake it
1665
1666 statusCode = status;
1667
1668 q->setAttribute(code: QNetworkRequest::HttpStatusCodeAttribute, value: status);
1669 q->setAttribute(code: QNetworkRequest::HttpReasonPhraseAttribute, value: attributes.value(key: QNetworkRequest::HttpReasonPhraseAttribute));
1670 q->setAttribute(code: QNetworkRequest::SourceIsFromCacheAttribute, value: true);
1671
1672 QHttpHeaders cachedHeaders = metaData.headers();
1673 QHttpHeaders h = headers();
1674 QUrl redirectUrl;
1675 for (qsizetype i = 0; i < cachedHeaders.size(); ++i) {
1676 const auto name = cachedHeaders.nameAt(i);
1677 const auto value = cachedHeaders.valueAt(i);
1678
1679 if (httpRequest.isFollowRedirects()
1680 && !name.compare(other: locationHeader(), cs: Qt::CaseInsensitive)) {
1681 redirectUrl = QUrl::fromEncoded(input: value);
1682 }
1683
1684 h.replaceOrAppend(name, newValue: value);
1685 }
1686 setHeaders(std::move(h));
1687
1688 if (!isHttpRedirectResponse())
1689 checkForRedirect(statusCode: status);
1690
1691 cacheLoadDevice = contents;
1692 q->connect(asender: cacheLoadDevice, SIGNAL(readyRead()), SLOT(_q_cacheLoadReadyRead()));
1693 q->connect(asender: cacheLoadDevice, SIGNAL(readChannelFinished()), SLOT(_q_cacheLoadReadyRead()));
1694
1695 // This needs to be emitted in the event loop because it can be reached at
1696 // the direct code path of qnam.get(...) before the user has a chance
1697 // to connect any signals.
1698 QMetaObject::invokeMethod(obj: q, member: "_q_metaDataChanged", c: Qt::QueuedConnection);
1699 QMetaObject::invokeMethod(obj: q, member: "_q_cacheLoadReadyRead", c: Qt::QueuedConnection);
1700
1701
1702#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
1703 qDebug() << "Successfully sent cache:" << url << contents->size() << "bytes";
1704#endif
1705
1706 // Do redirect processing
1707 if (httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(statusCode: status)) {
1708 QMetaObject::invokeMethod(obj: q, member: "onRedirected", c: Qt::QueuedConnection,
1709 Q_ARG(QUrl, redirectUrl),
1710 Q_ARG(int, status),
1711 Q_ARG(int, httpRequest.redirectCount() - 1));
1712 }
1713
1714 // Set the following flag so we can ignore some signals from HTTP thread
1715 // that would still come
1716 loadingFromCache = true;
1717 return true;
1718}
1719
1720static auto caseInsensitiveCompare(QByteArrayView value)
1721{
1722 return [value](QByteArrayView element)
1723 {
1724 return value.compare(a: element, cs: Qt::CaseInsensitive) == 0;
1725 };
1726}
1727
1728static bool isHopByHop(QByteArrayView header)
1729{
1730 constexpr QByteArrayView headers[] = { "connection",
1731 "keep-alive",
1732 "proxy-authenticate",
1733 "proxy-authorization",
1734 "te",
1735 "trailers",
1736 "transfer-encoding",
1737 "upgrade"};
1738 return std::any_of(first: std::begin(arr: headers), last: std::end(arr: headers), pred: caseInsensitiveCompare(value: header));
1739}
1740
1741QNetworkCacheMetaData QNetworkReplyHttpImplPrivate::fetchCacheMetaData(const QNetworkCacheMetaData &oldMetaData) const
1742{
1743 Q_Q(const QNetworkReplyHttpImpl);
1744
1745 QNetworkCacheMetaData metaData = oldMetaData;
1746 QHttpHeaders cacheHeaders = metaData.headers();
1747
1748 const auto newHeaders = q->headers();
1749 for (qsizetype i = 0; i < newHeaders.size(); ++i) {
1750 const auto name = newHeaders.nameAt(i);
1751 const auto value = newHeaders.valueAt(i);
1752
1753 if (isHopByHop(header: name))
1754 continue;
1755
1756 if (name.compare(other: "set-cookie", cs: Qt::CaseInsensitive) == 0)
1757 continue;
1758
1759 // for 4.6.0, we were planning to not store the date header in the
1760 // cached resource; through that we planned to reduce the number
1761 // of writes to disk when using a QNetworkDiskCache (i.e. don't
1762 // write to disk when only the date changes).
1763 // However, without the date we cannot calculate the age of the page
1764 // anymore.
1765 //if (header == "date")
1766 //continue;
1767
1768 // Don't store Warning 1xx headers
1769 if (name.compare(other: "warning", cs: Qt::CaseInsensitive) == 0) {
1770 if (value.size() == 3
1771 && value[0] == '1'
1772 && isAsciiDigit(c: value[1])
1773 && isAsciiDigit(c: value[2]))
1774 continue;
1775 }
1776
1777 if (cacheHeaders.contains(name)) {
1778 // Match the behavior of Firefox and assume Cache-Control: "no-transform"
1779 constexpr QByteArrayView headers[]=
1780 {"content-encoding", "content-range", "content-type"};
1781 if (std::any_of(first: std::begin(arr: headers), last: std::end(arr: headers), pred: caseInsensitiveCompare(value: name)))
1782 continue;
1783 }
1784
1785 // IIS has been known to send "Content-Length: 0" on 304 responses, so
1786 // ignore this too
1787 if (statusCode == 304 && name.compare(other: "content-length", cs: Qt::CaseInsensitive) == 0)
1788 continue;
1789
1790#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
1791 QByteArrayView n = newHeaders.value(name);
1792 QByteArrayView o = cacheHeaders.value(name);
1793 if (n != o && name.compare("date", Qt::CaseInsensitive) != 0) {
1794 qDebug() << "replacing" << name;
1795 qDebug() << "new" << n;
1796 qDebug() << "old" << o;
1797 }
1798#endif
1799 cacheHeaders.replaceOrAppend(name, newValue: value);
1800 }
1801 metaData.setHeaders(cacheHeaders);
1802
1803 bool checkExpired = true;
1804
1805 QHash<QByteArray, QByteArray> cacheControl;
1806 auto value = cacheHeaders.value(name: QHttpHeaders::WellKnownHeader::CacheControl);
1807 if (!value.empty()) {
1808 cacheControl = parseHttpOptionHeader(header: value);
1809 QByteArray maxAge = cacheControl.value(key: "max-age"_ba);
1810 if (!maxAge.isEmpty()) {
1811 checkExpired = false;
1812 QDateTime dt = QDateTime::currentDateTimeUtc();
1813 dt = dt.addSecs(secs: maxAge.toInt());
1814 metaData.setExpirationDate(dt);
1815 }
1816 }
1817 if (checkExpired) {
1818 if (const auto value = cacheHeaders.value(
1819 name: QHttpHeaders::WellKnownHeader::Expires); !value.isEmpty()) {
1820 QDateTime expiredDateTime = QNetworkHeadersPrivate::fromHttpDate(value);
1821 metaData.setExpirationDate(expiredDateTime);
1822 }
1823 }
1824
1825 if (const auto value = cacheHeaders.value(
1826 name: QHttpHeaders::WellKnownHeader::LastModified); !value.isEmpty()) {
1827 metaData.setLastModified(QNetworkHeadersPrivate::fromHttpDate(value));
1828 }
1829
1830
1831 bool canDiskCache;
1832 // only cache GET replies by default, all other replies (POST, PUT, DELETE)
1833 // are not cacheable by default (according to RFC 2616 section 9)
1834 if (httpRequest.operation() == QHttpNetworkRequest::Get) {
1835
1836 canDiskCache = true;
1837 // HTTP/1.1. Check the Cache-Control header
1838 if (cacheControl.contains(key: "no-store"_ba))
1839 canDiskCache = false;
1840
1841 // responses to POST might be cacheable
1842 } else if (httpRequest.operation() == QHttpNetworkRequest::Post) {
1843
1844 canDiskCache = false;
1845 // some pages contain "expires:" and "cache-control: no-cache" field,
1846 // so we only might cache POST requests if we get "cache-control: max-age ..."
1847 if (cacheControl.contains(key: "max-age"_ba))
1848 canDiskCache = true;
1849
1850 // responses to PUT and DELETE are not cacheable
1851 } else {
1852 canDiskCache = false;
1853 }
1854
1855 metaData.setSaveToDisk(canDiskCache);
1856 QNetworkCacheMetaData::AttributesMap attributes;
1857 if (statusCode != 304) {
1858 // update the status code
1859 attributes.insert(key: QNetworkRequest::HttpStatusCodeAttribute, value: statusCode);
1860 attributes.insert(key: QNetworkRequest::HttpReasonPhraseAttribute, value: reasonPhrase);
1861 } else {
1862 // this is a redirection, keep the attributes intact
1863 attributes = oldMetaData.attributes();
1864 }
1865 metaData.setAttributes(attributes);
1866 return metaData;
1867}
1868
1869bool QNetworkReplyHttpImplPrivate::canResume() const
1870{
1871 Q_Q(const QNetworkReplyHttpImpl);
1872
1873 // Only GET operation supports resuming.
1874 if (operation != QNetworkAccessManager::GetOperation)
1875 return false;
1876
1877 const auto h = q->headers();
1878
1879 // Can only resume if server/resource supports Range header.
1880 const auto acceptRanges = h.value(name: QHttpHeaders::WellKnownHeader::AcceptRanges);
1881 if (acceptRanges.empty() || acceptRanges == "none")
1882 return false;
1883
1884 // We only support resuming for byte ranges.
1885 const auto range = h.value(name: QHttpHeaders::WellKnownHeader::Range);
1886 if (!range.empty()) {
1887 if (!range.startsWith(other: bytesEqualPrefix()))
1888 return false;
1889 }
1890
1891 // If we're using a download buffer then we don't support resuming/migration
1892 // right now. Too much trouble.
1893 if (downloadZerocopyBuffer)
1894 return false;
1895
1896 return true;
1897}
1898
1899void QNetworkReplyHttpImplPrivate::setResumeOffset(quint64 offset)
1900{
1901 resumeOffset = offset;
1902}
1903
1904void QNetworkReplyHttpImplPrivate::_q_startOperation()
1905{
1906 // Ensure this function is only being called once, and not at all if we were
1907 // cancelled
1908 if (state >= Working)
1909 return;
1910
1911 state = Working;
1912
1913 postRequest(newHttpRequest: request);
1914
1915 setupTransferTimeout();
1916 if (synchronous) {
1917 state = Finished;
1918 q_func()->setFinished(true);
1919 }
1920}
1921
1922void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead()
1923{
1924 Q_Q(QNetworkReplyHttpImpl);
1925
1926 if (state != Working)
1927 return;
1928 if (!cacheLoadDevice || !q->isOpen() || !cacheLoadDevice->bytesAvailable())
1929 return;
1930
1931 // FIXME Optimize to use zerocopy download buffer if it is a QBuffer.
1932 // Needs to be done where sendCacheContents() (?) of HTTP is emitting
1933 // metaDataChanged ?
1934
1935 const auto totalSizeOpt = QNetworkHeadersPrivate::toInt(
1936 value: headers().value(name: QHttpHeaders::WellKnownHeader::ContentLength));
1937
1938 // emit readyRead before downloadProgress in case this will cause events to be
1939 // processed and we get into a recursive call (as in QProgressDialog).
1940
1941 if (!(isHttpRedirectResponse())) {
1942 // This readyRead() goes to the user. The user then may or may not read() anything.
1943 emit q->readyRead();
1944
1945 if (downloadProgressSignalChoke.isValid() &&
1946 downloadProgressSignalChoke.elapsed() >= progressSignalInterval) {
1947 downloadProgressSignalChoke.start();
1948 emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: totalSizeOpt.value_or(u: -1));
1949 }
1950 }
1951
1952 // A signal we've emitted might be handled by a slot that aborts,
1953 // so we need to check for that and bail out if it's happened:
1954 if (!q->isOpen())
1955 return;
1956
1957 // If there are still bytes available in the cacheLoadDevice then the user did not read
1958 // in response to the readyRead() signal. This means we have to load from the cacheLoadDevice
1959 // and buffer that stuff. This is needed to be able to properly emit finished() later.
1960 while (cacheLoadDevice->bytesAvailable() && !isHttpRedirectResponse())
1961 buffer.append(qba: cacheLoadDevice->readAll());
1962
1963 if (cacheLoadDevice->isSequential()) {
1964 // check if end and we can read the EOF -1
1965 char c;
1966 qint64 actualCount = cacheLoadDevice->read(data: &c, maxlen: 1);
1967 if (actualCount < 0) {
1968 cacheLoadDevice->deleteLater();
1969 cacheLoadDevice = nullptr;
1970 QMetaObject::invokeMethod(obj: q, member: "_q_finished", c: Qt::QueuedConnection);
1971 } else if (actualCount == 1) {
1972 // This is most probably not happening since most QIODevice returned something proper for bytesAvailable()
1973 // and had already been "emptied".
1974 cacheLoadDevice->ungetChar(c);
1975 }
1976 } else if ((!cacheLoadDevice->isSequential() && cacheLoadDevice->atEnd())) {
1977 // This codepath is in case the cache device is a QBuffer, e.g. from QNetworkDiskCache.
1978 cacheLoadDevice->deleteLater();
1979 cacheLoadDevice = nullptr;
1980 QMetaObject::invokeMethod(obj: q, member: "_q_finished", c: Qt::QueuedConnection);
1981 }
1982}
1983
1984
1985void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingDataFinished()
1986{
1987 Q_Q(QNetworkReplyHttpImpl);
1988
1989 // make sure this is only called once, ever.
1990 //_q_bufferOutgoingData may call it or the readChannelFinished emission
1991 if (state != Buffering)
1992 return;
1993
1994 // disconnect signals
1995 QObject::disconnect(sender: outgoingData, SIGNAL(readyRead()), receiver: q, SLOT(_q_bufferOutgoingData()));
1996 QObject::disconnect(sender: outgoingData, SIGNAL(readChannelFinished()), receiver: q, SLOT(_q_bufferOutgoingDataFinished()));
1997
1998 // finally, start the request
1999 QMetaObject::invokeMethod(obj: q, member: "_q_startOperation", c: Qt::QueuedConnection);
2000}
2001
2002void QNetworkReplyHttpImplPrivate::_q_cacheSaveDeviceAboutToClose()
2003{
2004 // do not keep a dangling pointer to the device around (device
2005 // is closing because e.g. QAbstractNetworkCache::remove() was called).
2006 cacheSaveDevice = nullptr;
2007}
2008
2009void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingData()
2010{
2011 Q_Q(QNetworkReplyHttpImpl);
2012
2013 if (!outgoingDataBuffer) {
2014 // first call, create our buffer
2015 outgoingDataBuffer = std::make_shared<QRingBuffer>();
2016
2017 QObject::connect(sender: outgoingData, SIGNAL(readyRead()), receiver: q, SLOT(_q_bufferOutgoingData()));
2018 QObject::connect(sender: outgoingData, SIGNAL(readChannelFinished()), receiver: q, SLOT(_q_bufferOutgoingDataFinished()));
2019 }
2020
2021 qint64 bytesBuffered = 0;
2022 qint64 bytesToBuffer = 0;
2023
2024 // read data into our buffer
2025 forever {
2026 bytesToBuffer = outgoingData->bytesAvailable();
2027 // unknown? just try 2 kB, this also ensures we always try to read the EOF
2028 if (bytesToBuffer <= 0)
2029 bytesToBuffer = 2*1024;
2030
2031 char *dst = outgoingDataBuffer->reserve(bytes: bytesToBuffer);
2032 bytesBuffered = outgoingData->read(data: dst, maxlen: bytesToBuffer);
2033
2034 if (bytesBuffered == -1) {
2035 // EOF has been reached.
2036 outgoingDataBuffer->chop(bytes: bytesToBuffer);
2037
2038 _q_bufferOutgoingDataFinished();
2039 break;
2040 } else if (bytesBuffered == 0) {
2041 // nothing read right now, just wait until we get called again
2042 outgoingDataBuffer->chop(bytes: bytesToBuffer);
2043
2044 break;
2045 } else {
2046 // don't break, try to read() again
2047 outgoingDataBuffer->chop(bytes: bytesToBuffer - bytesBuffered);
2048 }
2049 }
2050}
2051
2052void QNetworkReplyHttpImplPrivate::_q_transferTimedOut()
2053{
2054 Q_Q(QNetworkReplyHttpImpl);
2055 q->abort();
2056}
2057
2058void QNetworkReplyHttpImplPrivate::setupTransferTimeout()
2059{
2060 Q_Q(QNetworkReplyHttpImpl);
2061 if (!transferTimeout) {
2062 transferTimeout = new QTimer(q);
2063 QObject::connect(sender: transferTimeout, SIGNAL(timeout()),
2064 receiver: q, SLOT(_q_transferTimedOut()),
2065 Qt::QueuedConnection);
2066 }
2067 transferTimeout->stop();
2068 if (request.transferTimeoutAsDuration() > 0ms) {
2069 transferTimeout->setSingleShot(true);
2070 transferTimeout->setInterval(request.transferTimeoutAsDuration());
2071 QMetaObject::invokeMethod(obj: transferTimeout, member: "start",
2072 c: Qt::QueuedConnection);
2073
2074 }
2075}
2076
2077// need to have this function since the reply is a private member variable
2078// and the special backends need to access this.
2079void QNetworkReplyHttpImplPrivate::emitReplyUploadProgress(qint64 bytesSent, qint64 bytesTotal)
2080{
2081 Q_Q(QNetworkReplyHttpImpl);
2082 if (isFinished)
2083 return;
2084
2085 setupTransferTimeout();
2086
2087 if (!emitAllUploadProgressSignals) {
2088 //choke signal emissions, except the first and last signals which are unconditional
2089 if (uploadProgressSignalChoke.isValid()) {
2090 if (bytesSent != bytesTotal && uploadProgressSignalChoke.elapsed() < progressSignalInterval) {
2091 return;
2092 }
2093 }
2094 uploadProgressSignalChoke.start();
2095 }
2096 emit q->uploadProgress(bytesSent, bytesTotal);
2097}
2098
2099QNonContiguousByteDevice* QNetworkReplyHttpImplPrivate::createUploadByteDevice()
2100{
2101 Q_Q(QNetworkReplyHttpImpl);
2102
2103 if (outgoingDataBuffer)
2104 uploadByteDevice = QNonContiguousByteDeviceFactory::createShared(ringBuffer: outgoingDataBuffer);
2105 else if (outgoingData) {
2106 uploadByteDevice = QNonContiguousByteDeviceFactory::createShared(device: outgoingData);
2107 } else {
2108 return nullptr;
2109 }
2110
2111 // We want signal emissions only for normal asynchronous uploads
2112 if (!synchronous)
2113 QObject::connect(sender: uploadByteDevice.get(), SIGNAL(readProgress(qint64,qint64)),
2114 receiver: q, SLOT(emitReplyUploadProgress(qint64,qint64)));
2115
2116 return uploadByteDevice.get();
2117}
2118
2119void QNetworkReplyHttpImplPrivate::_q_finished()
2120{
2121 // This gets called queued, just forward to real call then
2122 finished();
2123}
2124
2125void QNetworkReplyHttpImplPrivate::finished()
2126{
2127 Q_Q(QNetworkReplyHttpImpl);
2128 if (transferTimeout)
2129 transferTimeout->stop();
2130 if (state == Finished || state == Aborted)
2131 return;
2132
2133 const auto totalSizeOpt = QNetworkHeadersPrivate::toInt(
2134 value: headers().value(name: QHttpHeaders::WellKnownHeader::ContentLength));
2135 const qint64 totalSize = totalSizeOpt.value_or(u: -1);
2136
2137 // if we don't know the total size of or we received everything save the cache.
2138 // If the data is compressed then this is done in readData()
2139 if ((totalSize == -1 || bytesDownloaded == totalSize)
2140 && !decompressHelper.isValid()) {
2141 completeCacheSave();
2142 }
2143
2144 // We check for errorCode too as in case of SSL handshake failure, we still
2145 // get the HTTP redirect status code (301, 303 etc)
2146 if (isHttpRedirectResponse() && errorCode == QNetworkReply::NoError)
2147 return;
2148
2149 state = Finished;
2150 q->setFinished(true);
2151
2152 if (totalSize == -1) {
2153 emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: bytesDownloaded);
2154 } else {
2155 emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: totalSize);
2156 }
2157
2158 if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer))
2159 emit q->uploadProgress(bytesSent: 0, bytesTotal: 0);
2160
2161 emit q->readChannelFinished();
2162 emit q->finished();
2163}
2164
2165void QNetworkReplyHttpImplPrivate::_q_error(QNetworkReplyImpl::NetworkError code, const QString &errorMessage)
2166{
2167 this->error(code, errorString: errorMessage);
2168}
2169
2170
2171void QNetworkReplyHttpImplPrivate::error(QNetworkReplyImpl::NetworkError code, const QString &errorMessage)
2172{
2173 Q_Q(QNetworkReplyHttpImpl);
2174 // Can't set and emit multiple errors.
2175 if (errorCode != QNetworkReply::NoError) {
2176 // But somewhat unavoidable if we have cancelled the request:
2177 if (errorCode != QNetworkReply::OperationCanceledError)
2178 qWarning(msg: "QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.");
2179 return;
2180 }
2181
2182 errorCode = code;
2183 q->setErrorString(errorMessage);
2184
2185 // note: might not be a good idea, since users could decide to delete us
2186 // which would delete the backend too...
2187 // maybe we should protect the backend
2188 emit q->errorOccurred(code);
2189}
2190
2191void QNetworkReplyHttpImplPrivate::_q_metaDataChanged()
2192{
2193 // FIXME merge this with replyDownloadMetaData(); ?
2194
2195 Q_Q(QNetworkReplyHttpImpl);
2196 // 1. do we have cookies?
2197 // 2. are we allowed to set them?
2198 Q_ASSERT(manager);
2199
2200 const auto cookiesOpt = QNetworkHeadersPrivate::toSetCookieList(
2201 values: headers().values(name: QHttpHeaders::WellKnownHeader::SetCookie));
2202 const auto cookies = cookiesOpt.value_or(u: QList<QNetworkCookie>());
2203 if (!cookies.empty()
2204 && request.attribute(code: QNetworkRequest::CookieSaveControlAttribute,
2205 defaultValue: QNetworkRequest::Automatic).toInt() == QNetworkRequest::Automatic) {
2206 QNetworkCookieJar *jar = manager->cookieJar();
2207 if (jar) {
2208 jar->setCookiesFromUrl(cookieList: cookies, url);
2209 }
2210 }
2211 emit q->metaDataChanged();
2212}
2213
2214void QNetworkReplyHttpImplPrivate::createCache()
2215{
2216 // check if we can save and if we're allowed to
2217 if (!managerPrivate->networkCache
2218 || !request.attribute(code: QNetworkRequest::CacheSaveControlAttribute, defaultValue: true).toBool())
2219 return;
2220 cacheEnabled = true;
2221}
2222
2223bool QNetworkReplyHttpImplPrivate::isCachingEnabled() const
2224{
2225 return (cacheEnabled && managerPrivate->networkCache != nullptr);
2226}
2227
2228void QNetworkReplyHttpImplPrivate::setCachingEnabled(bool enable)
2229{
2230 if (!enable && !cacheEnabled)
2231 return; // nothing to do
2232 if (enable && cacheEnabled)
2233 return; // nothing to do either!
2234
2235 if (enable) {
2236 if (Q_UNLIKELY(bytesDownloaded)) {
2237 qDebug() << "setCachingEnabled: " << bytesDownloaded << " bytesDownloaded";
2238 // refuse to enable in this case
2239 qCritical(msg: "QNetworkReplyImpl: backend error: caching was enabled after some bytes had been written");
2240 return;
2241 }
2242
2243 createCache();
2244 } else {
2245 // someone told us to turn on, then back off?
2246 // ok... but you should make up your mind
2247 qDebug(msg: "QNetworkReplyImpl: setCachingEnabled(true) called after setCachingEnabled(false)");
2248 managerPrivate->networkCache->remove(url);
2249 cacheSaveDevice = nullptr;
2250 cacheEnabled = false;
2251 }
2252}
2253
2254bool QNetworkReplyHttpImplPrivate::isCachingAllowed() const
2255{
2256 return operation == QNetworkAccessManager::GetOperation || operation == QNetworkAccessManager::HeadOperation;
2257}
2258
2259void QNetworkReplyHttpImplPrivate::completeCacheSave()
2260{
2261 if (cacheEnabled && errorCode != QNetworkReplyImpl::NoError) {
2262 managerPrivate->networkCache->remove(url);
2263 } else if (cacheEnabled && cacheSaveDevice) {
2264 managerPrivate->networkCache->insert(device: cacheSaveDevice);
2265 }
2266 cacheSaveDevice = nullptr;
2267 cacheEnabled = false;
2268}
2269
2270QT_END_NAMESPACE
2271
2272#include "moc_qnetworkreplyhttpimpl_p.cpp"
2273

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