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

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