From b2c0f9713c1c39bc3214df49f0d2b44bcc84d790 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Tue, 12 Jan 2016 14:45:47 +0100 Subject: HTTP2 protocol handler Add HTTP2 support in QNAM - QHttp2ProtocolHandler + aux. classes. [ChangeLog][QtNetwork] Added support for HTTP/2 protocol Task-number: QTBUG-50956 Change-Id: I3a119cfbb1fb3118d9003225f5e54b46ae2829b6 Reviewed-by: Edward Welbourne --- src/network/access/access.pri | 6 +- src/network/access/http2/http2.pri | 10 +- src/network/access/http2/http2frames.cpp | 525 +++++++++ src/network/access/http2/http2frames_p.h | 173 +++ src/network/access/http2/http2protocol.cpp | 156 +++ src/network/access/http2/http2protocol_p.h | 164 +++ src/network/access/http2/http2streams.cpp | 100 ++ src/network/access/http2/http2streams_p.h | 92 ++ src/network/access/qhttp2protocolhandler.cpp | 1219 ++++++++++++++++++++ src/network/access/qhttp2protocolhandler_p.h | 196 ++++ src/network/access/qhttpnetworkconnection.cpp | 6 +- src/network/access/qhttpnetworkconnection_p.h | 4 +- .../access/qhttpnetworkconnectionchannel.cpp | 14 +- src/network/access/qhttpnetworkreply_p.h | 1 + src/network/access/qhttpnetworkrequest.cpp | 14 +- src/network/access/qhttpnetworkrequest_p.h | 5 + src/network/access/qhttpthreaddelegate.cpp | 8 +- src/network/access/qnetworkreplyhttpimpl.cpp | 3 + src/network/access/qnetworkrequest.h | 2 + src/network/ssl/qsslconfiguration.cpp | 1 + src/network/ssl/qsslconfiguration.h | 1 + 21 files changed, 2688 insertions(+), 12 deletions(-) create mode 100644 src/network/access/http2/http2frames.cpp create mode 100644 src/network/access/http2/http2frames_p.h create mode 100644 src/network/access/http2/http2protocol.cpp create mode 100644 src/network/access/http2/http2protocol_p.h create mode 100644 src/network/access/http2/http2streams.cpp create mode 100644 src/network/access/http2/http2streams_p.h create mode 100644 src/network/access/qhttp2protocolhandler.cpp create mode 100644 src/network/access/qhttp2protocolhandler_p.h diff --git a/src/network/access/access.pri b/src/network/access/access.pri index 746ddc6916..70ace3f55e 100644 --- a/src/network/access/access.pri +++ b/src/network/access/access.pri @@ -38,7 +38,8 @@ HEADERS += \ access/qhttpthreaddelegate_p.h \ access/qhttpmultipart.h \ access/qhttpmultipart_p.h \ - access/qnetworkfile_p.h + access/qnetworkfile_p.h \ + access/qhttp2protocolhandler_p.h SOURCES += \ access/qftp.cpp \ @@ -70,7 +71,8 @@ SOURCES += \ access/qnetworkdiskcache.cpp \ access/qhttpthreaddelegate.cpp \ access/qhttpmultipart.cpp \ - access/qnetworkfile.cpp + access/qnetworkfile.cpp \ + access/qhttp2protocolhandler.cpp mac: LIBS_PRIVATE += -framework Security diff --git a/src/network/access/http2/http2.pri b/src/network/access/http2/http2.pri index 2157e35e2f..e9f30aeb4a 100644 --- a/src/network/access/http2/http2.pri +++ b/src/network/access/http2/http2.pri @@ -2,10 +2,16 @@ HEADERS += \ access/http2/bitstreams_p.h \ access/http2/huffman_p.h \ access/http2/hpack_p.h \ - access/http2/hpacktable_p.h + access/http2/hpacktable_p.h \ + access/http2/http2frames_p.h \ + access/http2/http2streams_p.h \ + access/http2/http2protocol_p.h SOURCES += \ access/http2/bitstreams.cpp \ access/http2/huffman.cpp \ access/http2/hpack.cpp \ - access/http2/hpacktable.cpp + access/http2/hpacktable.cpp \ + access/http2/http2frames.cpp \ + access/http2/http2streams.cpp \ + access/http2/http2protocol.cpp diff --git a/src/network/access/http2/http2frames.cpp b/src/network/access/http2/http2frames.cpp new file mode 100644 index 0000000000..471fb2c7fb --- /dev/null +++ b/src/network/access/http2/http2frames.cpp @@ -0,0 +1,525 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "http2frames_p.h" + +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +namespace Http2 +{ + +// HTTP/2 frames are defined by RFC7540, clauses 4 and 6. + +FrameStatus validate_frame_header(FrameType type, FrameFlags flags, quint32 payloadSize) +{ + // 4.2 Frame Size + if (payloadSize > maxPayloadSize) + return FrameStatus::sizeError; + + switch (type) { + case FrameType::SETTINGS: + // SETTINGS ACK can not have any payload. + // The payload of a SETTINGS frame consists of zero + // or more parameters, each consisting of an unsigned + // 16-bit setting identifier and an unsigned 32-bit value. + // Thus the payload size must be a multiple of 6. + if (flags.testFlag(FrameFlag::ACK) ? payloadSize : payloadSize % 6) + return FrameStatus::sizeError; + break; + case FrameType::PRIORITY: + // 6.3 PRIORITY + if (payloadSize != 5) + return FrameStatus::sizeError; + break; + case FrameType::PING: + case FrameType::GOAWAY: + // 6.7 PING, 6.8 GOAWAY + if (payloadSize != 8) + return FrameStatus::sizeError; + break; + case FrameType::RST_STREAM: + case FrameType::WINDOW_UPDATE: + // 6.4 RST_STREAM, 6.9 WINDOW_UPDATE + if (payloadSize != 4) + return FrameStatus::sizeError; + break; + case FrameType::PUSH_PROMISE: + // 6.6 PUSH_PROMISE + if (payloadSize < 4) + return FrameStatus::sizeError; + default: + // DATA/HEADERS/CONTINUATION will be verified + // when we have payload. + // Frames of unknown types are ignored (5.1) + break; + } + + return FrameStatus::goodFrame; +} + +FrameStatus validate_frame_payload(FrameType type, FrameFlags flags, + quint32 size, const uchar *src) +{ + Q_ASSERT(!size || src); + + // Ignored, 5.1 + if (type == FrameType::LAST_FRAME_TYPE) + return FrameStatus::goodFrame; + + // 6.1 DATA, 6.2 HEADERS + if (type == FrameType::DATA || type == FrameType::HEADERS) { + if (flags.testFlag(FrameFlag::PADDED)) { + if (!size || size < src[0]) + return FrameStatus::sizeError; + size -= src[0]; + } + if (type == FrameType::HEADERS && flags.testFlag(FrameFlag::PRIORITY)) { + if (size < 5) + return FrameStatus::sizeError; + } + } + + // 6.6 PUSH_PROMISE + if (type == FrameType::PUSH_PROMISE) { + if (flags.testFlag(FrameFlag::PADDED)) { + if (!size || size < src[0]) + return FrameStatus::sizeError; + size -= src[0]; + } + + if (size < 4) + return FrameStatus::sizeError; + } + + return FrameStatus::goodFrame; +} + +FrameStatus validate_frame_payload(FrameType type, FrameFlags flags, + const std::vector &payload) +{ + const uchar *src = payload.size() ? &payload[0] : nullptr; + return validate_frame_payload(type, flags, quint32(payload.size()), src); +} + + +FrameReader::FrameReader(FrameReader &&rhs) + : framePayload(std::move(rhs.framePayload)) +{ + type = rhs.type; + rhs.type = FrameType::LAST_FRAME_TYPE; + + flags = rhs.flags; + rhs.flags = FrameFlag::EMPTY; + + streamID = rhs.streamID; + rhs.streamID = 0; + + payloadSize = rhs.payloadSize; + rhs.payloadSize = 0; + + incompleteRead = rhs.incompleteRead; + rhs.incompleteRead = false; + + offset = rhs.offset; + rhs.offset = 0; +} + +FrameReader &FrameReader::operator = (FrameReader &&rhs) +{ + framePayload = std::move(rhs.framePayload); + + type = rhs.type; + rhs.type = FrameType::LAST_FRAME_TYPE; + + flags = rhs.flags; + rhs.flags = FrameFlag::EMPTY; + + streamID = rhs.streamID; + rhs.streamID = 0; + + payloadSize = rhs.payloadSize; + rhs.payloadSize = 0; + + incompleteRead = rhs.incompleteRead; + rhs.incompleteRead = false; + + offset = rhs.offset; + rhs.offset = 0; + + return *this; +} + +FrameStatus FrameReader::read(QAbstractSocket &socket) +{ + if (!incompleteRead) { + if (!readHeader(socket)) + return FrameStatus::incompleteFrame; + + const auto status = validate_frame_header(type, flags, payloadSize); + if (status != FrameStatus::goodFrame) { + // No need to read any payload. + return status; + } + + if (Http2PredefinedParameters::maxFrameSize < payloadSize) + return FrameStatus::sizeError; + + framePayload.resize(payloadSize); + offset = 0; + } + + if (framePayload.size()) { + if (!readPayload(socket)) + return FrameStatus::incompleteFrame; + } + + return validate_frame_payload(type, flags, framePayload); +} + +bool FrameReader::padded(uchar *pad) const +{ + Q_ASSERT(pad); + + if (!flags.testFlag(FrameFlag::PADDED)) + return false; + + if (type == FrameType::DATA + || type == FrameType::PUSH_PROMISE + || type == FrameType::HEADERS) { + Q_ASSERT(framePayload.size() >= 1); + *pad = framePayload[0]; + return true; + } + + return false; +} + +bool FrameReader::priority(quint32 *streamID, uchar *weight) const +{ + Q_ASSERT(streamID); + Q_ASSERT(weight); + + if (!framePayload.size()) + return false; + + const uchar *src = &framePayload[0]; + if (type == FrameType::HEADERS && flags.testFlag(FrameFlag::PADDED)) + ++src; + + if ((type == FrameType::HEADERS && flags.testFlag(FrameFlag::PRIORITY)) + || type == FrameType::PRIORITY) { + *streamID = qFromBigEndian(src); + *weight = src[4]; + return true; + } + + return false; +} + +quint32 FrameReader::dataSize() const +{ + quint32 size = quint32(framePayload.size()); + uchar pad = 0; + if (padded(&pad)) { + // + 1 one for a byte with padding number itself: + size -= pad + 1; + } + + quint32 dummyID = 0; + uchar dummyW = 0; + if (priority(&dummyID, &dummyW)) + size -= 5; + + return size; +} + +const uchar *FrameReader::dataBegin() const +{ + if (!framePayload.size()) + return nullptr; + + const uchar *src = &framePayload[0]; + uchar dummyPad = 0; + if (padded(&dummyPad)) + ++src; + + quint32 dummyID = 0; + uchar dummyW = 0; + if (priority(&dummyID, &dummyW)) + src += 5; + + return src; +} + +bool FrameReader::readHeader(QAbstractSocket &socket) +{ + if (socket.bytesAvailable() < frameHeaderSize) + return false; + + uchar src[frameHeaderSize] = {}; + socket.read(reinterpret_cast(src), frameHeaderSize); + + payloadSize = src[0] << 16 | src[1] << 8 | src[2]; + + type = FrameType(src[3]); + if (int(type) >= int(FrameType::LAST_FRAME_TYPE)) + type = FrameType::LAST_FRAME_TYPE; // To be ignored, 5.1 + + flags = FrameFlags(src[4]); + streamID = qFromBigEndian(src + 5); + + return true; +} + +bool FrameReader::readPayload(QAbstractSocket &socket) +{ + Q_ASSERT(offset <= framePayload.size()); + + // Casts and ugliness - to deal with MSVC. Values are guaranteed to fit into quint32. + if (const auto residue = std::min(qint64(framePayload.size() - offset), socket.bytesAvailable())) { + socket.read(reinterpret_cast(&framePayload[offset]), residue); + offset += quint32(residue); + } + + incompleteRead = offset < framePayload.size(); + return !incompleteRead; +} + + +FrameWriter::FrameWriter() +{ + frameBuffer.reserve(Http2PredefinedParameters::maxFrameSize + + Http2PredefinedParameters::frameHeaderSize); +} + +FrameWriter::FrameWriter(FrameType type, FrameFlags flags, quint32 streamID) +{ + frameBuffer.reserve(Http2PredefinedParameters::maxFrameSize + + Http2PredefinedParameters::frameHeaderSize); + start(type, flags, streamID); +} + +void FrameWriter::start(FrameType type, FrameFlags flags, quint32 streamID) +{ + frameBuffer.resize(frameHeaderSize); + // The first three bytes - payload size, which is 0 for now. + frameBuffer[0] = 0; + frameBuffer[1] = 0; + frameBuffer[2] = 0; + + frameBuffer[3] = uchar(type); + frameBuffer[4] = uchar(flags); + + qToBigEndian(streamID, &frameBuffer[5]); +} + +void FrameWriter::setPayloadSize(quint32 size) +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + Q_ASSERT(size < maxPayloadSize); + + frameBuffer[0] = size >> 16; + frameBuffer[1] = size >> 8; + frameBuffer[2] = size; +} + +quint32 FrameWriter::payloadSize() const +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + return frameBuffer[0] << 16 | frameBuffer[1] << 8 | frameBuffer[2]; +} + +void FrameWriter::setType(FrameType type) +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + frameBuffer[3] = uchar(type); +} + +FrameType FrameWriter::type() const +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + return FrameType(frameBuffer[3]); +} + +void FrameWriter::setFlags(FrameFlags flags) +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + frameBuffer[4] = uchar(flags); +} + +void FrameWriter::addFlag(FrameFlag flag) +{ + setFlags(flags() | flag); +} + +FrameFlags FrameWriter::flags() const +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + return FrameFlags(frameBuffer[4]); +} + +quint32 FrameWriter::streamID() const +{ + return qFromBigEndian(&frameBuffer[5]); +} + +void FrameWriter::append(uchar val) +{ + frameBuffer.push_back(val); + updatePayloadSize(); +} + +void FrameWriter::append(const uchar *begin, const uchar *end) +{ + Q_ASSERT(begin && end); + Q_ASSERT(begin < end); + + frameBuffer.insert(frameBuffer.end(), begin, end); + updatePayloadSize(); +} + +void FrameWriter::updatePayloadSize() +{ + // First, compute size: + const quint32 payloadSize = quint32(frameBuffer.size() - frameHeaderSize); + Q_ASSERT(payloadSize <= maxPayloadSize); + setPayloadSize(payloadSize); +} + +bool FrameWriter::write(QAbstractSocket &socket) const +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + // Do some sanity check first: + Q_ASSERT(int(type()) < int(FrameType::LAST_FRAME_TYPE)); + Q_ASSERT(validate_frame_header(type(), flags(), payloadSize()) == FrameStatus::goodFrame); + + const auto nWritten = socket.write(reinterpret_cast(&frameBuffer[0]), + frameBuffer.size()); + return nWritten != -1 && size_type(nWritten) == frameBuffer.size(); +} + +bool FrameWriter::writeHEADERS(QAbstractSocket &socket, quint32 sizeLimit) +{ + Q_ASSERT(frameBuffer.size() >= frameHeaderSize); + + if (sizeLimit > quint32(maxPayloadSize)) + sizeLimit = quint32(maxPayloadSize); + + if (quint32(frameBuffer.size() - frameHeaderSize) <= sizeLimit) { + updatePayloadSize(); + return write(socket); + } + + // Write a frame's header (not controlled by sizeLimit) and + // as many bytes of payload as we can within sizeLimit, + // then send CONTINUATION frames, as needed. + setPayloadSize(sizeLimit); + const quint32 firstChunkSize = frameHeaderSize + sizeLimit; + qint64 written = socket.write(reinterpret_cast(&frameBuffer[0]), + firstChunkSize); + + if (written != qint64(firstChunkSize)) + return false; + + FrameWriter continuationFrame(FrameType::CONTINUATION, FrameFlag::EMPTY, streamID()); + quint32 offset = firstChunkSize; + + while (offset != frameBuffer.size()) { + const auto chunkSize = std::min(sizeLimit, quint32(frameBuffer.size() - offset)); + if (chunkSize + offset == frameBuffer.size()) + continuationFrame.addFlag(FrameFlag::END_HEADERS); + continuationFrame.setPayloadSize(chunkSize); + if (!continuationFrame.write(socket)) + return false; + written = socket.write(reinterpret_cast(&frameBuffer[offset]), + chunkSize); + if (written != qint64(chunkSize)) + return false; + + offset += chunkSize; + } + + return true; +} + +bool FrameWriter::writeDATA(QAbstractSocket &socket, quint32 sizeLimit, + const uchar *src, quint32 size) +{ + // With DATA frame(s) we always have: + // 1) frame's header (9 bytes) + // 2) a separate payload (from QNonContiguousByteDevice). + // We either fit within a sizeLimit, or split into several + // DATA frames. + + Q_ASSERT(src); + + if (sizeLimit > quint32(maxPayloadSize)) + sizeLimit = quint32(maxPayloadSize); + // We NEVER set END_STREAM, since QHttp2ProtocolHandler works with + // QNonContiguousByteDevice and this 'writeDATA' is probably + // not the last one for a given request. + // This has to be done externally (sending an empty DATA frame with END_STREAM). + for (quint32 offset = 0; offset != size;) { + const auto chunkSize = std::min(size - offset, sizeLimit); + setPayloadSize(chunkSize); + // Frame's header first: + if (!write(socket)) + return false; + // Payload (if any): + if (chunkSize) { + const auto written = socket.write(reinterpret_cast(src + offset), + chunkSize); + if (written != qint64(chunkSize)) + return false; + } + + offset += chunkSize; + } + + return true; +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/http2frames_p.h b/src/network/access/http2/http2frames_p.h new file mode 100644 index 0000000000..860555c180 --- /dev/null +++ b/src/network/access/http2/http2frames_p.h @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef HTTP2FRAMES_P_H +#define HTTP2FRAMES_P_H + +#include "http2protocol_p.h" +#include "hpack_p.h" + +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +class QHttp2ProtocolHandler; +class QAbstractSocket; + +namespace Http2 +{ + +class Q_AUTOTEST_EXPORT FrameReader +{ + friend class QT_PREPEND_NAMESPACE(QHttp2ProtocolHandler); + +public: + FrameReader() = default; + + FrameReader(const FrameReader &) = default; + FrameReader(FrameReader &&rhs); + + FrameReader &operator = (const FrameReader &) = default; + FrameReader &operator = (FrameReader &&rhs); + + FrameStatus read(QAbstractSocket &socket); + + bool padded(uchar *pad) const; + bool priority(quint32 *streamID, uchar *weight) const; + + // N of bytes without padding and/or priority + quint32 dataSize() const; + // Beginning of payload without priority/padding + // bytes. + const uchar *dataBegin() const; + + FrameType type = FrameType::LAST_FRAME_TYPE; + FrameFlags flags = FrameFlag::EMPTY; + quint32 streamID = 0; + quint32 payloadSize = 0; + +private: + bool readHeader(QAbstractSocket &socket); + bool readPayload(QAbstractSocket &socket); + + // As soon as we got a header, we + // know payload size, offset is + // needed if we do not have enough + // data and will read the next chunk. + bool incompleteRead = false; + quint32 offset = 0; + std::vector framePayload; +}; + +class Q_AUTOTEST_EXPORT FrameWriter +{ + friend class QT_PREPEND_NAMESPACE(QHttp2ProtocolHandler); + +public: + using payload_type = std::vector; + using size_type = payload_type::size_type; + + FrameWriter(); + FrameWriter(FrameType type, FrameFlags flags, quint32 streamID); + + void start(FrameType type, FrameFlags flags, quint32 streamID); + + void setPayloadSize(quint32 size); + quint32 payloadSize() const; + + void setType(FrameType type); + FrameType type() const; + + void setFlags(FrameFlags flags); + void addFlag(FrameFlag flag); + FrameFlags flags() const; + + quint32 streamID() const; + + // All append functions also update frame's payload + // length. + template + void append(ValueType val) + { + uchar wired[sizeof val] = {}; + qToBigEndian(val, wired); + append(wired, wired + sizeof val); + } + void append(uchar val); + void append(Settings identifier) + { + append(quint16(identifier)); + } + void append(const payload_type &payload) + { + append(&payload[0], &payload[0] + payload.size()); + } + + void append(const uchar *begin, const uchar *end); + + // Write 'frameBuffer' as a single frame: + bool write(QAbstractSocket &socket) const; + // Write as a single frame if we can, or write headers and + // CONTINUATION(s) frame(s). + bool writeHEADERS(QAbstractSocket &socket, quint32 sizeLimit); + // Write either a single DATA frame or several DATA frames + // depending on 'sizeLimit'. Data itself is 'external' to + // FrameWriter, since it's a 'readPointer' from QNonContiguousData. + bool writeDATA(QAbstractSocket &socket, quint32 sizeLimit, + const uchar *src, quint32 size); + + std::vector &rawFrameBuffer() + { + return frameBuffer; + } + +private: + void updatePayloadSize(); + std::vector frameBuffer; +}; + +} + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/http2/http2protocol.cpp b/src/network/access/http2/http2protocol.cpp new file mode 100644 index 0000000000..7f788a6f42 --- /dev/null +++ b/src/network/access/http2/http2protocol.cpp @@ -0,0 +1,156 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#include "http2protocol_p.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(QT_HTTP2, "qt.network.http2") + +namespace Http2 +{ + +// 3.5 HTTP/2 Connection Preface: +// "That is, the connection preface starts with the string +// PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n)." +const char Http2clientPreface[clientPrefaceLength] = + {0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, + 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, + 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, + 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a}; + + +void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error, + QString &errorMessage) +{ + if (errorCode > quint32(HTTP_1_1_REQUIRED)) { + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("RST_STREAM with unknown error code (%1)"); + errorMessage = errorMessage.arg(errorCode); + return; + } + + const Http2Error http2Error = Http2Error(errorCode); + + switch (http2Error) { + case HTTP2_NO_ERROR: + error = QNetworkReply::NoError; + errorMessage.clear(); + break; + case PROTOCOL_ERROR: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("HTTP/2 protocol error"); + break; + case INTERNAL_ERROR: + error = QNetworkReply::InternalServerError; + errorMessage = QLatin1String("Internal server error"); + break; + case FLOW_CONTROL_ERROR: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Flow control error"); + break; + case SETTINGS_TIMEOUT: + error = QNetworkReply::TimeoutError; + errorMessage = QLatin1String("SETTINGS ACK timeout error"); + break; + case STREAM_CLOSED: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Server received frame(s) on a half-closed stream"); + break; + case FRAME_SIZE_ERROR: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Server received a frame with an invalid size"); + break; + case REFUSE_STREAM: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Server refused a stream"); + break; + case CANCEL: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Stream is no longer needed"); + break; + case COMPRESSION_ERROR: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Server is unable to maintain the " + "header compression context for the connection"); + break; + case CONNECT_ERROR: + // TODO: in Qt6 we'll have to add more error codes in QNetworkReply. + error = QNetworkReply::UnknownNetworkError; + errorMessage = QLatin1String("The connection established in response " + "to a CONNECT request was reset or abnormally closed"); + break; + case ENHANCE_YOUR_CALM: + error = QNetworkReply::UnknownServerError; + errorMessage = QLatin1String("Server dislikes our behavior, excessive load detected."); + break; + case INADEQUATE_SECURITY: + error = QNetworkReply::ContentAccessDenied; + errorMessage = QLatin1String("The underlying transport has properties " + "that do not meet minimum security " + "requirements"); + break; + case HTTP_1_1_REQUIRED: + error = QNetworkReply::ProtocolFailure; + errorMessage = QLatin1String("Server requires that HTTP/1.1 " + "be used instead of HTTP/2."); + } +} + +QString qt_error_string(quint32 errorCode) +{ + QNetworkReply::NetworkError error = QNetworkReply::NoError; + QString message; + qt_error(errorCode, error, message); + return message; +} + +QNetworkReply::NetworkError qt_error(quint32 errorCode) +{ + QNetworkReply::NetworkError error = QNetworkReply::NoError; + QString message; + qt_error(errorCode, error, message); + return error; +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/http2protocol_p.h b/src/network/access/http2/http2protocol_p.h new file mode 100644 index 0000000000..c7088e2179 --- /dev/null +++ b/src/network/access/http2/http2protocol_p.h @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef HTTP2PROTOCOL_P_H +#define HTTP2PROTOCOL_P_H + +#include +#include +#include + +// Different HTTP/2 constants/values as defined by RFC 7540. + +QT_BEGIN_NAMESPACE + +class QString; + +namespace Http2 +{ + +enum class Settings : quint16 +{ + HEADER_TABLE_SIZE_ID = 0x1, + ENABLE_PUSH_ID = 0x2, + MAX_CONCURRENT_STREAMS_ID = 0x3, + INITIAL_WINDOW_SIZE_ID = 0x4, + MAX_FRAME_SIZE_ID = 0x5, + MAX_HEADER_LIST_SIZE_ID = 0x6 +}; + +enum class FrameType : uchar +{ + DATA = 0x0, + HEADERS = 0x1, + PRIORITY = 0x2, + RST_STREAM = 0x3, + SETTINGS = 0x4, + PUSH_PROMISE = 0x5, + PING = 0x6, + GOAWAY = 0x7, + WINDOW_UPDATE = 0x8, + CONTINUATION = 0x9, + // ATTENTION: enumerators must be sorted. + // We use LAST_FRAME_TYPE to check if + // frame type is known, if not - this frame + // must be ignored, HTTP/2 5.1). + LAST_FRAME_TYPE +}; + +enum class FrameFlag : uchar +{ + EMPTY = 0x0, // Valid for any frame type. + ACK = 0x1, // Valid for PING, SETTINGS + END_STREAM = 0x1, // Valid for HEADERS, DATA + END_HEADERS = 0x4, // Valid for PUSH_PROMISE, HEADERS, + PADDED = 0x8, // Valid for PUSH_PROMISE, HEADERS, DATA + PRIORITY = 0x20 // Valid for HEADERS, +}; + +Q_DECLARE_FLAGS(FrameFlags, FrameFlag) +Q_DECLARE_OPERATORS_FOR_FLAGS(FrameFlags) + +enum Http2PredefinedParameters +{ + // Old-style enum, so we + // can use as Http2::frameHeaderSize for example. + clientPrefaceLength = 24, // HTTP/2, 3.5 + connectionStreamID = 0, // HTTP/2, 5.1.1 + frameHeaderSize = 9, // HTTP/2, 4.1 + + // It's our max frame size we send in SETTINGS frame, + // it's also the default one and we also use it to later + // validate incoming frames: + maxFrameSize = 16384, // HTTP/2 6.5.2 + + defaultSessionWindowSize = 65535, // HTTP/2 6.5.2 + maxPayloadSize = (1 << 24) - 1, // HTTP/2 6.5.2 + // Using 1000 (rather arbitrarily), just to + // impose *some* upper limit: + maxPeerConcurrentStreams = 1000, + maxConcurrentStreams = 100 // HTTP/2, 6.5.2 +}; + +extern const Q_AUTOTEST_EXPORT char Http2clientPreface[clientPrefaceLength]; + +enum class FrameStatus +{ + protocolError, + sizeError, + incompleteFrame, + goodFrame +}; + +enum Http2Error +{ + // Old-style enum to avoid excessive name + // qualification ... + // NB: + // I use the last enumerator to check + // that errorCode (quint32) is valid, + // so it needs to be the highest-numbered! + // HTTP/2 7: + HTTP2_NO_ERROR = 0x0, + PROTOCOL_ERROR = 0x1, + INTERNAL_ERROR = 0x2, + FLOW_CONTROL_ERROR = 0x3, + SETTINGS_TIMEOUT = 0x4, + STREAM_CLOSED = 0x5, + FRAME_SIZE_ERROR = 0x6, + REFUSE_STREAM = 0x7, + CANCEL = 0x8, + COMPRESSION_ERROR = 0x9, + CONNECT_ERROR = 0xa, + ENHANCE_YOUR_CALM = 0xb, + INADEQUATE_SECURITY = 0xc, + HTTP_1_1_REQUIRED = 0xd +}; + +void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error, QString &errorString); +QString qt_error_string(quint32 errorCode); +QNetworkReply::NetworkError qt_error(quint32 errorCode); + +} + +Q_DECLARE_LOGGING_CATEGORY(QT_HTTP2) + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/http2/http2streams.cpp b/src/network/access/http2/http2streams.cpp new file mode 100644 index 0000000000..f57f8d8367 --- /dev/null +++ b/src/network/access/http2/http2streams.cpp @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "http2streams_p.h" + +#include "private/qhttp2protocolhandler_p.h" +#include "private/qhttpnetworkreply_p.h" + +#include + +QT_BEGIN_NAMESPACE + +namespace Http2 +{ + +Stream::Stream(const HttpMessagePair &message, quint32 id, qint32 sendSize, qint32 recvSize) + : httpPair(message), + streamID(id), + sendWindow(sendSize), + recvWindow(recvSize) +{ +} + +QHttpNetworkReply *Stream::reply() const +{ + return httpPair.second; +} + +const QHttpNetworkRequest &Stream::request() const +{ + return httpPair.first; +} + +QHttpNetworkRequest &Stream::request() +{ + return httpPair.first; +} + +QHttpNetworkRequest::Priority Stream::priority() const +{ + return httpPair.first.priority(); +} + +uchar Stream::weight() const +{ + switch (priority()) { + case QHttpNetworkRequest::LowPriority: + return 0; + case QHttpNetworkRequest::NormalPriority: + return 127; + case QHttpNetworkRequest::HighPriority: + default: + return 255; + } +} + +QNonContiguousByteDevice *Stream::data() const +{ + return httpPair.first.uploadByteDevice(); +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/http2streams_p.h b/src/network/access/http2/http2streams_p.h new file mode 100644 index 0000000000..7db5d21694 --- /dev/null +++ b/src/network/access/http2/http2streams_p.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef HTTP2STREAMS_P_H +#define HTTP2STREAMS_P_H + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +class QNonContiguousByteDevice; + +namespace Http2 +{ + +struct Q_AUTOTEST_EXPORT Stream +{ + enum StreamState { + idle, + open, + halfClosedLocal, + halfClosedRemote, + closed + }; + + Stream() = default; + Stream(const HttpMessagePair &message, quint32 streamID, qint32 sendSize, + qint32 recvSize); + + // TODO: check includes!!! + QHttpNetworkReply *reply() const; + const QHttpNetworkRequest &request() const; + QHttpNetworkRequest &request(); + QHttpNetworkRequest::Priority priority() const; + uchar weight() const; + + QNonContiguousByteDevice *data() const; + + HttpMessagePair httpPair; + quint32 streamID = 0; + // Signed as window sizes can become negative: + qint32 sendWindow = 65535; + qint32 recvWindow = 65535; + + StreamState state = idle; +}; + +} + +QT_END_NAMESPACE + +#endif + diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp new file mode 100644 index 0000000000..6a0aae9693 --- /dev/null +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -0,0 +1,1219 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#define NOMINMAX + +#include "qhttpnetworkconnection_p.h" +#include "qhttp2protocolhandler_p.h" + +#if !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +#include "http2/bitstreams_p.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +namespace +{ + +HPack::HttpHeader build_headers(const QHttpNetworkRequest &request, quint32 maxHeaderListSize) +{ + using namespace HPack; + + HttpHeader header; + header.reserve(300); + + // 1. Before anything - mandatory fields, if they do not fit into maxHeaderList - + // then stop immediately with error. + const auto auth = request.url().authority(QUrl::FullyEncoded | QUrl::RemoveUserInfo).toLatin1(); + header.push_back(HeaderField(":authority", auth)); + header.push_back(HeaderField(":method", request.methodName())); + header.push_back(HeaderField(":path", request.uri(false))); + header.push_back(HeaderField(":scheme", request.url().scheme().toLatin1())); + + HeaderSize size = header_size(header); + if (!size.first) // Ooops! + return HttpHeader(); + + if (size.second > maxHeaderListSize) + return HttpHeader(); // Bad, we cannot send this request ... + + for (const auto &field : request.header()) { + const HeaderSize delta = entry_size(field.first, field.second); + if (!delta.first) // Overflow??? + break; + if (std::numeric_limits::max() - delta.second < size.second) + break; + size.second += delta.second; + if (size.second > maxHeaderListSize) + break; + + QByteArray key(field.first.toLower()); + if (key == "connection" || key == "host" || key == "keep-alive" + || key == "proxy-connection" || key == "transfer-encoding") + continue; // Those headers are not valid (section 3.2.1) - from QSpdyProtocolHandler + // TODO: verify with specs, which fields are valid to send .... + // toLower - 8.1.2 .... "header field names MUST be converted to lowercase prior + // to their encoding in HTTP/2. + // A request or response containing uppercase header field names + // MUST be treated as malformed (Section 8.1.2.6)". + header.push_back(HeaderField(key, field.second)); + } + + return header; +} + +bool sum_will_overflow(qint32 windowSize, qint32 delta) +{ + if (windowSize > 0) + return std::numeric_limits::max() - windowSize < delta; + return std::numeric_limits::min() - windowSize > delta; +} + +}// Unnamed namespace + +// Since we anyway end up having this in every function definition: +using namespace Http2; + +const std::deque::size_type QHttp2ProtocolHandler::maxRecycledStreams = 10000; +const qint32 QHttp2ProtocolHandler::sessionMaxRecvWindowSize; +const qint32 QHttp2ProtocolHandler::streamInitialRecvWindowSize; +const quint32 QHttp2ProtocolHandler::maxAcceptableTableSize; + +QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *channel) + : QAbstractProtocolHandler(channel), + decoder(HPack::FieldLookupTable::DefaultSize), + encoder(HPack::FieldLookupTable::DefaultSize, true) +{ + continuedFrames.reserve(20); +} + +void QHttp2ProtocolHandler::_q_uploadDataReadyRead() +{ + auto data = qobject_cast(sender()); + Q_ASSERT(data); + const qint32 streamID = data->property("HTTP2StreamID").toInt(); + Q_ASSERT(activeStreams.contains(streamID)); + auto &stream = activeStreams[streamID]; + + if (!sendDATA(stream)) { + finishStreamWithError(stream, QNetworkReply::UnknownNetworkError, + QLatin1String("failed to send DATA")); + sendRST_STREAM(streamID, INTERNAL_ERROR); + markAsReset(streamID); + deleteActiveStream(streamID); + } +} + +void QHttp2ProtocolHandler::_q_replyDestroyed(QObject *reply) +{ + const quint32 streamID = reply->property("HTTP2StreamID").toInt(); + if (activeStreams.contains(streamID)) { + sendRST_STREAM(streamID, CANCEL); + markAsReset(streamID); + deleteActiveStream(streamID); + } +} + +void QHttp2ProtocolHandler::_q_readyRead() +{ + _q_receiveReply(); +} + +void QHttp2ProtocolHandler::_q_receiveReply() +{ + Q_ASSERT(m_socket); + Q_ASSERT(m_channel); + + const auto result = inboundFrame.read(*m_socket); + switch (result) { + case FrameStatus::incompleteFrame: + return; + case FrameStatus::protocolError: + return connectionError(PROTOCOL_ERROR, "invalid frame"); + case FrameStatus::sizeError: + return connectionError(FRAME_SIZE_ERROR, "invalid frame size"); + default: + break; + } + + Q_ASSERT(result == FrameStatus::goodFrame); + + if (continuationExpected && inboundFrame.type != FrameType::CONTINUATION) + return connectionError(PROTOCOL_ERROR, "CONTINUATION expected"); + + switch (inboundFrame.type) { + case FrameType::DATA: + handleDATA(); + break; + case FrameType::HEADERS: + handleHEADERS(); + break; + case FrameType::PRIORITY: + handlePRIORITY(); + break; + case FrameType::RST_STREAM: + handleRST_STREAM(); + break; + case FrameType::SETTINGS: + handleSETTINGS(); + break; + case FrameType::PUSH_PROMISE: + handlePUSH_PROMISE(); + break; + case FrameType::PING: + handlePING(); + break; + case FrameType::GOAWAY: + handleGOAWAY(); + break; + case FrameType::WINDOW_UPDATE: + handleWINDOW_UPDATE(); + break; + case FrameType::CONTINUATION: + handleCONTINUATION(); + break; + case FrameType::LAST_FRAME_TYPE: + // 5.1 - ignore unknown frames. + break; + } + + if (goingAway && !activeStreams.size()) + return; + + if (m_socket->bytesAvailable()) + QMetaObject::invokeMethod(m_channel, "_q_receiveReply", Qt::QueuedConnection); +} + +bool QHttp2ProtocolHandler::sendRequest() +{ + if (goingAway) + return false; + + if (!prefaceSent && !sendClientPreface()) + return false; + + auto &requests = m_channel->spdyRequestsToSend; + if (!requests.size()) + return true; + + const auto streamsToUse = std::min(maxConcurrentStreams - activeStreams.size(), + requests.size()); + auto it = requests.begin(); + m_channel->state = QHttpNetworkConnectionChannel::WritingState; + for (quint32 i = 0; i < streamsToUse; ++i) { + const qint32 newStreamID = createNewStream(*it); + if (!newStreamID) { + // TODO: actually we have to open a new connection. + qCCritical(QT_HTTP2, "sendRequest: out of stream IDs"); + break; + } + + it = requests.erase(it); + + Stream &newStream = activeStreams[newStreamID]; + if (!sendHEADERS(newStream)) { + finishStreamWithError(newStream, QNetworkReply::UnknownNetworkError, + QLatin1String("failed to send HEADERS frame(s)")); + deleteActiveStream(newStreamID); + continue; + } + + if (newStream.data() && !sendDATA(newStream)) { + finishStreamWithError(newStream, QNetworkReply::UnknownNetworkError, + QLatin1String("failed to send DATA frame(s)")); + sendRST_STREAM(newStreamID, INTERNAL_ERROR); + markAsReset(newStreamID); + deleteActiveStream(newStreamID); + } + } + + m_channel->state = QHttpNetworkConnectionChannel::IdleState; + + return true; +} + + +bool QHttp2ProtocolHandler::sendClientPreface() +{ + // 3.5 HTTP/2 Connection Preface + Q_ASSERT(m_socket); + + if (prefaceSent) + return true; + + const qint64 written = m_socket->write(Http2clientPreface, + Http2::clientPrefaceLength); + if (written != Http2::clientPrefaceLength) + return false; + + // 6.5 SETTINGS + outboundFrame.start(FrameType::SETTINGS, FrameFlag::EMPTY, Http2::connectionStreamID); + // MAX frame size (16 kb), disable PUSH + outboundFrame.append(Settings::MAX_FRAME_SIZE_ID); + outboundFrame.append(quint32(Http2::maxFrameSize)); + outboundFrame.append(Settings::ENABLE_PUSH_ID); + outboundFrame.append(quint32(0)); + + if (!outboundFrame.write(*m_socket)) + return false; + + sessionRecvWindowSize = sessionMaxRecvWindowSize; + if (defaultSessionWindowSize < sessionMaxRecvWindowSize) { + const auto delta = sessionMaxRecvWindowSize - defaultSessionWindowSize; + if (!sendWINDOW_UPDATE(connectionStreamID, delta)) + return false; + } + + prefaceSent = true; + waitingForSettingsACK = true; + + return true; +} + +bool QHttp2ProtocolHandler::sendSETTINGS_ACK() +{ + Q_ASSERT(m_socket); + + if (!prefaceSent && !sendClientPreface()) + return false; + + outboundFrame.start(FrameType::SETTINGS, FrameFlag::ACK, Http2::connectionStreamID); + + return outboundFrame.write(*m_socket); +} + +bool QHttp2ProtocolHandler::sendHEADERS(Stream &stream) +{ + using namespace HPack; + + outboundFrame.start(FrameType::HEADERS, FrameFlag::PRIORITY | FrameFlag::END_HEADERS, + stream.streamID); + + if (!stream.data()) { + outboundFrame.addFlag(FrameFlag::END_STREAM); + stream.state = Stream::halfClosedLocal; + } else { + stream.state = Stream::open; + } + + outboundFrame.append(quint32()); // No stream dependency in Qt. + outboundFrame.append(stream.weight()); + + const auto headers = build_headers(stream.request(), maxHeaderListSize); + if (!headers.size()) // nothing fits into maxHeaderListSize + return false; + + // Compress in-place: + BitOStream outputStream(outboundFrame.frameBuffer); + if (!encoder.encodeRequest(outputStream, headers)) + return false; + + return outboundFrame.writeHEADERS(*m_socket, maxFrameSize); +} + +bool QHttp2ProtocolHandler::sendDATA(Stream &stream) +{ + Q_ASSERT(maxFrameSize > frameHeaderSize); + Q_ASSERT(m_socket); + Q_ASSERT(stream.data()); + + const auto &request = stream.request(); + auto reply = stream.reply(); + Q_ASSERT(reply); + const auto replyPrivate = reply->d_func(); + Q_ASSERT(replyPrivate); + + auto slot = std::min(sessionSendWindowSize, stream.sendWindow); + while (!stream.data()->atEnd() && slot) { + qint64 chunkSize = 0; + const uchar *src = + reinterpret_cast(stream.data()->readPointer(slot, chunkSize)); + + if (chunkSize == -1) + return false; + + if (!src || !chunkSize) { + // Stream is not suspended by the flow control, + // we do not have data ready yet. + return true; + } + + outboundFrame.start(FrameType::DATA, FrameFlag::EMPTY, stream.streamID); + const qint32 bytesWritten = std::min(slot, chunkSize); + + if (!outboundFrame.writeDATA(*m_socket, maxFrameSize, src, bytesWritten)) + return false; + + stream.data()->advanceReadPointer(bytesWritten); + stream.sendWindow -= bytesWritten; + sessionSendWindowSize -= bytesWritten; + replyPrivate->totallyUploadedData += bytesWritten; + emit reply->dataSendProgress(replyPrivate->totallyUploadedData, + request.contentLength()); + slot = std::min(sessionSendWindowSize, stream.sendWindow); + } + + if (replyPrivate->totallyUploadedData == request.contentLength()) { + outboundFrame.start(FrameType::DATA, FrameFlag::END_STREAM, stream.streamID); + outboundFrame.setPayloadSize(0); + outboundFrame.write(*m_socket); + stream.state = Stream::halfClosedLocal; + stream.data()->disconnect(this); + removeFromSuspended(stream.streamID); + } else if (!stream.data()->atEnd()) { + addToSuspended(stream); + } + + return true; +} + +bool QHttp2ProtocolHandler::sendWINDOW_UPDATE(quint32 streamID, quint32 delta) +{ + Q_ASSERT(m_socket); + + outboundFrame.start(FrameType::WINDOW_UPDATE, FrameFlag::EMPTY, streamID); + outboundFrame.append(delta); + return outboundFrame.write(*m_socket); +} + +bool QHttp2ProtocolHandler::sendRST_STREAM(quint32 streamID, quint32 errorCode) +{ + Q_ASSERT(m_socket); + + outboundFrame.start(FrameType::RST_STREAM, FrameFlag::EMPTY, streamID); + outboundFrame.append(errorCode); + return outboundFrame.write(*m_socket); +} + +bool QHttp2ProtocolHandler::sendGOAWAY(quint32 errorCode) +{ + Q_ASSERT(m_socket); + + outboundFrame.start(FrameType::GOAWAY, FrameFlag::EMPTY, connectionStreamID); + outboundFrame.append(quint32(connectionStreamID)); + outboundFrame.append(errorCode); + return outboundFrame.write(*m_socket); +} + +void QHttp2ProtocolHandler::handleDATA() +{ + Q_ASSERT(inboundFrame.type == FrameType::DATA); + + const auto streamID = inboundFrame.streamID; + if (streamID == connectionStreamID) + return connectionError(PROTOCOL_ERROR, "DATA on stream 0x0"); + + if (!activeStreams.contains(streamID) && !streamWasReset(streamID)) + return connectionError(ENHANCE_YOUR_CALM, "DATA on invalid stream"); + + if (qint32(inboundFrame.payloadSize) > sessionRecvWindowSize) + return connectionError(FLOW_CONTROL_ERROR, "Flow control error"); + + sessionRecvWindowSize -= inboundFrame.payloadSize; + + if (activeStreams.contains(streamID)) { + auto &stream = activeStreams[streamID]; + + if (qint32(inboundFrame.payloadSize) > stream.recvWindow) { + finishStreamWithError(stream, QNetworkReply::ProtocolInvalidOperationError, + QLatin1String("flow control error")); + sendRST_STREAM(streamID, FLOW_CONTROL_ERROR); + markAsReset(streamID); + deleteActiveStream(streamID); + } else { + stream.recvWindow -= inboundFrame.payloadSize; + // Uncompress data if needed and append it ... + updateStream(stream, inboundFrame); + + if (inboundFrame.flags.testFlag(FrameFlag::END_STREAM)) { + finishStream(stream); + deleteActiveStream(stream.streamID); + } else if (stream.recvWindow < streamInitialRecvWindowSize / 2) { + QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection, + Q_ARG(quint32, stream.streamID), + Q_ARG(quint32, streamInitialRecvWindowSize - stream.recvWindow)); + stream.recvWindow = streamInitialRecvWindowSize; + } + } + } + + if (sessionRecvWindowSize < sessionMaxRecvWindowSize / 2) { + QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection, + Q_ARG(quint32, connectionStreamID), + Q_ARG(quint32, sessionMaxRecvWindowSize - sessionRecvWindowSize)); + sessionRecvWindowSize = sessionMaxRecvWindowSize; + } +} + +void QHttp2ProtocolHandler::handleHEADERS() +{ + Q_ASSERT(inboundFrame.type == FrameType::HEADERS); + + const auto streamID = inboundFrame.streamID; + if (streamID == connectionStreamID) + return connectionError(PROTOCOL_ERROR, "HEADERS on 0x0 stream"); + + if (!activeStreams.contains(streamID) && !streamWasReset(streamID)) + return connectionError(ENHANCE_YOUR_CALM, "HEADERS on invalid stream"); + + if (inboundFrame.flags.testFlag(FrameFlag::PRIORITY)) { + handlePRIORITY(); + if (goingAway) + return; + } + + const bool endHeaders = inboundFrame.flags.testFlag(FrameFlag::END_HEADERS); + continuedFrames.clear(); + continuedFrames.push_back(std::move(inboundFrame)); + if (!endHeaders) { + continuationExpected = true; + return; + } + + handleContinuedHEADERS(); +} + +void QHttp2ProtocolHandler::handlePRIORITY() +{ + Q_ASSERT(inboundFrame.type == FrameType::PRIORITY || + inboundFrame.type == FrameType::HEADERS); + + const auto streamID = inboundFrame.streamID; + if (streamID == connectionStreamID) + return connectionError(PROTOCOL_ERROR, "PIRORITY on 0x0 stream"); + + if (!activeStreams.contains(streamID) && !streamWasReset(streamID)) + return connectionError(ENHANCE_YOUR_CALM, "PRIORITY on invalid stream"); + + quint32 streamDependency = 0; + uchar weight = 0; + const bool noErr = inboundFrame.priority(&streamDependency, &weight); + Q_UNUSED(noErr) Q_ASSERT(noErr); + + + const bool exclusive = streamDependency & 0x80000000; + streamDependency &= ~0x80000000; + + // Ignore this for now ... + // Can be used for streams (re)prioritization - 5.3 + Q_UNUSED(exclusive); + Q_UNUSED(weight); +} + +void QHttp2ProtocolHandler::handleRST_STREAM() +{ + Q_ASSERT(inboundFrame.type == FrameType::RST_STREAM); + + // "RST_STREAM frames MUST be associated with a stream. + // If a RST_STREAM frame is received with a stream identifier of 0x0, + // the recipient MUST treat this as a connection error (Section 5.4.1) + // of type PROTOCOL_ERROR. + if (inboundFrame.streamID == connectionStreamID) + return connectionError(PROTOCOL_ERROR, "RST_STREAM on 0x0"); + + if (!(inboundFrame.streamID & 0x1)) { + // RST_STREAM on a promised stream: + // since we do not keep track of such streams, + // just ignore. + return; + } + + if (inboundFrame.streamID >= nextID) { + // "RST_STREAM frames MUST NOT be sent for a stream + // in the "idle" state. .. the recipient MUST treat this + // as a connection error (Section 5.4.1) of type PROTOCOL_ERROR." + return connectionError(PROTOCOL_ERROR, "RST_STREAM on idle stream"); + } + + if (!activeStreams.contains(inboundFrame.streamID)) { + // 'closed' stream, ignore. + return; + } + + Q_ASSERT(inboundFrame.dataSize() == 4); + + Stream &stream = activeStreams[inboundFrame.streamID]; + finishStreamWithError(stream, qFromBigEndian(inboundFrame.dataBegin())); + markAsReset(stream.streamID); + deleteActiveStream(stream.streamID); +} + +void QHttp2ProtocolHandler::handleSETTINGS() +{ + // 6.5 SETTINGS. + Q_ASSERT(inboundFrame.type == FrameType::SETTINGS); + + if (inboundFrame.streamID != connectionStreamID) + return connectionError(PROTOCOL_ERROR, "SETTINGS on invalid stream"); + + if (inboundFrame.flags.testFlag(FrameFlag::ACK)) { + if (!waitingForSettingsACK) + return connectionError(PROTOCOL_ERROR, "unexpected SETTINGS ACK"); + waitingForSettingsACK = false; + return; + } + + if (inboundFrame.dataSize()) { + auto src = inboundFrame.dataBegin(); + for (const uchar *end = src + inboundFrame.dataSize(); src != end; src += 6) { + const Settings identifier = Settings(qFromBigEndian(src)); + const quint32 intVal = qFromBigEndian(src + 2); + if (!acceptSetting(identifier, intVal)) { + // If not accepted - we finish with connectionError. + return; + } + } + } + + sendSETTINGS_ACK(); +} + + +void QHttp2ProtocolHandler::handlePUSH_PROMISE() +{ + // 6.6 PUSH_PROMISE. + Q_ASSERT(inboundFrame.type == FrameType::PUSH_PROMISE); + + if (prefaceSent && !waitingForSettingsACK) { + // This means, server ACKed our 'NO PUSH', + // but sent us PUSH_PROMISE anyway. + return connectionError(PROTOCOL_ERROR, "unexpected PUSH_PROMISE frame"); + } + + const auto streamID = inboundFrame.streamID; + if (streamID == connectionStreamID) { + return connectionError(PROTOCOL_ERROR, + "PUSH_PROMISE with invalid associated stream (0x0)"); + } + + if (!activeStreams.contains(streamID) && !streamWasReset(streamID)) { + return connectionError(ENHANCE_YOUR_CALM, + "PUSH_PROMISE with invalid associated stream"); + } + + const auto reservedID = qFromBigEndian(inboundFrame.dataBegin()); + if (!reservedID || (reservedID & 0x1)) { + return connectionError(PROTOCOL_ERROR, + "PUSH_PROMISE with invalid promised stream ID"); + } + + // "ignoring a PUSH_PROMISE frame causes the stream + // state to become indeterminate" - let's RST_STREAM it then ... + sendRST_STREAM(reservedID, REFUSE_STREAM); + markAsReset(reservedID); + + const bool endHeaders = inboundFrame.flags.testFlag(FrameFlag::END_HEADERS); + continuedFrames.clear(); + continuedFrames.push_back(std::move(inboundFrame)); + + if (!endHeaders) { + continuationExpected = true; + return; + } + + handleContinuedHEADERS(); +} + +void QHttp2ProtocolHandler::handlePING() +{ + // Since we're implementing a client and not + // a server, we only reply to a PING, ACKing it. + Q_ASSERT(inboundFrame.type == FrameType::PING); + Q_ASSERT(m_socket); + + if (inboundFrame.streamID != connectionStreamID) + return connectionError(PROTOCOL_ERROR, "PING on invalid stream"); + + if (inboundFrame.flags & FrameFlag::ACK) + return connectionError(PROTOCOL_ERROR, "unexpected PING ACK"); + + Q_ASSERT(inboundFrame.dataSize() == 8); + + outboundFrame.start(FrameType::PING, FrameFlag::ACK, connectionStreamID); + outboundFrame.append(inboundFrame.dataBegin(), inboundFrame.dataBegin() + 8); + outboundFrame.write(*m_socket); +} + +void QHttp2ProtocolHandler::handleGOAWAY() +{ + // 6.8 GOAWAY + + Q_ASSERT(inboundFrame.type == FrameType::GOAWAY); + // "An endpoint MUST treat a GOAWAY frame with a stream identifier + // other than 0x0 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR." + if (inboundFrame.streamID != connectionStreamID) + return connectionError(PROTOCOL_ERROR, "GOAWAY on invalid stream"); + + const auto src = inboundFrame.dataBegin(); + quint32 lastStreamID = qFromBigEndian(src); + const quint32 errorCode = qFromBigEndian(src + 4); + + if (!lastStreamID) { + // "The last stream identifier can be set to 0 if no + // streams were processed." + lastStreamID = 1; + } + + if (!(lastStreamID & 0x1)) { + // 5.1.1 - we (client) use only odd numbers as stream identifiers. + return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID"); + } + + if (lastStreamID >= nextID) { + // "A server that is attempting to gracefully shut down a connection SHOULD + // send an initial GOAWAY frame with the last stream identifier set to 2^31-1 + // and a NO_ERROR code." + if (lastStreamID != (quint32(1) << 31) - 1 || errorCode != HTTP2_NO_ERROR) + return connectionError(PROTOCOL_ERROR, "GOAWAY invalid stream/error code"); + lastStreamID = 1; + } else { + lastStreamID += 2; + } + + goingAway = true; + + QNetworkReply::NetworkError error = QNetworkReply::NoError; + QString message; + qt_error(errorCode, error, message); + + for (quint32 id = lastStreamID; id < nextID; id += 2) { + const auto it = activeStreams.find(id); + if (it != activeStreams.end()) { + Stream &stream = *it; + finishStreamWithError(stream, error, message); + markAsReset(id); + deleteActiveStream(id); + } else { + removeFromSuspended(id); + } + } + + if (!activeStreams.size()) + closeSession(); +} + +void QHttp2ProtocolHandler::handleWINDOW_UPDATE() +{ + Q_ASSERT(inboundFrame.type == FrameType::WINDOW_UPDATE); + + + const quint32 delta = qFromBigEndian(inboundFrame.dataBegin()); + const bool valid = delta && delta <= quint32(std::numeric_limits::max()); + const auto streamID = inboundFrame.streamID; + + if (streamID == Http2::connectionStreamID) { + if (!valid || sum_will_overflow(sessionSendWindowSize, delta)) + return connectionError(PROTOCOL_ERROR, "WINDOW_UPDATE invalid delta"); + sessionSendWindowSize += delta; + } else { + if (!activeStreams.contains(streamID)) { + // WINDOW_UPDATE on closed streams can be ignored. + return; + } + auto &stream = activeStreams[streamID]; + if (!valid || sum_will_overflow(stream.sendWindow, delta)) { + finishStreamWithError(stream, QNetworkReply::ProtocolInvalidOperationError, + QLatin1String("invalid WINDOW_UPDATE delta")); + sendRST_STREAM(streamID, PROTOCOL_ERROR); + markAsReset(streamID); + deleteActiveStream(streamID); + return; + } + stream.sendWindow += delta; + } + + // Since we're in _q_receiveReply at the moment, let's first handle other + // frames and resume suspended streams (if any) == start sending our own frame + // after handling these frames, since one them can be e.g. GOAWAY. + QMetaObject::invokeMethod(this, "resumeSuspendedStreams", Qt::QueuedConnection); +} + +void QHttp2ProtocolHandler::handleCONTINUATION() +{ + Q_ASSERT(inboundFrame.type == FrameType::CONTINUATION); + Q_ASSERT(continuedFrames.size()); // HEADERS frame must be already in. + + if (inboundFrame.streamID != continuedFrames.front().streamID) + return connectionError(PROTOCOL_ERROR, "CONTINUATION on invalid stream"); + + const bool endHeaders = inboundFrame.flags.testFlag(FrameFlag::END_HEADERS); + continuedFrames.push_back(std::move(inboundFrame)); + + if (!endHeaders) + return; + + continuationExpected = false; + handleContinuedHEADERS(); +} + +void QHttp2ProtocolHandler::handleContinuedHEADERS() +{ + Q_ASSERT(continuedFrames.size()); + + const auto streamID = continuedFrames[0].streamID; + + if (continuedFrames[0].type == FrameType::HEADERS) { + if (activeStreams.contains(streamID)) { + Stream &stream = activeStreams[streamID]; + if (stream.state != Stream::halfClosedLocal) { + // If we're receiving headers, they're a response to a request we sent; + // and we closed our end when we finished sending that. + finishStreamWithError(stream, QNetworkReply::ProtocolInvalidOperationError, + QLatin1String("HEADERS on invalid stream")); + sendRST_STREAM(streamID, CANCEL); + markAsReset(streamID); + deleteActiveStream(streamID); + return; + } + } else if (!streamWasReset(streamID)) { + return connectionError(PROTOCOL_ERROR, "HEADERS on invalid stream"); + } + } + + quint32 total = 0; + for (const auto &frame : continuedFrames) + total += frame.dataSize(); + + if (!total) { + // It could be a PRIORITY sent in HEADERS - handled by this point. + return; + } + + std::vector hpackBlock(total); + auto dst = hpackBlock.begin(); + for (const auto &frame : continuedFrames) { + if (!frame.dataSize()) + continue; + const uchar *src = frame.dataBegin(); + std::copy(src, src + frame.dataSize(), dst); + dst += frame.dataSize(); + } + + HPack::BitIStream inputStream{&hpackBlock[0], + &hpackBlock[0] + hpackBlock.size()}; + + if (!decoder.decodeHeaderFields(inputStream)) + return connectionError(COMPRESSION_ERROR, "HPACK decompression failed"); + + if (continuedFrames[0].type == FrameType::HEADERS) { + if (activeStreams.contains(streamID)) { + Stream &stream = activeStreams[streamID]; + updateStream(stream, decoder.decodedHeader()); + if (continuedFrames[0].flags & FrameFlag::END_STREAM) { + finishStream(stream); + deleteActiveStream(stream.streamID); + } + } + } +} + +bool QHttp2ProtocolHandler::acceptSetting(Http2::Settings identifier, quint32 newValue) +{ + if (identifier == Settings::HEADER_TABLE_SIZE_ID) { + if (newValue > maxAcceptableTableSize) { + connectionError(PROTOCOL_ERROR, "SETTINGS invalid table size"); + return false; + } + encoder.setMaxDynamicTableSize(newValue); + } + + if (identifier == Settings::INITIAL_WINDOW_SIZE_ID) { + // For every active stream - adjust its window + // (and handle possible overflows as errors). + if (newValue > quint32(std::numeric_limits::max())) { + connectionError(FLOW_CONTROL_ERROR, "SETTINGS invalid initial window size"); + return false; + } + + const qint32 delta = qint32(newValue) - streamInitialSendWindowSize; + streamInitialSendWindowSize = newValue; + + std::vector brokenStreams; + brokenStreams.reserve(activeStreams.size()); + for (auto &stream : activeStreams) { + if (sum_will_overflow(stream.sendWindow, delta)) { + brokenStreams.push_back(stream.streamID); + continue; + } + stream.sendWindow += delta; + } + + for (auto id : brokenStreams) { + auto &stream = activeStreams[id]; + finishStreamWithError(stream, QNetworkReply::ProtocolInvalidOperationError, + QLatin1String("SETTINGS window overflow")); + sendRST_STREAM(id, PROTOCOL_ERROR); + markAsReset(id); + deleteActiveStream(id); + } + + QMetaObject::invokeMethod(this, "resumeSuspendedStreams", Qt::QueuedConnection); + } + + if (identifier == Settings::MAX_CONCURRENT_STREAMS_ID) { + if (maxConcurrentStreams > maxPeerConcurrentStreams) { + connectionError(PROTOCOL_ERROR, "SETTINGS invalid number of concurrent streams"); + return false; + } + maxConcurrentStreams = newValue; + } + + if (identifier == Settings::MAX_FRAME_SIZE_ID) { + if (newValue < Http2::maxFrameSize || newValue > Http2::maxPayloadSize) { + connectionError(PROTOCOL_ERROR, "SETTGINGS max frame size is out of range"); + return false; + } + maxFrameSize = newValue; + } + + if (identifier == Settings::MAX_HEADER_LIST_SIZE_ID) { + // We just remember this value, it can later + // prevent us from sending any request (and this + // will end up in request/reply error). + maxHeaderListSize = newValue; + } + + return true; +} + +void QHttp2ProtocolHandler::updateStream(Stream &stream, const HPack::HttpHeader &headers) +{ + const auto httpReply = stream.reply(); + Q_ASSERT(httpReply); + const auto httpReplyPrivate = httpReply->d_func(); + for (const auto &pair : headers) { + const auto &name = pair.name; + auto value = pair.value; + + if (name == ":status") { + httpReply->setStatusCode(value.left(3).toInt()); + httpReplyPrivate->reasonPhrase = QString::fromLatin1(value.mid(4)); + } else if (name == ":version") { + httpReplyPrivate->majorVersion = value.at(5) - '0'; + httpReplyPrivate->minorVersion = value.at(7) - '0'; + } else if (name == "content-length") { + bool ok = false; + const qlonglong length = value.toLongLong(&ok); + if (ok) + httpReply->setContentLength(length); + } else { + QByteArray binder(", "); + if (name == "set-cookie") + binder = "\n"; + httpReply->setHeaderField(name, value.replace('\0', binder)); + } + } + + emit httpReply->headerChanged(); +} + +void QHttp2ProtocolHandler::updateStream(Stream &stream, const Http2::FrameReader &frame) +{ + Q_ASSERT(frame.type == FrameType::DATA); + + if (const auto length = frame.dataSize()) { + const char *data = reinterpret_cast(frame.dataBegin()); + auto &httpRequest = stream.request(); + auto httpReply = stream.reply(); + Q_ASSERT(httpReply); + auto replyPrivate = httpReply->d_func(); + + replyPrivate->compressedData.append(data, length); + replyPrivate->totalProgress += length; + + const QByteArray wrapped(data, length); + if (httpRequest.d->autoDecompress && replyPrivate->isCompressed()) { + QByteDataBuffer inDataBuffer; + inDataBuffer.append(wrapped); + replyPrivate->uncompressBodyData(&inDataBuffer, &replyPrivate->responseData); + } else { + replyPrivate->responseData.append(wrapped); + } + + if (replyPrivate->shouldEmitSignals()) { + emit httpReply->readyRead(); + emit httpReply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); + } + } +} + +void QHttp2ProtocolHandler::finishStream(Stream &stream) +{ + stream.state = Stream::closed; + auto httpReply = stream.reply(); + Q_ASSERT(httpReply); + httpReply->disconnect(this); + if (stream.data()) + stream.data()->disconnect(this); + + qCDebug(QT_HTTP2) << "stream" << stream.streamID << "closed"; + + emit httpReply->finished(); +} + +void QHttp2ProtocolHandler::finishStreamWithError(Stream &stream, quint32 errorCode) +{ + QNetworkReply::NetworkError error = QNetworkReply::NoError; + QString message; + qt_error(errorCode, error, message); + finishStreamWithError(stream, error, message); +} + +void QHttp2ProtocolHandler::finishStreamWithError(Stream &stream, QNetworkReply::NetworkError error, + const QString &message) +{ + stream.state = Stream::closed; + auto httpReply = stream.reply(); + Q_ASSERT(httpReply); + httpReply->disconnect(this); + if (stream.data()) + stream.data()->disconnect(this); + + qCWarning(QT_HTTP2) << "stream" << stream.streamID + << "finished with error:" << message; + + // TODO: error message must be translated!!! (tr) + emit httpReply->finishedWithError(error, message); +} + +quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message) +{ + const qint32 newStreamID = allocateStreamID(); + if (!newStreamID) + return 0; + + Q_ASSERT(!activeStreams.contains(newStreamID)); + + const auto reply = message.second; + const auto replyPrivate = reply->d_func(); + replyPrivate->connection = m_connection; + replyPrivate->connectionChannel = m_channel; + reply->setSpdyWasUsed(true); + reply->setProperty("HTTP2StreamID", newStreamID); + connect(reply, SIGNAL(destroyed(QObject*)), + this, SLOT(_q_replyDestroyed(QObject*))); + + const Stream newStream(message, newStreamID, + streamInitialSendWindowSize, + streamInitialRecvWindowSize); + + if (auto src = newStream.data()) { + connect(src, SIGNAL(readyRead()), this, + SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection); + src->setProperty("HTTP2StreamID", newStreamID); + } + + activeStreams.insert(newStreamID, newStream); + + return newStreamID; +} + +void QHttp2ProtocolHandler::addToSuspended(Stream &stream) +{ + qCDebug(QT_HTTP2) << "stream" << stream.streamID + << "suspended by flow control"; + const auto priority = stream.priority(); + Q_ASSERT(int(priority) >= 0 && int(priority) < 3); + suspendedStreams[priority].push_back(stream.streamID); +} + +void QHttp2ProtocolHandler::markAsReset(quint32 streamID) +{ + // For now, we trace only client's streams (created by us, + // odd integer numbers). + if (streamID & 0x1) { + qCDebug(QT_HTTP2) << "stream" << streamID << "was reset"; + // This part is quite tricky: I have to clear this set + // so that it does not become tOOO big. + if (recycledStreams.size() > maxRecycledStreams) { + // At least, I'm erasing the oldest first ... + recycledStreams.erase(recycledStreams.begin(), + recycledStreams.begin() + + recycledStreams.size() / 2); + } + + const auto it = std::lower_bound(recycledStreams.begin(), recycledStreams.end(), + streamID); + if (it != recycledStreams.end() && *it == streamID) + return; + + recycledStreams.insert(it, streamID); + } +} + +quint32 QHttp2ProtocolHandler::popStreamToResume() +{ + quint32 streamID = connectionStreamID; + const int nQ = sizeof suspendedStreams / sizeof suspendedStreams[0]; + using QNR = QHttpNetworkRequest; + const QNR::Priority ranks[nQ] = {QNR::HighPriority, + QNR::NormalPriority, + QNR::LowPriority}; + + for (int i = 0; i < nQ; ++i) { + auto &queue = suspendedStreams[ranks[i]]; + auto it = queue.begin(); + for (; it != queue.end(); ++it) { + if (!activeStreams.contains(*it)) + continue; + if (activeStreams[*it].sendWindow > 0) + break; + } + + if (it != queue.end()) { + streamID = *it; + queue.erase(it); + break; + } + } + + return streamID; +} + +void QHttp2ProtocolHandler::removeFromSuspended(quint32 streamID) +{ + const int nQ = sizeof suspendedStreams / sizeof suspendedStreams[0]; + for (int i = 0; i < nQ; ++i) { + auto &q = suspendedStreams[i]; + q.erase(std::remove(q.begin(), q.end(), streamID), q.end()); + } +} + +void QHttp2ProtocolHandler::deleteActiveStream(quint32 streamID) +{ + if (activeStreams.contains(streamID)) { + auto &stream = activeStreams[streamID]; + if (stream.reply()) + stream.reply()->disconnect(this); + if (stream.data()) + stream.data()->disconnect(this); + activeStreams.remove(streamID); + } + + removeFromSuspended(streamID); + if (m_channel->spdyRequestsToSend.size()) + QMetaObject::invokeMethod(this, "sendRequest", Qt::QueuedConnection); +} + +bool QHttp2ProtocolHandler::streamWasReset(quint32 streamID) const +{ + const auto it = std::lower_bound(recycledStreams.begin(), + recycledStreams.end(), + streamID); + return it != recycledStreams.end() && *it == streamID; +} + +void QHttp2ProtocolHandler::resumeSuspendedStreams() +{ + while (sessionSendWindowSize > 0) { + const auto streamID = popStreamToResume(); + if (!streamID) + return; + + if (!activeStreams.contains(streamID)) + continue; + + Stream &stream = activeStreams[streamID]; + if (!sendDATA(stream)) { + finishStreamWithError(stream, QNetworkReply::UnknownNetworkError, + QLatin1String("failed to send DATA")); + sendRST_STREAM(streamID, INTERNAL_ERROR); + markAsReset(streamID); + deleteActiveStream(streamID); + } + } +} + +quint32 QHttp2ProtocolHandler::allocateStreamID() +{ + // With protocol upgrade streamID == 1 will become + // invalid. The logic must be updated. + if (nextID > quint32(std::numeric_limits::max())) + return 0; + + const quint32 streamID = nextID; + nextID += 2; + + return streamID; +} + +void QHttp2ProtocolHandler::connectionError(Http2::Http2Error errorCode, + const char *message) +{ + Q_ASSERT(message); + Q_ASSERT(!goingAway); + + qCCritical(QT_HTTP2) << "connection error:" << message; + + goingAway = true; + sendGOAWAY(errorCode); + const auto error = qt_error(errorCode); + m_channel->emitFinishedWithError(error, message); + + for (auto &stream: activeStreams) + finishStreamWithError(stream, error, QLatin1String(message)); + + closeSession(); +} + +void QHttp2ProtocolHandler::closeSession() +{ + activeStreams.clear(); + for (auto &q: suspendedStreams) + q.clear(); + recycledStreams.clear(); + + m_channel->close(); +} + +#endif // !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +QT_END_NAMESPACE diff --git a/src/network/access/qhttp2protocolhandler_p.h b/src/network/access/qhttp2protocolhandler_p.h new file mode 100644 index 0000000000..b9658982c0 --- /dev/null +++ b/src/network/access/qhttp2protocolhandler_p.h @@ -0,0 +1,196 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QHTTP2PROTOCOLHANDLER_P_H +#define QHTTP2PROTOCOLHANDLER_P_H + +#include +#include +#include + +#if !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +#include "http2/http2protocol_p.h" +#include "http2/http2streams_p.h" +#include "http2/http2frames_p.h" +#include "http2/hpacktable_p.h" +#include "http2/hpack_p.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QHttp2ProtocolHandler : public QObject, public QAbstractProtocolHandler +{ + Q_OBJECT + +public: + QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *channel); + + QHttp2ProtocolHandler(const QHttp2ProtocolHandler &rhs) = delete; + QHttp2ProtocolHandler(QHttp2ProtocolHandler &&rhs) = delete; + + QHttp2ProtocolHandler &operator = (const QHttp2ProtocolHandler &rhs) = delete; + QHttp2ProtocolHandler &operator = (QHttp2ProtocolHandler &&rhs) = delete; + +private slots: + void _q_uploadDataReadyRead(); + void _q_replyDestroyed(QObject* reply); + +private: + using Stream = Http2::Stream; + + void _q_readyRead() override; + void _q_receiveReply() override; + Q_INVOKABLE bool sendRequest() override; + + bool sendClientPreface(); + bool sendSETTINGS_ACK(); + bool sendHEADERS(Stream &stream); + bool sendDATA(Stream &stream); + Q_INVOKABLE bool sendWINDOW_UPDATE(quint32 streamID, quint32 delta); + bool sendRST_STREAM(quint32 streamID, quint32 errorCoder); + bool sendGOAWAY(quint32 errorCode); + + void handleDATA(); + void handleHEADERS(); + void handlePRIORITY(); + void handleRST_STREAM(); + void handleSETTINGS(); + void handlePUSH_PROMISE(); + void handlePING(); + void handleGOAWAY(); + void handleWINDOW_UPDATE(); + void handleCONTINUATION(); + + void handleContinuedHEADERS(); + + bool acceptSetting(Http2::Settings identifier, quint32 newValue); + + void updateStream(Stream &stream, const HPack::HttpHeader &headers); + void updateStream(Stream &stream, const Http2::FrameReader &dataFrame); + void finishStream(Stream &stream); + // Error code send by a peer (GOAWAY/RST_STREAM): + void finishStreamWithError(Stream &stream, quint32 errorCode); + // Locally encountered error: + void finishStreamWithError(Stream &stream, QNetworkReply::NetworkError error, + const QString &message); + + // Stream's lifecycle management: + quint32 createNewStream(const HttpMessagePair &message); + void addToSuspended(Stream &stream); + void markAsReset(quint32 streamID); + quint32 popStreamToResume(); + void removeFromSuspended(quint32 streamID); + void deleteActiveStream(quint32 streamID); + bool streamWasReset(quint32 streamID) const; + + bool prefaceSent = false; + // In the current implementation we send + // SETTINGS only once, immediately after + // the client's preface 24-byte message. + bool waitingForSettingsACK = false; + + static const quint32 maxAcceptableTableSize = 16 * HPack::FieldLookupTable::DefaultSize; + // HTTP/2 4.3: Header compression is stateful. One compression context and + // one decompression context are used for the entire connection. + HPack::Decoder decoder; + HPack::Encoder encoder; + + QHash activeStreams; + std::deque suspendedStreams[3]; // 3 for priorities: High, Normal, Low. + static const std::deque::size_type maxRecycledStreams; + std::deque recycledStreams; + + // Peer's max frame size. + quint32 maxFrameSize = Http2::maxFrameSize; + + Http2::FrameReader inboundFrame; + Http2::FrameWriter outboundFrame; + // Temporary storage to assemble HEADERS' block + // from several CONTINUATION frames ... + bool continuationExpected = false; + std::vector continuedFrames; + + // Peer's max number of streams ... + quint32 maxConcurrentStreams = Http2::maxConcurrentStreams; + + // Control flow: + static const qint32 sessionMaxRecvWindowSize = Http2::defaultSessionWindowSize * 10; + // Signed integer, it can become negative (it's still a valid window size): + qint32 sessionRecvWindowSize = sessionMaxRecvWindowSize; + + // We do not negotiate this window size + // We have to send WINDOW_UPDATE frames to our peer also. + static const qint32 streamInitialRecvWindowSize = Http2::defaultSessionWindowSize; + + // Updated by SETTINGS and WINDOW_UPDATE. + qint32 sessionSendWindowSize = Http2::defaultSessionWindowSize; + qint32 streamInitialSendWindowSize = Http2::defaultSessionWindowSize; + + // It's unlimited by default, but can be changed via SETTINGS. + quint32 maxHeaderListSize = (std::numeric_limits::max)(); + + Q_INVOKABLE void resumeSuspendedStreams(); + // Our stream IDs (all odd), the first valid will be 1. + quint32 nextID = 1; + quint32 allocateStreamID(); + bool validPeerStreamID() const; + bool goingAway = false; + + // Errors: + void connectionError(Http2::Http2Error errorCode, + const char *message); + void closeSession(); +}; + +QT_END_NAMESPACE + +#endif // !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +#endif diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 79f418f675..e62d662b52 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -82,7 +82,8 @@ QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(const QString &host networkLayerState(Unknown), hostName(hostName), port(port), encrypt(encrypt), delayIpv4(true) #ifndef QT_NO_SSL -, channelCount((type == QHttpNetworkConnection::ConnectionTypeSPDY) ? 1 : defaultHttpChannelCount) +, channelCount((type == QHttpNetworkConnection::ConnectionTypeSPDY || type == QHttpNetworkConnection::ConnectionTypeHTTP2) + ? 1 : defaultHttpChannelCount) #else , channelCount(defaultHttpChannelCount) #endif // QT_NO_SSL @@ -1011,7 +1012,8 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() } break; } - case QHttpNetworkConnection::ConnectionTypeSPDY: { + case QHttpNetworkConnection::ConnectionTypeSPDY: + case QHttpNetworkConnection::ConnectionTypeHTTP2: { #ifndef QT_NO_SSL if (channels[0].spdyRequestsToSend.isEmpty()) return; diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index fefd5af281..4d1bdb74c4 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -99,7 +99,8 @@ public: enum ConnectionType { ConnectionTypeHTTP, - ConnectionTypeSPDY + ConnectionTypeSPDY, + ConnectionTypeHTTP2 }; #ifndef QT_NO_BEARERMANAGEMENT @@ -161,6 +162,7 @@ private: friend class QHttpNetworkReply; friend class QHttpNetworkReplyPrivate; friend class QHttpNetworkConnectionChannel; + friend class QHttp2ProtocolHandler; friend class QHttpProtocolHandler; friend class QSpdyProtocolHandler; diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 2f7be01078..791a7495c0 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -47,6 +47,7 @@ #ifndef QT_NO_HTTP +#include #include #include @@ -971,7 +972,8 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket } while (!connection->d_func()->highPriorityQueue.isEmpty() || !connection->d_func()->lowPriorityQueue.isEmpty()); #ifndef QT_NO_SSL - if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY || + connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2) { QList spdyPairs = spdyRequestsToSend.values(); for (int a = 0; a < spdyPairs.count(); ++a) { // emit error for all replies @@ -1003,7 +1005,8 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket void QHttpNetworkConnectionChannel::_q_proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator* auth) { #ifndef QT_NO_SSL - if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY || + connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2) { connection->d_func()->emitProxyAuthenticationRequired(this, proxy, auth); } else { // HTTP #endif // QT_NO_SSL @@ -1043,6 +1046,10 @@ void QHttpNetworkConnectionChannel::_q_encrypted() // no need to re-queue requests, if SPDY was enabled on the request it // has gone to the SPDY queue already break; + } else if (nextProtocol == QSslConfiguration::ALPNProtocolHTTP2) { + protocolHandler.reset(new QHttp2ProtocolHandler(this)); + connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP2); + break; } else { emitFinishedWithError(QNetworkReply::SslHandshakeFailedError, "detected unknown Next Protocol Negotiation protocol"); @@ -1066,7 +1073,8 @@ void QHttpNetworkConnectionChannel::_q_encrypted() state = QHttpNetworkConnectionChannel::IdleState; pendingEncrypt = false; - if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY || + connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2) { // we call setSpdyWasUsed(true) on the replies in the SPDY handler when the request is sent if (spdyRequestsToSend.count() > 0) // wait for data from the server first (e.g. initial window, max concurrent requests) diff --git a/src/network/access/qhttpnetworkreply_p.h b/src/network/access/qhttpnetworkreply_p.h index dea13834ed..f3b007f594 100644 --- a/src/network/access/qhttpnetworkreply_p.h +++ b/src/network/access/qhttpnetworkreply_p.h @@ -180,6 +180,7 @@ private: friend class QHttpNetworkConnection; friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; + friend class QHttp2ProtocolHandler; friend class QHttpProtocolHandler; friend class QSpdyProtocolHandler; }; diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 5d51e7fb1a..802043d847 100644 --- a/src/network/access/qhttpnetworkrequest.cpp +++ b/src/network/access/qhttpnetworkrequest.cpp @@ -47,7 +47,7 @@ QT_BEGIN_NAMESPACE QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(QHttpNetworkRequest::Operation op, QHttpNetworkRequest::Priority pri, const QUrl &newUrl) : QHttpNetworkHeaderPrivate(newUrl), operation(op), priority(pri), uploadByteDevice(0), - autoDecompress(false), pipeliningAllowed(false), spdyAllowed(false), + autoDecompress(false), pipeliningAllowed(false), spdyAllowed(false), http2Allowed(false), withCredentials(true), preConnect(false), followRedirect(false), redirectCount(0) { } @@ -61,6 +61,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest autoDecompress(other.autoDecompress), pipeliningAllowed(other.pipeliningAllowed), spdyAllowed(other.spdyAllowed), + http2Allowed(other.http2Allowed), withCredentials(other.withCredentials), ssl(other.ssl), preConnect(other.preConnect), @@ -82,6 +83,7 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot && (autoDecompress == other.autoDecompress) && (pipeliningAllowed == other.pipeliningAllowed) && (spdyAllowed == other.spdyAllowed) + && (http2Allowed == other.http2Allowed) // we do not clear the customVerb in setOperation && (operation != QHttpNetworkRequest::Custom || (customVerb == other.customVerb)) && (withCredentials == other.withCredentials) @@ -331,6 +333,16 @@ void QHttpNetworkRequest::setSPDYAllowed(bool b) d->spdyAllowed = b; } +bool QHttpNetworkRequest::isHTTP2Allowed() const +{ + return d->http2Allowed; +} + +void QHttpNetworkRequest::setHTTP2Allowed(bool b) +{ + d->http2Allowed = b; +} + bool QHttpNetworkRequest::withCredentials() const { return d->withCredentials; diff --git a/src/network/access/qhttpnetworkrequest_p.h b/src/network/access/qhttpnetworkrequest_p.h index d85f8ce58f..d1abb76e28 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -117,6 +117,9 @@ public: bool isSPDYAllowed() const; void setSPDYAllowed(bool b); + bool isHTTP2Allowed() const; + void setHTTP2Allowed(bool b); + bool withCredentials() const; void setWithCredentials(bool b); @@ -144,6 +147,7 @@ private: friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; friend class QHttpProtocolHandler; + friend class QHttp2ProtocolHandler; friend class QSpdyProtocolHandler; }; @@ -165,6 +169,7 @@ public: bool autoDecompress; bool pipeliningAllowed; bool spdyAllowed; + bool http2Allowed; bool withCredentials; bool ssl; bool preConnect; diff --git a/src/network/access/qhttpthreaddelegate.cpp b/src/network/access/qhttpthreaddelegate.cpp index 3adf71ccfb..e16519c2f2 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -287,7 +287,13 @@ void QHttpThreadDelegate::startRequest() QHttpNetworkConnection::ConnectionType connectionType = QHttpNetworkConnection::ConnectionTypeHTTP; #ifndef QT_NO_SSL - if (httpRequest.isSPDYAllowed() && ssl) { + if (httpRequest.isHTTP2Allowed() && ssl) { + connectionType = QHttpNetworkConnection::ConnectionTypeHTTP2; + QList protocols; + protocols << QSslConfiguration::ALPNProtocolHTTP2 + << QSslConfiguration::NextProtocolHttp1_1; + incomingSslConfiguration.setAllowedNextProtocols(protocols); + } else if (httpRequest.isSPDYAllowed() && ssl) { connectionType = QHttpNetworkConnection::ConnectionTypeSPDY; urlCopy.setScheme(QStringLiteral("spdy")); // to differentiate SPDY requests from HTTPS requests QList nextProtocols; diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index d98a1a1d62..b5e44fa29a 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -738,6 +738,9 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq if (request.attribute(QNetworkRequest::SpdyAllowedAttribute).toBool()) httpRequest.setSPDYAllowed(true); + if (request.attribute(QNetworkRequest::HTTP2AllowedAttribute).toBool()) + httpRequest.setHTTP2Allowed(true); + if (static_cast (newHttpRequest.attribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual) diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index f284c11cd4..00375f285c 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -87,6 +87,8 @@ public: BackgroundRequestAttribute, SpdyAllowedAttribute, SpdyWasUsedAttribute, + HTTP2AllowedAttribute, + HTTP2WasUsedAttribute, EmitAllUploadProgressSignalsAttribute, FollowRedirectsAttribute, diff --git a/src/network/ssl/qsslconfiguration.cpp b/src/network/ssl/qsslconfiguration.cpp index bc8394afdc..75a880f115 100644 --- a/src/network/ssl/qsslconfiguration.cpp +++ b/src/network/ssl/qsslconfiguration.cpp @@ -53,6 +53,7 @@ const QSsl::SslOptions QSslConfigurationPrivate::defaultSslOptions = QSsl::SslOp |QSsl::SslOptionDisableCompression |QSsl::SslOptionDisableSessionPersistence; +const char QSslConfiguration::ALPNProtocolHTTP2[] = "h2"; const char QSslConfiguration::NextProtocolSpdy3_0[] = "spdy/3"; const char QSslConfiguration::NextProtocolHttp1_1[] = "http/1.1"; diff --git a/src/network/ssl/qsslconfiguration.h b/src/network/ssl/qsslconfiguration.h index b637c4f9c7..1c57bebd65 100644 --- a/src/network/ssl/qsslconfiguration.h +++ b/src/network/ssl/qsslconfiguration.h @@ -168,6 +168,7 @@ public: QByteArray nextNegotiatedProtocol() const; NextProtocolNegotiationStatus nextProtocolNegotiationStatus() const; + static const char ALPNProtocolHTTP2[]; static const char NextProtocolSpdy3_0[]; static const char NextProtocolHttp1_1[]; -- cgit v1.2.3