| 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 | |
| 36 | QT_BEGIN_NAMESPACE |
| 37 | |
| 38 | using namespace Qt::StringLiterals; |
| 39 | |
| 40 | namespace |
| 41 | { |
| 42 | |
| 43 | HPack::HttpHeader (const QHttpNetworkRequest &request, quint32 , |
| 44 | bool useProxy) |
| 45 | { |
| 46 | using namespace HPack; |
| 47 | |
| 48 | HttpHeader ; |
| 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 = 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 | |
| 95 | QUrl 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: |
| 109 | using namespace Http2; |
| 110 | |
| 111 | QHttp2ProtocolHandler::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 | |
| 143 | void 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 | |
| 155 | void 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 | |
| 162 | void QHttp2ProtocolHandler::_q_readyRead() |
| 163 | { |
| 164 | _q_receiveReply(); |
| 165 | } |
| 166 | |
| 167 | void 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 | |
| 176 | bool 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 | */ |
| 265 | bool 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 | |
| 277 | bool 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 = 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 | |
| 297 | bool 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 | |
| 309 | void QHttp2ProtocolHandler::handleHeadersReceived(const HPack::HttpHeader &, 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 | |
| 395 | void 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! |
| 427 | void 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 |
| 509 | void 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 | |
| 540 | void 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 | |
| 565 | void 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 | |
| 573 | void 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 | */ |
| 594 | QHttp2Stream *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 | |
| 636 | void 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 | |
| 672 | void 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 & = 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 | |
| 701 | void 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 | |
| 713 | void QHttp2ProtocolHandler::closeSession() |
| 714 | { |
| 715 | m_channel->close(); |
| 716 | } |
| 717 | |
| 718 | QT_END_NAMESPACE |
| 719 | |
| 720 | #include "moc_qhttp2protocolhandler_p.cpp" |
| 721 | |