Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhttp2protocolhandler.cpp
Go to the documentation of this file.
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#include "access/http2/http2protocol_p.h"
6#include "access/qhttp2connection_p.h"
9
10#include "http2/http2frames_p.h"
11
12#include <private/qnoncontiguousbytedevice_p.h>
13#include <private/qsocketabstraction_p.h>
14
15#include <QtNetwork/qabstractsocket.h>
16
17#include <QtCore/qloggingcategory.h>
18#include <QtCore/qendian.h>
19#include <QtCore/qdebug.h>
20#include <QtCore/qlist.h>
21#include <QtCore/qnumeric.h>
22#include <QtCore/qurl.h>
23
24#include <qhttp2configuration.h>
25
26#ifndef QT_NO_NETWORKPROXY
27# include <QtNetwork/qnetworkproxy.h>
28#endif
29
30#include <qcoreapplication.h>
31
32#include <algorithm>
33#include <vector>
34#include <optional>
35
37
38using namespace Qt::StringLiterals;
39
40namespace
41{
42
43HPack::HttpHeader build_headers(const QHttpNetworkRequest &request, quint32 maxHeaderListSize,
44 bool useProxy)
45{
46 using namespace HPack;
47
48 HttpHeader header;
49 header.reserve(300);
50
51 // 1. Before anything - mandatory fields, if they do not fit into maxHeaderList -
52 // then stop immediately with error.
53 const auto auth = request.url().authority(QUrl::FullyEncoded | QUrl::RemoveUserInfo).toLatin1();
54 header.emplace_back(":authority", auth);
55 header.emplace_back(":method", request.methodName());
56 header.emplace_back(":path", request.uri(useProxy));
57 header.emplace_back(":scheme", request.url().scheme().toLatin1());
58
59 HeaderSize size = header_size(header);
60 if (!size.first) // Ooops!
61 return HttpHeader();
62
63 if (size.second > maxHeaderListSize)
64 return HttpHeader(); // Bad, we cannot send this request ...
65
66 const QHttpHeaders requestHeader = request.header();
67 using WK = QHttpHeaders::WellKnownHeader;
68 for (qsizetype i = 0; i < requestHeader.size(); ++i) {
69 const auto name = requestHeader.nameAt(i);
70 const auto value = requestHeader.valueAt(i);
71 const HeaderSize delta = entry_size(name, value);
72 if (!delta.first) // Overflow???
73 break;
74 if (std::numeric_limits<quint32>::max() - delta.second < size.second)
75 break;
76 size.second += delta.second;
77 if (size.second > maxHeaderListSize)
78 break;
79
80 if (name == QHttpHeaders::wellKnownHeaderName(WK::Connection)
81 || name == QHttpHeaders::wellKnownHeaderName(WK::Host)
82 || name == QHttpHeaders::wellKnownHeaderName(WK::KeepAlive)
83 || name.compare("proxy-connection"_L1, Qt::CaseInsensitive) == 0
84 || name == QHttpHeaders::wellKnownHeaderName(WK::TransferEncoding)) {
85 continue; // Those headers are not valid (section 3.2.1) - from QSpdyProtocolHandler
86 }
87 // TODO: verify with specs, which fields are valid to send ....
88 //
89 // Note: RFC 7450 8.1.2 (HTTP/2) states that header field names must be lower-cased
90 // prior to their encoding in HTTP/2
91 header.emplace_back(QByteArray{name.data(), name.size()}.toLower(),
92 QByteArray{value.data(), value.size()});
93 }
94
95 return header;
96}
97
98QUrl urlkey_from_request(const QHttpNetworkRequest &request)
99{
100 QUrl url;
101
102 url.setScheme(request.url().scheme());
103 url.setAuthority(request.url().authority(QUrl::FullyEncoded | QUrl::RemoveUserInfo));
104 url.setPath(QLatin1StringView(request.uri(false)));
105
106 return url;
107}
108
109} // Unnamed namespace
110
111// Since we anyway end up having this in every function definition:
112using namespace Http2;
113
114QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *channel)
116{
117 const auto h2Config = m_connection->http2Parameters();
118
119 if (!channel->ssl
120 && m_connection->connectionType() != QHttpNetworkConnection::ConnectionTypeHTTP2Direct) {
121 h2Connection = QHttp2Connection::createUpgradedConnection(channel->socket, h2Config);
122 // Since we upgraded there is already one stream (the request was sent as http1)
123 // and we need to handle it:
124 QHttp2Stream *stream = h2Connection->getStream(1);
125 Q_ASSERT(stream);
126 Q_ASSERT(channel->reply);
127 connectStream({ channel->request, channel->reply }, stream);
128 } else {
129 Q_ASSERT(QSocketAbstraction::socketState(channel->socket) == QAbstractSocket::ConnectedState);
130 h2Connection = QHttp2Connection::createDirectConnection(channel->socket, h2Config);
131 }
132 connect(h2Connection, &QHttp2Connection::receivedGOAWAY, this,
133 &QHttp2ProtocolHandler::handleGOAWAY);
134 connect(h2Connection, &QHttp2Connection::errorOccurred, this,
135 &QHttp2ProtocolHandler::connectionError);
136 connect(h2Connection, &QHttp2Connection::newIncomingStream, this,
137 [this](QHttp2Stream *stream){
138 // Having our peer start streams doesn't make sense. We are
139 // doing regular http request-response.
140 stream->sendRST_STREAM(REFUSE_STREAM);
141 if (!h2Connection->isGoingAway())
142 h2Connection->close(Http2::PROTOCOL_ERROR);
143 });
144 connect(h2Connection, &QHttp2Connection::connectionClosed, this,
145 &QHttp2ProtocolHandler::closeSession);
146}
147
148bool QHttp2ProtocolHandler::isGoingAway() const noexcept
149{
150 return h2Connection && h2Connection->isGoingAway();
151}
152
154{
155 // The channel has just received RemoteHostClosedError and since it will
156 // not try (for HTTP/2) to re-connect, it's time to finish all replies
157 // with error.
158
159 // Maybe we still have some data to read and can successfully finish
160 // a stream/request?
161 _q_receiveReply();
162 h2Connection->handleConnectionClosure();
163}
164
165void QHttp2ProtocolHandler::_q_uploadDataDestroyed(QObject *uploadData)
166{
167 QPointer<QHttp2Stream> stream = streamIDs.take(uploadData);
168 if (stream && stream->isActive())
169 stream->sendRST_STREAM(CANCEL);
170}
171
173{
174 _q_receiveReply();
175}
176
178{
179 // not using QObject::connect because the QHttpNetworkConnectionChannel
180 // already handles the signals we care about, so we just call the slot
181 // directly.
182 Q_ASSERT(h2Connection);
183 h2Connection->handleReadyRead();
184}
185
187{
188 if (isGoingAway())
189 return false;
190
191 // Process 'fake' (created by QNetworkAccessManager::connectToHostEncrypted())
192 // requests first:
193 auto &requests = m_channel->h2RequestsToSend;
194 for (auto it = requests.begin(), endIt = requests.end(); it != endIt;) {
195 const auto &pair = *it;
196 if (pair.first.isPreConnect()) {
197 m_connection->preConnectFinished();
198 emit pair.second->finished();
199 it = requests.erase(it);
200 if (requests.empty()) {
201 // Normally, after a connection was established and H2
202 // was negotiated, we send a client preface. connectToHostEncrypted
203 // though is not meant to send any data, it's just a 'preconnect'.
204 // Thus we return early:
205 return true;
206 }
207 } else {
208 ++it;
209 }
210 }
211
212 if (requests.empty())
213 return true;
214
215 m_channel->state = QHttpNetworkConnectionChannel::WritingState;
216 // Check what was promised/pushed, maybe we do not have to send a request
217 // and have a response already?
218
219 for (auto it = requests.begin(), end = requests.end(); it != end;) {
220 HttpMessagePair &httpPair = *it;
221
222 QUrl promiseKey = urlkey_from_request(httpPair.first);
223 if (h2Connection->promisedStream(promiseKey) != nullptr) {
224 // There's a PUSH_PROMISE for this request, so we don't send one
225 initReplyFromPushPromise(httpPair, promiseKey);
226 it = requests.erase(it);
227 continue;
228 }
229
230 QHttp2Stream *stream = createNewStream(httpPair);
231 if (!stream) { // There was an issue creating the stream
232 // Check if it was unrecoverable, ie. the reply is errored out and finished:
233 if (httpPair.second->isFinished()) {
234 it = requests.erase(it);
235 }
236 // ... either way we stop looping:
237 break;
238 }
239
240 QHttpNetworkRequest &request = requestReplyPairs[stream].first;
241 if (!sendHEADERS(stream, request)) {
242 finishStreamWithError(stream, QNetworkReply::UnknownNetworkError,
243 "failed to send HEADERS frame(s)"_L1);
244 continue;
245 }
246 if (request.uploadByteDevice()) {
247 if (!sendDATA(stream, httpPair.second)) {
248 finishStreamWithError(stream, QNetworkReply::UnknownNetworkError,
249 "failed to send DATA frame(s)"_L1);
250 continue;
251 }
252 }
253 it = requests.erase(it);
254 }
255
256 m_channel->state = QHttpNetworkConnectionChannel::IdleState;
257
258 return true;
259}
260
261/*!
262 \internal
263 This gets called during destruction of \a reply, so do not call any functions
264 on \a reply. We check if there is a stream associated with the reply and,
265 if there is, we remove the request-reply pair associated with this stream,
266 delete the stream and return \c{true}. Otherwise nothing happens and we
267 return \c{false}.
268*/
269bool QHttp2ProtocolHandler::tryRemoveReply(QHttpNetworkReply *reply)
270{
271 QHttp2Stream *stream = streamIDs.take(reply);
272 if (stream) {
273 stream->sendRST_STREAM(stream->isUploadingDATA() ? Http2::CANCEL : Http2::HTTP2_NO_ERROR);
274 clearStreamState(stream);
275 stream->deleteLater();
276 return true;
277 }
278 return false;
279}
280
281bool QHttp2ProtocolHandler::sendHEADERS(QHttp2Stream *stream, QHttpNetworkRequest &request)
282{
283 using namespace HPack;
284
285 bool useProxy = false;
286#ifndef QT_NO_NETWORKPROXY
287 useProxy = m_connection->d_func()->networkProxy.type() != QNetworkProxy::NoProxy;
288#endif
289 if (request.withCredentials()) {
290 m_connection->d_func()->createAuthorization(m_socket, request);
291 request.d->needResendWithCredentials = false;
292 }
293 const auto headers = build_headers(request, h2Connection->maxHeaderListSize(), useProxy);
294 if (headers.empty()) // nothing fits into maxHeaderListSize
295 return false;
296
297 bool mustUploadData = request.uploadByteDevice();
298 return stream->sendHEADERS(headers, !mustUploadData);
299}
300
301bool QHttp2ProtocolHandler::sendDATA(QHttp2Stream *stream, QHttpNetworkReply *reply)
302{
303 Q_ASSERT(reply);
304 QHttpNetworkReplyPrivate *replyPrivate = reply->d_func();
305 Q_ASSERT(replyPrivate);
306 QHttpNetworkRequest &request = replyPrivate->request;
307 Q_ASSERT(request.uploadByteDevice());
308
309 bool startedSending = stream->sendDATA(request.uploadByteDevice(), true);
310 return startedSending && !stream->wasReset();
311}
312
313void QHttp2ProtocolHandler::handleHeadersReceived(const HPack::HttpHeader &headers, bool endStream)
314{
315 QHttp2Stream *stream = qobject_cast<QHttp2Stream *>(sender());
316 Q_ASSERT(stream);
317 auto &requestPair = requestReplyPairs[stream];
318 auto *httpReply = requestPair.second;
319 auto &httpRequest = requestPair.first;
320 if (!httpReply)
321 return;
322
323 auto *httpReplyPrivate = httpReply->d_func();
324
325 // For HTTP/1 'location' is handled (and redirect URL set) when a protocol
326 // handler emits channel->allDone(). Http/2 protocol handler never emits
327 // allDone, since we have many requests multiplexed in one channel at any
328 // moment and we are probably not done yet. So we extract url and set it
329 // here, if needed.
330 int statusCode = 0;
331 for (const auto &pair : headers) {
332 const auto &name = pair.name;
333 const auto value = QByteArrayView(pair.value);
334
335 // TODO: part of this code copies what SPDY protocol handler does when
336 // processing headers. Binary nature of HTTP/2 and SPDY saves us a lot
337 // of parsing and related errors/bugs, but it would be nice to have
338 // more detailed validation of headers.
339 if (name == ":status") {
340 bool ok = false;
341 if (int status = value.toInt(&ok); ok && status >= 0 && status <= 999) {
342 statusCode = status;
343 httpReply->setStatusCode(statusCode);
344 m_channel->lastStatus = statusCode; // Mostly useless for http/2, needed for auth
345 } else {
346 finishStreamWithError(stream, QNetworkReply::ProtocolInvalidOperationError,
347 "invalid :status value"_L1);
348 return;
349 }
350 } else if (name == "content-length") {
351 bool ok = false;
352 const qlonglong length = value.toLongLong(&ok);
353 if (ok)
354 httpReply->setContentLength(length);
355 } else {
356 const auto binder = name == "set-cookie" ? QByteArrayView("\n") : QByteArrayView(", ");
357 httpReply->appendHeaderField(name, QByteArray(pair.value).replace('\0', binder));
358 }
359 }
360
361 // Discard all informational (1xx) replies with the exception of 101.
362 // Also see RFC 9110 (Chapter 15.2)
363 if (statusCode == 100 || (102 <= statusCode && statusCode <= 199)) {
364 httpReplyPrivate->clearHttpLayerInformation();
365 return;
366 }
367
368 if (QHttpNetworkReply::isHttpRedirect(statusCode) && httpRequest.isFollowRedirects()) {
371 if (result.errorCode != QNetworkReply::NoError) {
372 auto errorString = m_connection->d_func()->errorDetail(result.errorCode, m_socket);
373 finishStreamWithError(stream, result.errorCode, errorString);
374 stream->sendRST_STREAM(INTERNAL_ERROR);
375 return;
376 }
377
378 if (result.redirectUrl.isValid())
379 httpReply->setRedirectUrl(result.redirectUrl);
380 }
381
382 if (httpReplyPrivate->isCompressed() && httpRequest.d->autoDecompress)
383 httpReplyPrivate->removeAutoDecompressHeader();
384
385 if (QHttpNetworkReply::isHttpRedirect(statusCode)) {
386 // Note: This status code can trigger uploadByteDevice->reset() in
387 // QHttpNetworkConnectionChannel::handleStatus. Alas, we have no single
388 // request/reply, we multiplex several requests and thus we never simply
389 // call 'handleStatus'. If we have a byte-device - we try to reset it
390 // here, we don't (and can't) handle any error during reset operation.
391 if (auto *byteDevice = httpRequest.uploadByteDevice()) {
392 byteDevice->reset();
393 httpReplyPrivate->totallyUploadedData = 0;
394 }
395 }
396
397 QMetaObject::invokeMethod(httpReply, &QHttpNetworkReply::headerChanged, Qt::QueuedConnection);
398 if (endStream)
399 finishStream(stream, Qt::QueuedConnection);
400}
401
402void QHttp2ProtocolHandler::handleDataReceived(const QByteArray &data, bool endStream)
403{
404 QHttp2Stream *stream = qobject_cast<QHttp2Stream *>(sender());
405 auto &httpPair = requestReplyPairs[stream];
406 auto *httpReply = httpPair.second;
407 if (!httpReply)
408 return;
409 Q_ASSERT(!stream->isPromisedStream());
410
411 if (!data.isEmpty() && !httpPair.first.d->needResendWithCredentials) {
412 auto *replyPrivate = httpReply->d_func();
413
414 replyPrivate->totalProgress += data.size();
415
416 replyPrivate->responseData.append(data);
417
418 if (replyPrivate->shouldEmitSignals()) {
419 QMetaObject::invokeMethod(httpReply, &QHttpNetworkReply::readyRead,
420 Qt::QueuedConnection);
421 QMetaObject::invokeMethod(httpReply, &QHttpNetworkReply::dataReadProgress,
422 Qt::QueuedConnection, replyPrivate->totalProgress,
423 replyPrivate->bodyLength);
424 }
425 }
426 stream->clearDownloadBuffer();
427 if (endStream)
428 finishStream(stream, Qt::QueuedConnection);
429}
430
431// After calling this function, either the request will be re-sent or
432// the reply will be finishedWithError! Do not emit finished() or similar on the
433// reply after this!
434void QHttp2ProtocolHandler::handleAuthorization(QHttp2Stream *stream)
435{
436 auto &requestPair = requestReplyPairs[stream];
437 auto *httpReply = requestPair.second;
438 auto *httpReplyPrivate = httpReply->d_func();
439 auto &httpRequest = requestPair.first;
440
441 Q_ASSERT(httpReply && (httpReply->statusCode() == 401 || httpReply->statusCode() == 407));
442
443 const auto handleAuth = [&, this](QByteArrayView authField, bool isProxy) -> bool {
444 Q_ASSERT(httpReply);
445 const QByteArrayView auth = authField.trimmed();
446 if (auth.startsWith("Negotiate") || auth.startsWith("NTLM")) {
447 // NTLM/Kerberos/Negotiate authentication is not supported with HTTP/2.
448 // Finish the stream with an error so QNetworkReply::finished is emitted.
449 // Falling back to HTTP/1.1 is a separate, larger effort (QTBUG-143926).
450 emit httpReply->headerChanged();
451 emit httpReply->readyRead();
452 const QNetworkReply::NetworkError error = isProxy
453 ? QNetworkReply::ProxyAuthenticationRequiredError
454 : QNetworkReply::AuthenticationRequiredError;
455 finishStreamWithError(stream, error,
456 m_connection->d_func()->errorDetail(error, m_socket));
457 return false;
458 }
459 // Somewhat mimics parts of QHttpNetworkConnectionChannel::handleStatus
460 bool resend = false;
461 const bool authenticateHandled = m_connection->d_func()->handleAuthenticateChallenge(
462 m_socket, httpReply, isProxy, resend);
463 if (authenticateHandled) {
464 if (resend) {
465 httpReply->d_func()->eraseData();
466 // Add the request back in queue, we'll retry later now that
467 // we've gotten some username/password set on it:
468 httpRequest.d->needResendWithCredentials = true;
469 m_channel->h2RequestsToSend.insert(httpRequest.priority(), requestPair);
470 httpReply->d_func()->clearHeaders();
471 // If we have data we were uploading we need to reset it:
472 if (auto *byteDevice = httpRequest.uploadByteDevice()) {
473 byteDevice->reset();
474 httpReplyPrivate->totallyUploadedData = 0;
475 }
476 // We automatically try to send new requests when the stream is
477 // closed, so we don't need to call sendRequest ourselves.
478 return true;
479 } // else: we're just not resending the request.
480 // @note In the http/1.x case we (at time of writing) call close()
481 // for the connectionChannel (which is a bit weird, we could surely
482 // reuse the open socket outside "connection:close"?), but in http2
483 // we only have one channel, so we won't close anything.
484 } else {
485 // No authentication header or authentication isn't supported, but
486 // we got a 401/407 so we cannot succeed. We need to emit signals
487 // for headers and data, and then finishWithError.
488 emit httpReply->headerChanged();
489 emit httpReply->readyRead();
490 QNetworkReply::NetworkError error = httpReply->statusCode() == 401
491 ? QNetworkReply::AuthenticationRequiredError
492 : QNetworkReply::ProxyAuthenticationRequiredError;
493 finishStreamWithError(stream, QNetworkReply::AuthenticationRequiredError,
494 m_connection->d_func()->errorDetail(error, m_socket));
495 }
496 return false;
497 };
498
499 // These statuses would in HTTP/1.1 be handled by
500 // QHttpNetworkConnectionChannel::handleStatus. But because h2 has
501 // multiple streams/requests in a single channel this structure does not
502 // map properly to that function.
503 bool authOk = true;
504 switch (httpReply->statusCode()) {
505 case 401:
506 authOk = handleAuth(httpReply->headerField("www-authenticate"), false);
507 break;
508 case 407:
509 authOk = handleAuth(httpReply->headerField("proxy-authenticate"), true);
510 break;
511 default:
512 Q_UNREACHABLE();
513 }
514 if (authOk) {
515 stream->sendRST_STREAM(CANCEL);
516 clearStreamState(stream);
517 stream->deleteLater();
518 } // else: errors handled inside handleAuth
519}
520
521// Called when we have received a frame with the END_STREAM flag set
522void QHttp2ProtocolHandler::finishStream(QHttp2Stream *stream, Qt::ConnectionType connectionType)
523{
524 if (stream->state() != QHttp2Stream::State::Closed)
525 stream->sendRST_STREAM(CANCEL);
526
527 auto &pair = requestReplyPairs[stream];
528 auto *httpReply = pair.second;
529 if (httpReply) {
530 int statusCode = httpReply->statusCode();
531 if (statusCode == 401 || statusCode == 407) {
532 // handleAuthorization will either re-send the request or
533 // finishWithError. In either case we don't want to emit finished
534 // here.
535 handleAuthorization(stream);
536 return;
537 }
538
539 httpReply->disconnect(this);
540
541 if (!pair.first.d->needResendWithCredentials) {
542 if (connectionType == Qt::DirectConnection)
543 emit httpReply->finished();
544 else
545 QMetaObject::invokeMethod(httpReply, &QHttpNetworkReply::finished, connectionType);
546 }
547 }
548
549 clearStreamState(stream);
550 qCDebug(QT_HTTP2) << "stream" << stream->streamID() << "closed";
551
552 // Detach the reply and tear down the stream via canonical path, so a
553 // later stray frame cannot fail an already-finished reply. tryRemoveReply()
554 // also clears the reply->stream map, its sendRST_STREAM() does nothing on a
555 // gracefully closed stream.
556 if (!httpReply || !tryRemoveReply(httpReply)) {
557 requestReplyPairs.remove(stream);
558 stream->deleteLater();
559 }
560}
561
562void QHttp2ProtocolHandler::handleGOAWAY(Http2Error errorCode, quint32 lastStreamID)
563{
564 qCDebug(QT_HTTP2) << "GOAWAY received, error code:" << errorCode << "last stream ID:"
565 << lastStreamID;
566
567 if (errorCode == HTTP2_NO_ERROR) {
568 // Graceful GOAWAY: the queued requests are sent on the next connection.
569 // *noop*
570 } else {
571 // Error GOAWAY: the queued requests were never processed, so a retry could still
572 // succeed, but something went wrong on this connection - report it to the user
573 // rather than silently trying again.
574 m_channel->emitFinishedWithError(QNetworkReply::ProtocolUnknownError,
575 "GOAWAY received, cannot start a request");
576 m_channel->h2RequestsToSend.clear();
577 }
578
579 // Active streams are not handled here: QHttp2Connection::handleGOAWAY() cancels the ones
580 // the peer will not process and we pick them up through QHttp2Stream::errorOccurred.
581}
582
583void QHttp2ProtocolHandler::finishStreamWithError(QHttp2Stream *stream, Http2Error errorCode)
584{
585 QNetworkReply::NetworkError error = QNetworkReply::NoError;
586 QString message;
587 qt_error(errorCode, error, message);
588 finishStreamWithError(stream, error, message);
589}
590
591void QHttp2ProtocolHandler::finishStreamWithError(QHttp2Stream *stream,
592 QNetworkReply::NetworkError error, const QString &message)
593{
594 stream->sendRST_STREAM(CANCEL);
595 const HttpMessagePair &pair = requestReplyPairs.value(stream);
596 if (auto *httpReply = pair.second) {
597 httpReply->disconnect(this);
598
599 // TODO: error message must be translated!!! (tr)
600 emit httpReply->finishedWithError(error, message);
601 }
602
603 clearStreamState(stream);
604 stream->deleteLater();
605 qCWarning(QT_HTTP2) << "stream" << stream->streamID() << "finished with error:" << message;
606}
607
608/*!
609 \internal
610
611 Creates a QHttp2Stream for the request, will return \nullptr if the stream
612 could not be created for some reason, and will finish the reply if required.
613*/
614QHttp2Stream *QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message,
615 bool uploadDone)
616{
617 QUrl streamKey = urlkey_from_request(message.first);
618 if (auto promisedStream = h2Connection->promisedStream(streamKey)) {
619 Q_ASSERT(promisedStream->state() != QHttp2Stream::State::Closed);
620 return promisedStream;
621 }
622
623 QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError>
624 streamResult = h2Connection->createStream();
625 if (!streamResult.ok()) {
626 if (streamResult.error()
627 == QHttp2Connection::CreateStreamError::MaxConcurrentStreamsReached) {
628 // We have to wait for a stream to be closed before we can create a new one, so
629 // we just return nullptr, the caller should not remove it from the queue.
630 return nullptr;
631 }
632 qCDebug(QT_HTTP2) << "failed to create new stream:" << streamResult.error();
633 auto *reply = message.second;
634 const char *cstr = "Failed to initialize HTTP/2 stream with errorcode: %1";
635 const QString errorString = QCoreApplication::tr("QHttp", cstr)
636 .arg(QDebug::toString(streamResult.error()));
637 emit reply->finishedWithError(QNetworkReply::ProtocolFailure, errorString);
638 return nullptr;
639 }
640 QHttp2Stream *stream = streamResult.unwrap();
641
642 if (!uploadDone) {
643 if (auto *src = message.first.uploadByteDevice()) {
644 connect(src, &QObject::destroyed, this, &QHttp2ProtocolHandler::_q_uploadDataDestroyed);
645 streamIDs.insert(src, stream);
646 }
647 }
648
649 auto *reply = message.second;
650 QMetaObject::invokeMethod(reply, &QHttpNetworkReply::requestSent, Qt::QueuedConnection);
651
652 connectStream(message, stream);
653 return stream;
654}
655
656void QHttp2ProtocolHandler::connectStream(const HttpMessagePair &message, QHttp2Stream *stream)
657{
658 auto *reply = message.second;
659 auto *replyPrivate = reply->d_func();
660 replyPrivate->connection = m_connection;
661 replyPrivate->connectionChannel = m_channel;
662
663 reply->setHttp2WasUsed(true);
664 QPointer<QHttp2Stream> &oldStream = streamIDs[reply];
665 if (oldStream) {
666 disconnect(oldStream, nullptr, this, nullptr);
667 clearStreamState(oldStream);
668 }
669 oldStream = stream;
670 requestReplyPairs.emplace(stream, message);
671
672 QObject::connect(stream, &QHttp2Stream::headersReceived, this,
673 &QHttp2ProtocolHandler::handleHeadersReceived);
674 QObject::connect(stream, &QHttp2Stream::dataReceived, this,
675 &QHttp2ProtocolHandler::handleDataReceived);
676 QObject::connect(stream, &QHttp2Stream::errorOccurred, this,
677 [this, stream](Http2Error errorCode, const QString &errorString) {
678 qCWarning(QT_HTTP2)
679 << "stream" << stream->streamID() << "error:" << errorString;
680 // A graceful GOAWAY cancels exactly the streams the peer will not
681 // process, so those requests can still be sent on a new connection.
682 // An error GOAWAY, or any other failure, keeps its own error.
683 if (const auto lastId = h2Connection->lastGoAwayStreamID();
684 errorCode == HTTP2_NO_ERROR && lastId
685 && stream->streamID() > *lastId) {
686 finishStreamWithError(
687 stream, QNetworkReply::ContentReSendError,
688 QCoreApplication::translate(
689 "QHttp",
690 "Server stopped accepting new streams before this "
691 "stream was established"));
692 } else {
693 finishStreamWithError(stream, errorCode);
694 }
695 });
696
697 QObject::connect(stream, &QHttp2Stream::stateChanged, this, [this](QHttp2Stream::State state) {
698 if (state == QHttp2Stream::State::Closed) {
699 // Try to send more requests if we have any
700 if (!m_channel->h2RequestsToSend.empty()) {
701 QMetaObject::invokeMethod(this, &QHttp2ProtocolHandler::sendRequest,
702 Qt::QueuedConnection);
703 }
704 }
705 });
706}
707
708void QHttp2ProtocolHandler::clearStreamState(QHttp2Stream *stream)
709{
710 auto it = requestReplyPairs.find(stream);
711 if (it == requestReplyPairs.end())
712 return;
713
714 if (auto *reply = it->second)
715 streamIDs.remove(reply);
716 if (auto *uploadDevice = it->first.uploadByteDevice())
717 streamIDs.remove(uploadDevice);
718
719 requestReplyPairs.erase(it);
720}
721
722void QHttp2ProtocolHandler::initReplyFromPushPromise(const HttpMessagePair &message,
723 const QUrl &cacheKey)
724{
725 QHttp2Stream *promise = h2Connection->promisedStream(cacheKey);
726 Q_ASSERT(promise);
727 Q_ASSERT(message.second);
728 message.second->setHttp2WasUsed(true);
729
730 qCDebug(QT_HTTP2) << "found cached/promised response on stream" << promise->streamID();
731
732 const bool replyFinished = promise->state() == QHttp2Stream::State::Closed;
733
734 connectStream(message, promise);
735
736 // Now that we have connect()ed, re-emit signals so that the reply
737 // can be processed as usual:
738
739 QByteDataBuffer downloadBuffer = promise->takeDownloadBuffer();
740 if (const auto &headers = promise->receivedHeaders(); !headers.empty())
741 emit promise->headersReceived(headers, replyFinished && downloadBuffer.isEmpty());
742
743 if (!downloadBuffer.isEmpty()) {
744 for (qsizetype i = 0; i < downloadBuffer.bufferCount(); ++i) {
745 const bool streamEnded = replyFinished && i == downloadBuffer.bufferCount() - 1;
746 emit promise->dataReceived(downloadBuffer[i], streamEnded);
747 }
748 }
749}
750
751void QHttp2ProtocolHandler::connectionError(Http2::Http2Error errorCode, const QString &message)
752{
753 Q_ASSERT(!message.isNull());
754
755 qCCritical(QT_HTTP2) << "connection error:" << message;
756
757 const auto error = qt_error(errorCode);
758 m_channel->emitFinishedWithError(error, message);
759 m_channel->h2RequestsToSend.clear();
760
761 closeSession();
762}
763
764void QHttp2ProtocolHandler::closeSession()
765{
766 m_channel->close();
767}
768
769QT_END_NAMESPACE
770
771#include "moc_qhttp2protocolhandler_p.cpp"
bool isGoingAway() const noexcept
QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *channel)
Q_INVOKABLE void handleConnectionClosure()
Q_INVOKABLE void _q_receiveReply() override
Q_INVOKABLE bool sendRequest() override
bool tryRemoveReply(QHttpNetworkReply *reply) override
static ParseRedirectResult parseRedirectResponse(QHttpNetworkReply *reply)
Combined button and popup list for selecting options.
std::pair< QHttpNetworkRequest, QHttpNetworkReply * > HttpMessagePair