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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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